The general ideas of selection and iteration are explained under Programming Concepts. This page covers what is specific to C. Every example is a complete program, compiled with gcc -std=c17 -Wall -Wextra -Werror as described on compiling and running C; several of them show a mistake that those flags turn into an error before the program can run.

Conditions are numbers

C has no separate "true" and "false" in its conditions. An if, while or for evaluates an expression and compares it with zero: zero means false, every other value means true. The comparison operators (<, >=, ==, != …) and the logical operators &&, || and ! produce an int: 1 for true, 0 for false.

#include <stdio.h>

int main(void)
{
    int open_tickets = 3;

    printf("%d\n", open_tickets > 0);
    printf("%d\n", open_tickets > 5);
    printf("%d\n", open_tickets > 0 && open_tickets < 10);

    if (open_tickets) {
        printf("Work to do: %d open tickets\n", open_tickets);
    }
    return 0;
}
1
0
1
Work to do: 3 open tickets

if (open_tickets) is short for if (open_tickets != 0), and a pointer works the same way: if (p) means "if p is not a null pointer" (see pointers). Since C99, <stdbool.h> offers bool, true and false as readable names for the same idea; in C23 they are keywords and need no header (see modern C). && and || stop as soon as the answer is known, which is explained with the other operators on operators, expressions and conversions.

= instead of ==

Because any number is a valid condition, the typo = (assignment) for == (comparison) is legal C. if (status = 1) stores 1 in status, and the condition is then always true. gcc's -Wall warns about it, and with -Werror the program does not compile:

#include <stdio.h>

int main(void)
{
    int status = 0;

    if (status = 1) {
        printf("Paging the incident manager\n");
    }
    return 0;
}
example.c:7:9: error: suggest parentheses around assignment used as truth value [-Werror=parentheses]

Sometimes an assignment inside a condition is exactly what you want, as in the reading loop further down, while ((c = getchar()) != EOF). Writing the comparison explicitly, or wrapping the assignment in an extra pair of parentheses, tells both the compiler and the next reader that it is deliberate.

if, else if and else

An if statement runs one statement when its condition is true; an optional else runs another when it is false. That one statement is almost always a compound statement: a block of declarations and statements between braces { } that C treats as a single statement. There is no elif: else if is simply an else whose statement is another if.

#include <stdio.h>

int main(void)
{
    double temperature = 31.5;

    if (temperature >= 30) {
        printf("Heat alert: check the transformer cooling.\n");
    } else if (temperature <= 0) {
        printf("Frost alert: check the outdoor meters.\n");
    } else {
        printf("Normal temperature.\n");
    }
    return 0;
}
Heat alert: check the transformer cooling.

A compound statement also starts a new block: a variable declared inside the braces exists only until the closing brace. How long variables live and where their names are visible is covered on functions and header files.

Indentation means nothing

Without braces, only the next single statement belongs to the if. The compiler ignores indentation, so a second indented line runs every time. gcc 12 recognizes this pattern and reports it as misleading indentation (part of -Wall):

#include <stdio.h>

int main(void)
{
    int usage_kwh = 120;

    if (usage_kwh > 500)
        printf("High usage this month.\n");
        printf("Sending usage warning to the customer.\n");
    return 0;
}
example.c:7:5: error: this ‘if’ clause does not guard... [-Werror=misleading-indentation]

Without the warning, the program would send the warning to every customer. Many C style guides, including the coding standards used for safety-critical firmware, require braces around every block, even a one-line one.

The dangling else

With two nested if statements and one else, which if does the else belong to? This ambiguity is called the dangling else. C's rule is that an else belongs to the nearest if that does not have one yet, whatever the indentation suggests. The code below looks as if "Low usage" is printed for small readings; it is actually printed for large readings in homes without solar panels. gcc refuses it:

#include <stdio.h>

int main(void)
{
    int kwh = 350;
    int solar = 0;

    if (kwh > 100)
        if (solar)
            printf("Net usage, solar home\n");
    else
        printf("Low usage\n");
    return 0;
}
example.c:8:8: error: suggest explicit braces to avoid ambiguous ‘else’ [-Werror=dangling-else]

Braces remove the question. Put them where you meant the else to go:

#include <stdio.h>

