An Assertion Is a Claim, Not a Check

assert lives in <assert.h> and takes one expression. If the expression is true it does nothing whatsoever. If it is false it prints the file, the line, the enclosing function and the source text of the expression to standard error, then aborts the program where it stands. Notice what is missing from that description: there is no way for the program to carry on, no status to return, no caller to inform. assert is not error handling. Last lesson's machinery exists because failures genuinely happen, and a program that meets one is doing its job by reporting it. An assertion is for the opposite case: a condition you believe is guaranteed by the rest of your own code, so that if it is ever false the program is not in trouble, it is wrong. That gives three categories, and sorting a given condition into the right one is most of the skill.

  • User input gets validated, because the user is free to type anything. A check, a diagnostic on standard error, and a nonzero exit.
  • Library calls get checked, because malloc really can return NULL and scanf really can convert nothing. Failure is a legitimate outcome and the caller handles it.
  • Invariants get asserted, because you are the one who guaranteed them: a precondition every caller in this program has already satisfied, a branch that cannot be reached, a pointer that was assigned two lines up.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

static int sum_of(const int *values, int count)
{
    assert(count > 0);

    int total = 0;

    for (int i = 0; i < count; ++i)
    {
        total += values[i];
    }

    return total;
}

int main(void)
{
    int count = 0;

    if (scanf("%d", &count) != 1 || count <= 0)
    {
        fprintf(stderr, "expected a positive count\n");
        return 1;
    }

    int *values = malloc(count * sizeof *values);

    if (values == NULL)
    {
        fprintf(stderr, "could not allocate\n");
        return 1;
    }

    for (int i = 0; i < count; ++i)
    {
        values[i] = i + 1;
    }

    printf("sum is %d\n", sum_of(values, count));
    free(values);

    return 0;
}

All three appear once each. main validates what the user typed and refuses a count that is not positive; it checks what malloc handed back; and sum_of asserts. Read assert(count > 0) carefully, because it is not a second opinion on the user's input. It is a statement about main: by the time control reaches sum_of, every non-positive count has already been turned away, so a count of zero arriving here would mean the guard is gone or a new caller forgot it. The assertion writes that agreement into the code where a comment would otherwise have to. Given 4 the program prints sum is 10; given 0 it prints expected a positive count on standard error and exits with status 1, and the assertion never has an opinion either way. Now break the agreement: delete the || count <= 0 half of main's guard so a count of zero reaches sum_of, feed it 0, and this is what arrives on standard error.

