DOCODIVE
Intermediate Free Learning Path

C & C++ Intermediate Course

Level up your C and C++ skills. Switch between languages using the buttons below — each has 20 lessons, 3 projects, and a capstone.

4–6 weeks 40 lessons Beginner knowledge required

C Intermediate Lessons

01

File I/O (fopen/fclose)

C reads and writes files using FILE pointers. fopen opens a file in a mode (read, write, append), and fclose closes it. Always check if fopen returns NULL before using the file.

C
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "w");

    if (fp == NULL) {
        printf("Error opening file\n");
        return 1;
    }

    fprintf(fp, "Hello File!\n");
    fclose(fp);

    printf("File written\n");
    return 0;
}
fopen("test.txt", "w") opens a file for writing. The NULL check guards against failure. fprintf writes formatted text to the file. fclose closes it. The success message prints to the console.
terminal
File written
The program created test.txt with the text inside, then printed the confirmation. The actual file content is on disk, not in the console output.
02

Pointers & Arrays Relationship

An array name decays to a pointer to its first element. array[i] and *(array + i) are equivalent. This dual nature lets you pass arrays to functions efficiently without copying them.

C
#include <stdio.h>

int main() {
    int nums[] = {10, 20, 30};
    int *ptr = nums;

    printf("%d\n", nums[1]);
    printf("%d\n", *(ptr + 1));
    printf("%d\n", ptr[2]);
    return 0;
}
ptr points to nums' first element. nums[1] accesses index 1 directly. *(ptr + 1) dereferences the pointer moved forward by one element. ptr[2] shows pointer indexing works like array indexing.
terminal
20 20 30
All three printed the same values because arrays and pointers are interchangeable in expressions. The compiler translates array indexing to pointer arithmetic.
03

Pointer Arithmetic

Adding 1 to a pointer moves it by the size of its type, not one byte. This makes iterating arrays with pointers natural. Pointer arithmetic is the foundation of C's speed and power.

C
#include <stdio.h>

int main() {
    int nums[] = {5, 10, 15, 20};
    int *p = nums;

    for (int i = 0; i < 4; i++) {
        printf("%d ", *p);
        p++;
    }
    printf("\n");
    return 0;
}
p starts at the first element. *p dereferences the current element. p++ advances the pointer by sizeof(int) bytes, moving to the next integer. The loop prints each value.
terminal
5 10 15 20
Pointer arithmetic automatically scaled by the type size, so each p++ moved exactly one int forward. This is why pointers are efficient for array traversal.
04

Function Pointers

A function pointer stores the address of a function, letting you call different functions dynamically. This powers callbacks and flexible designs where behavior is chosen at runtime.

C
#include <stdio.h>

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

int main() {
    int (*op)(int, int);

    op = add;
    printf("Add: %d\n", op(5, 3));

    op = mul;
    printf("Mul: %d\n", op(5, 3));
    return 0;
}
int (*op)(int, int) declares a pointer to a function taking two ints and returning int. op = add stores add's address; op(5, 3) calls it. Reassigning op to mul switches behavior without changing the call syntax.
terminal
Add: 8 Mul: 15
The same op call produced addition then multiplication because op pointed to different functions. Function pointers enable runtime-selected behavior.
05

Dynamic Memory (calloc/realloc)

calloc allocates and zeroes memory, while realloc resizes an existing allocation. Together with malloc and free, they give full control over runtime-sized data. Always free to prevent leaks.

C
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = (int*)calloc(3, sizeof(int));
    arr[0] = 10;
    arr[1] = 20;

    arr = (int*)realloc(arr, 5 * sizeof(int));
    arr[4] = 99;

    printf("arr[0]=%d, arr[1]=%d, arr[4]=%d\n", arr[0], arr[1], arr[4]);
    free(arr);
    return 0;
}
calloc(3, sizeof(int)) allocates and zeroes 3 ints. After setting two values, realloc grows the array to 5 ints, preserving existing data. arr[4] is set, then free releases everything.
terminal
arr[0]=10, arr[1]=20, arr[4]=99
Existing values survived realloc, and the new slot was usable. calloc zeroed initially, and realloc resized safely while keeping old data.
06

2D Arrays

A 2D array is an array of arrays. It is declared with two sizes and accessed with two indices. 2D arrays model grids, matrices, and tables.

C
#include <stdio.h>

