Mastering CPP Pointers
Mastering CPP Pointers
Advanced Techniques
January 2025
Contents
Contents 2
Introduction 10
2
3
All praise is due to Allah, the Lord of all worlds, and may peace and blessings be upon the most
noble of messengers, our Prophet Muhammad, and upon his family and companions.
As for what follows,
The C++ programming language is one of the most powerful and flexible programming
languages, combining the strength of low-level programming with the ease of high-level
programming. Among the fundamental concepts that make C++ a unique and powerful
language is the concept of Pointers. Pointers are a powerful tool that gives programmers precise
control over memory management and direct access to system resources, making them essential
for building high-performance applications.
This book, ”Mastering C++ Pointers: From Fundamentals to Advanced Techniques”, is
designed to be a comprehensive guide for anyone who wants to master the use of pointers in
C++, starting from the basics and progressing to advanced techniques. Whether you are a
student at the beginning of your programming journey or a professional programmer seeking to
deepen your understanding, this book will be a valuable resource for you.
Why This Book?
Throughout my years of teaching and software development, I have noticed that pointers are one
of the most challenging concepts for programmers to understand and use correctly. However,
mastering them opens wide doors to a deeper understanding of how programs work and how to
optimize their performance. Therefore, I decided to provide you with a book that explains
pointers in a gradual manner, starting from the basics and moving to advanced techniques, with a
10
11
Stay Connected
For more discussions and valuable content about Modern C++ Pointers, I invite you to follow
me on LinkedIn:
https://linkedin.com/in/aymanalheraki
You can also visit my personal website:
https://simplifycpp.org
Ayman Alheraki
Chapter 1
13
14
To declare a pointer, you specify the data type it points to, followed by an asterisk (*), and then
the pointer's name. For example:
int* ptr;
Here, ptr is a pointer to an integer. The asterisk (*) indicates that ptr is a pointer, and int
specifies that it points to an integer type. The pointer itself does not store an integer value but
rather the address of an integer variable.
To initialize a pointer, you assign it the address of a variable using the address-of operator (&).
For example:
int x = 10;
int* ptr = &x;
In this example, ptr now holds the memory address of the variable x. The address-of operator
(&) retrieves the location of x in memory, and this address is stored in ptr.
To access the value stored at the memory address held by the pointer, you use the dereference
operator (\*). For example:
Here, *ptr retrieves the value stored at the memory address pointed to by ptr, which is the
value of x (i.e., 10).
• The new operator is used to dynamically allocate memory, and the delete
operator is used to deallocate it. For example:
• Pointers allow direct access to memory, which can lead to more efficient code. For
example, passing large structures or arrays to functions using pointers avoids the
overhead of copying the entire data.
Here, the array is passed to the function as a pointer, avoiding the need to copy the
entire array.
16
struct Node {
int data;
std::unique_ptr<Node> next; // Modern C++: Using smart
,→ pointers
};
Here, the next pointer allows the creation of a chain of nodes, forming a linked list.
• For example, in embedded systems, pointers are used to access hardware registers
directly.
• In C++, arrays and strings are closely related to pointers. The name of an array is
essentially a pointer to its first element.
• For example:
17
6. Resource Management:
• Pointers are essential for managing resources such as files, network connections, and
dynamically allocated memory. They allow programs to control the lifetime of
resources explicitly.
• Pointers use the * and & operators for dereferencing and obtaining addresses,
respectively.
int x = 10;
int* ptr = &x; // Pointer to x
int value = *ptr; // Dereference ptr to get the value of x
• References use the & symbol in their declaration and do not require explicit
dereferencing.
int x = 10;
int& ref = x; // Reference to x
int value = ref; // Directly access the value of x through ref
18
2. Nullability:
• Pointers can be nullptr (or NULL in older C++ standards), meaning they do not
point to any valid memory location.
• References must always refer to a valid object and cannot be null. Attempting to
create a null reference results in a compilation error.
3. Reassignment:
• References cannot be reassigned after initialization; they always refer to the same
object.
4. Memory Management:
• References are safer in this regard, as they are automatically bound to valid objects
and do not require manual memory management.
5. Use Cases:
• Pointers are more flexible and are used in scenarios requiring dynamic memory
allocation, complex data structures, or low-level programming.
• References are often used for function parameters to avoid copying large objects and
to enable pass-by-reference semantics.
• While pointers can be challenging for beginners, they are a powerful tool when used
correctly. Understanding memory management and proper usage can mitigate risks
like memory leaks and dangling pointers.
• Although arrays and pointers are closely related, they are not identical. An array
name is a constant pointer to the first element, but it cannot be reassigned.
20
• While references and pointers share similarities, they serve different purposes.
References provide a safer and more intuitive way to alias variables, while pointers
offer greater flexibility and control.
• Pointers are still widely used in modern C++, especially in low-level programming
and legacy code. However, modern C++ encourages the use of smart pointers (e.g.,
std::unique ptr and std::shared ptr) to manage memory more safely.
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
• std::unique ptr: A smart pointer that owns and manages a single object. It
cannot be copied, ensuring unique ownership.
• std::weak ptr: A smart pointer that does not own the object but can observe it.
It is used to break circular references in std::shared ptr.
2. Move Semantics:
3. nullptr:
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
This section provides a thorough understanding of pointers in modern C++, covering their
definition, importance, differences from references, and common misconceptions. By mastering
these concepts, readers will be well-prepared to explore the advanced techniques and
applications of pointers in subsequent chapters.
24
• For example, pointers can be used to access specific memory locations, such as
hardware registers or memory-mapped I/O.
• Pointers are essential for implementing efficient and complex data structures, such as
linked lists, trees, graphs, and hash tables. These data structures rely on pointers to
connect nodes and manage relationships between elements.
• For example, a linked list node can be defined as:
struct Node {
int data;
std::unique_ptr<Node> next; // Modern C++: Using smart
,→ pointers
};
26
Here, the next pointer allows the creation of a chain of nodes, forming a linked list.
void printHello() {
std::cout << "Hello, World!" << std::endl;
}
void (*funcPtr)() = printHello; // Function pointer
funcPtr(); // Call the function through the pointer
• Many C libraries and APIs use pointers extensively. To interface with these libraries
in C++, pointers are often required.
1. Dynamic Arrays:
• Pointers are used to create and manage dynamic arrays, where the size of the array is
not known at compile time.
• For example:
• In modern C++, std::vector is often preferred over raw pointers for dynamic
arrays, as it provides automatic memory management and additional functionality.
• Pointers are essential for implementing polymorphism and runtime binding in C++.
By using base class pointers, you can achieve dynamic behavior through virtual
functions.
• For example:
class Base {
public:
28
3. Resource Management:
• Pointers are used to manage resources such as files, network connections, and
dynamically allocated memory. They allow programs to control the lifetime of
resources explicitly.
• In modern C++, smart pointers and RAII (Resource Acquisition Is Initialization) are
preferred for resource management, as they ensure automatic cleanup.
29
4. Data Structures:
• Pointers are used to implement complex data structures such as linked lists, trees,
graphs, and hash tables. These data structures rely on pointers to connect nodes and
manage relationships between elements.
• For example, a binary tree node can be defined as:
struct TreeNode {
int data;
std::unique_ptr<TreeNode> left; // Modern C++: Smart
,→ pointers
std::unique_ptr<TreeNode> right; // Modern C++: Smart
,→ pointers
};
• Pointers to functions are used to implement callbacks, event handlers, and strategy
patterns. This allows for dynamic behavior and code reuse.
• For example:
void greet() {
std::cout << "Hello!" << std::endl;
}
void farewell() {
std::cout << "Goodbye!" << std::endl;
}
void callFunction(void (*func)()) {
func(); // Call the function through the pointer
}
30
6. Low-Level Programming:
• Pointers are used in low-level programming tasks, such as interacting with hardware,
performing memory-mapped I/O, and implementing custom memory allocators.
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
• std::unique ptr: A smart pointer that owns and manages a single object. It
cannot be copied, ensuring unique ownership.
• std::weak ptr: A smart pointer that does not own the object but can observe it.
It is used to break circular references in std::shared ptr.
2. Move Semantics:
• For example:
3. nullptr:
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
• Pointers are used to implement custom memory allocators, which can optimize
memory usage for specific applications.
33
• For example, a custom allocator for a memory pool can be implemented using raw
pointers:
class MemoryPool {
public:
MemoryPool(size_t size) {
pool = static_cast<char*>(malloc(size));
}
˜MemoryPool() {
free(pool);
}
void* allocate(size_t size) {
void* block = pool + offset;
offset += size;
return block;
}
private:
char* pool;
size_t offset = 0;
};
• Pointers are often required when interfacing with legacy C code or libraries that use
raw pointers extensively.
• For example, a C++ program can use pointers to interact with a C library:
extern "C" {
void legacyFunction(int* ptr);
}
int main() {
34
3. Pointer Arithmetic:
• Pointer arithmetic allows for efficient manipulation of arrays and memory blocks. It
is often used in low-level programming and performance-critical applications.
• For example:
• Pointers to functions can be combined with function objects and lambdas to create
flexible and reusable code.
• For example:
This section provides a thorough understanding of why pointers are used in C++, covering their
benefits and common use cases. By mastering these concepts, readers will be well-prepared to
explore the advanced techniques and applications of pointers in subsequent chapters.
36
1. Stack Memory:
• Example:
void foo() {
int x = 10; // x is allocated on the stack
// x is automatically deallocated when foo() exits
}
2. Heap Memory:
void bar() {
int* ptr = new int(10); // ptr points to memory allocated on
,→ the heap
38
• Memory for local variables and function parameters is automatically allocated on the
stack when a function is called and deallocated when the function exits.
• This automatic management ensures that memory is efficiently reused and prevents
memory leaks for stack-allocated variables.
• Example:
void foo() {
int x = 10; // x is allocated on the stack
// x is automatically deallocated when foo() exits
}
• Memory on the heap must be explicitly allocated and deallocated by the programmer
using the new and delete operators.
39
• Allocation:
• Deallocation:
• Failure to deallocate memory can lead to memory leaks, where memory is no longer
accessible but remains allocated.
• RAII is a modern C++ programming technique that ties the lifetime of a resource
(e.g., memory, file handles) to the lifetime of an object. When the object goes out of
scope, its destructor is automatically called, ensuring that resources are properly
released.
• Example:
{
std::unique_ptr<int> ptr = std::make_unique<int>(10); //
,→ Allocate memory
// Memory is automatically deallocated when ptr goes out of
,→ scope
}
40
4. Smart Pointers:
• Smart pointers are a modern C++ feature that automates memory management for
dynamically allocated objects. They ensure that memory is deallocated when it is no
longer needed, preventing memory leaks and dangling pointers.
• Types of Smart Pointers:
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
– std::weak ptr: A smart pointer that does not own the object but can
observe it. It is used to break circular references in std::shared ptr.
• The new operator is used to allocate memory on the heap. It returns a pointer to the
allocated memory.
• Example:
• The delete operator is used to deallocate memory that was allocated with new.
Failure to deallocate memory can lead to memory leaks.
• Example:
3. Common Pitfalls:
• In modern C++, smart pointers and RAII are preferred over raw pointers and manual
memory management. They provide automatic memory management and help avoid
common pitfalls.
• Example:
{
std::unique_ptr<int> ptr = std::make_unique<int>(10); //
,→ Allocate memory
// Memory is automatically deallocated when ptr goes out of
,→ scope
}
class CustomAllocator {
public:
void* allocate(size_t size) {
return malloc(size);
}
void deallocate(void* ptr) {
free(ptr);
}
};
43
2. Memory Pools:
• Memory pools are a technique where a large block of memory is allocated upfront
and then divided into smaller chunks as needed. This reduces the overhead of
frequent memory allocations and deallocations.
• Example:
class MemoryPool {
public:
MemoryPool(size_t size) {
pool = static_cast<char*>(malloc(size));
}
˜MemoryPool() {
free(pool);
}
void* allocate(size_t size) {
void* block = pool + offset;
offset += size;
return block;
}
private:
char* pool;
size_t offset = 0;
};
3. Placement New:
• Example:
char buffer[sizeof(int)];
int* ptr = new (buffer) int(10); // Construct an integer in the
,→ buffer
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
– std::weak ptr: A smart pointer that does not own the object but can
observe it. It is used to break circular references in std::shared ptr.
2. Move Semantics:
• For example:
3. nullptr:
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
2. Use RAII:
• Implement RAII by tying resource management to object lifetimes. This ensures that
resources are properly released when objects go out of scope.
47
• Minimize the use of raw pointers and manual memory management (new and
delete). Instead, rely on smart pointers and standard library containers.
• Always check for null pointers before dereferencing them to avoid runtime errors.
• Ensure that pointers are not used after the memory they point to has been
deallocated. Smart pointers can help prevent this issue.
6. Use nullptr:
This section provides a thorough understanding of memory management in C++, covering the
stack and heap, dynamic memory allocation, and modern C++ techniques like smart pointers
and RAII. By mastering these concepts, readers will be well-prepared to explore the advanced
techniques and applications of pointers in subsequent chapters.
Chapter 2
A pointer is a variable that stores the memory address of another variable. To declare a pointer,
you specify the data type it points to, followed by an asterisk (*), and then the pointer's name.
The general syntax for declaring a pointer is:
48
49
data_type* pointer_name;
Here, data type is the type of data the pointer will point to (e.g., int, char, double), and
pointer name is the name of the pointer variable.
Examples:
int* ptr;
char* chPtr;
double* dblPtr;
1. Null Pointers:
• A null pointer is a pointer that does not point to any valid memory location. In
modern C++, the preferred way to represent a null pointer is by using nullptr.
50
• Example:
• Using nullptr is safer and more expressive than using NULL or 0, as it avoids
potential ambiguities and type-related issues.
int x = 10;
int* ptr = &x; // ptr points to the memory address of x
• Pointers can be initialized by dynamically allocating memory using the new operator.
This is useful when the size of the data structure is not known at compile time.
• Example:
2.1.3 Example: Declaring and Using Pointers to int, char, and double
Below are examples of declaring and using pointers to different data types, incorporating
modern C++ concepts.
51
1. Pointer to int:
int x = 10;
int* ptr = &x; // ptr points to the memory address of x
std::cout << "Value of x: " << *ptr << std::endl; // Output: 10
2. Pointer to char:
char ch = 'A';
char* chPtr = &ch; // chPtr points to the memory address of ch
std::cout << "Value of ch: " << *chPtr << std::endl; // Output: A
3. Pointer to double:
double d = 3.14;
double* dblPtr = &d; // dblPtr points to the memory address of d
std::cout << "Value of d: " << *dblPtr << std::endl; // Output:
,→ 3.14
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
53
– std::weak ptr: A smart pointer that does not own the object but can
observe it. It is used to break circular references in std::shared ptr.
2. nullptr:
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
54
2. Use nullptr:
3. Initialize Pointers:
• Always initialize pointers when they are declared. Uninitialized pointers can lead to
undefined behavior.
• Always check for null pointers before dereferencing them to avoid runtime errors.
• Minimize the use of raw pointers and manual memory management (new and
delete). Instead, rely on smart pointers and standard library containers.
• Pointer arithmetic allows for efficient manipulation of arrays and memory blocks. It
is often used in low-level programming and performance-critical applications.
• Example:
2. Function Pointers:
56
• Pointers to functions are used to implement callbacks, event handlers, and strategy
patterns. This allows for dynamic behavior and code reuse.
• Example:
void greet() {
std::cout << "Hello!" << std::endl;
}
void farewell() {
std::cout << "Goodbye!" << std::endl;
}
void callFunction(void (*func)()) {
func(); // Call the function through the pointer
}
callFunction(greet); // Calls greet()
callFunction(farewell); // Calls farewell()
class CustomAllocator {
public:
void* allocate(size_t size) {
return malloc(size);
}
void deallocate(void* ptr) {
free(ptr);
}
};
57
4. Memory Pools:
• Memory pools are a technique where a large block of memory is allocated upfront
and then divided into smaller chunks as needed. This reduces the overhead of
frequent memory allocations and deallocations.
• Example:
class MemoryPool {
public:
MemoryPool(size_t size) {
pool = static_cast<char*>(malloc(size));
}
˜MemoryPool() {
free(pool);
}
void* allocate(size_t size) {
void* block = pool + offset;
offset += size;
return block;
}
private:
char* pool;
size_t offset = 0;
};
5. Placement New:
char buffer[sizeof(int)];
int* ptr = new (buffer) int(10); // Construct an integer in the
,→ buffer
This section provides a thorough understanding of declaring and initializing pointers in C++,
covering the syntax, initialization techniques, and modern C++ concepts like smart pointers and
nullptr. By mastering these concepts, readers will be well-prepared to explore the advanced
techniques and applications of pointers in subsequent chapters.
59
• When you add an integer to a pointer, the pointer is incremented by the number of
elements (not bytes) corresponding to the integer value.
• Example:
• When you subtract an integer from a pointer, the pointer is decremented by the
number of elements (not bytes) corresponding to the integer value.
• Example:
• You can also use the increment (++) and decrement (--) operators to move a pointer
to the next or previous element in an array.
• Example:
1. Pointer Differences:
61
• The difference between two pointers of the same type is the number of elements
between them. This is useful for determining the distance between two elements in
an array.
• Example:
2. Pointer Comparisons:
• You can compare pointers using relational operators (<, >, <=, >=, ==, !=) to
determine their relative positions in memory.
• Example:
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // ptr points to the first element of arr
return 0;
}
Output:
Element 0: 10
Element 1: 20
Element 2: 30
Element 3: 40
Element 4: 50
1. Smart Pointers:
63
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
• Types of Smart Pointers:
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
4. nullptr:
• Always ensure that pointer arithmetic does not result in out-of-bounds access, which
can lead to undefined behavior.
• Always check for null pointers before performing arithmetic operations to avoid
runtime errors.
• Use range-based for loops to simplify iteration over arrays and containers, reducing
the need for manual pointer arithmetic.
• Pointer arithmetic can also be used with multidimensional arrays. For example, you
can traverse a 2D array using pointer arithmetic.
• Example:
• Pointer arithmetic is often used with dynamically allocated arrays to traverse and
manipulate elements.
66
• Example:
• Pointer arithmetic can be used to traverse and manipulate custom data structures,
such as linked lists and trees.
• Example:
struct Node {
int data;
Node* next;
};
Node* head = new Node{10, new Node{20, new Node{30, nullptr}}};
Node* ptr = head;
while (ptr != nullptr) {
std::cout << ptr->data << " ";
ptr = ptr->next;
}
• Pointer arithmetic can also be applied to function pointers, allowing for dynamic
function calls.
67
• Example:
This section provides a thorough understanding of pointer arithmetic in C++, covering the
addition and subtraction of integers from pointers, pointer differences and comparisons, and an
example of traversing an array using pointer arithmetic. By mastering these concepts, readers
will be well-prepared to explore the advanced techniques and applications of pointers in
subsequent chapters.
68
1. Accessing Values:
• To access the value stored at the memory address held by a pointer, use the
dereference operator (*).
• Example:
int x = 10;
int* ptr = &x; // ptr points to the memory address of x
int value = *ptr; // Access the value stored at the address held
,→ by ptr
std::cout << "Value: " << value << std::endl; // Output: 10
2. Modifying Values:
69
• To modify the value stored at the memory address held by a pointer, use the
dereference operator (*) on the left-hand side of an assignment.
• Example:
int x = 10;
int* ptr = &x; // ptr points to the memory address of x
*ptr = 20; // Modify the value stored at the address held by ptr
std::cout << "New value of x: " << x << std::endl; // Output: 20
#include <iostream>
int main() {
int x = 10, y = 20;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
return 0;
}
Output:
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
2. References:
• References are an alternative to pointers that provide a safer and more intuitive way
to alias variables. They must be initialized when declared and cannot be reassigned.
• Example:
int x = 10;
int& ref = x; // ref is a reference to x
ref = 20; // Modify the value of x through ref
std::cout << "Value of x: " << x << std::endl; // Output: 20
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
• Always check for null pointers before dereferencing them to avoid runtime errors.
• Example:
• Prefer using references over pointers when you need to alias a variable. References
are safer and more intuitive.
• Ensure that pointers are not used after the memory they point to has been
deallocated. Smart pointers can help prevent this issue.
• A pointer to a pointer allows you to indirectly access and modify the value of a
pointer. This is useful in scenarios where you need to modify the pointer itself.
• Example:
74
int x = 10;
int* ptr = &x; // ptr points to the memory address of x
int** ptrToPtr = &ptr; // ptrToPtr points to the memory address
,→ of ptr
**ptrToPtr = 20; // Modify the value of x through ptrToPtr
std::cout << "Value of x: " << x << std::endl; // Output: 20
2. Function Pointers:
• Function pointers allow you to store and call functions dynamically. They are useful
for implementing callbacks and strategy patterns.
• Example:
void greet() {
std::cout << "Hello!" << std::endl;
}
void farewell() {
std::cout << "Goodbye!" << std::endl;
}
void callFunction(void (*func)()) {
func(); // Call the function through the pointer
}
callFunction(greet); // Calls greet()
callFunction(farewell); // Calls farewell()
• Example:
• Pointers are often used to pass large data structures to functions by reference,
avoiding the overhead of copying the data.
• Many C libraries and APIs use pointers extensively. To interface with these libraries
in C++, pointers and dereferencing are often required.
• Example:
class CustomAllocator {
public:
void* allocate(size_t size) {
return malloc(size);
}
void deallocate(void* ptr) {
free(ptr);
}
};
2. Memory Pools:
• Memory pools are a technique where a large block of memory is allocated upfront
and then divided into smaller chunks as needed. This reduces the overhead of
frequent memory allocations and deallocations.
• Example:
77
class MemoryPool {
public:
MemoryPool(size_t size) {
pool = static_cast<char*>(malloc(size));
}
˜MemoryPool() {
free(pool);
}
void* allocate(size_t size) {
void* block = pool + offset;
offset += size;
return block;
}
private:
char* pool;
size_t offset = 0;
};
3. Placement New:
char buffer[sizeof(int)];
int* ptr = new (buffer) int(10); // Construct an integer in the
,→ buffer
pointers, and modern C++ concepts like smart pointers and references. By mastering these
concepts, readers will be well-prepared to explore the advanced techniques and applications of
pointers in subsequent chapters.
79
• The name of an array is a constant pointer to the first element of the array.
• Example:
• You can use pointer arithmetic to access elements of an array. Adding an integer to a
pointer moves it to the corresponding element in the array.
80
• Example:
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // ptr points to the first element of arr
return 0;
}
Output:
Element 0: 10
Element 1: 20
Element 2: 30
Element 3: 40
Element 4: 50
1. 2D Arrays:
• A 2D array is an array of arrays. The name of a 2D array is a pointer to its first row.
• Example:
• You can use pointer arithmetic to access elements in a 2D array. The formula *(ptr
+ i * cols + j) is used to access the element at row i and column j.
• Example:
#include <iostream>
int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int* ptr = &arr[0][0]; // ptr points to the first element of the 2D
,→ array
return 0;
}
83
Output:
1 2 3
4 5 6
7 8 9
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
• Range-based for loops simplify iteration over arrays and containers, reducing the
need for raw pointers in many cases.
4. nullptr:
• Always check for null pointers before dereferencing them to avoid runtime errors.
• Always ensure that pointer arithmetic does not result in out-of-bounds access, which
can lead to undefined behavior.
• Use range-based for loops to simplify iteration over arrays and containers, reducing
the need for manual pointer arithmetic.
86
• A pointer to a pointer allows you to indirectly access and modify the value of a
pointer. This is useful in scenarios where you need to modify the pointer itself.
• Example:
int x = 10;
int* ptr = &x; // ptr points to the memory address of x
int** ptrToPtr = &ptr; // ptrToPtr points to the memory address
,→ of ptr
**ptrToPtr = 20; // Modify the value of x through ptrToPtr
std::cout << "Value of x: " << x << std::endl; // Output: 20
2. Function Pointers:
• Function pointers allow you to store and call functions dynamically. They are useful
for implementing callbacks and strategy patterns.
• Example:
void greet() {
std::cout << "Hello!" << std::endl;
}
void farewell() {
std::cout << "Goodbye!" << std::endl;
}
void callFunction(void (*func)()) {
func(); // Call the function through the pointer
}
87
• Example:
• Pointers and arrays are essential for managing dynamically allocated memory, such
as creating and manipulating dynamic arrays, linked lists, and trees.
• Pointers are often used to pass large data structures to functions by reference,
avoiding the overhead of copying the data.
• Pointers and arrays are crucial for implementing custom data structures, such as
linked lists, trees, graphs, and hash tables.
• Many C libraries and APIs use pointers and arrays extensively. To interface with
these libraries in C++, pointers and arrays are often required.
class CustomAllocator {
public:
void* allocate(size_t size) {
return malloc(size);
}
void deallocate(void* ptr) {
free(ptr);
}
};
89
2. Memory Pools:
• Memory pools are a technique where a large block of memory is allocated upfront
and then divided into smaller chunks as needed. This reduces the overhead of
frequent memory allocations and deallocations.
• Example:
class MemoryPool {
public:
MemoryPool(size_t size) {
pool = static_cast<char*>(malloc(size));
}
˜MemoryPool() {
free(pool);
}
void* allocate(size_t size) {
void* block = pool + offset;
offset += size;
return block;
}
private:
char* pool;
size_t offset = 0;
};
3. Placement New:
char buffer[sizeof(int)];
int* ptr = new (buffer) int(10); // Construct an integer in the
,→ buffer
• You can dynamically allocate a 2D array using pointers to pointers. This allows for
flexible array sizes and efficient memory management.
• Example:
2. Flattened 2D Arrays:
• You can represent a 2D array as a 1D array and use pointer arithmetic to access
elements. This approach is often used for performance optimization.
91
• Example:
• Example:
• Multi-dimensional arrays are used to represent and manipulate images, where each
element corresponds to a pixel.
2. Scientific Computing:
92
3. Game Development:
4. Machine Learning:
This section provides a thorough understanding of pointers and arrays in C++, covering the
relationship between pointers and arrays, accessing array elements using pointers,
multi-dimensional arrays and pointers, and traversing a 2D array with pointers. By mastering
these concepts, readers will be well-prepared to explore the advanced techniques and
applications of pointers in subsequent chapters.
Chapter 3
93
94
• By passing a pointer to a variable, you can modify the original variable within the
function.
• Example:
int main() {
int x = 10;
increment(&x); // Pass the address of x
std::cout << "x after increment: " << x << std::endl; //
,→ Output: 11
return 0;
}
• Arrays are passed to functions as pointers to their first element. This allows you to
work with arrays efficiently without copying them.
• Example:
int main() {
95
• You can return a pointer to a dynamically allocated array from a function. The caller
is responsible for deallocating the memory.
• Example:
int main() {
int* arr = createArray(5); // Call the function
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " "; // Output: 1 2 3 4 5
}
96
• Example:
int main() {
auto arr = createArray(5); // Call the function
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " "; // Output: 1 2 3 4 5
}
return 0;
}
97
void greet() {
std::cout << "Hello!" << std::endl;
}
void farewell() {
std::cout << "Goodbye!" << std::endl;
}
int main() {
void (*funcPtr)() = greet; // funcPtr points to greet
funcPtr(); // Calls greet()
funcPtr = farewell; // funcPtr now points to farewell
funcPtr(); // Calls farewell()
return 0;
}
• Example:
int main() {
process(10, 20, add); // Output: Result: 30
process(10, 20, multiply); // Output: Result: 200
return 0;
}
• In modern C++, lambda expressions can be used to create anonymous functions and
assign them to function pointers.
• Example:
int main() {
auto lambda = [](int a, int b) { return a + b; };
int (*funcPtr)(int, int) = lambda; // Assign lambda to
,→ function pointer
std::cout << funcPtr(10, 20) << std::endl; // Output: 30
99
return 0;
}
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
• Types of Smart Pointers:
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
2. Lambda Expressions:
100
• Lambda expressions allow you to create anonymous functions, which can be used
with function pointers or passed as arguments to functions.
• Example:
3. std::function:
• The std::function class template provides a type-safe way to store and call
functions, including function pointers, lambda expressions, and function objects.
• Example:
#include <functional>
int main() {
process(10, 20, [](int a, int b) { return a + b; }); //
,→ Output: Result: 30
return 0;
}
• Never return pointers to local variables, as they go out of scope when the function
exits. Instead, return dynamically allocated memory or use smart pointers.
• Always check for null pointers before dereferencing them to avoid runtime errors.
• A pointer to a pointer allows you to indirectly access and modify the value of a
pointer. This is useful in scenarios where you need to modify the pointer itself.
• Example:
102
int main() {
int* ptr = nullptr;
allocateMemory(&ptr); // Pass the address of the pointer
std::cout << *ptr << std::endl; // Output: 10
delete ptr; // Deallocate memory
return 0;
}
• Function pointers can be stored in data structures, such as arrays or vectors, to create
flexible and dynamic systems.
• Example:
#include <vector>
void greet() {
std::cout << "Hello!" << std::endl;
}
void farewell() {
std::cout << "Goodbye!" << std::endl;
}
int main() {
std::vector<void(*)()> functions = {greet, farewell};
103
• The std::bind function can be used to create function objects with bound
arguments, which can then be assigned to function pointers or passed as callbacks.
• Example:
#include <functional>
int main() {
auto boundFunc = std::bind(printSum, 10,
,→ std::placeholders::_1);
boundFunc(20); // Output: Sum: 30
return 0;
}
2. Callback Mechanisms:
3. Plugin Architectures:
• Function pointers are used in plugin architectures to dynamically load and call
functions from shared libraries or modules.
• Advanced pointer techniques are crucial for implementing custom data structures,
such as linked lists, trees, graphs, and hash tables.
• Many C libraries and APIs use function pointers and raw pointers extensively. To
interface with these libraries in C++, advanced pointer techniques are often required.
• You can dynamically allocate a 2D array using pointers to pointers. This allows for
flexible array sizes and efficient memory management.
105
• Example:
2. Flattened 2D Arrays:
• You can represent a 2D array as a 1D array and use pointer arithmetic to access
elements. This approach is often used for performance optimization.
• Example:
• Multi-dimensional arrays are used to represent and manipulate images, where each
element corresponds to a pixel.
2. Scientific Computing:
3. Game Development:
4. Machine Learning:
This section provides a thorough understanding of advanced pointer techniques in C++, covering
passing pointers as function arguments, returning pointers from functions, and function pointers.
By mastering these concepts, readers will be well-prepared to explore the advanced techniques
and applications of pointers in subsequent chapters.
108
• Example:
class MyClass {
public:
int data;
void display() {
std::cout << "Data: " << data << std::endl;
}
};
int main() {
MyClass obj;
MyClass* ptr = &obj; // ptr points to obj
109
• You can dynamically allocate objects using the new operator and access their
members via pointers.
• Example:
• Example:
#include <iostream>
#include <memory> // For smart pointers
class MyClass {
public:
int data;
void display() {
std::cout << "Data: " << data << std::endl;
}
};
int main() {
// Using raw pointers
MyClass* rawPtr = new MyClass();
rawPtr->data = 10;
rawPtr->display();
delete rawPtr;
return 0;
}
Output:
111
Data: 10
Data: 20
1. Explanation:
• The this pointer is implicitly passed to all non-static member functions and can be
used to access the object's members.
• It is particularly useful when a member function parameter has the same name as a
class member.
2. Use Cases:
class MyClass {
public:
int data;
void setData(int data) {
this->data = data; // Use this to disambiguate
}
};
112
class MyClass {
public:
MyClass* getThis() {
return this; // Return the current object
}
};
class MyClass {
public:
int data;
void setData(int data) {
this->data = data; // Use this to disambiguate
}
void display() {
std::cout << "Data: " << this->data << std::endl;
}
MyClass* getThis() {
return this; // Return the current object
}
};
int main() {
MyClass obj;
obj.setData(10);
obj.display(); // Output: Data: 10
113
return 0;
}
1. Smart Pointers:
• Smart pointers are a modern alternative to raw pointers that automatically manage
the lifetime of dynamically allocated memory. They help prevent memory leaks and
dangling pointers.
• Types of Smart Pointers:
– std::unique ptr: A smart pointer that owns and manages a single object.
It cannot be copied, ensuring unique ownership.
• Member initializer lists allow you to initialize class members directly in the
constructor, improving performance and readability.
• Example:
class MyClass {
public:
int data;
MyClass(int data) : data(data) {} // Member initializer list
void display() {
std::cout << "Data: " << data << std::endl;
}
};
3. Lambda Expressions:
class MyClass {
public:
void process(int x, int y, std::function<int(int, int)>
,→ callback) {
115
int main() {
MyClass obj;
obj.process(10, 20, [](int a, int b) { return a + b; }); //
,→ Output: Result: 30
return 0;
}
• Minimize the use of raw pointers and manual memory management (new and
delete). Instead, rely on smart pointers and standard library containers.
• Always check for null pointers before dereferencing them to avoid runtime errors.
• Use the this pointer to disambiguate member names and improve code readability,
especially when member function parameters have the same name as class members.
• A pointer to a pointer allows you to indirectly access and modify the value of a
pointer. This is useful in scenarios where you need to modify the pointer itself.
• Example:
int main() {
MyClass* ptr = nullptr;
allocateMemory(&ptr); // Pass the address of the pointer
ptr->data = 50;
ptr->display(); // Output: Data: 50
delete ptr; // Deallocate memory
return 0;
}
• Function pointers can be stored in classes to create flexible and dynamic systems.
• Example:
class MyClass {
public:
void (*funcPtr)();
void setFunction(void (*func)()) {
funcPtr = func;
}
void callFunction() {
funcPtr();
}
};
void greet() {
std::cout << "Hello!" << std::endl;
}
int main() {
MyClass obj;
obj.setFunction(greet);
obj.callFunction(); // Output: Hello!
return 0;
}
• The std::bind function can be used to create function objects with bound
arguments, which can then be assigned to function pointers or passed as callbacks.
• Example:
118
#include <functional>
class MyClass {
public:
void printSum(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
};
int main() {
MyClass obj;
auto boundFunc = std::bind(&MyClass::printSum, &obj, 10,
,→ std::placeholders::_1);
boundFunc(20); // Output: Sum: 30
return 0;
}
• Pointers to structures and classes are essential for managing dynamically allocated
memory, such as creating and manipulating dynamic arrays, linked lists, and trees.
2. Callback Mechanisms:
3. Plugin Architectures:
119
• Function pointers are used in plugin architectures to dynamically load and call
functions from shared libraries or modules.
• Pointers to structures and classes are crucial for implementing custom data
structures, such as linked lists, trees, graphs, and hash tables.
• Many C libraries and APIs use pointers and structures extensively. To interface with
these libraries in C++, pointers and structures/classes are often required.
• You can dynamically allocate a 2D array using pointers to pointers. This allows for
flexible array sizes and efficient memory management.
• Example:
}
delete[] arr;
2. Flattened 2D Arrays:
• You can represent a 2D array as a 1D array and use pointer arithmetic to access
elements. This approach is often used for performance optimization.
• Example:
• Example:
• Multi-dimensional arrays are used to represent and manipulate images, where each
element corresponds to a pixel.
2. Scientific Computing:
3. Game Development:
4. Machine Learning:
The new operator not only allocates memory but also initializes objects by calling their
constructors. For example:
class MyClass {
public:
MyClass() { std::cout << "Constructor called!" << std::endl; }
˜MyClass() { std::cout << "Destructor called!" << std::endl; }
};
MyClass* obj = new MyClass; // Allocates memory and calls the constructor
The delete operator calls the destructor of the object (if it exists) before deallocating the
memory. For example:
Let’s consider an example where we dynamically allocate an array of integers, populate it with
values, and then deallocate the memory:
#include <iostream>
int main() {
// Allocate memory for an array of 5 integers
int* arr = new int[5];
return 0;
}
In this example:
Memory Leaks
A memory leak occurs when dynamically allocated memory is not deallocated, leading to a
gradual increase in memory usage over time. This can happen if you forget to call delete or
delete[] after allocating memory with new.
void createMemoryLeak() {
int* ptr = new int; // Allocate memory
// Forget to delete ptr
}
In this example, the memory allocated for ptr is never deallocated, resulting in a memory leak.
Over time, such leaks can exhaust the available memory, causing the program to crash or behave
unpredictably.
Dangling Pointers
A dangling pointer is a pointer that points to memory that has already been deallocated.
Accessing or modifying memory through a dangling pointer leads to undefined behavior,
which can manifest as crashes, data corruption, or security vulnerabilities.
int* createDanglingPointer() {
int* ptr = new int; // Allocate memory
delete ptr; // Deallocate memory
return ptr; // Return a dangling pointer
}
In this example, ptr becomes a dangling pointer after the memory it points to is deallocated.
Accessing ptr after this point is unsafe.
3. Use smart pointers, which automatically manage memory and prevent dangling pointers.
#include <memory>
void noMemoryLeak() {
std::unique_ptr<int> ptr = std::make_unique<int>(42); //
,→ Automatically deallocated
}
In this example, std::unique ptr automatically deallocates the memory when it goes out
of scope, preventing a memory leak.
#include <memory>
void sharedOwnership() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // Both pointers share ownership
}
leaks.
#include <memory>
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Use weak_ptr to avoid circular
,→ references
};
void noCircularReference() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // No circular reference
}
3.3.5 Conclusion
Dynamic memory management is a cornerstone of C++ programming, enabling the creation of
flexible and efficient applications. However, it requires careful handling to avoid memory leaks,
dangling pointers, and other issues. By leveraging modern C++ techniques such as smart
pointers, RAII, and standard containers, you can write safer and more maintainable code.
Chapter 4
In modern C++, memory management is one of the most critical aspects of writing efficient,
safe, and maintainable code. Traditional raw pointers, while powerful, come with significant
risks, such as memory leaks, dangling pointers, and double deletions. These issues can lead to
undefined behavior, crashes, and security vulnerabilities. To address these challenges, C++11
introduced smart pointers, which are a cornerstone of modern C++ programming. Smart
pointers provide automatic memory management, ensuring that resources are properly
deallocated when they are no longer needed. This section introduces the concept of smart
pointers, explains why they are essential, explores the three main types of smart pointers in C++,
and demonstrates how to replace raw pointers with std::unique ptr through practical
examples.
130
131
1. Memory Leaks: If memory allocated on the heap is not explicitly deallocated using
delete, it remains occupied even after the program finishes using it. Over time, this can
exhaust available memory, leading to performance degradation or program crashes.
2. Dangling Pointers: When a pointer points to memory that has already been deallocated,
accessing or modifying it leads to undefined behavior. This can cause crashes, data
corruption, or security vulnerabilities.
3. Double Deletion: Accidentally deleting the same memory location twice can corrupt the
program's state and cause crashes. This often happens when multiple pointers point to the
same resource, and the programmer loses track of ownership.
4. Exception Safety: If an exception is thrown before delete is called, the memory may
never be released. This can lead to memory leaks in exception-prone code paths.
Smart pointers address these issues by wrapping raw pointers in objects that automatically
manage their lifetime. They ensure that memory is deallocated when it is no longer needed, even
in the presence of exceptions. This makes code safer, more robust, and easier to maintain. Smart
pointers also provide clear ownership semantics, making it easier to reason about resource
management in complex programs.
132
1. std::unique ptr
• Key Features:
• Use Cases:
• Example:
133
#include <memory>
#include <iostream>
void exampleUniquePtr() {
// Create a unique_ptr to manage an integer
std::unique_ptr<int> ptr(new int(42));
std::cout << *ptr << std::endl; // Access the value
• Advanced Usage:
– Custom Deleters: You can specify a custom deleter for unique ptr to
handle non-trivial cleanup.
2. std::shared ptr
std::shared ptr is a smart pointer that implements shared ownership. Multiple
shared ptr instances can point to the same resource, and the resource is deallocated
only when the last shared ptr pointing to it is destroyed or reset. This is achieved
using reference counting.
• Key Features:
– Shared Ownership: Multiple shared ptr instances can manage the same
resource.
– Reference Counting: Tracks the number of shared ptr instances pointing
to the resource.
– Thread Safety: Reference counting is thread-safe, but accessing the underlying
resource is not.
– Custom Deleters: Supports custom deleters for specialized cleanup.
– Slightly Higher Overhead: Due to reference counting, shared ptr has
more overhead than unique ptr.
• Use Cases:
• Example:
135
#include <memory>
#include <iostream>
void exampleSharedPtr() {
// Create a shared_ptr to manage an integer
std::shared_ptr<int> ptr1(new int(42));
std::shared_ptr<int> ptr2 = ptr1; // Share ownership
std::cout << *ptr1 << " " << *ptr2 << std::endl; // Access
,→ the value
std::cout << "Use count: " << ptr1.use_count() << std::endl;
,→ // Prints 2
• Advanced Usage:
– Custom Deleters: You can specify a custom deleter for shared ptr.
3. std::weak ptr
• Key Features:
• Use Cases:
• Example:
137
#include <memory>
#include <iostream>
void exampleWeakPtr() {
// Create a shared_ptr to manage an integer
std::shared_ptr<int> sharedPtr(new int(42));
std::weak_ptr<int> weakPtr = sharedPtr; // Create a weak_ptr
• Advanced Usage:
– Circular Reference Example:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Use weak_ptr to break
,→ circular reference
};
node1->next = node2;
node2->prev = node1; // No circular reference
#include <iostream>
void rawPointerExample() {
int* rawPtr = new int(42); // Dynamically allocate memory
std::cout << *rawPtr << std::endl; // Access the value
delete rawPtr; // Manually deallocate memory
}
(b) Exception Unsafe: If an exception is thrown before delete, the memory is leaked.
(c) Manual Management: The programmer must manually track the lifetime of the
resource.
#include <memory>
#include <iostream>
void uniquePtrExample() {
std::unique_ptr<int> smartPtr(new int(42)); // Automatically
,→ manages memory
std::cout << *smartPtr << std::endl; // Access the value
} // Memory is automatically deallocated when smartPtr goes out of
,→ scope
(a) Automatic Cleanup: Memory is automatically deallocated when the unique ptr
goes out of scope.
(b) Exception Safety: Resources are released even if an exception is thrown.
(c) Clear Ownership: The ownership semantics are explicit, making the code easier to
understand.
4.1.4 Summary
Smart pointers are a fundamental tool in modern C++ for managing dynamic memory safely and
efficiently. They eliminate many of the risks associated with raw pointers, such as memory leaks
and dangling pointers, while providing clear ownership semantics. By understanding and using
std::unique ptr, std::shared ptr, and std::weak ptr, you can write robust,
exception-safe, and maintainable C++ code. In the following sections, we will dive deeper into
each type of smart pointer, exploring their implementation, best practices, and advanced use
cases. We will also discuss how to choose the right smart pointer for your specific needs and
how to avoid common pitfalls when using them.
140
Key Points:
1. Exclusive Ownership: Only one unique ptr can own a resource at a time.
2. Move-Only: A unique ptr cannot be copied, but it can be moved using std::move.
3. Automatic Cleanup: When the unique ptr goes out of scope, the resource it owns is
automatically deallocated.
Example:
#include <memory>
#include <iostream>
void ownershipSemanticsExample() {
std::unique_ptr<int> ptr1(new int(42)); // ptr1 owns the resource
std::cout << *ptr1 << std::endl; // Access the resource
if (!ptr1) {
std::cout << "ptr1 is now null" << std::endl;
}
std::cout << *ptr2 << std::endl; // ptr2 now owns the resource
} // ptr2 goes out of scope, memory is automatically freed
Key Points:
2. Automatic Cleanup: The array is automatically deallocated when the unique ptr goes
out of scope.
142
Example:
#include <memory>
#include <iostream>
void dynamicArrayExample() {
// Create a unique_ptr to manage a dynamic array of 5 integers
std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
Key Points:
3. Syntax: The custom deleter is passed as a second template argument and a constructor
argument.
#include <memory>
#include <iostream>
#include <cstdio> // For FILE and fopen/fclose
void fileHandleExample() {
// Custom deleter for FILE*
auto fileDeleter = [](FILE* file) {
if (file) {
std::cout << "Closing file" << std::endl;
fclose(file); // Close the file
}
};
if (filePtr) {
std::cout << "File opened successfully" << std::endl;
144
Explanation:
1. Custom Deleter: The lambda fileDeleter closes the file using fclose.
2. Unique Pointer: The unique ptr is declared with FILE* as the resource type and
decltype(fileDeleter) as the deleter type.
3. Automatic Cleanup: When filePtr goes out of scope, the custom deleter is invoked,
ensuring the file is closed.
Example:
#include <memory>
#include <iostream>
void customDeleterArrayExample() {
// Custom deleter for arrays
auto arrayDeleter = [](int* arr) {
145
Example:
#include <memory>
#include <iostream>
146
class Base {
public:
virtual void print() const {
std::cout << "Base class" << std::endl;
}
virtual ˜Base() = default;
};
void polymorphismExample() {
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
ptr->print(); // Calls Derived::print()
} // Derived object is automatically deleted
Example:
#include <memory>
#include <vector>
147
#include <iostream>
void stlContainerExample() {
std::vector<std::unique_ptr<int>> vec;
// Access elements
for (const auto& ptr : vec) {
std::cout << *ptr << " ";
}
std::cout << std::endl;
} // All elements are automatically deleted
Example:
#include <memory>
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mtx;
148
std::unique_ptr<int> sharedPtr;
void multithreadedExample() {
sharedPtr = std::make_unique<int>(42);
t1.join();
t2.join();
}
4.2.8 Summary
std::unique ptr is a powerful tool for managing dynamically allocated resources with
exclusive ownership. Its move-only semantics ensure clear ownership and prevent common
issues like memory leaks and double deletions. By using std::unique ptr, you can
manage single objects, dynamic arrays, and even non-memory resources like file handles.
Custom deleters further enhance its flexibility, allowing you to handle specialized cleanup tasks.
In this section, we explored:
• Using custom deleters for specialized cleanup, such as closing file handles.
• Advanced topics like polymorphism, STL container integration, and multithreaded usage.
150
Key Points:
1. Shared Ownership: Multiple shared ptr instances can manage the same resource.
2. Reference Counting: Tracks the number of shared ptr instances pointing to the
resource.
3. Automatic Cleanup: The resource is deallocated when the reference count drops to zero.
4. Thread Safety: Reference counting is thread-safe, but accessing the underlying resource
is not.
151
Example:
#include <memory>
#include <iostream>
void sharedOwnershipExample() {
// Create a shared_ptr to manage an integer
std::shared_ptr<int> ptr1(new int(42));
std::cout << "Use count: " << ptr1.use_count() << std::endl; // Prints
,→ 1
Example:
152
#include <memory>
#include <iostream>
#include <vector>
class Node {
public:
int value;
std::shared_ptr<Node> next;
void sharedResourceExample() {
// Create a shared_ptr to manage a Node
std::shared_ptr<Node> node1 = std::make_shared<Node>(10);
std::shared_ptr<Node> node2 = std::make_shared<Node>(20);
#include <memory>
#include <iostream>
class Node {
public:
std::shared_ptr<Node> next;
Node() { std::cout << "Node created" << std::endl; }
˜Node() { std::cout << "Node destroyed" << std::endl; }
};
void circularReferenceExample() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
std::cout << "Use count for node1: " << node1.use_count() << std::endl;
,→ // Prints 2
std::cout << "Use count for node2: " << node2.use_count() << std::endl;
,→ // Prints 2
} // node1 and node2 are not deleted due to circular reference
Key Points:
1. Non-Owning Reference: weak ptr does not affect the reference count.
2. Convertible to shared ptr: You can convert a weak ptr to a shared ptr to
access the resource.
Example:
#include <memory>
#include <iostream>
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Use weak_ptr to break circular reference
void weakPtrExample() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
std::cout << "Use count for node1: " << node1.use_count() << std::endl;
,→ // Prints 1
std::cout << "Use count for node2: " << node2.use_count() << std::endl;
,→ // Prints 1
} // node1 and node2 are automatically deleted
Explanation:
1. Circular Reference Broken: By using weak ptr for the prev pointer, the circular
reference is broken.
2. Automatic Cleanup: When node1 and node2 go out of scope, they are automatically
deleted because the reference count drops to zero.
Example:
#include <memory>
#include <iostream>
void customDeleterExample() {
auto deleter = [](int* p) {
std::cout << "Custom deleter called" << std::endl;
delete p;
};
Example:
#include <memory>
#include <iostream>
struct Foo {
int value;
Foo(int val) : value(val) {}
};
void aliasingConstructorExample() {
std::shared_ptr<Foo> fooPtr = std::make_shared<Foo>(42);
std::shared_ptr<int> aliasPtr(fooPtr, &fooPtr->value);
std::cout << "Foo value: " << *aliasPtr << std::endl; // Prints 42
} // fooPtr and aliasPtr are automatically deleted
Example:
#include <memory>
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mtx;
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
void multithreadedExample() {
std::thread t1(threadFunction, 1);
std::thread t2(threadFunction, 2);
t1.join();
t2.join();
}
Example:
#include <memory>
#include <iostream>
#include <unordered_map>
class Resource {
public:
Resource(int id) : id(id) { std::cout << "Resource " << id << "
,→ created" << std::endl; }
˜Resource() { std::cout << "Resource " << id << " destroyed" <<
,→ std::endl; }
int id;
};
class Cache {
public:
std::shared_ptr<Resource> getResource(int id) {
auto it = cache.find(id);
if (it != cache.end()) {
if (auto resource = it->second.lock()) {
return resource; // Return shared_ptr if resource exists
}
}
private:
std::unordered_map<int, std::weak_ptr<Resource>> cache;
};
void cachingExample() {
Cache cache;
{
auto resource1 = cache.getResource(1);
std::cout << "Resource 1 use count: " << resource1.use_count() <<
,→ std::endl; // Prints 1
}
{
auto resource1 = cache.getResource(1);
std::cout << "Resource 1 use count: " << resource1.use_count() <<
,→ std::endl; // Prints 1
}
} // Resource 1 is automatically deleted
4.3.9 Summary
std::shared ptr is a powerful tool for managing resources with shared ownership. It
allows multiple objects to share access to the same resource and ensures that the resource is
deallocated when no longer needed. However, shared ownership can lead to circular references,
which can be resolved using std::weak ptr. By understanding and using
std::shared ptr and std::weak ptr, you can write robust, memory-safe, and
maintainable C++ code.
In this section, we explored:
160
• Example:
• Prefer smart pointers over raw pointers to ensure automatic memory management.
• If you must use raw pointers, ensure they are non-owning and do not manage
memory.
• Prefer std::make unique and std::make shared over direct calls to new.
• These functions provide exception safety and reduce code verbosity.
163
• Example:
6. Be Mindful of Performance
• std::shared ptr has higher overhead due to reference counting. Use it only
when shared ownership is necessary.
• std::unique ptr is lightweight and efficient, making it suitable for most use
cases.
• Mixing smart pointers and raw pointers can lead to ownership ambiguity and bugs.
• Ensure that all pointers to a resource are either smart pointers or non-owning raw
pointers.
• Use custom deleters to manage resources like file handles, sockets, or mutexes.
• Example:
• Example:
std::unique_ptr<int> createResource() {
return std::make_unique<int>(42);
}
#include <memory>
#include <iostream>
class Node {
165
public:
int value;
std::shared_ptr<Node> next;
class LinkedList {
public:
std::shared_ptr<Node> head;
};
void singlyLinkedListExample() {
LinkedList list;
list.append(10);
list.append(20);
list.append(30);
list.print(); // Prints: 10 20 30
} // All nodes are automatically deleted
#include <memory>
#include <iostream>
class Node {
public:
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Use weak_ptr to avoid circular
,→ references
class DoublyLinkedList {
public:
std::shared_ptr<Node> head;
167
while (current) {
std::cout << current->value << " ";
168
void doublyLinkedListExample() {
DoublyLinkedList list;
list.append(10);
list.append(20);
list.append(30);
list.print(); // Prints: 10 20 30
list.printReverse(); // Prints: 30 20 10
} // All nodes are automatically deleted
• Smart pointers can be used with STL containers like std::vector, std::map,
and std::list.
• Example:
#include <memory>
#include <vector>
169
#include <iostream>
void stlContainerExample() {
std::vector<std::shared_ptr<int>> vec;
vec.push_back(std::make_shared<int>(10));
vec.push_back(std::make_shared<int>(20));
vec.push_back(std::make_shared<int>(30));
#include <memory>
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mtx;
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
170
void multithreadedExample() {
std::thread t1(threadFunction, 1);
std::thread t2(threadFunction, 2);
t1.join();
t2.join();
}
#include <memory>
#include <iostream>
#include <cstdio>
void customDeleterExample() {
auto deleter = [](FILE* file) {
std::cout << "Closing file" << std::endl;
fclose(file);
};
171
std::unique_ptr<FILE, decltype(deleter)>
,→ filePtr(fopen("example.txt", "w"), deleter);
if (filePtr) {
std::cout << "File opened successfully" << std::endl;
fprintf(filePtr.get(), "Hello, World!");
}
} // File is automatically closed
4.4.4 Summary
Smart pointers are a powerful tool for managing dynamic memory in modern C++. By following
best practices and understanding their use cases, you can write safer, more robust, and
maintainable code. In this section, we explored:
• Best practices for using smart pointers, including when to use std::unique ptr,
std::shared ptr, and std::weak ptr.
• A doubly linked list implementation using std::shared ptr and std::weak ptr
to avoid circular references.
• Advanced topics such as integrating smart pointers with STL containers, using them in
multithreaded environments, and leveraging custom deleters for specialized resource
management.
Chapter 5
172
173
Key Points:
1. Base Class Pointer: A pointer of the base class type can point to an object of the derived
class.
2. Derived Class Pointer: A pointer of the derived class type can only point to objects of the
derived class or its further derived classes.
3. Upcasting: Converting a derived class pointer to a base class pointer is called upcasting.
It is implicit and safe.
Example:
#include <iostream>
class Base {
public:
void show() {
std::cout << "Base class show()" << std::endl;
}
};
};
void baseAndDerivedPointersExample() {
Base* basePtr; // Base class pointer
Derived derivedObj; // Derived class object
Key Points:
1. Virtual Functions: Declare a function as virtual in the base class to enable runtime
polymorphism.
2. Override: Use the override keyword in the derived class to explicitly indicate that a
function is overriding a base class virtual function.
3. Dynamic Binding: The function call is resolved at runtime based on the actual object
type, not the pointer type.
175
Example:
#include <iostream>
class Base {
public:
virtual void show() {
std::cout << "Base class show()" << std::endl;
}
};
void runtimePolymorphismExample() {
Base* basePtr; // Base class pointer
Derived derivedObj; // Derived class object
allows the correct function to be called at runtime based on the actual object type.
Key Points:
1. Vtable: Each class with virtual functions has a vtable, which contains pointers to its
virtual functions.
2. Vptr: Each object of a class with virtual functions contains a hidden pointer (vptr) to the
vtable.
3. Dynamic Dispatch: When a virtual function is called, the vptr is used to look up the
correct function in the vtable.
Example:
#include <iostream>
class Base {
public:
virtual void func1() {
std::cout << "Base::func1()" << std::endl;
}
virtual void func2() {
std::cout << "Base::func2()" << std::endl;
}
};
void vtableExample() {
Base* basePtr = new Derived(); // Base pointer points to Derived
,→ object
basePtr->func1(); // Calls Derived::func1()
basePtr->func2(); // Calls Derived::func2()
delete basePtr;
}
#include <iostream>
Base() {
// Initialize vptr to point to Base's vtable
vptr = &Base::vtable[0];
178
// Simulated vtable
static FuncPtr vtable[];
// Simulated vptr
FuncPtr* vptr;
};
// Simulated vtable
static FuncPtr vtable[];
};
void simulateVtableExample() {
Base* basePtr = new Derived(); // Base pointer points to Derived
,→ object
delete basePtr;
}
• In multiple inheritance, a class can inherit from more than one base class. This can
lead to complex vtable structures.
180
• Example:
#include <iostream>
class Base1 {
public:
virtual void func1() {
std::cout << "Base1::func1()" << std::endl;
}
};
class Base2 {
public:
virtual void func2() {
std::cout << "Base2::func2()" << std::endl;
}
};
void multipleInheritanceExample() {
Derived derivedObj;
Base1* base1Ptr = &derivedObj;
Base2* base2Ptr = &derivedObj;
181
2. Virtual Inheritance
#include <iostream>
class Base {
public:
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
};
};
void virtualInheritanceExample() {
FinalDerived finalDerivedObj;
Base* basePtr = &finalDerivedObj;
basePtr->func(); // Calls FinalDerived::func()
}
• Pointers are essential for achieving runtime polymorphism. They allow you to write
code that works with any derived class, providing flexibility and extensibility.
• Example:
#include <iostream>
#include <vector>
class Shape {
public:
virtual void draw() const = 0; // Pure virtual function
};
void polymorphismWithPointersExample() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Square());
5.1.6 Summary
Pointers and polymorphism are closely intertwined in C++. By using base class pointers and
virtual functions, you can achieve runtime polymorphism, enabling flexible and reusable code.
The vtable mechanism underpins this behavior, allowing the correct function to be called at
184
• Advanced topics such as multiple inheritance, virtual inheritance, and the role of pointers
in these scenarios.
185
Key Points:
1. Upcasting: Converting a derived class pointer to a base class pointer is called upcasting.
It is implicit and safe.
3. Accessing Derived Members: A base class pointer cannot directly access members of the
derived class. Downcasting is required to access derived class members.
Example:
186
#include <iostream>
class Base {
public:
void show() {
std::cout << "Base class show()" << std::endl;
}
};
void accessingDerivedObjectsExample() {
Base* basePtr; // Base class pointer
Derived derivedObj; // Derived class object
Key Points:
1. Type Safety: dynamic cast ensures that the cast is valid at runtime.
2. Runtime Check: If the cast is invalid, dynamic cast returns nullptr (for pointers)
or throws an exception (for references).
3. Requires Polymorphism: dynamic cast requires the base class to have at least one
virtual function (i.e., it must be polymorphic).
Example:
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual void show() {
std::cout << "Base class show()" << std::endl;
}
};
void dynamicCastingExample() {
Base* basePtr = new Derived(); // Base pointer points to Derived
,→ object
delete basePtr;
}
• When using dynamic cast, it is important to handle cases where the cast fails.
For pointers, dynamic cast returns nullptr if the cast is invalid.
• Example:
189
void handlingInvalidCastsExample() {
Base* basePtr = new Base(); // Base pointer points to Base
,→ object
delete basePtr;
}
#include <iostream>
#include <typeinfo>
void dynamicCastWithReferencesExample() {
Derived derivedObj;
Base& baseRef = derivedObj; // Base reference refers to
,→ Derived object
190
try {
Derived& derivedRef = dynamic_cast<Derived&>(baseRef);
derivedRef.show();
derivedRef.derivedFunction();
} catch (const std::bad_cast& e) {
std::cout << "Dynamic cast failed: " << e.what() <<
,→ std::endl;
}
}
• dynamic cast can also be used with multiple inheritance to safely cast between
base and derived class pointers.
• Example:
#include <iostream>
class Base1 {
public:
virtual void func1() {
std::cout << "Base1::func1()" << std::endl;
}
};
class Base2 {
public:
virtual void func2() {
std::cout << "Base2::func2()" << std::endl;
}
};
191
void multipleInheritanceDynamicCastExample() {
Derived derivedObj;
Base1* base1Ptr = &derivedObj;
• Example:
#include <iostream>
class Base {
public:
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
};
void virtualInheritanceDynamicCastExample() {
FinalDerived finalDerivedObj;
Base* basePtr = &finalDerivedObj;
• C-style casts (e.g., (Derived*)basePtr) are unsafe and should be avoided. Use
C++ casting operators like dynamic cast, static cast,
reinterpret cast, and const cast instead.
194
3. Prefer Polymorphism:
• Use virtual functions and polymorphism to avoid the need for downcasting whenever
possible.
4. Minimize Downcasting:
• Downcasting can indicate a design flaw. Consider refactoring your code to minimize
the need for downcasting.
5.2.5 Summary
Pointers and inheritance are closely intertwined in C++. By using base class pointers, you can
write flexible and reusable code that works with any derived class. However, accessing derived
class members through a base class pointer requires careful handling, particularly when
downcasting. The dynamic cast operator provides a safe and type-safe way to perform
downcasting, ensuring that your code is robust and maintainable.
In this section, we explored:
Key Points:
3. Polymorphism: Pointers to abstract classes can point to derived class objects, enabling
runtime polymorphism.
4. Interface Implementation: Abstract classes are often used to define interfaces that
derived classes must implement.
196
Example:
#include <iostream>
// Abstract class
class Shape {
public:
virtual void draw() const = 0; // Pure virtual function
virtual ˜Shape() = default; // Virtual destructor
};
// Derived class
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};
// Derived class
class Square : public Shape {
public:
void draw() const override {
std::cout << "Drawing a square" << std::endl;
}
};
void abstractClassPointersExample() {
Shape* shapePtr; // Pointer to abstract class
Circle circle;
Square square;
197
#include <iostream>
#include <vector>
// Derived class
class Circle : public Shape {
private:
double radius;
198
public:
Circle(double r) : radius(r) {}
// Derived class
class Square : public Shape {
private:
double side;
public:
Square(double s) : side(s) {}
void interfaceImplementationExample() {
std::vector<Shape*> shapes;
199
shapes.push_back(new Circle(5.0));
shapes.push_back(new Square(4.0));
• Smart pointers like std::unique ptr and std::shared ptr can be used
with abstract classes to manage dynamically allocated objects safely.
• Example:
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual void draw() const = 0;
virtual ˜Shape() = default;
200
};
void smartPointersWithAbstractClassesExample() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
• The factory pattern is a design pattern that uses abstract classes to create objects
without specifying the exact class of the object.
• Example:
#include <iostream>
#include <memory>
class Shape {
public:
virtual void draw() const = 0;
201
void factoryPatternExample() {
auto shape1 = createShape("circle");
auto shape2 = createShape("square");
if (shape1) shape1->draw();
if (shape2) shape2->draw();
202
• Abstract classes can be used in multiple inheritance to define interfaces that derived
classes must implement.
• Example:
#include <iostream>
class Drawable {
public:
virtual void draw() const = 0;
virtual ˜Drawable() = default;
};
class Scalable {
public:
virtual void scale(double factor) = 0;
virtual ˜Scalable() = default;
};
};
void multipleInheritanceWithAbstractClassesExample() {
Circle circle;
Drawable* drawablePtr = &circle;
Scalable* scalablePtr = &circle;
drawablePtr->draw();
scalablePtr->scale(2.0);
}
#include <iostream>
class Shape {
public:
virtual void draw() const = 0;
virtual ˜Shape() = default;
};
void virtualInheritanceWithAbstractClassesExample() {
Circle circle;
Shape* shapePtr = &circle;
Scalable* scalablePtr = &circle;
shapePtr->draw();
scalablePtr->scale(2.0);
}
• Abstract classes are ideal for defining interfaces that specify a set of methods that
derived classes must implement.
• Use smart pointers like std::unique ptr and std::shared ptr to manage
dynamically allocated objects safely.
• Prefer smart pointers over raw pointers to avoid memory leaks and ensure proper
resource management.
4. Minimize Downcasting:
• Downcasting can indicate a design flaw. Consider refactoring your code to minimize
the need for downcasting.
5.3.5 Summary
Pointers to abstract classes are a powerful tool for implementing polymorphism and defining
interfaces in C++. By using abstract classes, you can enforce specific behaviors in derived
classes and write flexible, reusable code. Smart pointers further enhance safety and
manageability when working with dynamically allocated objects.
In this section, we explored:
• Advanced topics such as smart pointers, the factory pattern, multiple inheritance, and
virtual inheritance with abstract classes.
Chapter 6
A singly linked list is a linear data structure where each node contains data and a pointer to the
next node. The last node points to nullptr, indicating the end of the list.
Key Points:
206
207
1. Node Structure: Each node contains data and a pointer to the next node.
3. Operations: Common operations include adding nodes, deleting nodes, and traversing the
list.
#include <iostream>
// Node structure
struct Node {
int data;
Node* next;
public:
SinglyLinkedList() : head(nullptr) {}
void singlyLinkedListExample() {
SinglyLinkedList list;
210
list.append(10);
list.append(20);
list.append(30);
list.prepend(5);
std::cout << "After prepending 5: ";
list.print();
list.deleteNode(20);
std::cout << "After deleting 20: ";
list.print();
list.deleteNode(5);
std::cout << "After deleting 5: ";
list.print();
list.deleteNode(30);
std::cout << "After deleting 30: ";
list.print();
}
1. Add Nodes: Use the append method to add nodes to the end of the list and the
prepend method to add nodes to the beginning.
3. Traverse the List: Use the print method to traverse and print the list.
211
Key Points:
1. Node Structure: Each node contains data, a pointer to the next node, and a pointer to the
previous node.
2. Head and Tail Pointers: A pointer to the first node (head) and a pointer to the last node
(tail).
3. Operations: Common operations include adding nodes, deleting nodes, and traversing the
list in both directions.
#include <iostream>
// Node structure
struct Node {
int data;
Node* prev;
Node* next;
Node* head;
Node* tail;
public:
DoublyLinkedList() : head(nullptr), tail(nullptr) {}
if (!head) return;
void doublyLinkedListExample() {
DoublyLinkedList list;
list.append(10);
list.append(20);
list.append(30);
list.prepend(5);
std::cout << "After prepending 5 (forward): ";
list.printForward();
list.deleteNode(20);
std::cout << "After deleting 20 (forward): ";
list.printForward();
list.deleteNode(5);
std::cout << "After deleting 5 (forward): ";
list.printForward();
216
list.deleteNode(30);
std::cout << "After deleting 30 (forward): ";
list.printForward();
}
1. Add Nodes: Use the append method to add nodes to the end of the list and the
prepend method to add nodes to the beginning.
• A circular linked list is a variation where the last node points back to the first node,
creating a loop.
• Example:
class CircularLinkedList {
private:
Node* head;
public:
217
CircularLinkedList() : head(nullptr) {}
˜CircularLinkedList() {
if (!head) return;
Node* next;
do {
next = current->next;
delete current;
current = next;
} while (current != head);
}
};
• Smart pointers like std::unique ptr can be used to manage memory in linked
lists, reducing the risk of memory leaks.
• Example:
struct Node {
int data;
std::unique_ptr<Node> next;
class SinglyLinkedList {
private:
std::unique_ptr<Node> head;
public:
void append(int val) {
auto newNode = std::make_unique<Node>(val);
if (!head) {
head = std::move(newNode);
219
} else {
Node* current = head.get();
while (current->next) {
current = current->next.get();
}
current->next = std::move(newNode);
}
}
• Tail Pointer: Maintaining a tail pointer can optimize appending nodes to the end of
the list.
• Size Tracking: Keeping track of the list size can optimize operations that depend on
the list length.
• Memory Pooling: Using a memory pool can reduce the overhead of frequent
memory allocations and deallocations.
220
6.1.4 Summary
Linked lists are a versatile and dynamic data structure that can be implemented using pointers in
C++. In this section, we explored:
• Implementing a singly linked list with operations like adding, deleting, and traversing
nodes.
• Advanced topics such as circular linked lists, using smart pointers for memory
management, and optimizing linked list operations.
221
Key Points:
1. Node Structure: Each node contains data and pointers to its left and right children.
#include <iostream>
// Node structure
struct TreeNode {
int data;
TreeNode* left;
222
TreeNode* right;
Key Points:
#include <iostream>
// Node structure
struct TreeNode {
int data;
TreeNode* left;
TreeNode* right;
public:
BinarySearchTree() : root(nullptr) {}
void binarySearchTreeExample() {
BinarySearchTree bst;
bst.insert(50);
bst.insert(30);
226
bst.insert(70);
bst.insert(20);
bst.insert(40);
bst.insert(60);
bst.insert(80);
std::cout << "Search for 40: " << (bst.search(40) ? "Found" : "Not
,→ Found") << std::endl;
std::cout << "Search for 90: " << (bst.search(90) ? "Found" : "Not
,→ Found") << std::endl;
bst.deleteValue(40);
std::cout << "Search for 40 after deletion: " << (bst.search(40) ?
,→ "Found" : "Not Found") << std::endl;
}
Key Points:
1. In-Order Traversal: Visit the left subtree, then the root, then the right subtree.
2. Pre-Order Traversal: Visit the root, then the left subtree, then the right subtree.
3. Post-Order Traversal: Visit the left subtree, then the right subtree, then the root.
#include <iostream>
// Node structure
struct TreeNode {
int data;
TreeNode* left;
TreeNode* right;
// In-order traversal
void inOrder(TreeNode* node) {
if (!node) return;
inOrder(node->left);
std::cout << node->data << " ";
inOrder(node->right);
}
// Pre-order traversal
void preOrder(TreeNode* node) {
if (!node) return;
std::cout << node->data << " ";
preOrder(node->left);
preOrder(node->right);
}
// Post-order traversal
void postOrder(TreeNode* node) {
if (!node) return;
postOrder(node->left);
postOrder(node->right);
228
void treeTraversalExample() {
TreeNode* root = new TreeNode(50);
root->left = new TreeNode(30);
root->right = new TreeNode(70);
root->left->left = new TreeNode(20);
root->left->right = new TreeNode(40);
root->right->left = new TreeNode(60);
root->right->right = new TreeNode(80);
• Balanced trees, such as AVL trees and Red-Black trees, maintain a balanced
229
• Example:
TreeNode* rotateRight(TreeNode* y) {
TreeNode* x = y->left;
TreeNode* T2 = x->right;
x->right = y;
y->left = T2;
return x;
}
TreeNode* rotateLeft(TreeNode* x) {
TreeNode* y = x->right;
230
TreeNode* T2 = y->left;
y->left = x;
x->right = T2;
return y;
}
return node;
}
public:
AVLTree() : root(nullptr) {}
2. Iterative Traversal
• Example:
232
#include <stack>
current = stack.top();
stack.pop();
std::cout << current->data << " ";
current = current->right;
}
}
• Smart pointers like std::unique ptr can be used to manage memory in trees,
reducing the risk of memory leaks.
• Example:
#include <memory>
struct TreeNode {
int data;
std::unique_ptr<TreeNode> left;
233
std::unique_ptr<TreeNode> right;
class BinarySearchTree {
private:
std::unique_ptr<TreeNode> root;
public:
BinarySearchTree() : root(nullptr) {}
};
6.2.5 Summary
Trees are hierarchical data structures that are widely used in computer science. In this section,
we explored:
• Implementing a binary search tree with insertion, deletion, and search operations.
• Advanced topics such as balanced trees, iterative traversal, and memory management with
smart pointers.
235
Key Points:
3. Adjacency List: A collection of lists, where each list corresponds to a vertex and contains
its adjacent vertices.
#include <iostream>
#include <vector>
// Vertex structure
struct Vertex {
int id;
std::vector<Vertex*> neighbors;
// Graph class
class Graph {
private:
std::vector<Vertex*> vertices;
public:
// Add a vertex to the graph
void addVertex(int id) {
vertices.push_back(new Vertex(id));
}
if (vertex->id == to) {
toVertex = vertex;
}
}
void adjacencyListExample() {
Graph graph;
238
graph.addVertex(1);
graph.addVertex(2);
graph.addVertex(3);
graph.addVertex(4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 4);
graph.print();
}
#include <iostream>
#include <vector>
#include <stack>
// Vertex structure
struct Vertex {
int id;
std::vector<Vertex*> neighbors;
bool visited;
// Graph class
class Graph {
private:
std::vector<Vertex*> vertices;
public:
// Add a vertex to the graph
void addVertex(int id) {
vertices.push_back(new Vertex(id));
}
fromVertex = vertex;
}
if (vertex->id == to) {
toVertex = vertex;
}
}
std::stack<Vertex*> stack;
stack.push(start);
while (!stack.empty()) {
Vertex* current = stack.top();
stack.pop();
if (!current->visited) {
std::cout << current->id << " ";
current->visited = true;
}
}
}
}
void dfsExample() {
Graph graph;
graph.addVertex(1);
graph.addVertex(2);
graph.addVertex(3);
graph.addVertex(4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 4);
242
graph.resetVisited();
}
• BFS explores all the neighbors of a vertex before moving on to their neighbors.
#include <iostream>
#include <vector>
#include <queue>
// Vertex structure
struct Vertex {
int id;
std::vector<Vertex*> neighbors;
bool visited;
// Graph class
class Graph {
private:
243
std::vector<Vertex*> vertices;
public:
// Add a vertex to the graph
void addVertex(int id) {
vertices.push_back(new Vertex(id));
}
std::queue<Vertex*> queue;
queue.push(start);
start->visited = true;
while (!queue.empty()) {
Vertex* current = queue.front();
queue.pop();
}
};
void bfsExample() {
Graph graph;
graph.addVertex(1);
graph.addVertex(2);
graph.addVertex(3);
graph.addVertex(4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 4);
graph.resetVisited();
}
• In weighted graphs, each edge has an associated weight. The adjacency list can be
extended to store these weights.
• Example:
246
struct Edge {
Vertex* to;
int weight;
struct Vertex {
int id;
std::vector<Edge> neighbors;
2. Directed Graphs
}
}
• Smart pointers like std::unique ptr can be used to manage memory in graphs,
reducing the risk of memory leaks.
• Example:
#include <memory>
struct Vertex {
int id;
std::vector<std::unique_ptr<Vertex>> neighbors;
class Graph {
private:
std::vector<std::unique_ptr<Vertex>> vertices;
public:
void addVertex(int id) {
vertices.push_back(std::make_unique<Vertex>(id));
248
4. Graph Algorithms
• Shortest Path: Algorithms like Dijkstra's and Bellman-Ford can be used to find the
shortest path between two vertices.
• Minimum Spanning Tree: Algorithms like Kruskal's and Prim's can be used to find
the minimum spanning tree of a graph.
• Topological Sorting: Used for directed acyclic graphs (DAGs) to order vertices such
that for every directed edge (u, v), vertex u comes before vertex v.
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
struct Edge {
int to;
int weight;
struct Vertex {
249
int id;
std::vector<Edge> neighbors;
class Graph {
private:
std::vector<Vertex*> vertices;
public:
void addVertex(int id) {
vertices.push_back(new Vertex(id));
}
dist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
˜Graph() {
251
void dijkstraExample() {
Graph graph;
graph.addVertex(0);
graph.addVertex(1);
graph.addVertex(2);
graph.addVertex(3);
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 4);
graph.addEdge(1, 2, 2);
graph.addEdge(1, 3, 6);
graph.addEdge(2, 3, 3);
graph.dijkstra(0);
}
6.3.4 Summary
Graphs are a versatile data structure used to represent relationships between objects. In this
section, we explored:
• Advanced topics such as weighted graphs, directed graphs, memory management with
smart pointers, and graph algorithms like Dijkstra's.
Chapter 7
Key Points:
253
254
2. Template Classes: Classes that can work with any data type.
3. Pointers in Templates: Pointers can be used within template functions and classes to
manage dynamic memory and create complex data structures.
#include <iostream>
void templateFunctionExample() {
int x = 5, y = 10;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
swap(&x, &y);
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
Key Points:
1. Node Structure: Each node contains data of a generic type and a pointer to the next node.
2. Linked List Class: A class that manages the nodes and provides operations like insertion,
deletion, and traversal.
3. Template Class: The linked list class is defined as a template to support any data type.
#include <iostream>
// Node structure
template <typename T>
struct Node {
T data;
Node* next;
Node<T>* head;
public:
LinkedList() : head(nullptr) {}
void genericLinkedListExample() {
LinkedList<int> intList;
intList.append(10);
intList.append(20);
intList.append(30);
intList.prepend(5);
intList.deleteNode(20);
std::cout << "After deleting 20: ";
intList.print();
LinkedList<std::string> stringList;
stringList.append("Hello");
stringList.append("World");
stringList.prepend("C++");
stringList.deleteNode("World");
259
• Example:
template <>
void LinkedList<std::string>::print() const {
Node<std::string>* current = head;
while (current) {
std::cout << "\"" << current->data << "\" -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
• Smart pointers like std::unique ptr can be used with templates to manage
memory safely.
• Example:
260
#include <memory>
public:
void append(T val) {
auto newNode = std::make_unique<Node<T>>(val);
if (!head) {
head = std::move(newNode);
} else {
Node<T>* current = head.get();
while (current->next) {
current = current->next.get();
}
current->next = std::move(newNode);
}
}
3. Template Metaprogramming
• Example:
template <>
struct Factorial<0> {
static const int value = 1;
};
void templateMetaprogrammingExample() {
std::cout << "Factorial of 5: " << Factorial<5>::value <<
,→ std::endl;
}
4. Variadic Templates
262
• Variadic templates allow you to define templates that accept a variable number of
arguments.
• Example:
#include <iostream>
void variadicTemplateExample() {
print(1, 2.5, "Hello", 'A');
}
5. Type Traits
• Type traits are a powerful feature in C++ that allow you to inspect and manipulate
type properties at compile time.
• Example:
#include <iostream>
#include <type_traits>
263
void typeTraitsExample() {
checkType<int>(); // Output: Type is integral
checkType<double>(); // Output: Type is not integral
}
6. Template Aliases
• Template aliases allow you to create aliases for complex template types, improving
code readability.
• Example:
void templateAliasExample() {
Vec<int> intVec = {1, 2, 3};
Vec<std::string> strVec = {"Hello", "World"};
}
7. Concepts (C++20)
264
• Concepts are a C++20 feature that allows you to specify constraints on template
parameters, making templates more expressive and easier to use.
• Example:
#include <concepts>
#include <iostream>
void conceptsExample() {
printIntegral(42); // Valid
// printIntegral(3.14); // Error: Does not satisfy
,→ std::integral
}
7.1.4 Summary
Templates are a powerful feature in C++ that enable you to write generic and reusable code.
When combined with pointers, templates allow you to create dynamic and type-safe data
structures. In this section, we explored:
• Advanced topics such as template specialization, smart pointers with templates, template
metaprogramming, variadic templates, type traits, template aliases, and concepts.
265
Key Points:
2. Race Conditions: Occur when multiple threads access shared data concurrently, leading
to undefined behavior.
3. Synchronization: Techniques to ensure that only one thread accesses shared data at a
time.
#include <iostream>
#include <thread>
#include <vector>
void sharingDataExample() {
int counter = 0;
const int iterations = 100000;
std::vector<std::thread> threads;
std::cout << "Final counter value: " << counter << std::endl;
}
In this example, multiple threads increment a shared counter using a pointer. However, this code
is prone to race conditions because the counter is accessed concurrently without synchronization.
267
Key Points:
2. Locking: A thread locks a mutex before accessing a shared resource and unlocks it after
accessing the resource.
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
void synchronizingAccessExample() {
268
int counter = 0;
const int iterations = 100000;
std::vector<std::thread> threads;
std::cout << "Final counter value: " << counter << std::endl;
}
In this example, a std::mutex is used to synchronize access to the shared counter. The
std::lock guard class is used to automatically lock and unlock the mutex, ensuring that
only one thread can increment the counter at a time.
1. Deadlocks
• A deadlock occurs when two or more threads are blocked forever, waiting for each
other to release locks.
• Example:
269
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(mtx2); // Deadlock
}
void thread2() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock1(mtx1); // Deadlock
}
void deadlockExample() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
}
• To avoid deadlocks, always lock mutexes in the same order or use std::lock to
lock multiple mutexes simultaneously.
2. Condition Variables
• Condition variables are used to block threads until a certain condition is met.
• Example:
270
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waitForReady() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Thread is ready!" << std::endl;
}
void setReady() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all();
}
void conditionVariableExample() {
std::thread t1(waitForReady);
std::thread t2(setReady);
t1.join();
t2.join();
}
271
3. Atomic Operations
• Atomic operations are operations that are executed without interruption, ensuring
that no other thread can observe a partially completed operation.
• Example:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
std::atomic<int> counter(0);
void atomicOperationsExample() {
const int iterations = 100000;
std::vector<std::thread> threads;
std::cout << "Final counter value: " << counter << std::endl;
}
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <queue>
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(value);
}
return false;
}
value = queue.front();
queue.pop();
return true;
}
};
void threadSafeQueueExample() {
ThreadSafeQueue<int> queue;
std::vector<std::thread> threads;
// Producer threads
for (int i = 0; i < 5; ++i) {
threads.emplace_back([&queue, i] {
for (int j = 0; j < 10; ++j) {
queue.push(i * 10 + j);
}
});
}
// Consumer threads
for (int i = 0; i < 5; ++i) {
threads.emplace_back([&queue] {
int value;
while (queue.pop(value)) {
std::cout << "Consumed: " << value << std::endl;
}
});
}
5. Thread Pools
• A thread pool is a collection of worker threads that are used to execute tasks
concurrently.
• Example:
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop;
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
275
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return stop ||
,→ !tasks.empty(); });
if (stop && tasks.empty()) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
˜ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
276
}
cv.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
};
void threadPoolExample() {
ThreadPool pool(4);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
• Thread local storage (TLS) allows each thread to have its own instance of a variable.
• Example:
#include <iostream>
#include <thread>
#include <vector>
void incrementThreadLocalVar() {
++threadLocalVar;
std::cout << "Thread " << std::this_thread::get_id() << ": "
,→ << threadLocalVar << std::endl;
}
void threadLocalStorageExample() {
std::vector<std::thread> threads;
7.2.4 Summary
Multithreading is a powerful feature in C++ that allows you to execute multiple threads
concurrently. However, sharing data between threads can lead to race conditions and undefined
behavior if not handled properly. In this section, we explored:
Key Points:
1. Move Constructor: Transfers resources from a source object to a newly created object.
#include <iostream>
class Resource {
private:
int* data;
public:
// Constructor
Resource(int size) {
data = new int[size];
std::cout << "Resource allocated" << std::endl;
}
// Destructor
˜Resource() {
delete[] data;
std::cout << "Resource deallocated" << std::endl;
}
// Move Constructor
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr; // Leave the source object in a valid state
std::cout << "Resource moved (constructor)" << std::endl;
}
// Accessor
int* getData() const {
return data;
}
};
void moveSemanticsExample() {
Resource res1(10); // Create a Resource object
Resource res2(std::move(res1)); // Move construct res2 from res1
In this example, we implement a class Resource that manages a dynamically allocated array.
The class uses move semantics to efficiently transfer ownership of the array between objects.
Key Points:
282
1. Move Constructor: Transfers ownership of the dynamically allocated array from the
source object to the newly created object.
#include <iostream>
class Resource {
private:
int* data;
size_t size;
public:
// Constructor
Resource(size_t size) : size(size) {
data = new int[size];
std::cout << "Resource allocated with size " << size << std::endl;
}
// Destructor
˜Resource() {
delete[] data;
std::cout << "Resource deallocated" << std::endl;
}
// Move Constructor
283
// Accessor
int* getData() const {
return data;
}
284
void moveAwareClassExample() {
Resource res1(10); // Create a Resource object
Resource res2(std::move(res1)); // Move construct res2 from res1
• Destructor
• Copy Constructor
• Copy Assignment Operator
• Move Constructor
• Move Assignment Operator
This ensures proper resource management and avoids issues like memory leaks or double
deletions.
285
Example:
class RuleOfFive {
private:
int* data;
size_t size;
public:
// Constructor
RuleOfFive(size_t size) : size(size), data(new int[size]) {}
// Destructor
˜RuleOfFive() {
delete[] data;
}
// Copy Constructor
RuleOfFive(const RuleOfFive& other) : size(other.size), data(new
,→ int[other.size]) {
std::copy(other.data, other.data + other.size, data);
}
// Move Constructor
RuleOfFive(RuleOfFive&& other) noexcept : data(other.data),
,→ size(other.size) {
other.data = nullptr;
other.size = 0;
}
Smart pointers like std::unique ptr and std::shared ptr inherently support
move semantics, making them ideal for managing dynamically allocated resources. They
automatically handle resource deallocation, reducing the risk of memory leaks.
Example:
#include <iostream>
#include <memory>
287
class Resource {
private:
std::unique_ptr<int[]> data;
size_t size;
public:
// Constructor
Resource(size_t size) : size(size),
,→ data(std::make_unique<int[]>(size)) {
std::cout << "Resource allocated with size " << size <<
,→ std::endl;
}
// Move Constructor
Resource(Resource&& other) noexcept : data(std::move(other.data)),
,→ size(other.size) {
other.size = 0;
std::cout << "Resource moved (constructor)" << std::endl;
}
// Accessor
288
void smartPointersAndMoveSemanticsExample() {
Resource res1(10); // Create a Resource object
Resource res2(std::move(res1)); // Move construct res2 from res1
Example:
#include <iostream>
#include <vector>
#include <string>
void stlContainersMoveSemanticsExample() {
std::vector<std::string> vec1 = {"Hello", "World"};
289
std::cout << "vec1 size: " << vec1.size() << std::endl; // vec1
,→ is now empty
std::cout << "vec2 size: " << vec2.size() << std::endl; // vec2
,→ contains the moved elements
}
7.3.4 Summary
Move semantics is a powerful feature in C++ that enables efficient transfer of resources between
objects. Pointers play a crucial role in implementing move semantics, as they allow the transfer
of ownership of dynamically allocated resources without deep copying. In this section, we
explored:
• Advanced topics such as the Rule of Five, smart pointers with move semantics, and move
semantics in STL containers.
Chapter 8
A memory leak occurs when dynamically allocated memory is not properly deallocated,
causing the program to gradually consume more and more memory until it exhausts
system resources. Memory leaks are particularly problematic in long-running applications,
such as servers or daemons, where even small leaks can accumulate over time and lead to
290
291
system instability.
(a) Forgetting to Deallocate Memory: The most common cause is simply forgetting to
call delete or delete[] after allocating memory with new or new[].
(b) Losing Pointer References: If a pointer to dynamically allocated memory is
overwritten or goes out of scope without being deallocated, the memory becomes
unreachable and cannot be freed.
(c) Exception Handling Issues: If an exception is thrown before memory is
deallocated, the program may terminate without releasing resources.
void memoryLeakExample() {
int* ptr = new int(10); // Allocate memory
// Forget to delete ptr
// Memory is leaked
}
(a) Use Smart Pointers: Smart pointers like std::unique ptr and
std::shared ptr automatically deallocate memory when they go out of scope,
eliminating the need for manual memory management.
(b) Follow RAII (Resource Acquisition Is Initialization): Encapsulate resources in
classes that manage their lifetimes. For example, use a class to manage a
dynamically allocated array, and deallocate the memory in the destructor.
292
(c) Always Pair new with delete: Ensure that every new or new[] has a
corresponding delete or delete[]. Use tools like valgrind or address
sanitizers to detect memory leaks during development.
#include <memory>
void safeMemoryManagementExample() {
std::unique_ptr<int> ptr = std::make_unique<int>(10); //
,→ Automatically deallocated
// No need to manually delete
}
2. Dangling Pointers
A dangling pointer is a pointer that points to memory that has already been deallocated.
Accessing or modifying such memory leads to undefined behavior, which can manifest
as crashes, data corruption, or security vulnerabilities.
(a) Deleting a Pointer and Then Using It: If a pointer is deleted and then dereferenced,
it becomes a dangling pointer.
(c) Multiple Pointers to the Same Memory: If two pointers point to the same memory
and one of them deletes it, the other pointer becomes dangling.
int* danglingPointerExample() {
int x = 10;
int* ptr = &x;
return ptr; // ptr becomes dangling after the function returns
}
(a) Avoid Returning Pointers to Local Variables: Ensure that pointers do not outlive
the memory they point to. If you need to return a pointer, allocate memory
dynamically and transfer ownership.
(b) Set Pointers to nullptr After Deletion: After deleting a pointer, set it to
nullptr to make it easier to detect invalid accesses.
(c) Use Smart Pointers: Smart pointers like std::unique ptr and
std::shared ptr automatically manage the lifetime of the pointed-to memory,
preventing dangling pointers.
(d) Avoid Multiple Owners: If multiple pointers point to the same memory, ensure that
only one of them is responsible for deallocating it.
#include <memory>
std::unique_ptr<int> safePointerExample() {
auto ptr = std::make_unique<int>(10); // Memory is managed
,→ automatically
return ptr; // No dangling pointer
}
(a) Accessing Deallocated Memory: Using a pointer after the memory it points to has
been deallocated.
(b) Out-of-Bounds Access: Accessing memory outside the bounds of an allocated
block, such as using an invalid array index.
(c) Dereferencing Null or Uninitialized Pointers: Attempting to access memory
through a null or uninitialized pointer.
void invalidMemoryAccessExample() {
int* ptr = nullptr;
*ptr = 10; // Dereferencing a null pointer
}
295
(a) Always Initialize Pointers: Set pointers to nullptr or a valid memory address
when they are declared.
(b) Check for Null Pointers: Before dereferencing a pointer, ensure it is not null.
(c) Use Bounds Checking: When working with arrays, ensure that indices are within
valid ranges. Use standard library containers like std::vector or std::array
to avoid out-of-bounds errors.
(d) Use Tools for Detection: Tools like valgrind, address sanitizers, and static
analyzers can help detect invalid memory accesses during development.
#include <iostream>
#include <vector>
void safeMemoryAccessExample() {
std::vector<int> vec = {1, 2, 3};
if (!vec.empty()) {
std::cout << vec[0] << std::endl; // Safe access
}
}
Example:
#include <memory>
void smartPointerExample() {
std::unique_ptr<int> ptr = std::make_unique<int>(10); //
,→ Automatically deallocated
// No need to manually delete
}
Example:
class Resource {
private:
int* data;
public:
Resource(int size) : data(new int[size]) {}
˜Resource() { delete[] data; }
};
297
Minimize the use of raw pointers unless absolutely necessary. Use standard library
containers and smart pointers instead.
Example:
#include <vector>
void avoidRawPointersExample() {
std::vector<int> vec = {1, 2, 3}; // No need for raw pointers
}
Always check if a pointer is valid before dereferencing it. This includes checking for null
pointers and ensuring that the pointer points to valid memory.
Example:
Example:
#include <vector>
void useContainersExample() {
std::vector<int> vec = {1, 2, 3}; // Safe and efficient
}
#include <iostream>
#include <memory>
#include <vector>
class SafeResource {
private:
std::unique_ptr<int[]> data;
size_t size;
public:
// Constructor
SafeResource(size_t size) : size(size),
,→ data(std::make_unique<int[]>(size)) {
std::cout << "Resource allocated with size " << size << std::endl;
}
// Accessor
int* getData() const {
return data.get();
}
void safeAndEfficientPointerCodeExample() {
SafeResource res(10); // Resource is managed automatically
Sometimes, a pointer may be optional (i.e., it may or may not point to valid memory). In
such cases, use std::optional to clearly indicate the absence of a value.
300
Example:
#include <iostream>
#include <optional>
void optionalPointerExample() {
auto ptr = getOptionalPointer(false);
if (ptr.has_value()) {
std::cout << *ptr.value() << std::endl;
delete ptr.value(); // Clean up
} else {
std::cout << "No pointer returned" << std::endl;
}
}
Example:
301
#include <iostream>
#include <span>
void spanExample() {
int arr[] = {1, 2, 3, 4, 5};
printArray(arr); // Safe and bounds-checked
}
Example:
#include <iostream>
#include <variant>
void variantExample() {
std::variant<int*, double*> ptr;
int x = 10;
ptr = &x;
if (std::holds_alternative<int*>(ptr)) {
std::cout << *std::get<int*>(ptr) << std::endl; // Safe
,→ access
302
}
}
1. Use of valgrind
valgrind is a powerful tool for detecting memory leaks, invalid memory access, and
other pointer-related issues.
Example Command:
2. Address Sanitizers
Address sanitizers are compiler features that detect memory errors such as out-of-bounds
access and use-after-free.
3. Static Analyzers
Static analyzers like clang-tidy and cppcheck can detect potential pointer-related
issues at compile time.
303
Example Command:
8.1.6 Summary
Avoiding common pointer pitfalls is essential for writing robust and efficient C++ programs. By
understanding and addressing issues like memory leaks, dangling pointers, and invalid memory
access, you can create safer and more reliable code. Key takeaways include:
• Use tools like valgrind and address sanitizers to detect and fix issues during
development.
304
• Memory Leak Detection: Identifies memory that was allocated but not deallocated.
• Invalid Memory Access Detection: Detects reads or writes to invalid memory
locations.
• Use of Uninitialized Memory: Flags the use of uninitialized variables.
• Detailed Reports: Provides stack traces and line numbers to help locate the source
of errors.
Example Command:
305
Example Output:
#include <iostream>
void memoryLeakExample() {
int* ptr = new int(10); // Allocate memory
// Forget to delete ptr
// Memory is leaked
}
int main() {
memoryLeakExample();
return 0;
}
The report indicates that 4 bytes were definitely lost due to a memory leak in
memoryLeakExample().
2. AddressSanitizer
AddressSanitizer is a memory error detector built into modern compilers like GCC and
Clang. It is designed to detect memory errors such as out-of-bounds access, use-after-free,
and memory leaks. AddressSanitizer is faster than Valgrind and can be used during
development and testing.
• Memory Leaks: Identifies memory that was allocated but not deallocated.
• Stack and Heap Errors: Detects errors in both stack and heap memory.
Example Output:
AddressSanitizer will generate a report like this:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
#include <iostream>
void memoryLeakExample() {
int* ptr = new int(10); // Allocate memory
// Forget to delete ptr
// Memory is leaked
}
309
int main() {
memoryLeakExample();
return 0;
}
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
1. Problem Code
#include <iostream>
void createMemoryLeak() {
int* ptr = new int(42); // Allocate memory
// Forget to delete ptr
// Memory is leaked
}
int main() {
createMemoryLeak();
std::cout << "Memory leak example" << std::endl;
return 0;
}
./memory_leak_example
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
To fix the memory leak, ensure that the allocated memory is properly deallocated:
#include <iostream>
313
void createMemoryLeak() {
int* ptr = new int(42); // Allocate memory
delete ptr; // Deallocate memory
}
int main() {
createMemoryLeak();
std::cout << "Memory leak fixed" << std::endl;
return 0;
}
Re-run the program with Valgrind or AddressSanitizer to confirm that the memory leak is
resolved.
#include <iostream>
int main() {
int* ptr = new int(10); // Logs allocation
delete ptr; // Logs deallocation
return 0;
}
2. Static Analyzers
Static analyzers like clang-tidy and cppcheck can detect potential pointer-related issues at
compile time. These tools analyze your code without executing it and provide warnings or
errors for problematic patterns.
Example Command:
Example Output:
GDB (GNU Debugger) is a powerful tool for debugging C++ programs. It allows you to
set breakpoints, inspect variables, and step through your code to identify issues.
Example Command:
gdb ./your_program
8.2.4 Summary
Debugging pointer-related issues is essential for writing robust and efficient C++ programs.
Tools like Valgrind and AddressSanitizer are invaluable for detecting memory leaks, invalid
memory access, and other pointer-related errors. By using these tools and following best
practices, you can identify and resolve issues early in the development process, ensuring your
programs are both correct and efficient.
Chapter 9
1. Raw Pointers
Raw pointers are the most basic type of pointer in C++. They are variables that store the
memory address of another variable or object. Raw pointers provide direct access to
memory, but they come with significant risks, such as memory leaks, dangling pointers,
and invalid memory access.
316
317
Example:
2. Smart Pointers
Smart pointers are objects that manage the lifetime of dynamically allocated memory.
They automatically deallocate memory when it is no longer needed, reducing the risk of
memory leaks and dangling pointers. C++ provides three types of smart pointers:
std::unique ptr, std::shared ptr, and std::weak ptr.
Example:
318
#include <memory>
void smartPointerExample() {
std::unique_ptr<int> smartPtr = std::make_unique<int>(10); //
,→ Automatically deallocated
std::cout << *smartPtr << std::endl; // Access memory
}
3. Memory Management
Memory management refers to the process of allocating, using, and deallocating memory
in a program. Effective memory management is essential for preventing memory leaks,
dangling pointers, and other memory-related issues.
Example:
void memoryManagementExample() {
int* ptr = new int(10); // Allocate memory
std::cout << *ptr << std::endl; // Use memory
delete ptr; // Deallocate memory
}
319
4. Dangling Pointers
A dangling pointer is a pointer that points to memory that has already been deallocated.
Accessing or modifying such memory leads to undefined behavior, which can manifest as
crashes, data corruption, or security vulnerabilities.
• Deleting a Pointer and Then Using It: If a pointer is deleted and then dereferenced,
it becomes a dangling pointer.
• Returning Pointers to Local Variables: If a function returns a pointer to a local
variable, the pointer becomes invalid once the function returns.
• Multiple Pointers to the Same Memory: If two pointers point to the same memory
and one of them deletes it, the other pointer becomes dangling.
Example:
int* danglingPointerExample() {
int x = 10;
int* ptr = &x;
return ptr; // ptr becomes dangling after the function returns
}
5. Memory Leaks
A memory leak occurs when dynamically allocated memory is not properly deallocated,
causing the program to gradually consume more and more memory until it exhausts
system resources. Memory leaks are particularly problematic in long-running applications.
Example:
void memoryLeakExample() {
int* ptr = new int(10); // Allocate memory
// Forget to delete ptr
// Memory is leaked
}
Example:
class Resource {
private:
int* data;
public:
Resource(int size) : data(new int[size]) {}
˜Resource() { delete[] data; }
};
void raiiExample() {
Resource res(10); // Resource is managed automatically
}
7. Ownership Semantics
Ownership semantics refer to the rules that determine which part of the code is
responsible for managing the lifetime of a resource. Clear ownership semantics are
essential for preventing memory leaks and dangling pointers.
• Weak Ownership: A pointer that does not affect the lifetime of the resource.
Example: std::weak ptr.
322
Example:
#include <memory>
void ownershipExample() {
std::unique_ptr<int> uniquePtr = std::make_unique<int>(10); //
,→ Unique ownership
std::shared_ptr<int> sharedPtr = std::make_shared<int>(20); //
,→ Shared ownership
std::weak_ptr<int> weakPtr = sharedPtr; // Weak ownership
}
8. Null Pointer
A null pointer is a pointer that does not point to any valid memory location.
Dereferencing a null pointer leads to undefined behavior, typically resulting in a crash.
• Initialization: Null pointers are often used to initialize pointers that do not yet point
to valid memory.
Example:
void nullPointerExample() {
int* ptr = nullptr; // Null pointer
if (ptr) {
*ptr = 10; // Safe to dereference
323
}
}
9. Pointer Arithmetic
Pointer arithmetic refers to the operations that can be performed on pointers, such as
addition, subtraction, and comparison. Pointer arithmetic is commonly used when
working with arrays and dynamic memory.
Example:
void pointerArithmeticExample() {
int arr[] = {1, 2, 3, 4, 5};
int* ptr = arr;
std::cout << *(ptr + 2) << std::endl; // Access the third element
}
Example:
void memoryAllocationExample() {
int stackVar = 10; // Stack allocation
int* heapVar = new int(20); // Heap allocation
delete heapVar; // Deallocate heap memory
}
Summary
Understanding key terminology is essential for mastering pointers and memory management in
C++. This appendix provides detailed explanations of terms such as raw pointers, smart
pointers, memory management, dangling pointers, memory leaks, RAII, ownership
semantics, null pointers, pointer arithmetic, and memory allocation. By familiarizing
yourself with these concepts, you can write safer, more efficient, and more maintainable C++
code.
325
9.2.1 Books
1. ”Effective Modern C++” by Scott Meyers
• Description: This book is a must-read for anyone looking to master modern C++
(C++11, C++14, and beyond). It covers best practices for using smart pointers, move
semantics, and other advanced features.
• Key Topics:
• Why Read It: Scott Meyers is a renowned C++ expert, and this book provides clear,
practical advice for writing efficient and safe C++ code.
• Key Topics:
• Why Read It: This book is ideal for beginners and intermediate programmers who
want a solid foundation in C++.
• Description: Written by the creator of C++, this book is the definitive guide to the
language. It covers everything from basic syntax to advanced features, including
pointers and memory management.
• Key Topics:
• Why Read It: This book is an authoritative resource for understanding the design
and philosophy of C++.
• Description: This book focuses on writing clean, maintainable C++ code using
test-driven development (TDD). It includes practical examples of using smart
pointers and managing memory safely.
• Key Topics:
• Why Read It: If you want to learn how to write robust, testable C++ code, this book
is an excellent choice.
• Why Read It: If you are working on concurrent or parallel programs, this book will
help you manage memory safely and efficiently.
• Description: An exhaustive online reference for the C++ Standard Library and
language features. It includes detailed documentation on pointers, smart pointers,
and memory management.
• Key Features:
• Why Use It: This is the go-to resource for looking up C++ syntax, functions, and
libraries.
2. Stack Overflow
• Key Features:
• Why Use It: If you encounter a problem, chances are someone has already asked
about it on Stack Overflow.
3. LearnCpp.com
• Key Features:
• Why Use It: This is a great resource for beginners who want to learn C++ from
scratch.
• Key Features:
• Why Use It: GeeksforGeeks is a great place to find quick explanations and practice
problems.
• Key Features:
• Why Use It: If you prefer video content, this channel is an excellent way to learn
C++ in bite-sized chunks.
• Key Topics:
• Why Take It: This course is ideal for C programmers looking to learn C++.
• Description: An advanced course that covers modern C++ features, including smart
pointers, move semantics, and multithreading.
• Key Topics:
• Why Take It: This course is perfect for experienced programmers who want to
deepen their knowledge of C++.
• Why Take It: This course is a great starting point for beginners.
331
• Description: A powerful tool for detecting memory leaks, invalid memory access,
and other memory-related errors.
• Key Features:
• Why Use It: Valgrind is essential for debugging memory-related issues in C++
programs.
2. AddressSanitizer
• Description: A memory error detector built into modern compilers like GCC and
Clang. It detects out-of-bounds access, use-after-free, and memory leaks.
• Key Features:
• Why Use It: AddressSanitizer is a great tool for catching memory errors during
development.
3. Cppcheck
• Description: A static analysis tool for C++ that detects potential errors, including
memory leaks and invalid memory access.
• Key Features:
332
• Why Use It: Cppcheck is a useful tool for identifying potential issues in your code
before runtime.
9.2.5 Summary
This appendix provides a curated list of books, websites, online courses, and tools to help you
master C++ pointers and memory management. Whether you are a beginner or an experienced
programmer, these resources will serve as valuable references for further learning. By leveraging
these resources, you can deepen your understanding of C++ and write safer, more efficient, and
more maintainable code.
333
One of the most common uses of pointers is to implement dynamic data structures like
arrays, linked lists, and trees. In this exercise, you will implement a dynamic array using
raw pointers.
Problem Statement:
• Resize: Change the size of the array while preserving existing elements.
• Access Elements: Provide a way to access and modify elements in the array.
Example Implementation:
334
#include <iostream>
#include <stdexcept>
class DynamicArray {
private:
int* data;
size_t size;
public:
// Constructor
DynamicArray(size_t size) : size(size) {
data = new int[size];
std::cout << "Array allocated with size " << size <<
,→ std::endl;
}
// Destructor
˜DynamicArray() {
delete[] data;
std::cout << "Array deallocated" << std::endl;
}
// Access elements
int& operator[](size_t index) {
if (index >= size) {
throw std::out_of_range("Index out of range");
}
return data[index];
}
int main() {
DynamicArray arr(5); // Create an array of size 5
for (size_t i = 0; i < 5; ++i) {
arr[i] = i * 2; // Initialize array elements
}
arr.print(); // Print the array
return 0;
}
336
Questions:
(a) What happens if you try to access an index that is out of bounds? How can you
prevent this?
(b) How does the resize function preserve existing elements when resizing the array?
(c) What are the potential pitfalls of using raw pointers in this implementation? How
can smart pointers improve it?
Problem Statement:
Implement a class LinkedList that manages a singly linked list of integers. The class
should support the following operations:
Example Implementation:
#include <iostream>
class LinkedList {
private:
struct Node {
337
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
Node* head;
public:
// Constructor
LinkedList() : head(nullptr) {}
// Destructor
˜LinkedList() {
while (head) {
Node* temp = head;
head = head->next;
delete temp;
}
}
if (!prev) {
head = curr->next; // Remove the head
} else {
prev->next = curr->next; // Remove a non-head node
}
delete curr;
}
}
std::cout << std::endl;
}
};
int main() {
LinkedList list;
list.insert(10);
list.insert(20);
list.insert(30);
list.print(); // Output: 30 20 10
list.remove(20);
list.print(); // Output: 30 10
return 0;
}
Questions:
(a) How does the remove function handle the case where the node to be deleted is the
head of the list?
(b) What are the advantages and disadvantages of using a linked list over a dynamic
array?
(c) How can you modify this implementation to use smart pointers instead of raw
340
pointers?
(a) What is the difference between stack and heap memory? When would you use each?
(b) Explain the concept of RAII (Resource Acquisition Is Initialization). How does it
help prevent memory leaks?
(c) What are the differences between new/delete and malloc/free? When would
you use one over the other?
(b) Explain the differences between std::unique ptr, std::shared ptr, and
std::weak ptr. When would you use each?
(c) What is the Rule of Five in C++? Why is it important for classes that manage
resources?
Advanced Topics
3. What are the potential risks of using raw pointers in multithreaded programs? How can
smart pointers help mitigate these risks?
341
(a) How would you use Valgrind to detect memory leaks in a C++ program? Provide an
example.
(b) What is AddressSanitizer, and how does it help debug memory errors? Provide an
example of its usage.
(c) How can you use GDB to debug a segmentation fault caused by invalid pointer
usage?
2. Real-World Scenarios
(a) You are tasked with implementing a custom memory allocator for a
performance-critical application. What are the key considerations, and how would
you approach this problem?
(b) How would you design a class to manage a dynamic 2D array using pointers? What
are the potential pitfalls, and how can you avoid them?
(c) You are working on a multithreaded application that uses shared resources. How
would you ensure thread-safe memory management using smart pointers?
9.3.4 Summary
This appendix provides a variety of practical exercises and theoretical questions to test and
enhance your understanding of C++ pointers and memory management. By working through
these exercises and answering the questions, you will gain hands-on experience and deepen your
knowledge of key concepts. Whether you are implementing dynamic arrays, debugging memory
issues, or exploring advanced topics, these exercises will help you become a more proficient C++
programmer.