int main(void)
{
    int kwh = 350;
    int solar = 0;

    if (kwh > 100) {
        if (solar) {
            printf("Net usage, solar home\n");
        }
    } else {
        printf("Low usage\n");
    }
    printf("checked %d kWh\n", kwh);
    return 0;
}
checked 350 kWh

The stray semicolon

A semicolon on its own is a null statement: a statement that does nothing. It is useful in a loop whose work happens entirely in its condition, but after an if it is almost always a typo. if (kwh > 1000); ends the if at the semicolon, and the block below it runs every time. -Wextra catches it:

#include <stdio.h>

int main(void)
{
    int kwh = 350;

    if (kwh > 1000);
    {
        printf("High usage: %d kWh\n", kwh);
    }
    return 0;
}
example.c:7:20: error: suggest braces around empty body in an ‘if’ statement [-Werror=empty-body]

When you do want an empty body, put the semicolon on a line of its own (or write { }), so it is clearly intentional. The for loop section has an example.

switch

A switch compares one integer value with a list of case labels and jumps to the one that matches; default catches everything else. A break leaves the switch. Several labels in a row share the same code.

#include <stdio.h>

int main(void)
{
    int priority = 2;

    switch (priority) {
    case 1:
        printf("Respond within 15 minutes.\n");
        break;
    case 2:
    case 3:
        printf("Respond within 4 hours.\n");
        break;
    default:
        printf("Respond on the next working day.\n");
        break;
    }
    return 0;
}
Respond within 4 hours.

The value must have an integer type: int, char, long, an enum and so on. Each case label must be an integer constant expression, known when the program is compiled: a literal, a character such as 'K', an enum constant or a macro that expands to one. Strings, floating-point values and variables are not allowed:

#include <stdio.h>

int main(void)
{
    const char *tariff = "night";

    switch (tariff) {
    case "day":
        printf("0.32\n");
        break;
    }
    return 0;
}
example.c:7:13: error: switch quantity not an integer

To choose on text, compare with strcmp in an if/else if chain (see strings). Characters work well, because a character constant is an integer. This loop classifies the characters of a line from a smart meter's P1 telegram:

#include <stdio.h>

int main(void)
{
    const char *line = "1-0:1.7.0(01.234*kW)";
    int digits = 0, separators = 0, other = 0;

    for (int i = 0; line[i] != '\0'; i++) {
        switch (line[i]) {
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
            digits++;
            break;
        case '-': case ':': case '.':
            separators++;
            break;
        default:
            other++;
            break;
        }
    }
    printf("digits %d, separators %d, other %d\n", digits, separators, other);
    return 0;
}
digits 10, separators 5, other 5

Fall-through

A case label is only an entry point. Once C has jumped to a matching label, it runs every statement below it, straight through the following labels, until it meets a break or the end of the switch. This fall-through is what makes shared labels work, and a forgotten break is one of the best-known C bugs. Since gcc 7, -Wextra includes -Wimplicit-fallthrough, which flags a case that runs into the next one:

#include <stdio.h>

int main(void)
{
    int priority = 1;

    switch (priority) {
    case 1:
        printf("page the on-call engineer\n");
    case 2:
        printf("notify the service desk\n");
        break;
    default:
        printf("add to the backlog\n");
    }
    return 0;
}
example.c:9:9: error: this statement may fall through [-Werror=implicit-fallthrough=]

When the fall-through is deliberate, say so. gcc accepts a comment such as /* fall through */ on its own line right before the next label:

#include <stdio.h>

int main(void)
{
    for (int priority = 1; priority <= 3; priority++) {
        printf("P%d:\n", priority);
        switch (priority) {
        case 1:
            printf("  page the on-call engineer\n");
            /* fall through */
        case 2:
            printf("  notify the service desk\n");
            break;
        default:
            printf("  add to the backlog\n");
            break;
        }
    }
    return 0;
}
P1:
  page the on-call engineer
  notify the service desk
P2:
  notify the service desk
P3:
  add to the backlog

C23 made this official with the attribute [[fallthrough]];, which every C23 compiler understands and which does not depend on the wording of a comment. This version is compiled with clang 21 in C23 mode:

#include <stdio.h>

