Syntax in C

Subpage of C Deep Dive

A close look at the C language.

Here are the key syntax pointers about the language:

  • Every line ends with a semicolon
  • Comments can be written two ways: /* ... */ for block comments and // ... for end-of-line comments.
  • Keywords such as int, if, return, and struct are reserved.
  • Identifiers start with a letter or underscore and may contain letters, digits, and underscores.
  • \n is an escape sequence that adds a newline to a string. \t is an escape sequence that adds a tab of spaces to a string.

C’s syntax is compact so many semantic details (type checking, lifetime, side effects) are conveyed by small tokens and their combination into declarations and expressions.

The main function

All C code runs inside functions. The most important function you will find in any C program is called the main() function. The main() function is the starting point for all of the code in your program.

Unlike the main() function in languages like Java, the main() function in C has a return type of int. This is because when the computer runs the program, it will need to have some way of deciding if the program ran successfully or not.

It does this by checking the return value of the main() function.

If you tell your main() function to return 0, this means that the program was successful.

If you tell it to return any other value, this means that there was a problem.

Thus, all C programs will feature:

int main() {}

If you want to check the exit status of a program, type:

echo %ErrorLeveddtivel%

in Windows, or:

echo $?

in Linux or on the Mac.

The main() function has an int return type, so you should include a return statement when you get to the end. But if you leave the return statement out, the code will still compile—though you may get a warning from the compiler. This is because the compiler will automatically insert the return statement.

Command Line Arguments

In C and C++, the main function can be defined in two common ways:

int main(void)              // No arguments
int main(int argc, char *argv[])   // With command line arguments
  • argc (argument count)
    • An integer that tells you how many arguments were passed to the program.
    • Always at least 1, because the program’s name itself counts as the first argument.
  • argv (argument vector)
    • An array of strings (character pointers).
    • argv[0] → the program name.
    • argv[1] → the first argument.
    • argv[2] → the second argument, and so on.
    • argv[argc] → always NULL (marks the end).

Here’s a simple C program that prints all arguments passed to it:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Number of arguments: %d\n", argc);

    for (int i = 0; i < argc; i++) {
        printf("Argument %d: %s\n", i, argv[i]);
    }

    return 0;
}

Here is how it can be used:

  1. Compile the program:
    gcc args.c -o args
    
  2. Run with arguments:
    ./args hello world 123
    
  3. Output:
    Number of arguments: 4
    Argument 0: ./args
    Argument 1: hello
    Argument 2: world
    Argument 3: 123
    

Here is an example of a script that takes in 2 numbers from the command line and adds them up:

#include <stdio.h>
#include <stdlib.h>  // for atoi()

int main(int argc, char *argv[]) {
    if (argc < 3) {
        printf("Usage: %s num1 num2\n", argv[0]);
        return 1;
    }

    int num1 = atoi(argv[1]);
    int num2 = atoi(argv[2]);
    printf("Sum: %d\n", num1 + num2);

    return 0;
}

Run it like:

./sum 10 20

Output:

Sum: 30

Pre-processor directives

Preprocessor directives are commands in source code (like C/C++) that start with #, telling a preprocessor program to modify the code before it's actually compiled, by performing:

  • text substitutions
  • macro expansions
  • including other files
  • conditionally compiling sections
  • defining constants
  • enabling different features.

Key examples include #include for file inclusion, #define for macros (constants/functions), and #ifdef/#ifndef for conditional compilation, all acting as powerful text manipulation tools for the compiler. 

#define directive

The #define directive causes the compiler to substitute token-string for each occurrence of identifier in the source file. It does what is also called a macro expansion.

The identifier is replaced only when it forms a token. That is, identifier is not replaced if it appears in a comment, in a string, or as part of a longer identifier. 

The preprocessor treats /* comments */ and string literals (e.g., "myIdentifier") as opaque, ignoring any matches inside them.

As a result, the #define directive acts as a way to define constants for a program.

