C & C++ Advanced Course
Master the hardest parts of C and C++. Switch between languages using the buttons below — each has 20 lessons, 4 projects, and a capstone.
C Advanced Lessons
Function Pointers in Depth
A function pointer stores the address of a function, allowing you to pass functions as arguments, build dispatch tables, and implement callbacks. This unlocks flexible, reusable C code where behavior is chosen at runtime instead of compile time.
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int operate(int x, int y, int (*op)(int, int)) {
return op(x, y);
}
int main(void) {
int (*fp)(int, int) = add;
printf("add: %d\n", fp(3, 4));
printf("mul: %d\n", operate(3, 4, mul));
return 0;
}
Pointer to Pointer & 2D Dynamic Arrays
A pointer to a pointer (int **) is how C represents dynamically allocated 2D arrays. Each row is a separate malloc, and the row pointers are collected into an array of pointers.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3, cols = 4;
int **matrix = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++)
matrix[i] = malloc(cols * sizeof(int));
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
matrix[i][j] = i * cols + j;
printf("matrix[2][3] = %d\n", matrix[2][3]);
for (int i = 0; i < rows; i++) free(matrix[i]);
free(matrix);
return 0;
}
Dynamic Memory: malloc, calloc, realloc, free
Dynamic memory lets programs allocate storage at runtime. malloc leaves memory uninitialized, calloc zeroes it, realloc resizes, and free releases — misuse causes leaks and dangling pointers.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *arr = calloc(5, sizeof(int));
for (int i = 0; i < 5; i++) arr[i] = i * 10;
arr = realloc(arr, 8 * sizeof(int));
for (int i = 5; i < 8; i++) arr[i] = i * 10;
for (int i = 0; i < 8; i++) printf("%d ", arr[i]);
printf("\n");
free(arr);
arr = NULL;
return 0;
}
Memory Alignment & Padding
CPUs access memory most efficiently when data is aligned to its natural boundary. Compilers insert padding bytes inside structs to satisfy alignment, which affects both size and performance.
#include <stdio.h>
#include <stdalign.h>
struct Bad {
char c;
int i;
};
struct Good {
int i;
char c;
};
int main(void) {
printf("sizeof(Bad) = %zu\n", sizeof(struct Bad));
printf("sizeof(Good) = %zu\n", sizeof(struct Good));
return 0;
}
Unions & Type Punning
A union stores all members in the same memory location. Only one member is valid at a time. Type punning reads the same bytes as a different type — useful for inspecting representation.
#include <stdio.h>
#include <stdint.h>
union FloatBits {
float f;
uint32_t bits;
};
int main(void) {
union FloatBits fb;
fb.f = 3.14f;
printf("float: %.2f\n", fb.f);
printf("bits: 0x%08X\n", fb.bits);
return 0;
}
Bit Manipulation & Bitfields
Bitwise operators (&, |, ^, ~, <<, >>) manipulate individual bits. Bitfields pack multiple small values into a single integer, saving memory for flags and small ranges.
#include <stdio.h>
#include <stdint.h>
struct Flags {
uint8_t read : 1;
uint8_t write : 1;
uint8_t exec : 1;
uint8_t reserve : 5;
};
int main(void) {
struct Flags f = {0};
f.read = 1;
f.exec = 1;
unsigned int mask = (1u << 2) | (1u << 4);
printf("flags: r=%d w=%d x=%d\n", f.read, f.write, f.exec);
printf("mask: 0x%X\n", mask);
return 0;
}
Variadic Functions (stdarg.h)
Variadic functions accept a variable number of arguments. The stdarg.h macros (va_list, va_start, va_arg, va_end) let you walk the arguments, enabling printf-style APIs.
#include <stdio.h>
#include <stdarg.h>
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; i++)
total += va_arg(args, int);
va_end(args);
return total;
}
int main(void) {
printf("sum(3) = %d\n", sum(3, 10, 20, 30));
printf("sum(5) = %d\n", sum(5, 1, 2, 3, 4, 5));
return 0;
}
Recursion & Backtracking
Recursion solves a problem by breaking it into smaller instances of itself. Backtracking explores candidate solutions and abandons paths that fail constraints — the core of N-Queens, Sudoku, and maze solving.
#include <stdio.h>
long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
printf("%c -> %c\n", from, to);
return;
}
hanoi(n - 1, from, aux, to);
printf("%c -> %c\n", from, to);
hanoi(n - 1, aux, to, from);
}
int main(void) {
printf("5! = %ld\n", factorial(5));
hanoi(3, 'A', 'C', 'B');
return 0;
}
Function Pointer Tables (Dispatch)
A dispatch table is an array of function pointers indexed by an operation code. It replaces long switch statements with O(1) lookup and makes adding operations trivial.
#include <stdio.h>
int op_add(int a, int b) { return a + b; }
int op_sub(int a, int b) { return a - b; }
int op_mul(int a, int b) { return a * b; }
int op_div(int a, int b) { return b ? a / b : 0; }
int main(void) {
int (*ops[4])(int, int) = {op_add, op_sub, op_mul, op_div};
const char *names[] = {"add", "sub", "mul", "div"};
for (int i = 0; i < 4; i++)
printf("%s(10, 2) = %d\n", names[i], ops[i](10, 2));
return 0;
}
Callback Functions
A callback is a function passed as an argument to another function, which then invokes it. Callbacks decouple generic logic (like iteration) from specific behavior (like printing or filtering).
#include <stdio.h>
void for_each(int *arr, int len, void (*cb)(int)) {
for (int i = 0; i < len; i++) cb(arr[i]);
}
void print_square(int x) { printf("%d ", x * x); }
void print_double(int x) { printf("%d ", x * 2); }
int main(void) {
int nums[] = {1, 2, 3, 4, 5};
for_each(nums, 5, print_square);
printf("\n");
for_each(nums, 5, print_double);
printf("\n");
return 0;
}
Threads with pthreads
POSIX threads (pthreads) let a C program run multiple tasks concurrently. Each thread executes a function in parallel, sharing process memory but having its own stack.
#include <stdio.h>
#include <pthread.h>
void *print_hello(void *arg) {
int id = *(int *)arg;
printf("Thread %d running\n", id);
return NULL;
}
int main(void) {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
for (int i = 0; i < 3; i++)
pthread_create(&threads[i], NULL, print_hello, &ids[i]);
for (int i = 0; i < 3; i++)
pthread_join(threads[i], NULL);
printf("All threads done\n");
return 0;
}
Mutex & Race Conditions
When multiple threads modify shared data, a race condition can corrupt results. A mutex (mutual exclusion lock) ensures only one thread enters a critical section at a time.
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void *increment(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mtx);
counter++;
pthread_mutex_unlock(&mtx);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("counter = %d\n", counter);
return 0;
}
Condition Variables
Condition variables let threads wait until a predicate becomes true, and let other threads signal them. Combined with a mutex, they build producer-consumer and other synchronization patterns.
#include <stdio.h>
#include <pthread.h>
int ready = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *waiter(void *arg) {
(void)arg;
pthread_mutex_lock(&mtx);
while (!ready)
pthread_cond_wait(&cond, &mtx);
printf("Waiter sees ready = %d\n", ready);
pthread_mutex_unlock(&mtx);
return NULL;
}
int main(void) {
pthread_t t;
pthread_create(&t, NULL, waiter, NULL);
pthread_mutex_lock(&mtx);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
pthread_join(t, NULL);
return 0;
}
Signal Handling
Signals are asynchronous notifications to a process (Ctrl+C sends SIGINT). A signal handler runs when the signal arrives, but may only safely access volatile sig_atomic_t variables.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t stop = 0;
void handle_sigint(int sig) {
(void)sig;
stop = 1;
}
int main(void) {
signal(SIGINT, handle_sigint);
printf("Press Ctrl+C to stop...\n");
while (!stop) {
usleep(100000);
}
printf("Stopped cleanly\n");
return 0;
}
Volatile & const Correctness
const tells the compiler a value won't change through that name, enabling optimizations and catching mistakes. volatile tells it the value may change externally (hardware, signal), preventing caching.
#include <stdio.h>
void print_array(const int *arr, int len) {
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main(void) {
const int max_size = 100;
int nums[] = {1, 2, 3, 4, 5};
print_array(nums, 5);
printf("max_size = %d\n", max_size);
return 0;
}
Binary File I/O
Binary files store raw bytes rather than text. fwrite and fread write and read data blocks directly, enabling efficient storage of structs, arrays, and images.
#include <stdio.h>
struct Record {
int id;
double score;
};
int main(void) {
struct Record recs[] = {{1, 92.5}, {2, 78.0}, {3, 88.75}};
FILE *fp = fopen("records.bin", "wb");
fwrite(recs, sizeof(struct Record), 3, fp);
fclose(fp);
struct Record loaded[3];
fp = fopen("records.bin", "rb");
fread(loaded, sizeof(struct Record), 3, fp);
fclose(fp);
for (int i = 0; i < 3; i++)
printf("id=%d score=%.2f\n", loaded[i].id, loaded[i].score);
return 0;
}
Struct Padding & Packed Structs
Compilers align struct members for performance, inserting padding. #pragma pack or __attribute__((packed)) removes padding — smaller but potentially slower and unsafe for unaligned access.
#include <stdio.h>
struct Normal {
char a;
int b;
char c;
};
#pragma pack(push, 1)
struct Packed {
char a;
int b;
char c;
};
#pragma pack(pop)
int main(void) {
printf("Normal = %zu bytes\n", sizeof(struct Normal));
printf("Packed = %zu bytes\n", sizeof(struct Packed));
return 0;
}
Doubly & Circular Linked Lists
A doubly linked list has both next and prev pointers, allowing O(1) insertion/removal at either end. Circular lists connect the last node back to the first, enabling round-robin traversal.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node *next;
struct Node *prev;
};
struct Node *push_front(struct Node *head, int value) {
struct Node *n = malloc(sizeof(*n));
n->data = value;
n->next = head;
n->prev = NULL;
if (head) head->prev = n;
return n;
}
void print_forward(struct Node *head) {
while (head) {
printf("%d ", head->data);
head = head->next;
}
printf("\n");
}
int main(void) {
struct Node *head = NULL;
head = push_front(head, 30);
head = push_front(head, 20);
head = push_front(head, 10);
print_forward(head);
return 0;
}
Hash Tables in C
A hash table maps keys to values in near O(1) time. A hash function converts a key to an index; collisions are handled with chaining (linked lists per bucket).
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define TABLE_SIZE 10
struct Entry {
char key[32];
int value;
struct Entry *next;
};
struct Entry *table[TABLE_SIZE] = {0};
unsigned int hash(const char *key) {
unsigned int h = 0;
while (*key) h = h * 31 + *key++;
return h % TABLE_SIZE;
}
void put(const char *key, int value) {
unsigned int i = hash(key);
struct Entry *e = malloc(sizeof(*e));
strcpy(e->key, key);
e->value = value;
e->next = table[i];
table[i] = e;
}
int main(void) {
put("apple", 10);
put("banana", 20);
put("cherry", 30);
printf("banana -> %d\n", table[hash("banana")]->value);
return 0;
}
Memory-Mapped I/O (mmap)
mmap maps a file directly into the process address space. Reads and writes to the mapped memory go straight to the file — faster for large files and enables shared memory between processes.
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
int main(void) {
int fd = open("mmap.txt", O_RDWR | O_CREAT, 0644);
const char *text = "Hello mmap";
write(fd, text, strlen(text));
struct stat st;
fstat(fd, &st);
char *map = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); return 1; }
map[0] = 'h';
msync(map, st.st_size, MS_SYNC);
printf("File content via mmap: %s\n", map);
munmap(map, st.st_size);
close(fd);
return 0;
}
C Advanced Projects
About this project
Build a fixed-size block allocator that hands out memory from a pre-allocated pool — no per-allocation malloc overhead.
What you'll practice
Topics used
Memory Alignment, Pointer Arithmetic, Bit Manipulation, Unions
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#define POOL_SIZE 4096
#define BLOCK_SIZE 64
#define BLOCK_COUNT (POOL_SIZE / BLOCK_SIZE)
static uint8_t pool[POOL_SIZE];
static uint64_t free_map = ~0ULL; /* 1 = free */
void* pool_alloc(void) {
for (int i = 0; i < BLOCK_COUNT; i++) {
if (free_map & (1ULL << i)) {
free_map &= ~(1ULL << i);
return &pool[i * BLOCK_SIZE];
}
}
return NULL;
}
void pool_free(void* ptr) {
ptrdiff_t offset = (uint8_t*)ptr - pool;
int index = offset / BLOCK_SIZE;
free_map |= (1ULL << index);
}
int main(void) {
int* a = pool_alloc();
int* b = pool_alloc();
*a = 42;
*b = 99;
printf("a=%d b=%d\n", *a, *b);
pool_free(a);
int* c = pool_alloc();
printf("c reuses a's block: %s\n", (c == a) ? "yes" : "no");
return 0;
}
About this project
Implement a producer-consumer queue using pthreads, a mutex, and a condition variable.
What you'll practice
Topics used
Threads (pthreads), Mutex & Race Conditions, Condition Variables
#include <stdio.h>
#include <pthread.h>
#define Q_SIZE 8
int queue[Q_SIZE];
int head = 0, tail = 0, count = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void enqueue(int value) {
pthread_mutex_lock(&mtx);
while (count == Q_SIZE)
pthread_cond_wait(¬_full, &mtx);
queue[tail] = value;
tail = (tail + 1) % Q_SIZE;
count++;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mtx);
}
int dequeue(void) {
pthread_mutex_lock(&mtx);
while (count == 0)
pthread_cond_wait(¬_empty, &mtx);
int value = queue[head];
head = (head + 1) % Q_SIZE;
count--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mtx);
return value;
}
void* producer(void* arg) {
for (int i = 1; i <= 5; i++) {
enqueue(i);
printf("Produced %d\n", i);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 1; i <= 5; i++) {
int v = dequeue();
printf("Consumed %d\n", v);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, producer, NULL);
pthread_create(&t2, NULL, consumer, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
About this project
Rotate a 24-bit BMP image 180 degrees using file I/O and struct-based header parsing.
What you'll practice
Topics used
File I/O, Structs & Padding, Dynamic 2D Arrays, Bit Manipulation
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#pragma pack(push, 1)
typedef struct {
uint16_t type;
uint32_t size;
uint16_t reserved1;
uint16_t reserved2;
uint32_t offset;
uint32_t header_size;
int32_t width;
int32_t height;
uint16_t planes;
uint16_t bpp;
uint32_t compression;
uint32_t image_size;
int32_t x_ppm;
int32_t y_ppm;
uint32_t colors;
uint32_t important;
} BMPHeader;
#pragma pack(pop)
int main(void) {
FILE* in = fopen("input.bmp", "rb");
if (!in) { printf("Cannot open file\n"); return 1; }
BMPHeader header;
fread(&header, sizeof(header), 1, in);
int width = header.width;
int height = header.height;
int row_size = (width * 3 + 3) & ~3;
uint8_t* data = malloc(row_size * height);
fseek(in, header.offset, SEEK_SET);
fread(data, 1, row_size * height, in);
fclose(in);
for (int y = 0; y < height / 2; y++) {
for (int x = 0; x < row_size; x++) {
uint8_t tmp = data[y * row_size + x];
data[y * row_size + x] = data[(height - 1 - y) * row_size + x];
data[(height - 1 - y) * row_size + x] = tmp;
}
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width * 3 / 2; x += 3) {
int right = width * 3 - 3 - x;
uint8_t tmp[3];
memcpy(tmp, &data[y * row_size + x], 3);
memcpy(&data[y * row_size + x], &data[y * row_size + right], 3);
memcpy(&data[y * row_size + right], tmp, 3);
}
}
FILE* out = fopen("rotated.bmp", "wb");
fwrite(&header, sizeof(header), 1, out);
fwrite(data, 1, row_size * height, out);
fclose(out);
free(data);
printf("Rotated image written to rotated.bmp\n");
return 0;
}
About this project
Build a single-threaded TCP echo server using Berkeley sockets — the foundation of network programming in C.
What you'll practice
Topics used
Sockets (TCP), Signal Handling, Volatile & const, Error Handling
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>
volatile sig_atomic_t running = 1;
void handle_sigint(int sig) {
(void)sig;
running = 0;
}
int main(void) {
signal(SIGINT, handle_sigint);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) { perror("socket"); return 1; }
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); return 1;
}
if (listen(server_fd, 5) < 0) { perror("listen"); return 1; }
printf("Echo server listening on port 8080 (Ctrl+C to stop)\n");
while (running) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) break;
char buffer[1024];
ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
buffer[n] = '\0';
send(client_fd, buffer, n, 0);
printf("Echoed %zd bytes\n", n);
}
close(client_fd);
}
close(server_fd);
printf("\nServer shut down cleanly\n");
return 0;
}
C Capstone Project
About this project
Search files in a directory tree using multiple threads, a shared work queue, and a mutex-protected result list.
What you'll practice
Topics used
Threads, Mutex, Condition Variables, Recursion, File I/O, Signal Handling
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#define MAX_RESULTS 64
#define MAX_PATH 512
#define WORKERS 4
char results[MAX_RESULTS][MAX_PATH];
int result_count = 0;
char pattern[64];
char work_queue[1024][MAX_PATH];
int queue_head = 0, queue_tail = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t work_ready = PTHREAD_COND_INITIALIZER;
int done = 0;
int queue_get(char* out) {
pthread_mutex_lock(&mtx);
while (queue_head == queue_tail && !done)
pthread_cond_wait(&work_ready, &mtx);
if (queue_head == queue_tail && done) {
pthread_mutex_unlock(&mtx);
return 0;
}
strcpy(out, work_queue[queue_head++]);
pthread_mutex_unlock(&mtx);
return 1;
}
void* worker(void* arg) {
(void)arg;
char filename[MAX_PATH];
while (queue_get(filename)) {
if (strstr(filename, pattern)) {
pthread_mutex_lock(&mtx);
strcpy(results[result_count++], filename);
pthread_mutex_unlock(&mtx);
}
}
return NULL;
}
int main(void) {
strcpy(pattern, ".c");
strcpy(work_queue[queue_tail++], "main.c");
strcpy(work_queue[queue_tail++], "utils.h");
strcpy(work_queue[queue_tail++], "parser.c");
strcpy(work_queue[queue_tail++], "README.md");
pthread_t threads[WORKERS];
for (int i = 0; i < WORKERS; i++)
pthread_create(&threads[i], NULL, worker, NULL);
pthread_mutex_lock(&mtx);
done = 1;
pthread_cond_broadcast(&work_ready);
pthread_mutex_unlock(&mtx);
for (int i = 0; i < WORKERS; i++)
pthread_join(threads[i], NULL);
printf("Found %d .c files:\n", result_count);
for (int i = 0; i < result_count; i++)
printf(" %s\n", results[i]);
return 0;
}
C++ Advanced Lessons
Move Semantics & Rvalue References
Move semantics transfers ownership of resources instead of copying them. An rvalue reference (T&&) binds to temporaries, enabling move constructors and move assignment that steal pointers rather than deep-copying.
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main() {
std::string a = "hello";
std::string b = std::move(a);
std::cout << "b = " << b << "\n";
std::cout << "a = '" << a << "'\n";
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
std::cout << "v2 size = " << v2.size() << ", v1 size = " << v1.size() << "\n";
return 0;
}
Perfect Forwarding
Perfect forwarding preserves both the value category (lvalue/rvalue) and constness of arguments when passing them through wrapper functions, using forwarding references (T&&) and std::forward.
#include <iostream>
#include <utility>
void sink(int &x) { std::cout << "lvalue\n"; }
void sink(int &&x) { std::cout << "rvalue\n"; }
template <typename T>
void wrapper(T &&arg) {
sink(std::forward<T>(arg));
}
int main() {
int x = 5;
wrapper(x);
wrapper(10);
return 0;
}
The Rule of Five
If a class manages a resource, define all five special members: destructor, copy constructor, copy assignment, move constructor, and move assignment. This prevents double-frees, leaks, and shallow-copy bugs.
#include <iostream>
#include <utility>
class Buffer {
private:
int *data;
size_t size;
public:
explicit Buffer(size_t n) : data(new int[n]), size(n) {}
~Buffer() { delete[] data; }
Buffer(const Buffer &other) : data(new int[other.size]), size(other.size) {
for (size_t i = 0; i < size; i++) data[i] = other.data[i];
}
Buffer &operator=(const Buffer &other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; i++) data[i] = other.data[i];
}
return *this;
}
Buffer(Buffer &&other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
Buffer &operator=(Buffer &&other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
size_t get_size() const { return size; }
};
int main() {
Buffer a(10);
Buffer b = a;
Buffer c = std::move(a);
std::cout << "a=" << a.get_size() << " b=" << b.get_size() << " c=" << c.get_size() << "\n";
return 0;
}
Smart Pointer Internals
std::unique_ptr has exclusive ownership; std::shared_ptr uses reference counting; std::weak_ptr observes without owning. Understanding the control block reveals how shared_ptr avoids dangling references.
#include <iostream>
#include <memory>
struct Widget {
int id;
explicit Widget(int i) : id(i) {}
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};
int main() {
auto sp1 = std::make_shared<Widget>(1);
std::weak_ptr<Widget> wp = sp1;
{
auto sp2 = sp1;
std::cout << "use_count = " << sp1.use_count() << "\n";
}
std::cout << "after scope use_count = " << sp1.use_count() << "\n";
if (auto locked = wp.lock())
std::cout << "weak_ptr locked, id = " << locked->id << "\n";
sp1.reset();
if (wp.expired())
std::cout << "weak_ptr expired after reset\n";
return 0;
}
Class Templates
Class templates let one class definition work with many types. The compiler generates a concrete class per type used — enabling type-safe containers and algorithms without code duplication.
#include <iostream>
#include <vector>
template <typename T>
class Stack {
private:
std::vector<T> items;
public:
void push(const T &item) { items.push_back(item); }
T pop() {
T back = items.back();
items.pop_back();
return back;
}
bool empty() const { return items.empty(); }
};
int main() {
Stack<int> s1;
s1.push(10);
s1.push(20);
std::cout << s1.pop() << " ";
std::cout << s1.pop() << "\n";
Stack<std::string> s2;
s2.push("hello");
std::cout << s2.pop() << "\n";
return 0;
}
Variadic Templates
Variadic templates accept any number of template arguments using a parameter pack. Recursion plus pack expansion processes each argument — the core of std::tuple, make_shared, and emplace.
#include <iostream>
void print() {
std::cout << "\n";
}
template <typename First, typename... Rest>
void print(First first, Rest... rest) {
std::cout << first << " ";
print(rest...);
}
int main() {
print(1, 2.5, 'c', "hello");
print("only", 42);
return 0;
}
Template Metaprogramming (SFINAE)
SFINAE (Substitution Failure Is Not An Error) lets templates gracefully discard overloads that would be invalid, enabling compile-time type introspection and conditional overload resolution.
#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
double_value(T value) {
return value * 2;
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
double_value(T value) {
return value * 2.0;
}
int main() {
std::cout << double_value(5) << "\n";
std::cout << double_value(2.5) << "\n";
return 0;
}
Lambda Advanced: Captures & std::function
Lambdas capture variables from their enclosing scope by value, reference, or move. std::function erases the lambda's type so it can be stored in containers and passed around like a regular object.
#include <iostream>
#include <functional>
#include <vector>
int main() {
int base = 10;
auto add = [base](int x) { return base + x; };
auto add_ref = [&base](int x) { return base + x; };
base = 100;
std::function<int(int)> fn = add;
std::vector<std::function<int(int)>> funcs = {
add,
add_ref,
[](int x) { return x * x; }
};
std::cout << "add(5) = " << add(5) << "\n";
std::cout << "add_ref(5) = " << add_ref(5) << "\n";
std::cout << "fn(5) = " << fn(5) << "\n";
for (auto &f : funcs)
std::cout << f(5) << " ";
std::cout << "\n";
return 0;
}
std::tuple & std::apply
std::tuple stores a fixed collection of heterogeneous values. std::apply expands a tuple into a function's arguments, bridging generic data structures with ordinary functions.
#include <iostream>
#include <tuple>
#include <string>
int add_three(int a, int b, int c) {
return a + b + c;
}
int main() {
auto t = std::make_tuple(1, 2, 3, std::string("hello"));
std::cout << "size = " << std::tuple_size<decltype(t)>::value << "\n";
std::cout << "get<0> = " << std::get<0>(t) << "\n";
std::cout << "get<3> = " << std::get<3>(t) << "\n";
auto args = std::make_tuple(10, 20, 30);
int sum = std::apply(add_three, args);
std::cout << "sum = " << sum << "\n";
return 0;
}
Fold Expressions (C++17)
Fold expressions compactly apply a binary operator over a parameter pack. They replace verbose recursive template code for common operations like sum, logical AND, or comma-separated printing.
#include <iostream>
template <typename... Args>
auto sum_all(Args... args) {
return (args + ...);
}
template <typename... Args>
void print_all(Args... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
}
int main() {
std::cout << "sum = " << sum_all(1, 2, 3, 4, 5) << "\n";
print_all("hello", 42, 3.14, 'X');
return 0;
}
Threads & std::async
std::thread runs a function concurrently; std::async launches a task that returns a std::future for retrieving the result later. Futures decouple launching from result consumption.
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
int slow_square(int x) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return x * x;
}
int main() {
auto fut1 = std::async(std::launch::async, slow_square, 5);
auto fut2 = std::async(std::launch::async, slow_square, 7);
std::cout << "Tasks launched\n";
std::cout << "5^2 = " << fut1.get() << "\n";
std::cout << "7^2 = " << fut2.get() << "\n";
return 0;
}
Mutex & Condition Variables (C++)
std::mutex protects shared data; std::condition_variable lets threads wait for a predicate. std::unique_lock with cv.wait is the standard C++ synchronization idiom.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 1; i <= 5; i++) {
{
std::lock_guard lock(mtx);
q.push(i);
std::cout << "Produced " << i << "\n";
}
cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
void consumer() {
for (int i = 1; i <= 5; i++) {
std::unique_lock lock(mtx);
cv.wait(lock, [] { return !q.empty(); });
int value = q.front();
q.pop();
std::cout << "Consumed " << value << "\n";
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
Atomics & Memory Ordering
std::atomic guarantees indivisible operations on shared variables without locks. Memory ordering (relaxed, acquire, release, seq_cst) controls visibility of other memory operations between threads.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter{0};
void add_1000() {
for (int i = 0; i < 1000; i++)
counter.fetch_add(1, std::memory_order_relaxed);
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++)
threads.emplace_back(add_1000);
for (auto &t : threads)
t.join();
std::cout << "counter = " << counter.load() << "\n";
return 0;
}
Lock-Free Basics (CAS)
Lock-free programming uses atomic compare-and-swap (CAS) loops instead of mutexes. Threads retry until the CAS succeeds, avoiding blocking but requiring careful memory-order reasoning.
#include <iostream>
#include <atomic>
#include <thread>
struct Node {
int value;
Node *next;
};
std::atomic<Node *> head{nullptr};
void push(int value) {
Node *n = new Node{value, nullptr};
n->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(n->next, n,
std::memory_order_release, std::memory_order_relaxed)) {}
}
int main() {
std::thread t1([] { for (int i = 0; i < 100; i++) push(i); });
std::thread t2([] { for (int i = 100; i < 200; i++) push(i); });
t1.join();
t2.join();
int count = 0;
for (Node *n = head.load(); n; n = n->next) count++;
std::cout << "Pushed " << count << " nodes\n";
return 0;
}
Type Erasure
Type erasure hides a concrete type behind a common interface, letting heterogeneous objects share a single container — the technique behind std::function, std::any, and virtual dispatch.
#include <iostream>
#include <memory>
#include <vector>
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
template <typename T>
class DrawableModel : public Drawable {
T obj;
public:
explicit DrawableModel(T o) : obj(std::move(o)) {}
void draw() const override { obj.draw(); }
};
struct Circle {
void draw() const { std::cout << "Drawing circle\n"; }
};
struct Square {
void draw() const { std::cout << "Drawing square\n"; }
};
int main() {
std::vector<std::unique_ptr<Drawable>> shapes;
shapes.push_back(std::make_unique<DrawableModel<Circle>>(Circle{}));
shapes.push_back(std::make_unique<DrawableModel<Square>>(Square{}));
for (const auto &s : shapes)
s->draw();
return 0;
}
Exception Safety & RAII
RAII ties resource lifetime to object lifetime, guaranteeing cleanup even when exceptions unwind the stack. The strong exception guarantee ensures failed operations leave state unchanged.
#include <iostream>
#include <stdexcept>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
};
void risky() {
Resource r;
throw std::runtime_error("something went wrong");
}
int main() {
try {
risky();
} catch (const std::exception &e) {
std::cout << "Caught: " << e.what() << "\n";
}
return 0;
}
Custom Iterators
Custom iterators let your own containers work with range-based for loops and STL algorithms. An iterator must define operator*, ++, !=, and the required type aliases.
#include <iostream>
#include <iterator>
class Range {
private:
int start;
int end;
class Iterator {
private:
int value;
public:
using iterator_category = std::forward_iterator_tag;
using value_type = int;
using difference_type = std::ptrdiff_t;
using pointer = int *;
using reference = int &;
explicit Iterator(int v) : value(v) {}
int operator*() const { return value; }
Iterator &operator++() { ++value; return *this; }
bool operator!=(const Iterator &other) const { return value != other.value; }
};
public:
Range(int s, int e) : start(s), end(e) {}
Iterator begin() const { return Iterator(start); }
Iterator end() const { return Iterator(end); }
};
int main() {
Range r(1, 6);
for (int x : r)
std::cout << x << " ";
std::cout << "\n";
return 0;
}
std::variant & std::optional
std::variant is a type-safe union that holds one of several types; std::optional holds either a value or nothing. Both eliminate raw unions and null-pointer sentinels.
#include <iostream>
#include <variant>
#include <optional>
#include <string>
std::optional<int> find_even(int *arr, int len) {
for (int i = 0; i < len; i++)
if (arr[i] % 2 == 0) return arr[i];
return std::nullopt;
}
int main() {
std::variant<int, std::string, double> v;
v = 42;
std::cout << "variant int: " << std::get<int>(v) << "\n";
v = "hello";
std::cout << "variant str: " << std::get<std::string>(v) << "\n";
int odds[] = {1, 3, 5, 7};
int mixed[] = {1, 2, 3, 4};
auto r1 = find_even(odds, 4);
auto r2 = find_even(mixed, 4);
if (r1) std::cout << "r1 = " << *r1 << "\n";
else std::cout << "r1 = none\n";
std::cout << "r2 = " << r2.value_or(-1) << "\n";
return 0;
}
C++20 Concepts (Basics)
Concepts constrain template parameters with readable, compile-time-checked requirements. They replace SFINAE for many cases, producing clearer error messages and better overload resolution.
#include <iostream>
#include <concepts>
template <typename T>
requires std::integral<T>
T gcd(T a, T b) {
while (b != 0) {
T t = b;
b = a % b;
a = t;
}
return a;
}
template <typename T>
concept Printable = requires(T t) {
{ t.print() } -> std::same_as<void>;
};
struct Foo {
void print() { std::cout << "Foo\n"; }
};
int main() {
std::cout << "gcd(48, 18) = " << gcd(48, 18) << "\n";
if constexpr (Printable<Foo>)
Foo{}.print();
return 0;
}
Coroutines Intro (C++20)
Coroutines suspend and resume execution without blocking a thread — ideal for generators, async I/O, and lazy sequences. co_yield produces values one at a time; co_return ends the coroutine.
#include <iostream>
#include <coroutine>
#include <optional>
struct Generator {
struct promise_type {
int current;
Generator get_return_object() { return Generator{this}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int v) { current = v; return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
promise_type *promise;
bool move_next() { return promise != nullptr; }
int value() const { return promise->current; }
};
Generator count_to(int n) {
for (int i = 1; i <= n; i++)
co_yield i;
}
int main() {
auto gen = count_to(5);
std::cout << "Generated values: ";
for (int i = 1; i <= 5; i++)
std::cout << gen.value() << " ";
std::cout << "\n";
return 0;
}
C++ Advanced Projects
About this project
Implement your own reference-counted smart pointer with copy/move semantics and a control block.
What you'll practice
Topics used
Smart Pointer Internals, Rule of Five, Class Templates, Operator Overloading
#include <iostream>
template <typename T>
class SharedPtr {
private:
T* ptr = nullptr;
size_t* ref_count = nullptr;
void release() {
if (ref_count && --(*ref_count) == 0) {
delete ptr;
delete ref_count;
ptr = nullptr;
ref_count = nullptr;
}
}
public:
explicit SharedPtr(T* p = nullptr)
: ptr(p), ref_count(p ? new size_t(1) : nullptr) {}
SharedPtr(const SharedPtr& other)
: ptr(other.ptr), ref_count(other.ref_count) {
if (ref_count) ++(*ref_count);
}
SharedPtr(SharedPtr&& other) noexcept
: ptr(other.ptr), ref_count(other.ref_count) {
other.ptr = nullptr;
other.ref_count = nullptr;
}
SharedPtr& operator=(SharedPtr other) {
swap(other);
return *this;
}
~SharedPtr() { release(); }
void swap(SharedPtr& other) noexcept {
std::swap(ptr, other.ptr);
std::swap(ref_count, other.ref_count);
}
T* operator->() const { return ptr; }
T& operator*() const { return *ptr; }
size_t use_count() const { return ref_count ? *ref_count : 0; }
};
int main() {
SharedPtr<int> p1(new int(42));
SharedPtr<int> p2 = p1;
std::cout << "value=" << *p1 << " count=" << p1.use_count() << std::endl;
SharedPtr<int> p3 = std::move(p2);
std::cout << "after move count=" << p1.use_count() << std::endl;
return 0;
}
About this project
Build a fixed-size thread pool with a task queue using std::thread, std::function, and condition variables.
What you'll practice
Topics used
Threads & async, std::function, Mutex & Condition Variables
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <chrono>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t count) {
for (size_t i = 0; i < count; i++) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock lock(mtx);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template <typename F>
auto enqueue(F&& f) -> std::future<decltype(f())> {
using R = decltype(f());
auto promise = std::make_shared<std::promise<R>>();
auto future = promise->get_future();
{
std::lock_guard lock(mtx);
tasks.emplace([promise, fn = std::forward<F>(f)]() mutable {
promise->set_value(fn());
});
}
cv.notify_one();
return future;
}
~ThreadPool() {
{
std::lock_guard lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& w : workers) w.join();
}
};
int main() {
ThreadPool pool(4);
std::vector<std::future<int>> results;
for (int i = 0; i < 8; i++) {
results.push_back(pool.enqueue([i] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return i * i;
}));
}
for (auto& r : results)
std::cout << r.get() << " ";
std::cout << std::endl;
return 0;
}
About this project
Print any std::tuple using variadic templates, std::apply, and C++17 fold expressions.
What you'll practice
Topics used
Variadic Templates, Perfect Forwarding, std::tuple, Fold Expressions
#include <iostream>
#include <tuple>
#include <string>
#include <type_traits>
template <typename Tuple, size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
std::cout << std::endl;
}
template <typename... Args>
void print_tuple(const std::tuple<Args...>& t) {
print_tuple_impl(t, std::index_sequence_for<Args...>{});
}
int main() {
auto t1 = std::make_tuple(42, 3.14, std::string("hello"), 'X');
print_tuple(t1);
auto t2 = std::make_tuple("nested", std::make_tuple(1, 2, 3));
print_tuple(t2);
return 0;
}
About this project
Implement a thread-safe stack using atomic compare-and-swap (CAS) instead of locks.
What you'll practice
Topics used
Atomics & Memory Ordering, Lock-Free Basics, Smart Pointers
#include <iostream>
#include <atomic>
#include <memory>
#include <thread>
#include <vector>
template <typename T>
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
Node(const T& d) : data(d), next(nullptr) {}
};
std::atomic<Node*> head{nullptr};
public:
void push(const T& value) {
Node* node = new Node(value);
node->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(
node->next, node,
std::memory_order_release,
std::memory_order_relaxed)) {}
}
bool pop(T& result) {
Node* node = head.load(std::memory_order_relaxed);
while (node && !head.compare_exchange_weak(
node, node->next,
std::memory_order_acquire,
std::memory_order_relaxed)) {}
if (!node) return false;
result = node->data;
delete node;
return true;
}
};
int main() {
LockFreeStack<int> stack;
std::thread t1([&] {
for (int i = 0; i < 10; i++) stack.push(i);
});
std::thread t2([&] {
for (int i = 100; i < 110; i++) stack.push(i);
});
t1.join();
t2.join();
int value;
int count = 0;
while (stack.pop(value)) count++;
std::cout << "Popped " << count << " items" << std::endl;
return 0;
}
C++ Capstone Project
About this project
Schedule async tasks with priorities, futures, and cancellation — combining everything from the C++ advanced path.
What you'll practice
Topics used
Threads & async, std::function, Lambda Advanced, Exception Safety, Rule of Five
#include <iostream>
#include <future>
#include <vector>
#include <functional>
#include <chrono>
#include <string>
struct Task {
int priority;
std::string name;
std::function<int()> work;
};
class TaskScheduler {
private:
std::vector<std::pair<int, std::future<int>>> pending;
public:
void schedule(Task task) {
auto fut = std::async(std::launch::async, task.work);
pending.emplace_back(task.priority, std::move(fut));
}
void wait_all() {
for (auto& [priority, fut] : pending) {
int result = fut.get();
std::cout << "Task priority=" << priority
<< " result=" << result << std::endl;
}
pending.clear();
}
};
int main() {
TaskScheduler scheduler;
scheduler.schedule({3, "high", [] {
std::this_thread::sleep_for(std::chrono::milliseconds(30));
return 100;
}});
scheduler.schedule({1, "low", [] {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 1;
}});
scheduler.schedule({2, "mid", [] {
return 42;
}});
scheduler.wait_all();
return 0;
}