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[]
| Aspect | malloc / free (C) | new / delete (C++) |
|---|---|---|
| Returns | void *, needs cast | Typed pointer |
| Runs constructor | No | Yes |
| Size | You compute bytes | Inferred from type |
| On failure | Returns NULL | Throws 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.
| Feature | Pointer | Reference |
|---|---|---|
| Can be null | Yes | No |
| Reassignable | Yes | No (bound once) |
| Arithmetic | Yes | No |
| Syntax to use | Dereference 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.
| Type | Ownership | Use when |
|---|---|---|
unique_ptr<T> | Exclusive | One owner; cheapest, default choice |
shared_ptr<T> | Shared (refcount) | Multiple owners, freed at last one |
weak_ptr<T> | Non-owning observer | Break 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
- Write a C program that reverses an array in place using two pointers walking toward the middle — no extra array allowed.
- Implement a C string-length function
my_strlenusing pointer arithmetic, and verify it againststrlen. - 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. - Build a small class hierarchy (
Shapebase withCircleandSquare) using a virtualarea(), and store them in astd::vector<std::unique_ptr<Shape>>. - Write a templated
min_offunction constrained by a C++20 concept so it only accepts comparable types. - Read a file into an
std::unordered_map<std::string, int>word-frequency counter, then print the top 5 words sorted by count usingstd::sort. Run it under valgrind to confirm zero leaks.