#define without a token-string removes occurrences of identifier from the source file. The identifier remains defined and can be tested by using the #if defined and #ifdef directives.

This example illustrates the #define directive:

#define WIDTH       80
#define LENGTH      ( WIDTH + 10 )
var = LENGTH * 20;

After the preprocessing stage the statement becomes:

var = ( 80 + 10 ) * 20;

which evaluates to 1800. Note that if the #define directive did not include paranthesis, the result will be:

var = 80 + 10 * 20;

Also note that you do not need to put a semicolon at the end of the #define directive.

You can also define macros whose use looks like a function call. These are called function-like macros. To define a function-like macro, you use the same ‘#define’ directive, but you put a pair of parentheses immediately after the macro name. For example,

#define SQUARE(x) ((x) * (x))

expands SQUARE(5) into ((5) * (5)).

Unlike functions, macros do not involve stack frame creation or return jumps, making them potentially faster for extremely small operations.

You should always wrap both the entire macro and each individual parameter in parentheses to prevent operator precedence issues. For example, the following definition fails:

#define MULTIPLY(a, b) a * b

int result = MULTIPLY(2 + 3, 3 + 5) // Does not result in 40, instead it is 2 + 3 * 3 + 5

#include directive

C is a very, very small language and it can do almost nothing without the use of external libraries. You will need to tell the compiler what external code to use by including header files for the relevant libraries.

You can organize constant and macro definitions into include files (also known as header files) and then use #include directives to add them to any source file.

In C programming, a header file (ending in .h) is a file containing declarations of functions, macros, data types, and constants that are shared across multiple source files.

Include files are also useful for incorporating declarations of external variables and complex data types. The types may be defined and named only once in an include file created for that purpose.

For user defined header file, use syntax that looks something like: #include "myHeader.h". For file from the standard library use syntax #include <header-file-name.h> that signals to the compiler that it needs to search from the system search path.

Here are the important headers that you need to know:

stdio.h

The stdio library contains code that allows you to read and write data from and to the terminal.

