- Getting Started
- Compiler Concepts in C++
- Arrays and Vectors
- Statements and Operators
- Controlling Program Flow
- Characters and Strings
- Functions
- Random number generation
- Nearest integer floating-point operations
- Power Functions
- Trigonometric Functions
- Function Prototypes
- Default Arguments
- Pass by Reference, Value, Pointer
- Const usage for printing with reference inputs
- Local Global - Scope Rules
- Function Calls - Memory Stack - Recursive Function
- Memory
- Pointers
- Classes and Objects
- Polymorphism
- Function Pointers & Lambdas
staticin C++- C++ Visibility / Access Specifiers
- Templates
- Operator Overloading
- explicit Keyword
- Structured Bindings
std::sort— C++ Sorting- Casting in C++
std::optional— Optional Data in C++std::variant— Type-Safe Unionstd::any— Type-Erased Single Value- Enumeration
- Threads
std::asyncandstd::future— Asynchronous Functions- Timing in C++
- C++ Exception Handling (try-catch)
- Singleton in C++ (Design Pattern)
<fstream>in C++<iostream>— C++ Standard I/O
RAII (Resource Acquisition Is Initialization) is a C++ programming technique that binds the life cycle of a resource (like heap memory, file handles, or network sockets) to the life cycle of a local object.
Instead of manually managing "start" and "stop" actions (like new and delete), you wrap the resource in a class. The resource is "acquired" when the object is created and "released" automatically when the object is destroyed.
- Constructor (Acquisition): You allocate the resource (e.g.,
new int[10]) inside the class constructor. - Destructor (Release): You free the resource (e.g.,
delete[]) inside the class destructor.
Because C++ guarantees that local (stack) objects are destroyed the moment they go out of scope, the resource is guaranteed to be released—even if your code crashes, returns early, or throws an exception.
Without RAII (Manual):
void manual_process() {
int* data = new int[100]; // Acquire
delete[] data; // Release
}With RAII (Automatic):
void raii_process() {
std::vector<int> data(100); // Resource bound to object 'data'
} // SAFE! 'data' destructor runs here normallyRAII turns "manual management" into "automatic cleanup." It is the reason why modern C++ developers rarely use new and delete. Common RAII wrappers include:
std::vector(manages heap memory)std::unique_ptr(manages a single pointer)std::lock_guard(manages mutex locks)std::fstream(manages file access)
The preprocessor runs before compilation. It handles all lines starting with # — no C++ syntax, no types, just text manipulation.
Copies and pastes the contents of another file into your current file.
#include <iostream> // pastes the iostream header — gives you std::cout etc.
#include "my_file.h" // pastes a local fileThis is why using namespace std works after #include <iostream> — the std namespace is already defined by the time the compiler sees your code.
Defines a find-and-replace rule. Wherever the macro name appears, the preprocessor substitutes the value before compilation.
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
int main() {
double area = PI * SQUARE(5);
// preprocessor turns this into:
// double area = 3.14159 * ((5) * (5));
}No type checking. The preprocessor doesn't know C++ types or scopes — it's pure text substitution. Errors from macros often point to the expanded code, not the macro definition, making them hard to debug. Prefer
constorconstexprfor constants, and inline functions or lambdas over function macros.
// prefer this over #define for constants
constexpr double PI = 3.14159;Shows or hides parts of your code based on conditions — evaluated at compile time.
#define DEBUG
int main() {
#ifdef DEBUG
std::cout << "Debug mode on" << std::endl; // included
#else
std::cout << "Release mode" << std::endl; // excluded
#endif
}Common use — platform-specific code:
#ifdef _WIN32
// Windows-only code
#else
// Linux / macOS code
#endifSpecial directives for the compiler. The most common is #pragma once — prevents a header file from being included more than once.
// my_sensor.h
#pragma once // if this file is included multiple times, ignore duplicates
class Sensor {
// ...
};The alternative is the classic include guard using #ifndef:
#ifndef MY_SENSOR_H
#define MY_SENSOR_H
class Sensor {
// ...
};
#endifBoth do the same thing. #pragma once is shorter and used in most modern codebases.
| Directive | What it does |
|---|---|
#include |
Paste another file's contents here |
#define |
Find-and-replace before compilation |
#ifdef / #ifndef |
Include code only if a macro is / isn't defined |
#if / #else / #endif |
Conditional code blocks |
#pragma once |
Include this file only once |
A variable is just a name (or label) for a location in your computer's memory where a value is stored.
int age = 25;
size_t position; // unsigned int or unsigned long
The computer reserves a spot in memory big enough to store an int (usually 4 bytes).
- It puts the value 25 into that memory.
- It gives that memory location a name — in this case, age.
- So when you use age, you're referring to that memory location.
A namespace is a named scope that groups related code and prevents name collisions.
#include <iostream>
namespace MySensors {
int id = 1;
void read() {
std::cout << "id: " << id << std::endl;
}
}
namespace MyActuator {
int id = 2; // no collision with MySensors::id
void read() {
std::cout << "id: " << id << std::endl;
}
}
int main() {
MySensors::read(); // :: is the scope resolution operator
MyActuator::read();
}Both namespaces define id and read() — no collision because they live in separate scopes.
Tells the compiler to look inside a namespace automatically — so you don't need to type the prefix every time.
Important:
using namespacemust come after the namespace definition. The compiler reads top to bottom — using a namespace before defining it causes a compile error.
#include <iostream>
namespace MySensors {
int id = 1;
void read() {
std::cout << "id: " << id << std::endl;
}
}
namespace MyActuator {
int id = 2;
void read() {
std::cout << "id: " << id << std::endl;
}
}
using namespace MySensors; // defined above, safe to use here
int main() {
read(); // compiler knows you mean MySensors::read()
MyActuator::read(); // still need prefix for everything else
}You can also bring in a single name instead of the whole namespace:
using MySensors::read; // only import read(), nothing else
int main() {
read(); // works
MyActuator::read(); // still needs prefix
}Avoid
using namespacein larger projects — if two namespaces have a function with the same name, the compiler won't know which one you mean.
The entire C++ Standard Library lives inside the std namespace.
std::vector<int> data;
std::string name;
std::cout << name << std::endl;With using namespace std you can drop the prefix — fine for small scripts, risky in large codebases:
using namespace std;
vector<int> data; // same as std::vector
string name; // same as std::stringNamespaces are used to wrap classes so they don't collide with other libraries:
namespace fanuc_gripper_controller {
using CallbackReturn = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn;
class FanucGripperInterface : public hardware_interface::SystemInterface {
// ...
};
} // namespace fanuc_gripper_controllerfanuc_gripper_controller::FanucGripperInterface obj; // unambiguousThe using alias inside the namespace is also scoped — it won't leak out and pollute the global scope.
| Meaning | |
|---|---|
namespace Foo { } |
Create a named scope called Foo |
Foo::bar |
Access bar from namespace Foo |
using namespace Foo |
Skip the Foo:: prefix for everything in Foo (define Foo first!) |
using Foo::bar |
Skip the prefix for bar only |
int main(int num_args, char *args[]){
std::cout << "Number of arguments: " << num_args << "\n";
std::cout << "5th argument is: " << args[4] << "\n";
return 1;
}int num_rooms = 0;
std::cout << "How many rooms do you want to be cleaned? ";
std::cin >> num_rooms;const int age = 30;
// age = 31; // ❌ Error! Cannot modifyclass Person {
std::string name;
public:
Person(std::string n) : name(n) {}
void printName() const { // ✅ const member function
// name += "!"; // ❌ Error
std::cout << name << std::endl;
}
void changeName(std::string n) { // Non-const
name = n;
}
};class Example {
mutable int counter; // can be changed even in const objects
public:
Example() : counter(0) {}
void increment() const { // const member function
counter++; // allowed because counter is mutable
}
int getCounter() const {
return counter;
}
};#include <iostream>
#include <string>
int main() {
std::cout << sizeof(char) << "\n";
std::cout << sizeof(int) << "\n";
std::cout << sizeof(double) << "\n";
std::cout << sizeof(std::string) << "\n";
return 0;
}1
4
8
32
long double large_amount = 2.7e120;| Type | Bit width | Range |
|---|---|---|
int (signed 32-bit) |
32 | −2,147,483,648 → +2,147,483,647 |
unsigned int (32-bit) |
32 | 0 → 4,294,967,295 |
Compile time is when the compiler reads your source code and turns it into an executable. Runtime is when the executable is actually running.
int i = 0;- The type
int→ resolved at compile time - The value
0→ lives in memory at runtime
C++ tuple must know the index value before the program runs. Because depending on the index, std::get may return a double, std::string, etc. — different types entirely.
std::get<0>(t); // ✅ 0 is a hardcoded literal — compile time
std::get<i>(t); // ❌ i is a variable — its value is only known at runtimeEven if you can clearly see that i = 0, the compiler does not evaluate variables — it only sees "a variable of type int" and moves on.
std::vector doesn't need the index at compile time because all elements share the same type — the return type of v[i] is always known regardless of i.
std::vector<float> v = {20.4, 89.7, 66.0};
for (int i = 0; i < v.size(); i++) {
print(v[i]); // ✅ — v[i] is always float, regardless of i
}| Compile Time | Runtime | |
|---|---|---|
| When | Before program runs | While program runs |
| Examples | Types, templates, std::get<0> |
Variable values, for loops, user input |
| Error caught | Compiler error | Crash or wrong output |
Catching errors at compile time is always better — the compiler stops you before the program ever runs.
When you run g++ main.cpp, it goes through several stages:
source code (.cpp)
↓
1. Preprocessor — handles #include, #define, #pragma once
↓
2. Parser — reads text and builds an AST
↓
3. Semantic analysis — checks types, resolves names
↓
4. Optimization — restructures code to run faster
↓
5. Code generation — produces machine code
↓
object file (.o)
Parsing is step 2 — the compiler reads your source text and builds an internal tree called an AST (Abstract Syntax Tree). All later stages work from this tree, never from raw text again.
int x = 5 + 3;Becomes:
Declaration
└── type: int
└── name: x
└── value: Add
├── 5
└── 3
This is why headers are expensive — <vector> is thousands of lines of template code. Every .cpp that includes it forces the parser to rebuild that entire tree from scratch. PCH skips this by saving the pre-built tree to disk.
| Step | Input | What happens | Output | Location |
|---|---|---|---|---|
| Compilation | .cpp files |
Each file compiled separately into machine code — no connections between files yet | .o object files |
build/CMakeFiles/... |
| Linking | .o files + libraries |
All object files combined, symbol references resolved across files | final executable | build/ |
Every function or global variable name (like foo, bar, std::cout) becomes a symbol inside object files.
There are two types:
-
Defined symbols — functions/variables you define yourself
void greet() {} // defined here
-
Undefined symbols — references to something defined elsewhere
greet(); // defined somewhere else — linker must find it
The linker's job:
Match every undefined symbol with its corresponding defined symbol across all object files and libraries.
If a symbol is referenced but never defined anywhere, you get a linker error — not a compiler error:
undefined reference to `greet()`
Headers like <vector> and <string> are parsed by the compiler once per .cpp file that includes them. In a project with 50 source files, <vector> gets parsed 50 times — even though the result is identical every time.
PCH compiles headers once into a binary .gch file. On every subsequent build the parser is skipped entirely — the pre-built AST is loaded directly from disk.
1. Create pch.h — stable, heavy headers only:
#pragma once
#include <vector>
#include <string>
#include <map>
#include <unordered_map>
#include <algorithm>
#include <iostream>
#include <memory>2. Compile it once:
g++ -std=c++17 pch.h
# produces pch.h.gch in the same directory3. Include it first in every .cpp:
#include "pch.h" // must be first — loads pch.h.gch automatically
#include "my_stuff.h"4. Compile normally:
g++ -std=c++17 main.cpp -o mainWhen the compiler sees #include "pch.h" it checks: does pch.h.gch exist in the same directory with matching compile flags? If yes — load it. If no — parse normally. No configuration needed.
#include "pch.h"
↓
pch.h.gch exists?
↓ yes ↓ no
flags match? parse pch.h normally
↓ yes ↓ no
load .gch parse pch.h normally
target_precompile_headers(my_target PRIVATE pch.h)CMake handles compilation and flag matching automatically.
| Rule | Why |
|---|---|
| PCH must be the first include | Anything before it invalidates the cache |
| Compile flags must match | Different -std= or -D flags = stale cache |
| Only put stable headers in PCH | Frequently changed headers force full PCH recompile |
| Don't include headers you don't use | Bloats .gch, slower to load |
✓ Standard library headers (<vector>, <string>, <map> ...)
✓ Heavy third-party SDKs (OpenCV, Boost, Qt ...)
✗ Your own headers (change frequently → invalidates PCH)
PCH pays off when you have 20+ .cpp files all including the same heavy headers. For small/sandbox projects the build is already fast — skip it.
Without protection, including the same header twice in one .cpp causes redefinition errors. Two solutions:
// Option 1 — pragma once (simpler, universally supported)
#pragma once
// Option 2 — include guards (portable standard C++)
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... content ...
#endif#pragma once protects within a single translation unit — it does not prevent the same header from being parsed in other .cpp files. That is what PCH solves.
The classic array inherited from C. Fixed size, stack-allocated, no bounds checking.
#include <iostream>
int main()
{
int example[5];
std::cout << "Array address: " << example << std::endl; // pointer
std::cout << "Length of array in bytes: " << sizeof(example) << std::endl; // bytes
// Array For Loop
size_t len = sizeof(example) / sizeof(example[0]);
for(int i = 0; i < len; i++){
example[i] = 9;
}
std::cout << "Forth element of example: " << example[3] << std::endl;
// Array Pointer Aritmethic
int *ptr = example;
*(ptr+3) = 1;
std::cout << "Forth element of example: " << example[3] << std::endl;
std::cout << "Example address: " << ptr << std::endl;
std::cout << "Address of second element in example: " << (ptr+1) << std::endl;
// Heap Memory Array
int *another = new int[5];
for(int i = 0; i < 5; i++){
another[i] = 66;
}
std::cout << "Forth element of another: " << another[3] << std::endl;
delete[] another;
return 0;
}
example[3]and*(ptr+3)are identical — indexing[]is just pointer arithmetic under the hood.
A thin wrapper around a C-style array. Same stack allocation and fixed size, but with STL support and bounds checking via .at().
#include <iostream>
#include <array>
int main()
{
std::array<int, 5> new_array;
// Index-based loop
for(int i = 0; i < new_array.size(); i++){
new_array[i] = 55;
}
std::cout << "Forth element of new_array: " << new_array[3] << std::endl;
// Range-based loop (preferred)
for(int val : new_array){
std::cout << val << " ";
}
// Bounds-checked access — throws std::out_of_range if out of bounds
std::cout << new_array.at(3) << std::endl;
return 0;
}Prefer
std::arrayover C-style arrays. Same performance, but safer and STL-compatible.
std::vector<T> stores its elements in a contiguous dynamic array on the heap.
Internally, it uses raw pointers to manage this array.
template<typename T>
class Vector {
T* data_; // raw pointer to heap memory
size_t size_;
size_t capacity_;
};When you push_back(), it may allocate a new array, copy/move the elements, and delete the old array.
#include <iostream>
#include <vector>
int main()
{
std::vector<double> number_vector = {0.5, 0.6, 0.7, 0.8, 0.9};
std::cout << number_vector.at(0) << std::endl;
number_vector.at(0) = 1000;
number_vector.push_back(1.0); // add to end — O(1) amortized
number_vector.insert(number_vector.begin(), -0.6); // add to front — O(n), shifts everything
std::cout << "Length of the vector is: " << number_vector.size() << std::endl;
number_vector.pop_back(); // remove last element
// Range-based loop (preferred)
for(double val : number_vector){
std::cout << val << " ";
}
// 2D vector — each inner vector is a separate heap allocation
std::vector<std::vector<int>> my_2d_vector = {
{1, 2, 3},
{4, 5, 6}
};
std::cout << my_2d_vector.at(1).at(2) << std::endl;
return 0;
}
.at()throwsstd::out_of_rangeon bad index.[]does not — it causes undefined behavior.
C-Style int arr[5] |
std::array<int, 5> |
std::vector<int> |
|
|---|---|---|---|
| Size | Fixed at compile time | Fixed at compile time | Dynamic |
| Memory | Stack | Stack | Heap |
| Bounds checking | No | Yes (.at()) |
Yes (.at()) |
| Knows its own size | No (sizeof trick) |
Yes (.size()) |
Yes (.size()) |
| STL compatible | No | Yes | Yes |
| Manual memory management | No | No | No |
| When to use | Rarely (legacy/C) | Fixed-size collections | Most cases |
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
std::cout << arr[1][2] << "\n"; // 6
for(int i = 0; i < 2; i++){
for(int j = 0; j < 3; j++){
std::cout << arr[i][j] << " ";
}
std::cout << "\n";
}No
.size()— you must hardcode or usesizeof(arr) / sizeof(arr[0])for row count.
std::array<std::array<int, 3>, 2> arr = {{
{1, 2, 3},
{4, 5, 6}
}};
std::cout << arr[1][2] << "\n"; // 6
for(size_t i = 0; i < arr.size(); i++){
for(size_t j = 0; j < arr[i].size(); j++){
std::cout << arr[i][j] << " ";
}
std::cout << "\n";
}Double braces
{{}}needed becausestd::arrayis a struct wrapping a raw array — the outer{}initializes the struct, the inner{}initializes the raw array inside it.
std::vector<std::vector<int>> vec = {
{1, 2, 3},
{4, 5, 6}
};
std::cout << vec[1][2] << "\n"; // 6
for(size_t i = 0; i < vec.size(); i++){
for(size_t j = 0; j < vec[i].size(); j++){
std::cout << vec[i][j] << " ";
}
std::cout << "\n";
}
vec.push_back({7, 8, 9}); // rows can be added at runtimeEach inner vector is a separate heap allocation — not contiguous in memory. More flexible but slower than the fixed-size alternatives.
int arr[2][3] |
std::array<std::array<int,3>,2> |
std::vector<std::vector<int>> |
|
|---|---|---|---|
| Size fixed | Yes | Yes | No |
| Memory | Stack (contiguous) | Stack (contiguous) | Heap (non-contiguous rows) |
.size() |
No | Yes | Yes |
| Bounds checking | No | Yes (.at()) |
Yes (.at()) |
| Rows addable at runtime | No | No | Yes |
| Init syntax | {} |
{{}} (double brace) |
{} |
- Operator = The symbol that performs an action
- Operand = The value or variable the operator acts on
int a = 10;
int b = 5;
int c = a + b;- "+" is the operator
- a and b are the operands
- The operator + adds the two operands
std::cout << "Precise average is: " << static_cast<double>(total) / count << std::endl;11.99999999999999999999999 and 12.0 could be equal for C++ code so be careful with the library usage
| Operator | Meaning | Equivalent To | ||
|---|---|---|---|---|
+= |
Add and assign | x = x + y |
||
-= |
Subtract and assign | x = x - y |
||
*= |
Multiply and assign | x = x * y |
||
/= |
Divide and assign | x = x / y |
||
%= |
Modulo and assign | x = x % y |
||
&= |
Bitwise AND and assign | x = x & y |
||
| ` | =` | Bitwise OR and assign | `x = x | y` |
^= |
Bitwise XOR and assign | x = x ^ y |
||
<<= |
Left shift and assign | x = x << y |
||
>>= |
Right shift and assign | x = x >> y |
int x = 10;
x += 5; // x = 15
x *= 2; // x = 30
x -= 3; // x = 27| Precedence | Operator(s) | Type | Associativity | ||
|---|---|---|---|---|---|
| 1 (Highest) | () [] -> . |
Function call, member | Left to right | ||
| 2 | ++ -- + - ! |
Unary operators | Right to left | ||
| 3 | * / % |
Multiplicative | Left to right | ||
| 4 | + - |
Additive | Left to right | ||
| 5 | < <= > >= |
Relational | Left to right | ||
| 6 | == != |
Equality | Left to right | ||
| 7 | && |
Logical AND | Left to right | ||
| 8 | Logical OR | Left to right | |||
| 9 | = += -= *= /= |
Assignment | Right to left | ||
| 10 (Low) | , |
Comma | Left to right |
#include <iomanip>
if (temperatures.size() != 0){
std::cout << std::fixed << std::setprecision(2) ;
std::cout << "Average: " << sum/temperatures.size() << std::endl;
}if (score >= 90)
{
letter_grade = 'A';
}
else if (score >= 80)
{
letter_grade = 'B';
}
else if (score >= 70){
letter_grade = 'C';
}
else if (score >= 60){
letter_grade = 'D';
}
else{
letter_grade = 'F';
}In C++, you cannot use switch with double or std::string directly. The switch statement only works with integral or enumeration types, such as:
- int
- char
- enum
int day;
std::cout << "Enter a number (1-7): ";
std::cin >> day;
switch (day) {
case 1:
std::cout << "Monday\n";
break;
case 2:
std::cout << "Tuesday\n";
break;
case 3:
std::cout << "Wednesday\n";
break;
case 4:
std::cout << "Thursday\n";
break;
case 5:
std::cout << "Friday\n";
break;
case 6:
std::cout << "Saturday\n";
break;
case 7:
std::cout << "Sunday\n";
break;
default:
std::cout << "Invalid number! Please enter a number between 1 and 7.\n";
}int num1, num2, bigger, smaller;
std::cout << "Enter two integers seperated by space: ";
std::cin >> num1 >> num2;
if (num1 != num2) {
bigger = (num1 > num2) ? num1 : num2;
smaller = (num1 < num2) ? num1 : num2;
std::cout << "Bigger is " << bigger << std::endl;
std::cout << "Smaller is " << smaller << std::endl;
} for (int i = 1; i <=100; i++)
{
std::cout << i;
std::cout << ((i%10 == 0) ? "\n" : " ");
}#include <iostream>
#include <vector>
int main() {
for (int i = 0; i < 5; i++){
std::cout << i << std::endl;
}
for (int i = 10; i > 0; i--){
std::cout << i << std::endl;
}
for (int i=0; i<=100; i+=10){
if (i%15 == 0){
std::cout << i << std::endl;
}
}
for (int i=1, j=5; i<=5; i++, j++){
std::cout << i << " + " << j << " = " << i+j << std::endl;
}
std::vector<int> my_vector = {10,20,30,40};
for (int i = 0; i<my_vector.size() ; i++){
std::cout << my_vector.at(i) << std::endl;
}
return 0;
}The term range refers to a collection of elements you can iterate over — like arrays, vectors, lists, maps, and any container that provides begin() and end() functions (which define a range of iterators).
#include <iostream>
#include <vector>
int main() {
double sum = 0.0;
std::vector <double> temperatures = {22.3, 15.6, 40.4} ;
for (auto temperature : temperatures){
sum += temperature;
}
std::cout << "Average: " << sum/temperatures.size() << std::endl;
return 0;
}#include <iostream>
int main() {
bool done = false;
int number = 0;
while (!done)
{
std::cout << "Enter an integer between 1 and 5: ";
std::cin >> number;
if (number<1 || number>5){
std::cout << "Out of range try again." << std::endl;
}
else{
std::cout << "Thanks!" << std::endl;
done = true;
}
}
return 0;
}If you know that you must perform at least one iteration of the loop, then you should consider the do while loop over while loop.
#include <iostream>
int main() {
char selection;
do {
std::cout << "\n------------" << std::endl;
std::cout << "1: Do this" << std::endl;
std::cout << "2: Do that" << std::endl;
std::cout << "3: Do something else" << std::endl;
std::cout << "Q: Quit" << std::endl;
std::cout << "Enter your selection: ";
std::cin >> selection;
switch (selection){
case '1':
std::cout << "I am doing this." << std::endl;
break ;
case '2':
std::cout << "I am doing that." << std::endl;
break ;
case '3':
std::cout << "I am doing something else." << std::endl;
break ;
case 'Q':
case 'q':
std::cout << "I am exitting from loop." << std::endl;
break ;
default:
std::cout << "Wrong option" << std::endl;
}
} while(selection != 'q' && selection != 'Q') ;
return 0;
}When a continue statement is executed in the loop, no further statements in the body of the loop or executed and control immediately goes directly to the beginning of the loop for the next iteration. So you can think of this as skip processing in the rest of this iteration and go to the beginning of the loop.
When the brake statement is executed in the loop, no further statements in the body are executed and the loop is terminated. So controllers transfer to the statement immediately following the loop.
#include <iostream>
#include <vector>
int main() {
std::vector <int> my_vector = {1,2,-1,3,-1,-99,7,8,10};
for (int element : my_vector){
if (element == -99){
break;
}
else if (element == -1){
continue;
}
else{
std::cout << element << std::endl;
}
}
return 0;
}1
2
3For Loop
for(;;){
std::cout << "This will print forver" << std::endl;
}While Loop
while(true){
std::cout << "This will print forver" << std::endl;
}Do-While Loop
do{
std::cout << "This will print forver" << std::endl;
} while(true);#include <iostream>
#include <vector>
int main (){
int num_items;
std::cout << "How many items you have?: ";
std::cin >> num_items;
std::vector <int> my_vector;
for(int i=1;i<=num_items;i++){
int vector_item;
std::cout << "Enter vector item " << i << ": ";
std::cin >> vector_item;
my_vector.push_back(vector_item);
}
std::cout << "\nDisplaying Histogram" << std::endl;
for (auto val : my_vector){
for(int i=1; i<=val; i++){
if(i%5 == 0){
std::cout << " ";
}
else{
std::cout << "+";
}
}
std::cout << std::endl;
}
std::cout << std::endl;
return 0;
}Before std::string, strings were handled with const char* or char[] — a pointer to a null-terminated array of characters.
const char* name = "Oben"; // pointer to string literal in read-only memoryEvery C-style string ends with a null terminator '\0' that marks the end:
"Oben" in memory: [ 'O' ][ 'b' ][ 'e' ][ 'n' ][ '\0' ]
All C string functions walk the pointer until they hit '\0' to know where the string ends.
#include <iostream>
int main() {
const char* name = "Oben";
// length
std::cout << strlen(name) << std::endl; // 4
// indexing and iteration
std::cout << name[0] << std::endl; // O
for (int i = 0; name[i] != '\0'; i++) {
std::cout << name[i] << " "; // O b e n
}
std::cout << std::endl;
// printing — cout has special operator<< for const char*
// walks pointer until '\0' and prints each char automatically
std::cout << name << std::endl; // Oben (not the address)
std::cout << *name << std::endl; // O (first char only)
// to print the actual address, cast to void*
std::cout << (void*)name << std::endl; // 0x...
return 0;
}| C-style | std::string |
|
|---|---|---|
| Concatenation | Manual buffer + strcat |
s1 + s2 |
| Comparison | strcmp(a, b) == 0 |
a == b |
| Bounds checking | None — buffer overflow | .at() throws |
| Length | strlen(s) |
.size() |
| Modifiable | char[] only |
Always |
Internally std::string has the same structure as std::vector<char>:
class String {
char* data_;
size_t size_;
size_t capacity_;
};Heap-allocated, dynamically sized, with +, ==, and other operators built in.
#include <iostream>
#include <string>
int main(){
std::string s1 = "Apple";
std::string s2 = "Kanana";
s2.at(0) = 'B';
std::cout << s2; // "Banana"
std::cout << std::boolalpha;
std::cout << (s1 < s2); // true ('A' comes before 'B' in the ASCII table)
std::string s3 = s1 + " and " + s2 + " juice"; // "Apple and Banana juice"
std::string my_text = "My name is Oben";
std::cout << my_text.size() << std::endl; // 15
std::cout << my_text.length() << std::endl; // 15
for (int i = 0; i < s1.length(); ++i)
std::cout << s1.at(i);
std::cout << std::endl; // "Apple"
s1 = "This is a test";
std::cout << s1.substr(0,4) << std::endl; // "This"
std::cout << s1.substr(5,2) << std::endl; // "is"
std::cout << s1.substr(10,4) << std::endl; // "test"
s1.erase(0,5);
std::cout << s1 << std::endl; // "is a test"
std::string full_name;
std::cout << "Enter your full name: ";
std::getline(std::cin, full_name);
std::cout << full_name << std::endl ; // "Oben Sustam"
std::cin >> full_name;
std::cout << full_name << std::endl; // "Oben"
s1 = "The secret word is Boo";
std::string word;
std::cout << "Enter the word to find: ";
std::cin >> word;
int position = s1.find(word);
if (position < s1.length())
std::cout << "Found " << word << " at position: " << position << std::endl;
else
std::cout << "Sorry, " << word << " not found" << std::endl;
return 0;
}#include <iostream>
#include <string>
void* operator new(size_t size) { // size = bytes requested, not the value
std::cout << "Allocating " << size << " bytes\n";
void* ptr = malloc(size); // malloc: requests raw bytes from the heap
return ptr;
}
void printName(std::string_view name){
std::cout << name << std::endl;
}
int main(){
#if 0
std::string full_name = "Msc. Eng. Oben Sustam - Robotics Engineer - Munich";
std::string first_part = full_name.substr(0, 21);
std::string second_part = full_name.substr(22, 35);
printName(full_name);
printName(first_part);
printName(second_part);
#elif 1
std::string full_name = "Msc. Eng. Oben Sustam - Robotics Engineer - Munich";
const char* name_ptr = full_name.c_str(); // c_str(): converts std::string to C-style string
std::string_view first_part(name_ptr, 21);
std::string_view second_part(name_ptr + 22, 35);
printName(full_name);
printName(first_part);
printName(second_part);
#else
const char* name_ptr = "Msc. Eng. Oben Sustam - Robotics Engineer - Munich";
std::string_view first_part(name_ptr, 21);
std::string_view second_part(name_ptr + 22, 35);
printName(name_ptr);
printName(first_part);
printName(second_part);
#endif
return 0;
}The core difference is ownership:
std::string s = "Oben"; // owns the memory, manages its own copy
std::string_view view = "Oben"; // points into existing memory, owns nothingstd::string |
std::string_view |
|
|---|---|---|
| Owns memory | Yes | No |
| Heap allocation | Yes (unless SSO) | Never |
| Modifiable | Yes | No |
| Lifetime dependency | Independent | Original must outlive view |
| Use case | Own/modify strings | Read-only access |
To see which options allocate heap memory, operator new is overridden globally:
void* operator new(size_t size) { // size = bytes requested, not the value
std::cout << "Allocating " << size << " bytes\n";
void* ptr = malloc(size); // malloc: requests raw bytes from the heap
return ptr;
}Every heap allocation in the program triggers this — making allocations visible.
void printName(std::string_view name) {
std::cout << name << std::endl;
}std::string_view as a parameter accepts std::string, const char*, and string literals — all without copying. It is the modern replacement for const std::string&.
std::string full_name = "Msc. Eng. Oben Sustam - Robotics Engineer - Munich";
std::string first_part = full_name.substr(0, 21);
std::string second_part = full_name.substr(22, 35);
printName(full_name);
printName(first_part);
printName(second_part);Allocations: 3 — full_name, first_part, second_part each allocate heap memory. substr() always returns a new std::string with its own copy of the data.
std::string full_name = "Msc. Eng. Oben Sustam - Robotics Engineer - Munich";
const char* name_ptr = full_name.c_str(); // c_str(): converts std::string to C-style string
std::string_view first_part(name_ptr, 21);
std::string_view second_part(name_ptr + 22, 35);
printName(full_name);
printName(first_part);
printName(second_part);Allocations: 1 — only full_name allocates. The two string_views point into full_name's heap memory using c_str() which returns a const char* to the underlying data.
c_str() = C string — returns the std::string data as a C-style null-terminated const char*, needed here for pointer arithmetic (+ 22).
Lifetime warning — the views depend on full_name staying alive. If full_name is destroyed or reallocated, the views point to dead memory.
const char* name_ptr = "Msc. Eng. Oben Sustam - Robotics Engineer - Munich";
std::string_view first_part(name_ptr, 21);
std::string_view second_part(name_ptr + 22, 35);
printName(name_ptr);
printName(first_part);
printName(second_part);Allocations: 0 — const char* is a pointer to string literal stored in read-only memory (not heap). Both string_views just point into that same memory with an offset and length — no copies, no allocation.
name_ptr: "Msc. Eng. Oben Sustam - Robotics Engineer - Munich"
first_part: ^--------------------^ (0, 21)
second_part: ^----------------------------------^ (22, 35)
| Option | Allocations | Modifiable | Notes |
|---|---|---|---|
std::string + substr |
3 | Yes | Most copies, safest lifetime |
std::string + string_view via c_str() |
1 | full_name only |
Balance — one allocation, views are free |
const char* + string_view |
0 | No | Fastest, literal in read-only memory |
std::string does not always allocate on the heap. Short strings (typically under 15 chars on GCC) are stored directly inside the std::string object on the stack:
std::string a = "Oben"; // 4 chars — SSO, no heap allocation
std::string b = "Oben Sustam..."; // long string — heap allocationstd::string_view never allocates regardless of length.
#include <iostream>
#include <ctime> // time
#include <cstdlib> // random
int main() {
// Random number generation
int random_number;
int count = 10;
int min = 1;
int max = 6;
// seed the random generator, if not you will get the same sequence random numbers
std::cout << "RAND_MAX on my system is: " << RAND_MAX << std::endl;
srand(time(nullptr));
for (int i=0; i<count; i++){
random_number = rand() % (max - min + 1) + min;
std::cout << random_number << std::endl;
}
return 0;
}#include <iostream>
#inclue <cmath>
int main(){
double num = 31.7;
std::cout << "The ceil of " << num << " is " << ceil(num) << std::endl; // 32
std::cout << "The floor of " << num << " is " << floor(num) << std::endl; // 31
std::cout << "The round of " << num << " is " << round(num) << std::endl; // 32
return 0;
}#include <iostream>
#inclue <cmath>
int main(){
std::cout << "Enter a double number: ";
std::cin >> num;
std::cout << "The square root of " << num << " is " << sqrt(num) << std::endl;
std::cout << "The cubed root of " << num << " is " << cbrt(num) << std::endl;
double power;
std::cout << "Enter a power: ";
std::cin >> power;
std::cout << num << " power " << power << " is " << pow(num, power) << std::endl;
return 0;
}#include <iostream>
#inclue <cmath>
int main(){
double num = 30;
std::cout << "The sine of " << num << " is " << sin(num*(M_PI/180)) << std::endl;
std::cout << "The cosine of " << num << " is " << cos(num*(M_PI/180)) << std::endl;
return 0;
}#include <iostream>
#include <cmath>
double area_circle(double);
double volume_cylinder(double, double);
int main() {
double radius, height;
std::cin >> radius >> height;
double my_volume = volume_cylinder(radius, height);
std::cout << my_volume << std::endl;
return 0;
}
double area_circle(double r){
double area = M_PI * pow(r,2);
return area;
}
double volume_cylinder(double radius, double height){
double volume = area_circle(radius) * height;
return volume;
}Put the default arguments in prototypes
#include <iostream>
#include <iomanip>
double calc_cost(double base_cost, double tax_rate = 0.4, double shipping = 0.0);
int main(){
double cost = 0.0;
cost = calc_cost(100.0, 0.08, 4.25);
std::cout << std::fixed << std::setprecision(3);
std::cout << "Cost is: " << cost << std::endl; // 112.250
cost = calc_cost(100.0);
std::cout << "Cost is: " << cost << std::endl; // 140.000
return 0;
}
double calc_cost(double base_cost, double tax_rate, double shipping){
return base_cost += (base_cost*tax_rate) + shipping;
}#include <iostream>
void pass_by_pointer(int *num){
*num -= 5;
}
void swap(int *x, int *y){
int temp = *x;
*x = *y;
*y = temp;
}
void pass_by_reference(int &x){
x -= 5;
}
void pass_by_value(int x){
x -= 5;
}
int pass_by_value_return(int x){
x -= 5;
return x;
}
int main(){
std::cout << "-------Pass by Pointer----" << std::endl;
int my_num = 100;
std::cout << "Before Decrement: " << my_num << std::endl;
pass_by_pointer(&my_num);
std::cout << "After Decrement: " << my_num << "\n" << std::endl;
std::cout << "---------Swap with Pointer----" << std::endl;
int a = 10;
int b = 20;
std::cout << "Before Swap A: " << a << " B: " << b << std::endl;
swap(&a, &b);
std::cout << "After Swap A: " << a << " B: " << b << "\n" << std::endl;
std::cout << "--------Pass by Reference-----" << std::endl;
int c = 100;
std::cout << "Before Decrement: " << c << std::endl;
pass_by_reference(c);
std::cout << "After Decrement: " << c << "\n" << std::endl;
std::cout << "------Pass by Value------" << std::endl;
int d = 100;
std::cout << "Before Decrement: " << d << std::endl;
pass_by_value(d);
std::cout << "After Decrement: " << d << "\n" << std::endl;
std::cout << "-----Pass by Value with Return------" << std::endl;
int e = 100;
std::cout << "Before Decrement: " << e << std::endl;
std::cout << "After Decrement: " << pass_by_value_return(e) << std::endl;
return 0;
}-------Pass by Pointer----
Before Decrement: 100
After Decrement: 95
---------Swap with Pointer----
Before Swap A: 10 B: 20
After Swap A: 20 B: 10
--------Pass by Reference-----
Before Decrement: 100
After Decrement: 95
------Pass by Value------
Before Decrement: 100
After Decrement: 100
-----Pass by Value with Return------
Before Decrement: 100
After Decrement: 95
|
#include <iostream>
#include <string>
#include <typeinfo>
using namespace std;
void print_guest_list(const std::string &g1, const std::string &g2, const std::string &g3);
void clear_guest_list(std::string &g1, std::string &g2, std::string &g3);
int main() {
string guest_1 {"Larry"};
string guest_2 {"Moe"};
string guest_3 {"Curly"};
print_guest_list(guest_1, guest_2, guest_3);
clear_guest_list(guest_1, guest_2, guest_3);
print_guest_list(guest_1, guest_2, guest_3);
return 0;
}
void print_guest_list(const std::string &g1, const std::string &g2, const std::string &g3) {
cout << g1 << endl;
cout << g2 << endl;
cout << g3 << endl;
}
void clear_guest_list(std::string &g1, std::string &g2, std::string &g3) {
g1 = " ";
g2 = " ";
g3 = " ";
}| Concept | Explanation |
|---|---|
| Scope | A variable is only accessible within the block it is declared in and its inner blocks. |
| Shadowing | A variable in an inner scope can have the same name as one in an outer scope, temporarily hiding the outer one. |
| Lifetime | Inner num (value 200) only exists within its block. Once the block ends, it's destroyed. |
| Outer Access | Inner blocks can access variables from outer blocks unless they’re shadowed. |
#include <iostream>
int num = 300;
int main() {
int num = 100;
int num1 = 500;
std::cout << "Local num is : " << num << " in main" << std::endl; // 100
{
int num = 200;
std::cout << "Local num is: " << num << " in inner block in main" << std::endl; // 200
std::cout << "Inner block in main can see out - num1 is: " << num1 << std::endl; // 500
}
std::cout << "Local num is : " << num << " in main" << std::endl; // 100
return 0;
}#include <iostream>
unsigned long long factorial(unsigned long long val);
int main (){
unsigned long long input;
std::cout << "Enter factorial input: ";
std::cin >> input;
int result = factorial(input);
std::cout << input << "! = " << result << std::endl;
return 0;
}
unsigned long long factorial(unsigned long long val){
if(val == 1){
return 1;
}
return val * factorial(val-1);
}int a = 175; // 0x1000 — int, 4 bytes
double b = 255.0; // 0x1008 — double, 8 bytes
int c = 43; // 0x1010 — int, 4 bytes
int* sp = &a; // 0x1018 — points to a on stack (0x1000)
int* val1 = new int; // 0x1020 — points to heap (0x2018)
int* p = new int; // 0x1028 — points to heap (0x2040)
*val1 = 300; // heap @ 0x2018 → 2C 01 00 00
*p = 255; // heap @ 0x2040 → FF 00 00 00
delete val1;
delete p;| Feature | Stack | Heap |
|---|---|---|
| Size limit | Small & fixed (e.g., ~8 MB per thread on Linux) | Large & flexible (limited by system RAM, often GBs) |
| Lifetime | Automatic: variables destroyed when scope ends | Manual: memory stays until delete or smart pointer frees it |
| Speed | Very fast (simple push/pop operations) | Slower (requires OS bookkeeping and possible fragmentation) |
| Allocation | Done automatically by compiler | Done manually with new, malloc, or containers like std::vector |
| Deallocation | Automatic when scope ends | Manual (delete / delete[]), or automatic with smart pointers/RAII |
| Typical usage | Local variables, function parameters, small temporary objects | Large data, dynamic arrays, objects needing custom lifetime |
| Errors | Stack overflow (too much usage) | Memory leak (forgetting to free), dangling pointers, fragmentation |
| Example | int x = 10; |
int* p = new int(10); delete p; |
| Analogy | Lunch tray (items stacked & removed in order) | Warehouse (flexible storage, but must clean up yourself) |
#include <iostream>
#include <memory>
#include <string>
struct AllocationMetrics{
uint32_t totalAllocatedMemory = 0;
uint32_t totalFreed = 0;
uint32_t currentUsage(){
return totalAllocatedMemory - totalFreed;
}
};
static AllocationMetrics s_AllocationMetrics;
void* operator new(size_t size) {
s_AllocationMetrics.totalAllocatedMemory += size;
void* p = malloc(size);
return p;
}
void operator delete(void* memory, size_t size){
s_AllocationMetrics.totalFreed += size;
free(memory);
}
struct Object{
int x, y, z;
};
void printMemoryUsage(){
std::cout << "Memory Usage: " << s_AllocationMetrics.currentUsage() << std::endl;
}
int main() {
printMemoryUsage();
int* i_ptr = new int(77);
printMemoryUsage();
{
std::unique_ptr<Object> obj_p = std::make_unique<Object>();
printMemoryUsage();
}
printMemoryUsage();
delete i_ptr;
printMemoryUsage();
return 0;
}Memory Usage: 0
Memory Usage: 4
Memory Usage: 16
Memory Usage: 4
Memory Usage: 0When you pass an object to a function in C++, it makes a full copy by default.
For a small type like int (4 bytes), that's fine. But imagine a sensor struct:
struct SensorData {
double readings[1000]; // 8000 bytes of data
std::string name;
int id;
};Now pass it to a function normally:
void process(SensorData data) {
// C++ copied all 8000+ bytes just to call this function
data.readings[0] = 99.0; // modifies the copy, NOT the original!
}
int main() {
SensorData sensor;
process(sensor); // expensive copy, and the original is unchanged
}Two problems:
- Slow — copying 8000+ bytes on every function call adds up fast
- Wrong — you modified a copy, not the real sensor data
A pointer stores the memory address of the object — always just 8 bytes on a 64-bit system, regardless of how big the object is.
void process(SensorData* data) {
// No copy made — just passed an 8-byte address
data->readings[0] = 99.0; // modifies the ORIGINAL
}
int main() {
SensorData sensor;
process(&sensor); // pass the address, not the whole object
}| Method | Bytes passed | Modifies original? |
|---|---|---|
| Pass by value | 8000+ | ❌ No (copy) |
| Pass by pointer | 8 | ✅ Yes |
A pointer is just a memory address. On modern 64-bit hardware, all addresses are 64-bit numbers — so every pointer is 8 bytes, no matter what it points to:
SensorData sensor; // 8000+ bytes
SensorData* p = &sensor; // 8 bytes — just the address
int x = 42; // 4 bytes
int* px = &x; // still 8 bytesNote: Using a pointer to avoid copying an
intmakes no sense — the pointer (8 bytes) is already bigger than theint(4 bytes). Pointers pay off for large objects like structs, vectors, and ROS2 messages.
A reference is an alias for an existing variable. Under the hood it works like a pointer (just passes the address), but the syntax is cleaner — no & at the call site, no -> inside the function:
void process(SensorData& data) { // & means "reference to"
data.readings[0] = 99.0; // dot syntax, modifies the ORIGINAL
}
int main() {
SensorData sensor;
process(sensor); // looks like pass by value, but isn't
}If you want to guarantee the function won't modify the original, add const:
void printSensor(const SensorData& data) {
// data.readings[0] = 99.0; // compile error — const prevents this
std::cout << data.id << std::endl;
}This is the most common pattern in ROS2 callbacks:
void callback(const sensor_msgs::msg::LaserScan& msg) {
// msg is not copied, and cannot be modified
}Pointer * |
Reference & |
|
|---|---|---|
| Can be null | ✅ Yes | ❌ No (always valid) |
| Can be reassigned | ✅ Yes | ❌ No (bound once) |
| Syntax | data->field, &sensor |
data.field, no extras |
| Use when | Optional values, heap objects, smart pointers | Function params, ROS2 callbacks |
Rule of thumb: prefer references for function parameters. Use pointers when the object might not exist (nullable) or you need to manage its lifetime.
Raw pointers work, but you have to manually free the memory — easy to forget and crash. Modern C++ uses smart pointers that clean up automatically:
#include <memory>
// unique_ptr — one owner (e.g. a single ROS2 node owns the sensor driver)
auto driver = std::make_unique<SensorData>();
// shared_ptr — multiple owners (e.g. multiple nodes share sensor config)
auto config = std::make_shared<SensorData>();Same efficiency as raw pointers, no manual memory management.
| Concept | What it means |
|---|---|
| Pass by value | Full copy — safe but slow for large objects |
| Pass by pointer | Just the address (8 bytes) — fast, modifies original |
| Pass by reference | Alias for the original — clean syntax, same speed as pointer |
const reference |
Read-only alias — no copy, no modification (ROS2 callbacks) |
Raw pointer (*) |
Manual memory management — error prone |
| Smart pointer | Automatic cleanup — use these in modern C++ and ROS2 |
#include <iostream>
#include <string>
int main() {
// --- Stack memory ---
int stackVar = 42; // stack variable
int *stackPtr = &stackVar; // pointer to stack variable
std::cout << "Stack variable value: " << stackVar << std::endl;
std::cout << "Stack variable address: " << &stackVar << std::endl;
std::cout << "Stack pointer value (points to stackVar): " << stackPtr << std::endl;
std::cout << "Stack pointer dereferenced: " << *stackPtr << std::endl;
// --- Heap memory ---
int* heapVar = new int(99); // allocate int on heap
std::cout << "\nHeap variable value: " << *heapVar << std::endl;
std::cout << "Heap pointer address: " << &heapVar << std::endl; // address of the pointer itself (on stack)
std::cout << "Heap memory address it points to: " << heapVar << std::endl; // address of heap memory
// Update heap value through pointer
*heapVar = 123;
std::cout << "Heap variable updated value: " << *heapVar << std::endl;
// Free heap memory
delete heapVar;
heapVar = nullptr; // avoid dangling pointer
return 0;
}Stack variable value: 42
Stack variable address: 0x7ffee3c8a6ac
Stack pointer value (points to stackVar): 0x7ffee3c8a6ac
Stack pointer dereferenced: 42
Heap variable value: 99
Heap pointer address: 0x7ffee3c8a6b0
Heap memory address it points to: 0x600003e000
Heap variable updated value: 123#include <iostream>
#include <string>
class Entity{
private:
std::string m_Name;
public:
Entity() :
m_Name("Unknown") {
}
Entity(const std::string &name) :
m_Name(name){
}
const std::string &get_name() const{
return m_Name;
}
};
int main()
{
Entity entity;
std::cout << entity.get_name() << std::endl;
Entity entity2("Oben");
std::cout << entity2.get_name() << std::endl;
// stack memory
Entity *entity_ptr = nullptr;
{
Entity entity3("Orbay");
entity_ptr = &entity3;
std::cout << entity3.get_name() << std::endl;
std::cout << entity_ptr->get_name() << std::endl;
}
// heap memory
Entity *entity_ptr2 = nullptr;
{
entity_ptr2 = new Entity("Orcun");
std::cout << entity_ptr2->get_name() << std::endl;
delete entity_ptr2;
}
return 0;
}int stores 4 bytes. BigStackArray has 4.000.000 elements -> 4.000.000 x 4 = 16.000.000 Byte = 16 MB. Which is higher than stack memory size (8 MB). Code will give error.
#include <iostream>
int main(){
// stack memory
int bigStackArray[4000000];
bigStackArray[0] = 0;
std::cout << "First Element: " << bigStackArray[0] << std::endl;
}#include <iostream>
int main(){
while(true){
int *ptr = new int[4000000];
std::cout << "Leaking..." << std::endl;
}
return 0;
}-
The Heap: The OS reserves a contiguous block of memory on the heap large enough to hold 4,000,000 integers.
-
The Stack: The variable ptr is created on the stack. It stores the memory address of the first integer in that 16MB block.
-
Allocation (new): In every iteration of the while(true) loop, the OS sets aside 16MB of heap memory and gives your ptr pointer the address (the "key") to access it.
-
Scope End: When the loop iteration ends, the variable ptr (the pointer/key) is destroyed because it goes out of scope.
-
The Orphan: Because you didn't call delete[] before the loop repeated, you have "lost the key" to those 16MB. The memory remains reserved by your program, but you no longer have a way to reach it or tell the OS you are finished with it.
-
Repeat: This happens over and over. Within a few seconds, your program will have requested gigabytes of RAM.
-
Copying raw pointers copies only the address, not the actual object → two objects point to the same memory.
-
Leads to double deletion when both destructors try to free the same heap memory.
-
Deep copy is needed to safely duplicate objects that own dynamic memory.
| Feature | Raw Pointer (T*) |
unique_ptr<T> |
shared_ptr<T> |
weak_ptr<T> |
|---|---|---|---|---|
| Ownership | Manual / unclear | Exclusive | Shared (ref-counted) | Non-owning observer |
| Memory management | Manual (delete) |
Automatic (RAII) | Automatic (RAII) | None (doesn't own) |
| Copyable | Yes | No | Yes (increments ref count) | Yes |
| Movable | Yes | Yes | Yes | Yes |
| Overhead | None | None | Ref-count + control block | Same as shared_ptr |
| Null-safe | No | No | No | Via .lock() check |
| Access syntax | ->, * |
->, * |
->, * |
Must call .lock() first |
| Risk of dangling pointer | High | None | None | Yes (if owner is gone) |
| Risk of double free | High | None | None | N/A |
| Cyclic reference safe | N/A | N/A | No (causes memory leak) | Yes (breaks cycles) |
| Use case | Legacy C code, low-level APIs | Single owner, factory returns | Shared ownership across components | Cache, observer, breaking cycles |
| Header | Built-in | <memory> |
<memory> |
<memory> |
| C++ standard | All | C++11 | C++11 | C++11 |
#include <iostream>
#include <memory>
struct Robot{
int id;
int distance;
Robot(int id) : id(id), distance(0){
std::cout << "Robot created with id: " << id << std::endl;
}
~Robot(){
std::cout << "Robot destroyed with id: " << id << std::endl;
}
void move(int meter){
distance += meter;
std::cout << "Robot moved to distance: " << distance << std::endl;
}
};
void move(Robot& r, int val){
r.move(val);
}
void move2(Robot* ptr, int val){
ptr->move(val);
}
int main(){
std::unique_ptr<Robot> rbt_ptr1 = std::make_unique<Robot>(7);
rbt_ptr1->move(7);
move(*rbt_ptr1, 7);
std::unique_ptr<Robot> rbt_ptr2 = std::move(rbt_ptr1);
rbt_ptr2->id = 5;
if (rbt_ptr1 == nullptr) {
std::cout << "rbt_ptr1 gave up ownership" << std::endl;
}
else{
std::cout << "rbt_ptr1 still owns it" << std::endl;
}
move(*rbt_ptr2, 5);
move2(rbt_ptr2.get(), 5);
return 0;
}Robot created with id: 7
Robot moved to distance: 7
Robot moved to distance: 14
rbt_ptr1 gave up ownership
Robot moved to distance: 19
Robot moved to distance: 24
Robot destroyed with id: 5#include <iostream>
#include <memory>
struct Robot{
int id;
int distance;
Robot(int id) : id(id), distance(0){
std::cout << "Robot created with id: " << id << std::endl;
}
~Robot(){
std::cout << "Robot destroyed with id: " << id << std::endl;
}
void move(int meter){
distance += meter;
std::cout << "Robot moved to distance: " << distance << std::endl;
}
};
void move(Robot& r, int val){
r.move(val);
}
void move2(Robot* ptr, int val){
ptr->move(val);
}
int main(){
std::shared_ptr<Robot> rbt_ptr1 = std::make_shared<Robot>(7);
std::cout << "refs to robot: " << rbt_ptr1.use_count() << std::endl;
rbt_ptr1->move(7);
move(*rbt_ptr1, 7);
std::shared_ptr<Robot> rbt_ptr2 = rbt_ptr1;
if (rbt_ptr1 == nullptr) {
std::cout << "rbt_ptr1 gave up ownership" << std::endl;
}
else{
std::cout << "rbt_ptr1 still owns it" << std::endl;
}
std::cout << "refs to robot: " << rbt_ptr2.use_count() << std::endl;
rbt_ptr2->id = 5;
move(*rbt_ptr2, 5);
move2(rbt_ptr2.get(), 5);
{
std::shared_ptr<Robot> rbt_ptr3 = rbt_ptr1;
rbt_ptr3->id = 9;
std::cout << "refs to robot: " << rbt_ptr3.use_count() << std::endl;
move(*rbt_ptr3, 9);
}
std::cout << "refs to robot: " << rbt_ptr1.use_count() << std::endl;
return 0;
}Robot created with id: 7
refs to robot: 1
Robot moved to distance: 7
Robot moved to distance: 14
rbt_ptr1 still owns it
refs to robot: 2
Robot moved to distance: 19
Robot moved to distance: 24
refs to robot: 3
Robot moved to distance: 33
refs to robot: 2
Robot destroyed with id: 9this is always a pointer to the object itself, but it does not know whether the object is on the stack or heap.
#include <iostream>
#include <string>
class Entity;
void printEntity(Entity &e);
class Entity{
public:
int x, y;
Entity(int x, int y) {
this->x = x;
this->y = y;
printEntity(*this);
}
int get_x() const{
return this->x;
}
};
void printEntity(Entity &e){
std::cout << e.x << ", " << e.y << std::endl;
}
int main(){
Entity e1(3,5);
return 0;
}3, 5#include <iostream>
int main(){
double a = 5;
void* p = &a;
std::cout << p << std::endl;
std::cout << *static_cast<double*>(p) << std::endl;
return 0;
}0x7fffb44effa8
5
#include <iostream>
#include <string>
#include <vector>
class Player{
public:
// attributes
std::string name;
int health;
int experience;
// methods
void talk(std::string text_to_say){ std::cout << name << " says " << text_to_say << std::endl;}
bool is_dead();
};
class Account{
public:
// attributes
std::string name;
double balance;
// methods
bool deposit(double bal){ balance += bal; std::cout << "In deposit" << std::endl; return true;}
bool withdraw(double bal){ balance -= bal; std::cout << "In withdraw" << std::endl; return true;}
};
int main (){
Player frank;
frank.name = "Frank";
frank.health = 100;
frank.experience = 12;
frank.talk("Hi");
Player *enemy = new Player;
enemy->name = "Enemy";
enemy->health = 100;
enemy->talk("I will destroy you");
Account n26;
n26.balance = 1000;
n26.deposit(500);
std::cout << n26.balance << std::endl;
return 0;
}#include <iostream>
#include <string>
#include <vector>
class Account{
private:
// attributes
std::string name;
double balance;
public:
// methods declared inline
void set_balance(double bal){
balance = bal;
}
double get_balance(){
return balance;
}
// methods will be declared outside the class declaration
void set_name(std::string name);
std::string get_name();
bool deposit(double amount);
bool withdraw(double amount);
};
void Account::set_name(std::string n){
name = n;
}
std::string Account::get_name(){
return name;
}
bool Account::deposit(double amount){
balance += amount;
return true;
}
bool Account::withdraw(double amount){
if (balance-amount >= 0){
balance -= amount;
return true;
}
else {
return false;
}
}
int main (){
Account n26;
n26.set_name("Oben Main Account");
n26.set_balance(1000.0);
if (n26.deposit(200.0)){
std::cout << "Deposit OK" << std::endl;
}
else{
std::cout << "Deposit not allowed" << std::endl;
}
if (n26.withdraw(500.0)){
std::cout << "Withdraw OK" << std::endl;
}
else{
std::cout << "Not sufficient funds" << std::endl;
}
if (n26.withdraw(1500.0)){
std::cout << "Withdraw OK" << std::endl;
}
else{
std::cout << "Not sufficient funds" << std::endl;
}
return 0;
}Deposit OK
Withdraw OK
Not sufficient fundsaccount.h
#ifndef _ACCOUNT_H_
#define _ACCOUNT_H_
#include <iostream>
#include <string>
class Account{
private:
std::string name;
double balance;
public:
std::string get_name();
double get_balance();
void set_name(std::string);
void set_balance(double);
bool deposit(double);
bool withdraw(double);
};
#endif // _ACCOUNT_H_04_account.cpp
#include "account.h"
void Account::set_name(std::string n){
name = n;
}
std::string Account::get_name(){
return name;
}
bool Account::deposit(double dep){
balance += dep;
return true;
}
bool Account::withdraw(double with){
if(balance-with >= 0){
balance -= with;
return 1;
}
else{
return 0;
}
}
void Account::set_balance(double bal){
balance = bal;
}
double Account::get_balance(){
return balance;
}04_main.cpp
#include "account.h"
int main(){
Account n26;
n26.set_name("Oben Sustam");
n26.set_balance(1000);
std::cout << "Username: " << n26.get_name() << std::endl;
std::cout << "Balance: " << n26.get_balance() << std::endl;
n26.deposit(500);
std::cout << "Balance after deposit: " << n26.get_balance() << std::endl;
if(n26.withdraw(200)){
std::cout << "Balance after withdraw: " << n26.get_balance() << std::endl;
}
else{
std::cout << "Not enough money" << std::endl;
}
if(n26.withdraw(2200)){
std::cout << "Balance after withdraw: " << n26.get_balance() << std::endl;
}
else{
std::cout << "Not enough money" << std::endl;
}
return 0;
}#include <iostream>
#include <string>
class Player{
private:
std::string name;
int health;
int xp;
public:
// Overloaded Constructors (with different parameters (type, number, or order).)
Player() :
name("Oben"), health(99), xp(29){
}
Player(std::string &name_val) :
name(name_val), health(80), xp(22){
}
Player(std::string &name_val, int &health_val, int &xp_val) :
name(name_val), health(health_val), xp(xp_val){
}
// Destructor
~Player(){
std::cout << "Destructor called for " << name << std::endl;
}
std::string get_name(){
return name;
}
int get_health(){
return health;
}
};
int main() {
std::cout << "--- Creating first player ---" << std::endl;
Player first_player;
std::cout << first_player.get_name() << std::endl;
std::cout << first_player.get_health() << std::endl;
std::cout << "\n--- Creating second player ---" << std::endl;
Player second_player("Orbay");
std::cout << second_player.get_name() << std::endl;
std::cout << second_player.get_health() << std::endl;
std::cout << "\n--- Creating third player ---" << std::endl;
Player third_player("Orcun", 100, 20);
std::cout << third_player.get_name() << std::endl;
std::cout << third_player.get_health() << std::endl;
std::cout << "\n--- End of main ---" << std::endl;
return 0;
}--- Creating first player ---
Default constructor called for Oben
Oben
99
--- Creating second player ---
Single-parameter constructor called for Orbay
Orbay
80
--- Creating third player ---
Three-parameter constructor called for Orcun
Orcun
100
--- End of main ---
Destructor called for Orcun
Destructor called for Orbay
Destructor called for Oben#include <iostream>
#include <memory>
#include <string>
class Car{
private:
std::string model_;
public:
// Constructor
Car(const std::string &model) : model_(model){
std::cout << "Car " << model_ << " created 🚗\n";
}
// Copy constructor
Car(const Car& other) : model_(other.model_){
std::cout << "Car " << model_ << " copied 🧬\n";
}
// Destructor
~Car(){
std::cout << "Car " << model_ << " destroyed 💥\n";
}
void drive() const{
std::cout << "Car " << model_ << " is driving...\n";
}
};
int main(){
std::cout << "--- Copy Constructor Example ---\n";
// Normal stack object
Car car1("BMW");
// Copy construction
Car car2 = car1; // <-- calls copy constructor
car2.drive();
std::cout << "End of main()\n";
return 0;
}--- Copy Constructor Example ---
Car BMW created 🚗
Car BMW copied 🧬
Car BMW is driving...
End of main()
Car BMW destroyed 💥
Car BMW destroyed 💥#include <iostream>
#include <cstring>
class Car {
private:
char* model_; // heap-allocated string
public:
Car(const char* model) {
model_ = new char[strlen(model) + 1];
std::strcpy(model_, model);
std::cout << "Car " << model_ << " created 🚗\n";
}
// ❌ Shallow copy constructor
Car(const Car& other) {
model_ = other.model_; // pointer copied only
std::cout << "Car shallow-copied ⚠️\n";
}
~Car() {
std::cout << "Destroying car " << model_ << "\n";
delete[] model_; // double delete risk!
}
};
int main() {
Car c1("BMW");
Car c2 = c1; // shallow copy
return 0; // 💥 undefined behavior when both destructors run
}- c1 and c2 share same memory
- both call delete[]
- → crash / undefined behavior
#include <iostream>
#include <cstring>
class Car {
private:
char* model_;
public:
Car(const char* model) {
model_ = new char[strlen(model) + 1];
std::strcpy(model_, model);
std::cout << "Car " << model_ << " created 🚗\n";
}
// ✅ Deep copy constructor
Car(const Car& other) {
model_ = new char[strlen(other.model_) + 1];
std::strcpy(model_, other.model_);
std::cout << "Car deep-copied ✅\n";
}
~Car() {
std::cout << "Destroying car " << model_ << "\n";
delete[] model_;
}
};
int main() {
Car c1("Audi");
Car c2 = c1; // deep copy
return 0; // safe
}| Feature | class |
struct |
|---|---|---|
| Default access | private |
public |
| Default inheritance | private |
public |
#include <iostream>
#include <string>
struct Log {
int severity;
std::string text;
void print() {
std::cout << "Logging Severity: " << severity << ", Text: " << text << "\n";
}
};
int main(){
Log my_log;
my_log.severity = 2;
my_log.text = "Hello World";
my_log.print();
return 0;
}Logging Severity: 2, Text: Hello WorldInheritance means that one class (called the child or derived class) can reuse and extend the properties and behaviors of another class (called the parent or base class).
A derived class inherits everything from its base class — and can add or change features.
#include <iostream>
class Entity{
public:
float X, Y;
void move(float xa, float ya){
X += xa;
Y += ya;
}
};
class Player : public Entity{
public:
const char *name;
void printName(){
std::cout << name << std::endl;
}
};
int main(){
std::cout << sizeof(Entity) << std::endl;
std::cout << sizeof(Player) << std::endl;
Player p1;
p1.name = "Oben";
p1.move(5,5);
std::cout << p1.X << std::endl;
p1.printName();
return 0;
}8
16
5
ObenPolymorphism literally means “many forms.” In programming, it means the same function, operator, or method name can behave differently depending on the type of object or data it’s working with.
Why It Exists?
It allows you to write flexible and reusable code — you don’t have to know the exact type of object in advance, yet the correct behavior will still happen automatically.
Two Main Types
| Type | Also called | Achieved by | When it happens |
|---|---|---|---|
| Compile-time polymorphism | Static polymorphism | Function overloading, operator overloading, templates | During compilation |
| Runtime polymorphism | Dynamic polymorphism | Virtual functions and inheritance | While the program runs |
The function that will be called is known at compile time.
#include <iostream>
void print(int x) { std::cout << "Integer: " << x << "\n"; }
void print(double x) { std::cout << "Double: " << x << "\n"; }
int main() {
print(5); // Calls print(int)
print(5.5); // Calls print(double)
}#include <iostream>
#include <vector>
#include <tuple>
#include <string>
template<typename T>
void print(T value){
std::cout << "Value: " << value << std::endl;
}
int main(){
std::vector<float> v = {20.4, 89.7, 66.0};
for(float vi: v){
print(vi);
}
std::tuple<bool, std::string> t = {false, "true"};
print(std::get<0>(t));
print(std::get<1>(t));
print("Oben");
print(29);
return 0;
}Same function name (print), different behavior depending on parameter type.
#include <iostream>
class Animal {
public:
virtual void speak() { std::cout << "Some sound\n"; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "Meow!\n"; }
};
int main() {
Animal* a1 = new Dog();
Animal* a2 = new Cat();
a1->speak(); // Outputs "Woof!"
a2->speak(); // Outputs "Meow!"
}Even though both a1 and a2 are pointers to Animal, the correct function (Dog::speak or Cat::speak) is chosen at runtime.
What is an Interface in C++?
-
C++ does not have a dedicated interface keyword like Java or C#. Instead, interfaces are implemented using abstract classes, specifically classes that contain only pure virtual functions.
-
Definition of an Interface (Abstract Class)
- A class is considered an interface when:
- All its methods are pure virtual (declared with = 0).
- It contains no data members (ideally).
- It defines a contract that derived classes must implement.
// Interface for any robot gripper
class IGripper {
public:
virtual void open() = 0; // pure virtual
virtual void close() = 0; // pure virtual
virtual bool isHolding() const = 0;
virtual ~IGripper() {} // always add virtual destructor
};class FanucGripper : public IGripper {
public:
void open() override {
std::cout << "Fanuc gripper opening...\n";
}
void close() override {
std::cout << "Fanuc gripper closing...\n";
}
bool isHolding() const override {
return true; // example
}
};IGripper* gripper = new FanucGripper();
gripper->open();
gripper->close();
delete gripper;#include <iostream>
class Base {
public:
Base() { std::cout << "Base Constructor" << std::endl; }
virtual ~Base() { std::cout << "Base Destructor" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived Constructor" << std::endl; }
~Derived() { std::cout << "Derived Destructor" << std::endl; }
};
int main() {
Base *base = new Base(); // Case 1: Base* -> Base obj
delete base;
Derived *derived = new Derived(); // Case 2: Derived* -> Derived obj
delete derived;
Base *poly = new Derived(); // Case 3: Base* -> Derived obj
delete poly;
}| Case | Pointer type | Object | Destructor(s) called | Safe? |
|---|---|---|---|---|
Base *base = new Base() |
Base* |
Base | ~Base() |
Yes |
Derived *derived = new Derived() |
Derived* |
Derived | ~Derived() → ~Base() |
Yes |
Base *poly = new Derived() |
Base* |
Derived | ~Base() only — leak |
No* |
*Safe only if ~Base() is virtual.
Constructors always run base-first. Destructors always run derived-first.
The compiler sees Base* at the delete line and hard-codes a call to ~Base() at compile time. It never inspects the actual object. The Derived part silently leaks — this is undefined behavior.
class Base {
public:
virtual ~Base() = default;
};With virtual, the compiler defers the destructor call to runtime via the vtable. Each object gets a hidden vptr pointing to its class's vtable — an array of function pointers for all virtual functions. At delete poly, the program follows the vptr to Derived's vtable, finds ~Derived(), and calls it — then automatically chains to ~Base().
If a class is meant to be inherited from, always declare its destructor
virtual.
A variable that stores the memory address of a function.
int add(int a, int b) { return a + b; }
int (*fn_ptr)(int, int) = &add; // pointer to a function: takes 2 ints, returns int
fn_ptr(3, 4); // calls add(3, 4) → 7Passing a function as an argument:
void apply(int a, int b, int (*fn)(int, int)) {
std::cout << fn(a, b) << std::endl;
}
apply(3, 4, add); // 7Raw function pointers are C-style. In modern C++ prefer lambdas or
std::function.
An anonymous function defined inline. Syntax:
[ capture ] ( parameters ) { body }
auto add = [](int a, int b) {
return a + b;
};
add(3, 4); // 7The key difference from raw function pointers — lambdas can access variables from the surrounding scope:
int x = 10;
auto add_x = [x](int a) { return a + x; }; // capture x by value
add_x(5); // 15| Capture | Meaning |
|---|---|
[x] |
capture x by value (copy) |
[&x] |
capture x by reference |
[=] |
capture everything by value |
[&] |
capture everything by reference |
[] |
capture nothing |
std::vector<int> nums = {3, 1, 4, 1, 5};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // descending → {5, 4, 3, 1, 1}
});#include <iostream>
void doMath(int a, int b) {
auto add = [a, b]() {
return a + b;
};
auto multiply = [a, b]() {
return a * b;
};
std::cout << "Add: " << add() << std::endl;
std::cout << "Multiply: " << multiply() << std::endl;
}
int main(){
doMath(4, 6);
return 0;
}| Raw Function Pointer | Lambda | |
|---|---|---|
| Syntax | int (*fn)(int, int) |
[](int a, int b) { ... } |
| Defined inline | No | Yes |
| Can capture variables | No | Yes |
| Modern C++ | No | Yes |
Initialized once, persists across calls. Scope stays limited to the function.
void function() {
static int i = 0; // initialized once, persists across calls
i++;
std::cout << i << std::endl;
}
int main() {
for (int j = 0; j < 5; j++) {
function(); // prints 1, 2, 3, 4, 5
}
}Without static, i resets to 0 every call and always prints 1.
| Property | Normal local | Local static |
|---|---|---|
| Initialized | Every call | Once |
| Lifetime | Dies on return | Entire program |
| Scope | Inside function | Inside function |
Limits the function's visibility to the current translation unit (.cpp file). Other .cpp files cannot see or call it — useful for internal helpers.
// utils.cpp
static void helper() {
std::cout << "internal helper" << std::endl;
}
void publicFunction() {
helper(); // fine — same file
}// main.cpp
helper(); // ERROR — not visible hereBelongs to the class itself, not to any instance. Can be called via :: without creating an object. Has no this pointer — cannot access non-static member variables.
class Instrumentor {
private:
int m_ProfileCount; // belongs to an instance
public:
static Instrumentor& get() {
static Instrumentor* instance = new Instrumentor();
return *instance;
}
static void printLabel() {
std::cout << "Instrumentor" << std::endl;
// m_ProfileCount++; ERROR — no instance, no this pointer
}
};
Instrumentor::get(); // no object needed
Instrumentor::printLabel();Common uses: singleton get(), factory functions, utility functions that are logically related to the class but don't need instance data.
Shared across all instances — one copy exists regardless of how many objects are created. Must be defined outside the class.
class Instrumentor {
public:
static int instanceCount;
Instrumentor() { instanceCount++; }
};
int Instrumentor::instanceCount = 0; // definition outside the class
int main() {
Instrumentor a;
Instrumentor b;
std::cout << Instrumentor::instanceCount << std::endl; // 2
}| Usage | Effect |
|---|---|
| Local variable | Initialized once, persists for program lifetime, scope stays local |
| Free function | Visible only in current .cpp file |
| Member function | Belongs to class, callable without an instance, no this pointer |
| Member variable | Shared across all instances, one copy for the whole class |
| Specifier | Accessible From Class Itself | Accessible from Derived Classes | Accessible from Outside | Typical Use |
|---|---|---|---|---|
public |
Yes | Yes | Yes | Public API, interface to the class |
protected |
Yes | Yes | No | Allow derived classes to reuse/extend functionality |
private |
Yes | No | No | Strict encapsulation; internal details |
- Default access:
class→privatestruct→public
- Inheritance can also change visibility (public / protected / private inheritance).
Templates avoid code duplication.
They are resolved at compile time, which means the compiler generates a specific function/class for each type used.
template<typename T>
void print(T value){
std::cout << "Value: " << value << std::endl;
}
int main(){
std::vector<float> v = {20.4, 89.7, 66.0};
for(float vi: v){
print(vi);
}
std::tuple<bool, std::string> t = {false, "true"};
print(std::get<0>(t));
print(std::get<1>(t));
print("Oben");
print(29);
return 0;
}Output
Value: 20.4
Value: 89.7
Value: 66
Value: 0
Value: true
Value: Oben
Value: 29Operator overloading lets you define what +, *, ==, << etc. do for your own types.
Vector2 v3 = v1 + v2; // calls v1.operator+(v2)
Vector2 v3 = v1.operator+(v2); // equivalent, explicit formThe compiler translates operator syntax into function calls automatically.
return_type operator<symbol>(parameters)The operator is always a member function (or free function) named operator followed directly by the symbol:
operator+ // addition
operator* // multiplication
operator== // equality
operator<< // output stream
operator new // heap allocation#include <iostream>
struct Vector2{
float x, y;
Vector2 operator +(const Vector2& other){
return {x+other.x, y+other.y};
}
Vector2 operator *(const Vector2& other){
return {x*other.x, y*other.y};
}
bool operator ==(const Vector2& other){
return (x == other.x && y == other.y);
}
};
void* operator new(size_t size) { // size = bytes requested, not the value
std::cout << "Allocating " << size << " bytes\n";
void* ptr = malloc(size); // malloc: requests raw bytes from the heap
return ptr;
}
int main(){
Vector2 v1 = {1.0, 2.0};
Vector2 v2 = {3.0, 4.0};
Vector2 v3 = v1 + v2; // Vector2 v3 = v1.operator+(v2)
std::cout << "V3-x: " << v3.x << ", V3-y: " << v3.y << std::endl;
std::cout << "v1 == v2: " << (v1 == v2) << "\n" << std::endl;
int* pi = new int(76);
double* pd = new double(76.85);
return 0;
}V3-x: 4, V3-y: 6
v1 == v2: 0
Allocating 4 bytes
Allocating 8 bytesInside the operator, x and y refer to the left-hand side object (via implicit this), and other is the right-hand side:
Vector2 v3 = v1 + v2;
// ^^ ^^
// this other
// x other.xnew is a keyword that acts as an operator — not a plain function. In C++, operators are broader than just symbols like + or ==:
new // operator new — heap allocation
delete // operator delete — heap deallocation
sizeof // operator sizeof — size queryThe distinction:
- function — called explicitly by name:
malloc(4) - operator — compiler translates syntax into a call:
new int(42)→operator new(4)
new is an operator because you never call it by name — the compiler translates new int(42) into the call automatically.
operator new can also be overloaded — every heap allocation in the program goes through it:
void* operator new(size_t size) { // size = bytes requested, not the value
std::cout << "Allocating " << size << " bytes\n";
void* ptr = malloc(size); // malloc: requests raw bytes from the heap
return ptr;
}new works in two steps internally:
operator new(sizeof(T))— allocates raw bytes, this is what gets overridden- Constructor/value placement — happens after, outside of
operator new
int* pi = new int(76); // operator new(4) → then places 76
double* pd = new double(76.85); // operator new(8) → then places 76.85Output:
Allocating 4 bytes
Allocating 8 bytes
operator new only sees the size — never the value. The value 76 is placed into memory after allocation.
uint32_t is a fixed-width integer type from <cstdint>:
u → unsigned (no negatives)
int → integer
32 → exactly 32 bits guaranteed
_t → type naming convention
Used instead of int when exact bit width matters — on most platforms int happens to be 32 bits, but it's not guaranteed by the standard.
Prevents the compiler from silently converting one type into your class.
class Box {
public:
Box(int value) : value_(value) {}
};
void ship(Box b) {}
ship(42); // compiles! compiler secretly does Box(42) behind your backclass Box {
public:
explicit Box(int value) : value_(value) {}
};
ship(42); // ERROR — caught by compiler
ship(Box(42)); // OK — intent is clearMark every single-argument constructor
explicitunless you specifically want implicit conversions.
Implicit conversions are almost never what you want — they hide intent and create hard-to-spot bugs. Most modern C++ guidelines recommend explicit by default.
| Feature | std::tuple | struct |
|---|---|---|
| Naming | Anonymous (accessed by index) | Descriptive names (.name, .age). |
| Readability | Low. std::get<0>(data) is cryptic | High. data.name is clear. |
| Definition | None required. Use "on the fly." | Requires a struct { ... }; definition. |
| Maintenance | Fragile. Changing order breaks code. | Robust. Member order doesn't matter. |
| Best Use Case | Quick, internal helper functions. | Public APIs and shared data models. |
#include <iostream>
#include <string>
#include <tuple>
////////// TUPLE ////////////////
// Function returning tuple
std::tuple<std::string, int, float> tupleStudentData(){
return {"Oben", 29, 1.4};
}
int tuple_solution(){
// Modern C++17 way to unpack (Structured Bindings)
auto [name, age, gpa] = tupleStudentData();
// Older way (if you can't use C++17)
// std::string n = std::get<0>(getStudentData());
std::cout << "Name: " << name << ", Age: " << age << ", GPA: " << gpa << std::endl;
return 0;
}
////////// STRUCT ////////////////
struct StudentData
{
std::string name;
int age;
float gpa;
};
StudentData structStudentData(){
return {"Verena", 28, 1};
}
int struct_solution(){
StudentData s = structStudentData();
std::cout << "Name: " << s.name << ", Age: " << s.age << ", GPA: " << s.gpa << std::endl;
return 0;
}
int main(){
tuple_solution();
struct_solution();
return 0;
}An iterator is a pointer-like object that marks a position inside a container.
v.begin()→ points to the first elementv.end()→ points to one past the last element (half-open range[begin, end))
std::sort(v.begin(), v.end()); // sort entire vector
std::sort(v.begin() + 1, v.begin() + 4); // sort only index 1..3| Function | Use case | Stable? | Complexity |
|---|---|---|---|
std::sort |
General purpose | ❌ | O(n log n) |
std::stable_sort |
Preserve order of equal elements | ✅ | O(n log² n) |
std::partial_sort |
Only need top N elements | ❌ | O(n log k) |
std::is_sorted |
Check if already sorted | — | O(n) |
The comparator returns true if a should come before b.
std::vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end()); // ascending
std::sort(v.begin(), v.end(), std::greater<int>()); // descendingstd::vector<int> v = {-5, 3, -1, 4, -2};
std::sort(v.begin(), v.end(), [](int a, int b) {
return std::abs(a) < std::abs(b); // sort by absolute value
});
// → -1, -2, 3, 4, -5bool byAbsoluteValue(int a, int b) {
return std::abs(a) < std::abs(b);
}
std::vector<int> v = {-5, 3, -1, 4, -2};
std::sort(v.begin(), v.end(), byAbsoluteValue);
// → -1, -2, 3, 4, -5Use a plain function over a lambda when the comparator logic is complex or reused across multiple sort calls.
Pairs sort by .first first, then .second as tiebreaker — built-in, no comparator needed.
std::vector<std::pair<int, std::string>> scores = {
{90, "Alice"}, {85, "Charlie"}, {90, "Bob"}
};
std::sort(scores.begin(), scores.end());
// → {85,"Charlie"}, {90,"Alice"}, {90,"Bob"}
// ↑ by score, then alphabetically for tiesTo sort by .second instead, use a lambda:
std::sort(scores.begin(), scores.end(), [](const auto& a, const auto& b) {
return a.second < b.second; // sort by name alphabetically
});
// → {90,"Alice"}, {85,"Charlie"}, {90,"Bob"}Maps are always sorted by key internally — you can't sort them directly. To sort by value, dump into a vector first and sort that.
Since map entries are already pairs internally, you can use C++17 CTAD and let the vector type be deduced automatically:
std::map<std::string, int> wordCount = {
{"apple", 5}, {"banana", 2}, {"cherry", 8}
};
std::vector v(wordCount.begin(), wordCount.end()); // type deduced via CTAD
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
return a.second > b.second; // sort by value descending
});
// → cherry:8, apple:5, banana:2Preserves the original relative order of equal elements. Use when order within a group matters.
struct Task { std::string name; int priority; };
std::vector<Task> tasks = {
{"Write tests", 2},
{"Fix bug", 1},
{"Code review", 2},
{"Deploy", 1}
};
std::stable_sort(tasks.begin(), tasks.end(), [](const Task& a, const Task& b) {
return a.priority < b.priority;
});
// → Fix bug(1), Deploy(1), Write tests(2), Code review(2)
// ↑ original order preserved within each priority groupSorts only the first k elements. More efficient than full sort when you only need the top N.
std::vector<int> v = {5, 2, 8, 1, 9, 3, 7};
std::partial_sort(v.begin(), v.begin() + 3, v.end());
// → 1, 2, 3, [rest in unspecified order]Checks if a range is already sorted. Returns bool.
std::vector<int> v = {1, 2, 3, 4, 5};
if (std::is_sorted(v.begin(), v.end())) {
std::cout << "Already sorted!\n";
}C++ provides four named cast operators. Unlike C-style casts, they make your intent explicit and are easier to search in code.
| Cast | Purpose | Runtime Check? | Safety |
|---|---|---|---|
static_cast |
Numeric conversions, related types | No | Safe (mostly) |
dynamic_cast |
Safe polymorphic downcast | Yes | Safe |
const_cast |
Add or remove const |
No | Situational |
reinterpret_cast |
Raw memory / bit reinterpretation | No | Dangerous |
Converts between related types at compile time. No runtime overhead. The most commonly used cast in everyday C++.
float pi = 3.14f;
int x = static_cast<int>(pi); // 3 (truncates decimal)
int i = 42;
double d = static_cast<double>(i); // 42.0Use for: numeric type conversions, upcasting (Dog* → Animal*), converting void* to a typed pointer.
Avoid when: you don't know the actual runtime type — use dynamic_cast for that.
Safely checks the real runtime type of a pointer or reference. Requires at least one virtual function in the class (enables RTTI).
class Animal {
public:
virtual ~Animal() {} // virtual destructor enables RTTI
virtual void speak() {}
};
class Dog : public Animal {
public:
void speak() override { std::cout << "woof\n"; }
void fetch() { std::cout << "fetched!\n"; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "meow\n"; }
};
Animal* a1 = new Dog();
Animal* a2 = new Cat();
Dog* d1 = dynamic_cast<Dog*>(a1); // succeeds — a1 is really a Dog
if (d1) d1->fetch();
Dog* d2 = dynamic_cast<Dog*>(a2); // fails — a2 is a Cat → returns nullptr
if (!d2) std::cout << "nullptr\n";// Pointer — returns nullptr on failure
Dog* d = dynamic_cast<Dog*>(animalPtr);
// Reference — throws std::bad_cast on failure
Dog& d = dynamic_cast<Dog&>(animalRef);Use pointer cast when failure is expected and normal. Use reference cast when you're certain of the type and want a hard error if wrong.
| Direction | Example | static_cast? |
dynamic_cast? |
|---|---|---|---|
| Upcast (Derived → Base) | Dog* → Animal* |
Yes (implicit too) | Not needed |
| Downcast (Base → Derived) | Animal* → Dog* |
Only if certain | Yes — safe check |
| Cross-cast (sibling classes) | Animal* → Flyable* |
Cannot | Yes |
⚠️ dynamic_casthas a runtime cost. Cast once, store the result — avoid calling it in tight loops.
⚠️ Needing manydynamic_castchecks often signals a missingvirtualfunction in the base class.
The only cast that can add or remove const. Most commonly used to call legacy APIs that take non-const parameters but don't actually modify the data.
// Legacy function you cannot modify
void legacyProcess(std::string& str) {
std::cout << str << std::endl; // read-only, but takes non-const ref
}
const std::string message = "Hello, World!";
// legacyProcess(message); // ERROR: cannot bind non-const ref to const
legacyProcess(const_cast<std::string&>(message)); // OKconst int x = 10;
int* p = const_cast<int*>(&x);
*p = 99; // undefined behavior — x may be in read-only memory| Scenario | Safe? |
|---|---|
| Object was originally non-const, passed as const | ✅ Safe to remove const |
| Object was originally const from the start | ❌ Writing to it is UB |
✅ In modern C++, if you find yourself using
const_castoften, the better fix is updating the function signatures.
Tells the compiler to treat the raw bits of a value as a completely different type. No conversion happens — just a different lens on the same memory. Mostly used in low-level systems, embedded, and hardware code.
int x = 42;
char* bytes = reinterpret_cast<char*>(&x);
for (size_t i = 0; i < sizeof(int); i++) {
std::cout << static_cast<int>(bytes[i]) << " ";
}
// Output on little-endian (x86): 42 0 0 0struct Entity { int x, y; };
Entity e;
e.x = 3;
e.y = 5;
// treat struct memory as a plain int array
int* position = reinterpret_cast<int*>(&e);
std::cout << position[0] << "\n"; // 3 → reads e.x
std::cout << position[1] << "\n"; // 5 → reads e.y
⚠️ reinterpret_castviolates strict aliasing rules in many cases. Usestd::memcpyorstd::bit_cast(C++20) for safe type punning between unrelated types.
C-style casts still work but are a blunt tool. The compiler silently tries static_cast → const_cast → reinterpret_cast in sequence. You lose clarity about your intent.
// C-style — what kind of cast is this?
int* position = (int*)(&e);
// C++ style — intent is explicit
int* position = reinterpret_cast<int*>(&e);Rule of thumb: reach for static_cast first. Only escalate to dynamic_cast, const_cast, or reinterpret_cast when you have a specific, justified reason.
std::optional<T> represents a value that may or may not be present. No heap allocation, no null pointers, no sentinel values. Available since C++17.
std::optional<int> a = 42; // has a value
std::optional<int> b = std::nullopt; // empty — no value
std::optional<int> c; // also empty (default)A named constant meaning "no value" — the optional equivalent of nullptr.
std::optional<int> find(const std::vector<int>& v, int target) {
for (int i = 0; i < (int)v.size(); i++) {
if (v[i] == target) return i; // implicitly wraps value
}
return std::nullopt; // explicitly empty
}| Method | Behavior when empty |
|---|---|
has_value() / operator bool |
Returns false — safe to check |
value() |
Throws std::bad_optional_access |
*opt / opt->field |
Undefined behavior — dangerous |
value_or(default) |
Returns the default — no check needed |
std::optional<int> result = find(v, 20);
// Safe check then access
if (result.has_value()) {
std::cout << result.value() << "\n";
}
// Shorthand — optional is truthy when it has a value
if (result) {
std::cout << *result << "\n";
}
// Fallback — no if-check needed
std::cout << result.value_or(-1) << "\n";Returns the value if present, otherwise the fallback. Useful when you want a default and don't need to distinguish absence from presence.
std::optional<int> get_age(bool logged_in) {
if (!logged_in) return std::nullopt;
return 25;
}
int age = get_age(false); // wrong — optional not implicitly convertible to int
int age = get_age(false).value_or(0); // correct — prints 0
int age = get_age(true).value_or(0); // prints 25A common pattern: return std::nullopt if the file can't be opened, otherwise return the content.
#include <fstream>
#include <optional>
#include <sstream>
#include <string>
std::optional<std::string> read_file(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) return std::nullopt;
std::ostringstream ss;
ss << file.rdbuf(); // reads entire file in one shot
return ss.str();
}
int main() {
std::optional<std::string> content = read_file("input.txt");
if (!content) {
std::cerr << "File not found\n";
return 1;
}
std::cout << *content << "\n";
// Or with fallback — skips the if-check entirely
std::cout << content.value_or("(unavailable)") << "\n";
}std::variant is a type-safe union introduced in C++17. It holds exactly one value at a time from a fixed set of types. Unlike a raw union, it always knows which type is currently active.
#include <variant>
#include <string>
std::variant<int, double, std::string> var;Simply assign — the variant tracks which type is now active:
var = 42; // holds int
var = 3.14; // holds double
var = "Oben"; // holds std::stringvar = "Oben";
std::string name = std::get<std::string>(var); // OK
std::string bad = std::get<int>(var); // throws std::bad_variant_accessReturns a pointer to the value, or nullptr if the type doesn't match:
var = "Oben";
std::string* isString = std::get_if<std::string>(&var);
if (isString) {
std::cout << *isString << std::endl; // "Oben"
}
double* isDouble = std::get_if<double>(&var);
if (isDouble) {
std::cout << *isDouble << std::endl;
} else {
std::cout << "var is not a double" << std::endl; // prints this
}| Method | Wrong type behaviour | Use when |
|---|---|---|
std::get<T> |
throws std::bad_variant_access |
you are certain of the type |
std::get_if<T> |
returns nullptr |
you want to check safely |
std::variant<int, double, std::string> var;
var = 42; // index() == 0
var = 3.14; // index() == 1
var = "Oben"; // index() == 2
std::cout << var.index() << std::endl; // 2var = "Oben";
if (std::holds_alternative<std::string>(var)) {
std::cout << "It's a string" << std::endl;
}std::variant reserves space for the largest member plus a small index field (padded for alignment):
std::variant<int, double, std::string> var;
sizeof(int) // 4
sizeof(double) // 8
sizeof(std::string) // 32
sizeof(var) // 40 (32 + index overhead, padded to alignment)Compare with raw union — same storage, but no index tracking:
union U { int i; double d; std::string s; };
sizeof(U) // 32 — no overhead, but unsafeMultiple variables sharing the same memory. Size = largest member. Only one is valid at a time.
union Data {
int i;
float f;
char c;
}; // 4 bytes total, not 9
Data d;
d.i = 42; // valid
d.f = 3.14f; // now only d.f is valid — d.i is garbageThe union doesn't track which member is active — you do. Pair with an enum if you need safety, or just use std::variant (C++17).
Still useful for: embedded systems, protocol parsing, type punning (inspecting raw bytes).
| Feature | std::variant |
union |
|---|---|---|
| Tracks active type | yes | no |
| Type-safe access | yes | no |
| Throws on bad access | yes (std::get) |
undefined behaviour |
| Overhead | small (index field) | none |
| C++ version | C++17 | all |
A default-constructed std::variant holds the first type, value-initialized:
std::variant<int, double, std::string> var; // holds int = 0
std::cout << var.index(); // 0
std::cout << std::get<int>(var); // 0Use std::monostate as the first type if you want an explicit "empty" state:
std::variant<std::monostate, int, std::string> var; // empty by defaultstd::variant<T1, T2, T3>
├── holds exactly one type at a time
├── .index() → which type is active (0-based)
├── std::holds_alternative<T> → bool check
├── std::get<T> → direct access, throws if wrong
├── std::get_if<T> → pointer access, nullptr if wrong
└── std::visit → dispatch over all possible types
std::any is a C++17 type that can hold a single value of any type. Unlike std::variant, you don't declare the possible types upfront — std::any accepts anything. The tradeoff is that type information is erased at compile time and must be explicitly recovered at runtime.
#include <any>
std::any a = 42;
a = 3.14;
a = std::string("Oben"); // can reassign to completely different typestd::any a; // empty
a = 42; // holds int
a = 3.14; // now holds double
a = std::string("Oben"); // now holds std::stringEach assignment replaces the previous value and type entirely.
Since the type is erased, you must explicitly tell it what type to recover:
std::any a = std::string("Oben");
std::string s = std::any_cast<std::string>(a); // OK
int x = std::any_cast<int>(a); // throws std::bad_any_castWith a normal variable the compiler knows the type at compile time. With std::any the type is hidden — the cast is how you bring it back:
int x = 42 → no cast needed (type fully known at compile time)
std::any a = 42 → any_cast<int> needed (type erased, recovered at runtime)
Pass a pointer to get a nullptr instead of an exception on wrong type:
std::any a = std::string("Oben");
std::string* s = std::any_cast<std::string>(&a); // OK, valid pointer
int* x = std::any_cast<int>(&a); // nullptr, no throw| Form | Wrong type behaviour |
|---|---|
std::any_cast<T>(a) |
throws std::bad_any_cast |
std::any_cast<T>(&a) |
returns nullptr |
std::any a = 42;
a.has_value(); // true — something is stored
a.type() == typeid(int); // true — check the active type
a.reset(); // clear to empty
a.has_value(); // falsestd::any a = std::string("Oben");
try {
int x = std::any_cast<int>(a); // wrong type
} catch (const std::bad_any_cast& e) {
std::cout << "bad cast: " << e.what() << std::endl;
}std::any typically heap-allocates for large types. Small types (like int) may be stored inline via small buffer optimization, depending on the implementation.
This makes std::any heavier than std::variant, which always stores on the stack.
| Types known upfront | Type safe | Runtime cost | Use when | |
|---|---|---|---|---|
void* |
no | no | none | never in modern C++ |
std::variant<...> |
yes | yes | minimal (stack) | finite known set of types |
std::any |
no | yes | higher (heap) | truly unknown type |
Good fit:
- Plugin systems where types are unknown at compile time
- Heterogeneous containers where element types vary arbitrarily
- Passing arbitrary user data through a generic interface
Prefer std::variant instead when the set of possible types is known — it's faster, safer, and more explicit.
std::any
├── holds any single value, any type
├── .has_value() → bool, is something stored
├── .type() → std::type_info, compare with typeid(T)
├── .reset() → clear to empty
├── std::any_cast<T>(a) → value, throws if wrong type
└── std::any_cast<T>(&a) → pointer, nullptr if wrong type
An enum (short for enumeration) is a user-defined type that assigns names to a set of integer constants.
It makes code more readable and easier to maintain.
#include <iostream>
enum Direction {
North, // 0
East, // 1
South, // 2
West // 3
};
int main() {
Direction dir = South;
if (dir == South)
std::cout << "Going South!" << std::endl;
std::cout << "Direction value: " << dir << std::endl;
}Going South
Direction Value: 2#include <iostream>
enum class Season {
Spring,
Summer,
Autumn,
Winter
};
std::string seasonMessage(Season s) {
switch (s) {
case Season::Spring:
return "Flowers blooming";
case Season::Summer:
return "Hot and sunny";
case Season::Autumn:
return "Leaves falling";
case Season::Winter:
return "Cold and snowy";
}
return "Unknown";
}
int main() {
Season current = Season::Autumn;
std::cout << seasonMessage(current) << std::endl;
return 0;
}By default, your program runs on a single thread — one sequence of instructions executed one at a time. A thread is an independent execution path. With multiple threads, you can do several things concurrently inside the same process.
void task() {
std::cout << "Running in a thread\n";
}
int main() {
std::thread t(task); // starts task() on a new thread
t.join(); // wait for it to finish
return 0;
}Pass arguments to the thread function after the function name:
void greet(std::string name) {
std::cout << name << std::endl;
}
int main() {
std::thread t(greet, "Oben"); // pass arguments after the function
t.join();
return 0;
}You can also use a lambda:
std::thread t([]() {
std::cout << "Hello from lambda\n";
});
t.join();A data race happens when two threads read/write the same variable at the same time without coordination.
int counter = 0; // plain int — no protection
void increment() {
for (int i = 0; i < 100000; i++)
counter++;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter << "\n"; // should be 200000, never is
}Result: 143721 // run 1
Result: 167342 // run 2
Result: 158901 // run 3
counter++ looks like one operation but the CPU actually does three separate steps:
1. read counter from memory (gets 5)
2. add 1 (gets 6)
3. write 6 back to memory
When two threads do these three steps simultaneously they constantly overlap and overwrite each other:
thread A: read counter → 5
thread B: read counter → 5 ← reads same value as A, before A wrote back
thread A: write counter → 6
thread B: write counter → 6 ← overwrites A's result, one increment is lost
Both threads incremented, but counter only went from 5 to 6 instead of 5 to 7. This is called a lost update.
std::atomic does not prevent multiple threads from accessing a variable at the same time. Multiple threads can still all read and write concurrently.
What it does guarantee is that no thread ever sees a half-written value. To understand why that matters, consider that a double is 8 bytes. Without atomic, the CPU may write those 8 bytes in two separate steps:
thread A writes 3.14 to a plain double:
step 1 → writes first 4 bytes [3.14 first half | 0.0 second half] ← broken state
step 2 → writes second 4 bytes [3.14 first half | 3.14 second half] ← complete
if thread B reads between step 1 and step 2, it gets garbage
std::atomic<double> makes the entire 8-byte write happen as one single step. Thread B will either see the old value or the new value — never something in between. This is what indivisible means: the operation either happened completely or not at all.
std::atomic<int> a_counter = 0;
void a_increment() {
for (int i = 0; i < 100000; i++)
a_counter++; // safe — ++ is overloaded for atomics
}
int main() {
std::thread t1(a_increment);
std::thread t2(a_increment);
t1.join();
t2.join();
std::cout << a_counter << "\n"; // always 200000
}If two variables must change together and be seen as a consistent pair, atomic cannot help — another thread may read the first store before the second one happens:
std::atomic<bool> stopValue = true;
std::atomic<bool> warnValue = true;
// thread A writes:
stopValue.store(false); // thread B could read here...
warnValue.store(false); // ...and see stopValue=false, warnValue=true — inconsistentFor this case, use a mutex to protect both stores as a single operation.
A std::mutex is a lock. Only one thread can hold it at a time. The other waits. This is what actually enforces "only one thread at a time" — unlike atomic which only protects a single operation.
int m_counter = 0;
std::mutex mtx;
void m_increment() {
for (int i = 0; i < 100000; i++) {
mtx.lock();
m_counter++;
mtx.unlock();
}
}
int main() {
std::thread t1(m_increment);
std::thread t2(m_increment);
t1.join();
t2.join();
std::cout << m_counter << "\n"; // always 200000
}std::lock_guard is a wrapper around a mutex. It locks on construction and unlocks automatically when it goes out of scope — even if an exception is thrown. This is the preferred way over calling lock() and unlock() manually.
int lg_counter = 0;
std::mutex lg_mtx;
void lg_increment() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> lock(lg_mtx);
lg_counter++;
} // lock released automatically here
}lock()/unlock() |
lock_guard |
|
|---|---|---|
| Unlocks automatically | No | Yes |
| Safe if exception thrown | No — unlock never called | Yes |
| Verbose | Yes | No |
After creating a thread you must call either join() or detach() before the thread object is destroyed — otherwise the program crashes.
void slow_task(std::string label) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::cout << "[" << label << "] finished\n";
}
int main() {
// -- join --
std::thread t1(slow_task, "join thread");
std::cout << "[main] waiting...\n";
t1.join(); // main blocks here until t1 finishes
std::cout << "[main] continues\n";
// -- detach --
std::thread t2(slow_task, "detach thread");
t2.detach(); // main does not wait
std::cout << "[main] already here\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}[main] waiting...
[join thread] finished ← main was blocked until this printed
[main] continues
[main] already here ← main rushed past immediately
[detach thread] finished
join() |
detach() |
|
|---|---|---|
| What it does | Waits for the thread to finish | Lets the thread run independently |
| When to use | You need the result before moving on | Fire-and-forget background work |
| Risk | Blocks the caller | Thread may outlive the data it uses |
std::mutex |
std::atomic |
|
|---|---|---|
| What it prevents | Any other thread entering the protected block | Torn/partial reads or writes on one variable |
| Enforces "one thread at a time" | Yes | No |
| Works across multiple variables | Yes | No — one variable only |
| Performance | Slower (OS-level blocking) | Faster (single CPU instruction) |
| Use when | Protecting a block of logic or multiple variables | Single variable with simple load/store/increment |
Threads require linking against the system thread library. Use CMake's built-in Threads package:
cmake_minimum_required(VERSION 3.10)
project(my_project)
set(CMAKE_CXX_STANDARD 17)
find_package(Threads REQUIRED)
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE Threads::Threads)Without target_link_libraries(... Threads::Threads) you will get a linker error on Linux.
| Concept | Purpose |
|---|---|
std::thread |
Create a new thread |
join() |
Wait for thread to finish |
detach() |
Let thread run independently |
std::mutex |
Enforce one thread at a time over a block or multiple variables |
std::lock_guard |
RAII wrapper — auto-unlocks the mutex |
std::atomic<T> |
Safe load/store on a single variable without a mutex |
#include <iostream>
#include <thread>
#include <chrono>
#include <future>
#include <vector>
#include <mutex>
std::mutex sensor_mtx;
double reading = 5.0;
std::vector<double> sensor_readings;
void load_mesh(){
std::cout << "load_mesh: started loading" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "load_mesh: loading completed" << std::endl;
}
int move_robot(){
std::cout << "move_robot: started moving" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "move_robot: moving completed" << std::endl;
return 0;
}
double read_sensor(){
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::lock_guard<std::mutex> lock(sensor_mtx);
reading = reading * 1.5;
sensor_readings.push_back(reading);
return reading;
}
template<typename T>
void measure_time(T func, int loop_num){
std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
for(int i=0; i<loop_num; i++){
func();
}
std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
std::chrono::milliseconds elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start);
std::cout << "Elapsed Time: " << elapsed.count() << " ms" << std::endl;
}
template<typename T>
void measure_time_with_asyn_func(T func, int loop_num){
std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
// invoke_result_t<T>: give me the return type of called function
// std::future<XX>: holds the pending result with XX type
std::vector<std::future<std::invoke_result_t<T>>> futures;
for(int i=0; i<loop_num; i++){
futures.push_back(std::async(std::launch::async, func));
}
for (auto& f : futures) {
f.get(); // wait for each task to finish
}
std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
std::chrono::milliseconds elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start);
std::cout << "Async Function Elapsed Time: " << elapsed.count() << " ms" << std::endl;
}
int main(){
measure_time(load_mesh, 4);
std::cout << "----------------------" << std::endl;
measure_time_with_asyn_func(load_mesh, 4);
std::cout << "----------------------" << std::endl;
measure_time(move_robot, 4);
std::cout << "----------------------" << std::endl;
measure_time_with_asyn_func(move_robot, 4);
std::cout << "----------------------" << std::endl;
measure_time(read_sensor, 5);
std::cout << "All sensor readings:" << std::endl;
for (double r : sensor_readings) {
std::cout << r << " ";
}
std::cout << "\n----------------------" << std::endl;
sensor_readings = {};
reading = 5;
measure_time_with_asyn_func(read_sensor, 5);
std::cout << "All sensor readings:" << std::endl;
for (double r : sensor_readings) {
std::cout << r << " ";
}
std::cout << "\n";
return 0;
}load_mesh: started loading
load_mesh: loading completed
load_mesh: started loading
load_mesh: loading completed
load_mesh: started loading
load_mesh: loading completed
load_mesh: started loading
load_mesh: loading completed
Elapsed Time: 8001 ms
----------------------
load_mesh: started loading
load_mesh: started loading
load_mesh: started loading
load_mesh: started loading
load_mesh: loading completed
load_mesh: loading completed
load_mesh: loading completed
load_mesh: loading completed
Async Function Elapsed Time: 2000 ms
----------------------
move_robot: started moving
move_robot: moving completed
move_robot: started moving
move_robot: moving completed
move_robot: started moving
move_robot: moving completed
move_robot: started moving
move_robot: moving completed
Elapsed Time: 12002 ms
----------------------
move_robot: started moving
move_robot: started moving
move_robot: started moving
move_robot: started moving
move_robot: moving completed
move_robot: moving completed
move_robot: moving completed
move_robot: moving completed
Async Function Elapsed Time: 3000 ms
----------------------
Elapsed Time: 2500 ms
All sensor readings:
7.5 11.25 16.875 25.3125 37.9688
----------------------
Async Function Elapsed Time: 500 ms
All sensor readings:
7.5 11.25 16.875 25.3125 37.9688 When tasks are independent, running them one after another wastes time:
void load_mesh() { sleep(2s); } // 2 seconds
void move_robot() { sleep(3s); } // 3 seconds
// sequential — 4 calls = 8 seconds total
for (int i = 0; i < 4; i++) {
load_mesh();
}std::async launches a function on a new thread and returns immediately:
#include <future>
std::future<void> f = std::async(std::launch::async, load_mesh);
// load_mesh is now running in background — execution continues herestd::launch::async forces a new thread immediately. Without it, the runtime may defer execution.
std::future<T> holds one value that isn't ready yet. It will be produced by the async task.
std::future<int> // will hold one int when ready
std::future<double> // will hold one double when ready
std::future<void> // holds nothing, but signals task completion.get() blocks until the result is available:
std::future<int> f = std::async(std::launch::async, move_robot);
int result = f.get(); // blocks here until move_robot finishesCritical rule: launch ALL tasks first, then call .get().
// CORRECT — all tasks launched before any .get()
std::vector<std::future<void>> futures;
for (int i = 0; i < 4; i++) {
futures.push_back(std::async(std::launch::async, load_mesh));
}
for (auto& f : futures) {
f.get(); // waits for each, but they're already all running
}
// WRONG — launches one, waits, launches next — sequential again
for (int i = 0; i < 4; i++) {
std::future<void> f = std::async(std::launch::async, load_mesh);
f.get(); // blocks immediately, no concurrency
}Also critical: never discard the future.
// WRONG — future destroyed immediately, destructor blocks until done
for (int i = 0; i < 4; i++) {
std::async(std::launch::async, load_mesh); // future discarded here
}When a std::future is destroyed without calling .get(), its destructor blocks until the task finishes — making it sequential.
sequential load_mesh x4:
t=0s [task1]
t=2s [task2]
t=4s [task3]
t=6s [task4]
t=8s done → 8000 ms
async load_mesh x4:
t=0s [task1][task2][task3][task4] all running
t=2s done → 2000 ms
template<typename T>
void measure_time_with_asyn_func(T func, int loop_num) {
std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
// invoke_result_t<T>: deduces return type of T at compile time
// std::future<XX>: holds one pending result of type XX
std::vector<std::future<std::invoke_result_t<T>>> futures;
for (int i = 0; i < loop_num; i++) {
futures.push_back(std::async(std::launch::async, func));
}
for (auto& f : futures) {
f.get(); // wait for all tasks to finish
}
std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
std::chrono::milliseconds elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Elapsed Time: " << elapsed.count() << " ms" << std::endl;
}std::invoke_result_t<T> deduces the return type of T at compile time:
load_mesh → void → std::vector<std::future<void>>
move_robot → int → std::vector<std::future<int>>
read_sensor→ double→ std::vector<std::future<double>>
When async tasks share a resource, protect it with std::mutex:
std::mutex sensor_mtx;
double reading = 5.0;
std::vector<double> sensor_readings;
double read_sensor() {
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // heavy work — runs concurrently
std::lock_guard<std::mutex> lock(sensor_mtx); // only the write is protected
reading = reading * 1.5;
sensor_readings.push_back(reading);
return reading;
}The mutex only wraps the write to shared state — not the heavy work. This way tasks run concurrently for the slow part and only serialize for the brief write.
| Situation | async faster? | Reason |
|---|---|---|
Long independent tasks (load_mesh, move_robot) |
Yes | Concurrency benefit outweighs thread overhead |
| Short tasks with mutex on entire body | No | Serialized by mutex + thread overhead added |
| Short tasks, no shared state | Maybe | Thread creation overhead may dominate |
| Shared resource, mutex on write only | Yes | Heavy work concurrent, only write serialized |
load_mesh x4 sequential → 8000 ms
load_mesh x4 async → 2000 ms (4x faster)
move_robot x4 sequential → 12000 ms
move_robot x4 async → 3000 ms (4x faster)
read_sensor x5 sequential → ~2500 ms
read_sensor x5 async → ~500 ms (mutex only on write, heavy work concurrent)
All timing utilities live in the <chrono> header. The library is built around three concepts: clocks, time_points, and durations.
A clock is the source of time. Each clock has a now() static method that returns the current time_point.
| Clock | Monotonic | Use for |
|---|---|---|
steady_clock |
Yes | Measuring elapsed time, timeouts, sleep |
system_clock |
No | Timestamps, logging, real wall time |
high_resolution_clock |
Implementation-defined | Finest tick — often alias of steady_clock |
steady_clock is guaranteed to never go backwards. system_clock tracks real wall time and can be adjusted by the OS (NTP sync, DST, manual changes), so it is not suitable for measuring elapsed time.
A time_point represents a specific moment in time relative to a clock's epoch. Obtained via now():
std::chrono::time_point<std::chrono::steady_clock> t = std::chrono::steady_clock::now();A duration represents a span of time. Subtracting two time_points yields a duration:
std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
// ... work ...
std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
std::chrono::duration<long, std::nano> elapsed = end - start;Use duration_cast to convert to a specific unit, then .count() to extract the raw value:
std::chrono::milliseconds ms = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
std::chrono::microseconds us = std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
std::cout << ms.count() << " ms\n";
std::cout << us.count() << " us\n";std::chrono::nanoseconds
std::chrono::microseconds
std::chrono::milliseconds
std::chrono::seconds
std::chrono::minutes
std::chrono::hours#include <iostream>
#include <chrono>
#include <thread>
int main() {
std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
std::chrono::milliseconds elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Elapsed: " << elapsed.count() << " ms\n"; // 200 ms
}#include <thread>
#include <chrono>
// sleep for a duration
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// sleep until a specific time_point
std::chrono::time_point<std::chrono::steady_clock> wake = std::chrono::steady_clock::now() + std::chrono::seconds(5);
std::this_thread::sleep_until(wake);system_clock is convertible to std::time_t, which allows formatting with standard C functions.
#include <iostream>
#include <chrono>
#include <ctime>
int main() {
std::chrono::time_point<std::chrono::system_clock> now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::cout << std::ctime(&t); // Wed Apr 1 14:32:10 2026
}#include <chrono>
#include <ctime>
void print_current_time(){
std::chrono::time_point<std::chrono::system_clock> system_now = std::chrono::system_clock::now(); // time point
time_t system_now_t = std::chrono::system_clock::to_time_t(system_now); // integer value of time point in seconds
std::tm* local = std::localtime(&system_now_t); // breaks the total second count into calendar fields (hour, min, day, month, year...)
char buffer[64];
std::strftime(buffer, sizeof(buffer), "%H:%M - %d/%m/%Y", local); // formatting the local time
std::cout << "Current Time: " << buffer << std::endl;
}struct tm {
int tm_sec; // seconds [0, 60]
int tm_min; // minutes [0, 59]
int tm_hour; // hours [0, 23]
int tm_mday; // day of month [1, 31]
int tm_mon; // month [0, 11] ← 0 = January
int tm_year; // years since 1900
int tm_wday; // day of week [0, 6] ← 0 = Sunday
int tm_yday; // day of year [0, 365]
int tm_isdst; // daylight saving time flag
};
| Concept | Purpose |
|---|---|
steady_clock |
Monotonic clock — measuring elapsed time |
system_clock |
Real wall clock — timestamps and logging |
time_point |
A moment in time from a clock's perspective |
duration |
A span of time between two time_points |
duration_cast |
Convert duration to a specific unit |
.count() |
Extract the raw numeric value from a duration |
sleep_for |
Pause the current thread for a duration |
sleep_until |
Pause the current thread until a time_point |
to_time_t |
Convert system_clock time_point to std::time_t |
std::put_time |
Format time using strftime-style tokens |
#include <iostream>
#include <stdexcept>
int divide(int a, int b){
if (b == 0){
throw std::runtime_error("Division by zero");
}
return a / b;
}
int parse_and_divide(const std::string& a, const std::string& b){
if (!std::isdigit(a[0]) || !std::isdigit(b[0])){
throw std::invalid_argument("Arguments must be numeric");
}
return divide(std::stoi(a), std::stoi(b));
}
int main(){
try{
int result = divide(10, 0);
// int result = parse_and_divide("10", "abc");
std::cout << result << std::endl;
} catch (const std::runtime_error &e){
std::cout << "Caught: " << e.what() << std::endl;
} catch (const std::invalid_argument &e){
std::cout << "Caught: " << e.what() << std::endl;
}
return 0;
}| Exception | Inherits From | When to Use | Auto-thrown? |
|---|---|---|---|
std::runtime_error |
std::exception |
General runtime failures | No — throw manually |
std::invalid_argument |
std::logic_error |
Bad input to a function | No — throw manually |
std::out_of_range |
std::logic_error |
Index or value out of valid range | By std::vector::at(), std::string::at() |
std::overflow_error |
std::runtime_error |
Arithmetic overflow | No — throw manually |
std::logic_error |
std::exception |
Violated logical precondition | No — throw manually |
std::bad_alloc |
std::exception |
Memory allocation failure | Yes — by new |
std::bad_cast |
std::exception |
Failed dynamic_cast to reference |
Yes — by dynamic_cast |
Exceptions don't throw themselves.
std::invalid_argument, std::runtime_error, etc. are just classes. They only fire when you explicitly throw them. The names are conventions to communicate intent.
Catch order matters.
Derived types must be caught before base types. Since most standard exceptions inherit from std::exception, catching std::exception first would swallow everything.
// Wrong — std::exception catches everything before the specific handlers
catch (const std::exception &e) { ... }
catch (const std::runtime_error &e) { ... } // never reached
// Correct — specific first, general last
catch (const std::runtime_error &e) { ... }
catch (const std::exception &e) { ... }Catch-all.
catch (...) catches any thrown type, including non-standard ones. Use as a last resort.
No finally in C++.
Use RAII instead — destructors run automatically when objects go out of scope, even during stack unwinding from an exception.
A singleton ensures only one instance of a class ever exists, accessible globally without passing an object around.
class Database {
private:
std::string m_Name;
Database(const std::string& name) : m_Name(name) {} // private — blocks direct construction
public:
static Database& get() {
static Database* instance = new Database("MyDB"); // created once
return *instance;
}
void query(const std::string& sql) {
std::cout << "[" << m_Name << "] Running: " << sql << std::endl;
}
};
int main() {
Database::get().query("SELECT * FROM users");
Database::get().query("SELECT * FROM orders");
}Private constructor — prevents creating an object directly. Forces all access through get().
Database db("MyDB"); // ERROR — constructor is private
Database::get().query(...); // only allowed waystatic Database* instance — local static variable, initialized once on the first call to get(). Every subsequent call skips the line and reuses the same pointer.
static Database& get() — static member function, callable without an existing object via ::. Breaks the chicken-and-egg problem: you need get() to get the object, but you can't call a non-static function without one.
// non-static would require an object first:
Database db; // can't — constructor is private
db.get(); // can't reach here
// static breaks the cycle:
Database::get(); // no object neededreturn *instance — dereferences the pointer to return a reference, so callers use . instead of ->.
static Database* instance = new Database("MyDB"); // intentional leakIf instance were a value instead of a pointer, its destructor would run at program exit. At that point other static objects may already be destroyed, causing undefined behavior. The pointer is intentionally leaked — the OS reclaims memory when the process exits anyway.
// singleton — no passing needed
void function1() { Database::get().query("SELECT * FROM users"); }
void function2() { Database::get().query("SELECT * FROM orders"); }
// passing by reference — every function needs an extra parameter
void function1(Database& db) { db.query("SELECT * FROM users"); }
void function2(Database& db) { db.query("SELECT * FROM orders"); }| Singleton | Passing by reference | |
|---|---|---|
| Convenience | No parameters needed | Extra parameter everywhere |
| Explicitness | Dependency hidden | Dependency visible |
| Testability | Hard to swap out | Easy to swap out |
:: means "look inside this scope for that name." It works identically for namespaces and classes.
std::string // look inside std namespace for string
Database::get() // look inside Database class for get()Use :: for anything that belongs to the class itself — static functions, static variables, nested types. For anything that belongs to an instance, use . or -> instead.
Database::get() // belongs to the class — ::
Database::instanceCount // static variable — ::
Database& db = Database::get();
db.query("SELECT..."); // belongs to the instance — .The private constructor in a singleton has nothing to do with ::. Any class with a public constructor works the same way:
class Dog {
public:
Dog(const std::string& name) {}
static void bark() { std::cout << "Woof!" << std::endl; }
};
Dog::bark(); // :: on a class with public constructor — fine
Dog dog("Rex"); // instance creation still works normallySingletons are a good fit when there is genuinely one shared resource for the whole program — a logger, a profiler, a configuration manager, a database connection. Avoid using them just for convenience when passing an object would be clearer.
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
enum class FSTREAM_OP{
READ_BY_WORD,
READ_BY_LINE,
READ_ENTIRE,
WRITE,
APPEND,
READ_WRITE
};
int main(){
FSTREAM_OP selection = FSTREAM_OP::READ_WRITE;
switch (selection){
// Reading word by word
case (FSTREAM_OP::READ_BY_WORD): {
std::ifstream file("/home/oben/Projects/cpp_exercises/cherno/config/input.txt");
std::string word;
if (file.is_open()) {
while (file >> word) {
std::cout << word << std::endl;
}
} else {
std::cout << "File not opened" << std::endl;
}
break;
}
// Reading line by line
case (FSTREAM_OP::READ_BY_LINE): {
std::ifstream file("/home/oben/Projects/cpp_exercises/cherno/config/input.txt");
std::string line;
if(file.is_open()){
while(std::getline(file, line)){
std::cout << line << std::endl;
}
}
else{
std::cout << "File not opened" << std::endl;
}
break;
}
// Reading entire file into a string
case (FSTREAM_OP::READ_ENTIRE): {
std::ifstream file("/home/oben/Projects/cpp_exercises/cherno/config/input.txt");
std::ostringstream buffer; //sstream definition
if(file.is_open()){
buffer << file.rdbuf();
std::string content = buffer.str();
std::cout << content << std::endl;
}
else{
std::cout << "File not opened" << std::endl;
}
break;
}
// Writing into a new file
case (FSTREAM_OP::WRITE): {
std::ofstream outFile("/home/oben/Projects/cpp_exercises/cherno/config/output.txt");
if(outFile.is_open()){
outFile << "Hello World\n";
outFile << "Second Line\n";
std::cout << "File written succesfully" << std::endl;
}
else{
std::cout << "File not opened" << std::endl;
}
break;
}
// Appending to a file
case (FSTREAM_OP::APPEND): {
std::ofstream appendFile("/home/oben/Projects/cpp_exercises/cherno/config/output.txt", std::ios::app);
if(appendFile.is_open()){
appendFile << "Appended Line\n";
std::cout << "File appended successfully" << std::endl;
}
else{
std::cout << "File not opened" << std::endl;
}
break;
}
// Reading and writing the same file
case (FSTREAM_OP::READ_WRITE): {
std::fstream rwFile("/home/oben/Projects/cpp_exercises/cherno/config/output.txt",
std::ios::in | std::ios::out | std::ios::app);
if(rwFile.is_open()){
rwFile << "Read/Write Line\n";
rwFile.seekg(0); // seek back to beginning for reading
std::string line;
while(std::getline(rwFile, line)){
std::cout << line << std::endl;
}
}
else{
std::cout << "File not opened" << std::endl;
}
break;
}
default:
std::cout << "Default" << std::endl;
break;
}
return 0;
}| Type | Default mode | Use |
|---|---|---|
std::ifstream |
std::ios::in |
Reading from a file |
std::ofstream |
std::ios::out |
Writing to a file |
std::fstream |
none — must specify | Reading and writing the same file |
ifstream and ofstream have their modes baked in — no need to specify them explicitly. fstream is a blank slate so both must be specified.
| Mode | Effect |
|---|---|
std::ios::in |
Enable reading |
std::ios::out |
Enable writing |
std::ios::app |
Writes go to end of file |
std::ios::trunc |
Wipe file on open (default with out) |
std::ios::binary |
Binary mode — no newline translation |
Combine with |:
std::fstream file("data.txt", std::ios::in | std::ios::out | std::ios::app);std::ifstream file("input.txt");
std::string word;
if (file.is_open()) {
while (file >> word) {
std::cout << word << std::endl;
}
}>> extracts one whitespace-separated token at a time.
std::ifstream file("input.txt");
std::string line;
if (file.is_open()) {
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
}std::getline reads until \n, stripping the newline character.
#include <sstream>
std::ifstream file("input.txt");
std::ostringstream buffer;
if (file.is_open()) {
buffer << file.rdbuf();
std::string content = buffer.str();
std::cout << content << std::endl;
}file.rdbuf() dumps the entire file buffer into the ostringstream at once. The string already contains all \n characters so it prints naturally line by line.
std::ofstream file("output.txt"); // truncates existing content
if (file.is_open()) {
file << "Hello World\n";
file << "Second Line\n";
}File is closed automatically when file goes out of scope — RAII.
std::ofstream file("output.txt", std::ios::app);
if (file.is_open()) {
file << "Appended Line\n";
}Without std::ios::app, ofstream truncates the file on open by default.
std::fstream file("output.txt", std::ios::in | std::ios::out | std::ios::app);
if (file.is_open()) {
file << "New Line\n"; // appended to end
file.seekg(0); // rewind read position to beginning
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
}seekg(0) rewinds the read position. app ensures writes go to the end. Both in and out must be specified explicitly since fstream has no defaults.
file.is_open() // opened successfully
file.good() // no errors
file.eof() // end of file reached
file.fail() // operation failed
file.bad() // unrecoverable error
file.clear() // reset error flagsOr check as bool directly:
if (!file) {
std::cerr << "Something went wrong" << std::endl;
}| Function | Moves |
|---|---|
seekg(0) |
Read position |
seekp(0) |
Write position |
Used with fstream when you need to reposition after reading or writing.
| Object | Direction | Buffered | Target |
|---|---|---|---|
std::cin |
input | yes | stdin (keyboard) |
std::cout |
output | yes | stdout (terminal) |
std::cerr |
output | no | stderr (immediate) |
std::clog |
output | yes | stderr (diagnostic) |
#include <iostream>
std::cout << "Hello\n"; // output
std::cerr << "Fatal error\n"; // unbuffered — written immediately
std::clog << "Debug info\n"; // buffered diagnosticint x = 42;
double pi = 3.14;
std::string s = "world";
std::cout << "x=" << x << " pi=" << pi << " s=" << s << '\n';
// x=42 pi=3.14 s=worldstd::cout << "line\n"; // fast — newline only
std::cout << "line" << std::endl; // newline + flush (expensive syscall)Prefer '\n' in loops. Use std::endl (or std::flush) only when you need output to appear immediately.
std::endl = '\n' + std::flush. The difference is only visible when something slow follows:
std::cout << "Computing... " << std::flush; // appears immediately
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "done\n";
std::cout << "Computing... "; // may not appear until after sleep
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "done\n";// Single value — stops at whitespace
int age;
std::cin >> age;
// Multiple values on one line
int x, y;
std::cin >> x >> y;
// Whole line — reads until '\n'
std::string fullName;
std::getline(std::cin, fullName);>> leaves a '\n' in the buffer. getline sees it immediately and returns empty.
int age;
std::cin >> age; // buffer still has '\n'
// BAD — getline returns "" immediately
std::getline(std::cin, name);
// GOOD — consume leftover whitespace first
std::getline(std::cin >> std::ws, name);int x;
while (!(std::cin >> x)) {
std::cin.clear(); // clear error flags
std::cin.ignore(1000, '\n'); // discard bad input
std::cout << "Invalid, try again: ";
}#include <iomanip>constexpr double pi = 3.14159265;
std::cout << std::setprecision(4) << pi << '\n'; // 3.142 (4 significant digits)
std::cout << std::fixed << std::setprecision(4) << pi << '\n'; // 3.1416 (4 after decimal)std::fixed makes setprecision control digits after the decimal point.
Without it, setprecision controls total significant digits.
int val = 255;
std::cout << std::hex << val << '\n'; // ff
std::cout << std::oct << val << '\n'; // 377
std::cout << std::dec << val << '\n'; // 255 (reset to decimal)std::cout << std::boolalpha << true << '\n'; // true
std::cout << std::noboolalpha << true << '\n'; // 1// std::left — pad on the right (text columns)
// std::right — pad on the left (number columns, default)
// std::setw(n) — field width, resets after each use (non-sticky)
std::cout << std::left
<< std::setw(12) << "Item"
<< std::setw(8) << "Qty"
<< std::setw(10) << "Price" << '\n';
std::cout << std::setw(12) << "Widget"
<< std::setw(8) << 42
<< std::setw(10) << 9.99 << '\n';Item Qty Price
Widget 42 9.99
std::cout << std::setfill('0') << std::setw(6) << 42 << '\n'; // 000042
std::cout << std::setfill('-') << std::setw(10) << "hi" << '\n'; // --------hiMost format flags persist until you explicitly reset them. std::setw is the exception.
| Flag | Sticky |
|---|---|
std::fixed, std::hex, std::left |
yes |
std::boolalpha |
yes |
std::setprecision |
yes |
std::setfill |
yes |
std::setw |
no — resets after one use |
std::cout << std::fixed << std::setprecision(2);
std::cout << 3.14159 << '\n'; // 3.14 (fixed still active)
std::cout << 2.71828 << '\n'; // 2.72 (fixed still active)
// Reset
std::cout << std::defaultfloat << std::setprecision(6);| Manipulator | Effect |
|---|---|
std::endl |
'\n' + flush |
std::flush |
flush buffer only |
std::ws |
skip leading whitespace (input) |
std::fixed |
decimal notation |
std::scientific |
scientific notation |
std::defaultfloat |
reset to default |
std::setprecision(n) |
precision / decimal places |
std::setw(n) |
field width (non-sticky) |
std::setfill(c) |
padding character |
std::left / std::right |
alignment |
std::hex / std::oct / std::dec |
numeric base |
std::boolalpha / std::noboolalpha |
bool as text |
