Advanced~20 min read

C & C++

A deep dive into C and modern C++20, contrasting manual memory management and pointers with RAII, smart pointers, templates, and the STL.

PointersMemoryRAIISTL

C is a small, procedural systems language that gives you direct control over memory and hardware. C++ began as "C with classes" and has grown into a large multi-paradigm language: it keeps C's low-level control while adding object orientation, generic programming via templates, RAII, and a rich standard library (the STL). This guide covers modern C17 and C++20, contrasting the two throughout.

The Compilation Model

Both languages are compiled ahead of time. A source file passes through three logical stages before it becomes an executable.

1. Preprocessor — handles lines starting with #. It expands #include directives (pasting header text in place), substitutes #define macros, and resolves conditional compilation like #ifdef.

2. Compile — the compiler turns each translation unit into an object file (.o) of machine code, checking syntax and types along the way.

3. Link — the linker stitches object files and libraries together, resolving symbol references, into a final executable.

// hello.c
#include <stdio.h>

int main(void) {
    printf("Hello, C!\n");
    return 0;
}
// hello.cpp
#include <iostream>

int main() {
    std::cout << "Hello, C++!" << std::endl;
    return 0;
}

C Basics

Types and I/O

C's fundamental types include char, int, float, double, and fixed-width variants from <stdint.h> such as int32_t. Console I/O uses printf and scanf with format specifiers.

int age;
printf("Enter age: ");
scanf("%d", &age);          // & passes the address to write into
printf("You are %d\n", age);

Format string safety

A mismatch between the format specifier and the argument type (e.g. printing an int with %s) is undefined behavior. Enable -Wformat warnings.

Arrays, Strings, Structs, Functions

Arrays are contiguous blocks with no bounds checking. C strings are just char arrays terminated by a null byte '\0'. A struct groups related fields under one name.

int nums[3] = {1, 2, 3};
char name[] = "Ada";        // 4 bytes: 'A','d','a','\0'

struct Point { int x; int y; };

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

struct Point p = {3, 4};
printf("%d\n", add(p.x, p.y));

Pointers

A pointer holds the memory address of a value. This is C's defining feature and its sharpest edge. You declare with *, take an address with &, and dereference (read/write the pointee) with *.

int x = 42;
int *p = &x;        // p points at x
printf("%d\n", *p);  // 42 (dereference)
*p = 100;            // now x == 100

// Pointer arithmetic: moving by element size, not bytes
int arr[3] = {10, 20, 30};
int *q = arr;        // arrays decay to pointers
printf("%d\n", *(q + 2)); // 30

int *n = NULL;       // null pointer: points at nothing

A void * is a typeless pointer used for generic memory; you must cast it before dereferencing. A function pointer stores the address of a function, enabling callbacks.

int square(int n) { return n * n; }

int (*fn)(int) = square;   // function pointer
printf("%d\n", fn(5));      // 25

void *raw = &x;             // typeless
int back = *(int *)raw;     // cast, then deref

Undefined behavior

Dereferencing a null or uninitialized pointer, reading past an array's end, or using a pointer after its memory is freed is undefined behavior. The program may crash, corrupt data, or appear to work — until it does not.

Memory Management

Local variables live on the stack: automatically allocated on function entry and freed on return. Longer-lived or dynamically sized data lives on the heap, which you manage manually.

In C you use malloc/free; in C++ you use new/delete (which also run constructors and destructors).

// C
int *buf = malloc(10 * sizeof(int));
if (buf == NULL) { /* handle allocation failure */ }
buf[0] = 1;
free(buf);
buf = NULL;   // avoid dangling pointer

// C++
int *one = new int(7);
delete one;

int *many = new int[10];
delete[] many;   // array form MUST match new[]
Aspectmalloc / free (C)new / delete (C++)
Returnsvoid *, needs castTyped pointer
Runs constructorNoYes
SizeYou compute bytesInferred from type
On failureReturns NULLThrows std::bad_alloc

Memory safety hazards

Dangling pointer: using memory after freeing it. Leak: losing the last pointer to allocated memory without freeing it. Double free: calling free/delete twice on the same block. Modern C++ avoids all three by preferring RAII and smart pointers over raw new/delete.

What C++ Adds

References vs Pointers

A reference (int &r = x;) is an alias for an existing object. Unlike a pointer it cannot be null and cannot be rebound after initialization, making it safer for parameters you know are always valid.

FeaturePointerReference
Can be nullYesNo
ReassignableYesNo (bound once)
ArithmeticYesNo
Syntax to useDereference with *Used like the value

Namespaces, std::string, iostream, Overloading

Namespaces group names to avoid clashes; the standard library lives in std. std::string is a self-managing string type. Functions may be overloaded (same name, different parameters) and have default arguments.

#include <iostream>
#include <string>

std::string greet(const std::string &who = "world") {
    return "Hello, " + who;
}

int area(int s)          { return s * s; }      // overload 1
int area(int w, int h)   { return w * h; }      // overload 2

std::string name;
std::cin >> name;
std::cout << greet(name) << '\n';

Classes & OOP

A class bundles data and behavior with access control: public, protected, and private. A constructor initializes an object; a destructor cleans up when it dies. Marking a method virtual enables polymorphism: the call resolves to the derived override at runtime through a base pointer or reference.