int main(void)
{
    int priority = 1;

    switch (priority) {
    case 1:
        printf("page the on-call engineer\n");
        [[fallthrough]];
    case 2:
        printf("notify the service desk\n");
        break;
    default:
        printf("add to the backlog\n");
        break;
    }
    return 0;
}
page the on-call engineer
notify the service desk

Switching on an enum

When the value is an enum and the switch has no default, -Wall checks that every enum constant has a case. Add a status to the enum later, and the compiler points at every switch that does not handle it yet:

#include <stdio.h>

enum status { OPEN, IN_PROGRESS, RESOLVED, ON_HOLD };

int main(void)
{
    enum status s = ON_HOLD;

    switch (s) {
    case OPEN:
        printf("open\n");
        break;
    case IN_PROGRESS:
        printf("in progress\n");
        break;
    case RESOLVED:
        printf("resolved\n");
        break;
    }
    return 0;
}
example.c:9:5: error: enumeration value ‘ON_HOLD’ not handled in switch [-Werror=switch]

A default label would silence this check, which is a reason to leave it out of switches over an enum. Enums themselves are explained on structs, unions and enums.

while and do-while

A while loop repeats its body as long as its loop condition, the expression in parentheses, is true (nonzero). The condition is tested before every round, including the first, so the body may run zero times.

#include <stdio.h>

int main(void)
{
    int battery = 100;          /* percent */
    int hours = 0;

    while (battery > 20) {
        battery -= 15;
        hours++;
    }
    printf("Below 20%% after %d hours (battery at %d%%)\n", hours, battery);
    return 0;
}
Below 20% after 6 hours (battery at 10%)

A do…while loop tests its condition after each round, so the body always runs at least once. That fits a retry: try first, then decide whether to try again. Note the semicolon after the closing while (…).

#include <stdio.h>

int main(void)
{
    int attempt = 0;
    int connected = 0;

    do {
        attempt++;
        printf("Connecting to the broker, attempt %d\n", attempt);
        connected = (attempt == 3);        /* pretend the third try works */
    } while (!connected && attempt < 5);

    printf("%s\n", connected ? "Connected" : "Giving up");
    return 0;
}
Connecting to the broker, attempt 1
Connecting to the broker, attempt 2
Connecting to the broker, attempt 3
Connected

Real firmware waits longer after each failed attempt and adds a random delay; why that matters is told on provisioning and over-the-air updates. A device's main program is often one loop that never ends, written while (1) or for (;;): the super loop.

Reading input until EOF

A classic C loop reads characters until the input ends. getchar() returns the next character as an int, or the special value EOF (a negative number, −1 with glibc) when there is nothing left. The assignment happens inside the condition, and the result is compared with EOF:

#include <stdio.h>

int main(void)
{
    int c;                      /* int, not char: it must hold EOF too */
    int lines = 0, chars = 0;

    while ((c = getchar()) != EOF) {
        chars++;
        if (c == '\n') {
            lines++;
        }
    }
    printf("%d lines, %d characters\n", lines, chars);
    return 0;
}
3 lines, 53 characters

Declaring c as char is a famous bug, and on our test machine, a 64-bit Linux machine (ARM64), it shows up in the worst possible way. A plain char is unsigned there (it is signed on most x86-64 systems), so it can never hold −1: the comparison with EOF is never true and the loop never ends. Recorded with the compiler flags used on this site, -Wextra catches it:

eof.c:5:28: error: comparison is always true due to limited range of data type [-Werror=type-limits]
    5 |     while ((c = getchar()) != EOF) {
      |                            ^~
cc1: all warnings being treated as errors

On a machine where char is signed, the same code seems to work, but stops early as soon as the input contains the byte 0xFF, which looks like −1 once it is squeezed into a signed char. Both versions are fixed the same way: store the result of getchar() in an int. Why char differs between machines is explained on variables, types and integers.

The for loop

A for loop puts the three parts of a counting loop on one line: an initialization that runs once, the loop condition tested before every round, and an update that runs after every round. Since C99 the loop variable can be declared in the initialization, and then exists only inside the loop.

#include <stdio.h>

int main(void)
{
    int count = 0;

    for (int hour = 0; hour < 24; hour += 6) {
        printf("reading at %02d:00\n", hour);
        count++;
    }
    printf("%d readings\n", count);
    return 0;
}
reading at 00:00
reading at 06:00
reading at 12:00
reading at 18:00
4 readings

A for loop is a while loop in a compact form, and every part is optional. for (;;) has no condition and runs until something inside it breaks out. The comma operator lets one part do two things, and a loop whose work happens in its header can have an empty body:

#include <stdio.h>

int main(void)
{
    int sum = 0;
    for (int i = 0, j = 10; i < j; i++, j--) {
        sum += j - i;
    }
    printf("sum %d\n", sum);

    for (;;) {
        printf("runs once\n");
        break;
    }

    int k = 5;
    while (k--)
        ;                           /* deliberately empty body */
    printf("k is %d\n", k);
    return 0;
}
sum 30
runs once
k is -1

The last loop shows a detail worth knowing: k-- tests the old value and then decreases it, so after the test that finds k at 0, it still goes down once more, to −1.

Off-by-one errors

An off-by-one error is a loop or calculation that does one step too many or one too few: <= where < was meant, a count that forgets to include both ends, an index that starts at 1 instead of 0. In most languages it gives a wrong number or an exception. In C, one step too far in an array reads or writes memory that belongs to something else, and nothing stops it.

The loop below should add four readings but runs five times. Built normally, it printed a total with a garbage value added, and exited as if all was well. Built with AddressSanitizer (-fsanitize=address, explained on debugging C), the program stops at the first read past the end and names the line:

#include <stdio.h>

int main(void)
{
    int readings[4] = {120, 95, 130, 110};
    int count = 4;
    int total = 0;

    for (int i = 0; i <= count; i++) {
        printf("reading %d\n", i);
        total += readings[i];
    }
    printf("total %d\n", total);
    return 0;
}
reading 0
reading 1
reading 2
reading 3
reading 4
SUMMARY: AddressSanitizer: stack-buffer-overflow example.c:11 in main

The habit that prevents it: count from 0 and test with i < count, where count is the number of elements. The other classic is the fencepost problem, counting intervals instead of the points between them:

#include <stdio.h>

int main(void)
{
    /* Quarter-hour readings from 08:00 up to and including 10:00 */
    int first = 8 * 60, last = 10 * 60;

    printf("wrong: %d readings\n", (last - first) / 15);
    printf("right: %d readings\n", (last - first) / 15 + 1);
    return 0;
}
wrong: 8 readings
right: 9 readings

Counting down with an unsigned counter

Array sizes are usually size_t, an unsigned type. An unsigned number is never below zero: after 0 it wraps around to the largest possible value. A loop that counts down with i >= 0 therefore never ends, and reads far outside the array. -Wextra sees that the test is always true:

#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int readings[4] = {120, 95, 130, 110};
    size_t count = 4;

    for (size_t i = count - 1; i >= 0; i--) {
        printf("%d\n", readings[i]);
    }
    return 0;
}
example.c:9:34: error: comparison of unsigned expression in ‘>= 0’ is always true [-Werror=type-limits]

The usual idiom tests first and decreases in the same step, so the body sees count - 1 down to 0 and the loop stops cleanly:

#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int readings[4] = {120, 95, 130, 110};
    size_t count = 4;

    for (size_t i = count; i-- > 0; ) {
        printf("readings[%zu] = %d\n", i, readings[i]);
    }
    return 0;
}
readings[3] = 110
readings[2] = 130
readings[1] = 95
readings[0] = 120

break and continue

break leaves the innermost loop (or switch) at once. continue skips the rest of the body and starts the next round; in a for loop the update part still runs first. This loop walks through a P1 telegram character by character, counts the lines and stops at the ! that starts the CRC line:

#include <stdio.h>

int main(void)
{
    const char *telegram =
        "/XYZ5\\2DEMO-P1-METER\r\n"
        "\r\n"
        "1-0:1.8.1(000412.338*kWh)\r\n"
        "1-0:1.7.0(01.234*kW)\r\n"
        "0-0:96.13.0()\r\n"
        "!F1F4\r\n";
    int lines = 0, data_lines = 0;

    for (const char *p = telegram; *p != '\0'; p++) {
        if (*p == '!') {
            break;                  /* the CRC line ends the telegram */
        }
        if (*p != '\n') {
            continue;               /* only line ends are interesting */
        }
        lines++;
        if (p[1] == '0' || p[1] == '1') {
            data_lines++;           /* the next line starts with an OBIS code */
        }
    }
    printf("%d lines before the CRC, %d data lines\n", lines, data_lines);
    return 0;
}
5 lines before the CRC, 3 data lines

