Hello World & Build
Hello, World
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
} #include <iostream>
int main() {
std::cout << "Hello, World!\n";
return 0;
} std::cout is a stream object from <iostream>; << is the stream insertion operator. No format-string verbs like %s or %d — any type that defines operator<< can be streamed directly. std::endl also works but flushes the buffer on every call; "\n" is faster when flushing isn't needed.Compile & run
// gcc -std=c17 -Wall -Wextra -o hello hello.c && ./hello
// With math library: gcc hello.c -lm -o hello
// With sanitizers: gcc -fsanitize=address,undefined hello.c -o hello // g++ -std=c++23 -Wall -Wextra -o hello hello.cpp && ./hello
// With sanitizers: g++ -std=c++23 -fsanitize=address,undefined hello.cpp -o hello
// With optimisation: g++ -std=c++23 -O2 -o hello hello.cpp g++ links the C++ standard library (libstdc++) automatically; using plain gcc to link C++ object files requires adding -lstdc++. The -std=c++23 flag unlocks the full C++23 standard library including std::format, ranges, and std::expected.C headers in C++
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(void) {
printf("sqrt(2) = %.4f\n", sqrt(2.0));
return 0;
} #include <cstdio>
#include <cstdlib>
#include <cmath>
#include <iostream>
int main() {
// C names also live in std:: via <c...> headers
std::printf("sqrt(2) = %.4f\n", std::sqrt(2.0));
return 0;
} C++ provides C's standard headers as
<c...> variants — <cstdio>, <cmath>, <cstring> — where every symbol also lives in the std:: namespace. The old <stdio.h> form still works but dumps names into the global namespace. Prefer <c...> in new C++ code.Formatted output
#include <stdio.h>
int main(void) {
const char *name = "Alice";
int age = 30;
double score = 98.5;
printf("%-10s age %3d score %6.2f\n", name, age, score);
return 0;
} #include <format>
#include <iostream>
int main() {
std::string name = "Alice";
int age = 30;
double score = 98.5;
std::cout << std::format("{:<10} age {:3} score {:6.2f}\n",
name, age, score);
return 0;
} std::format (C++23) provides Python-style {} placeholders with type-safe formatting — no %s/%d mismatch possible. Alignment uses < (left), > (right), ^ (center). printf still works in C++ but loses type safety.Namespaces
Namespace declaration
#include <stdio.h>
// C has no namespaces — prefix everything manually
double geometry_area_circle(double radius);
double geometry_area_rect(double width, double height);
double geometry_area_circle(double radius) { return 3.14159 * radius * radius; }
double geometry_area_rect(double width, double height) { return width * height; }
int main(void) {
printf("%.2f\n", geometry_area_circle(5.0));
printf("%.2f\n", geometry_area_rect(4.0, 6.0));
return 0;
} #include <iostream>
#include <numbers>
namespace geometry {
double area_circle(double radius) { return std::numbers::pi * radius * radius; }
double area_rect(double width, double height) { return width * height; }
}
int main() {
std::cout << geometry::area_circle(5.0) << "\n";
std::cout << geometry::area_rect(4.0, 6.0) << "\n";
return 0;
} Namespaces replace the C convention of prefixing every symbol with
geometry_. Access is explicit with ::, keeping names short inside the namespace without risking collisions outside. std::numbers::pi (C++20) is the properly rounded compile-time constant; no M_PI needed.using namespace
#include <stdio.h>
// In C, all names are global — no way to opt in or out
int add(int a, int b) { return a + b; }
int main(void) {
printf("%d\n", add(3, 4));
return 0;
} #include <iostream>
namespace math {
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
}
// using math::add; // bring one name into scope
// using namespace math; // bring all names — avoid in headers
int main() {
using math::add; // scoped to this block
std::cout << add(3, 4) << "\n";
std::cout << math::multiply(3, 4) << "\n";
return 0;
} using namespace std; is common in tutorials but risky in header files — it forces every includer to accept all 1000+ names from std:: into their global namespace, creating invisible collision hazards. Prefer qualified names (std::cout) or targeted using std::cout; declarations scoped to a function.Nested namespaces
#include <stdio.h>
// C: double-prefixing convention, gets verbose fast
void mylib_net_http_get(const char *url) {
printf("GET %s\n", url);
}
int main(void) {
mylib_net_http_get("https://example.com");
return 0;
} #include <iostream>
#include <string>
namespace mylib::net::http { // C++17 nested namespace shorthand
void get(const std::string &url) {
std::cout << "GET " << url << "\n";
}
}
int main() {
mylib::net::http::get("https://example.com");
using namespace mylib::net::http; // bring this sub-namespace in
get("https://example.com/api");
return 0;
} The
namespace A::B::C {} shorthand (C++17) avoids the old triple-nested brace pyramid. The fully qualified name mylib::net::http::get is unambiguous regardless of scope. For deeply nested namespaces, a namespace alias (namespace http = mylib::net::http;) provides a shorter alias.Variables & Types
auto type deduction
#include <stdio.h>
#include <stddef.h>
int main(void) {
// Every type must be written out explicitly
int count = 42;
unsigned long length = 100UL;
double ratio = 3.14;
printf("%d %lu %.2f\n", count, length, ratio);
return 0;
} #include <iostream>
#include <vector>
#include <string>
int main() {
auto count = 42; // int — deduced from literal
auto length = 100UL; // unsigned long
auto ratio = 3.14; // double
auto greeting = std::string("hello");
std::vector<int> numbers = {1, 2, 3};
auto it = numbers.begin(); // std::vector<int>::iterator — without auto: painful
std::cout << count << " " << length << " " << ratio << " " << greeting << "\n";
return 0;
} auto deduces the type at compile time from the initializer — the type is still fixed and static, not dynamic like a scripting language. It eliminates redundant repetition (std::map<std::string, std::vector<int>>::iterator) while keeping full type safety. Always provide an initializer; auto x; is a compile error.nullptr vs NULL
#include <stdio.h>
#include <stdlib.h>
void process(int *pointer) { printf("pointer is null\n"); }
// In C, NULL is 0 — this is ambiguous:
// void process(int value) would be called if NULL is 0
// void process(int *pointer) should be called for null pointer
int main(void) {
int *pointer = NULL; // NULL is ((void*)0) — an integer constant
if (pointer == NULL)
printf("null\n");
free(NULL); // safe in C
return 0;
} #include <iostream>
#include <memory>
void process(int value) { std::cout << "got int\n"; }
void process(int *pointer) { std::cout << "got pointer\n"; }
int main() {
int *pointer = nullptr; // typed null pointer constant, not an integer
if (pointer == nullptr)
std::cout << "null\n";
// process(NULL); // ambiguous: could call either overload
process(nullptr); // unambiguous: calls pointer overload
return 0;
} nullptr has type std::nullptr_t, not int. This fixes NULL's ambiguity: in C++, NULL is the integer 0, so f(NULL) could resolve to f(int) instead of f(int*). nullptr always resolves to the pointer overload. Never use NULL in new C++ code.Type aliases
#include <stdio.h>
typedef unsigned long Count;
// typedef for function pointers is unreadable:
typedef void (*Callback)(int);
typedef struct { float x; float y; } Point;
int main(void) {
Count items = 5;
Point origin = {0.0f, 0.0f};
printf("%lu items at (%.1f, %.1f)\n", items, origin.x, origin.y);
return 0;
} #include <iostream>
using Count = unsigned long;
using Callback = void(*)(int); // readable left-to-right
using Point = struct { float x; float y; };
int main() {
Count items = 5;
Point origin = {0.0f, 0.0f};
std::cout << items << " items at (" << origin.x << ", " << origin.y << ")\n";
return 0;
} using Name = Type; replaces typedef and reads naturally left-to-right for all types, including function pointers. Both are valid in C++; using is preferred in modern code. In C++, struct names are already usable without typedef — no typedef struct { ... } Name; idiom needed.constexpr vs #define
#include <stdio.h>
#define MAX_SIZE 100 // preprocessor: untyped, no scope, no debug
#define SQUARE(x) ((x)*(x)) // macro: no type safety, evaluates x twice
const int limit = 50; // C99: runtime constant (no compile-time guarantee)
int main(void) {
printf("%d %d %d\n", MAX_SIZE, SQUARE(7), limit);
return 0;
} #include <iostream>
constexpr int max_size = 100;
constexpr int square(int value) { return value * value; }
// constexpr if: compile-time branch — no macro tricks needed
template<typename T>
constexpr T clamp(T value, T low, T high) {
return value < low ? low : (value > high ? high : value);
}
int main() {
constexpr int result = square(7); // computed at compile time
constexpr int clamped = clamp(150, 0, 100);
std::cout << max_size << " " << result << " " << clamped << "\n";
return 0;
} constexpr values and functions are evaluated at compile time, are type-safe, respect scopes, and are visible to the debugger — unlike #define macros, which are handled by the preprocessor before compilation. A constexpr function can also be called at runtime with non-constant arguments, making it more versatile than a C macro.bool as a real type
#include <stdio.h>
#include <stdbool.h> // C99: bool, true, false
int main(void) {
bool flag = true;
bool weird = 42; // converted to 1 (true)
printf("%d %d\n", flag, weird);
// printf has no bool format — print 0 or 1
int result = (3 > 2); // comparison returns int, not bool
printf("%d\n", result);
return 0;
} #include <iostream>
int main() {
bool flag = true;
bool other = false;
std::cout << std::boolalpha; // print "true"/"false" not 1/0
std::cout << flag << "\n"; // true
std::cout << other << "\n"; // false
std::cout << (3 > 2) << "\n"; // true
std::cout << (1 == 2) << "\n"; // false
return 0;
} In C++,
bool is a built-in type and true/false are keywords — no <stdbool.h> needed. Comparisons and logical operators return bool, not int. std::boolalpha makes cout print true/false instead of 1/0.References
Lvalue references
#include <stdio.h>
void increment(int *value) {
(*value)++; // explicit dereference required
}
int main(void) {
int counter = 10;
increment(&counter); // caller must take address explicitly
printf("%d\n", counter);
return 0;
} #include <iostream>
void increment(int &value) { // reference: alias, cannot be null, cannot be reseated
value++; // no dereference needed
}
int main() {
int counter = 10;
increment(counter); // no & at call site
std::cout << counter << "\n";
int &alias = counter; // alias for counter
alias = 99;
std::cout << counter << "\n"; // 99
return 0;
} A reference is an alias for an existing variable — not a separate pointer value. It cannot be null, cannot be reseated to a different variable after initialization, and needs no
* to dereference. At the call site the & is implicit. Internally the compiler usually implements references as pointers.const references
#include <stdio.h>
#include <string.h>
typedef struct { char name[64]; int score; } Player;
// Pass pointer to avoid copying the struct
void print_player(const Player *player) {
printf("%s: %d\n", player->name, player->score);
}
int main(void) {
Player hero = {"Arthur", 99};
print_player(&hero); // caller must take address
return 0;
} #include <iostream>
#include <string>
struct Player { std::string name; int score; };
// const ref: no copy, no modification, no explicit & at call site
void print_player(const Player &player) {
std::cout << player.name << ": " << player.score << "\n";
}
int main() {
Player hero = {"Arthur", 99};
print_player(hero); // reads naturally
return 0;
} const T& is the C++ idiom for "read-only, no copy" — the C++ equivalent of const T * but without the & at the call site or -> for member access. For any type larger than a pointer (structs, strings, vectors), prefer const T& over pass-by-value to avoid an unnecessary deep copy.Swap via reference
#include <stdio.h>
void swap_ints(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int x = 10, y = 20;
swap_ints(&x, &y);
printf("%d %d\n", x, y);
return 0;
} #include <iostream>
#include <utility> // std::swap
#include <string>
int main() {
int x = 10, y = 20;
std::swap(x, y);
std::cout << x << " " << y << "\n";
// std::swap works on any type — no separate implementations
std::string first = "hello";
std::string second = "world";
std::swap(first, second);
std::cout << first << " " << second << "\n";
return 0;
} std::swap works on any assignable type without duplicating code. For containers (std::vector, std::string), it is O(1) — it swaps internal pointers rather than copying elements. In C, you would need a separate swap per type, or an unsafe void*/memcpy version.Move semantics
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Transfer ownership manually: return heap-allocated buffer,
// caller must free — easy to leak or double-free
char *make_greeting(const char *name) {
char *buffer = malloc(64);
snprintf(buffer, 64, "Hello, %s!", name);
return buffer;
}
int main(void) {
char *greeting = make_greeting("World");
printf("%s\n", greeting);
free(greeting);
return 0;
} #include <iostream>
#include <string>
#include <utility>
std::string make_greeting(const std::string &name) {
return "Hello, " + name + "!"; // NRVO: no copy in practice
}
int main() {
std::string greeting = make_greeting("World");
std::cout << greeting << "\n";
std::string source = "expensive string data";
std::string destination = std::move(source); // O(1) transfer, no copy
std::cout << destination << "\n";
std::cout << "source empty: " << std::boolalpha << source.empty() << "\n";
return 0;
} std::move transfers ownership of a value without copying — the source is left in a valid but unspecified state (usually empty). For std::string and containers this is O(1) instead of O(n). The compiler often applies Named Return Value Optimization (NRVO) automatically, eliminating even the move for returned locals.Strings
std::string vs char*
#include <stdio.h>
#include <string.h>
int main(void) {
char greeting[32] = "Hello";
strncat(greeting, ", World!", sizeof(greeting) - strlen(greeting) - 1);
printf("%s\n", greeting);
printf("Length: %zu\n", strlen(greeting));
char copy[32];
strncpy(copy, greeting, sizeof(copy) - 1);
copy[sizeof(copy) - 1] = '\0'; // must null-terminate manually
printf("%s\n", copy);
return 0;
} #include <iostream>
#include <string>
int main() {
std::string greeting = "Hello";
greeting += ", World!"; // safe, auto-resizes
std::cout << greeting << "\n";
std::cout << "Length: " << greeting.size() << "\n";
std::string copy = greeting; // deep copy, no size management
std::cout << copy << "\n";
return 0;
} std::string manages its own memory — no fixed-size buffers, no strcat overflow risk, no manual null termination. Assignment copies deeply. The + and += operators concatenate safely. Call .c_str() to get a const char * for C API compatibility.String operations
#include <stdio.h>
#include <string.h>
int main(void) {
const char *sentence = "The quick brown fox";
printf("Length: %zu\n", strlen(sentence));
// Substring via pointer arithmetic and width specifier
printf("%.5s\n", sentence + 4); // "quick"
const char *found = strstr(sentence, "brown");
if (found)
printf("Found at %td\n", found - sentence);
return 0;
} #include <iostream>
#include <string>
int main() {
std::string sentence = "The quick brown fox";
std::cout << "Length: " << sentence.size() << "\n";
std::cout << sentence.substr(4, 5) << "\n"; // "quick"
auto position = sentence.find("brown");
if (position != std::string::npos)
std::cout << "Found at " << position << "\n";
// Replace, erase, insert
sentence.replace(4, 5, "slow");
std::cout << sentence << "\n";
return 0;
} substr(pos, len) returns a new std::string — no pointer arithmetic. find returns std::string::npos (the maximum size_t value) when not found — always check against npos, not -1. replace, insert, and erase modify the string in place.String ↔ number conversion
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// Number → string
char buffer[32];
snprintf(buffer, sizeof(buffer), "%d", 42);
printf("'%s'\n", buffer);
// String → number (atoi returns 0 on failure — silent)
int value = atoi("123");
printf("%d\n", value + 1);
return 0;
} #include <iostream>
#include <string>
#include <stdexcept>
int main() {
// Number → string
std::string text = std::to_string(42);
std::cout << "'" << text << "'\n";
// String → number (throws on failure)
int value = std::stoi("123");
std::cout << value + 1 << "\n";
std::cout << std::stod("3.14") << "\n";
try {
std::stoi("not a number"); // throws std::invalid_argument
} catch (const std::exception &error) {
std::cout << "Error: " << error.what() << "\n";
}
return 0;
} std::stoi/std::stod throw std::invalid_argument if the string is not a valid number and std::out_of_range if the value overflows — unlike atoi, which silently returns 0 on failure. std::to_string replaces snprintf into a fixed buffer.std::string_view
#include <stdio.h>
#include <string.h>
// C: non-owning string views are const char* + length — no standard type
void print_first(const char *text, size_t length) {
printf("%.*s\n", (int)length, text);
}
int main(void) {
const char *message = "Hello, World!";
print_first(message, 5); // "Hello"
print_first(message + 7, 5); // "World"
return 0;
} #include <iostream>
#include <string>
#include <string_view>
void print_first(std::string_view text, size_t length) {
std::cout << text.substr(0, length) << "\n";
}
int main() {
std::string message = "Hello, World!";
print_first(message, 5); // no copy — views string's buffer
print_first("Hello, World!", 5); // also accepts string literals
print_first({message.data() + 7, 5}, 5); // views a sub-range
return 0;
} std::string_view is a non-owning read-only reference to a string — it stores a pointer and length without allocating. It accepts both std::string and const char * without conversion. Never return or store a string_view that may outlive its source — it is a view, not an owner.Arrays & Vectors
Dynamic arrays
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int capacity = 4, count = 0;
int *numbers = malloc(capacity * sizeof(int));
for (int i = 1; i <= 6; i++) {
if (count == capacity) {
capacity *= 2;
numbers = realloc(numbers, capacity * sizeof(int));
}
numbers[count++] = i * i;
}
for (int i = 0; i < count; i++)
printf("%d ", numbers[i]);
printf("\n");
free(numbers);
return 0;
} #include <iostream>
#include <vector>
int main() {
std::vector<int> numbers; // auto-resizing, memory-managed
for (int i = 1; i <= 6; i++)
numbers.push_back(i * i);
for (int number : numbers)
std::cout << number << " ";
std::cout << "\n";
return 0; // vector freed automatically
} std::vector<T> replaces the malloc/realloc/free dance. It doubles capacity when full (amortized O(1) push_back) and frees memory when it goes out of scope. Elements are contiguous — numbers.data() returns a T* for C API calls.Fixed-size arrays
#include <stdio.h>
// C array loses its size when passed to a function
void print_array(int *items, size_t count) {
for (size_t i = 0; i < count; i++)
printf("%d ", items[i]);
printf("\n");
}
int main(void) {
int primes[] = {2, 3, 5, 7, 11};
size_t count = sizeof(primes) / sizeof(primes[0]);
print_array(primes, count); // must pass count separately
return 0;
} #include <iostream>
#include <array>
#include <algorithm>
void print_array(const std::array<int, 5> &items) {
for (int item : items)
std::cout << item << " ";
std::cout << "\n";
}
int main() {
std::array<int, 5> primes = {2, 3, 5, 7, 11};
std::cout << "Size: " << primes.size() << "\n";
print_array(primes); // size is part of the type
std::sort(primes.begin(), primes.end()); // STL algorithms work
return 0;
} std::array<T, N> is a thin wrapper around a C array that knows its own size and works with STL algorithms. Unlike a plain C array, it does not decay to a pointer when passed to functions, so size() is always available. Stack-allocated, zero overhead — use it instead of int arr[N] in C++.Range-based for
#include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
int count = sizeof(numbers) / sizeof(numbers[0]);
// Must track length; index off-by-one errors are common
for (int i = 0; i < count; i++)
printf("%d\n", numbers[i]);
return 0;
} #include <iostream>
#include <vector>
#include <map>
#include <string>
int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
for (int number : numbers) // read-only
std::cout << number << " ";
std::cout << "\n";
for (int &number : numbers) // modify in place
number *= 2;
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto &[name, score] : scores) // structured binding (C++17)
std::cout << name << ": " << score << "\n";
return 0;
} Range-based
for works on anything with begin() and end() — vectors, arrays, strings, maps, and custom types. Use const auto & for large elements to avoid copies; use auto & to modify elements in place. Structured bindings (auto &[key, value]) unpack map pairs cleanly.Initializer lists
#include <stdio.h>
typedef struct { int x; int y; int z; } Vector3;
int main(void) {
// C99 designated initializers
Vector3 position = {.x = 1, .y = 2, .z = 3};
int numbers[5] = {10, 20, 30, 40, 50};
printf("(%d, %d, %d)\n", position.x, position.y, position.z);
return 0;
} #include <iostream>
#include <vector>
#include <map>
#include <string>
struct Vector3 { int x; int y; int z; };
int main() {
Vector3 position = {1, 2, 3}; // struct
std::vector<int> numbers = {10, 20, 30, 40, 50}; // vector
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
// Narrowing conversions are errors:
// int value = {3.14}; // compile error
std::cout << "(" << position.x << ", " << position.y << ", " << position.z << ")\n";
std::cout << numbers[2] << "\n";
std::cout << scores["Alice"] << "\n";
return 0;
} Brace initialization (
{}) works uniformly for scalars, structs, vectors, and maps in C++11+. It prevents narrowing conversions at compile time — int value = {3.14}; is an error, not a silent truncation. C++20 added designated initializers matching C99's .field = value syntax.Classes & OOP
struct vs class
#include <stdio.h>
// C struct: all public, no methods, use free functions
typedef struct {
char name[64];
int age;
} Person;
void person_greet(const Person *person) {
printf("Hi, I'm %s, age %d.\n", person->name, person->age);
}
int main(void) {
Person alice = {"Alice", 30};
person_greet(&alice);
return 0;
} #include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
void greet() const { // const: does not modify the object
std::cout << "Hi, I'm " << name << ", age " << age << ".\n";
}
};
int main() {
Person alice = {"Alice", 30};
alice.greet();
return 0;
} In C++,
struct and class are identical except for default access: struct members are public by default, class members are private. Methods defined inside the class body are implicitly inline. The const qualifier after a method signature means the method guarantees it will not modify the object.Constructors & destructors
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { char *name; int age; } Person;
Person *person_create(const char *name, int age) {
Person *person = malloc(sizeof(Person));
person->name = strdup(name);
person->age = age;
printf("Created %s\n", person->name);
return person;
}
void person_destroy(Person *person) {
printf("Destroyed %s\n", person->name);
free(person->name);
free(person);
}
int main(void) {
Person *alice = person_create("Alice", 30);
printf("%s is %d\n", alice->name, alice->age);
person_destroy(alice);
return 0;
} #include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
Person(std::string name, int age)
: name(std::move(name)), age(age) { // member initializer list
std::cout << "Created " << this->name << "\n";
}
~Person() {
std::cout << "Destroyed " << name << "\n";
}
};
int main() {
Person alice("Alice", 30);
std::cout << alice.name << " is " << alice.age << "\n";
} // destructor called automatically — no manual free The constructor runs automatically at creation; the destructor runs automatically when the object leaves scope. No separate
_create/_destroy pattern, no manual free. The member initializer list (: name(...), age(...)) initializes members before the constructor body — the only way to initialize const or reference members.Access control
#include <stdio.h>
#include <stdlib.h>
// Opaque pointer: hide internals in a .c file
// Users see only the type — hacky, requires heap allocation
typedef struct BankAccount BankAccount;
struct BankAccount { double balance; }; // in .c only
BankAccount *account_create(double initial) {
BankAccount *account = malloc(sizeof(BankAccount));
account->balance = initial;
return account;
}
void account_deposit(BankAccount *account, double amount) { account->balance += amount; }
double account_balance(const BankAccount *account) { return account->balance; }
int main(void) {
BankAccount *account = account_create(100.0);
account_deposit(account, 50.0);
printf("%.2f\n", account_balance(account));
free(account);
return 0;
} #include <iostream>
class BankAccount {
private:
double balance; // hidden from users at compile time
public:
explicit BankAccount(double initial) : balance(initial) {}
void deposit(double amount) { balance += amount; }
double get_balance() const { return balance; }
};
int main() {
BankAccount account(100.0);
account.deposit(50.0);
std::cout << account.get_balance() << "\n";
// account.balance = 0; // compile error: 'balance' is private
return 0;
} private enforces encapsulation at compile time — no opaque pointer tricks or separate translation units needed. explicit on a single-argument constructor prevents accidental implicit conversions like BankAccount account = 100.0;. The compiler rejects any access to private members from outside the class.Inheritance & virtual
#include <stdio.h>
// C polymorphism: manual vtable via function pointers
typedef struct {
void (*describe)(void *self);
} ShapeVTable;
typedef struct { ShapeVTable *vtable; float x, y; } Shape;
typedef struct { Shape base; float radius; } Circle;
void circle_describe(void *self) {
Circle *circle = (Circle *)self;
printf("circle r=%.1f at (%.1f,%.1f)\n",
circle->radius, circle->base.x, circle->base.y);
}
int main(void) {
ShapeVTable circle_vtable = {circle_describe};
Circle circle = {{&circle_vtable, 1.0f, 2.0f}, 5.0f};
circle.base.vtable->describe(&circle);
return 0;
} #include <iostream>
#include <string>
#include <memory>
struct Shape {
float x, y;
virtual std::string describe() const = 0; // pure virtual: must override
virtual ~Shape() = default;
};
struct Circle : public Shape {
float radius;
Circle(float x, float y, float r) { this->x = x; this->y = y; radius = r; }
std::string describe() const override {
return "circle r=" + std::to_string(radius);
}
};
struct Rectangle : public Shape {
float width, height;
Rectangle(float x, float y, float w, float h) {
this->x = x; this->y = y; width = w; height = h;
}
std::string describe() const override {
return "rect " + std::to_string(width) + "x" + std::to_string(height);
}
};
int main() {
std::unique_ptr<Shape> shapes[] = {
std::make_unique<Circle>(1, 2, 5.0f),
std::make_unique<Rectangle>(0, 0, 4.0f, 6.0f),
};
for (auto &shape : shapes)
std::cout << shape->describe() << "\n";
return 0;
} virtual enables dynamic dispatch — calling through a base pointer or reference invokes the most-derived override. A pure virtual function (= 0) makes the class abstract. Always declare the base destructor virtual (or = default) to prevent undefined behavior when deleting a derived object through a base pointer. The compiler generates the vtable automatically.Struct methods & operators
#include <stdio.h>
#include <math.h>
typedef struct { double x; double y; } Vec2;
Vec2 vec2_add(Vec2 a, Vec2 b) { return (Vec2){a.x + b.x, a.y + b.y}; }
double vec2_length(Vec2 v) { return sqrt(v.x * v.x + v.y * v.y); }
int main(void) {
Vec2 a = {3.0, 4.0};
Vec2 b = {1.0, 2.0};
Vec2 result = vec2_add(a, b);
printf("(%.1f, %.1f) len=%.2f\n", result.x, result.y, vec2_length(result));
return 0;
} #include <iostream>
#include <cmath>
struct Vec2 {
double x, y;
Vec2 operator+(const Vec2 &other) const {
return {x + other.x, y + other.y};
}
double length() const { return std::sqrt(x * x + y * y); }
};
int main() {
Vec2 a = {3.0, 4.0};
Vec2 b = {1.0, 2.0};
Vec2 result = a + b; // natural syntax via operator+
std::cout << "(" << result.x << ", " << result.y
<< ") len=" << result.length() << "\n";
return 0;
} Operator overloading lets user-defined types use built-in syntax. The compiler translates
a + b into a.operator+(b). Prefer implementing binary operators as non-member functions (or friends) when both operands should be treated symmetrically. Overload sparingly — only when the operation has an obvious, unsurprising meaning.RAII & Smart Pointers
RAII principle
#include <stdio.h>
#include <stdlib.h>
// C: acquire + remember to release on every exit path
int process_data(void) {
void *buffer = malloc(1024);
if (!buffer) return -1;
FILE *file = fopen("data.txt", "r");
if (!file) {
free(buffer); // must free before every early return
return -2;
}
// ... work ...
fclose(file);
free(buffer);
return 0;
}
int main(void) { printf("%d\n", process_data()); return 0; } #include <iostream>
#include <vector>
#include <fstream>
// C++: constructor acquires, destructor releases — automatic on any exit
class FileGuard {
std::FILE *file;
public:
explicit FileGuard(const char *path) : file(std::fopen(path, "r")) {}
~FileGuard() { if (file) std::fclose(file); }
bool is_open() const { return file != nullptr; }
};
int process_data() {
std::vector<char> buffer(1024); // freed by destructor
FileGuard guard("data.txt"); // closed by destructor
if (!guard.is_open()) return -2; // both are cleaned up automatically
std::cout << "processing\n";
return 0;
} // destructors run here — always, even if an exception is thrown
int main() { std::cout << process_data() << "\n"; return 0; } RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime. Constructors acquire resources; destructors release them. Because destructors run automatically when a scope exits — whether by return, exception, or fall-through — there is no "remember to free on every path" problem.
std::vector, std::fstream, and smart pointers are all RAII types.unique_ptr (exclusive ownership)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { char name[64]; int value; } Resource;
Resource *resource_create(const char *name, int value) {
Resource *res = malloc(sizeof(Resource));
strncpy(res->name, name, 63);
res->value = value;
return res;
}
int main(void) {
Resource *res = resource_create("widget", 42);
printf("%s: %d\n", res->name, res->value);
free(res); // easy to forget, double-free, or use after free
return 0;
} #include <iostream>
#include <memory>
#include <string>
struct Resource {
std::string name;
int value;
};
int main() {
// make_unique: allocates + wraps in unique_ptr in one shot
auto resource = std::make_unique<Resource>(Resource{"widget", 42});
std::cout << resource->name << ": " << resource->value << "\n";
// Transfer ownership — source is now null
auto other = std::move(resource);
std::cout << (resource == nullptr) << "\n"; // 1
std::cout << other->name << "\n";
return 0; // other's destructor calls delete automatically
} std::unique_ptr owns the object exclusively — only one unique_ptr can point to a given object at a time. Ownership can be transferred with std::move but not copied. When the unique_ptr is destroyed, it calls delete automatically. Use std::make_unique rather than new directly.shared_ptr (shared ownership)
#include <stdio.h>
#include <stdlib.h>
// C: shared ownership requires manual reference counting — error-prone
typedef struct {
int value;
int ref_count;
} SharedData;
SharedData *shared_create(int value) {
SharedData *data = malloc(sizeof(SharedData));
data->value = value;
data->ref_count = 1;
return data;
}
void shared_retain(SharedData *data) { data->ref_count++; }
void shared_release(SharedData *data) {
if (--data->ref_count == 0) free(data);
}
int main(void) {
SharedData *a = shared_create(42);
SharedData *b = a; shared_retain(b);
shared_release(a);
printf("%d\n", b->value);
shared_release(b);
return 0;
} #include <iostream>
#include <memory>
int main() {
auto first = std::make_shared<int>(42); // ref count = 1
{
auto second = first; // ref count = 2, no copy
std::cout << *second << "\n";
std::cout << "count: " << first.use_count() << "\n"; // 2
} // second destroyed; ref count drops to 1
std::cout << "count: " << first.use_count() << "\n"; // 1
std::cout << *first << "\n";
return 0; // first destroyed; ref count 0 → delete called
} std::shared_ptr implements reference-counted shared ownership. Each copy increments the count; each destruction decrements it. When the count reaches zero the object is deleted. Use std::weak_ptr to break ownership cycles — a weak_ptr does not increment the count and must be locked to access the object.Avoiding new/delete
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
// Every malloc must have exactly one matching free
int *numbers = malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) numbers[i] = i * i;
for (int i = 0; i < 5; i++) printf("%d ", numbers[i]);
printf("\n");
free(numbers); // forget this → memory leak
char *message = strdup("hello");
printf("%s\n", message);
free(message);
return 0;
} #include <iostream>
#include <vector>
#include <string>
int main() {
// Stack-allocated: destructor runs automatically
std::vector<int> numbers = {0, 1, 4, 9, 16};
for (int number : numbers)
std::cout << number << " ";
std::cout << "\n";
std::string message = "hello";
std::cout << message << "\n";
// If you need heap allocation, use smart pointers:
// auto ptr = std::make_unique<MyClass>(...);
// Never write: new / delete / malloc / free in modern C++
return 0;
} In modern C++, raw
new/delete and malloc/free should almost never appear in application code. std::vector and std::string handle dynamic memory internally. When heap allocation is truly needed, std::make_unique or std::make_shared are the correct tools.Templates
Function templates
#include <stdio.h>
// C: separate function per type, or unsafe void* + macro
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // no type safety, evaluates twice
int max_int (int a, int b) { return a > b ? a : b; }
double max_double(double a, double b) { return a > b ? a : b; }
int main(void) {
printf("%d\n", max_int(3, 7));
printf("%.1f\n", max_double(3.5, 2.1));
return 0;
} #include <iostream>
// One template, any comparable type, zero runtime overhead
template<typename T>
T maximum(T a, T b) { return a > b ? a : b; }
int main() {
std::cout << maximum(3, 7) << "\n"; // T = int
std::cout << maximum(3.5, 2.1) << "\n"; // T = double
std::cout << maximum('a', 'z') << "\n"; // T = char
return 0;
} Templates are resolved at compile time — the compiler generates a separate copy of the function for each type used, with zero runtime overhead. Unlike C macros, templates are type-safe, respect scopes, work with references, and can be debugged. The C++ Standard Library is built almost entirely on templates.
Class templates
#include <stdio.h>
#include <stdlib.h>
// C: a generic stack requires void* and manual size tracking
typedef struct {
void **data;
size_t top, capacity;
size_t element_size;
} Stack;
// Lots of boilerplate; users must cast void* back to their type
int main(void) {
printf("no type-safe generic stack in C without heavy macros\n");
return 0;
} #include <iostream>
#include <vector>
#include <stdexcept>
template<typename T>
class Stack {
std::vector<T> data;
public:
void push(T value) { data.push_back(std::move(value)); }
T pop() {
if (data.empty()) throw std::underflow_error("empty stack");
T value = std::move(data.back());
data.pop_back();
return value;
}
bool empty() const { return data.empty(); }
};
int main() {
Stack<int> numbers;
numbers.push(1);
numbers.push(2);
numbers.push(3);
std::cout << numbers.pop() << "\n"; // 3
std::cout << numbers.pop() << "\n"; // 2
return 0;
} A class template is instantiated for each type argument used —
Stack<int> and Stack<std::string> are distinct classes, each fully type-safe. All template code is typically in headers because the compiler must see the definition to instantiate it.Variadic templates
#include <stdio.h>
#include <stdarg.h>
// C: variadic functions use va_args — no type safety, no compile-time checks
int sum_ints(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("%d\n", sum_ints(4, 1, 2, 3, 4)); // must pass count manually
return 0;
} #include <iostream>
// Variadic template: type-safe, any number of args, zero overhead
template<typename T>
T sum(T value) { return value; }
template<typename T, typename... Rest>
T sum(T first, Rest... rest) { return first + sum(rest...); }
int main() {
std::cout << sum(1, 2, 3, 4) << "\n"; // 10
std::cout << sum(1.5, 2.5, 3.0) << "\n"; // 7
std::cout << sum(std::string("a"), std::string("b"), std::string("c")) << "\n"; // abc
return 0;
} Variadic templates accept any number and mix of types, resolved at compile time. No
va_args hacks, no "count" parameter, no casts. The pack expansion rest... recursively unpacks the argument list. C++17 fold expressions simplify this further: (... + args) expands directly.Concepts (C++20)
#include <stdio.h>
// C: no way to constrain macro or void* generics
// Type errors appear as cryptic runtime bugs or memory corruption
#define DOUBLE_VALUE(x) ((x) + (x))
int main(void) {
printf("%d\n", DOUBLE_VALUE(5));
// DOUBLE_VALUE("hello") would compile but corrupt memory
printf("no constraints in C\n");
return 0;
} #include <iostream>
#include <concepts>
// Concept: named, reusable type constraint
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
T double_value(T value) { return value + value; }
// Concept directly in function signature (abbreviated template)
auto triple_value(std::integral auto value) { return value * 3; }
int main() {
std::cout << double_value(5) << "\n"; // 10
std::cout << double_value(3.14) << "\n"; // 6.28
std::cout << triple_value(7) << "\n"; // 21
// double_value("hello"); // compile error with readable message
return 0;
} Concepts (C++20) name and enforce constraints on template arguments. Violations produce readable error messages at the call site ("constraint not satisfied") rather than deep template instantiation walls. The standard library provides built-in concepts:
std::integral, std::floating_point, std::copyable, std::invocable, and many more.STL Containers
std::map (ordered)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// C: no built-in associative container
// Typical approach: sorted array + bsearch, or hand-rolled hash table
typedef struct { char key[32]; int value; } Entry;
int compare_entries(const void *a, const void *b) {
return strcmp(((Entry *)a)->key, ((Entry *)b)->key);
}
int main(void) {
Entry table[] = {{"alice", 95}, {"bob", 87}, {"carol", 91}};
qsort(table, 3, sizeof(Entry), compare_entries);
Entry key = {"bob", 0};
Entry *found = bsearch(&key, table, 3, sizeof(Entry), compare_entries);
if (found) printf("bob: %d\n", found->value);
return 0;
} #include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> scores;
scores["alice"] = 95;
scores["bob"] = 87;
scores["carol"] = 91;
std::cout << "bob: " << scores["bob"] << "\n";
// Iteration is sorted by key automatically
for (const auto &[name, score] : scores)
std::cout << name << ": " << score << "\n";
// Safe lookup that doesn't insert
auto it = scores.find("dave");
if (it == scores.end())
std::cout << "dave not found\n";
return 0;
} std::map<K,V> is a sorted associative container (red-black tree) with O(log n) lookup. Iteration visits keys in sorted order. Note that scores["dave"] inserts a default-constructed value if the key is absent — use find or count for non-inserting lookups.std::unordered_map (hash)
#include <stdio.h>
#include <string.h>
// C: no standard hash map — implement your own or use a library
// This example just shows the complexity of doing it manually
int hash(const char *key, int capacity) {
int h = 0;
while (*key) h = (h * 31 + *key++) % capacity;
return h;
}
int main(void) {
printf("no std hash map in C\n");
return 0;
} #include <iostream>
#include <unordered_map>
#include <string>
int main() {
std::unordered_map<std::string, int> word_count;
std::string words[] = {"the", "quick", "the", "fox", "the"};
for (const auto &word : words)
word_count[word]++;
for (const auto &[word, count] : word_count)
std::cout << word << ": " << count << "\n";
std::cout << "the appears " << word_count["the"] << " times\n";
return 0;
} std::unordered_map<K,V> is a hash table with O(1) average lookup — faster than std::map for most access patterns, but iteration order is undefined. Requires the key type to be hashable (built-in types and std::string work out of the box). For custom key types, provide a std::hash specialization.std::set & std::unordered_set
#include <stdio.h>
#include <string.h>
// C: no standard set — sort + unique, or manual bit arrays
int compare_ints(const void *a, const void *b) {
return *(int *)a - *(int *)b;
}
int main(void) {
int numbers[] = {5, 3, 5, 1, 3, 2, 5};
int count = 7;
qsort(numbers, count, sizeof(int), compare_ints);
// Manual deduplication
int unique_count = 0;
for (int i = 0; i < count; i++)
if (i == 0 || numbers[i] != numbers[i-1])
printf("%d ", numbers[i]);
printf("\n");
return 0;
} #include <iostream>
#include <set>
#include <unordered_set>
int main() {
// std::set: sorted, unique, O(log n)
std::set<int> unique_numbers = {5, 3, 5, 1, 3, 2, 5};
for (int number : unique_numbers)
std::cout << number << " "; // 1 2 3 5 — sorted, deduplicated
std::cout << "\n";
// std::unordered_set: hash-based, O(1) average, unordered
std::unordered_set<int> fast_set = {5, 3, 1, 2};
std::cout << (fast_set.count(3) ? "3 found" : "3 missing") << "\n";
return 0;
} std::set<T> stores unique sorted elements with O(log n) insert and lookup. std::unordered_set<T> uses a hash table for O(1) average operations when order doesn't matter. Both containers guarantee uniqueness — inserting a duplicate is silently ignored.STL Algorithms
std::sort vs qsort
#include <stdio.h>
#include <stdlib.h>
int compare_ints(const void *a, const void *b) {
// Must cast void* — error-prone, no type safety
return *(int *)a - *(int *)b;
}
int main(void) {
int numbers[] = {5, 3, 8, 1, 9, 2};
qsort(numbers, 6, sizeof(int), compare_ints);
for (int i = 0; i < 6; i++)
printf("%d ", numbers[i]);
printf("\n");
return 0;
} #include <iostream>
#include <vector>
#include <algorithm>
#include <string>
int main() {
std::vector<int> numbers = {5, 3, 8, 1, 9, 2};
std::sort(numbers.begin(), numbers.end()); // ascending
for (int number : numbers)
std::cout << number << " ";
std::cout << "\n";
// Sort with custom comparator — descending
std::sort(numbers.begin(), numbers.end(), std::greater<int>{});
for (int number : numbers)
std::cout << number << " ";
std::cout << "\n";
return 0;
} std::sort is type-safe (no void* casts), typically faster than qsort (the comparator can be inlined), and works with any random-access range. Custom comparators are lambdas or function objects — no separate function needed. std::stable_sort preserves equal elements' relative order (at O(n log² n) cost).std::find & std::find_if
#include <stdio.h>
#include <string.h>
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
// Linear search: manual loop
int target = 3;
for (int i = 0; i < 5; i++) {
if (numbers[i] == target) {
printf("Found %d at index %d\n", target, i);
break;
}
}
return 0;
} #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
// Find exact value
auto it = std::find(numbers.begin(), numbers.end(), 3);
if (it != numbers.end())
std::cout << "Found 3 at index " << (it - numbers.begin()) << "\n";
// Find first even number
auto even = std::find_if(numbers.begin(), numbers.end(),
[](int n) { return n % 2 == 0; });
if (even != numbers.end())
std::cout << "First even: " << *even << "\n";
bool any_large = std::any_of(numbers.begin(), numbers.end(),
[](int n) { return n > 4; });
std::cout << std::boolalpha << any_large << "\n";
return 0;
} STL algorithms return iterators pointing into the range. An iterator equal to
end() means "not found". std::find_if accepts any predicate — a function, lambda, or function object. std::any_of, std::all_of, and std::none_of are short-circuit predicates over a range.std::transform & std::accumulate
#include <stdio.h>
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
int squared[5];
// Manual map
for (int i = 0; i < 5; i++)
squared[i] = numbers[i] * numbers[i];
// Manual reduce
int total = 0;
for (int i = 0; i < 5; i++) total += numbers[i];
for (int i = 0; i < 5; i++) printf("%d ", squared[i]);
printf("\nsum=%d\n", total);
return 0;
} #include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squared(numbers.size());
std::transform(numbers.begin(), numbers.end(), squared.begin(),
[](int n) { return n * n; });
int total = std::accumulate(numbers.begin(), numbers.end(), 0);
int product = std::accumulate(numbers.begin(), numbers.end(), 1,
[](int acc, int n) { return acc * n; });
for (int number : squared) std::cout << number << " ";
std::cout << "\nsum=" << total << " product=" << product << "\n";
return 0;
} std::transform applies a function to each element and writes results to an output range (can be the same range for in-place transform). std::accumulate folds a range with a binary operation — the default is addition, but any binary function works. C++23 std::ranges versions of these algorithms are even more composable.Ranges (C++20)
#include <stdio.h>
#include <stdlib.h>
int compare_ints(const void *a, const void *b) { return *(int *)a - *(int *)b; }
int main(void) {
int numbers[] = {5, 1, 4, 2, 8, 3};
// Filter even + sort: two separate loops, temporary array needed
int evens[6]; int count = 0;
for (int i = 0; i < 6; i++)
if (numbers[i] % 2 == 0) evens[count++] = numbers[i];
qsort(evens, count, sizeof(int), compare_ints);
for (int i = 0; i < count; i++) printf("%d ", evens[i]);
printf("\n");
return 0;
} #include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 1, 4, 2, 8, 3};
// Composable pipeline — no intermediate storage, lazy evaluation
auto pipeline = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int value : pipeline)
std::cout << value << " "; // 16 4 64
std::cout << "\n";
return 0;
} C++20 ranges compose operations with
| into lazy pipelines — no intermediate vectors, no extra memory. The pipeline is evaluated element by element as it's consumed. std::views::filter, std::views::transform, std::views::take, std::views::drop, and many more are available in <ranges>.Lambdas
Lambda syntax
#include <stdio.h>
#include <stdlib.h>
// C: callbacks are function pointers — must name every function
int is_even(int n) { return n % 2 == 0; }
int square(int n) { return n * n; }
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++)
if (is_even(numbers[i]))
printf("%d ", square(numbers[i]));
printf("\n");
return 0;
} #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Lambda: [captures](parameters) { body }
auto is_even = [](int n) { return n % 2 == 0; };
auto square = [](int n) { return n * n; };
for (int number : numbers)
if (is_even(number))
std::cout << square(number) << " ";
std::cout << "\n";
// Inline with std::for_each
std::for_each(numbers.begin(), numbers.end(),
[](int n) { std::cout << n * 2 << " "; });
std::cout << "\n";
return 0;
} A lambda is an anonymous function object created inline. The
[] is the capture list; () is the parameter list; the return type is deduced automatically. Lambdas can be stored in auto variables or passed directly to algorithms. They replace the C pattern of naming a function solely to pass it as a callback.Lambda captures
#include <stdio.h>
// C callbacks cannot capture local variables — must use global state
// or a user_data void* pointer (error-prone)
static int global_threshold = 5;
int above_threshold(int n) { return n > global_threshold; }
int main(void) {
int threshold = 5; // can't capture this in a function pointer
int numbers[] = {2, 7, 3, 9, 1, 6};
for (int i = 0; i < 6; i++)
if (above_threshold(numbers[i]))
printf("%d ", numbers[i]);
printf("\n");
return 0;
} #include <iostream>
#include <vector>
#include <algorithm>
int main() {
int threshold = 5;
std::vector<int> numbers = {2, 7, 3, 9, 1, 6};
// [threshold] — capture by value (copy)
auto above = std::count_if(numbers.begin(), numbers.end(),
[threshold](int n) { return n > threshold; });
std::cout << "above " << threshold << ": " << above << "\n";
// [&threshold] — capture by reference
threshold = 3;
int count = std::count_if(numbers.begin(), numbers.end(),
[&threshold](int n) { return n > threshold; });
std::cout << "above " << threshold << ": " << count << "\n";
// [=] capture all by value; [&] capture all by reference
return 0;
} The capture list lets a lambda close over local variables without global state or
void* user-data tricks. [x] captures x by value (snapshot at creation); [&x] captures by reference (sees mutations but the lambda must not outlive x). [=] captures all accessed locals by value; [&] captures all by reference.std::function
#include <stdio.h>
// C: callbacks are raw function pointers — only free functions allowed
typedef int (*Transform)(int);
int apply(int *numbers, int count, Transform transform_fn) {
int total = 0;
for (int i = 0; i < count; i++)
total += transform_fn(numbers[i]);
return total;
}
int double_value(int n) { return n * 2; }
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
printf("%d\n", apply(numbers, 5, double_value));
return 0;
} #include <iostream>
#include <vector>
#include <functional>
#include <numeric>
// std::function accepts lambdas, function pointers, and function objects
int apply_all(const std::vector<int> &numbers,
std::function<int(int)> transform_fn) {
int total = 0;
for (int number : numbers)
total += transform_fn(number);
return total;
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int multiplier = 3;
// Capture local variable — impossible with a raw function pointer
std::cout << apply_all(numbers, [multiplier](int n) { return n * multiplier; }) << "\n";
std::cout << apply_all(numbers, [](int n) { return n * n; }) << "\n";
return 0;
} std::function<R(Args...)> is a type-erased callable wrapper that accepts lambdas, function pointers, and objects with operator(). It has a small overhead compared to a raw function pointer (heap allocation for large captures). For performance-critical callbacks, prefer template parameters or direct lambdas with auto.Error Handling
Exceptions vs errno
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
double safe_divide(double numerator, double denominator) {
if (denominator == 0.0) {
errno = EDOM;
return 0.0; // caller must check errno
}
return numerator / denominator;
}
int main(void) {
errno = 0;
double result = safe_divide(10.0, 0.0);
if (errno) {
fprintf(stderr, "Error: %s\n", strerror(errno));
} else {
printf("%.2f\n", result);
}
return 0;
} #include <iostream>
#include <stdexcept>
double safe_divide(double numerator, double denominator) {
if (denominator == 0.0)
throw std::invalid_argument("division by zero");
return numerator / denominator;
}
int main() {
try {
std::cout << safe_divide(10.0, 2.0) << "\n"; // 5
std::cout << safe_divide(10.0, 0.0) << "\n"; // throws
} catch (const std::invalid_argument &error) {
std::cout << "Caught: " << error.what() << "\n";
} catch (const std::exception &error) {
std::cout << "Unknown error: " << error.what() << "\n";
}
return 0;
} Exceptions unwind the stack automatically — every RAII destructor runs between the
throw and the catch, so resources are always released. Unlike errno, exceptions cannot be silently ignored. Catch by const ref to avoid slicing. std::exception::what() returns a human-readable description.noexcept
#include <stdio.h>
// C has no way to declare that a function does not fail
int safe_add(int a, int b) { return a + b; }
int main(void) {
printf("%d\n", safe_add(3, 4));
return 0;
} #include <iostream>
#include <type_traits>
// noexcept: promises this function will never throw
int safe_add(int a, int b) noexcept { return a + b; }
// Conditionally noexcept based on T's operations
template<typename T>
T clone(const T &value) noexcept(std::is_nothrow_copy_constructible_v<T>) {
return value;
}
int main() {
std::cout << safe_add(3, 4) << "\n";
// If a noexcept function does throw, std::terminate is called immediately
// This allows the compiler to generate faster code (no unwind tables)
std::cout << std::boolalpha;
std::cout << noexcept(safe_add(1, 2)) << "\n"; // true
return 0;
} noexcept is a compile-time contract: the function will never throw. The compiler can generate faster code for noexcept functions (no stack-unwinding metadata). Move constructors and destructors should be noexcept — some STL operations (like std::vector resize) only use moves when they are guaranteed not to throw.std::expected (C++23)
#include <stdio.h>
#include <stdlib.h>
// C: return error code; output through pointer parameter
// Callers can ignore the error code
int parse_int(const char *text, int *result) {
char *end;
long value = strtol(text, &end, 10);
if (*end != '\0') return -1; // error
*result = (int)value;
return 0; // success
}
int main(void) {
int value;
if (parse_int("42", &value) == 0) printf("%d\n", value);
if (parse_int("abc", &value) != 0) printf("parse error\n");
return 0;
} #include <iostream>
#include <expected>
#include <string>
#include <charconv>
std::expected<int, std::string> parse_int(std::string_view text) {
int value;
auto [ptr, error_code] = std::from_chars(text.data(), text.data() + text.size(), value);
if (error_code != std::errc{})
return std::unexpected("not a valid integer: " + std::string(text));
return value;
}
int main() {
auto result = parse_int("42");
if (result)
std::cout << *result << "\n"; // 42
auto failure = parse_int("abc");
if (!failure)
std::cout << failure.error() << "\n"; // error message
return 0;
} std::expected<T, E> (C++23) holds either a value or an error — like Rust's Result<T, E>. The error cannot be silently ignored: you must check whether the result holds a value before accessing it. Use it when errors are common and expected (not exceptional), avoiding exceptions for performance-critical or embedded code.Operator Overloading
Arithmetic operators
#include <stdio.h>
typedef struct { double real; double imag; } Complex;
Complex complex_add(Complex a, Complex b) {
return (Complex){a.real + b.real, a.imag + b.imag};
}
Complex complex_mul(Complex a, Complex b) {
return (Complex){a.real*b.real - a.imag*b.imag,
a.real*b.imag + a.imag*b.real};
}
int main(void) {
Complex a = {1.0, 2.0}, b = {3.0, 4.0};
Complex sum = complex_add(a, b);
printf("(%.1f + %.1fi)\n", sum.real, sum.imag);
return 0;
} #include <iostream>
struct Complex {
double real, imag;
Complex operator+(const Complex &other) const {
return {real + other.real, imag + other.imag};
}
Complex operator*(const Complex &other) const {
return {real*other.real - imag*other.imag,
real*other.imag + imag*other.real};
}
bool operator==(const Complex &other) const = default; // C++20
};
int main() {
Complex a = {1.0, 2.0}, b = {3.0, 4.0};
Complex total = a + b; // natural syntax
std::cout << "(" << total.real << " + " << total.imag << "i)\n";
return 0;
} Operator overloading lets user-defined types use built-in syntax. The compiler translates
a + b into a.operator+(b). = default on operator== (C++20) generates a member-by-member comparison automatically. Overload sparingly — only when the operation has an obvious unsurprising meaning matching the built-in semantics.Stream operator<<
#include <stdio.h>
typedef struct { int x; int y; } Point;
// C: separate print function per type, no chaining
void print_point(const Point *point) {
printf("(%d, %d)", point->x, point->y);
}
int main(void) {
Point a = {3, 4};
printf("Point: "); print_point(&a); printf("\n");
return 0;
} #include <iostream>
struct Point {
int x, y;
};
// Non-member operator<<: enables natural stream syntax
std::ostream &operator<<(std::ostream &stream, const Point &point) {
stream << "(" << point.x << ", " << point.y << ")";
return stream; // return stream for chaining
}
int main() {
Point a = {3, 4}, b = {1, 2};
std::cout << "Points: " << a << " and " << b << "\n";
// Also works with std::cerr, std::stringstream, file streams, etc.
return 0;
} Defining
operator<< as a non-member function lets your type work with any std::ostream — including std::cout, std::cerr, file streams, and string streams. Returning stream enables chaining: cout << a << b works because the first << returns the stream for the second.Spaceship operator (C++20)
#include <stdio.h>
#include <string.h>
typedef struct { int major; int minor; int patch; } Version;
int version_compare(Version a, Version b) {
// Must write all 3 comparisons manually
if (a.major != b.major) return a.major - b.major;
if (a.minor != b.minor) return a.minor - b.minor;
return a.patch - b.patch;
}
int main(void) {
Version v1 = {1, 2, 3}, v2 = {1, 3, 0};
int cmp = version_compare(v1, v2);
printf("%s\n", cmp < 0 ? "older" : cmp > 0 ? "newer" : "same");
return 0;
} #include <iostream>
#include <compare>
struct Version {
int major, minor, patch;
// Spaceship operator: auto-generates <, <=, >, >=, == and !=
auto operator<=>(const Version &) const = default;
};
int main() {
Version v1 = {1, 2, 3}, v2 = {1, 3, 0};
if (v1 < v2) std::cout << "older\n";
else if (v1 > v2) std::cout << "newer\n";
else std::cout << "same\n";
std::cout << std::boolalpha << (v1 == v1) << "\n"; // true
std::cout << (v1 <= v2) << "\n"; // true
return 0;
} The three-way comparison operator
operator<=> (C++20) returns a category — std::strong_ordering, std::weak_ordering, or std::partial_ordering. When defaulted, the compiler generates it member-by-member in declaration order and automatically derives all six comparison operators. No more writing six separate operators by hand.Modern C++
Structured bindings (C++17)
#include <stdio.h>
typedef struct { int quotient; int remainder; } DivResult;
DivResult divide(int numerator, int denominator) {
return (DivResult){numerator / denominator, numerator % denominator};
}
int main(void) {
// Must access members by name — no destructuring
DivResult result = divide(17, 5);
printf("quotient=%d remainder=%d\n", result.quotient, result.remainder);
return 0;
} #include <iostream>
#include <tuple>
#include <map>
#include <string>
std::pair<int, int> divide(int numerator, int denominator) {
return {numerator / denominator, numerator % denominator};
}
int main() {
// Unpack a pair/tuple/struct directly into named variables
auto [quotient, remainder] = divide(17, 5);
std::cout << "quotient=" << quotient << " remainder=" << remainder << "\n";
// Unpack map entries in range-for
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto &[name, score] : scores)
std::cout << name << ": " << score << "\n";
return 0;
} Structured bindings (C++17) unpack pairs, tuples, arrays, and structs into named variables. The names are bound to the actual members (references internally), not copies. They eliminate the need for
.first/.second on pairs and are especially clean for map iteration.std::optional (C++17)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// C: sentinel values or out-parameters for "no result"
int find_first_even(const int *numbers, int count, int *result) {
for (int i = 0; i < count; i++)
if (numbers[i] % 2 == 0) { *result = numbers[i]; return 1; }
return 0; // not found — result is undefined
}
int main(void) {
int numbers[] = {1, 3, 4, 7};
int value;
if (find_first_even(numbers, 4, &value))
printf("found: %d\n", value);
else
printf("not found\n");
return 0;
} #include <iostream>
#include <optional>
#include <vector>
std::optional<int> find_first_even(const std::vector<int> &numbers) {
for (int number : numbers)
if (number % 2 == 0) return number;
return std::nullopt;
}
int main() {
std::vector<int> numbers = {1, 3, 4, 7};
auto result = find_first_even(numbers);
if (result)
std::cout << "found: " << *result << "\n";
else
std::cout << "not found\n";
// value_or provides a default
std::cout << result.value_or(-1) << "\n";
return 0;
} std::optional<T> explicitly represents "a value that may or may not exist" — no sentinel values (-1, NULL, ""), no out-parameters, no boolean flags. Accessing a disengaged optional via * is undefined behavior; use value() (throws) or value_or(default) (safe) for guarded access.std::variant (C++17)
#include <stdio.h>
#include <string.h>
// C: tagged union — error-prone, no enforcement of which field is active
typedef enum { INT_VAL, DOUBLE_VAL, STRING_VAL } Tag;
typedef struct {
Tag tag;
union { int integer; double floating; char text[64]; } data;
} Value;
void print_value(const Value *value) {
if (value->tag == INT_VAL) printf("%d\n", value->data.integer);
else if (value->tag == DOUBLE_VAL) printf("%.2f\n", value->data.floating);
else printf("%s\n", value->data.text);
}
int main(void) {
Value items[] = {{INT_VAL,{.integer=42}}, {DOUBLE_VAL,{.floating=3.14}}};
for (int i = 0; i < 2; i++) print_value(&items[i]);
return 0;
} #include <iostream>
#include <variant>
#include <string>
using Value = std::variant<int, double, std::string>;
void print_value(const Value &value) {
// std::visit dispatches to the right overload at runtime
std::visit([](const auto &item) {
std::cout << item << "\n";
}, value);
}
int main() {
Value items[] = {42, 3.14, std::string("hello")};
for (const auto &item : items)
print_value(item);
std::cout << std::holds_alternative<int>(items[0]) << "\n"; // 1
std::cout << std::get<int>(items[0]) << "\n"; // 42
return 0;
} std::variant<T...> is a type-safe tagged union — accessing the wrong alternative throws std::bad_variant_access rather than causing undefined behavior. std::visit dispatches to the right overload at runtime via a visitor. Use it instead of manual tagged unions or virtual dispatch when the set of types is closed.if with initializer (C++17)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
// Must declare in outer scope; leaks into surrounding code
char *end;
long value = strtol("42abc", &end, 10);
if (*end == '\0') {
printf("parsed: %ld\n", value);
} else {
printf("partial parse at: %s\n", end);
}
// value and end still visible here — but shouldn't be used
return 0;
} #include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
// init-statement scopes the variable to the if block
if (auto it = scores.find("Alice"); it != scores.end()) {
std::cout << "Alice: " << it->second << "\n";
} // it not accessible here
if (auto it = scores.find("Dave"); it != scores.end()) {
std::cout << "Dave: " << it->second << "\n";
} else {
std::cout << "Dave not found\n";
}
return 0;
} C++17 allows an initializer in
if and switch statements: if (init; condition). The initialized variable is scoped to the if/else block, preventing it from leaking into the surrounding scope. This is especially useful for map lookups and regex matches where you need the iterator only for the duration of the branch.Gotchas for C Programmers
No VLAs in standard C++
#include <stdio.h>
void process(int count) {
int buffer[count]; // VLA: C99, stack allocation with runtime size
for (int i = 0; i < count; i++) buffer[i] = i * i;
for (int i = 0; i < count; i++) printf("%d ", buffer[i]);
printf("\n");
}
int main(void) {
process(5);
return 0;
} #include <iostream>
#include <vector>
// C++ standard does not include VLAs — use std::vector for dynamic size
void process(int count) {
std::vector<int> buffer(count); // heap-allocated, auto-freed
for (int i = 0; i < count; i++) buffer[i] = i * i;
for (int number : buffer) std::cout << number << " ";
std::cout << "\n";
}
// For fixed-size stack allocation, use std::array<T, N> with a known N
int main() {
process(5);
return 0;
} Variable-length arrays (VLAs) are C99 but not standard C++. GCC supports them as an extension, but using them in C++ is non-portable and disabled by
-Wall -pedantic. Use std::vector<T>(count) for runtime-sized arrays — it's heap-allocated and safe. For compile-time sizes, use std::array<T, N>.One Definition Rule
/* In C, defining the same function in two translation units
is a linker error, but defining the same global variable
with extern in headers is common — tricky but legal.
// header.h
extern int global_counter; // declaration only
// a.c
int global_counter = 0; // definition in one .c file
// b.c
#include "header.h"
// uses global_counter — links to a.c's definition */
#include <stdio.h>
int main(void) { printf("ODR is a linker concern in C\n"); return 0; } #include <iostream>
// C++17 inline variables: one definition, all translation units share it
// (replaces the extern-in-header pattern)
inline int global_counter = 0;
// inline functions defined in headers are also ODR-safe
inline int next_counter() { return ++global_counter; }
// constexpr implies inline
constexpr int max_count = 100;
int main() {
std::cout << next_counter() << "\n"; // 1
std::cout << next_counter() << "\n"; // 2
std::cout << max_count << "\n";
return 0;
} The One Definition Rule (ODR) says every entity must be defined exactly once across all translation units. In C++,
inline variables (C++17) and inline functions can be defined in headers without violating the ODR — the linker merges them. Violating the ODR (two definitions with different bodies) is undefined behavior, not a guaranteed linker error.Most vexing parse
#include <stdio.h>
typedef struct { int value; } Widget;
Widget widget_create(int value) {
Widget widget;
widget.value = value;
return widget;
}
int main(void) {
// In C, this is always a variable definition — no ambiguity
Widget widget = widget_create(42);
printf("%d\n", widget.value);
return 0;
} #include <iostream>
struct Widget {
int value;
Widget() : value(0) {}
explicit Widget(int value) : value(value) {}
};
int main() {
Widget a(42); // object — fine
Widget b{42}; // object — preferred in modern C++
// The most vexing parse: interpreted as a function declaration, not an object
// Widget c(); // declares function c() returning Widget — not a variable!
Widget c{}; // brace-init always creates an object, never a function
std::cout << a.value << "\n"; // 42
std::cout << c.value << "\n"; // 0
return 0;
} The Most Vexing Parse:
Widget c(); is parsed as a function declaration (a function named c that takes no arguments and returns Widget), not as a default-constructed Widget. Brace initialization Widget c{}; is always an object construction, never a function declaration — use it to avoid the ambiguity.Static initialization order
#include <stdio.h>
// C: global initializers must be compile-time constants
// Cross-TU initialization order is undefined
#define BASE_VALUE 100
int derived_value = BASE_VALUE + 50; // OK: macro expands to literal
int main(void) {
printf("base: %d, derived: %d\n", BASE_VALUE, derived_value);
return 0;
} #include <iostream>
#include <string>
// Non-trivial global constructors run before main
// Order between translation units is UNDEFINED (Static Initialization Order Fiasco)
// Use function-local statics to guarantee initialization order
std::string &get_prefix() {
static std::string prefix = "Hello"; // initialized on first call
return prefix;
}
int global_message = []{ // IIFE: runs at static init time
std::cout << "Initialized\n";
return 42;
}();
int main() {
std::cout << get_prefix() << "\n"; // safe: initialized on first use
std::cout << global_message << "\n";
return 0;
} The Static Initialization Order Fiasco: when a global object in TU A depends on a global object in TU B, the initialization order between TUs is undefined. The solution is Meyers' Singleton: wrap the global in a function-local
static — it is guaranteed to initialize on the first call to the function. Function-local statics are also thread-safe since C++11.Include guards & #pragma once
/* C traditional include guard — verbose but portable */
/* widget.h */
#ifndef WIDGET_H
#define WIDGET_H
typedef struct { int value; } Widget;
#endif /* WIDGET_H */
/* A missing or misspelled guard causes multiple-definition errors */ /* C++ option 1: #pragma once — supported by all major compilers */
/* widget.hpp */
#pragma once
struct Widget { int value; };
/* C++ option 2: traditional guard — still portable, still works */
/* #ifndef WIDGET_HPP
#define WIDGET_HPP
struct Widget { int value; };
#endif */
/* C++20 option: named modules (no headers needed at all)
export module widget;
export struct Widget { int value; }; */ #pragma once is simpler than traditional include guards and supported by GCC, Clang, and MSVC. It cannot be accidentally broken by a mismatched macro name. C++20 modules (export module name;) eliminate headers entirely, but toolchain support is still maturing. New projects can start with #pragma once confidently.Argument-Dependent Lookup
#include <stdio.h>
// C: all function calls require explicit qualification — no ADL
// mylib_print(&widget); // always this, never just print(&widget)
// This is verbose but unambiguous
typedef struct { int value; } Widget;
void widget_print(const Widget *widget) { printf("%d\n", widget->value); }
int main(void) {
Widget widget = {42};
widget_print(&widget); // must use full name
return 0;
} #include <iostream>
namespace mylib {
struct Widget { int value; };
// ADL: calling print(widget) finds this function automatically
// because Widget is in namespace mylib
void print(const Widget &widget) {
std::cout << widget.value << "\n";
}
}
namespace other {
void demonstrate() {
mylib::Widget widget = {42};
print(widget); // ADL: finds mylib::print without mylib:: prefix!
}
}
int main() {
mylib::Widget widget = {99};
print(widget); // ADL finds mylib::print
other::demonstrate();
return 0;
} Argument-Dependent Lookup (ADL, also called Koenig lookup): when you call an unqualified function, the compiler also searches the namespaces of the argument types. This is how
std::swap(a, b) finds a type-specific swap in the type's namespace, and how operator<< for custom types works. ADL is powerful but can cause surprising name resolution — always qualify function calls when precision matters.