Level 1. Acquaintance

Our mascot for this level, the common raven, is a very sociable corvid and known for its problem-solving capacity. Ravens organize in teams and have been observed playing even as adults.

This level will acquaint you with the C programming language: that is, it will provide you with enough knowledge to write and use good C programs. “Good” here refers to a modern understanding of the language, avoiding most of the pitfalls of early dialects of C, and offering you some constructs that were not present before and that are portable across the vast majority of modern computer architectures, from your cell phone to a mainframe computer. Having worked through these chapters, you should be able to write short code for everyday needs: not extremely sophisticated, but useful and portable.

Buckle up

In many ways, C is a permissive language; programmers are allowed to shoot themselves in the foot or other body parts if they choose to, and C will make no effort to stop them. Therefore, just for the moment, we will introduce some restrictions. We’ll try to avoid handing out guns in this level, and place the key to the gun safe out of your reach for the moment, marking its location with big and visible exclamation marks.

The most dangerous constructs in C are the so-called castsC, so we’ll skip them at this level. However, there are many other pitfalls that are less easy to avoid. We will approach some of them in a way that might look unfamiliar to you, in particular if you learned your C basics in the last millennium or if you were introduced to C on a platform that wasn’t upgraded to current ISO C for years.

Some of “getting used to” our approach on this level may concern the emphasis and ordering in which we present the material:

You might also be surprised by some style considerations that we will discuss in the following points. On the next level, we will dedicate an entire chapter (chapter 9) to these questions, so please be patient and accept them for the moment as they are.

  1. We bind type modifiers and qualifiers to the left. We want to separate identifiers visually from their type. So we will typically write things as
    char* name;
    where char* is the type and name is the identifier. We also apply the left-binding rule to qualifiers and write
    char const* const path_name;
    Here the first const qualifies the char to its left, the * makes it to a pointer, and the second const again qualifies what is to its left.
  2. We do not use continued declarations. They obfuscate the bindings of type declarators. For example:
    unsigned const*const a, b;
    Here, b has type unsigned const: that is, the first const goes to the type, and the second const only goes to the declaration of a. Such rules are highly confusing, and you have more important things to learn.
  3. We use array notation for pointer parameters. We do so wherever these assume that the pointer can’t be null. Examples:
    /* These emphasize that the arguments cannot be null. */
    size_t strlen(char const string[static 1]);
    int main(int argc, char* argv[argc+1]);
    /* Compatible declarations for the same functions. */
    size_t strlen(const char *string);
    int main(int argc, char **argv);
    The first stresses the fact that strlen must receive a valid (non-null) pointer and will access at least one element of string. The second summarizes the fact that main receives an array of pointers to char: the program name, argc-1 program arguments, and one null pointer that terminates the array. Note that the previous code is valid as it stands. The second set of declarations only adds additional equivalent declarations for features that are already known to the compiler.
  4. We use function notation for function pointer parameters. Along the same lines, we do so whenever we know that a function pointer can’t be null:
    /* This emphasizes that the handler'' argument cannot be null. */
    int atexit(void handler(void));
    /* Compatible declaration for the same function.              */
    int atexit(void (*handler)(void));
    Here, the first declaration of atexit emphasizes that, semantically, it receives a function named handler as an argument and that a null function pointer is not allowed. Technically, the function parameter handler is “rewritten” to a function pointer much as array parameters are rewritten to object pointers, but this is of minor interest for a description of the functionality. Note, again, that the previous code is valid as it stands and that the second declaration just adds an equivalent declaration for atexit.
  5. We define variables as close to their first use as possible. Lack of variable initialization, especially for pointers, is one of the major pitfalls for novice C programmers. This is why we should, whenever possible, combine the declaration of a variable with the first assignment to it: the tool that C gives us for this purpose is the definition: a declaration together with an initialization. This gives a name to a value and introduces this name at the first place where it is used. This is particularly convenient for for loops. The iterator variable of one loop is semantically a different object from that in another loop, so we declare the variable within the for to ensure it stays within the loop’s scope.
  6. We use prefix notation for code blocks. To be able to read a code block, it is important to capture two things about it easily: its purpose and its extent. Therefore:

    Examples:
    int main(int argc, char* argv[argc+1]) {
      puts("Hello world!");
      if (argc > 1) {
        while (true) {
          puts("some programs never stop");
        }
      } else {
        do {
          puts("but this one does");
        } while (false);
      }
      return EXIT_SUCCESS;
    }