out: /tmp/main.c:7: sum_of: Assertion `count > 0' failed.
Aborted

That first line is the whole report and it is enough: the file, the line, the function, and the expression that turned out to be false. The second line is the shell noting that the process died on SIGABRT, and the exit status is 134. Two details matter beyond the text. A failed assertion does not return, does not unwind and does not run any cleanup, so anything still allocated stays allocated and the program simply stops. And abort does not flush standard output, which is last lesson's buffering point arriving in a new costume: a program that printed three perfect lines and then tripped an assertion delivers none of them when its output is a pipe. On this platform that settles the practical question. An assertion failure is a failed exercise, because the sandbox compares standard output and an aborted program has produced none.

NDEBUG Deletes Every Assertion

Compiling with -DNDEBUG does not disable assertions, it removes them. The macro expands to nothing, the expression is never compiled, and the release binary contains no trace of it. That is the design, and it is why assertions can be scattered as freely as you like. It also has two consequences that are not optional. First: never put work inside an assertion.

#include <assert.h>
#include <stdio.h>

int main(void)
{
    int remaining = 3;

    assert(--remaining > 0);
    printf("remaining is %d\n", remaining);

    return 0;
}

Compiled the way this platform compiles it, that prints remaining is 2. Compiled with -DNDEBUG added and nothing else changed, the very same source prints remaining is 3, because the --remaining was inside the macro and went with it. A program whose behaviour depends on the assertions being present is a program with two different meanings, and the one you tested is not the one you shipped. Keep assertions pure: read state, never change it, and never call a function that does.

Second: an assertion is never a substitute for a real check, precisely because it may not be there. assert(values != NULL) after a malloc looks like diligence and is nothing of the sort, since the release build allocates, gets NULL, and walks straight into the dereference. That is why the three-way sort above is not a matter of taste. Input and library results must be checked by code that survives the compiler flags.

Platform note. This sandbox does not define NDEBUG, so assertions do fire in every exercise you write here: an assert in exercise code is a real gate, not documentation. One relative also deserves naming. C11 added static_assert (spelled _Static_assert without the <assert.h> macro), which checks a constant expression while the program is being compiled rather than while it runs, so it fails the build instead of the run; this course does not use it, but you will meet it in real code.

Reading a gcc Diagnostic

The other half of diagnostic fluency is the compiler, and gcc's output has as fixed a shape as a sanitizer report. The next program is wrong on purpose, in three separate ways.

#include <stdio.h>

int main(void)
{
    unsigned int count = 4;
    int total = 0;

    for (int i = 0; i < count; ++i)
    {
        total += i
    }

    printf("total is %ld\n", total);

    return 0;
}
/tmp/main.c: In function 'main':
/tmp/main.c:8:23: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
    8 |     for (int i = 0; i < count; ++i)
      |                       ^
/tmp/main.c:10:19: error: expected ';' before '}' token
   10 |         total += i
      |                   ^
      |                   ;
   11 |     }
      |     ~
/tmp/main.c:13:24: warning: format '%ld' expects argument of type 'long int', but argument 2 has type 'int' [-Wformat=]
   13 |     printf("total is %ld\n", total);
      |                      ~~^     ~~~~~
      |                        |     |
      |                        |     int
      |                        long int
      |                      %d

Every diagnostic there has the same five parts. The location opens the line: /tmp/main.c:8:23 is file, line, and then the column, which is the part people skim past and the part that points at the exact token. The severity follows, and only two of them matter: a warning still produces a program, an error means no program was produced at all. The message states the problem in words. The check that fired is named in brackets at the end, [-Wsign-compare], and that name is the most useful thing on the line, because it is what you search for and it tells you a specific documented rule fired rather than the compiler having a hunch. The excerpt below repeats your own source with the line number down the side and a caret under the offending token, and gcc frequently goes further: it prints a ; under the caret where the semicolon belongs, and under the format warning it labels each side of the mismatch and suggests %d. Take those suggestions seriously and never as authority, because they fix the symptom the compiler can see.

Then read them in the right order. Fix the first error, then recompile. An error means parsing or type checking failed at that point, and everything gcc says afterwards was produced by a compiler that is now guessing, which is why chapter 6's bare Book pick; drew a correct first error followed by ten more that all vanished together. gcc helps where it can: restore the semicolon and delete int total = 0; instead, and the output is a single error at the first use of total followed by note: each undeclared identifier is reported only once for each function it appears in, a note being extra context attached under the diagnostic it belongs to rather than a complaint of its own. Warnings are the opposite: they are independent of each other, so read every one. Here the semicolon is one edit and it fixes nothing else; the two warnings survive it untouched, and only making count an int and the format %d gets a clean build printing total is 6. This course has required warning-free code since chapter 1 on the argument that a warning is a bug report from a program that has read your code more carefully than you have, and real projects stop relying on discipline and add -Werror, which promotes every warning to an error so a warning cannot be ignored because it cannot build. That is the same rule with the honour system removed.

Your Warning Collection

You have accumulated a working set, and as with the sanitizer reports, the flag name narrows the search before you read the message.

  • -Wformat (chapter 1): the conversion specifier and the argument disagree, %d given a double or %ld given an int.
  • -Wmaybe-uninitialized (chapter 4): a value is read on some path before anything was stored in it.
  • -Wsign-compare (chapter 2): a signed and an unsigned value are compared, and the signed one converts.
  • -Wsequence-point and -Wshift-count-overflow (chapter 2): one object modified twice in an expression with no ordering between the two, and a shift by at least the width of the type. Both are undefined behaviour.
  • -Wparentheses (chapter 2): the precedence you wrote is probably not the precedence you meant, & against == being the classic.
  • -Wempty-body, -Wmisleading-indentation and -Wimplicit-fallthrough (chapter 2): a stray semicolon left an if with no body, an indented block that only looks guarded, a switch case running into the next with no break.
  • -Wtype-limits (chapter 2): a comparison is always true or always false given the type's range, such as testing an unsigned for < 0.
  • -Wreturn-type and -Wreturn-local-addr (chapter 3): a function can reach its end without returning, or hands back the address of one of its own locals.
  • -Warray-bounds and -Wstringop-overflow (chapters 4 and 5): an index or a string write is provably outside the object.
  • -Wsizeof-pointer-div (chapter 4) and -Wswitch (chapter 6): the sizeof array / sizeof array[0] count applied to a pointer, and a switch over an enum leaving one enumerator unhandled.
  • implicit declaration of function (chapter 3) is not a warning at all but an error in C17, which is why a missing #include fails the build instead of quietly producing a wrong program.

Compile Time, Run Time, and the Gap

Those two catalogues are the whole toolkit. gcc works at compile time and reasons about the text of your program, so it catches what is provably wrong before anything runs and costs nothing to consult. AddressSanitizer works at run time and watches the addresses your program actually touches, which is chapter 4 lesson 5's subject: read the kind on the ERROR line, the operation, your first frame in the stack trace, and the memory's story in the is located and allocated by lines. Between them they cover a great deal, and neither covers everything. The gap is the whole reason this chapter exists: signed overflow, shifting past the width of a type, dividing by zero and reading an uninitialized int are undefined behaviour that the compiler may not prove and the sanitizer does not watch for. A warning-free compile and a clean sanitizer run together are necessary and still not sufficient. What closes the gap is not another tool but writing code whose meaning is defined in the first place, and stating what you believe with an assertion so that the belief fails loudly on the day it stops being true.

Key Takeaways

  • assert(expression) from <assert.h> states something you believe cannot be false. When it is false it prints file:line: function: Assertion 'expr' failed. to standard error and aborts: no return, no cleanup, no flush of standard output, exit status 134.
  • Sort every condition into one of three bins. User input is validated, library results such as malloc and scanf are checked, and invariants you guaranteed yourself are asserted. Never assert user input or a failed allocation.
  • -DNDEBUG removes assertions entirely, so never put work or side effects inside one (assert(--count > 0) loses the decrement in release) and never let an assertion stand in for a real check. This sandbox does not define NDEBUG, so a failed assertion here aborts and fails the exercise.
  • A gcc diagnostic has five parts: file:line:column, the severity, the message, the [-Wflag-name] naming the check that fired, and the source excerpt with a caret and often a suggested fix. Fix the first error and recompile, because later errors are usually cascade; warnings are independent, so read all of them. -Werror is how real projects enforce warning-free mechanically.
  • gcc catches what is provably wrong at compile time and AddressSanitizer catches memory errors at run time, and the undefined behaviour in between belongs to neither. Clean output from both is necessary and never sufficient.