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_tfor 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.
