Copyright (C) 2021 Michele Welponer
Table of contents generated with markdown-toc
Pointers are variables that hold the memory address of another variable. A pointer "points" to a specific location in memory, which is the starting location of a memory block (its size depends on the type of data stored).
To retrieve the memory address of a variable, use the & (address-of operator) in front of the variable name.
std::cout << "myVar addr: " << &myVar << std::endl;A pointer is declared using an asterisk (*) after the type of the variable it will point to:
int* myPointer; // or
int* myPointer = nullptr;NOTE: nullptr is safer than NULL or 0
A pointer can be assigned the memory address of a variable using the & operator:
int a = 3;
int* myPointer = &a;Now, myPointer points to a and holds its memory address.
Dereferencing a pointer means accessing the value stored at the memory address it points to. This is done using the asterisk (*) operator in front of the pointer.
Reading the Value
int b = *myPtr; // b is assigned the value stored at the address pointed to by myPtr (3)Writing to the Value
*myPtr = 6; // Assigns 6 to the variable 'a', which is pointed to by myPtrIncrementing the Value via the Pointer
You can increment the value of the variable pointed to by a pointer:
++(*myPtr); // Pre-increment: increments the value before using it
(*myPtr)++; // Post-increment: increments the value after using itPointers support arithmetic operations. For example, incrementing a pointer moves it to the next memory location for the type it points to:
int arr[] = {10, 20, 30};
int* ptr = arr;
std::cout << *ptr << std::endl; // 10
ptr++;
std::cout << *ptr << std::endl; // 20The array subscript operator ([]) is essentially syntactic sugar for pointer arithmetic.
char str[] = "Hello";
char* ptr = str;
std::cout << str << std::endl; // Hello
std::cout << str[1] << std::endl; // 'e'
std::cout << ptr << std::endl; // Hello
std::cout << *ptr << std::endl; // H
std::cout << ptr[1] << std::endl; // 'e', equivalent to *(ptr + 1)NOTE: The array subscript operator [] in char str[] is secretly a pointer, so it's somehow like to write something like
(that is syntactically wrong!)char* str
Certain pointer expressions can seem confusing, but understanding the order of operations clarifies them:
Syntax Examples:
-
Pre-increment Pointer, Then Dereference:
*++ptr; // Increment the pointer, then dereference the new address -
Dereference, Then Pre-increment the Value:
++*ptr; // Increment the value stored at the address pointed to by ptr -
Dereference, Then Post-increment the Pointer:
*ptr++; // Dereference the current pointer, then increment the pointer
p*++, p++* and ++p* are all syntax errors.
NOTE: if you are asking yourself where a pointer is stored, the answer is anywhere in the memory, as a pointer is just an integer variable holding the memory address of the variable it is pointing to. If you are curious to know the memory address of a pointer, use the address-of operator & or the std::addressof() function:
int* ptr = nullptr;
std::cout << "Address of ptr: " << &ptr << std::endl;The following example demonstrates various pointer operations:
int b[] = {3, 0, 7};
int* p = b;
switch (4) {
case 1:
std::cout << *++p << std::endl; // Increment p, then dereference -> 0
std::cout << *++p << std::endl; // -> 7
break;
case 2:
std::cout << ++*p << std::endl; // Increment the value at p -> 4
std::cout << ++*p << std::endl; // -> 5
break;
case 3:
std::cout << *p++ << std::endl; // Dereference p, then increment -> 3
std::cout << *p++ << std::endl; // -> 0
break;
case 4:
std::cout << (*p)++ << std::endl; // Dereference p, print, then increment -> 3
std::cout << (*p)++ << std::endl; // -> 4
break;
default:
break;
}
std::cout << b[0] << " " << b[1] << " " << b[2] << std::endl;References in C++ are less flexible but more convenient than pointers. They serve as aliases for other variables, providing an alternative way to access them. References are essentially syntax sugar for pointers, and anything you can achieve with references can also be achieved using pointers.
A reference is declared using the & symbol (as part of the type). When declaring a reference, it must be initialized immediately to bind it to a variable.
int& myRef1 = myVar; // myRef1 is now an alias for myVar
int& myRef2 = *myPtr; // myRef2 is an alias for the variable pointed to by myPtr
myRef2 = 3; // Assigns 3 to the variable referenced by myPtrReferences are often used to pass variables to functions "by reference," allowing the function to modify the original variable. This is a more concise alternative to passing pointers.
Example: Passing by Pointer vs. Passing by Reference
Using pointers:
void incrementUsingPointer(int* value) {
(*value)++; // Dereference the pointer first to modify the value
std::cout << "Value in function: " << *value << std::endl;
}Using references:
void incrementUsingReference(int& value) {
value++; // Directly modify the referenced variable
std::cout << "Value in function: " << value << std::endl;
}By value (no modification to the original variable):
void increment(int value) {
value++;
std::cout << "Value in function: " << value << std::endl;
}NOTE: if we don't use by reference or by pointer and we simply use increment(int value) a new int value variable will be created, initialized and incremented inside the function, but the external variable (the one we pass to the function won't be modified!)
Main Function Example:
int main() {
int a = 5;
int& ref = a; // Create a reference to a
incrementUsingPointer(&a); // '6', a is incremented
std::cout << a << std::endl; // => 6
incrementUsingReference(ref); // '7', a is incremented
std::cout << a << ref << std::endl; // => 7 7
incrementUsingReference(a); // '8', a is incremented
std::cout << a << ref << std::endl; // => 8 8
increment(a); // '9', but a is not modified
std::cout << a << ref << std::endl; // => 8 8
}Example with Classes:
References also work with objects:
class Entity {};
void incrementUsingPointer(Entity* e) {
// Work with the object using the pointer
}
void incrementUsingReference(Entity& e) {
// Work with the object using the reference
}
int main() {
Entity e1; // Instance on the stack
incrementUsingPointer(&e1); // Pass pointer
incrementUsingReference(e1); // Pass reference
Entity* e2 = new Entity(); // Instance on the heap
incrementUsingPointer(e2); // Pass pointer
incrementUsingReference(*e2); // Pass dereferenced pointer as a reference
delete e2;
}If a variable is passed by value (e.g., increment(int value)), a new variable is created inside the function. Any modifications made to it do not affect the original variable outside the function.
| Code | Meaning |
|---|---|
T* p = nullptr; |
Declare a pointer p of type T, initialized to nullptr |
T& r = v; |
Declare a reference r of type T to the variable v; r is an alias for v |
*p |
Dereference pointer p to access the value it points to |
&v |
Get the memory address of variable v, which can be assigned to a pointer |
int a = 3; // Declare a variable
int* ptr = nullptr; // Declare a pointer
ptr = &a; // Assign the address of a to ptr
std::cout << *ptr << std::endl; // Dereference ptr to get the value, => 3
int& ref = a; // Create a reference (alias) to a
std::cout << ref << std::endl; // Output the value of a via the reference, => 3
int& ref2 = *ptr; // Create a reference to the value pointed to by ptr
std::cout << ref2 << std::endl; // Output the value, => 3
ref++; // Increment the value using the reference
std::cout << a << " " << *ptr << " " << ref << std::endl; // => 4 4 4
a++; // Increment the variable directly
std::cout << a << " " << *ptr << " " << ref << std::endl; // => 5 5 5
(*ptr)++; // Increment the value via the pointer
std::cout << a << " " << *ptr << " " << ref << std::endl; // => 6 6 6The static keyword has different uses depending on the context. It can be applied to variables, functions, and class members, with behavior varying between global, local, and object-oriented scopes.
static Variables and Functions (Outside a Class/Struct)
static int myVariable = 5;
static void myFunc();myVariable: A static global variable is visible only within the specific translation unit (file) where it is declared. This means you can define another variable with the same name in a different translation unit without causing linking conflicts.myFunc: A static global function is similarly restricted to the translation unit.
This behavior is equivalent to making the variable or function "private" to the file.
NOTE: if you don't declare a global variable as static (e.g., int var = 5;), you can access it from another translation unit using the extern keyword:
// In translation unit A:
int var = 5;
// In translation unit B:
extern int var; // Refers to the variable defined in unit ABest Practice: Always mark global functions and variables as static unless you explicitly need them to be accessible across different translation units.
A static variable declared within a local scope (e.g., inside a function or loop) retains its value between calls to that scope. Unlike regular local variables, it is only initialized once and persists for the lifetime of the program.
Example:
void func() {
static int i = 0;
std::cout << ++i << std::endl;
}- The first time
func()is called,iis initialized to0, then incremented to1. - On subsequent calls,
iretains its value and is incremented further.
Output:
func(); // Output: 1
func(); // Output: 2
func(); // Output: 3This is similar to declaring i globally, but since it is scoped to the function, it can only be modified within func().
When used inside a class or struct, static members are shared across all instances of that class.
Example:
struct Entity {
static int x; // Shared by all Entity instances
int y; // Unique to each instance
void print() {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
static void s_print() {
std::cout << "x: " << x << std::endl;
// Cannot access non-static members (e.g., y) here
}
};- Static Variables (
x): Shared among all instances. Modifyingxfrom one instance affects all instances. - Static Methods (
s_print): Can only access static members. They do not have access to non-static members (y).
Example Usage:
Entity::x = 10; // Set the shared static member
Entity e1, e2;
e1.y = 20; // Instance-specific member
e2.y = 30;
e1.print(); // Output: x: 10, y: 20
e2.print(); // Output: x: 10, y: 30
Entity::s_print(); // Output: x: 10The static keyword is commonly used in implementing the Singleton Pattern to ensure only one instance of a class exists.
Traditional Singleton Implementation:
class Singleton {
private:
static Singleton* s_instance; // Static global declaration
public:
static Singleton& get() {
return *s_instance;
}
void hello() {
std::cout << "Hello from Singleton!" << std::endl;
}
};
// Define the static instance
Singleton* Singleton::s_instance = nullptr;
int main() {
Singleton::get().hello();
return 0;
}Cleaner Singleton with Static Local Variable:
A more concise implementation involves declaring the static instance within the scope of the get method:
class Singleton {
public:
static Singleton& get() {
static Singleton instance; // Scoped static instance
return instance;
}
void hello() {
std::cout << "Hello from Singleton!" << std::endl;
}
};
int main() {
Singleton::get().hello();
return 0;
}- In this approach, the
staticinstance is initialized only once, the first timeget()is called. This avoids manual memory management (e.g.,newanddelete).
| Context | Behavior |
|---|---|
| Global Scope | Limits visibility of variables or functions to the translation unit. |
| Local Scope | Persists the variable's value across multiple function calls. |
| Class Members | Shared across all instances (for static variables and methods). |
Enums (short for enumerations) define a set of named constant integer values. They provide a way to make code more readable by replacing magic numbers with meaningful names.
enum Example /* Optionally specify the underlying type */ : unsigned char {
A, B, C // A = 0, B = 1, C = 2 (default behavior)
// A = 4, B, C // A = 4, B = 5, C = 6
// A = 5, B = 3, C = 7 // Custom values can be assigned
};- By default, the first enumerator starts at
0, and subsequent values increment by1. - You can optionally specify the underlying integer type for the enum (e.g.,
unsigned char,int,unsigned int, etc.). - Values can also be explicitly assigned.
Example:
enum Color : unsigned int {
Red = 1, Green = 2, Blue = 4
};
Color myColor = Green;
std::cout << myColor << std::endl; // Output: 2Enums can also be declared inside classes, often to define states, levels, or other related constants.
Example:
class Log {
public:
enum Level {
Error, // 0
Warning, // 1
Info // 2
};
private:
Level m_logLevel = Info; // Default level
public:
void setLevel(Level level) { m_logLevel = level; }
void printLevel() const {
switch (m_logLevel) {
case Error:
std::cout << "Error" << std::endl;
break;
case Warning:
std::cout << "Warning" << std::endl;
break;
case Info:
std::cout << "Info" << std::endl;
break;
}
}
};int main() {
// Using an enum outside of a class
Example value = B; // Assign enum value B (1) to 'value'
std::cout << value << std::endl; // Output: 1
// Using an enum within a class
Log log;
log.setLevel(Log::Error); // Set log level to Error
log.printLevel(); // Output: Error
return 0;
}- Enumerators as Constants: Enum values are constants, so they can be used anywhere a constant expression is allowed.
- Scope:
- Regular enums are placed in the global namespace, so enumerator names can clash with other identifiers.
- Use scoped enums (introduced in C++11) to avoid this (e.g.,
enum class).
- Underlying Type:
- By default, enums use
intas the underlying type. - You can explicitly specify a different type (e.g.,
unsigned charorunsigned int).
- By default, enums use
Scoped enums (declared with enum class) provide better type safety and scoping compared to regular enums. Enumerators of scoped enums must be prefixed with the enum type name.
Example:
enum class Fruit : int {
Apple = 1,
Orange = 2,
Banana = 3
};
Fruit f = Fruit::Apple; // Must specify the enum type name (Fruit::)
std::cout << static_cast<int>(f) << std::endl; // Output: 1- Scoped enums prevent implicit conversion to integers, improving type safety.
- To use the value as an integer, you must explicitly cast it (e.g.,
static_cast<int>).
class Entity {
private:
std::string m_name;
int m_age;
public:
Entity() {} // Default constructor
const std::string& getName() const { return m_name; }
void setName(const std::string& name) {
this->m_name = name;
}
};thisis a pointer to the current object.- The
->operator dereferences the pointer and accesses the member method or field.
The Entity object can be initialized with default values for its fields using a constructor. Here's an example with initialization:
class Entity {
private:
std::string m_name;
int m_age;
public:
// Constructor with initializer list (more efficient)
Entity()
: m_name("Unknown"), m_age(0)
{
std::cout << "Entity created" << std::endl;
}
// Destructor
~Entity() {
std::cout << "Entity destroyed!" << std::endl;
}
};- Initializer list is preferred because it avoids redundant default construction followed by reassignment.
- The destructor
~Entity()is automatically called when the object goes out of scope or is explicitly deleted.
NOTE:
- If the object is created on the heap (using
new), it must be manually deallocated usingdelete. - If the object is created on the stack, it will automatically be destroyed when the program exits the scope.
Example:
int main() {
Entity* e1 = new Entity(); // On the heap
delete e1; // Manual cleanup
Entity e2; // On the stack
// Automatically destroyed when exiting scope
return 0;
}Inheritance allows one class to derive from another.
class Player : public Entity {
private:
int m_points;
public:
Player() : m_points(0) {}
};- The
Playerclass inherits all methods and fields fromEntity. - It can also add new functionality or override methods from the base class.
Virtual functions allow derived classes to override base class methods.
class Entity {
public:
virtual std::string getName() { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_name;
public:
Player(const std::string& name) : m_name(name) {}
std::string getName() override { return m_name; }
};
int main() {
Entity* entity = new Player("Mike");
std::cout << entity->getName() << std::endl; // Output: Mike
delete entity;
}- If the base class method is not declared
virtual, the output would beEntity(no polymorphism). - Always mark overridden methods in derived classes with
overridefor clarity and error checking.
Interfaces are implemented using pure virtual functions. These are functions that have no implementation in the base class and must be implemented by derived classes.
class Printable {
public:
virtual std::string getClassName() const = 0; // Pure virtual function
};- The
= 0syntax makes the function pure virtual. - A class with at least one pure virtual function is an abstract class and cannot be instantiated directly.
Example:
class Player : public Printable {
public:
std::string getClassName() const override { return "Player"; }
};Access specifiers control the visibility of class members.
class Entity {
private:
int x, y; // Accessible only within the class
protected:
std::string name; // Accessible in the class and derived classes
public:
void getCoordinates() {} // Accessible from anywhere
};public: Accessible everywhere.protected: Accessible within the class and its subclasses.private: Accessible only within the class.
Default Visibility:
- Classes: Members are
privateby default. - Structs: Members are
publicby default.
The only difference between a class and a struct in C++ is the default visibility of members:
- Class: Private by default.
- Struct: Public by default.
Example:
class CEntity {
int a, b; // Private by default
public:
CEntity(int a, int b) : a(a), b(b) {}
};
struct SEntity {
int a, b; // Public by default
};NOTE: struct remain in c++ just to maintain the compatibility with c that doesn't have classes
To maintain code organization, class declarations are typically separated into .h (header) and .cpp (implementation) files.
Human.h
#pragma once
#include <string>
class Human {
private:
std::string m_name;
int m_age;
public:
Human();
Human(const std::string& name, int age);
~Human();
void print() const;
int getAge() const;
};Human.cpp
#include "Human.h"
#include <iostream>
Human::Human()
: m_name("unknown"), m_age(0) {}
Human::Human(const std::string& name, int age)
: m_name(name), m_age(age) {}
Human::~Human() {
std::cout << "Human destroyed" << std::endl;
}
void Human::print() const {
std::cout << "Human: " << m_name << ", age: " << m_age << std::endl;
}
int Human::getAge() const {
return m_age;
}int stackArr[4]; // Declare: stackArr is now a pointer to the first element of the array
int stackArr[] = {1, 2, 3, 4}; // Declare and initialize
stackArr[3] = 666; // Modify the fourth element- The array name (e.g.,
stackArr) is a constant pointer to the first element of the array. - You cannot use increment (
++) or decrement (--) operators on the array name (e.g.,) because it is not an lvalue pointer.stackArr++
However, you can work with a separate pointer:
int* ptr = stackArr; // Pointer to the array
std::cout << *(++ptr) << std::endl; // Output: 2
*(ptr + 2) = 666; // Equivalent to stackArr[3] = 666FUN FACT: In release mode, accessing out-of-bounds elements in an array does not throw an error (no bounds checking). This can lead to undefined behavior. Always implement safety checks to avoid this!
int* heapArr = new int[10]; // Allocate an array of 10 integers on the heap
heapArr[0] = 1; // Assign a value
delete[] heapArr; // Deallocate the memoryWhy use heap arrays?
The difference lies in the lifetime of the array:
- Stack memory: Automatically deallocated when the scope ends.
- Heap memory: Persists until explicitly deallocated using
delete[].
Example:
If a function needs to return an array, it must allocate it on the heap:
int* createArray(int size) {
return new int[size];
}In general, prefer stack arrays for better performance and safety, as they avoid pointer indirection.
Example:
class Entity {
public:
int stackArr[10]; // Array stored directly in the object
// If using a heap array:
// int* heapArr = new int[10]; // Entity will only store a pointer (the data is in another space in memory)
Entity() {
for (int i = 0; i < 10; i++)
stackArr[i] = 0; // Initialize stack array
}
};Raw arrays in C++ do not have built-in .size() or .length() methods. Here are two ways to determine the size:
-
For Stack Arrays:
int stackArr[4]; int length = sizeof(stackArr) / sizeof(int); // Calculate the size
-
For Heap Arrays: You must manually track the size:
static const int numOfElements = 10; int* heapArr = new int[numOfElements];
The C++ Standard Library provides a safer and more feature-rich alternative: std::array.
#include <array>
std::array<int, 10> stdArr; // Declare an array of size 10
stdArr[0] = 1; // Assign a value
std::cout << stdArr.size() << std::endl; // Output: 10Advantages:
- Built-in size tracking with
.size(). - Bounds checking (with exceptions) when using
.at().
Disadvantage:
- Slightly more overhead compared to raw arrays.
| Code | Meaning |
|---|---|
T ar[n] |
Declare an array ar of n elements of type T. |
&ar[0] |
Address of the first element of the array ar. |
&ar |
Address of the first element of the array ar. (Same as &ar[0]) |
ar |
Address of the first element of the array ar. (Implicit pointer) |
T* p = ar |
Create a pointer p that points to the first element of array ar. |
*(ar + 1) |
Access the second element of array ar. |
ar[1] |
Access the second element of array ar. (Equivalent to *(ar + 1)) |
T* har = new T[n] |
Declare a heap array har of n elements of type T. |
delete[] har |
Delete the heap array har. |
C-Style strings, also known as null-terminated strings, are simply arrays of characters terminated with a special character: '\0'. This null character signals the end of the string.
If you declare the string manually, you must include the null terminator explicitly:
char str[] = "Hello"; // Null terminator '\0' is automatically added
char myString[5] = {'M', 'i', 'k', 'e', '\0'}; // Manual null terminator
std::cout << myString << std::endl; // Output: MikeTo manipulate C-Style strings, you can use functions provided by the <cstring> library.
#include <cstring>| Function | Description |
|---|---|
strcpy(destination, source) |
Copies the source string to the destination. The destination must be large enough to hold the source string, including the null terminator. |
strcat(destination, source) |
Appends the source string to the destination string. The destination must have enough space for the combined string. |
strcmp(str1, str2) |
Compares two strings lexicographically. Returns 0 if they are equal, a negative value if str1 is less than str2, or a positive value if str1 is greater. |
strlen(str) |
Returns the length of the string (excluding the null terminator). |
Additionally, you can use functions from <cctype> for character-level checks:
#include <cctype>| Function | Description |
|---|---|
isalnum(char) |
Checks if the character is alphanumeric. |
When you declare a string as a character pointer (char*), it is immutable in the sense that you cannot change the length of the string. For this reason, such declarations are often marked as const. The <cstring> library can also be used to manipulate strings declared in this way.
Example:
#include <cstring>
const char* name = "Mike";
std::cout << name << ", " << strlen(name) << std::endl; // Output: Mike, 4
char* str1 = "Ciao";
char str2[40];
strcpy(str2, str1); // Copy str1 into str2
std::cout << str2 << std::endl; // Output: CiaoNOTE: Modifying a string literal (like "Ciao") through a char* pointer is undefined behavior.
C++ provides a safer and more versatile string implementation in the Standard Library: std::string.
Example:
#include <string>
std::string myString = "ciao";
myString += "!"; // Append a character or string
std::cout << myString << std::endl; // Output: ciao!
// Useful methods:
std::cout << myString.size() << std::endl; // Get the length of the string
std::cout << myString.find("ao") << std::endl; // Find the position of a substring
bool contains = myString.find("c") != std::string::npos; // Check if a substring exists| Method | Description |
|---|---|
size() or length() |
Returns the length of the string. |
find(substring) |
Returns the position of the first occurrence of substring, or std::string::npos if not found. |
substr(pos, len) |
Returns a substring starting at position pos with length len. |
empty() |
Checks if the string is empty. |
clear() |
Removes all characters from the string. |
Advantages of std::string:
- Dynamic size.
- Built-in support for various operations like concatenation, searching, and substring extraction.
- Safer and less error-prone compared to C-Style strings.
const int MAX_AGE = 90;The const keyword makes a variable constant, meaning its value cannot be modified after initialization. In this example, MAX_AGE is a constant.
Const BEFORE the asterisk (*):
The data the pointer points to cannot be modified, but the pointer itself can change to point to another address.
const int* a = new int; // or
int const* a = new int;
*a = 5; // Compile Error!!!
a = &MAX_AGE; // Allowed: Pointer can point to a different addressConst AFTER the asterisk (*):
The pointer itself is constant, so its address cannot change, but the data it points to can be modified.
int* const b = new int;
*b = 5; // Allowed: Can modify the value the pointer points to
b = &MAX_AGE; // Compile Error!Const BOTH BEFORE and AFTER the asterisk (*):
Both the pointer and the value it points to are immutable.
const int* const c = new int;
*c = 5; // Compile Error: Cannot modify the value the pointer points to
c = &MAX_AGE; // Compile Error: Cannot modify the pointer's addressDeclaring a method as const ensures that the method does not modify any member variables of the class. This distinction is crucial when working with const objects or references.
Rule:
Const objects can only call const methods. Non-const objects can call both const and non-const methods.
Example:
class Entity {
private:
int m_X, m_Y;
public:
// Non-const method: Can modify member variables
int getX() {
m_X = 2; // Allowed
return m_X;
}
// Const method: Cannot modify member variables
int getX() const {
// m_X = 2; // Error: Cannot modify m_X in a const method
return m_X;
}
};Why Use const Methods?
If an Entity object is passed as a const reference to a function, the non-const method getX() cannot be called. You need a const version of the method (getX() const) to handle such cases. This guarantees that the method does not unintentionally modify the object.
Declaring a class member as mutable allows it to be modified even inside a const method. This is useful for fields used for debugging or caching, where modification doesn't conceptually change the object's state.
class Entity {
private:
int m_X;
mutable int m_debugCount = 0;
public:
int getX() const {
m_debugCount++; // Allowed because m_debugCount is mutable
// m_X = 3; // Error: Cannot modify m_X in a const method
return m_X;
}
};| Context | Effect |
|---|---|
const variable |
Variable's value cannot be changed after initialization. |
const pointer |
Prevents modification of what the pointer points to or the pointer itself, depending on placement of const. |
| Const method | Ensures the method cannot modify any non-mutable member variables. |
mutable members |
Allows modification of specific variables in a const method. |
The ternary operator is just a syntactic sugar for if else
static int s_level = 1;
static int s_speed = 2;
void printSpeed() {
s_speed = s_level > 3 ? 10 : 5; // speed becomes 10 if lev>3 else 5
std::cout << "s_speen: " << s_speed << std::endl;
}
int main() {
printSpeed();
s_level = 7;
printSpeed();
}#include <iostream>
#include <string>
class Entity
{
private:
std::string m_name;
int m_age;
public:
// Default constructor
Entity()
: m_name("Unknown"), m_age(-1) {}
// Constructor with name
Entity(const std::string& name)
: m_name(name), m_age(-1) {}
// Constructor with age
Entity(int age)
: m_name("Unknown"), m_age(age) {}
// Getters
std::string getName() const { return m_name; }
int getAge() const { return m_age; }
// Setters
void setName(const std::string& name) { m_name = name; }
void setAge(int age) { m_age = age; }
};Stack Allocation
Stack allocation happens automatically. When a variable or object goes out of scope, its memory is automatically freed. Stack memory is limited but very fast because it uses a contiguous block of memory. The destructor for an object is called automatically when it goes out of scope.
int value = 4; // Primitive variable
int array[] = {1, 2, 3, 4}; // Array allocated on the stack
Entity entity("Mike"); // Entity object on the stackHeap Allocation
Heap allocation is done manually using the new keyword. Objects on the heap persist until explicitly freed using delete. Allocating on the heap is slower than stack allocation because the memory manager must find a suitable block of free memory. Additionally, forgetting to free heap memory can lead to memory leaks.
int* hvalue = new int;
*hvalue = 4;
int* harray = new int[5]; // Array allocated on the heap
harray[0] = 1;
harray[1] = 2;
harray[2] = 3;
Entity* hentity = new Entity("Mike"); // Entity object on the heap
// Free the allocated memory
delete hvalue;
delete[] harray;
delete hentity;NOTE: whenever we use the word new, we need then to delete!
| Feature | Stack | Heap |
|---|---|---|
| Memory Allocation | Automatic (managed by the compiler) | Manual (requires new and delete) |
| Lifetime | Limited to the scope of the variable | Exists until manually freed |
| Speed | Faster | Slower |
| Storage | Contiguous memory blocks | Randomly available memory blocks |
| Size | Limited (typically 1-2 MB) | Large (dependent on system memory) |
| Access | Direct access | Indirect (via pointers) |
| Cleanup | Automatic (when scope ends) | Manual (requires explicit cleanup) |
| Use Cases | Local variables, small objects | Long-lived or large objects |
| Cost of Allocation | Minimal | More expensive (complex allocation) |
| Error Handling | Stack overflow (rare) | Memory leaks if not properly managed |
When to Use Heap Allocation:
- Objects need to persist beyond their scope (e.g., returned from a function).
- Objects are too large for the stack (e.g., large arrays).
- Too many objects might exhaust stack memory.
Rule of Thumb: Use stack allocation whenever possible for simplicity and speed. Use heap allocation only when necessary.
The new keyword allocates memory on the heap and automatically calls the constructor for the object.
int* a = new int; // Allocates memory for a single int
int* aArr = new int[50]; // Allocates memory for an array of 50 ints
Entity* e = new Entity(); // Allocates memory and calls the constructor
// Equivalent C-style allocation (without constructor call)
Entity* another_e = (Entity*)malloc(sizeof(Entity));
// Allocate an array of Entity objects
Entity* eArr = new Entity[50];
// Free the memory when done
delete a;
delete e;
delete another_e; // C-style allocation does not call the destructor
delete[] aArr;
delete[] eArr;Important Notes:
deleteshould be used to free memory allocated withnew.- Use
delete[]to free memory allocated for arrays withnew. - Forgetting to free heap memory leads to memory leaks.
Implicit conversion in C++ refers to the automatic type conversion performed by the compiler from one type to another. Here are some examples:
// A cool function to print an Entity object
void PrintEntity(const Entity& entity) {
// Print logic goes here
}
int main() {
// Implicit conversion!
Entity a = std::string("Mike");
// Equivalent to writing: Entity a("Mike");
// Implicit conversion!
Entity b = 43;
// Equivalent to writing: Entity b(43);
// Implicit conversion!
PrintEntity(22);
// `22` is implicitly converted into an Entity with age 22.
// Implicit conversion! "Cherno" is first cast from
// a const char array to a `std::string` and then implicitly
// converted into an Entity with name "Cherno".
PrintEntity(std::string("Cherno"));
// It can also be explicitly cast to Entity:
PrintEntity(Entity("Cherno"));
}Using the explicit keyword in front of a constructor disables implicit conversions. For example:
class Entity {
public:
explicit Entity(const std::string& name)
: m_name(name) {}
};In this case:
int main() {
// This will cause a compilation error because implicit conversion is disabled!
PrintEntity(std::string("Mike"));
// You need to explicitly construct the Entity object:
PrintEntity(Entity(std::string("Mike")));
}The explicit keyword ensures constructors are only called when explicitly invoked, which helps prevent unintended conversions that may lead to subtle bugs in your code.
The following operators can be overloaded in C++:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> && || ++ -- , ->* -> ( ) [ ]
Operators in C++ are essentially functions! You can overload these operators, which means redefining or changing their behavior for specific types in your program. To overload an operator, we use the keyword operator followed by the operator symbol. Then, we define the function like any other normal function with its parameters.
Overloading the << Operator to Print a std::vector<T>
template<typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& vec) {
int size = vec.size();
os << "[";
for (int i = 0; i < size; i++) {
i == size - 1 ? os << vec[i] : os << vec[i] << ", ";
}
os << "]";
return os;
}Overloading the << Operator to Print a Custom vec2<T>
vec2.h file:
#pragma once
#include <iostream>
struct vec2 {
float x, y;
vec2(float x = 0, float y = 0) : x(x), y(y) {}
};
// Declare the overloaded operator<< function
std::ostream& operator<<(std::ostream& os, const vec2& vec);vec2.cpp file:
#include "vec2.h"
// Define the operator<< function
std::ostream& operator<<(std::ostream& os, const vec2& vec) {
os << "vec2(" << vec.x << ", " << vec.y << ")";
return os;
}Usage:
#include "vec2.h"
int main() {
vec2 v(3.0f, 4.0f);
std::cout << v << std::endl;
}Overloading the + Operator to Add Two vec2 Objects
struct vec2 {
float x, y;
vec2(float x, float y) : x(x), y(y) {}
// Overloading the + operator to add two vec2 objects
vec2 operator+(const vec2& other) const {
return vec2(x + other.x, y + other.y);
}
};
int main(int, char**){
vec2 a(1, 2);
vec2 b(3, 4);
vec2 c = a + b;
std::cout << c.x << ", " << c.y << std::endl;
}Overloading the new Operator to Track Heap Allocation
static uint32_t s_AllocCount = 0;
void* operator new(size_t size) {
s_AllocCount++;
std::cout << "Allocating " << size << " bytes\n";
return malloc(size);
}
void operator delete(void* memory) {
s_AllocCount--;
std::cout << "Deallocating memory (" << s_AllocCount<< ")\n";
free(memory);
}
Entity* e = new Entity("Mike", 25);
delete e;
std::cout << s_AllocCount << " allocations\n";- When overloading operators, the function signature typically follows this pattern:
operator<operator-symbol>(parameters). - For most operators, we can overload both the normal and compound assignment versions (e.g.,
+and+=). operator newis a special case and can be overloaded to define custom behavior when allocating memory on the heap.
it is just the pointer to the current object instance that the method belongs to.
class Entity {
private:
int x, y;
public:
Entity(int x, int y) {
// this is a pointer so using -> we first dereference
// then we use the Entity object
this->x = x;
this->y = y;
}
// some fancy printing function
void print() {};
};Smart pointers are scoped pointers (created on the stack) that point to objects created on the heap. Once the smart pointer goes out of scope, both the pointer and the object it points to are automatically deleted.
A unique pointer is a simple type of smart pointer where the object it points to cannot have more than one reference. make_unique is used to create a new Entity object that can only have a single pointer reference.
NOTE: You cannot copy unique pointers: If you have two unique pointers pointing to the same memory location and one of them is destroyed, the memory will be freed, and the second pointer will point to memory that has been freed, which causes undefined behavior.
// Create a unique pointer to a new Entity
std::unique_ptr<Entity> u1_ptr = std::make_unique<Entity>(7, 4);
u1_ptr->print();
// Create another unique pointer and move u1's ownership to u2
std::unique_ptr<Entity> u2_ptr = std::move(u1_ptr);
u2_ptr->print(); // Let's check u2_ptr
// This will cause a segmentation fault!
/* u1_ptr->print(); */
// There can only be one unique pointer to the object at a time!A shared pointer can be copied, and it maintains a reference count. The object is only freed when the last shared pointer referencing it is deleted.
// Create a shared pointer to a new Entity
std::shared_ptr<Entity> s1_ptr = std::make_shared<Entity>(7, 4);
std::cout << "count: " << s1_ptr.use_count() << std::endl; // Let's check the reference count => 1
{ // An inner scope
// Create another shared pointer and assign it to s1
std::shared_ptr<Entity> s2_ptr = s1_ptr;
// Check the reference count
std::cout << "count: " << s1_ptr.use_count() << std::endl; // => 2
// When the inner scope exits, the reference count is decremented
}
// Check again
std::cout << "count: " << s1_ptr.use_count() << std::endl; // => 1A weak pointer does not increment the reference count of a shared pointer. In the following example, when the inner scope ends, the memory is freed. However, we can still ask the weak pointer if it is expired or still valid.
// Create a weak pointer
std::weak_ptr<Entity> w_ptr;
{ // An inner scope
// Create a shared pointer to a new Entity
std::shared_ptr<Entity> s_ptr = std::make_shared<Entity>(7, 4);
// Assign the shared pointer to the weak pointer
w_ptr = s_ptr; // The reference count remains 1
// Check if the weak pointer is intact
std::string state = w_ptr.lock() ? "intact" : "gone";
std::cout << "state: " << state << std::endl; // => intact
// When the inner scope exits, the reference count becomes 0,
// and the Entity object gets freed
}
// Check again
std::string state = w_ptr.lock() ? "intact" : "gone";
std::cout << "state: " << state << std::endl; // => goneref. https://en.cppreference.com/w/cpp/language/rule_of_three
The Rule of Five states that if a class manages resources (e.g., dynamic memory), you should explicitly define the following five special member functions to ensure proper resource management:
- Destructor
- Copy Constructor
- Copy Assignment Operator
- Move Constructor
- Move Assignment Operator
This ensures efficient and safe handling of resource ownership, especially during copying and moving operations.
The destructor is called when an object goes out of scope or is deleted. Its primary purpose is to release resources owned by the object, such as memory or file handles.
~ClassName() {
delete[] m_buffer; // Release dynamically allocated memory
}NOTE: If your class does not manage any resources, the default destructor is sufficient.
Unnecessary copying is to be avoided, but sometimes copying is essential. When working with copying class objects, we must ensure the class provides a proper copy constructor to handle copying safely. The copy constructor creates a new object as a copy of an existing one. For resource-managing classes, this typically requires deep copying to avoid two objects sharing the same resource.
ClassName(const ClassName& other)
: m_size(other.m_size) {
m_buffer = new char[m_size + 1]; // Allocate new memory
memcpy(m_buffer, other.m_buffer, m_size + 1); // Copy contents
}NOTE: If not defined, the compiler provides a shallow-copy copy constructor by default, which can lead to resource management issues like shared memory or double-deletion.
The copy assignment operator is used when you want to copy the contents of one object into another already-existing object. This typically involves allocating new resources and copying data from the source object. The copy assignment operator is invoked when an existing object is assigned from another object. Like the copy constructor, it must perform a deep copy while properly handling existing resources.
ClassName& operator=(const ClassName& other) {
if (this == &other) return *this; // Handle self-assignment
delete[] m_buffer; // Release existing resources
m_size = other.m_size;
m_buffer = new char[m_size + 1];
memcpy(m_buffer, other.m_buffer, m_size + 1);
return *this;
}NOTE: If not defined, the compiler provides a shallow-copy version.
The move constructor transfers resources from a temporary (rvalue) object to a new object, avoiding unnecessary deep copying and improving performance. It is especially useful for temporary objects or when optimizing performance.
ClassName(ClassName&& other) noexcept
: m_buffer(other.m_buffer), m_size(other.m_size) {
other.m_buffer = nullptr; // Leave source in a valid state
other.m_size = 0;
}NOTE: This is particularly useful when returning large objects from functions or inserting them into containers.
The move assignment operator transfers resources from a temporary (rvalue) object to an existing object. It must handle cleaning up the current object’s resources before taking ownership of the new ones.
ClassName& operator=(ClassName&& other) noexcept {
if (this == &other) return *this; // Handle self-assignment
delete[] m_buffer; // Release existing resources
m_buffer = other.m_buffer;
m_size = other.m_size;
other.m_buffer = nullptr; // Leave source in a valid state
other.m_size = 0;
return *this;
}NOTE: If not defined, the compiler falls back to inefficient copying or default behavior.
The Rule of Five ensures that:
- Resources are managed properly without memory leaks.
- Operations like copying and moving are efficient and safe.
- Your class integrates seamlessly with modern C++ features like standard containers (
std::vector,std::unique_ptr, etc.) that heavily rely on move semantics.
By following the Rule of Five, your classes will be efficient, safe, and compatible with modern C++ practices. If your class does not manage any resources, the compiler-generated special functions are typically sufficient.
Here is a complete implementation of a custom String class that adheres to the Rule of Five:
#include <iostream>
#include <cstring>
class String {
private:
char* m_buffer;
size_t m_size;
public:
// Constructor
String(const char* string)
: m_size(strlen(string)), m_buffer(new char[m_size + 1]) {
memcpy(m_buffer, string, m_size + 1);
}
// Destructor
~String() {
delete[] m_buffer;
}
// Copy Constructor
String(const String& other)
: m_size(other.m_size), m_buffer(new char[other.m_size + 1]) {
memcpy(m_buffer, other.m_buffer, other.m_size + 1);
std::cout << "Copy Constructor\n";
}
// Copy Assignment Operator
String& operator=(const String& other) {
if (this == &other) return *this; // Handle self-assignment
delete[] m_buffer; // Clean up current resources
m_size = other.m_size;
m_buffer = new char[m_size + 1];
memcpy(m_buffer, other.m_buffer, m_size + 1);
std::cout << "Copy Assignment Operator\n";
return *this;
}
// Move Constructor
String(String&& other) noexcept
: m_buffer(other.m_buffer), m_size(other.m_size) {
other.m_buffer = nullptr;
other.m_size = 0;
std::cout << "Move Constructor\n";
}
// Move Assignment Operator
String& operator=(String&& other) noexcept {
if (this == &other) return *this; // Handle self-assignment
delete[] m_buffer; // Clean up current resources
m_buffer = other.m_buffer;
m_size = other.m_size;
other.m_buffer = nullptr;
other.m_size = 0;
std::cout << "Move Assignment Operator\n";
return *this;
}
void Print() const {
std::cout << m_buffer << std::endl;
}
};
int main() {
String s1 = "Hello";
String s2 = s1; // Copy Constructor
String s3 = std::move(s1); // Move Constructor
s2 = s3; // Copy Assignment Operator
s3 = std::move(s2); // Move Assignment Operator
s3.Print();
return 0;
}The -> operator is used to access fields or methods of an object through a pointer. It is essentially a shortcut for dereferencing a pointer and then accessing a member of the dereferenced object.
- For an object declared on the stack, you access its fields or methods using the dot operator (
.). - For an object created on the heap (via
new), you use a pointer to the object and access its fields or methods with the arrow operator (->).
The arrow operator is equivalent to first dereferencing the pointer and then using the dot operator on the resulting object.
int main() {
Entity e; // Create an Entity on the stack
Entity* ptr = &e; // Pointer to the stack object
// Access fields/methods via dereferencing
(*ptr).print(); // Dereference first, then call print()
// Shortcut using the arrow operator
ptr->print(); // Equivalent to (*ptr).print()
ptr->age = 5; // Modify field through the pointer
}The -> operator simplifies accessing members of an object through a pointer by combining dereferencing and member access in a single step.
Within a class, the this pointer refers to the current object. Since this is a pointer, you use this-> to access the object's members or methods. For example:
class Entity {
public:
void print() {
this->doSomething(); // Access member method using `this->`
}
private:
void doSomething() { /* ... */ }
};You can overload the -> operator for custom classes to define how member access through a pointer behaves. For example, let's create a Scoped Pointer class that manages the lifetime of a dynamically allocated object:
Scoped Pointer Example
class Entity {
public:
void print() const { std::cout << "Entity::print()" << std::endl; }
};
class ScopedPtr {
private:
Entity* m_obj; // Pointer to the managed object
public:
// Constructor: takes ownership of the passed pointer
ScopedPtr(Entity* entity) : m_obj(entity) {}
// Destructor: deletes the managed object
~ScopedPtr() { delete m_obj; }
// Overload the arrow operator
Entity* operator->() const {
return m_obj; // Return the managed object
}
};
int main() {
// Scoped pointer automatically deletes the object when it goes out of scope
ScopedPtr entity = new Entity();
// Use overloaded arrow operator to access methods of the managed object
entity->print();
}A dynamic array is a resizable array that does not require specifying its size upfront. In C++, the std::vector from the Standard Template Library (STL) is commonly used for dynamic arrays.
#include <vector>
#include <algorithm> // For std::for_each
#include <iostream> // For std::cout
// Declaring/Initialize a vector
std::vector<int> v {1, 2, 3, 4, 5}; // Version 1: Direct initialization
std::vector<int> v = {1, 2, 3, 4, 5}; // Version 2: Alternative syntax
std::vector<int> v(3); // Size 3, all elements initialized to 0
std::vector<int> v(3, 5); // Size 3, all elements initialized to 5
std::vector<Point> pointArray; // Empty vector of custom type `Point`
// Iterating with a lambda function
std::for_each(v.begin(), v.end(), [](int i) { std::cout << i << std::endl; });
// Adding elements
pointArray.push_back(Point(x, y));Here are some ways to optimize the use of std::vector:
-
Reserve Memory in Advance
If you know the number of elements you will add to the vector, you can use thereservemethod to allocate memory ahead of time. This avoids the overhead of resizing the vector multiple times as elements are added.pointArray.reserve(8); // Reserve space for 8 elements
-
Use
emplace_backInstead ofpush_back
Whilepush_backcopies or moves an already constructed object into the vector,emplace_backconstructs the object directly in-place, reducing unnecessary copying or moving.pointArray.emplace_back(x, y); // Efficiently construct a new `Point` in the vector
#include <vector>
#include <iostream>
#include <algorithm>
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
void print() const { std::cout << "(" << x << ", " << y << ")" << std::endl; }
};
int main() {
// Declare a vector of `Point` objects
std::vector<Point> pointArray;
// Reserve memory for optimization
pointArray.reserve(5);
// Add elements using emplace_back
pointArray.emplace_back(1, 2);
pointArray.emplace_back(3, 4);
pointArray.emplace_back(5, 6);
// Iterate and print elements
std::for_each(pointArray.begin(), pointArray.end(), [](const Point& p) {
p.print();
});
}In C++, libraries can be linked to your program in two main ways: static linking and dynamic linking.
With static linking, the library code is compiled and embedded directly into your executable file. This results in a larger executable but improves runtime performance because no additional linking occurs during execution. Static linking is ideal when the library doesn't need to be updated independently of the executable.
Static Linking in Visual Studio (Windows)
-
Organize the Library Files:
Create aDependenciesdirectory in your project, and within it, create a subdirectory named after the dependency (e.g.,GLFW). Inside the dependency folder, include:- An
includefolder containing the header files (e.g.,glfw3.h). - A
libfolder containing the static library files (e.g.,glfw3.lib).
- An
-
Set Up Include Directories:
Go to Project Properties > C/C++ > General > Additional Include Directories:- Set the configuration to All Configurations and Win32.
- Add the include folder path:
$(SolutionDir)Dependencies\GLFW\include
-
Set Up Library Directories:
Go to Project Properties > Linker > General > Additional Library Directories:- Add the library folder path:
$(SolutionDir)Dependencies\GLFW\lib
- Add the library folder path:
-
Specify the Library Files:
Go to Project Properties > Linker > Input > Additional Dependencies:- Add the static library file:
glfw3.lib
- Add the static library file:
Static Linking in VS Code with CMake (Linux)
-
Install the Library:
- Install the library headers and static library files using your package manager (e.g.,
sudo apt install libglfw3-devfor GLFW). - Alternatively, download and build the library from source.
- Install the library headers and static library files using your package manager (e.g.,
-
Set Up the Project Directory:
-
Organize your project directory:
MyProject/ ├── CMakeLists.txt ├── src/ │ └── main.cpp └── include/
-
-
Write the
CMakeLists.txt:-
Create a
CMakeLists.txtfile at the root of your project:cmake_minimum_required(VERSION 3.10) project(MyProject) # Add include directories include_directories(${CMAKE_SOURCE_DIR}/include) # Link the static library (e.g., GLFW) find_package(glfw3 REQUIRED) # Add source files add_executable(MyProject src/main.cpp) # Link the library target_link_libraries(MyProject glfw)
-
-
Build the Project:
-
Create a build directory and run CMake to generate the build files:
mkdir build && cd build cmake .. make
-
The executable will be generated in the
builddirectory.
-
-
Run the Program:
./MyProject
With dynamic linking, the library is not embedded into the executable. Instead, the program depends on a separate file (usually a .dll on Windows, .so on Linux, or .dylib on macOS) that is loaded at runtime. This approach keeps the executable smaller and allows the library to be updated independently.
In Windows, dynamic linking involves .dll (Dynamic Link Library) files. The program will either:
- Automatically resolve and load the
.dllfiles at application launch. - Manually load the
.dllduring runtime using functions likeLoadLibraryandGetProcAddress.
In Linux, dynamic linking involves .so (Shared Object) files. The linker looks for the .so files in standard library paths (e.g., /usr/lib, /usr/local/lib) or paths specified in the environment variable LD_LIBRARY_PATH.
Templates in C++ are a powerful mechanism for creating generic and reusable code. They allow you to define methods, classes, or other constructs that can operate with different data types or parameters. Unlike traditional generic programming in some other languages, templates in C++ are not limited by a generic type system and enable compile-time code generation tailored to specific use cases.
Templates work by specifying a blueprint that the compiler uses to generate concrete implementations when the template is instantiated. This means that the code for the template is compiled only when it is used, and the specified types or parameters are substituted at compile time.
Here’s an example of a templated class that defines an array:
template<typename T, int N>
class Array {
private:
T m_array[N]; // Fixed-size array of type T
public:
// Method to get the size of the array
int getSize() const {
return N;
}
};
int main() {
Array<int, 50> array; // Instantiate Array with int type and size 50
std::cout << array.getSize() << std::endl; // Output: 50
return 0;
}-
Lazy Compilation: The
Arrayclass will only be compiled when it is used. TheTtype and theNsize parameter are replaced by the specified values (e.g.,intand50) at compile time. -
Flexible Data Types: Templates allow you to create type-agnostic classes and functions. For instance, you can create an
Array<double, 100>orArray<std::string, 10>without rewriting the class. -
Compile-Time Parameters: You can pass values like
N(the size of the array in this example) as non-type template parameters, which are resolved during compilation.
Templates are not limited to classes; you can also create templated functions:
template<typename T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add<int>(3, 5) << std::endl; // Output: 8
std::cout << add<double>(2.5, 4.5) << std::endl; // Output: 7.0
return 0;
}Here, the add function works with any type T that supports the + operator.
-
Type Safety: Templates ensure that your code works with the specified types without runtime errors (all checks happen at compile time).
-
Code Reusability: You can write generic and reusable functions or classes without duplicating code for every type.
-
Compile-Time Optimization: Since templates are resolved at compile time, they can result in highly optimized code compared to runtime polymorphism.
Macros are preprocessor directives that perform pure text replacement before the code gets compiled. This allows macros to simplify repetitive tasks or control the inclusion of debugging or conditional code.
A simple example:
#define WAIT std::cin.get()
int main(){
WAIT;
}A more useful example, using a parameter and a preprocess definition:
#ifdef PR_DEBUG // preprocess definition variable
#define LOG(x) std::cout << x << std::endl
#else
#define LOG(x) // this will basically remove LOG line in main
#endif
int main(){
LOG("Hello");
}NOTE: You can define the preprocess definition variables, e.g., PR_DEBUG at the top of your source file (before any relevant code) using #define.
However, it is common practice, especially for larger projects, to define the preprocess definitions using compiler flag.s This ensures the macros are globally available across all files without requiring repetitive #define directives.
For example, if you're using:
-
GCC/Clang: Add
-DPR_DEBUGto the compiler flags:g++ -DPR_DEBUG main.cpp -o mainor in a Makefile:CXXFLAGS += -DPR_DEBUG -
CMake: Add it in your
CMakeLists.txtfile:add_compile_definitions(PR_DEBUG) -
MSVC: Add
/DPR_DEBUGto the compiler flags:cl /DPR_DEBUG main.cpp
Macros are often used to create custom debugging tools or validation checks. Here’s an example of a custom assertion macro:
#include <iostream>
// Custom assert macro
#define MY_ASSERT(condition) \
do { \
if (!(condition)) { \
std::cerr << "Assertion failed: " << #condition << " in " << __FILE__ << " at line " << __LINE__ << std::endl; \
std::terminate(); \
} \
} while (false)
int main() {
int x = 5;
MY_ASSERT(x == 5); // Use the custom assert macro
std::cout << "Program continues after the assertion check." << std::endl;
}NOTE: here the macro is wrapped in a do { ... } while (false) block to ensure proper scoping/bracketing.
do { ... } while (false) executes once the do so doesn't change much the code, however it ensures that all the statements in the do block are within a curly bracket block (avoiding dangling else)
In the following Example:
if (x == 5)
MY_ASSERT(x > 0);
else
do_something_else();If the MY_ASSERT macro is defined like this:
#define MY_ASSERT(condition) \
if (!(condition)) { \
std::cerr << "Assertion failed: " << #condition << std::endl; \
std::terminate(); \
}the code expands to
if (x == 5)
if (!(x > 0)) {
std::cerr << "Assertion failed: " << std::endl;
std::terminate();
}
else
do_something_else();that is wrong! The else is always associated with the nearest unmatched if i.e., the dangling else becomes the else of the inner if.
If we define the macro using the do ... while(false) instead
#define MY_ASSERT(condition) \
do { \
if (!(condition)) { \
std::cerr << "Assertion failed: " << #condition << std::endl; \
std::terminate(); \
} \
} while(false)the expanded code looks and works fine
if (x == 5)
do {
if (!(x > 0)) {
std::cerr << "Assertion failed: " << std::endl;
std::terminate();
}
} while(false)
else
do_something_else();The primary purpose of namespaces is to avoid naming conflicts. Namespaces allow us to define symbols (such as classes, functions, and variables) with the same name in different contexts, avoiding collisions in larger projects or when using third-party libraries.
Example
#include <iostream>
namespace apple {
void print(const char* str) {
std::cout << "[apple] " << str << std::endl;
}
}
namespace orange {
void print(const char* str) {
std::cout << "[orange] " << str << std::endl;
}
}
int main() {
// Call the orange namespace's print function
orange::print("Ciao a tutti");
}-
Fully Qualified Names You can explicitly specify the namespace when calling a function or accessing a variable to avoid ambiguity:
orange::print("Hello from orange!"); apple::print("Hello from apple!");
-
using namespaceDirective Theusing namespacedirective imports all symbols from a namespace into the current scope, allowing you to call functions or access variables without qualifying their namespace:using namespace apple; print("Hello!"); // Equivalent to apple::print
Caution: Avoid using
using namespacein global scope, especially with large or commonly-used namespaces (likestd), as it can lead to naming conflicts. -
Aliasing with
namespaceTo simplify long or deeply nested namespace names, you can create an alias:namespace a = apple; a::print("Using alias for apple");
-
Anonymous Namespaces An anonymous namespace is used to limit the scope of symbols to a single file. This is particularly useful for internal linkage:
namespace { void internalFunction() { std::cout << "This function is only accessible in this file." << std::endl; } } int main() { internalFunction(); // OK return 0; }
Namespaces can be nested within other namespaces:
namespace fruit {
namespace apple {
void print() {
std::cout << "This is fruit::apple!" << std::endl;
}
}
}
int main() {
fruit::apple::print(); // Accessing nested namespace
return 0;
}In C++, you can assign functions to variables, pass functions as arguments to other functions, and use function pointers to build complex and flexible logic. Function pointers are variables that store the memory address of a function, allowing dynamic function invocation.
Here’s how you can assign a function to a pointer and invoke it:
// a very simple function
void helloWorld(){
std::coutn << "hello" << std::endl;
}
int main(){
// we can assign a function to a pointer
// assign myfunc the memory address of helloWorld function
auto myfunc = &helloWorld;
// because of implicit conversion we can also omit the ampersand &
auto myfunc = helloWorld;
// function pointers are declared using
// type(*)()
// so to explicitly declare myfunc we write
void(*myfunc)() = helloWorld;
// to make it more readable we can use typedef
typedef void (*HelloWorldFunc)();
// and then use it
HelloWorldFunc myfuncWithTypedef = helloWorld;
// Finally call the function through the pointer
myfunc();
myfuncWithTypedef();
}Function pointers can also point to functions that take parameters. Here's how:
void helloWorld(int value){
std::coutn << "hello: " << value << std::endl;
}
int main(){
typedef void(*hwfunc)(int)
hwfunc myfunc = helloWorld;
myfunc(8);
myfunc(4);
myfunc(9);
}Example
A common use case for function pointers is callbacks, where you pass a function pointer to another function to customize its behavior. Here's an example:
// this is the callback function
void printValue(int value){
std::cout << value << std::endl;
}
void foreach(const std::vector<int>& values, void(*func)(int)){
for (int v : values)
func(v);
}
int main(){
std::vector<int> vector = {1, 2, 3};
foreach(vector, printValue);
}so here I use foreach function to which I pass a vector of elements and a function pointer. The function the pointer points to defines what I want to do with each element of the vector.
..though we can rewrite the previous in a more simple way, avoiding to write the extra function printValue(), using lambda.
Lambdas are anonymous functions that can be defined inline. They are particularly useful for short, single-use logic.
In C++, the general expression form of a lambda function is as follows:
[capture_list] (parameter_list) -> return_type {
function_body
};Example
Here’s the same example we have seen above, rewritten with a lambda:
void foreach(const std::vector<int>& values, void(*func)(int)){
for (int v : values)
func(v);
}
int main(){
std::vector<int> vector = {1, 2, 3};
foreach(vector, [](int value) -> void {std::cout << value << std::endl;});
}-
Capture List (
[ ]): Specifies which variables from the enclosing scope are captured by the lambda. It can be:- Empty (
[]) if no variables are captured. [var]to capturevarby value.[&var]to capturevarby reference.[=]to capture all variables by value.[&]to capture all variables by reference.[=, &var]to capture all variables by value, exceptvar, which is captured by reference.[&, var]to capture all variables by reference, exceptvar, which is captured by value.
- Empty (
-
Parameter List (
( )):- Specifies the parameters that the lambda takes, similar to a regular function.
- If no parameters are needed, this can be empty:
(). - Example:
(int a, double b).
-
Return Type (
-> return_type) (optional):- Explicitly specifies the return type of the lambda.
- If omitted, the compiler deduces the return type from the
function_body. - Example:
-> int.
-
Function Body (
{ }):- Contains the actual code of the lambda.
- Similar to the body of a regular function.
Simple Lambda
auto greet = []() {
std::cout << "Hello, world!" << std::endl;
};
greet();Lambda with Parameters
auto add = [](int a, int b) {
return a + b;
};
std::cout << add(3, 5) << std::endl; // Output: 8Lambda with Explicit Return Type
auto divide = [](int a, int b) -> double {
if (b != 0) {
return static_cast<double>(a) / b;
} else {
return 0.0;
}
};
std::cout << divide(5, 2) << std::endl; // Output: 2.5Capturing Variables by Value
int x = 10;
auto printX = [x]() {
std::cout << "x = " << x << std::endl;
};
printX(); // Output: x = 10Capturing Variables by Reference
int x = 10;
auto modifyX = [&x]() {
x += 5;
};
modifyX();
std::cout << "x = " << x << std::endl; // Output: x = 15Mixed Captures
int a = 5, b = 10;
auto sum = [a, &b]() {
b += a;
return b;
};
std::cout << sum() << std::endl; // Output: 15
std::cout << b << std::endl; // Output: 15Lambda as an Argument to a Function
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n * 2 << " ";
});
// Output: 2 4 6 8 10
return 0;
}Lambda with Stateful Captures
auto counter = [count = 0]() mutable {
return ++count;
};
std::cout << counter() << std::endl; // Output: 1
std::cout << counter() << std::endl; // Output: 2Lambdas are often used in modern C++ for:
- Callbacks (e.g., in
std::for_eachor GUI frameworks). - Functional-style programming (e.g., map/filter/reduce).
- Short-lived functions (avoiding writing separate named functions).
std::vector<int> v {1, 2, 3, 4};
// defining my lambda: check if i is even or odd
auto is_even = [](int i) { return i % 2 == 0; };
// result is an iterator that points to the first match
// or to end() if no match are found
auto result = std::find_if(begin(v), end(v), is_even);
// if something is found dereference iterator to print the first match
(result != std::end(v))
? std::cout << "v contains an even number: " << *result << '\n'
: std::cout << "v does not contain even numbers\n";// inside [] I can put captures, variables/objects that are outside
// my lambda function
// I can use:
// [=] captures everything by value
// [&] captures everything by reference
int const max = 100;
std::vector<int> vec {178, 101, 123, 145, 45, 33};
auto lambda = [max](int i){ return i < max; };
auto it = std::find_if(begin(vec), end(vec), lambda);
(it != end(vec))
? std::cout << "first element <= max is: " << *it <<std::endl
: std::cout << "all element are > max." << std::endl;A simple (1D) array in C++ is essentially a contiguous block of memory, and can be accessed using a pointer to its first element:
// Stack-allocated array
int a[50];
int* ptr = a; // 'ptr' points to the first element of 'a'
// Heap-allocated array
char* buffer = new char[8];A 2D array is an array of arrays. You can initialize it directly:
int x[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// Equivalent flat initialization
int x2[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};A dynamically allocated 2D array is created using pointers. Conceptually, this is an array of pointers, each pointing to a 1D array.
int** a2d = new int*[50]; // 50 rows (pointers to int arrays)
// with this we just have allocated memory
// specifically 50 integer pointers, so
// 4bytes * 50 = 200 bytes of memory
// now we need to initialize the 50 arrays
for (int i = 0; i < 50; i++) {
a2d[i] = new int[50]; // Each row has 50 columns
}for (int i = 0; i < 50; i++) {
delete[] a2d[i]; // Free each row
}
delete[] a2d; // Free the row pointer arrayA 3D array is an array of 2D arrays, requiring nested loops for allocation:
int*** a3d = new int**[50];
for (int i = 0; i < 50; i++) {
a3d[i] = new int*[50];
for (int j = 0; j < 50; j++) {
a3d[i][j] = new int[50];
}
}Note: Remember to deallocate all levels in reverse order to prevent memory leaks.
Dynamically allocated multidimensional arrays are not contiguous in memory, which leads to poor cache performance. A better approach is to simulate a 2D or 3D array using a single flat array.
int* flat2d = new int[3 * 3];
// Simulate 2D access: [row][col]
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
flat2d[i * 3 + j] = 0;
}
}This layout ensures contiguous memory, which improves performance due to better spatial locality and cache usage.
Reference: cppreference.com - std::sort
#include <algorithm>
int main() {
int n = 7;
int a[] = {4, 2, 5, 3, 5, 8, 3};
std::sort(a, a + n); // Sorts the array in ascending order
}#include <vector>
#include <algorithm>
int main() {
std::vector<int> values = {3, 5, 1, 4, 2};
std::sort(values.begin(), values.end()); // Sorts ascending: 1 2 3 4 5
std::sort(values.rbegin(), values.rend()); // Sorts descending: 5 4 3 2 1
}#include <algorithm>
#include <string>
#include <iostream>
int main() {
std::string s = "monkey";
std::sort(s.begin(), s.end()); // Result: "ekmnoy"
std::cout << s << std::endl;
}#include <algorithm>
#include <vector>
class Man {
public:
Man(std::string name, int age) : m_name(name), m_age(age) {}
int age() const { return m_age; }
private:
std::string m_name;
int m_age;
};
bool compareByAge(const Man& m1, const Man& m2) {
return m1.age() < m2.age();
}
int main() {
std::vector<Man> men = {{"Mike", 30}, {"Alice", 25}, {"Bob", 27}};
std::sort(men.begin(), men.end(), compareByAge);
}#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> values = {3, 5, 1, 4, 2};
std::sort(values.begin(), values.end(), std::greater<int>()); // Descending: 5 4 3 2 1
std::sort(values.begin(), values.end(), [](int a, int b) {
return a < b;
}); // Ascending: 1 2 3 4 5
std::sort(values.begin(), values.end(), [](int a, int b) {
return a > b;
}); // Descending: 5 4 3 2 1
}Type punning refers to treating a block of memory of one type as if it were a different type.
int a = 5;
std::cout << "a: " << a << std::endl;
double value = a; // Implicit conversion from int to double
std::cout << "value: " << value << std::endl; // value: 5In this case, a is converted to a new double value. The original memory of a is not reused — a proper type conversion happens, and the memory layout changes accordingly.
If we want to reinterpret the memory of one type as another (without conversion), we can cast its address:
int a = 5;
// Reinterpret the memory of 'a' as a double
double value = *(double*)&a;
// we take memory address of 'a', cast it to double pointer, then dereference to get back the value
std::cout << "value: " << value << std::endl; // value: some garbage, e.g., 7.75152e+230This is type punning: we're treating the memory of a (which holds an int) as if it were a double. This is dangerous, as it results in undefined behavior — the value read is likely garbage due to different type sizes and memory layouts.
A more illustrative example is punning a struct into an array:
#include <iostream>
struct Entity {
int a, b;
};
int main() {
Entity e = {5, 8};
// Reinterpret the memory of the struct as an array of ints
int* arr = (int*)&e;
std::cout << "arr[0]: " << arr[0] << ", arr[1]: " << arr[1] << std::endl;
// Output: arr[0]: 5, arr[1]: 8
}Here, the struct Entity consists of two ints. Since the struct’s memory layout is just two consecutive ints, reinterpreting it as an array of int works correctly — though it still technically relies on compiler behavior and alignment.
Type punning may lead to undefined behavior and should be used with extreme caution. In modern C++, safer alternatives include:
std::memcpy(for copying memory between types)std::bit_cast(C++20)union(in legacy code)reinterpret_cast(with care)
A union is a special class type that can hold only one of its non-static data members at a time. All members share the same memory space. This is useful when you want to reinterpret memory without type conversion — in a safer and more structured way than raw pointer casting (type punning).
Suppose we have a Vector2 and a Vector4, and we want to access the Vector4 as two Vector2 instances — for example, treating it like a pair: a = (x, y) and b = (z, w).
First, let’s define a simple Vector2 struct with an overloaded output operator:
struct Vector2 {
float x, y;
};
// Overload operator<< for Vector2
std::ostream& operator<<(std::ostream& stream, const Vector2& vector) {
stream << vector.x << ", " << vector.y;
return stream;
}We can manually reinterpret the memory using pointer casting — this is type punning and is error-prone:
struct Vector4 {
float x, y, z, w;
Vector2& getA() { return *(Vector2*)&x; } // Treat x and y as Vector2
Vector2& getB() { return *(Vector2*)&z; } // Treat z and w as Vector2
};
int main() {
Vector4 v4 = {1, 2, 3, 4};
std::cout << v4.getA() << std::endl; // Output: 1, 2
}While this works in practice, it relies on assumptions about memory layout and aliasing that could lead to undefined behavior.
We can achieve the same result more safely and cleanly using an anonymous union inside the Vector4 struct:
struct Vector4 {
union {
struct { float x, y, z, w; }; // Layout of the full vector
struct { Vector2 a, b; }; // Alternative access: two Vector2s
// so now I can access data in two ways:
// 1. using x, y, z, w
// 2. using a and b, where a happens to be x, y
// b happens to be z, w
};
};
int main() {
Vector4 v4 = {1, 2, 3, 4};
v4.z = 33; // Modify an individual component
std::cout << v4.b << std::endl; // Output: 33, 4
}- Unions allow different views of the same memory.
- Anonymous structs/unions let you access fields directly (without
.a.xsyntax). - You must ensure the members don’t conflict or lead to misaligned access.
- Safer and more readable than pointer casting.
When working with inheritance and virtual functions, it’s crucial to understand the importance of virtual destructors.
class Base {
public:
Base() { std::cout << "B constr, "; }
~Base() { std::cout << "B destr, "; }
};
class Derived : public Base {
public:
Derived() { std::cout << "D constr, "; }
~Derived() { std::cout << "D destr, "; }
};
int main() {
Derived* derived = new Derived();
delete derived;
}Output:
B constr, D constr, D destr, B destr,
This behaves as expected — both constructors and destructors are called in the proper order.
Now let’s see what happens when using polymorphism:
int main() {
Base* poly = new Derived();
delete poly;
}Output:
B constr, D constr, B destr,
Oops! The Derived destructor is never called, leading to a potential memory/resource leak.
To fix this, declare the base class destructor as virtual:
class Base {
public:
Base() { std::cout << "B constr, "; }
virtual ~Base() { std::cout << "B destr, "; }
};With this change, delete poly will properly invoke both destructors:
B constr, D constr, D destr, B destr,
Whenever a class is intended to be inherited from, always declare its destructor as
virtual.
This ensures that the entire object hierarchy is properly destroyed, even when accessed via a base class pointer.
Type casting means converting a value from one type to another. In C++, this can happen either:
- Implicitly, when the compiler knows how to do the conversion safely.
- Explicitly, when you tell the compiler exactly how to convert a value.
Used for standard conversions between related types at compile-time. It's type-safe and preferred over C-style casts.
C-style cast:
double value = 5.32;
int a = (int)value + 5.4; // explicit C-style castC++-style cast:
double value = 5.32;
int a = static_cast<int>(value) + 5.4;NOTE: static_cast is better because it's easier to locate in code and allows better compile-time checks.
Used for safe downcasting in inheritance hierarchies (requires at least one virtual function in the base class). It performs runtime type checking, returning nullptr if the cast fails (when dealing with pointers).
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {};
class AnotherClass : public Base {};
int main() {
Base* base = new Derived(); // suppose we don't know that base is specialized to Derived
AnotherClass* ac1 = static_cast<AnotherClass*>(base); // Unsafe: no runtime check!
AnotherClass* ac2 = dynamic_cast<AnotherClass*>(base); // Safe: returns nullptr if cast fails
if (ac2) {
std::cout << "Cast successful.\n";
} else {
std::cout << "Cast failed.\n";
}
delete base;
}NOTE: Use dynamic_cast when you're not sure of the actual type at runtime and need type checking.
Used for low-level type punning — interpreting the raw memory of one type as another. It's dangerous and should be used with extreme caution.
int a = 5;
double* ptr = reinterpret_cast<double*>(&a);
std::cout << *ptr << std::endl; // undefined behaviorNOTE: This cast tells the compiler: "Trust me, I know what I'm doing." Often used in embedded or performance-critical code, but not type-safe.
Used to add or remove const or volatile qualifiers from a variable. Useful in rare cases, e.g., legacy APIs or when you know modifying a const object is safe (e.g., internal caching).
const int a = 42;
int& b = const_cast<int&>(a);
b = 10;
std::cout << a << " " << b << std::endl; // undefined behavior!NOTE: Modifying a const object after casting away its constness leads to undefined behavior.
A singleton is a design pattern that ensures a class has only one instance and provides a global point of access to it.
In C++, a singleton acts somewhat like a namespace—it organizes global data and behavior into a single, centralized instance. It’s commonly used for managers, configuration handlers, loggers, etc.
#include <iostream>
class SingletonExample {
public:
// The method to retrieve the single instance
static SingletonExample& get() {
static SingletonExample instance; // created once, on first use
return instance;
}
void setStatus() { this->status = 1; }
static int getStatus() { return get().status; }
private:
int status; // a variable to demonstrate uniqueness
// Private constructor to prevent external instantiation
SingletonExample() : status(0) {
std::cout << "Constructor called" << std::endl;
}
// Delete copy constructor and assignment operator
SingletonExample(const SingletonExample&) = delete;
SingletonExample& operator=(const SingletonExample&) = delete;
};
int main() {
std::cout << SingletonExample::getStatus() << std::endl;
// All references point to the same instance
SingletonExample& se1 = SingletonExample::get();
SingletonExample& se2 = SingletonExample::get();
auto& se3 = SingletonExample::get();
std::cout << "se1: " << se1.getStatus() << std::endl;
se1.setStatus();
std::cout << "se1: " << se1.getStatus() << std::endl;
std::cout << "se2: " << se2.getStatus() << std::endl;
std::cout << "se3: " << se3.getStatus() << std::endl;
std::cout << SingletonExample::getStatus() << std::endl;
}- The instance is created lazily and is thread-safe (guaranteed by C++11 and later).
- The constructor is private to prevent direct instantiation.
- Copying and assigning the singleton are explicitly disabled.
An lvalue (locator value) refers to an object that has an identifiable location in memory. In other words, it can appear on the left-hand side of an assignment because it has an address.
int a; // 'a' is a variable stored in memory — it is an lvalue
a = 1; // valid: 'a' appears on the left-hand side
int b = a; // valid: 'a' is used on the right-hand side, but it is still an lvalue
9 = a; // Error: '9' is a literal (an rvalue) and cannot appear on the left-hand sideAn rvalue (read value) is a temporary value that does not persist in memory and cannot be assigned to. It can only appear on the right-hand side of an assignment.
int a = 1, b;
a + 1 = b; // Error: 'a + 1' is an rvalue and cannot be assigned to
int* p, *q;
*p = 1; // Valid: '*p' is an lvalue because it refers to a memory location
p + 2 = 18; // Error: 'p + 2' is the result of pointer arithmetic — it is an rvalue
q = p + 5; // Valid: 'q' is an lvalue, 'p + 5' is an rvalue
*(p + 2) = 18; // Valid: dereferencing the result gives an lvalue
p = &b; // Valid: assigning the address of 'b' to pointer 'p'
int arr[20];
arr[12] = 5; // Valid: 'arr[12]' is equivalent to '*(arr + 12)' — an lvalue
&a = p; // Error: '&a' produces a temporary value (an rvalue)
struct S { int m; };
S obj; // 'obj' is an lvalue
obj.m = 10; // 'obj.m' is also an lvalue
S* ptr = &obj;
ptr->m = 20; // 'ptr->m' is an lvalue, equivalent to '(*ptr).m'| Expression Type | Can appear on left of = |
Has memory address | Examples |
|---|---|---|---|
| lvalue | Yes | Yes | a, *p, arr[5] |
| rvalue | No | No (typically) | 42, a + 1, p + 2 |
Logical operators are used to perform logical operations on boolean expressions. They combine multiple conditions and evaluate the result as either true or false. Conditions are evaluated from left to right.
- The logical AND operator (
&&) returnstrueif both of its operands aretrue. - The logical OR operator (
||) returnstrueif at least one of its operands istrue. - The logical NOT operator (
!) is a unary operator that negates the value of its operand.
bool a = true;
bool b = false;
bool result = a && b; // result is false (evaluates false immediately if a is false)
result = a || b; // result is true (evaluates true immediately if a is true)
result = !a; // result is false (negates the value of a)Bitwise operators are used to perform operations on individual bits of binary numbers. These operators allow manipulation of the binary representation of integers at a bit level. There are six bitwise operators in C++:
- The bitwise AND (
&) operator compares the corresponding bits of two operands and returns1if both bits are1, otherwise it returns0. - The bitwise OR (
|) operator compares the corresponding bits of two operands and returns1if at least one of the bits is1, otherwise it returns0. - The bitwise XOR (
^) operator compares the corresponding bits of two operands and returns1if the bits are different, otherwise it returns0. - The bitwise NOT (
~) operator is a unary operator that flips the bits of its operand. It returns the one's complement of the operand. For example:
int a = 5; // binary representation: 0101
int b = 3; // binary representation: 0011
int result = a & b; // binary result: 0001 (1 in decimal)
int result = a | b; // binary result: 0111 (7 in decimal)
int result = a ^ b; // binary result: 0110 (6 in decimal)
int result = ~a; // binary result: 1010 (-6 in decimal, two's complement)-
The left shift (
<<) operator shifts the bits of the left operand to the left by a specified number of positions. The vacant positions are filled with zeros.
Note: A left shift multiplies the original number by2. -
The right shift (
>>) operator shifts the bits of the left operand to the right by a specified number of positions. The vacant positions are filled with the sign bit (for signed types) or with zeros (for unsigned types).
Note: A right shift is equivalent to dividing a number by2.
int a = 5; // binary representation: 0101
int result = a << 2; // binary result: 010100 (20 in decimal)
result = a >> 2; // binary result: 0001 (1 in decimal)A thread is the smallest unit of execution in a process.
Multithreading involves the execution of multiple threads concurrently within a single program. It allows multiple threads to run independently, sharing the same resources (e.g., memory) but having their own execution contexts (stack and registers).
Once a thread is created, its function or callable object is executed concurrently with other threads.
#include <thread>
void myThreadFunction() {
// Code to be executed on a separate thread
}
int main() {
std::thread myThread(myThreadFunction); // myThread starts running immediately in parallel (with the main thread)
// The main thread can continue working here
myThread.join(); // Main thread waits for myThread to finish before proceeding
// Alternatively, myThread can run independently
// myThread.detach();
// Do other work here (only after myThreadFunction finishes executing)
}When multiple threads access shared data concurrently, synchronization mechanisms are necessary to avoid data races. Mutexes, condition variables, and atomic operations are commonly used for synchronization.
A mutex is a synchronization primitive in C++ used to protect shared resources from concurrent access by multiple threads. It ensures that only one thread can access the protected resource at a time, preventing data races and maintaining data integrity.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
int sharedVariable = 0;
void myThreadFunction() {
std::lock_guard<std::mutex> lock(myMutex); // Safely modify sharedVariable
sharedVariable++;
}
int main() {
std::thread myThread1(myThreadFunction);
std::thread myThread2(myThreadFunction);
myThread1.join();
myThread2.join();
// sharedVariable is now safely modified by both threads
}std::condition_variable is a synchronization primitive in C++ that allows threads to wait for a particular condition to be satisfied. It is often used in conjunction with a std::mutex to protect shared data and coordinate the execution of multiple threads.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mutex_;
std::condition_variable condVar;
std::queue<int> dataQueue;
const int MAX_QUEUE_SIZE = 5;
void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mutex_);
// Wait until there is space in the queue
condVar.wait(lock, [] { return dataQueue.size() < MAX_QUEUE_SIZE; });
// Produce data and add it to the queue
dataQueue.push(i);
std::cout << "Produced: " << i << std::endl;
// Notify consumers that data is available
condVar.notify_one();
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mutex_);
// Wait until there is data in the queue
condVar.wait(lock, [] { return !dataQueue.empty(); });
// Consume data from the queue
int data = dataQueue.front();
dataQueue.pop();
std::cout << "Consumed: " << data << std::endl;
// Notify producer that space is available
condVar.notify_one();
}
}
int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);
producerThread.join();
consumerThread.join();
}#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mutex_;
std::condition_variable condVar;
bool dataReady = false;
void producer() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate work
{
std::lock_guard<std::mutex> lock(mutex_);
dataReady = true;
}
condVar.notify_one();
}
void consumer() {
{
std::unique_lock<std::mutex> lock(mutex_);
condVar.wait(lock, [] { return dataReady; });
// Do something with the data
std::cout << "Consumed data." << std::endl;
}
}
int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);
producerThread.join();
consumerThread.join();
}- Use
std::lock_guardwhen you need a simple and concise way to lock and unlock a mutex within a scope, and no manual unlocking is required. - Use
std::unique_lockwhen you need more flexibility, such as manual unlocking, deferred locking, or using the lock with condition variables.
The choice between std::lock_guard and std::unique_lock depends on the specific requirements of your code and the level of control you need over the associated mutex.
#include <boost/thread.hpp>
boost::mutex myMutex;
int sharedVariable = 0;
void myThreadFunction() {
boost::unique_lock<boost::mutex> lock(myMutex);
sharedVariable++; // Safely modify sharedVariable
}
int main() {
boost::thread myThread1(myThreadFunction);
boost::thread myThread2(myThreadFunction);
myThread1.join();
myThread2.join();
}Boost provides a lock-free queue implementation, removing the need for explicit locking and unlocking when accessing the shared data structure.
#include <iostream>
#include <thread>
#include <boost/lockfree/queue.hpp>
const int MAX_QUEUE_SIZE = 5;
boost::lockfree::queue<int> dataQueue(MAX_QUEUE_SIZE);
void producer() {
for (int i = 0; i < 10; ++i) {
while (!dataQueue.push(i)) {
// Queue is full, spin-wait or implement backoff strategy
}
std::cout << "Produced: " << i << std::endl;
}
}
void consumer() {
int data;
while (true) {
while (dataQueue.pop(data)) {
// Process the consumed data
std::cout << "Consumed: " << data << std::endl;
}
// Queue is empty, spin-wait or implement backoff strategy
}
}
int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);
producerThread.join();
consumerThread.join();
return 0;
}The boost::atomic library provides atomic operations, which can be used to perform operations on variables in a way that is guaranteed to be atomic and avoid data races. Atomic operations are guaranteed to be executed as a single uninterruptable operation and are often implemented in a lock-free manner, making them suitable for scenarios where lock contention might be a concern.
Atomic operations are typically used for simple, low-level operations on shared variables without the need for explicit locking.
Example: Atomic Operation
#include <boost/atomic.hpp>
boost::atomic<int> atomicVariable(0);
void myThreadFunction() {
atomicVariable.fetch_add(1); // Atomically increment the variable
}std::cin.get() is a member function of the cin object, which is an instance of the istream class. This function is used to read a single character from the standard input.
#include <iostream>
//...
char ch;
std::cout << "Enter a character: ";
ch = std::cin.get(); // Reads any character (including spaces and new line characters)
std::cin >> std::ws; // Ignore leading whitespace before reading the character
std::cin >> ch; // Read the character after skipping any whitespace
std::cout << "You entered: " << ch << std::endl;
//...std::cin is also used to read words and strings.
std::string input;
std::cout << "Enter a word: ";
std::cin >> input; // To read a single word (sequence of characters without spaces)
std::getline(std::cin, input); // To read a whole line (until a newline character is encountered. It discards the newline character and stores the entire line)
std::cout << "You entered: " << input << std::endl;Use the <filesystem> library to work with file and directory paths, including obtaining the current working directory.
#include <filesystem>
// Get the current working directory
std::filesystem::path currentPath = std::filesystem::current_path();Use the std::ifstream class from the <fstream> header to open and read from a file. Use std::getline() to read a line from the file stream.
#include <fstream>
// ...
std::string filename = "example.txt";
std::ifstream ifs(filename); // Create an ifstream object
// Check if the file is open, which indicates that it exists
if (ifs.is_open()) {
std::cout << "File exists." << std::endl;
// You can proceed to read from the file here if needed
std::string line;
while (std::getline(ifs, line)) {
if (line.find("somethingToSearch") != std::string::npos) {
// "somethingToSearch" has been found
}
}
ifs.close(); // Close the file after using it
} else {
std::cout << "File does not exist." << std::endl;
}CMake is a widely-used build system generator that simplifies the process of managing complex software builds. It allows you to define the structure and requirements of your project in a CMakeLists.txt file, which CMake then uses to generate platform-specific build files (e.g., Makefiles, Visual Studio project files, Xcode project files). It is especially useful for cross-platform development and large projects with multiple components.
When working with CMake, you'll often encounter the following concepts:
- CMakeLists.txt: The main configuration file where you define how your project is built. Each directory in your project should contain a
CMakeLists.txtfile. - Targets: Targets represent executables or libraries that are built by CMake. Targets are created using commands like
add_executable()oradd_library(). - Subdirectories: Large projects are often divided into subdirectories, and CMake allows you to manage these with
add_subdirectory(). This is essential for handling multi-component projects. - Include Directories: Directories containing header files that need to be included during compilation. These are added with
target_include_directories(). - Sources: The actual source files that will be compiled to create the targets. These are specified with
target_sources()or directly in theadd_executable()/add_library()commands. - Variables: CMake allows you to define variables (e.g., paths, flags) that can be used throughout the
CMakeLists.txtfiles. - Commands: CMake has a set of predefined commands (e.g.,
cmake_minimum_required(),project(),set(),add_subdirectory()) used to configure and control the build process.
A typical project structure using CMake might look like this:
root/
CMakeLists.txt
Graphs/
CMakeLists.txt
include/
include/
This file is the entry point for your CMake configuration. It typically contains project-wide settings and includes for subdirectories. Here’s an example of what the root CMakeLists.txt might look like:
cmake_minimum_required(VERSION 3.0.0)
project(thecherno VERSION 0.1.0)
# Set C++17 standard for the project
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
# Add subdirectory for the Graphs component
add_subdirectory(Graphs)- cmake_minimum_required(): Specifies the minimum version of CMake required for the project.
- project(): Defines the project’s name and version.
- set(): Used to define project-wide variables like compiler flags (
CMAKE_CXX_FLAGS). - add_subdirectory(): Adds subdirectories to the build, in this case,
Graphs.
This file is specific to the Graphs submodule and contains the instructions for how to build the Graphs executable or library. Here's an example:
cmake_minimum_required(VERSION 3.0.0)
project(Graphs VERSION 0.1.0)
# Add executable target called 'Graphs' using the Graphs.cpp source file
add_executable(Graphs Graphs.cpp)
# Optionally, add additional source files using target_sources()
# target_sources(Graphs PRIVATE log.cpp)
# Set up include directories for the Graphs target
target_include_directories(Graphs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(Graphs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include)- add_executable(): Defines an executable target (in this case,
Graphs), and specifies the source files (Graphs.cpp). - target_include_directories(): Specifies directories that contain header files to be included during compilation.
If you have multiple translation units (source files) or multiple entry points (main functions), you can define separate executables. Here’s an example:
cmake_minimum_required(VERSION 3.0.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
project(miketests VERSION 0.1.0)
# Define two executables, each with different source files
add_executable(main1 main.cpp anotherTransUnit.cpp)
add_executable(main2 anotherMain.cpp)In this example:
- add_executable(): Defines two separate executables (
main1andmain2), each with its own set of source files.
Here are a few more tools and concepts that you’ll often encounter when writing CMakeLists.txt files:
-
target_sources(): Adds sources to an existing target after it’s been created. This is useful when you want to separate different source files into different parts of your project.target_sources(Graphs PRIVATE log.cpp)
-
target_link_libraries(): Links external libraries to your target. This is important for linking third-party libraries or other targets in your project.target_link_libraries(Graphs PRIVATE SomeLibrary)
-
find_package(): Finds and configures external packages or libraries, such as Boost, OpenGL, etc.find_package(Boost REQUIRED)
-
install(): Specifies how to install your built project, including where to place binaries, libraries, and headers.install(TARGETS Graphs DESTINATION /usr/local/bin)
-
Variables and CMake Cache: You can define variables to control various aspects of your project build process. CMake also caches these values, allowing you to configure and modify them between builds.