Introduction & Philosophy
The C programming language (1972, Dennis Ritchie) is small, powerful, and close to hardware. C favors explicitness and performance — you manage memory and program structure. Learn C to understand systems, compilers, and how higher-level languages work under the hood.
- Minimal runtime — the language exposes primitives.
- Undefined Behavior (UB) exists — learn it, avoid it.
- Portable if you respect implementation-defined behavior.
Toolchain & Build Pipeline
Stages: preprocessing → compilation (C → assembly) → assembly (→ object) → linking → executable.
GCC basics
gcc -Wall -Wextra -O2 -std=c11 -g -o myprog main.c util.c // -Wall -Wextra : enable warnings // -O2 : optimize // -g : include debug symbols
Makefile minimal example
CC=gcc CFLAGS=-std=c11 -Wall -Wextra -O2 SRCS=main.c util.c OBJS=$(SRCS:.c=.o) myprog: $(OBJS) $(CC) $(CFLAGS) -o $@ $(OBJS) .c.o: $(CC) $(CFLAGS) -c $< clean: rm -f *.o myprog
Static vs Shared linking
Static: embed libs (larger binary). Shared: smaller binary but depend on system libraries. Use -static or linker's flags.
Types & Variables
Built-in types: void, char, int, short, long, float, double. Use stdint.h for fixed-width types (int32_t, uint64_t).
Signed vs Unsigned & integer overflow
#include <stdint.h> int32_t a = INT32_MAX; // overflow is UB for signed uint32_t b = UINT32_MAX; b += 1; // wraps around (defined for unsigned)
Type qualifiers
const, volatile, restrict (C99) — learn how they affect aliasing and optimization.
Control Flow
Standard constructs: if/else, switch, for, while, do-while, break/continue, goto (sparingly).
Switch fall-through example (intentional)
switch(code) {
case 1:
// handle 1
break;
case 2:
case 3:
// 2 and 3 handled same
break;
default:
// default
}
Functions & Calling Conventions
Functions are declared and defined. Pass-by-value for scalars; pass pointers to mutate data. Watch stack usage for large locals.
Function pointer example
int add(int a,int b){return a+b;}
int apply(int (*fn)(int,int), int x, int y){ return fn(x,y); }
// usage: apply(add, 2, 3);
Inline & static functions
static at file scope hides symbol from linker; inline hints to compiler to inline (semantics subtle).
Pointers — the heart of C
Pointers hold addresses. Key: the type tells how to interpret memory at that address. Learn pointer arithmetic and levels (T*, T**, ...).
Basic pointer usage
int x = 42; int *p = &x; *p = 100; // writes to x int **pp = &p; // pointer-to-pointer
Pointer arithmetic & array equivalence
int a[5]; int *p = a; // &a[0] p[2] = 10; // *(p+2) p++; // move to next int
Common pitfalls
- Dereferencing NULL or uninitialized pointers → SIGSEGV
- Pointer to freed memory (use-after-free)
- Incorrect pointer arithmetic (type-size matters)
Arrays & Strings
Strings are char arrays terminated by '\0'. Use safe functions (strncpy, but mind no null). Prefer length-limited routines.
Safely read line (getline)
#include <stdio.h> char *line = NULL; size_t n = 0; ssize_t len = getline(&line, &n, stdin); // remember: free(line) when done
Common string bug — buffer overflow
Avoid gets(); use fgets() and check lengths.
Structs, Unions & Enums
Struct groups fields; union shares storage; enum gives named integers.
Struct example and padding
struct S {
char a;
int b; // compiler will insert padding between a and b typically
};
sizeof(struct S); // may be larger than sum of fields due to alignment
Bitfields
struct Flags {
unsigned int a:1;
unsigned int b:3;
unsigned int c:4;
};
Memory Model — Stack, Heap, BSS, Data, Text
Understanding memory layout is essential for debugging and performance.
Dynamic allocation
int *arr = malloc(n * sizeof *arr);
if(!arr){ perror("malloc"); exit(1); }
free(arr);
realloc usage
int *tmp = realloc(arr, newN * sizeof *arr);
if(!tmp){ /* handle OOM, arr still valid */ }
else arr = tmp;
Memory leak and valgrind
Use valgrind --leak-check=full ./myprog to find leaks and invalid reads/writes.
Preprocessor & Macros
Macros are expanded before compilation. Use inline functions where possible to avoid macro pitfalls.
Macro pitfalls (use parentheses!)
#define SQR(x) (x*x) // WRONG: use ((x)*(x)) int v = SQR(1+2); // expands to (1+2*1+2) = unexpected
Include guards
#ifndef MY_HEADER_H #define MY_HEADER_H // header content #endif
File I/O & Streams
Use fopen/fread/fwrite for binary; fgets/fputs for text. Check return values.
Binary file copy example
FILE *in = fopen("in.bin","rb");
FILE *out = fopen("out.bin","wb");
char buf[4096]; size_t n;
while((n=fread(buf,1,sizeof buf,in))>0) fwrite(buf,1,n,out);
fclose(in); fclose(out);
Debugging & Tools
Compile with -g and enable warnings. Use gdb, valgrind, and sanitizers (ASAN/UBSAN) during development.
gdb quick steps
gcc -g -O0 -o myprog main.c gdb ./myprog (gdb) break main (gdb) run (gdb) backtrace (gdb) print var
Use ASAN
gcc -fsanitize=address -g -O1 -o myprog main.c ./myprog // ASAN reports invalid accesses
System Programming Basics
POSIX API: fork, exec, pipes, signals, file descriptors. C is the language of system programming.
fork + exec example
pid_t pid = fork();
if(pid==0){
// child
execlp("ls","ls","-l",NULL);
_exit(1); // only if exec fails
} else if(pid>0){
// parent: wait
wait(NULL);
} else { perror("fork"); }
Pipes (parent -> child)
int fds[2];
pipe(fds);
if(fork()==0){
close(fds[1]); // child reads
// read from fds[0]
} else {
close(fds[0]); // parent writes
// write to fds[1]
}
Concurrency — pthreads & synchronization
POSIX threads (pthreads) provide threading in C. Protect shared data with mutexes or use atomic operations.
pthreads example (simple)
#include <pthread.h>
void *worker(void *arg){
// do work
return NULL;
}
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
pthread_join(t, NULL);
Mutex use
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&m); // critical section pthread_mutex_unlock(&m);
Optimization & Undefined Behavior
Undefined behavior lets compilers assume impossible conditions — optimizing with UB can create surprising results. Avoid UB; use sanitizers and strict aliasing rules.
Strict aliasing rule example
Don't access same memory as two incompatible types (use memcpy for type punning or use union carefully).
Compiler hints
if (__builtin_expect(x == 0, 0)) { /* unlikely path */ }
Advanced Topics
- Linker scripts and symbol visibility
- Inline assembly (gcc asm) — platform-specific
- Custom allocators & memory pools
- Network sockets (TCP/UDP) basics
- Interfacing C with other languages (FFI)
Useful Templates & Snippets
Fast integer input (scanf alternative)
int readInt(){
int sign=1, c=getchar_unlocked(), x=0;
while(c<=' ') c=getchar_unlocked();
if(c=='-'){ sign=-1; c=getchar_unlocked(); }
for(; c>='0' && c<='9'; c=getchar_unlocked()) x = x*10 + (c-'0');
return x*sign;
}
Safe strdup
char *safe_strdup(const char *s){
size_t n = strlen(s)+1;
char *t = malloc(n);
if(t) memcpy(t, s, n);
return t;
}
Practice Problems & Interview Questions
- Implement strlen, strcpy, strcmp
- Write your own malloc using sbrk (educational)
- Detect and fix memory leaks in given code
- Implement ring buffer
- Implement producer-consumer with pthreads
Interview strategy for C
- Explain memory layout and ownership clearly.
- Point out edge cases (null pointers, zero sizes).
- Show test cases and complexity.
References & Further Reading
- "The C Programming Language" — Kernighan & Ritchie
- ISO C Standard (C11/C17) — for definitive behavior
- GCC manual, glibc manual, POSIX docs