Here are the most commonly used functions declared in stdio.h:

  • scanf(): For formatted input from the standard input (usually the keyboard). Here is how to use scanf in practise:
    #include <stdio.h>
    
    int main() {
        int num;
        float fnum;
        
        printf("Enter an integer and a floating-point number: ");
        scanf("%d %f", &num, &fnum);
        
        printf("You entered %d and %f\n", num, fnum);
        
        return 0;
    }

    The first argument to scanf is the format string which contains format specifiers that indicate the type of input expected. Some common format specifiers for scanf include:

    • %d: for signed integers (decimal)
    • %f: for single-precision floating-point numbers (float)
    • %lf: for double-precision floating-point numbers (double)
    • %li: for long integer that can interpret numbers in different bases (decimal, octal, hexadecimal) based on prefixes (e.g., 0 for octal, 0x for hex)
    • %c: for a single character
    • %s: for a string (sequence of characters)

      This is not recommended to be used with scanf. See Segmentation Faults below.

    • %p: for a pointer/address in memory

    In the above example, the format string "%d %f" tells scanf() to read an integer value followed by a floating-point value, separated by a space.

    If we want to read up to a certain number of characters, we can specify this in the format specifier. For example, if we want to read up to 19 characters, then we write %19s.

    The second argument is a variable-length argument list that contains the memory addresses (pointers) of variables where the input values will be stored.

    These memory addresses must be passed as pointers. This is why the & operator is used.

    Here, the & operator is used to pass the address of the num and fnum variables to scanf(), so that the input values can be stored in those variables.

    The scanf() function returns the number of items successfully read, or EOF if an error occurs or the end of the input stream is reached. By checking this, we can do input validation as follows:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int num;
        printf("Enter an integer: ");
        if (scanf("%d", &num) != 1) {
            printf("Error: Invalid input\n");
            exit(1);
        }
        return 0;
    }

    Be careful with scanf. Using scanf reads exactly one character from the input buffer. So after you type something and press Enter, the value is consumed, but the newline character (\n) from pressing Enter is left in the buffer. The next time you use scanf, it immediately reads that leftover newline instead of waiting for new input.

    To fix this, insert a leading space to skip any whitespace characters (spaces, tabs, newlines) before reading the actual character.

    scanf(" %c", &operation);

    Segmentation Faults

    Good practise dictates that we carefully put a limit on the number of characters that the scanf function reads (eg: %39s).

    However, it is possible to just use %s. This however can be extremely buggy. The reason is scanf can write beyond the end of the space that is allocated to the string we are writing into.

    So in the following code:

    char food[5];
    printf("Enter favorite food: "); // Input: Tuuffle-Fries
    scanf("%s", food);
    printf("Favorite food: %s\n", food);

    the food array can only store 5 characters but the user can enter far more. The extra data then gets written into memory that has not been properly allocated by the computer.

    Now, you might get lucky and the data will simply be stored and not cause any problems. But it’s very likely that buffer overflows will cause bugs. It might be called a segmentation fault or an abort trap, but whatever the error message that appears, the result will be a crash.

  • fgets(): To read content from the file up to the next line break.

    fgets is the recommended way to read a line of text from a specified input stream into the string buffer. It is an improved version of the gets() function which is not to be used at all.

    Just like the scanf() function, it takes a char pointer, but unlike the scanf() function, the fgets() function must be given a maximum length. Here is the function prototype:

    char *fgets(char *str, int n, FILE *stream);

    The n parameter prevents buffer overflows which is the major security vulnerability associated with gets.

    fgets has a few more important features which need to be understood:

    • it reads characters up to and including the newline character (\n) if it fits within the buffer size n.
    • the function automatically appends a null character at the end of the string \0.
    • it returns a pointer stron success and NULLon error or if the end of the file is reached before any characters are read.

    Here is a common pattern that is used to read from standard input. We illustrate this with the array food:

    fgets(food, sizeof(food), stdin);

    Note that if we are not reading into an array, we should just enter the size we want. Here, if food was just a simple pointer variable, the sizeof operator would have just returned the size of a pointer.

    Here is a more comprehensive comparison between scanf and fgets:

    • fgets has a mandatory limit on number of characters so does not pose safety concerns
    • scanf will not only allow you to enter more than one field, but it also allows you to enter structured data including the ability to specify what characters appear between fields.

      fgets allows you to enter just one string into a buffer. No other data types. Just strings. Just one buffer.

    • scanf cannot handle strings with spaces elegantly. Fancy regular expression tricks are needed to handle spaces in inputs.

    Note that the fgets command will read the newline character. This is because it always reads the entire line. However, this is not desirable so we can get rid of it using the following code:

    		#include <string.h>    
        len = strlen(str);
    
        // Check if the last character is a newline and replace it with a null terminator
        if (len > 0 && str[len - 1] == '\n') {
            str[len - 1] = '\0';
        }
  • puts(): Print a string to the standard output (usually the console) and automatically adds a newline character (\n) to the end. 

    The syntax for the puts() function is:

    int puts(const char *str);

    It accepts a single argument, str, which is a pointer to the null-terminated string to be printed. It is exclusively for printing strings; it cannot handle other data types or format specifiers like printf().

  • printf(): For formatted output to the standard output (usually the console).

    It replaces format characters with the values of variables, like this:

    printf("%s says the count is %i", "Ben", 21);
    // This will print: Ben says the count is 21

    You can include as many parameters as you like when you call the printf() function, but make sure you have a matching % format character for each one.

    With printf we have the following unique format specifiers:

    • %f - float or double
    • %e - scientific notation
    • %m.nf - m is the minimum total field width and n is the number of digits to display after the decimal point
    • %s - string with a null terminator

    Note that format specifiers are different for printf and scanf. Remember that the format specifier  for scanf() is designed to match the structure of the input, not to enforce precision limits on the data being read.

  • fprintf(): See For this we use the fprintf function. This is the superset of printf which allows us to choose where you want to send text to. .
  • fopen(): To open a file for reading or writing.
  • fclose(): To close an opened file.
  • fread() and fwrite(): For reading and writing blocks of data to/from files.
  • getchar() and putchar(): For character-by-character input and output.

