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

  1. Implement strlen, strcpy, strcmp
  2. Write your own malloc using sbrk (educational)
  3. Detect and fix memory leaks in given code
  4. Implement ring buffer
  5. 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