Contents
Advance C++
STL (Standard Template Library)
Multithreading is a feature in C++ that allows multiple threads to run concurrently, making better use of the CPU. Each thread is a separate flow of execution within a process, which allows multiple parts of a program to run in parallel.
Multithreading support was added in C++11, and before that, programmers had to use the POSIX threads (pthreads
) library. C++11 introduced std::thread
, which made multithreading much easier and portable across different platforms. The std::thread
class and related utilities are provided in the <thread>
header.
Syntax for Creating a Thread:
std::thread thread_object(callable);
Here, std::thread
represents a single thread in C++. To start a new thread, we create a std::thread
object and pass a callable to its constructor. A callable can be:
1. A Function Pointer
2. A Lambda Expression
3. A Function Object (Functor)
4. A Non-Static Member Function
5. A Static Member Function
Once the callable is passed, the thread will execute the corresponding code.
Launching a Thread Using a Function Pointer
A function pointer can be passed to a std::thread
constructor to launch a thread:
void my_function(int value)
{
for (int i = 0; i < value; i++) {
std::cout << "Thread using function pointer\n";
}
}
// Creating and launching the thread
std::thread my_thread(my_function, 5);
Launching a Thread Using a Lambda Expression
A lambda expression is a convenient way to define a callable on the fly. Here’s how to use it to launch a thread:
auto my_lambda = [](int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using lambda expression\n";
}
};
// Launching a thread using the lambda expression
std::thread my_thread(my_lambda, 5);
Launching a Thread Using a Function Object (Functor)
A function object (functor) is a class with an overloaded ()
operator. Here’s an example:
class Functor {
public:
void operator()(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using functor\n";
}
}
};
// Launching a thread using a function object
std::thread my_thread(Functor(), 5);
Output:
a < b : 0
a > b : 1
a <= b: 0
a >= b: 1
a == b: 0
a != b : 1
Launching a Thread Using a Non-Static Member Function
Non-static member functions require an instance of the class to be called. Here’s how to use a non-static member function in a thread:
class MyClass {
public:
void my_method(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using non-static member function\n";
}
}
};
// Creating an instance of the class
MyClass my_obj;
// Launching the thread
std::thread my_thread(&MyClass::my_method, &my_obj, 5);
Launching a Thread Using a Static Member Function
Static member functions do not require an instance of the class and can be directly passed to a thread:
class MyClass {
public:
static void my_static_method(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using static member function\n";
}
}
};
// Launching the thread using the static member function
std::thread my_thread(&MyClass::my_static_method, 5);
Waiting for Threads to Finish
Once a thread is launched, we may need to wait for it to finish before proceeding. The join()
function blocks the calling thread until the specified thread completes execution.
int main() {
std::thread t1(my_function, 5);
t1.join(); // Wait for t1 to finish
// Proceed with other tasks after t1 finishes
}
Complete C++ Program for Multithreading
Below is a complete C++ program that demonstrates launching threads using different callables, including a function pointer, lambda expression, functor, and member functions:
#include
#include
// Function to be used as a function pointer
void function_pointer(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using function pointer\n";
}
}
// Functor (Function Object)
class Functor {
public:
void operator()(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using functor\n";
}
}
};
// Class with member functions
class MyClass {
public:
void non_static_function() {
std::cout << "Thread using non-static member function\n";
}
static void static_function() {
std::cout << "Thread using static member function\n";
}
};
int main() {
std::cout << "Launching threads...\n";
// Launch thread using function pointer
std::thread t1(function_pointer, 3);
// Launch thread using functor
Functor functor;
std::thread t2(functor, 3);
// Launch thread using lambda expression
auto lambda = [](int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using lambda expression\n";
}
};
std::thread t3(lambda, 3);
// Launch thread using non-static member function
MyClass obj;
std::thread t4(&MyClass::non_static_function, &obj);
// Launch thread using static member function
std::thread t5(&MyClass::static_function);
// Wait for all threads to finish
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
std::cout << "All threads finished.\n";
return 0;
}
Output:
Launching threads...
Thread using function pointer
Thread using function pointer
Thread using function pointer
Thread using lambda expression
Thread using lambda expression
Thread using lambda expression
Thread using functor
Thread using functor
Thread using functor
Thread using non-static member function
Thread using static member function
All threads finished.
Pointers in C++
Pointers in C++ are used to access external resources, such as heap memory. When you access an external resource without a pointer, you only interact with a copy, meaning changes to the copy won’t affect the original resource. However, when you use a pointer, you can modify the original resource directly.
Common Issues with Normal Pointers
1. Memory Leaks: Occur when memory is allocated but not freed, leading to wasted memory and potential program crashes.
2. Dangling Pointers: Happen when a pointer refers to memory that has already been deallocated.
3. Wild Pointers: Pointers that are declared but not initialized to point to a valid address.
4. Data Inconsistency: Happens when memory data is not updated uniformly across the program.
5. Buffer Overflow: Occurs when writing outside of allocated memory, which can corrupt data or cause security issues.
Example of Memory Leak:
#include
using namespace std;
class Demo {
private:
int data;
};
void memoryLeak() {
Demo* p = new Demo();
}
int main() {
while (true) {
memoryLeak();
}
return 0;
}
Explanation:
In the memoryLeak
function, a pointer to a dynamically created Demo
object is created, but the memory allocated by new
is never deallocated using delete
, leading to a memory leak as the program keeps allocating memory without freeing it.
Smart Pointers
Smart pointers in C++ automatically manage memory allocation and deallocation, avoiding manual delete
calls and preventing memory leaks. Unlike normal pointers, smart pointers automatically free the memory when they go out of scope. Smart pointers overload operators like *
and ->
to behave similarly to normal pointers but with additional memory management features.
Example of a Smart Pointer:
#include
using namespace std;
class SmartPtr {
int* ptr;
public:
explicit SmartPtr(int* p = nullptr) { ptr = p; }
~SmartPtr() { delete ptr; }
int& operator*() { return *ptr; }
};
int main() {
SmartPtr sp(new int());
*sp = 30;
cout << *sp << endl; // Outputs 30
return 0;
}
Explanation:
The destructor of the SmartPtr
class automatically frees the memory when the object goes out of scope, preventing memory leaks.
Differences Between Normal Pointers and Smart Pointers
Pointer | Smart Pointer |
---|---|
A pointer holds a memory address and data type info. | Smart pointers are objects that wrap a pointer. |
A normal pointer does not deallocate memory when it goes out of scope. | Automatically deallocates memory when out of scope. |
Manual memory management is required. | Handles memory management automatically. |
Generic Smart Pointer Using Templates:
#include
using namespace std;
template
class SmartPtr {
T* ptr;
public:
explicit SmartPtr(T* p = nullptr) { ptr = p; }
~SmartPtr() { delete ptr; }
T& operator*() { return *ptr; }
T* operator->() { return ptr; }
};
int main() {
SmartPtr sp(new int());
*sp = 40;
cout << *sp << endl; // Outputs 40
return 0;
}
Types of Smart Pointers
C++ provides several types of smart pointers that are available in the standard library:
1. unique_ptr: Manages a single object and ensures that only one unique_ptr instance can point to a particular object.
2. shared_ptr: Allows multiple shared_ptr objects to share ownership of the same object, managing reference counting.
3. weak_ptr: A non-owning smart pointer that is used in conjunction with shared_ptr to avoid circular references.
Example: Using unique_ptr
Area: 48
Area (after transfer): 48
Explanation:
The ownership of the object is transferred from up1
to up2
using std::move
. After the transfer, up1
becomes null, and up2
owns the object.
Example: Using shared_ptr
#include
#include
using namespace std;
class Circle {
int radius;
public:
Circle(int r) : radius(r) {}
int circumference() { return 2 * 3.14 * radius; }
};
int main() {
shared_ptr sp1(new Circle(7));
cout << "Circumference: " << sp1->circumference() << endl;
shared_ptr sp2 = sp1;
cout << "Reference count: " << sp1.use_count() << endl;
return 0;
}
Output:
Circumference: 43.96
Reference count: 2
Explanation:
Both sp1
and sp2
share ownership of the Circle
object, and the reference count is managed automatically.
Example: Using weak_ptr
#include
#include
using namespace std;
class Box {
int size;
public:
Box(int s) : size(s) {}
int getSize() { return size; }
};
int main() {
shared_ptr sp1(new Box(15));
weak_ptr wp1(sp1); // weak_ptr does not increase reference count
cout << "Box size: " << sp1->getSize() << endl;
cout << "Reference count: " << sp1.use_count() << endl;
return 0;
}
Output:
Box size: 15
Reference count: 1
auto_ptr vs unique_ptr vs shared_ptr vs weak_ptr in C++
Smart pointers in C++ are special objects that manage memory and resources automatically. They are part of the C++ Standard Library and are defined in the <memory>
header. The key types of smart pointers are:
1. auto_ptr (deprecated in C++11)
2. unique_ptr
3. shared_ptr
4. weak_ptr
These smart pointers are used to manage dynamic memory and other resources, ensuring proper cleanup and preventing issues such as memory leaks and dangling pointers.
1. auto_ptr :
auto_ptr
was a smart pointer in C++ before C++11 but was deprecated because of its limitations. It manages the memory of dynamically allocated objects and automatically deletes the object when the auto_ptr
goes out of scope. However, auto_ptr
follows a transfer-of-ownership model, meaning only one pointer can own an object at a time. Copying or assigning an auto_ptr
transfers ownership, making the original pointer empty.
Example:
(10 * 5) + (8 / 2) = 50 + 4 = 54
Example 1: C Program to Calculate the Area and Perimeter of a Rectangle
#include
#include
using namespace std;
class MyClass {
public:
void display() { cout << "MyClass::display()" << endl; }
};
int main() {
auto_ptr ptr1(new MyClass);
ptr1->display();
// Transfer ownership to ptr2
auto_ptr ptr2(ptr1);
ptr2->display();
// ptr1 is now empty
cout << "ptr1: " << ptr1.get() << endl;
cout << "ptr2: " << ptr2.get()
Output:
Area = 21
Perimeter = 20
Why is auto_ptr
deprecated?
Ownership Transfer:When copying or assigning an
auto_ptr
, ownership is transferred, and the source pointer becomesnull
. This behavior madeauto_ptr
unsuitable for use in STL containers, which require copy semantics.- Lack of Reference Counting:
auto_ptr
does not support shared ownership, so it cannot be used in scenarios where multiple pointers need to reference the same object.
2. unique_ptr:
unique_ptr
was introduced in C++11 to replace auto_ptr
. It provides exclusive ownership of a dynamically allocated object and ensures that only one unique_ptr
can manage a resource at a time. It prevents copying but supports transferring ownership using the std::move()
function.
Example:
#include
#include
using namespace std;
class MyClass {
public:
void display() { cout << "MyClass::display()" << endl; }
};
int main() {
unique_ptr ptr1(new MyClass);
ptr1->display();
// Transfer ownership to ptr2
unique_ptr ptr2 = move(ptr1);
ptr2->display();
// ptr1 is now empty
cout << "ptr1: " << ptr1.get() << endl;
cout << "ptr2: " << ptr2.get() << endl;
return 0;
}
Output:
MyClass::display()
MyClass::display()
ptr1: 0
ptr2:
Key Features of unique_ptr
:
- Exclusive Ownership: Only one
unique_ptr
can own a resource at a time. - Move Semantics: Ownership can be transferred using
std::move()
. - Resource Management: When the
unique_ptr
goes out of scope, the resource is automatically freed.
3. shared_ptr
:
shared_ptr
provides shared ownership of a dynamically allocated object. It uses a reference counting mechanism to keep track of how many pointers are pointing to the object. The object is only destroyed when the reference count reaches zero, meaning all shared_ptr
s referencing the object have been deleted.
Example:
#include
#include
using namespace std;
class MyClass {
public:
void display() { cout << "MyClass::display()" << endl; }
};
int main() {
shared_ptr ptr1(new MyClass);
ptr1->display();
// Share ownership with ptr2
shared_ptr ptr2(ptr1);
cout << "ptr1 count: " << ptr1.use_count() << endl;
cout << "ptr2 count: " << ptr2.use_count() << endl;
// Reset ptr1
ptr1.reset();
cout << "ptr1: " << ptr1.get() << endl;
cout << "ptr2 count after ptr1 reset: " << ptr2.use_count() << endl;
return 0;
}
Output:
MyClass::display()
ptr1 count: 2
ptr2 count: 2
ptr1: 0
ptr2 count after ptr1 reset: 1
this Pointer in C++
In C++, the this
pointer refers to the current object of a class and is implicitly passed to all non-static member function calls. It is a hidden argument that provides access to the calling object inside the member function.
Type of this
Pointer
The type of the this
pointer depends on whether the member function is const
, volatile
, or both. The this
pointer type will either be const ExampleClass*
or ExampleClass*
based on whether the member function is const
or not.
1) Const ExampleClass
: When a member function is declared as const
, the type of the this
pointer inside that function becomes const ExampleClass* const
. This ensures that the function cannot modify the object that called it.
Example:
#include
using namespace std;
class Demo {
public:
void show() const {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'const Demo* const'
cout << "Const member function called" << endl;
}
};
int main() {
Demo obj;
obj.show();
return 0;
}
2) Non-Const ExampleClass
: If the member function is not const
, the this
pointer is of type ExampleClass* const
. This means that the function can modify the state of the object it is called on.
Example:
#include
using namespace std;
class Demo {
public:
void show() {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'Demo* const'
cout << "Non-const member function called" << endl;
}
};
int main() {
Demo obj;
obj.show();
return 0;
}
3) Volatile ExampleClass
: When a member function is declared as volatile
, the type of the this
pointer becomes volatile ExampleClass* const
. This means that the function can work with objects that are volatile (i.e., objects that can be modified outside the program’s control).
Example:
#include
using namespace std;
class Demo {
public:
void show() volatile {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'volatile Demo* const'
cout << "Volatile member function called" << endl;
}
};
int main() {
volatile Demo obj;
obj.show();
return 0;
}
4) Const Volatile ExampleClass
: If a member function is declared as both const
and volatile
, the type of the this
pointer becomes const volatile ExampleClass* const
.
Example:
#include
using namespace std;
class Demo {
public:
void show() const volatile {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'const volatile Demo* const'
cout << "Const volatile member function called" << endl;
}
};
int main() {
const volatile Demo obj;
obj.show();
return 0;
}
“delete this” in C++
Using delete this
in C++
The delete
operator should ideally not be used on the this
pointer, as it can lead to undefined behavior if not handled carefully. However, if it is used, the following considerations must be taken into account:
1) The object must be created using new
: The delete
operator only works for objects that have been dynamically allocated using the new
operator. If an object is created on the stack or as a local variable (i.e., without new
), using delete this
will result in undefined behavior.
Example:
#include
using namespace std;
class MyClass {
public:
void destroy() {
delete this; // Deletes the current object
}
};
int main() {
// Valid: Object created using new
MyClass* obj = new MyClass;
obj->destroy();
obj = nullptr; // Ensure pointer is set to null after deletion
// Invalid: Undefined behavior, object created on the stack
MyClass obj2;
obj2.destroy(); // This will cause undefined behavior
return 0;
}
In the valid case, the object is dynamically allocated, and using delete this
will correctly free the memory. In the invalid case, the object is created locally, and deleting a non-dynamic object leads to undefined behavior.
2) Accessing members after delete this
leads to undefined behavior : Once delete this
is called, the object is destroyed, and any attempt to access its members after deletion results in undefined behavior. The program might appear to work, but accessing members of a deleted object is dangerous and unreliable.
Example:
#include
using namespace std;
class MyClass {
int value;
public:
MyClass() : value(42) {}
void destroy() {
delete this;
// Invalid: Undefined behavior
cout << value << endl; // This might work but is unsafe
}
};
int main() {
MyClass* obj = new MyClass;
obj->destroy(); // Calls delete this and tries to access a deleted object
return 0;
}
Output:
42 // This is unpredictable and could vary depending on the system
Passing a Function as a Parameter in C++
In C++, functions can be passed as parameters in various ways. This technique is useful, for instance, when passing custom comparator functions in algorithms like std::sort()
. There are three primary ways to pass a function as an argument:
1. Passing a function pointer
2. Using std::function<>
3. Using lambdas
1. Passing a Function Pointer : A function can be passed to another function by passing its address, which can be done through a pointer.
Example:
#include
using namespace std;
// Function to add two numbers
int add(int x, int y) { return x + y; }
// Function to multiply two numbers
int multiply(int x, int y) { return x * y; }
// Function that takes a pointer to another function
int execute(int x, int y, int (*func)(int, int)) {
return func(x, y);
}
int main() {
// Pass pointers to the 'add' and 'multiply' functions
cout << "Addition of 15 and 5: " << execute(15, 5, &add) << '\n';
cout << "Multiplication of 15 and 5: " << execute(15, 5, &multiply) << '\n';
return 0;
}
Output:
Addition of 15 and 5: 20
Multiplication of 15 and 5: 75
2. Using std::function<> :
From C++11, the std::function<>
template class allows passing functions as objects. A std::function<>
object can be created using the following format:
std::function obj_name;
You can then call the function object like this:
return_type result = obj_name(arg1, arg2);
Example:
#include
#include
using namespace std;
// Define add and multiply functions
int add(int x, int y) { return x + y; }
int multiply(int x, int y) { return x * y; }
// Function that accepts an object of type std::function<>
int execute(int x, int y, function func) {
return func(x, y);
}
int main() {
// Pass the function as a parameter using its name
cout << "Addition of 15 and 5: " << execute(15, 5, add) << '\n';
cout << "Multiplication of 15 and 5: " << execute(15, 5, multiply) << '\n';
return 0;
}
Output:
Addition of 15 and 5: 20
Multiplication of 15 and 5: 75
3. Using Lambdas : Lambdas in C++ provide a way to create anonymous function objects in place. This is particularly useful when you need a function for a specific task and don’t want to define it elsewhere.
Example:
#include
#include
using namespace std;
// Function that accepts a lambda as a parameter
int execute(int x, int y, function func) {
return func(x, y);
}
int main() {
// Lambda for addition
int result1 = execute(15, 5, [](int x, int y) { return x + y; });
cout << "Addition of 15 and 5: " << result1 << '\n';
// Lambda for multiplication
int result2 = execute(15, 5, [](int x, int y) { return x * y; });
cout << "Multiplication of 15 and 5: " << result2 << '\n';
return 0;
}
Output:
Addition of 15 and 5: 20
Multiplication of 15 and 5: 75
Signals in C++
Signals are interrupts that prompt an operating system (OS) to halt its current task and give attention to the task that triggered the interrupt. These signals can pause or interrupt processes running on the OS. Similarly, C++ provides several signals that can be caught and handled within a program. Below is a list of common signals and their associated operations in C++.
Signal | Operation |
---|---|
SIGINT | Produces a receipt for an active signal |
SIGTERM | Sends a termination request to the program |
SIGBUS | Indicates a bus error (e.g., accessing an invalid address) |
SIGILL | Detects an illegal instruction |
SIGALRM | Triggered by the alarm() function when the timer expires |
SIGABRT | Signals abnormal termination of a program |
SIGSTOP | Cannot be blocked, handled, or ignored; stops a process |
SIGSEGV | Indicates invalid access to memory (segmentation fault) |
SIGFPE | Signals erroneous arithmetic operations like division by zero |
SIGUSR1, SIGUSR2 | User-defined signals |
signal()
Function: The signal()
function, provided by the signal library, is used to catch and handle unexpected signals or interrupts in a C++ program.
Syntax:
signal(registered_signal, signal_handler);
- The first argument is an integer that represents the signal number.
- The second argument is a pointer to the function that will handle the signal.
The signal must be registered with a handler function before it can be caught. The handler function should have a return type of void
.
Example:
#include
#include
using namespace std;
void handle_signal(int signal_num) {
cout << "Received interrupt signal (" << signal_num << ").\n";
exit(signal_num); // Terminate the program
}
int main() {
// Register SIGABRT and set the handler
signal(SIGABRT, handle_signal);
while (true) {
cout << "Running program..." << endl;
}
return 0;
}
Output:
Running program...
Running program...
Running program...
When you press Ctrl+C
, which generates an interrupt signal (e.g., SIGABRT), the program will terminate and print:
Received interrupt signal (22).
raise()
Function : The raise()
function is used to generate signals in a program.
Syntax:
raise(signal);
It takes a signal from the predefined list as its argument.
Example:
// C program to demonstrate the use of relational operators
#include
int main() {
int num1 = 12, num2 = 8;
// greater than
if (num1 > num2)
printf("num1 is greater than num2\n");
else
printf("num1 is less than or equal to num2\n");
// greater than or equal to
if (num1 >= num2)
printf("num1 is greater than or equal to num2\n");
else
printf("num1 is less than num2\n");
// less than
if (num1 < num2)
printf("num1 is less than num2\n");
else
printf("num1 is greater than or equal to num2\n");
// less than or equal to
if (num1 <= num2)
printf("num1 is less than or equal to num2\n");
else
printf("num1 is greater than num2\n");
// equal to
if (num1 == num2)
printf("num1 is equal to num2\n");
else
printf("num1 and num2 are not equal\n");
// not equal to
if (num1 != num2)
printf("num1 is not equal to num2\n");
else
printf("num1 is equal to num2\n");
return 0;
}
Output:
Running program...
Running program...
Running program...
Caught signal (11).