1. Introduction to C

An introduction to C

C is still everywhere for one reason: it maps cleanly to the machine. It gives you direct access to memory, predictable performance, and a runtime model you can actually understand.

It also gives you enough rope to build reliable infrastructure—or to ship undefined behavior into production and spend the next six months chasing ghosts.

Most “intro to C” material starts with Hello, world. That’s fine, but it teaches nothing about what makes C uniquely powerful and uniquely dangerous. If you’re learning C today, it’s usually because you’re close to systems: Linux, networking, embedded, performance tooling, or security work.


Getting started

To write C safely, you need three things immediately:

  • A strict compiler configuration
  • A habit of checking return values
  • A refusal to rely on implicit assumptions

Compile loudly and fail fast:

cc -std=c17 -Wall -Wextra -Wpedantic -Werror -O2 -g file.c -o prog

During development, enable sanitizers:

cc -std=c17 -Wall -Wextra -Wpedantic -Werror -O1 -g \
  -fsanitize=address,undefined -fno-omit-frame-pointer file.c -o prog

These tools won’t save bad designs, but they dramatically shorten the feedback loop.


Variables and arithmetic expressions

Many security bugs begin with silent integer overflow and implicit type conversion.

Prefer types that reflect intent:

  • size_t for sizes and indexes
  • fixed-width integers (uint32_t, uint64_t) when layout matters
  • avoid mixing signed and unsigned values

A classic overflow bug:

size_t bytes = count * sizeof(int);
int *p = malloc(bytes);

If count is attacker-controlled, this can wrap and lead to heap corruption.

A safer pattern validates first:

int safe_mul_size(size_t a, size_t b, size_t *out) {
    if (a == 0 || b == 0) { *out = 0; return 1; }
    if (a > SIZE_MAX / b) return 0;
    *out = a * b;
    return 1;
}

Arithmetic in C is never “just math.” It is memory safety.


The for statement

Most buffer overruns are off-by-one errors.

A safe baseline:

for (size_t i = 0; i < len; i++) {
    /* operate on buf[i] */
}

If you don’t have len, you don’t have safety.


Symbolic constants

Use symbolic constants to make invariants explicit.

#define MAX_LINE 1024

or

static const size_t max_retries = 5;

Security improves when constraints are named instead of duplicated.


Character input and output

Always read characters into an int, not a char.

int c;
while ((c = getchar()) != EOF) {
    /* use c */
}

This allows you to correctly detect EOF and errors. Ignoring return values is how programs become unreliable.


File copying

File copying forces correct I/O habits:

int c;
while ((c = getchar()) != EOF) {
    if (putchar(c) == EOF) return 1;
}
if (ferror(stdin)) return 1;

This is minimal, but correct.


Character counting

Use wide counters and don’t assume small inputs:

uint64_t count = 0;
int c;

while ((c = getchar()) != EOF) {
    count++;
}

Portability and correctness matter early.


Line counting

Define what a “line” is. Most tools count \n.

if (c == '\n') lines++;

Edge cases should be explicit, not accidental.


Word counting

Word counting introduces state machines:

int in_word = 0;

if (isspace((unsigned char)c)) {
    in_word = 0;
} else {
    if (!in_word) words++;
    in_word = 1;
}

Always cast before calling ctype functions to avoid undefined behavior.


Arrays

Arrays do not know their own length.

Always pass (pointer, length) together:

void zero_buf(unsigned char *buf, size_t len) {
    for (size_t i = 0; i < len; i++) buf[i] = 0;
}

If you rely on sizeof inside a function, you are probably wrong.


Functions

Functions are security boundaries.

Design them to fail safely:

  • return status codes
  • validate inputs
  • document ownership rules

Example:

int parse_int(const char *s, int *out);

Clear contracts reduce entire classes of bugs.


Arguments and call by value

C is call-by-value. Pointers are values too.

void set_flag(int *flag) {
    *flag = 1;
}

Any pointer argument is a trust boundary. Validate or document assumptions.


Character arrays

C strings are byte arrays terminated by \0.

Avoid unsafe functions. Prefer bounded input:

fgets(buf, len, stdin);

Then reason about truncation explicitly.


External variables and scope

Global state increases risk.

Prefer local scope, static linkage, and explicit interfaces. Shared mutable state should be rare and controlled.


Final thoughts

C gives you precision and power, not safety rails.

If you treat input as hostile, arithmetic as dangerous, and bounds as mandatory, C becomes a sharp but reliable tool. If you don’t, it will fail in ways that are subtle, delayed, and expensive.

That tradeoff is why C still matters.

profile picture

SilentOxygen

Learning stuff.