Register

C for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Command line arguments and the preprocessor

Records, files and recursion13 min readLesson 5 of 6

5.1 The two arguments to main

A program that takes arguments declares int main(int argc, char **argv). argc counts the strings, including the program name, and argv points at the first of them.

argv[0] is the program name as typed. argv[1] through argv[argc - 1] are the arguments, and the standard guarantees argv[argc] is a null pointer.

Arguments arrive as text. Numbers need strtol or sscanf, and the program decides what a missing argument means.

c
#include <stdio.h>

int main(int argc, char **argv)
{
    /* run with: alpha beta */
    printf("argc: %d\n", argc);
    for (int i = 1; i < argc; i++) {
        printf("%d: %s\n", i, argv[i]);
    }
    if (argc < 2) printf("no arguments\n");
    return 0;
}

Output

argc: 3
1: alpha
2: beta

5.2 Validating before converting

Every argument is a string that may contain anything. strtol with an end pointer tells you whether the whole argument was a number.

A useful shape: check argc first, convert each argument, then report a usage line and return nonzero on failure. The usage line should name the expected arguments.

Reading past argv[argc - 1] is the classic bug. Once the program decides how many arguments it needs, it checks the count before indexing.

c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    /* run with: 4 5 */
    if (argc != 3) {
        printf("usage: prog WIDTH HEIGHT\n");
        return 1;
    }
    char *end = NULL;
    long width = strtol(argv[1], &end, 10);
    if (*end != '\0' || end == argv[1]) return 1;
    long height = strtol(argv[2], &end, 10);
    if (*end != '\0' || end == argv[2]) return 1;
    printf("%ld\n", width * height);
    return 0;
}

Output

20

5.3 Object-like macros

#define NAME value tells the preprocessor to replace every later NAME with value. No semicolon, no type, and no scope: the replacement starts at that line and runs to the end of the file.

A macro is textual. #define OFFSET 2 + 3 then OFFSET * 2 expands to 2 + 3 * 2, which is 8. Parentheses around the replacement prevent that.

For constants, static const int or an enum gives the compiler a type and the debugger a name. Macros still earn their place for conditional compilation.

c
#include <stdio.h>

#define LIMIT 10
#define OFFSET 2 + 3
#define OFFSET_SAFE (2 + 3)

int main(void)
{
    printf("%d\n", LIMIT / 2);
    printf("%d\n", OFFSET * 2);
    printf("%d\n", OFFSET_SAFE * 2);
    return 0;
}

Output

5
8
10

5.4 Function-like macros

#define SQUARE(x) ((x) * (x)) looks like a function and expands at the call site. Each parameter needs its own parentheses, and the whole body needs them too.

The argument is substituted textually, so SQUARE(i++) increments twice. No macro can prevent that, while a function evaluates its argument once.

A macro has no type checking and no single place for a breakpoint. For arithmetic, write a static inline function.

c
#include <stdio.h>

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define BAD_SQUARE(x) x * x

int main(void)
{
    printf("%d\n", SQUARE(2 + 3));
    printf("%d\n", MAX(4, 9));
    printf("%d\n", BAD_SQUARE(2 + 3));
    return 0;
}

Output

25
9
11

5.5 Stringize and token pasting

# inside a macro definition turns an argument into a string literal. #define NAME_OF(x) #x then NAME_OF(total) gives "total".

## joins two tokens into one. #define JOIN(a, b) a##b then JOIN(coun, ter) produces the identifier counter. Both operators build declarations and messages, and both are hard to debug. A plain function or a string constant covers the usual need with less surprise.

c
#include <stdio.h>

#define STRING_OF(x) #x
#define JOIN(a, b) a##b

int main(void)
{
    int JOIN(my, value) = 12;
    printf("%s\n", STRING_OF(my value));
    printf("%d\n", myvalue);
    return 0;
}

Output

my value
12

5.6 Conditional compilation

#if, #elif, #else, and #endif include or exclude text before the compiler sees it. The condition must be a constant expression, and undefined identifiers evaluate to 0.

#ifdef NAME is shorthand for #if defined(NAME). That form suits a build flag that is either defined or absent.

Conditional compilation supports several platforms and build types from one source file. It also hides code from the compiler, so a build that skips half the file catches fewer errors.

c
#include <stdio.h>

#define DEBUG 1
#define VERSION 3

int main(void)
{
#if DEBUG
    printf("debug build\n");
#endif
#if VERSION >= 3
    printf("version 3 or later\n");
#else
    printf("older version\n");
#endif
    return 0;
}

Output

debug build
version 3 or later

5.7 Include guards

A header included twice in one translation unit defines its types twice, which the compiler rejects. An include guard prevents the second inclusion.

The guard wraps the whole file: #ifndef MODULE_H at the top, #define MODULE_H, the declarations, and #endif at the end. The macro name is unique to the header, usually the file name in capitals.

Every compiler also supports #pragma once, which does the same with less typing. Guards are standard C, so they work everywhere.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01What do `argc` and `argv` hold?

    `argc` is the number of argument strings, including the program name. `argv` points at an array of `char *` holding those strings, with a null pointer after the last one.

  • 02Why do macro parameters need parentheses?

    The preprocessor substitutes text, so operator precedence can regroup the expression. `((x) * (x))` keeps the argument and the body grouped.

  • 03What does an include guard prevent?

    A header being processed twice in the same translation unit, which would define the same types and prototypes twice.