Introduction to C
What is C?
C is a general-purpose, procedural programming language developed in 1972 by Dennis Ritchie at Bell Labs. It is one of the most influential languages ever created — the Linux kernel, Python interpreter, and countless embedded systems are written in C.
- 1972 – C created at Bell Labs by Dennis Ritchie.
- 1978 – "The C Programming Language" (K&R) published.
- 1989 – ANSI C (C89) standardised. Also called C90.
- 1999 – C99 added new features (inline functions,
stdint.h, etc.). - 2011 – C11 brought threading and improved Unicode support.
Setting Up Your Environment
You need a C compiler. The most popular choice is GCC (GNU Compiler Collection).
- Linux/macOS – install with
sudo apt install gccor Xcode Command Line Tools. - Windows – install MinGW-w64 or use WSL.
- Online – use godbolt.org or onlinegdb.com to run C in the browser.
Your First C Program
#include <stdio.h> int main() { printf("Hello, World!\n"); return 0; }
Compiling and running
# Compile gcc hello.c -o hello # Run ./hello
Hello, World!
Anatomy of the Program
#include <stdio.h>– includes the Standard I/O library (needed forprintf).int main()– every C program must have amainfunction; execution starts here.printf()– prints text to the terminal.\nis a newline character.return 0;– returns exit code 0 (success) to the operating system.
stdio.h) declare functions from the C Standard Library. You include them
so the compiler knows the function signatures before you call them.
Create hello.c, compile it with GCC, and run it. Then modify the program to print your
name and the current year on separate lines using two printf() calls.
Variables & Data Types
Fundamental Data Types
| Type | Size | Range (typical) | Format specifier |
|---|---|---|---|
char |
1 byte | -128 to 127 | %c |
int |
4 bytes | -2,147,483,648 to 2,147,483,647 | %d |
float |
4 bytes | ~6-7 decimal digits precision | %f |
double |
8 bytes | ~15-16 decimal digits precision | %lf |
long |
8 bytes | very large integers | %ld |
unsigned int |
4 bytes | 0 to 4,294,967,295 | %u |
Declaring and Initialising Variables
#include <stdio.h> int main() { /* Integer */ int age = 25; /* Floating point */ float pi = 3.14159f; double precise_pi = 3.14159265358979; /* Character */ char grade = 'A'; /* Print them */ printf("Age: %d\n", age); printf("Pi (float): %.2f\n", pi); printf("Pi (double): %.10lf\n", precise_pi); printf("Grade: %c\n", grade); return 0; }
Age: 25 Pi (float): 3.14 Pi (double): 3.1415926536 Grade: A
Constants
Use const to declare a variable whose value cannot change after assignment.
const double GRAVITY = 9.81; const int MAX_SIZE = 100; /* GRAVITY = 10.0; ← compile error! */
Reading User Input with scanf()
#include <stdio.h> int main() { int num; printf("Enter a number: "); scanf("%d", &num); /* & gives scanf the address of num */ printf("You entered: %d\n", num); return 0; }
& operator gives scanf the memory address of the variable so
it can write the value there. Forgetting it causes undefined behaviour and crashes.
Write a program that asks the user for their name initial (a single char), their age (int), and their
GPA (float). Print a formatted summary like:
Student: A | Age: 20 | GPA: 3.75
Operators & Expressions
Arithmetic Operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ |
Addition | 5 + 3 |
8 |
- |
Subtraction | 5 - 3 |
2 |
* |
Multiplication | 5 * 3 |
15 |
/ |
Division | 7 / 2 |
3 (integer division!) |
% |
Modulus (remainder) | 7 % 2 |
1 |
Relational & Logical Operators
| Type | Operators | Example |
|---|---|---|
| Relational | == != < > <= >= |
a > b |
| Logical AND | && |
a > 0 && b > 0 |
| Logical OR | || |
a == 0 || b == 0 |
| Logical NOT | ! |
!(a == b) |
Increment, Decrement & Assignment
int x = 5; x++; // x = 6 (post-increment) ++x; // x = 7 (pre-increment) x--; // x = 6 (post-decrement) x += 3; // x = 9 (same as x = x + 3) x *= 2; // x = 18 x %= 5; // x = 3
Type Casting
int a = 7, b = 2; double result; result = a / b; // 3.0 ← integer division result = (double)a / b; // 3.5 ← cast to double first printf("%.1lf\n", result); // 3.5
7/2 is 3, not
3.5. Cast one operand to double or float when you need decimal
precision.
Write a program that reads two integers from the user and prints: their sum, difference, product,
integer quotient, remainder, and their exact division result as a double.
Control Flow
if / else if / else
#include <stdio.h> int main() { int score; printf("Enter score: "); scanf("%d", &score); if (score >= 90) { printf("Grade: A\n"); } else if (score >= 70) { printf("Grade: B\n"); } else if (score >= 50) { printf("Grade: C\n"); } else { printf("Grade: F\n"); } return 0; }
switch Statement
char day = 'M'; switch (day) { case 'M': printf("Monday\n"); break; case 'T': printf("Tuesday\n"); break; case 'W': printf("Wednesday\n"); break; default: printf("Other day\n"); break; }
Loops
for loop
for (int i = 1; i <= 5; i++) { printf("%d\n", i); }
while loop
int n = 1; while (n <= 100) { n *= 2; } printf("First power of 2 above 100: %d\n", n);
do-while loop
int choice; do { printf("Enter a positive number: "); scanf("%d", &choice); } while (choice <= 0);
break & continue
break– exits the loop immediately.continue– skips the rest of the current iteration and jumps to the next.
Write a program that prints a multiplication table for a number entered by the user (1 through 12).
Then write a second program that uses a do-while loop to keep asking for a password
until the user types 1234.
Functions
Defining and Calling Functions
Functions let you organise code into reusable blocks. In C you must declare a function before calling it (either via a prototype or by defining it first).
#include <stdio.h> /* Function prototype */ int add(int a, int b); void greet(const char *name); int main() { int result = add(3, 7); printf("3 + 7 = %d\n", result); greet("Alice"); return 0; } /* Function definitions */ int add(int a, int b) { return a + b; } void greet(const char *name) { printf("Hello, %s!\n", name); }
Recursion
A function that calls itself. The classic example: factorial.
long factorial(int n) { if (n <= 1) return 1; // base case return n * factorial(n - 1); // recursive call } // factorial(5) = 5 * 4 * 3 * 2 * 1 = 120
Pass by Value vs Pass by Reference
C is pass-by-value: functions receive a copy. To modify the original, pass a pointer.
/* Does NOT modify x — works on a copy */ void tryDouble(int n) { n *= 2; } /* DOES modify x — passes address */ void realDouble(int *n) { *n *= 2; } int main() { int x = 5; tryDouble(x); // x is still 5 realDouble(&x); // x is now 10 return 0; }
Write three functions: isPrime(int n) that returns 1 if n is prime,
fibonacci(int n) that returns the nth Fibonacci number recursively, and
swap(int *a, int *b) that swaps two integers. Test each in main().
Arrays & Strings
Arrays
An array stores multiple values of the same type in contiguous memory locations, accessed by index starting at 0.
#include <stdio.h> int main() { /* Declaration and initialisation */ int scores[5] = {85, 92, 78, 95, 88}; /* Access by index */ printf("First: %d, Last: %d\n", scores[0], scores[4]); /* Iterate with a loop */ int sum = 0; for (int i = 0; i < 5; i++) { sum += scores[i]; } printf("Average: %.1f\n", (float)sum / 5); return 0; }
2D Arrays
int matrix[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; /* Print the matrix */ for (int r = 0; r < 3; r++) { for (int c = 0; c < 3; c++) { printf("%3d", matrix[r][c]); } printf("\n"); }
Strings in C
Strings are arrays of char ending with a null terminator '\0'. The
<string.h> library provides useful functions.
#include <stdio.h> #include <string.h> int main() { char name[50] = "Alice"; char greeting[80]; printf("Length: %zu\n", strlen(name)); // 5 strcpy(greeting, "Hello, "); strcat(greeting, name); // "Hello, Alice" printf("%s\n", greeting); /* Compare strings (0 = equal) */ if (strcmp(name, "Alice") == 0) { printf("Found Alice!\n"); } return 0; }
strncpy(dest, src, size) and strncat() for safe copies with bounds checking.
Write a program that reads an array of 5 integers, then: sorts them in ascending order using bubble sort, prints the sorted array, and prints the minimum, maximum, and average values.
Pointers
What is a Pointer?
A pointer is a variable that stores the memory address of another variable. This is C's most powerful (and most tricky) feature.
#include <stdio.h> int main() { int x = 42; int *ptr = &x; /* ptr holds the address of x */ printf("Value of x : %d\n", x); printf("Address of x : %p\n", (void*)ptr); printf("Value via ptr : %d\n", *ptr); /* dereference */ *ptr = 100; /* modify x through the pointer */ printf("x is now: %d\n", x); // 100 return 0; }
Pointer Arithmetic
int arr[] = {10, 20, 30, 40}; int *p = arr; /* points to arr[0] */ printf("%d\n", *p); // 10 p++; printf("%d\n", *p); // 20 printf("%d\n", *(p+2)); // 40
Pointers and Arrays
In C, the name of an array is a pointer to its first element. So arr[i] and
*(arr + i) are equivalent.
Pointer to Pointer
int val = 5; int *p = &val; int **pp = &p; printf("%d\n", **pp); // 5
NULL when no address is available yet, and never dereference a NULL pointer. A
dangling pointer points to memory that has been freed — set the pointer to NULL
after free().
Write a function reverseArray(int *arr, int size) that reverses an array in-place using
pointer arithmetic (no index notation like arr[i]). Verify it works in
main().
Structures & Unions
Structures (struct)
A struct groups variables of different types under a single name — C's way of creating
custom data types.
#include <stdio.h> #include <string.h> /* Define a struct */ struct Student { char name[50]; int age; float gpa; }; void printStudent(struct Student s) { printf("%s | Age: %d | GPA: %.2f\n", s.name, s.age, s.gpa); } int main() { struct Student s1; strcpy(s1.name, "Bob"); s1.age = 20; s1.gpa = 3.8f; printStudent(s1); return 0; }
typedef for Cleaner Code
typedef struct { float x, y; } Point; Point p1 = {3.0f, 4.0f}; printf("(%.1f, %.1f)\n", p1.x, p1.y);
Pointer to Struct (the -> operator)
Point *pp = &p1; printf("x = %.1f\n", pp->x); /* same as (*pp).x */
Unions
A union is like a struct, but all members share the same memory. Only one member can hold a
value at a time. Useful for memory-efficient code and interpreting bits.
union Data { int i; float f; char c; }; union Data d; d.i = 65; printf("%d %c\n", d.i, d.c); // 65 A (same bytes!)
Create a struct Book with fields for title, author, year, and price. Write functions to
create a book, print it, and find the most expensive book from an array of 5 books. Use pointers to
pass structs efficiently.
File I/O
Opening and Closing Files
Use fopen() to open a file and fclose() when done. Always check that
fopen() didn't return NULL.
| Mode | Meaning |
|---|---|
"r" |
Read only. File must exist. |
"w" |
Write. Creates or truncates file. |
"a" |
Append. Creates if not exists. |
"r+" |
Read and write. File must exist. |
"rb" / "wb" |
Binary read / write. |
Writing to a File
#include <stdio.h> int main() { FILE *fp = fopen("data.txt", "w"); if (fp == NULL) { perror("Error opening file"); return 1; } fprintf(fp, "Line 1: Hello, file!\n"); fprintf(fp, "Line 2: Score = %d\n", 95); fclose(fp); printf("File written.\n"); return 0; }
Reading from a File
#include <stdio.h> int main() { FILE *fp = fopen("data.txt", "r"); if (!fp) { perror("open"); return 1; } char line[256]; while (fgets(line, sizeof(line), fp)) { printf("%s", line); /* fgets keeps the \n */ } fclose(fp); return 0; }
Binary Files
typedef struct { int id; float score; } Record; /* Write */ Record r = {1, 98.5f}; FILE *f = fopen("rec.bin", "wb"); fwrite(&r, sizeof(Record), 1, f); fclose(f); /* Read back */ Record r2; f = fopen("rec.bin", "rb"); fread(&r2, sizeof(Record), 1, f); fclose(f); printf("ID: %d, Score: %.1f\n", r2.id, r2.score);
Write a program that reads 5 student names and scores from the user, saves them to a text file
students.txt, then reads the file back and prints the student with the highest score.
Dynamic Memory Allocation
The Heap vs The Stack
Local variables live on the stack and are freed automatically. When you need memory
whose size is unknown at compile time, allocate it on the heap using the functions in
<stdlib.h>.
| Function | Purpose |
|---|---|
malloc(size) |
Allocates size bytes. Contents uninitialised. |
calloc(n, size) |
Allocates n × size bytes, zero-initialised. |
realloc(ptr, new_size) |
Resizes a previous allocation. |
free(ptr) |
Returns memory to the OS. |
#include <stdio.h> #include <stdlib.h> int main() { int n; printf("How many numbers? "); scanf("%d", &n); /* Allocate an array at runtime */ int *arr = (int*)malloc(n * sizeof(int)); if (!arr) { fprintf(stderr, "Out of memory!\n"); return 1; } for (int i = 0; i < n; i++) arr[i] = i * i; for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); free(arr); /* always free! */ arr = NULL; /* prevent dangling pointer */ return 0; }
Dynamic Linked List
typedef struct Node { int data; struct Node *next; } Node; Node* newNode(int val) { Node *n = (Node*)malloc(sizeof(Node)); n->data = val; n->next = NULL; return n; } /* Prepend a node to the list */ Node* prepend(Node *head, int val) { Node *n = newNode(val); n->next = head; return n; }
malloc()/calloc() must be matched with exactly one free().
Use Valgrind (valgrind --leak-check=full ./program) to detect memory leaks.
Implement a dynamic stack using a linked list with push(int val), pop(),
and peek() operations. Allocate each node with malloc and free all nodes
when done.
Preprocessor & Macros
What is the Preprocessor?
Before compilation, GCC runs the C preprocessor (cpp) which processes all lines starting
with #. This happens before the compiler sees your code.
#define — Object-like Macros
#define PI 3.14159265 #define MAX_SIZE 100 #define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) double area = PI * SQUARE(5.0); // 78.539... printf("%d\n", MAX(3, 7)); // 7
SQUARE(1+2) expands to 1+2 * 1+2 = 5 instead of
9. With parentheses: ((1+2)*(1+2)) = 9. ✅
Conditional Compilation
#define DEBUG 1 #ifdef DEBUG #define LOG(msg) printf("[DEBUG] %s\n", msg) #else #define LOG(msg) /* nothing */ #endif int main() { LOG("Starting program"); /* ... */ return 0; }
Header Guards
Prevent a header file from being included more than once:
#ifndef MYLIB_H #define MYLIB_H void myFunction(void); typedef struct { int x, y; } Point; #endif /* MYLIB_H */
Useful Predefined Macros
| Macro | Expands to |
|---|---|
__FILE__ |
Current filename as a string |
__LINE__ |
Current line number as an int |
__DATE__ |
Compilation date string |
__func__ |
Current function name (C99+) |
Create a header file mathutils.h with header guards and macros for ABS(x),
MIN(a,b), MAX(a,b), and CLAMP(val,lo,hi). Include a
DEBUG-controlled logging macro. Test all macros in a main program.
Capstone Project — Student Record System
Build a console-based Student Record Management System that exercises everything you have learned:
- Struct to represent a Student (name, roll number, marks[5], GPA).
- Dynamic array (malloc/realloc) to store an arbitrary number of students.
- Functions to add, delete, search, and display students.
- File I/O to persist records to a binary file and load them on startup.
- Sorting students by GPA using pointers and a comparison function.
- A preprocessor macro for debug logging.
- A menu-driven interface using a
do-whileloop andswitch.
Project File Structure
student_system/ ├── main.c /* entry point, menu loop */ ├── student.h /* struct definition, prototypes */ ├── student.c /* add, delete, search, sort */ ├── fileio.c /* save / load binary file */ └── utils.h /* DEBUG macro, MIN/MAX macros */
Starter Code
#ifndef STUDENT_H #define STUDENT_H #define MAX_NAME 60 #define NUM_SUBJ 5 typedef struct { int roll; char name[MAX_NAME]; float marks[NUM_SUBJ]; float gpa; } Student; /* Dynamic collection */ typedef struct { Student *data; int size; int capacity; } StudentDB; void db_init (StudentDB *db); void db_add (StudentDB *db, Student s); void db_remove (StudentDB *db, int roll); Student *db_find (StudentDB *db, int roll); void db_sort (StudentDB *db); void db_print (StudentDB *db); void db_free (StudentDB *db); void db_save (StudentDB *db, const char *fname); void db_load (StudentDB *db, const char *fname); float calcGPA (float marks[], int n); #endif
#include <stdio.h> #include "student.h" #include "utils.h" #define DB_FILE "students.bin" int main() { StudentDB db; db_init(&db); db_load(&db, DB_FILE); /* load from disk */ int choice; do { printf("\n=== STUDENT SYSTEM ===\n" "1. Add Student\n" "2. Delete Student\n" "3. Search by Roll\n" "4. Display All\n" "5. Sort by GPA\n" "0. Exit\n" "Choice: "); scanf("%d", &choice); switch (choice) { case 1: /* ... */ break; case 2: /* ... */ break; case 3: /* ... */ break; case 4: db_print(&db); break; case 5: db_sort(&db); break; } } while (choice != 0); db_save(&db, DB_FILE); /* persist to disk */ db_free(&db); return 0; }
What You Will Prove
- Correct use of
struct,typedef, and function pointers. - Dynamic memory management without leaks (verified with Valgrind).
- Binary file persistence across program restarts.
- Sorting with
qsort()and a custom comparator. - Multi-file project compiled with
gcc main.c student.c fileio.c -o srs. - Header guards and reusable utility macros.
- Explore data structures in C: linked lists, binary trees, hash tables.
- Learn POSIX threads (
pthreads) for concurrent programming. - Study network programming with the BSD socket API.
- Dive into C++ or embedded C (Arduino, STM32).
- Read The C Programming Language by Kernighan & Ritchie — the definitive reference.
Extend the system with a grade statistics report: for each subject compute the class
average, highest, and lowest mark, and print a bar chart to the terminal using only
printf. Run Valgrind — you must have zero leaks and zero errors.