stdio.h

This includes Standard Utility functions. Here are some useful ones:

  • atoi: used to convert a numeric string into an integer value (ASCII to Integer).

string.h

The Standard Library also contains code to process strings. String processing is required by a lot of the programs, and the string code in the Standard Library is tested, stable, and fast.

Here are some of the useful functions provided by string.h:

The functions in <string.h> allow for common operations such as copying, concatenating, comparing, and searching strings.

Function NameDescription
strlen()Returns the length of a string, excluding the terminating null character (\0).
strcpy()Copies one string (source) to another (destination).
strncpy()Copies a specified number of characters from one string to another. This is the safer version.
strcat()Appends the string pointed to by src to the end of the string pointed to by dest.
strncat()Appends a specified number of characters from one string to the end of another.
strcmp()Compares two strings lexicographically and returns an integer indicating their relationship (e.g., less than, equal to, or greater than).
strncmp()Compares a specified number of characters in two strings.
strstr()Finds the first occurrence of a substring (needle) within another string (haystack).
strchr()Locates the first occurrence of a specific character within a string.
strtok()Breaks a string into a series of tokens (smaller parts) based on a specified delimiter.

The header also defines the data type size_t (an unsigned integer type used to represent sizes of objects in bytes) and the macro NULL (a null pointer constant).

Note: In C, you use strcpy() because strings are essentially arrays of characters, and C does not permit assigning one entire array to another using the simple assignment operator (=) after the initial declaration. 

unistd.h

The unistd.h header is not actually part of the standard C library. Instead, it gives your programs access to some of the POSIX libraries. POSIX was an attempt to create a common set of functions for use across all popular operating systems.

  • getopt() - each time you call it, it returns the next option it finds on the command line.

math.h

For the mathematics library, for the compiler to load the code, you need to use the -lm option with gcc so that the functions can be loaded (i.e. gcc -lm).

Common Functions

Here are some of the most widely used functions: 

Power and Roots 

sqrt(x): Calculates the square root of x. • cbrt(x): Computes the cube root of x. • pow(x, y): Returns x raised to the power of y (xyx to the y-th power𝑥𝑦). • hypot(x, y): Calculates the hypotenuse of a right-angled triangle given sides x and y, or x2+y2the square root of x squared plus y squared end-root𝑥2+𝑦2√. 

Rounding and Absolute Values 

fabs(x): Returns the absolute value of a floating-point number x. • floor(x): Rounds x downwards to the nearest integer. • ceil(x): Rounds x upwards to the nearest integer. • round(x): Rounds x to the nearest integer. • fmod(x, y): Calculates the floating-point remainder of the division x/y. • trunc(x): Truncates the decimal part of x (rounds toward zero). 

Logarithmic and Exponential Functions 

exp(x): Calculates the value of exe to the x-th power𝑒𝑥. • log(x): Computes the natural logarithm (base ee𝑒) of x. • log10(x): Computes the base-10 logarithm of x

Trigonometric Functions 

Note that all trigonometric functions in C operate on angles specified in radians, not degrees. The constant M_PI (defined in <math.h>) can be used for conversions.  • sin(x): Calculates the sine of angle x. • cos(x): Calculates the cosine of angle x. • tan(x): Calculates the tangent of angle x. • asin(x): Calculates the arc sine (inverse sine) of x, returning the angle in radians. • acos(x): Calculates the arc cosine (inverse cosine) of x, returning the angle in radians. • atan(x): Calculates the arc tangent (inverse tangent) of x, returning the angle in radians. 

Escape Sequences