int main() {
    int grid[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    printf("Center: %d\n", grid[1][1]);

    for (int i = 0; i < 3; i++) {
        printf("%d ", grid[i][i]);
    }
    printf("\n");
    return 0;
}
grid is a 3x3 matrix initialized row by row. grid[1][1] accesses the center (5). The loop prints the diagonal using grid[i][i] (1, 5, 9).
terminal
Center: 5 1 5 9
The center element is 5, and the diagonal elements are 1, 5, 9. Two indices navigate rows and columns.
07

String Functions Deep (strcpy/strcmp/strcat)

string.h provides essential string functions. strcpy copies, strcmp compares (0 means equal), and strcat concatenates. These operate on null-terminated char arrays.

C
#include <stdio.h>
#include <string.h>

int main() {
    char dest[30] = "Hello";
    char src[] = " World";

    strcat(dest, src);
    printf("After strcat: %s\n", dest);
    printf("Compare: %d\n", strcmp(dest, "Hello World"));

    strcpy(dest, "Reset");
    printf("After strcpy: %s\n", dest);
    return 0;
}
strcat appends src to dest, making 'Hello World'. strcmp compares dest with the string, returning 0 since they match. strcpy replaces dest's content with 'Reset'.
terminal
After strcat: Hello World Compare: 0 After strcpy: Reset
Concatenation, comparison, and copying all worked. strcmp returned 0 for equal strings, confirming the match.
08

Command Line Arguments

main can accept arguments from the command line using argc (argument count) and argv (argument values). argv[0] is the program name, and argv[1] onward are user inputs.

C
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Argument count: %d\n", argc);

    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}
argc holds the number of arguments including the program name. argv is an array of strings. The loop prints each argument with its index.
terminal
Argument count: 3 argv[0] = ./program argv[1] = hello argv[2] = world
Running './program hello world' gives argc=3. argv[0] is the program, argv[1] and argv[2] are the user's arguments. This is how CLI tools receive input.
09

Structs & Pointers

Pointers to structs use -> to access members, equivalent to (*ptr).member. Passing struct pointers to functions allows modification without copying the whole struct.

C
#include <stdio.h>

struct Point {
    int x;
    int y;
};

