§ 01 — Fundamentals
Introduction & Overview
C was created at Bell Labs by Dennis Ritchie in 1972 to write the Unix operating system. Nearly every modern programming language — Python, Java, C++, Swift, JavaScript, Rust — inherits its syntax and core concepts from C. If you already know any of these, C will feel simultaneously familiar and surprisingly manual.
What Makes C Different
Coming from a higher-level language, these are the biggest mental-model shifts:
| Concept | Higher-level languages | C |
| Memory | Garbage-collected automatically | You allocate and free manually |
| Strings | First-class String type | Arrays of char ending in '\0' |
| Type safety | Runtime or compile errors | Many errors are silent undefined behavior |
| Arrays | Know their length (.length) | Just a pointer — you track the size yourself |
| OOP / Classes | Built-in | Not in C — use structs + function pointers |
| Exceptions | throw / catch | Return codes or errno |
| Execution | Interpreted or JIT-compiled | Compiled directly to machine code |
Hello, World!
hello.c
#include <stdio.h> /* include the standard I/O header */
int main(void) {
printf("Hello, World!\n"); /* \n = newline character */
return 0; /* return 0 to OS = success */
}
$ gcc -Wall -Wextra -std=c11 -o hello hello.c
$ ./hello
Hello, World!
ℹRecommended flags-Wall -Wextra enable all compiler warnings — always use them during development. -std=c11 targets the C11 standard, a safe modern choice.
The Compilation Pipeline
| Stage | Tool | Input → Output |
| Preprocessing | cpp | Source .c → expanded source (macros expanded, includes inserted) |
| Compilation | cc1 | Expanded source → assembly .s |
| Assembly | as | Assembly → object file .o |
| Linking | ld | Object files + libraries → executable |
§ 05
Functions
Syntax & Prototypes
function definition
/* return_type function_name ( param_type param, ... ) { body } */
int add(int a, int b) {
return a + b;
}
void greet(char *name) { /* void = no return value */
printf("Hello, %s!\n", name);
}
/* Prototypes at top of file (or in a .h header) */
int add(int a, int b);
void greet(char *name);
Pass by Value vs Pass by Pointer
C is strictly pass-by-value. To let a function modify the caller's variable, pass its address (a pointer).
pass-by-value vs pass-by-pointer
/* Pass by value: caller's x is NOT modified */
void doubleIt_val(int x) { x *= 2; }
/* Pass by pointer: caller's x IS modified */
void doubleIt_ptr(int *x) { *x *= 2; }
int n = 5;
doubleIt_val(n); /* n is still 5 */
doubleIt_ptr(&n); /* n is now 10 — & gives the address */
Function Pointers
function pointers
/* Declare: return_type (*name)(param_types) */
int (*op)(int, int);
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
op = add; printf("%d\n", op(3,4)); /* 7 */
op = mul; printf("%d\n", op(3,4)); /* 12 */
/* Use typedef for cleaner syntax */
typedef int (*BinaryOp)(int, int);
BinaryOp ops[] = { add, mul }; /* array of function pointers */
Variadic Functions
variadic (variable argument count)
#include <stdarg.h>
double average(int count, ...) {
va_list args;
va_start(args, count);
double sum = 0;
for (int i = 0; i < count; i++)
sum += va_arg(args, double);
va_end(args);
return sum / count;
}
/* average(3, 1.0, 2.0, 3.0) == 2.0 */
§ 06 — Intermediate
Pointers
A pointer is a variable that holds a memory address. Pointers are the core mechanism behind dynamic memory, passing data by reference, and working with arrays and strings in C.
Pointer Basics
pointer fundamentals
int x = 42;
int *p = &x; /* p holds x's address (& = address-of operator) */
printf("%d\n", x); /* 42 — value of x */
printf("%p\n", p); /* 0x7fff... — address */
printf("%d\n", *p); /* 42 — * dereferences: value at that address */
*p = 100; /* write through pointer — x is now 100 */
/* Pointer arithmetic */
int arr[] = {10, 20, 30};
int *q = arr; /* points to arr[0] */
printf("%d\n", *q); /* 10 */
printf("%d\n", *(q+1)); /* 20 — advances by sizeof(int) bytes */
q++; /* q now points to arr[1] */
| Operation | Syntax | Meaning |
| Address-of | &var | Get the memory address of var |
| Dereference | *ptr | Read or write the value at the address ptr holds |
| Arrow | ptr->field | Dereference ptr and access struct field (= (*ptr).field) |
| Pointer arithmetic | ptr + n | Address + n × sizeof(*ptr) bytes |
| Pointer difference | p2 - p1 | Number of elements between two pointers |
Pointers to Pointers
double pointer
int x = 5;
int *p = &x; /* pointer to int */
int **pp = &p; /* pointer to pointer to int */
printf("%d\n", **pp); /* 5 — double dereference */
/* Used when a function needs to change what a pointer points to */
void reallocRoster(Student **roster, int newCap) {
*roster = realloc(*roster, newCap * sizeof(Student));
}
⚠Never dereference NULL or uninitialized pointers.Always initialise: int *p = NULL; and check if (p != NULL) before dereferencing. Dereferencing NULL causes a segmentation fault (crash).
const and Pointers
const pointer variations
int x = 10;
const int *p1 = &x; /* pointer to const int: can't change *p1 */
int *const p2 = &x; /* const pointer: can't change p2 itself */
const int *const p3 = &x; /* neither can change */
/* In function params, const signals "I won't modify your data" */
void printName(const char *name) { printf("%s\n", name); }
§ 07
Arrays & Strings
Arrays
arrays
/* Fixed-size arrays — size must be a compile-time constant */
int scores[5]; /* uninitialized — garbage values */
int scores[5] = {90, 85, 78, 92, 88}; /* initialized */
int scores[] = {90, 85, 78}; /* size inferred: 3 */
int zeros[100] = {0}; /* all 100 elements set to 0 */
/* Access: zero-indexed */
scores[0] = 100; /* first element */
scores[4] = 75; /* last element — index = size - 1 */
/* Size of array (only works on the array itself, not a pointer to it) */
int n = sizeof(scores) / sizeof(scores[0]); /* = 5 */
/* 2D array */
int matrix[3][4]; /* 3 rows, 4 columns */
matrix[1][2] = 42; /* row 1, column 2 */
⚠No bounds checking! Accessing scores[10] on a 5-element array is undefined behavior — the program may crash, produce wrong results, or silently corrupt memory. C never warns you about this at runtime.
Strings
C strings are char arrays terminated by a null character '\0'. There is no built-in String type.
c strings
char name[20] = "Alice"; /* A l i c e \0 ? ? ? ... (20 bytes total) */
char *lit = "Hello"; /* string literal — stored in read-only memory! */
/* Read string from user — fgets is safe, gets() is DANGEROUS (never use it) */
fgets(name, sizeof(name), stdin); /* reads up to 19 chars + '\0' */
name[strcspn(name, "\n")] = '\0'; /* strip trailing newline from fgets */
| Function | Header | Description |
strlen(s) | <string.h> | Length of s (not counting '\0') |
strcpy(dst, src) | <string.h> | Copy src to dst — dst must be large enough! |
strncpy(dst, src, n) | <string.h> | Copy at most n chars — safer |
strcat(dst, src) | <string.h> | Append src onto dst |
strcmp(a, b) | <string.h> | 0=equal, <0 a<b, >0 a>b — never use == |
strstr(hay, needle) | <string.h> | Pointer to first occurrence of needle, or NULL |
sprintf(buf, fmt, …) | <stdio.h> | Format into a buffer |
snprintf(buf, n, fmt, …) | <stdio.h> | Safer: write at most n bytes |
atoi(s) | <stdlib.h> | String to int (no error checking) |
strtol(s, &end, base) | <stdlib.h> | String to long with error detection (prefer over atoi) |
§ 14
Full Annotated Source
Part 1 — Headers, Macros, Types, and Prototypes
student_tracker.c — part 1 of 5
/*
* CONCEPT 1 — #include & #define (Preprocessor)
* These run BEFORE compilation. #include inserts a header file.
* #define creates a constant via text substitution (no type checking).
*/
#include <stdio.h> /* printf, scanf, fopen, fclose, FILE */
#include <stdlib.h> /* malloc, realloc, free, exit */
#include <string.h> /* strcpy, strcmp, strlen, strcspn */
#include <ctype.h> /* toupper (converts char to upper) */
#define MAX_NAME 64
#define SAVE_FILE "students.dat"
#define PASSING_SCORE 60.0
/*
* CONCEPT 2 — enum (Named Integer Constants)
* Assigns readable names to integers. ADD=1, VIEW=2, etc.
* Use enums instead of bare magic numbers.
* typedef lets us write 'MenuChoice' instead of 'enum MenuChoice'.
*/
typedef enum {
ADD = 1, VIEW = 2, SEARCH = 3,
SAVE = 4, LOAD = 5, STATS = 6, QUIT = 7
} MenuChoice;
/*
* CONCEPT 3 — struct (Custom Composite Data Type)
* Groups related fields into one named type.
* char name[MAX_NAME] is a fixed-length string (char array).
*/
typedef struct {
char name[MAX_NAME]; /* fixed-size char array = C string */
int id;
double score;
char grade;
} Student;
/*
* CONCEPT 4 — Function Prototypes
* In C, a function must be declared before it is used.
* These prototypes tell the compiler the signature; the full
* definition appears after main().
*
* 'const Student *s' = pointer to read-only Student (we won't modify it)
* 'Student **roster' = pointer-to-pointer (needed when function may
* change what the pointer itself points to)
*/
char calculateGrade(double score);
void printStudent(const Student *s);
void addStudent(Student **roster, int *count, int *capacity);
void viewAll(const Student *roster, int count);
void searchByName(const Student *roster, int count);
void showStats(const Student *roster, int count);
void saveToFile(const Student *roster, int count);
void loadFromFile(Student **roster, int *count, int *capacity);
void printMenu(void);
void clearInputBuffer(void);
Part 2 — main() and the Menu Loop
student_tracker.c — part 2 of 5
/*
* CONCEPT 9 — Dynamic Memory
* malloc(n * sizeof(T)) allocates n×sizeof(T) bytes on the heap.
* Returns void* (raw pointer) or NULL on failure.
* We start with capacity=4 and grow with realloc() when full.
*/
int main(void) {
int capacity = 4;
int count = 0;
Student *roster = malloc(capacity * sizeof(Student));
if (roster == NULL) { /* ALWAYS check malloc's return */
fprintf(stderr, "Fatal: out of memory\n");
return 1;
}
printf("=== C Language Tour: Grade Tracker ===\n\n");
/*
* CONCEPT 8 — do-while loop
* Executes the body AT LEAST ONCE, then checks the condition.
* Perfect for menu loops where you always show the menu first.
*/
int running = 1;
do {
printMenu();
/*
* scanf("%d", &choice) reads one integer.
* The & gives scanf the ADDRESS of 'choice' so it can
* write into it. Without &, scanf would get a copy.
* scanf returns the number of items successfully read.
*/
int choice;
if (scanf("%d", &choice) != 1) {
clearInputBuffer();
printf(" Invalid input.\n\n");
continue;
}
clearInputBuffer();
/*
* CONCEPT 8 — switch
* Jumps directly to the matching case.
* 'break' is REQUIRED — without it execution falls through
* to the next case (usually a bug!).
*/
switch (choice) {
case ADD: addStudent(&roster, &count, &capacity); break;
case VIEW: viewAll(roster, count); break;
case SEARCH: searchByName(roster, count); break;
case SAVE: saveToFile(roster, count); break;
case LOAD: loadFromFile(&roster, &count, &capacity); break;
case STATS: showStats(roster, count); break;
case QUIT:
printf(" Goodbye!\n\n");
running = 0;
break;
default:
printf(" Unknown option.\n\n");
}
} while (running);
free(roster); /* every malloc needs a matching free */
roster = NULL; /* prevent use-after-free */
return 0;
}
Part 3 — Adding & Displaying Students
student_tracker.c — part 3 of 5
/*
* calculateGrade — if/else chain, comparison operators
*/
char calculateGrade(double score) {
if (score >= 90.0) return 'A';
else if (score >= 80.0) return 'B';
else if (score >= 70.0) return 'C';
else if (score >= 60.0) return 'D';
else return 'F';
}
/*
* printStudent — pointer parameter, -> operator, ternary
*
* 'const Student *s': we CAN'T modify *s through this pointer.
* s->name is shorthand for (*s).name
* The ternary (score >= PASSING ? "PASS" : "FAIL") picks a string.
*/
void printStudent(const Student *s) {
printf(" %-20s ID:%04d Score:%6.2f Grade:%c [%s]\n",
s->name, s->id, s->score, s->grade,
s->score >= PASSING_SCORE ? "PASS" : "FAIL");
}
/*
* addStudent — realloc, fgets, input validation with while loop
*
* Takes Student **roster because the function may need to change
* what roster POINTS TO after a realloc (which can move the block).
* A pointer-to-pointer lets us update the caller's pointer.
*/
void addStudent(Student **roster, int *count, int *capacity) {
if (*count >= *capacity) {
int newCap = *capacity * 2;
/* CRITICAL: use a temp pointer — if realloc fails,
* the original pointer is still valid.
* Writing *roster = realloc(*roster, ...) would
* overwrite the pointer on failure → memory leak! */
Student *tmp = realloc(*roster, newCap * sizeof(Student));
if (tmp == NULL) { printf(" Memory error.\n"); return; }
*roster = tmp;
*capacity = newCap;
printf(" (Array grown to capacity %d)\n", newCap);
}
Student *s = &(*roster)[*count]; /* pointer to next empty slot */
printf("\n Enter student name: ");
/* fgets(buf, size, stdin): reads up to size-1 chars safely.
* NEVER use gets() — no bounds checking, buffer overflow risk! */
if (fgets(s->name, MAX_NAME, stdin) == NULL) return;
s->name[strcspn(s->name, "\n")] = '\0'; /* strip trailing \n */
if (strlen(s->name) == 0) { printf(" Name cannot be empty.\n\n"); return; }
s->id = *count + 1001; /* auto-assign ID */
/* Input validation loop — keep asking until valid */
while (1) {
printf(" Enter score (0-100): ");
if (scanf("%lf", &s->score) == 1
&& s->score >= 0.0 && s->score <= 100.0) {
clearInputBuffer();
break;
}
clearInputBuffer();
printf(" Invalid. Enter a number 0-100.\n");
}
s->grade = calculateGrade(s->score);
(*count)++;
printf("\n Added: ");
printStudent(s);
printf("\n");
}
Part 4 — View, Search, and Statistics
student_tracker.c — part 4 of 5
/*
* viewAll — for loop, array-as-pointer, guard clause
*/
void viewAll(const Student *roster, int count) {
if (count == 0) { printf("\n No students yet.\n\n"); return; }
printf("\n %-20s %-8s %-10s %-7s %s\n",
"Name", "ID", "Score", "Grade", "Result");
printf(" %s\n", "------------------------------------------------------");
/* for loop: init; condition; update */
for (int i = 0; i < count; i++) {
printStudent(&roster[i]); /* &roster[i] = pointer to element i */
}
printf("\n");
}
/*
* searchByName — strcmp, boolean flag variable
* NOTE: We CANNOT use == to compare C strings!
* == compares pointer addresses, not string contents.
* strcmp(a, b) returns 0 when strings are equal.
*/
void searchByName(const Student *roster, int count) {
if (count == 0) { printf("\n No students.\n\n"); return; }
char query[MAX_NAME];
printf("\n Enter name to search: ");
if (fgets(query, MAX_NAME, stdin) == NULL) return;
query[strcspn(query, "\n")] = '\0';
int found = 0; /* boolean flag: 0=false, 1=true */
for (int i = 0; i < count; i++) {
if (strcmp(roster[i].name, query) == 0) {
if (!found) printf("\n Results:\n");
printStudent(&roster[i]);
found = 1;
}
}
if (!found) printf("\n No student named \"%s\".\n", query);
printf("\n");
}
/*
* showStats — accumulator pattern, min/max, cast, ternary
*
* (double)pass / count * 100.0
* The cast (double)pass converts int→double BEFORE division,
* preventing integer division which would give 0 for pass < count.
*/
void showStats(const Student *roster, int count) {
if (count == 0) { printf("\n No students.\n\n"); return; }
double sum = 0.0, high = roster[0].score, low = roster[0].score;
int pass = 0, fail = 0;
for (int i = 0; i < count; i++) {
sum += roster[i].score;
if (roster[i].score > high) high = roster[i].score;
if (roster[i].score < low) low = roster[i].score;
/* ternary increments one of two counters */
(roster[i].score >= PASSING_SCORE) ? pass++ : fail++;
}
printf("\n Students : %d\n", count);
printf(" Average : %.2f\n", sum / count);
printf(" Highest : %.2f\n", high);
printf(" Lowest : %.2f\n", low);
printf(" Pass Rate : %.1f%%\n\n",
count > 0 ? (double)pass / count * 100.0 : 0.0);
}
Part 5 — File I/O and Utilities
student_tracker.c — part 5 of 5
/*
* saveToFile — FILE*, fopen("w"), fprintf, fclose
* fopen returns FILE* or NULL. Always check.
* fprintf works like printf but writes to fp.
* fclose flushes the buffer and closes the file descriptor.
*/
void saveToFile(const Student *roster, int count) {
FILE *fp = fopen(SAVE_FILE, "w");
if (fp == NULL) { perror("fopen"); return; }
fprintf(fp, "%d\n", count);
for (int i = 0; i < count; i++)
fprintf(fp, "%s\n%d\n%.2f\n%c\n",
roster[i].name, roster[i].id,
roster[i].score, roster[i].grade);
fclose(fp);
printf("\n Saved %d student(s) to \"%s\".\n\n", count, SAVE_FILE);
}
/*
* loadFromFile — fopen("r"), fscanf, fgets, realloc
*/
void loadFromFile(Student **roster, int *count, int *capacity) {
FILE *fp = fopen(SAVE_FILE, "r");
if (fp == NULL) { printf("\n No save file found.\n\n"); return; }
int newCount;
if (fscanf(fp, "%d\n", &newCount) != 1) {
printf("\n Corrupt save file.\n\n");
fclose(fp); return;
}
Student *tmp = realloc(*roster, newCount * sizeof(Student));
if (tmp == NULL && newCount > 0) {
printf("\n Memory error.\n\n");
fclose(fp); return;
}
*roster = tmp; *capacity = newCount;
for (int i = 0; i < newCount; i++) {
if (fgets((*roster)[i].name, MAX_NAME, fp) == NULL) break;
(*roster)[i].name[strcspn((*roster)[i].name, "\n")] = '\0';
fscanf(fp, "%d\n", &(*roster)[i].id);
fscanf(fp, "%lf\n", &(*roster)[i].score);
fscanf(fp, " %c\n", &(*roster)[i].grade); /* space skips whitespace */
}
fclose(fp);
*count = newCount;
printf("\n Loaded %d student(s) from \"%s\".\n\n", newCount, SAVE_FILE);
}
void printMenu(void) {
printf("--- MENU ---\n");
printf("%d. Add %d. View %d. Search %d. Save %d. Load %d. Stats %d. Quit\n",
ADD, VIEW, SEARCH, SAVE, LOAD, STATS, QUIT);
printf("Choice: ");
}
/*
* clearInputBuffer — consume leftover chars in stdin
* After scanf reads a number, '\n' stays in the buffer.
* The next fgets would read that '\n' and return immediately.
* This loop discards everything up to and including '\n'.
*/
void clearInputBuffer(void) {
int c;
while ((c = getchar()) != '\n' && c != EOF)
; /* empty body — work is done in the condition */
}