class Animal {
public:
    Animal(std::string n) : name_(std::move(n)) {}
    virtual ~Animal() = default;          // virtual dtor for base classes
    virtual std::string speak() const { return "..."; }
protected:
    std::string name_;
};

class Dog : public Animal {
public:
    using Animal::Animal;
    std::string speak() const override { return "Woof"; }
};

Animal *a = new Dog("Rex");
std::cout << a->speak() << '\n';   // "Woof" via dynamic dispatch
delete a;

Always make base destructors virtual

Deleting a derived object through a base pointer with a non-virtual destructor is undefined behavior and leaks the derived part.

RAII & Smart Pointers

RAII (Resource Acquisition Is Initialization) is C++'s core idiom: tie a resource's lifetime to an object's lifetime, so the destructor releases it automatically — even when an exception unwinds the stack. Smart pointers apply RAII to heap memory.

TypeOwnershipUse when
unique_ptr<T>ExclusiveOne owner; cheapest, default choice
shared_ptr<T>Shared (refcount)Multiple owners, freed at last one
weak_ptr<T>Non-owning observerBreak shared_ptr reference cycles
#include <memory>

auto u = std::make_unique<Dog>("Rex");   // freed automatically
auto s = std::make_shared<int>(42);      // refcounted
std::weak_ptr<int> w = s;                 // does not extend lifetime

if (auto locked = w.lock()) {            // safe access
    std::cout << *locked << '\n';
}

The Rule of 0 / 3 / 5

Rule of 3: if a class needs a custom destructor, copy constructor, or copy assignment, it usually needs all three. Rule of 5 adds the move constructor and move assignment for modern C++. Rule of 0: best of all — own resources through RAII types (like unique_ptr or std::vector) so you write none of the special members and let the compiler default them.

Templates

Templates enable generic programming: write code once against a type parameter, and the compiler instantiates a version for each concrete type used. Both functions and classes can be templated.

template<class T>
T max_of(T a, T b) { return a > b ? a : b; }

template<class T>
class Box {
    T value_;
public:
    explicit Box(T v) : value_(std::move(v)) {}
    const T& get() const { return value_; }
};

auto m = max_of<int>(3, 9);      // 9
Box<std::string> b{"hi"};

C++20 concepts constrain template parameters with readable requirements and better error messages, replacing older SFINAE tricks:

#include <concepts>

template<std::integral T>      // only accepts integer types
T doubled(T x) { return x * 2; }

The Standard Template Library (STL)

The STL provides ready-made containers, iterators, and algorithms. Prefer these over raw arrays and hand-rolled data structures.

#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <algorithm>

std::vector<int> v = {5, 2, 8, 1};
std::sort(v.begin(), v.end());              // 1 2 5 8

auto it = std::find(v.begin(), v.end(), 8); // iterator
if (it != v.end()) { /* found */ }

std::map<std::string, int> ordered;         // sorted, O(log n)
ordered["apple"] = 3;

std::unordered_map<std::string, int> fast;  // hashed, avg O(1)
fast["banana"] = 5;

std::set<int> unique = {1, 2, 2, 3};        // {1, 2, 3}

An iterator generalizes a pointer: it marks a position in a container and advances with ++. Algorithms like std::sort, std::find, and std::accumulate operate on iterator ranges, decoupling logic from container type.

Move Semantics

An rvalue reference (T&&) binds to temporaries, letting you move resources instead of copying them. std::move casts an lvalue to an rvalue so a move constructor can steal its guts, leaving the source in a valid empty state.

std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);  // b steals a's buffer, no copy
// a is now valid but unspecified (typically empty)

Error Handling

C reports errors by return code and the global errno variable. C++ adds exceptions: throw raises one, try/catch handles it, and stack unwinding runs destructors along the way (RAII in action).

// C style
#include <errno.h>
FILE *f = fopen("x.txt", "r");
if (!f) { perror("fopen"); }   // reads errno

// C++ style
#include <stdexcept>
try {
    if (bad) throw std::runtime_error("boom");
} catch (const std::exception &e) {
    std::cerr << e.what() << '\n';
}

Tooling

Compilers: gcc/clang for C, g++/clang++ for C++. Build systems: make and the more portable CMake. Debug with gdb or lldb. Catch memory bugs with valgrind and the compiler sanitizers.

# Compile with warnings + AddressSanitizer
g++ -std=c++20 -Wall -Wextra -fsanitize=address,undefined app.cpp -o app

# Find leaks
valgrind --leak-check=full ./app

Turn on the safety nets

Always build with -Wall -Wextra and test with -fsanitize=address,undefined. AddressSanitizer catches use-after-free, buffer overflows, and leaks at runtime with minimal setup.

Practice Exercises

  1. Write a C program that reverses an array in place using two pointers walking toward the middle — no extra array allowed.
  2. Implement a C string-length function my_strlen using pointer arithmetic, and verify it against strlen.
  3. Write a C++ class that manages a heap buffer and correctly implements the Rule of 5 (destructor, copy, move). Then rewrite it following the Rule of 0 with std::vector.
  4. Build a small class hierarchy (Shape base with Circle and Square) using a virtual area(), and store them in a std::vector<std::unique_ptr<Shape>>.
  5. Write a templated min_of function constrained by a C++20 concept so it only accepts comparable types.
  6. Read a file into an std::unordered_map<std::string, int> word-frequency counter, then print the top 5 words sorted by count using std::sort. Run it under valgrind to confirm zero leaks.

Section navigation