void move(struct Point *p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

int main() {
    struct Point pt = {3, 4};
    move(&pt, 2, -1);
    printf("Point: (%d, %d)\n", pt.x, pt.y);
    return 0;
}
move takes a Point pointer. p->x is shorthand for (*p).x. move(&pt, 2, -1) passes the address, letting the function modify the original struct directly.
terminal
Point: (5, 3)
The original point changed from (3,4) to (5,3) because the function worked through the pointer. This is pass-by-reference for structs.
10

Linked Lists

A linked list is a chain of nodes, each storing data and a pointer to the next node. Unlike arrays, lists grow and shrink easily. Insertion and deletion are O(1) at the head.

C
#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

int main() {
    struct Node *head = NULL;

    struct Node *n1 = (struct Node*)malloc(sizeof(struct Node));
    n1->data = 10; n1->next = NULL;
    head = n1;

    struct Node *n2 = (struct Node*)malloc(sizeof(struct Node));
    n2->data = 20; n2->next = NULL;
    n1->next = n2;

    struct Node *cur = head;
    while (cur != NULL) {
        printf("%d -> ", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
    return 0;
}
Each Node holds data and a next pointer. n1 is created and becomes head. n2 is created and linked via n1->next. The while loop traverses from head following next pointers until NULL.
terminal
10 -> 20 -> NULL
Traversal followed the chain: 10, then 20, then NULL ended the loop. Linked lists store elements in scattered memory connected by pointers.
11

Unions

A union stores only one of its members at a time, sharing the same memory. Its size is the largest member. Unions save memory when only one value is needed at a time.

C
#include <stdio.h>

union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data d;
    d.i = 42;
    printf("Integer: %d\n", d.i);

    d.f = 3.14;
    printf("Float: %.2f\n", d.f);
    return 0;
}
union Data can hold an int, float, or char array — but only one at a time. Setting d.i stores an integer; later setting d.f overwrites the same memory with a float. Accessing each right after setting works.
terminal
Integer: 42 Float: 3.14
Both values printed correctly because each was read right after being written. The union reused the same memory space for different types.
12

Bitwise Operators

Bitwise operators work on individual bits: & (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), >> (right shift). They are used for flags, masks, and low-level manipulation.

C
#include <stdio.h>

int main() {
    int a = 5;   // 0101
    int b = 3;   // 0011

    printf("AND: %d\n", a & b);
    printf("OR: %d\n", a | b);
    printf("XOR: %d\n", a ^ b);
    printf("Left shift: %d\n", a << 1);
    return 0;
}
5 (0101) & 3 (0011) = 1 (0001). 5 | 3 = 7 (0111). 5 ^ 3 = 6 (0110). a << 1 shifts bits left, doubling 5 to 10 (1010).
terminal
AND: 1 OR: 7 XOR: 6 Left shift: 10
Each operator manipulated bits directly. Left shift by 1 is equivalent to multiplying by 2. These operations are the foundation of low-level programming.
13

Macros vs Functions

Macros (#define) are preprocessor text substitutions, while functions are real calls. Macros are faster (no call overhead) but can have side effects. Functions are safer and type-checked.

C
#include <stdio.h>

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    printf("Square: %d\n", SQUARE(5));
    printf("Max: %d\n", MAX(10, 25));
    return 0;
}
SQUARE(x) is a macro that expands to ((x)*(x)) before compilation. Parentheses around x prevent precedence bugs. MAX similarly picks the larger value. Macros are replaced by the preprocessor, not called.
terminal
Square: 25 Max: 25
SQUARE(5) expanded to ((5)*(5)) = 25. MAX(10,25) expanded to the ternary returning 25. Macros inline the expression.
14

Header Files & Multi-file Programs

Header files (.h) declare functions and types shared across .c files. #include brings declarations in. This splits programs into modules and enables code reuse.

C
// math.h
#ifndef MATH_H
#define MATH_H
int add(int a, int b);
#endif

// math.c
#include "math.h"
int add(int a, int b) {
    return a + b;
}

// main.c
#include <stdio.h>
#include "math.h"
int main() {
    printf("%d\n", add(5, 3));
    return 0;
}
math.h declares add() with include guards (#ifndef/#define/#endif). math.c implements it. main.c includes the header to call add(). Linking combines the compiled files.
terminal
8
The function declared in the header and defined in math.c was called from main.c. Headers share declarations across files.
15

Recursion

Recursion is when a function calls itself. It is elegant for problems like factorials, Fibonacci, and tree traversal. Every recursive function needs a base case to stop.

C
#include <stdio.h>

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main() {
    printf("Factorial of 5: %d\n", factorial(5));
    return 0;
}
factorial(5) calls factorial(4), which calls factorial(3), down to factorial(1) which returns 1. Each call multiplies its n by the result of the smaller call, unwinding back up.
terminal
Factorial of 5: 120
5 × 4 × 3 × 2 × 1 = 120. The base case (n <= 1) stopped the recursion. Each level returned its product up the chain.
16

Sorting (Bubble Sort)

Bubble sort repeatedly steps through the array, swapping adjacent elements that are out of order. Larger values 'bubble' to the end. It is simple but O(n²), good for learning and small arrays.

C
#include <stdio.h>

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int nums[] = {5, 2, 8, 1, 9};
    bubbleSort(nums, 5);
    for (int i = 0; i < 5; i++) printf("%d ", nums[i]);
    printf("\n");
    return 0;
}
The outer loop controls passes. The inner loop compares adjacent pairs and swaps when out of order using a temp variable. After each pass, the largest remaining value settles at the end.
terminal
1 2 5 8 9
The array was sorted in place. Bubble sort repeatedly swapped neighbors until everything was ordered ascending.
17

Searching (Linear & Binary)

Linear search checks each element until found — works on unsorted arrays, O(n). Binary search repeatedly halves a sorted array — O(log n), much faster for large data.

C
#include <stdio.h>

int binarySearch(int arr[], int low, int high, int target) {
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return -1;
}

int main() {
    int nums[] = {1, 3, 5, 7, 9};
    printf("Index of 7: %d\n", binarySearch(nums, 0, 4, 7));
    printf("Index of 6: %d\n", binarySearch(nums, 0, 4, 6));
    return 0;
}
binarySearch computes the middle index and compares. If matched, it returns the index. If the target is larger, it searches the right half; smaller, the left half. The range shrinks each iteration until found or empty.
terminal
Index of 7: 3 Index of 6: -1
7 was at index 3. 6 was not present, so -1 was returned. Binary search required a sorted array and worked in logarithmic time.
18

Error Handling & errno

C reports errors through return values and the global errno variable. strerror() converts an errno code to a readable message. Always check function return values for failure.

C
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *fp = fopen("missing.txt", "r");

    if (fp == NULL) {
        printf("Error: %s\n", strerror(errno));
        return 1;
    }

    fclose(fp);
    return 0;
}
fopen returns NULL because the file does not exist. The check catches it. strerror(errno) converts the error code to 'No such file or directory'. The program exits with code 1 indicating failure.
terminal
Error: No such file or directory
errno was set by the failed fopen, and strerror produced a human-readable message. Checking the NULL return prevented a crash.
19

Storage Classes Deep (static/extern/register)

Storage classes control lifetime and visibility. static in a function preserves value between calls; static global limits scope to the file. extern declares a variable defined elsewhere. register hints the compiler to use a CPU register.

C
#include <stdio.h>

void counter() {
    static int count = 0;
    count++;
    printf("Count: %d\n", count);
}

int main() {
    counter();
    counter();
    counter();
    return 0;
}
static int count initializes once and keeps its value between calls. Each counter() call increments the same variable. Without static, count would reset to 0 each time.
terminal
Count: 1 Count: 2 Count: 3
The static variable preserved its value across calls, incrementing from 1 to 3. This is static's key role inside functions.
20

C Intermediate Recap + Projects

You now know file I/O, pointers and arrays, pointer arithmetic, function pointers, dynamic memory, 2D arrays, string functions, command-line args, struct pointers, linked lists, unions, bitwise ops, macros, header files, recursion, sorting, searching, error handling, and storage classes. Apply them in projects like a Contact Book or a Simple Database.

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Contact {
    char name[50];
    char phone[20];
};

int main() {
    struct Contact contacts[3];
    int count = 0;

    strcpy(contacts[0].name, "Ali");
    strcpy(contacts[0].phone, "0300-1234567");
    count++;

    strcpy(contacts[1].name, "Sara");
    strcpy(contacts[1].phone, "0301-7654321");
    count++;

    for (int i = 0; i < count; i++) {
        printf("%s - %s\n", contacts[i].name, contacts[i].phone);
    }
    return 0;
}
Contact struct holds name and phone. strcpy fills the fields. count tracks entries. The loop prints all stored contacts. This is the foundation of a Contact Book project.
terminal
Ali - 0300-1234567 Sara - 0301-7654321
Two contacts were stored and printed. Structs, strings, and arrays combined to model real data — the core of the Contact Book project.

C Intermediate Projects

About this project

Build a singly linked list with insert, display, and search using dynamic memory and struct pointers.

What you'll practice
linked lists malloc struct pointers loops
Topics used

Linked Lists, Structs & Pointers, Dynamic Memory, Pointer Arithmetic

C
#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

struct Node* insertHead(struct Node *head, int value) {
    struct Node *n = (struct Node*)malloc(sizeof(struct Node));
    n->data = value;
    n->next = head;
    return n;
}

void display(struct Node *head) {
    struct Node *cur = head;
    while (cur != NULL) {
        printf("%d -> ", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

int main() {
    struct Node *head = NULL;
    head = insertHead(head, 30);
    head = insertHead(head, 20);
    head = insertHead(head, 10);
    display(head);
    return 0;
}
Sample Run
terminal
10 -> 20 -> 30 -> NULL

About this project

Sort an array with bubble sort, then search using binary search.

What you'll practice
bubble sort binary search function pointers arrays
Topics used

Sorting, Searching, Function Pointers, Arrays

C
#include <stdio.h>

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - i - 1; j++)
            if (arr[j] > arr[j + 1]) {
                int t = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = t;
            }
}

int binarySearch(int arr[], int low, int high, int target) {
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return -1;
}

int main() {
    int nums[] = {9, 2, 7, 1, 5};
    int size = 5;
    bubbleSort(nums, size);

    for (int i = 0; i < size; i++) printf("%d ", nums[i]);
    printf("\n");

    printf("Index of 7: %d\n", binarySearch(nums, 0, size - 1, 7));
    return 0;
}
Sample Run
terminal
1 2 5 7 9 Index of 7: 3

About this project

Split a sentence into words using strtok and count them — practicing string functions and pointers.

What you'll practice
strtok pointers string functions loops
Topics used

String Functions, Pointers & Arrays, Pointer Arithmetic

C
#include <stdio.h>
#include <string.h>

int main() {
    char sentence[] = "learn C programming today";
    int count = 0;

    char *token = strtok(sentence, " ");
    while (token != NULL) {
        printf("%s\n", token);
        count++;
        token = strtok(NULL, " ");
    }

    printf("Words: %d\n", count);
    return 0;
}
Sample Run
terminal
learn C programming today Words: 4

C Capstone Project

About this project

Save and load contacts using structs, dynamic memory, and error handling with file I/O.

What you'll practice
file I/O structs fprintf error handling
Topics used

File I/O, Structs, String Functions, Dynamic Memory, Error Handling (errno)

C
#include <stdio.h>
#include <string.h>
#include <errno.h>

struct Contact {
    char name[50];
    char phone[20];
};

int main() {
    struct Contact contacts[2] = {
        {"Ali", "0300-111"},
        {"Sara", "0301-222"}
    };

    FILE *fp = fopen("contacts.txt", "w");
    if (fp == NULL) {
        printf("Error: %s\n", strerror(errno));
        return 1;
    }

    for (int i = 0; i < 2; i++) {
        fprintf(fp, "%s %s\n", contacts[i].name, contacts[i].phone);
    }
    fclose(fp);

    printf("Contacts saved to contacts.txt\n");
    return 0;
}
Sample Run
terminal
Contacts saved to contacts.txt

C++ Intermediate Lessons

01

References Deep Dive

A reference is an alias for an existing variable, declared with &. Unlike pointers, references cannot be null and cannot be reassigned. They provide pass-by-reference and are the basis for modern C++ patterns like return-by-reference.

C++
#include <iostream>

void increment(int &num) {
    num++;
}

int main() {
    int x = 10;
    increment(x);
    std::cout << "x: " << x << std::endl;
    return 0;
}
increment takes an int& reference parameter, so num aliases x. num++ modifies the original x directly. No copy is made, and no pointer dereferencing is needed.
terminal
x: 11
x changed from 10 to 11 because the function operated on the original through the reference. This is pass-by-reference.
02

Const Correctness

const protects data from modification. const references pass data efficiently without allowing changes. const member functions promise not to modify the object. Const correctness catches bugs at compile time.

C++
#include <iostream>
#include <string>

class Person {
private:
    std::string name;

public:
    Person(std::string n) : name(n) {}

    std::string getName() const {
        return name;
    }
};

int main() {
    const Person p("Ali");
    std::cout << p.getName() << std::endl;
    return 0;
}
getName() is declared const, promising not to modify the object. A const Person can call it. The const keyword enforces read-only behavior, catching accidental modifications at compile time.
terminal
Ali
The const object called its const member function successfully. Const correctness made the code safer without runtime cost.
03

Copy Constructor & Copy Assignment

The copy constructor creates a new object from an existing one, and the copy assignment operator copies between existing objects. They control how object state is duplicated, especially important with dynamic resources.

C++
#include <iostream>
#include <string>

class Box {
public:
    std::string label;

    Box(std::string l) : label(l) {
        std::cout << "Constructor: " << label << std::endl;
    }

    Box(const Box& other) : label(other.label) {
        std::cout << "Copy: " << label << std::endl;
    }
};

int main() {
    Box a("First");
    Box b = a;  // copy constructor
    return 0;
}
Box defines a copy constructor that copies the label. Box b = a invokes it, copying a's state into b. The custom copy constructor logs when it runs.
terminal
Constructor: First Copy: First
a was constructed normally, then b was created via the copy constructor. Custom copy behavior ran when the object was copied.
04

Move Semantics & rvalue References

Move semantics transfer resources instead of copying them, using rvalue references (&&). std::move enables this. Moving avoids expensive deep copies, especially for large containers.

C++
#include <iostream>
#include <string>

int main() {
    std::string source = "Hello";
    std::string dest = std::move(source);

    std::cout << "Dest: " << dest << std::endl;
    std::cout << "Source empty: " << (source.empty() ? "yes" : "no") << std::endl;
    return 0;
}
std::move casts source to an rvalue, allowing dest to steal its internal buffer instead of copying. After the move, source is typically left empty or in a valid-but-unspecified state.
terminal
Dest: Hello Source empty: yes
dest took ownership of the string data, and source became empty. This is move semantics — transferring resources instead of duplicating them.
05

Operator Overloading

Operator overloading lets you define how operators (+, ==, <<, etc.) work for your classes. This makes user-defined types behave like built-in types and produces readable code.

C++
#include <iostream>

class Point {
public:
    int x, y;

    Point(int a, int b) : x(a), y(b) {}

    Point operator+(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }

    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

int main() {
    Point p1(1, 2);
    Point p2(3, 4);
    Point p3 = p1 + p2;

    std::cout << "p3: (" << p3.x << ", " << p3.y << ")" << std::endl;
    std::cout << "Equal: " << (p1 == p2) << std::endl;
    return 0;
}
operator+ adds coordinates and returns a new Point. operator== compares both fields. p1 + p2 uses the overloaded +, and p1 == p2 uses the overloaded ==.
terminal
p3: (4, 6) Equal: 0
p3 became (1+3, 2+4) = (4, 6). The equality check returned 0 (false) because p1 and p2 differ. Operators now work naturally on Point.
06

Friend Functions & Classes

A friend function or class can access private members of another class. The friend keyword grants special access. It is used for operator overloading and tight coupling between related types.

C++
#include <iostream>

class Circle {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    friend double area(const Circle& c);
};

double area(const Circle& c) {
    return 3.14159 * c.radius * c.radius;
}

int main() {
    Circle c(2);
    std::cout << "Area: " << area(c) << std::endl;
    return 0;
}
area() is declared as a friend inside Circle, granting it access to private radius. area() reads c.radius directly. Without friendship, this access would be a compile error.
terminal
Area: 12.5664
The friend function accessed the private radius and computed the area (πr² ≈ 12.57). Friendship selectively opens private access.
07

Smart Pointers (unique_ptr/shared_ptr)

Smart pointers automatically manage memory. unique_ptr owns a resource exclusively and deletes it on destruction. shared_ptr uses reference counting to share ownership. They prevent memory leaks.

C++
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> up = std::make_unique<int>(42);
    std::shared_ptr<int> sp = std::make_shared<int>(99);

    std::cout << "unique_ptr: " << *up << std::endl;
    std::cout << "shared_ptr: " << *sp << std::endl;
    return 0;
}
make_unique creates a unique_ptr owning an int. make_shared creates a shared_ptr. Dereferencing (*up, *sp) accesses values. When the pointers go out of scope, memory is freed automatically.
terminal
unique_ptr: 42 shared_ptr: 99
Both smart pointers printed their values. No manual delete was needed — destructors freed the memory at scope exit.
08

RAII (Resource Acquisition Is Initialization)

RAII ties resource lifetime to object lifetime. Resources are acquired in constructors and released in destructors. When an object goes out of scope, its destructor runs, guaranteeing cleanup even on exceptions.

C++
#include <iostream>

class FileGuard {
public:
    FileGuard() {
        std::cout << "Resource acquired" << std::endl;
    }

    ~FileGuard() {
        std::cout << "Resource released" << std::endl;
    }
};

int main() {
    {
        FileGuard guard;
    }
    std::cout << "Out of scope" << std::endl;
    return 0;
}
FileGuard acquires a resource in its constructor and releases it in the destructor. The object exists only inside the inner block. When the block ends, the destructor automatically releases the resource.
terminal
Resource acquired Resource released Out of scope
The destructor ran automatically when guard went out of scope, guaranteeing cleanup. This is the RAII pattern's core guarantee.
09

STL Containers Overview

The Standard Template Library provides reusable containers: vector (dynamic array), list (doubly-linked), map (key-value), set (unique sorted), and more. Each has different performance trade-offs for different operations.

C++
#include <iostream>
#include <vector>
#include <set>
#include <map>

int main() {
    std::vector<int> vec = {3, 1, 2};
    std::set<int> s = {3, 1, 2, 2};
    std::map<std::string, int> ages;
    ages["Ali"] = 25;

    std::cout << "Vector size: " << vec.size() << std::endl;
    std::cout << "Set size: " << s.size() << std::endl;
    std::cout << "Ali's age: " << ages["Ali"] << std::endl;
    return 0;
}
vector stores ordered elements allowing duplicates. set stores unique sorted elements, so the duplicate 2 is removed. map stores key-value pairs. Each container is suited to different needs.
terminal
Vector size: 3 Set size: 3 Ali's age: 25
Vector kept all 3 elements, set kept 3 unique sorted values (duplicate 2 dropped), and map retrieved Ali's age. Choosing the right container matters.
10

STL Iterators

Iterators generalize traversal across containers. begin() returns an iterator to the first element, end() to one past the last. They let algorithms work with any container uniformly.

C++
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = {10, 20, 30, 40};

    for (auto it = nums.begin(); it != nums.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}
begin() returns an iterator to the first element; end() is one past the last. The loop advances with ++it and dereferences with *it. The pattern works for any container with iterators.
terminal
10 20 30 40
The iterator traversed every element in order. This uniform traversal is why algorithms work across containers.
11

STL Algorithms

The <algorithm> header provides ready-made operations: sort, find, count, reverse, and more. They work with iterators, so one algorithm works on many container types.

C++
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> nums = {5, 2, 8, 1, 9};

    std::sort(nums.begin(), nums.end());
    int count8 = std::count(nums.begin(), nums.end(), 8);
    int maxVal = *std::max_element(nums.begin(), nums.end());

    for (int n : nums) std::cout << n << " ";
    std::cout << "\nCount of 8: " << count8 << std::endl;
    std::cout << "Max: " << maxVal << std::endl;
    return 0;
}
sort orders the range. count tallies occurrences of 8. max_element returns an iterator to the largest, dereferenced to get the value. Each algorithm operates on iterator ranges.
terminal
1 2 5 8 9 Count of 8: 1 Max: 9
The vector was sorted, 8 appeared once, and the max was 9. STL algorithms replaced hand-written loops with clear intent.
12

Function Templates

Function templates let one function work with many types. The type parameter T is deduced at compile time. Templates provide generic programming without duplicating code.

C++
#include <iostream>

template <typename T>
T maxValue(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maxValue(5, 10) << std::endl;
    std::cout << maxValue(3.5, 2.5) << std::endl;
    std::cout << maxValue('a', 'z') << std::endl;
    return 0;
}
template <typename T> makes maxValue generic. The compiler generates a version for each type used: int, double, and char. Type is deduced from arguments.
terminal
10 3.5 z
One template function worked with three types. The compiler instantiated the right version for each call.
13

Class Templates

Class templates let a class work with any type, like std::vector<T>. The type parameter is specified when instantiating the object. This is how generic containers are built.

C++
#include <iostream>

template <typename T>
class Box {
private:
    T value;

public:
    Box(T v) : value(v) {}

    T get() const {
        return value;
    }
};

int main() {
    Box<int> intBox(42);
    Box<std::string> strBox("Hello");

    std::cout << intBox.get() << std::endl;
    std::cout << strBox.get() << std::endl;
    return 0;
}
Box<T> is a class template. Box<int> creates an integer box, Box<std::string> a string box. Each instantiation is a separate type with the same logic.
terminal
42 Hello
One class template handled two different types. The compiler generated separate versions for int and string.
14

Custom Exception Classes

You can define your own exception classes by inheriting from std::exception and overriding what(). Custom exceptions make errors domain-specific and catchable by type.

C++
#include <iostream>
#include <exception>

class InsufficientFundsException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Insufficient funds";
    }
};

