For a program to do more than run top to bottom in a straight line, we need conditions that branch and loops that repeat. These are the basic building blocks of every algorithm in software. In this lesson we get comfortable with control flow in C.

1. Branching (conditionals)

Branching lets a program decide which piece of code to run, based on whether a condition comes out true or false.

Using if-else

This is the most common branching structure. The condition inside if must produce a logical value: true (non-zero) or false (zero).

check_score.c
#include <stdio.h>

int main() {
    double score;
    printf("Nhap diem cua ban: ");
    scanf("%lf", &score);

    if (score >= 9.0) {
        printf("Hoc sinh xuat sac\n");
    } else if (score >= 8.0) {
        printf("Hoc sinh gioi\n");
    } else if (score >= 6.5) {
        printf("Hoc sinh kha\n");
    } else if (score >= 5.0) {
        printf("Hoc sinh trung binh\n");
    } else {
        printf("Chua dat yeu cau\n");
    }

    return 0;
}

Using switch-case

When there are many branches keyed on the specific value of an integer or character, switch-case is a cleaner alternative to a long if-else if chain. Do not forget the break statement that ends each branch.

menu.c
#include <stdio.h>

int main() {
    int choice;
    printf("1. Choi game moi\n");
    printf("2. Tai game cu\n");
    printf("3. Thoat\n");
    printf("Nhap lua chon: ");
    scanf("%d", &choice);

    switch(choice) {
        case 1:
            printf("Dang khoi tao game moi...\n");
            break;
        case 2:
            printf("Dang tai du lieu game cu...\n");
            break;
        case 3:
            printf("Dang thoat game...\n");
            break;
        default:
            printf("Lua chon khong hop le.\n");
    }

    return 0;
}
⚠️ Two important notes about switch-case
Fall-through: without a break at the end of a case, execution keeps running straight down into the following cases, one after another, until it meets a break or reaches the end of the switch. See the C switch statement on cppreference.com.

Type restrictions: unlike modern languages such as JavaScript, Python or Java, the controlling expression of a C switch only accepts integers or characters (int, char, enum). Floating point types (float, double) and strings are not supported at all.

C. Short-circuit evaluation

When evaluating a compound logical expression using && (AND) or || (OR), C applies short-circuit evaluation:

  • For A && B: if A evaluates to false (0), B is never evaluated at all, because the whole expression is certain to be false.
  • For A || B: if A evaluates to true (non-zero), B is never evaluated, because the whole expression is certain to be true.

A safety use: this technique lets you write notably safer code, for example if (ptr != NULL && *ptr == 10). If ptr is NULL, the second condition β€” dereferencing *ptr, which would crash the program β€” is never reached.

The trap: avoid putting functions with side effects on the right-hand side, for example if (x == 1 && saveData()). If x != 1, then saveData() never runs at all.

D. What the compiler really does: jump tables

Why is switch-case faster than a long if-else if chain once there are dozens of options? When the case constants sit close together (1, 2, 3, 4, 5 and so on), the C compiler does not emit a sequence of comparisons. Instead it builds a jump table β€” an array of code addresses in memory. At run time the CPU simply uses the value as an index into that array, reads the address of the matching branch and jumps straight there. That is \(O(1)\), far cheaper than performing \(n\) comparisons the way if-else would.

2. Loops

Loops run a block of code over and over without you having to copy it out repeatedly.

The for loop

Use it when you know exactly how many iterations you need. It has three parts: initialisation; loop condition; step expression.

loop_for.c
#include <stdio.h>

int main() {
    // In cac so tu 1 den 5
    for (int i = 1; i <= 5; i++) {
        printf("i = %d\n", i);
    }
    return 0;
}

The while and do-while loops

  • while: tests the condition first, and only runs the body if it holds.
  • do-while: runs the body at least once first, and only then tests the condition.
loop_while.c
#include <stdio.h>

int main() {
    int count = 5;
    while (count > 0) {
        printf("count = %d\n", count);
        count--;
    }

    int input;
    // do-while phu hop de kiem tra tinh hop le cua du lieu nhap vao
    do {
        printf("Nhap mot so lon hon 10: ");
        scanf("%d", &input);
    } while (input <= 10);

    printf("Thanh cong! So ban nhap la: %d\n", input);
    return 0;
}

Steering a loop from inside: break and continue

All three loop forms test their condition at the top or the bottom of each pass. But often you need to decide in the middle of the body: you found what you were looking for and want out, or you hit an invalid element and want to skip it and carry on. C has one statement for each of those.

  • break β€” leaves the loop entirely, abandoning every remaining iteration. You met it in switch-case above; inside a loop it does exactly the same kind of thing.
  • continue β€” skips the rest of the current pass and jumps to the next one. The loop keeps running; this particular round just stops early.