The loop variable p is a pointer that steps through the string one character at a time; that style is explained on arrays and pointer arithmetic. In a while loop, take care that a continue does not skip the statement that moves the loop forward, or the loop never ends.

Nested loops

break only leaves the loop it is in. To find the first quarter hour above a limit in a table of homes, a break in the inner loop stops scanning that home, but the outer loop carries on with the next one:

#include <stdio.h>

int main(void)
{
    /* kWh per quarter hour for three homes */
    int usage[3][4] = {
        {12, 15, 11, 14},
        {13, 41, 12, 10},
        {11, 12, 55, 13},
    };
    int limit = 40;

    for (int home = 0; home < 3; home++) {
        for (int q = 0; q < 4; q++) {
            if (usage[home][q] > limit) {
                printf("home %d, quarter %d: %d\n", home, q, usage[home][q]);
                break;              /* leaves only the inner loop */
            }
        }
    }
    return 0;
}
home 1, quarter 1: 41
home 2, quarter 2: 55

To stop both loops at the first match, you can set a flag and test it in the outer condition, move the loops into a function and return from it, or use goto (next section). Java and JavaScript have a labeled break for this. C17 and C23 do not, but named loops with break name; have been accepted for the next standard, C2y; according to the compilers' status pages GCC 15 and Clang 22 already support them (as of October 2026).

goto

A goto statement jumps unconditionally to a label (a name followed by a colon) elsewhere in the same function. Unrestricted jumping makes a program hard to follow, which is why structured programming replaced it with loops and blocks. In C it survives for two situations where it is clearer than the alternatives. The first is leaving nested loops:

#include <stdio.h>

int main(void)
{
    int usage[3][4] = {
        {12, 15, 11, 14},
        {13, 41, 12, 10},
        {11, 12, 55, 13},
    };
    int limit = 40;

    for (int home = 0; home < 3; home++) {
        for (int q = 0; q < 4; q++) {
            if (usage[home][q] > limit) {
                printf("first peak: home %d, quarter %d (%d)\n",
                       home, q, usage[home][q]);
                goto done;          /* leaves both loops at once */
            }
        }
    }
    printf("no peak found\n");
done:
    printf("check finished\n");
    return 0;
}
first peak: home 1, quarter 1 (41)
check finished

The second is cleanup: a function that opens a file and allocates memory jumps forward to one block at the end that releases everything, whichever step failed. This pattern is common in large C code bases; it is explained on error handling.

Keep to these rules: jump forward, never backward (that is a loop in disguise); stay within one function (goto cannot leave it); and do not jump past the declaration of a variable-length array into its scope, which the C standard forbids. Coding standards for safety-critical code, such as MISRA C, restrict goto further (see undefined behavior and safer C).

What the compiler catches

Most of the mistakes on this page are caught at compile time, but only with the right flags. Recorded with gcc 12 on our test machine:

Mistake Flag (in) gcc's message begins
if (status = 1) -Wparentheses (-Wall) suggest parentheses around assignment
second indented line after an unbraced if -Wmisleading-indentation (-Wall) this ‘if’ clause does not guard…
ambiguous else -Wdangling-else (-Wall) suggest explicit braces to avoid ambiguous ‘else’
if (x); -Wempty-body (-Wextra) suggest braces around empty body
forgotten break -Wimplicit-fallthrough (-Wextra) this statement may fall through
enum constant missing from a switch -Wswitch (-Wall) enumeration value … not handled in switch
char c = getchar() compared with EOF; unsigned i >= 0 -Wtype-limits (-Wextra) comparison … is always true
loop one step past the end of an array not at compile time found at run time by AddressSanitizer (see debugging C)

Build every program with at least -Wall -Wextra, and treat the warnings as errors in your own code. What each flag covers, and why -Wall is not "all warnings", is explained on compiling and running C.