void withdraw(double balance, double amount) {
    if (amount > balance) {
        throw InsufficientFundsException();
    }
}

int main() {
    try {
        withdraw(50, 100);
    } catch (const InsufficientFundsException& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }
    return 0;
}
InsufficientFundsException inherits std::exception and overrides what(). withdraw throws it when amount exceeds balance. catch handles the custom type and prints its message.
terminal
Error: Insufficient funds
The custom exception carried a domain-specific message and was caught specifically. Custom exceptions clarify error handling.
15

Virtual Destructors

A base class destructor should be virtual when deleting derived objects through a base pointer. Otherwise, only the base destructor runs, leaking derived resources.

C++
#include <iostream>

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destroyed" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destroyed" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}
Base has a virtual destructor. Derived overrides it. delete ptr through a Base pointer runs the virtual destructor, which calls both Derived's and Base's destructors correctly.
terminal
Derived destroyed Base destroyed
Both destructors ran in the correct order (derived first, then base). Without virtual, only Base's would run, leaking Derived's resources.
16

Abstract Classes & Pure Virtual

An abstract class has at least one pure virtual function (declared with = 0). It cannot be instantiated and serves as an interface. Derived classes must implement all pure virtual functions.

C++
#include <iostream>

class Shape {
public:
    virtual double area() const = 0;
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle c(2);
    std::cout << "Area: " << c.area() << std::endl;
    return 0;
}
Shape declares area() as pure virtual (= 0), making it abstract. Circle implements area() with its own formula. A Circle object can call area(); a Shape object cannot be created.
terminal
Area: 12.5664
Circle implemented the pure virtual function. Abstract classes define interfaces that subclasses must fulfill.
17