Escape sequences in C are special character combinations used within string literals and character constants to represent characters that are either non-printable or have special meaning within the language syntax. They all begin with a backslash (\).

Escape SequenceNameDescription
\aAlert (Bell)Produces an audible beep or visual alert on some systems.
\bBackspaceMoves the cursor back one position.
\fForm feedAdvances the output to the next logical page or form.
\nNewlineMoves the cursor to the beginning of the next line (line feed).
\rCarriage returnMoves the cursor to the beginning of the current line, potentially overwriting previous output.
\tHorizontal tabMoves the cursor to the next horizontal tab stop.
\vVertical tabMoves the cursor to the next vertical tab stop.
\\BackslashRepresents a literal backslash character.
\'Single quoteRepresents a literal single quotation mark.
\"Double quoteRepresents a literal double quotation mark.
\?Question markRepresents a literal question mark, primarily used to avoid misinterpretation as a trigraph.
\0Null characterRepresents the null terminator character, crucial for marking the end of C strings.
\nnnOctal valueRepresents a character by its octal value (up to three digits).
\xhh...Hexadecimal valueRepresents a character by its hexadecimal value (one or more digits).
\uhhhhUnicodeRepresents a Unicode character using 4 hexadecimal digits (C99 standard).
\UhhhhhhhhUnicodeRepresents a Unicode character using 8 hexadecimal digits (C99 standard)

typedef

The typedef is a keyword that is used to provide existing data types with a new name. The C typedef keyword is used to redefine the name of already existing data types.

When names of datatypes become difficult to use in programs, typedef is used with user-defined datatypes, which behave similarly to defining an alias for commands.

For example, if you want to define an alias for a pointer type to avoid using pointer syntax throughout the code, you can write:

#include <stdio.h>

// Creating alias for pointer
typedef int* ip;

int main() {
    int a = 10;
    ip ptr = &a;

    printf("%d", *ptr);
    return 0;
}

Similarly, if you want to use an alias for an array, we can use:

// Here 'arr' is an alias
typedef int arr[4];

int main() {
    arr a = { 10, 20, 30, 40 };
   ...

const and static

const makes an object non-modifiable through that declared type. A const int x = 3; cannot be assigned to after initialization, and attempting to modify it is a constraint violation that compilers typically reject.

const is mainly about expressing invariants and enabling compiler checks. It helps catch bugs early and documents intent.

At file scope, static gives a name internal linkage, meaning it is visible only inside that source file, not to other translation units. This is how you make file-private globals and helper functions in C.

Inside a function, static changes storage duration to static duration: the object is created once and persists for the entire program run, while keeping its local scope. So a static local variable retains its value between function calls.

It gives you “remembering” state without using global variables. Here is an example:

#include <stdio.h>

int next_id(void) {
    static int id = 0;
    return id++;
}

int main(void) {
    printf("%d\n", next_id());  // 0
    printf("%d\n", next_id());  // 1
    printf("%d\n", next_id());  // 2
    return 0;
}

A clean way to remember it:

  • const answers: “Can this object be modified through this declaration?”
  • static answers: “How long does it live, and who can see it?”

const static combines both ideas: the object has static duration or internal linkage as appropriate, and it cannot be modified through that declaration. For a local variable, this means one persistent read-only object for the whole program run.

Square Brackets

C uses [] in two very different ways:

  • In an expressiona[i] means access the element at offset i from a, which is defined in terms of pointer arithmetic. See Pointer Arithmetic.
  • In a declarationint a[5]; means “a is an array of 5 ints.”

    Here the 5 is part of the type declaration, not an index operation.

    When you write int a[5];, the 5 is the number of elements in the array, not an index. The compiler uses the element type plus that count to reserve enough contiguous storage for 5 ints. This is why a[5] is not a single object named “5”; it is an array of five objects of type int.

An empty pair like int nums[] usually means the compiler must infer the size from an initializer:

int nums[] = {10, 20, 30};

That declares an array whose size is 3, because the initializer gives three elements.

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.