break_continue.c
#include <stdio.h>

int main() {
    int numbers[] = {4, 7, -2, 9, 0, 13, 6};
    int count = 7;

    // continue: skip the negative values, keep going
    printf("Positive values: ");
    for (int i = 0; i < count; i++) {
        if (numbers[i] < 0) {
            continue;          // skip THIS round only
        }
        printf("%d ", numbers[i]);
    }
    printf("\n");

    // break: stop at the first zero
    printf("Before the first zero: ");
    for (int i = 0; i < count; i++) {
        if (numbers[i] == 0) {
            break;             // leave the loop entirely
        }
        printf("%d ", numbers[i]);
    }
    printf("\n");

    return 0;
}

Run it and the two printed lines differ clearly: the first is 4 7 9 0 13 6 β€” the negative value was skipped but the loop still walked the whole array; the second is 4 7 -2 9 β€” it stopped dead at the zero and never looked at 13 or 6. Same array, same for structure, differing in exactly this: continue skips one pass, break skips all the rest.

πŸ•³οΈ continue inside while can hang
In a for loop the step expression (i++) still runs when you call continue, so the loop always advances. In a while loop, though, the counter is usually incremented at the end of the body β€” and if continue jumps over that line, the counter never moves and the program loops forever. When using continue inside while, increment the counter before the continue.

The goto statement and the resource cleanup pattern

goto is usually treated as a trap, because it makes it easy to produce spaghetti code whose flow nobody can follow. Yet in systems-level C β€” the Linux kernel source, for instance β€” goto is used very widely for cleaning up resources at a single exit point when an error happens partway through, avoiding repeated blocks of memory-freeing and file-closing code.

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

int process_data() {
    int *buffer1 = (int*) malloc(100 * sizeof(int));
    if (buffer1 == NULL) goto cleanup_none;

    int *buffer2 = (int*) malloc(200 * sizeof(int));
    if (buffer2 == NULL) goto cleanup_buf1;

    // Simulate an error happening midway through processing
    int error_occurred = 1;
    if (error_occurred) goto cleanup_all;

    // Everything succeeded
    free(buffer2);
    free(buffer1);
    return 0;

    // A single, central cleanup point
cleanup_all:
    printf("Error occurred, releasing both buffers...\n");
    free(buffer2);
cleanup_buf1:
    printf("Releasing buffer 1...\n");
    free(buffer1);
cleanup_none:
    return -1;
}

This is one of the very few cases where goto counts as clean code. The key point: every error branch jumps to the same place, and the labels are ordered in reverse of the allocation order β€” so each label releases exactly what had been allocated before it. Written with nested if statements instead, the freeing code is duplicated in every branch, and forgetting it in just one of them leaks memory.

3. A real algorithm: testing for a prime number

Let's combine if-else with a for loop to solve a classic problem: deciding whether a number n is prime.

A prime number is greater than 1 and divisible only by 1 and itself.

prime_check.c
#include <stdio.h>
#include <math.h>

int main() {
    int n;
    printf("Nhap vao mot so nguyen: ");
    scanf("%d", &n);

    if (n < 2) {
        printf("%d khong phai la so nguyen to\n", n);
        return 0;
    }

    int isPrime = 1; // 1 nghia la dung, 0 nghia la sai

    // Lap tu 2 den can bac hai cua n de kiem tra chia het
    // Su dung sqrt() trong math.h giup thuat toan chay nhanh hon rat nhieu
    for (int i = 2; i <= sqrt(n); i++) {
        if (n % i == 0) {
            isPrime = 0; // Co uoc so khac -> khong phai so nguyen to
            break;       // Ngat luong lap som vi khong can kiem tra them nua
        }
    }

    if (isPrime == 1) {
        printf("%d la so nguyen to\n", n);
    } else {
        printf("%d khong phai la so nguyen to\n", n);
    }

    return 0;
}

πŸ“₯ Download the sample source: prime_check.c

A small challenge for you

Write a program that prints every prime number below 100 using nested loops β€” the outer loop walking from 2 to 99, and the inner loop testing each candidate for primality.

πŸ“ Check your understanding β€” Lesson 4
What is the core difference between a while loop and a do-while loop in C?

Related lessons in this series

Lesson 5: Functions, recursion & variable scope Lesson 3: Operators, precedence & bitwise arithmetic in C Back to the C series roadmap (Vietnamese)