Namespaces Deep Dive

Namespaces group related code and avoid name collisions. You can nest namespaces, alias them, and use using-declarations to bring specific names into scope. The std namespace contains all standard library names.

C++
#include <iostream>

namespace math {
    int add(int a, int b) { return a + b; }
}

namespace util {
    int add(int a, int b) { return a * b; }
}

int main() {
    std::cout << "math::add: " << math::add(5, 3) << std::endl;
    std::cout << "util::add: " << util::add(5, 3) << std::endl;
    return 0;
}
Two namespaces define functions with the same name add but different behavior. math::add and util::add disambiguate them. Namespaces prevent the name collision.
terminal
math::add: 8 util::add: 15
Both add functions coexisted because namespaces separated them. The scope resolution operator (::) picked each one.
18

String Streams (istringstream/ostringstream)

String streams treat strings like I/O streams. istringstream reads from a string, ostringstream builds one, and stringstream does both. They are useful for parsing and formatting.

C++
#include <iostream>
#include <sstream>

int main() {
    std::istringstream input("42 3.14 hello");
    int i;
    double d;
    std::string s;

    input >> i >> d >> s;

    std::ostringstream output;
    output << "Parsed: " << i << ", " << d << ", " << s;

    std::cout << output.str() << std::endl;
    return 0;
}
istringstream extracts an int, double, and string from the text using >>. ostringstream builds a formatted result with <<. str() returns the built string.
terminal
Parsed: 42, 3.14, hello
The string was parsed into typed values and reformatted. String streams bridge strings and typed data.
19

