Skip to content

C Values, Declarations, and Control Flow

C's everyday syntax will look familiar, but its values are closer to machine representations than Java, C#, or Swift normally expose. A declaration chooses a type whose range and conversion rules affect every later expression.

A complete small program

c
#include <stdio.h>

static double mean(const int values[], size_t count) {
    long total = 0;
    for (size_t i = 0; i < count; ++i) {
        total += values[i];
    }
    return count == 0 ? 0.0 : (double)total / (double)count;
}

int main(void) {
    const int samples[] = {4, 7, 10};
    printf("%.1f\n", mean(samples, 3));
    return 0;
}

main returns an integer status to the host: zero conventionally means success. void in main(void) explicitly says that the C function takes no parameters. An empty parameter list in an old-style C declaration does not provide the same prototype guarantee.

const int samples[] creates an array whose elements are not modified through that name. C's const is not transitive immutability and does not create a compile-time constant in every context.

The example's three small values cannot overflow its long accumulator. A general-purpose mean must define an accepted input range or use checked accumulation before adding each value; widening from int to long alone is not a proof against overflow on every data model or input count.

Declarations introduce typed objects

c
int attempts = 3;
double ratio = 0.75;
char grade = 'A';
_Bool ready = 1;

Including <stdbool.h> in C17 provides the aliases bool, true, and false. C23 makes bool, true, and false language keywords while retaining compatibility provisions.

Automatic local objects are not initialized unless an initializer is provided. Reading an indeterminate value can be undefined behavior:

c
int count;
/* printf("%d\n", count);  invalid: count has no value to read */

Swift and managed-language definite-initialization checks may prevent similar mistakes. C expects the program and its diagnostics policy to enforce them.

Expressions may convert their operands

Integer division discards the fractional part:

c
#include <stdio.h>

int main(void) {
    printf("%d\n", 7 / 2);
    printf("%.1f\n", 7.0 / 2.0);
    return 0;
}

An explicit cast in the opening example ensures floating-point division. Casts should expose a deliberate representation change, not silence a warning without understanding it.

Many operands smaller than int undergo integer promotion before arithmetic. Mixed signed and unsigned arithmetic can convert a negative operand to a large unsigned value. Article 6 develops these rules.

Control flow is familiar, with C-specific conditions

Zero is false and any nonzero scalar value is true. Conditions need not have a dedicated Boolean type:

c
if (count > 0) {
    process(count);
} else {
    report_empty();
}

switch accepts an integer or enumeration expression. Cases fall through unless control leaves with break, return, or another jump. Intentional fallthrough should be visibly documented or annotated where the selected standard/compiler supports it.

for, while, and do loops behave conventionally. Prefer a size_t index for object sizes and array counts, while remaining careful about backwards loops because unsigned values cannot become negative.

c
for (size_t i = count; i-- > 0;) {
    visit(values[i]);
}

This condition tests the old value before decrementing. Clear forward iteration is preferable when order is not important.

Side effects need sequencing

An expression can both compute a value and modify state. Do not modify an object more than once where the language does not sequence those modifications:

c
/* i = i++ + 1;  undefined behavior */

Function argument evaluation order is generally not fixed by C in the way readers may expect. If order matters, use separate statements:

c
int left = read_left();
int right = read_right();
combine(left, right);

Short-circuit operators do impose useful sequencing: left && right evaluates right only if left is nonzero; left || right evaluates it only if left is zero.

Declarations should live near established values

C99 and later permit declarations throughout a block. Modern C does not require moving every local to the top of a function. Narrow scope reduces lifetime and makes initialization visible.

c
for (size_t i = 0; i < count; ++i) {
    const int value = values[i];
    consume(value);
}

Use braces even around short controlled statements in maintained code. The compiler does not require them, but they make later edits safer.

Optional prompts

Predict: What does 5 / 2 produce when assigned to a double?

Answer: The integer division happens first and produces 2; conversion to double then produces 2.0. Convert an operand before division to retain a fractional result.

Explain: Why is an uninitialized automatic int not reliably zero?

Answer: C does not initialize ordinary automatic storage by default. Static-storage objects are zero-initialized, but reading an indeterminate automatic value is not a portable way to observe leftover memory.

Further reference