Lambda Captures Deep

Lambdas capture outside variables using []. [=] captures by value, [&] by reference, and you can capture specific variables explicitly. Captures let lambdas use surrounding scope data.

C++
#include <iostream>

int main() {
    int base = 10;

    auto addBase = [base](int x) { return base + x; };

    auto increment = [&base]() { base++; };

    increment();

    std::cout << "addBase(5): " << addBase(5) << std::endl;
    std::cout << "base after increment: " << base << std::endl;
    return 0;
}
addBase captures base by value, so its copy is fixed at 10. increment captures base by reference, so it modifies the original. base becomes 11 after increment.
terminal
addBase(5): 15 base after increment: 11
By-value capture kept base at 10 for addBase, while by-reference capture let increment change the real base to 11.
20

C++ Intermediate Recap + Projects

You now know references, const correctness, copy and move semantics, operator overloading, friends, smart pointers, RAII, STL containers, iterators, algorithms, templates, exceptions, virtual destructors, abstract classes, namespaces, string streams, and lambda captures. Apply them in projects like a Smart Contact Manager or a Template-based Stack.

C++
#include <iostream>
#include <vector>
#include <memory>
#include <string>

class Contact {
public:
    std::string name;
    std::string phone;

    Contact(std::string n, std::string p) : name(n), phone(p) {}
};

int main() {
    std::vector<std::unique_ptr<Contact>> contacts;

    contacts.push_back(std::make_unique<Contact>("Ali", "0300-111"));
    contacts.push_back(std::make_unique<Contact>("Sara", "0301-222"));

    for (const auto& c : contacts) {
        std::cout << c->name << " - " << c->phone << std::endl;
    }
    return 0;
}
A vector of unique_ptr<Contact> manages memory automatically. make_unique creates each contact. push_back adds them. The range-for iterates and arrow (->) accesses members. No manual delete needed.
terminal
Ali - 0300-111 Sara - 0301-222
Smart pointers managed the contacts' lifetimes automatically. Modern C++ features combined: vector, unique_ptr, classes, and range-for.

C++ Intermediate Projects

About this project

Build a resource manager using smart pointers and RAII — memory is automatically freed.

What you'll practice
unique_ptr RAII move semantics vector
Topics used

Smart Pointers, RAII, Move Semantics, Classes

C++
#include <iostream>
#include <memory>
#include <vector>
#include <string>

class Resource {
public:
    std::string name;
    Resource(std::string n) : name(n) {
        std::cout << "Acquired: " << name << std::endl;
    }
    ~Resource() {
        std::cout << "Released: " << name << std::endl;
    }
};

int main() {
    std::vector<std::unique_ptr<Resource>> resources;
    resources.push_back(std::make_unique<Resource>("DB Connection"));
    resources.push_back(std::make_unique<Resource>("File Handle"));

    for (const auto& r : resources) {
        std::cout << "Using: " << r->name << std::endl;
    }
    return 0;
}
Sample Run
terminal
Acquired: DB Connection Acquired: File Handle Using: DB Connection Using: File Handle Released: File Handle Released: DB Connection

About this project

Build a generic stack using class templates and overloaded operators.

What you'll practice
class templates operator overloading vector functions
Topics used

Class Templates, Operator Overloading, STL Containers

C++
#include <iostream>
#include <vector>

template <typename T>
class Stack {
private:
    std::vector<T> items;
public:
    void operator+=(T item) { items.push_back(item); }
    T top() const { return items.back(); }
    void pop() { if (!items.empty()) items.pop_back(); }
    bool empty() const { return items.empty(); }
};

int main() {
    Stack<int> s;
    s += 10;
    s += 20;
    s += 30;

    while (!s.empty()) {
        std::cout << s.top() << " ";
        s.pop();
    }
    std::cout << std::endl;
    return 0;
}
Sample Run
terminal
30 20 10

About this project

Parse numbers using string streams and handle errors with a custom exception class.

What you'll practice
string streams custom exceptions lambdas try/catch
Topics used

String Streams, Custom Exceptions, Lambda Captures

C++
#include <iostream>
#include <sstream>
#include <exception>

class ParseException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Invalid number format";
    }
};

int main() {
    std::istringstream input("42 3.14 hello");

    try {
        int i;
        double d;

        input >> i >> d;

        if (input.fail()) throw ParseException();

        auto sum = [](int a, double b) { return a + b; };
        std::cout << "Sum: " << sum(i, d) << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }

    return 0;
}
Sample Run
terminal
Sum: 45.14

C++ Capstone Project

About this project

Build a calculator that parses expressions using string streams, overloaded operators, custom exceptions, and lambdas.

What you'll practice
string streams custom exceptions operator overloading lambdas
Topics used

String Streams, Custom Exceptions, Operator Overloading, Lambda Captures

C++
#include <iostream>
#include <sstream>
#include <string>
#include <exception>

class DivideByZeroException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Cannot divide by zero";
    }
};

int main() {
    std::istringstream input("10 / 4 + 2");
    double a, b;
    char op1, op2;
    std::string extra;

    input >> a >> op1 >> b >> op2 >> extra;

    try {
        double result;
        if (op1 == '/') {
            if (b == 0) throw DivideByZeroException();
            result = a / b;
        }

        auto addTwo = [result](double x) { return result + x; };
        if (op2 == '+') {
            result = addTwo(std::stod(extra));
        }

        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }

    return 0;
}
Sample Run
terminal
Result: 4.5
You've completed all 40 lessons (20 C + 20 C++). Ready for the final level?

Level up with C/C++ Advanced and Practice resources.

📱 Scan this QR code with your phone camera to instantly open this page.

Works on iOS, Android, and any modern device. No app installation required.

Account Verified!

Your email has been verified successfully.