Basic CPP
Basic CPP
• Encapsulation: Bundling data and methods that operate on the data within a single
unit (class) and restricting access to some of the object's components. It helps to hide
the internal state of the object.
• Abstraction: Simplifying complex systems by modeling classes appropriate to the
problem, focusing on essential features while hiding unnecessary details.
• Inheritance: Creating new classes from existing classes, enabling code reusability
and the creation of a class hierarchy.
• Polymorphism: Allowing objects to be treated as instances of their parent class. It
supports dynamic method binding, where a function call can invoke different methods
depending on the object type.
• Constructors: Special member functions of a class that are automatically called when
an object is created. They initialize the object's data members. Constructors can be
overloaded to allow different ways of initializing an object.
cpp
Copy code
class Example {
public:
int x;
// Constructor
Example(int val) : x(val) {}
};
• Destructors: Special member functions that are automatically called when an object
is destroyed. They are used for cleanup, such as freeing resources or memory.
cpp
Copy code
class Example {
public:
int* ptr;
// Constructor
Example(int val) {
ptr = new int(val);
}
// Destructor
~Example() {
delete ptr;
}
};
• Default Access Modifier: In C++, the members of a class are private by default,
while in a struct, the members are public by default.
• Usage: Classes are used for complex data structures that encapsulate both data and
methods, while structs are generally used for simple data structures that primarily
hold data.
• Inheritance: Both classes and structs support inheritance in C++, but the default
visibility for inheritance is private for classes and public for structs.
• Public: Members declared as public are accessible from anywhere outside the class.
• Private: Members declared as private are accessible only within the class itself.
• Protected: Members declared as protected are accessible within the class and by
derived classes.
Example:
cpp
Copy code
class Example {
public:
int publicVar; // Accessible anywhere
private:
int privateVar; // Accessible only within the class
protected:
int protectedVar; // Accessible within the class and derived classes
};
• The this pointer is an implicit parameter to all non-static member functions in C++.
It points to the object for which the member function is called.
• It is used to differentiate between member variables and parameters with the same
name, and for chaining member functions.
• Example:
cpp
Copy code
class Example {
public:
int value;
Example(int value) {
this->value = value; // Using 'this' to differentiate between
parameter and member variable
}
Example& setValue(int value) {
this->value = value;
return *this; // Returning the current object
}
};
These concepts are fundamental to understanding C++ and writing efficient, object-oriented
code.
• Stack Memory:
o Structure: Follows a Last-In-First-Out (LIFO) structure for managing
memory.
o Automatic Allocation: Memory allocation and deallocation are done
automatically when a function is called or exits. Local variables are stored on
the stack.
o Size Limitations: Limited in size, and exceeding the stack's capacity can
cause a stack overflow.
o Access Speed: Faster access compared to heap memory due to its simple
memory management.
o Lifetime: Memory is automatically freed when the function exits, and
variables go out of scope.
• Heap Memory:
o Structure: Memory is dynamically allocated and managed by the
programmer.
o Manual Allocation: Requires explicit allocation (new or malloc) and
deallocation (delete or free).
o Size Flexibility: Generally larger and more flexible than stack memory.
o Access Speed: Slower access compared to stack memory due to the
complexity of dynamic memory management.
o Lifetime: Memory must be explicitly freed by the programmer; otherwise, it
will remain allocated until the program terminates.
cpp
Copy code
int a = 10; // Allocated on the stack
cpp
Copy code
int* ptr = new int; // Allocating an integer on the heap
*ptr = 20;
delete ptr; // Deallocating the memory
cpp
Copy code
int* arr = new int[10]; // Allocating an array on the heap
delete[] arr; // Deallocating the array memory
Smart pointers are classes that manage dynamically allocated memory, automatically
handling memory deallocation. They help prevent memory leaks by ensuring proper cleanup
of resources.
• std::unique_ptr:
o Represents exclusive ownership of a dynamically allocated object.
o Only one std::unique_ptr can manage a specific resource, and it cannot be
copied but can be moved.
o Example:
cpp
Copy code
std::unique_ptr<int> ptr = std::make_unique<int>(10);
• std::shared_ptr:
o Represents shared ownership of a dynamically allocated object.
o Multiple std::shared_ptr instances can manage the same resource. The
resource is deallocated when the last std::shared_ptr goes out of scope.
o Example:
cpp
Copy code
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::shared_ptr<int> ptr2 = ptr1; // Both ptr1 and ptr2 share
ownership
• std::weak_ptr:
o A non-owning reference to a resource managed by a std::shared_ptr.
o Prevents circular references by allowing access to a resource without
increasing its reference count.
o Use lock() to convert a std::weak_ptr to a std::shared_ptr if the
resource is still available.
o Example:
cpp
Copy code
std::shared_ptr<int> sharedPtr = std::make_shared<int>(30);
std::weak_ptr<int> weakPtr = sharedPtr; // Does not increase
reference count
• Memory Leak:
o Occurs when dynamically allocated memory is not properly deallocated,
causing the memory to remain inaccessible even though it's no longer needed.
o Over time, memory leaks can exhaust available memory, leading to poor
performance or application crashes.
• Prevention Techniques:
o Manual Deallocation: Ensure every new or malloc has a corresponding
delete or free.
o Use of Smart Pointers: Utilizing smart pointers like std::unique_ptr and
std::shared_ptr automates memory management, significantly reducing the
risk of memory leaks.
o Avoid Raw Pointers: Prefer smart pointers over raw pointers for managing
dynamic memory.
o Static Analysis Tools: Use tools like Valgrind or AddressSanitizer to detect
memory leaks during development.
• Pointers:
o A pointer is a variable that stores the memory address of another variable.
o It can be reassigned to point to different memory addresses.
o Supports pointer arithmetic (e.g., incrementing to point to the next memory
location).
o Can be nullptr (i.e., it does not point to any memory location).
o Needs to be dereferenced using the * operator to access the value stored at the
memory address.
o Example:
cpp
Copy code
int a = 5;
int* ptr = &a; // ptr points to the memory address of a
• References:
o A reference is an alias for an existing variable and cannot be reassigned to
refer to a different variable once initialized.
o Must be initialized when declared.
o Does not support pointer arithmetic.
o Cannot be nullptr; it must always refer to a valid object.
o Automatically dereferenced when used, making syntax simpler.
o Example:
cpp
Copy code
int a = 5;
int& ref = a; // ref is an alias for a
The const keyword can be used with pointers and references to indicate that the value being
pointed to or referred to should not be modified.
• Pointers:
o Pointer to a const value: The value pointed to by the pointer cannot be
modified, but the pointer itself can be changed to point to another variable.
cpp
Copy code
const int* ptr = &a; // Pointer to a const int
*ptr = 10; // Error: cannot modify the value
ptr = &b; // Allowed: can change the address stored in ptr
cpp
Copy code
int* const ptr = &a; // Const pointer to an int
*ptr = 10; // Allowed: can modify the value
ptr = &b; // Error: cannot change the address stored in ptr
o const pointer to a const value: Neither the pointer nor the value it points to
can be modified.
cpp
Copy code
const int* const ptr = &a; // Const pointer to a const int
*ptr = 10; // Error: cannot modify the value
ptr = &b; // Error: cannot change the address stored in ptr
• References:
o const reference: A reference to a constant value that cannot be modified.
cpp
Copy code
const int& ref = a; // Const reference to an int
ref = 10; // Error: cannot modify the value
• Dangling Pointer:
o A dangling pointer is a pointer that refers to a memory location that has been
deallocated or released. Accessing or modifying such a memory location leads
to undefined behavior.
o Dangling pointers typically occur in scenarios such as:
▪ Deleting a dynamically allocated object and not setting the pointer to
nullptr.
▪ Returning a pointer to a local variable from a function.
▪ Using a pointer after the object it points to has gone out of scope.
• Avoiding Dangling Pointers:
o Set Pointers to nullptr After Deleting: After freeing memory with delete,
set the pointer to nullptr to avoid accidentally accessing invalid memory.
cpp
Copy code
int* ptr = new int(10);
delete ptr;
ptr = nullptr; // Prevents dangling
cpp
Copy code
int* getPointer() {
int a = 10;
return &a; // Dangerous, returns address of a local
variable
}
The STL provides several containers for storing and managing collections of data. Some
common containers include:
• vector:
o A dynamic array that can resize itself automatically when elements are added
or removed.
o Supports random access to elements in constant time (O(1)).
o Efficient for insertion and deletion at the end but slower for inserting or
deleting elements in the middle (O(n)).
• list:
o A doubly linked list that allows for efficient insertion and deletion of elements
from any position (O(1) for insertions/deletions).
o Does not support random access (O(n) time to access an element).
• deque (double-ended queue):
o Supports fast insertions and deletions at both ends (O(1)).
o Provides random access similar to a vector.
• map:
o An associative container that stores key-value pairs, with unique keys in sorted
order.
o Uses a self-balancing binary search tree internally (usually a Red-Black Tree).
• unordered_map:
o Similar to map but does not maintain any specific order for the elements.
o Uses a hash table internally for fast insertion and lookup.
• set:
o Stores unique elements in a sorted order.
o Allows fast lookup, insertion, and deletion (O(log n)).
• unordered_set:
o Stores unique elements without maintaining any particular order.
o Uses a hash table for fast insertion and lookup (O(1) average time
complexity).
16. How a map Works and Its Time Complexity for Insertion and Lookup
• Structure:
o A map is an associative container that stores key-value pairs with unique keys,
where each key is associated with a specific value.
o Internally, a map uses a self-balancing binary search tree (usually a Red-Black
Tree), which keeps the elements sorted based on the key.
• Time Complexity:
o Insertion: Insertion of an element takes O(log n) time because the tree needs
to be adjusted to maintain balance.
o Lookup: Lookup for a key also takes O(log n) time, as it requires traversing
the tree.
o Deletion: Deleting an element has a time complexity of O(log n).
• Usage Example:
cpp
Copy code
std::map<int, std::string> myMap;
myMap[1] = "One";
myMap[2] = "Two";
// Lookup
std::string value = myMap[1]; // O(log n)
Polymorphism is the ability of a function, object, or variable to take multiple forms. In C++,
it allows a base class reference or pointer to refer to objects of derived classes, enabling the
calling of derived class functions through base class references. Polymorphism is a key
concept in Object-Oriented Programming (OOP) and is mainly achieved through function
overloading, operator overloading, and inheritance.
cpp
Copy code
#include <iostream>
using namespace std;
// Base class
class Shape {
public:
virtual void draw() {
cout << "Drawing a shape" << endl;
}
};
// Derived class
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
// Derived class
class Rectangle : public Shape {
public:
void draw() override {
cout << "Drawing a rectangle" << endl;
}
};
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
delete shape1;
delete shape2;
return 0;
}
In the example above, polymorphism allows different derived class methods (Circle and
Rectangle) to be called through a base class (Shape) pointer.
• Compile-Time Polymorphism:
o Achieved using function overloading and operator overloading.
o The decision about which function or operator to use is made during compile
time.
o Function Overloading: Multiple functions with the same name but different
parameters.
cpp
Copy code
void print(int i) {
cout << "Integer: " << i << endl;
}
void print(double d) {
cout << "Double: " << d << endl;
}
cpp
Copy code
class Base {
public:
int data;
};
In the above scenario, Final contains two separate copies of Base. This can lead to
ambiguity when accessing members of the Base class.
cpp
Copy code
class Base {
public:
int data;
};
class Final : public Derived1, public Derived2 { // Only one copy of Base
is inherited
// No ambiguity when accessing Base's members
};
• The diamond problem arises when a class is derived from two classes that share a
common base class.
• This leads to ambiguity because the derived class has two copies of the base class.
cpp
Copy code
class A {
public:
void display() {
cout << "Class A" << endl;
}
};
class B : public A { };
class C : public A { };
In the above example, class D inherits from both B and C, which in turn inherit from A. This
results in two copies of A in D, leading to ambiguity when calling A's methods.
cpp
Copy code
class A {
public:
void display() {
cout << "Class A" << endl;
}
};
Operator overloading in C++ allows developers to redefine the behavior of operators for user-
defined types (e.g., classes). It enables the use of standard operators like +, -, ==, etc., with
custom classes, making the code more intuitive and readable. The overloaded operator is
implemented as a function, either as a member function or a non-member (friend) function.
• Member Function: If the left operand is an object of the class, the operator can be
overloaded as a member function.
• Non-Member Function: If the left operand is not an object of the class (e.g., primitive type),
a non-member function is used.
cpp
Copy code
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, 2.0);
Complex c3 = c1 + c2; // Uses the overloaded + operator
return 0;
}
In this example, the + operator is overloaded for the Complex class to add the real and
imaginary parts of two Complex objects.
The Rule of Three (or Rule of Five) is a guideline for managing resources in C++ classes
that handle dynamic memory or other resources. It states that if a class requires a user-defined
destructor, copy constructor, or copy assignment operator, then it likely requires all three.
In C++11 and later, the Rule of Five extends this to include the move constructor and move
assignment operator.
1. Destructor:
o Cleans up resources when an object goes out of scope.
o If a class allocates memory dynamically, it needs a destructor to free that memory.
2. Copy Constructor:
o Creates a new object as a copy of an existing object.
o Needed to handle deep copying when objects are copied.
3. Copy Assignment Operator (operator=):
o Assigns values from one object to another already existing object.
o Should handle self-assignment and manage any previously allocated resources.
4. Move Constructor:
o Transfers resources from a temporary object (rvalue) to a new object.
o Leaves the source object in a valid but unspecified state.
5. Move Assignment Operator:
o Transfers resources from a temporary object to an existing object.
o Handles resource cleanup for the existing object.
Why Is It Important? The Rule of Three/Five ensures that classes manage resources
correctly, avoiding issues like memory leaks, double deletion, or undefined behavior. It is
especially important for classes that manage dynamic memory, file handles, sockets, or any
other resources that need explicit management.
cpp
Copy code
#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
char* data;
size_t length;
public:
// Constructor
String(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// Destructor
~String() {
delete[] data;
}
// Copy Constructor
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
return *this;
}
// Move Constructor
String(String&& other) noexcept : data(other.data),
length(other.length) {
other.data = nullptr; // Leave the source object in a valid state
other.length = 0;
}
// Move Assignment Operator
String& operator=(String&& other) noexcept {
if (this == &other) {
return *this; // Self-assignment check
}
data = other.data;
length = other.length;
return *this;
}
int main() {
String str1("Hello");
String str2 = str1; // Calls copy constructor
String str3;
str3 = str1; // Calls copy assignment operator
str3.display();
str4.display();
str5.display();
return 0;
}
In C++, threads can be created and managed using the <thread> library, which provides a
simple interface for multithreading. A thread can be created by passing a function (or callable
object) to the std::thread constructor.
cpp
Copy code
#include <iostream>
#include <thread>
void printMessage() {
std::cout << "Hello from the thread!" << std::endl;
}
int main() {
std::thread t(printMessage); // Create a new thread that runs
printMessage
// Wait for the thread to finish (join)
t.join(); // Ensures the main thread waits for t to complete
return 0;
}
In this example:
Managing Threads:
• Joining (join()): The join method ensures the main thread waits for the spawned thread
to finish execution. It is important to call join to avoid program termination before threads
complete.
• Detaching (detach()): detach allows the thread to run independently, continuing its
execution even if the main thread completes. Once detached, the thread cannot be joined.
cpp
Copy code
std::thread t(printMessage);
t.detach(); // t runs independently from the main thread
Race Conditions
• A race condition occurs when two or more threads access shared data simultaneously, and
at least one thread modifies the data. The outcome of the program becomes unpredictable
because the sequence of execution by the threads is not guaranteed.
Example: If two threads are incrementing the same global counter, the final value of the
counter depends on the interleaving of thread execution, potentially leading to incorrect
results.
• Use synchronization mechanisms such as mutexes or locks to control access to shared data.
• Use atomic operations for simple operations on shared variables (e.g., std::atomic).
Deadlocks
• A deadlock occurs when two or more threads are waiting on each other to release resources,
resulting in a standstill where none of the threads can proceed.
Example: Thread A locks mutex1 and waits for mutex2, while thread B locks mutex2 and
waits for mutex1. Neither thread can proceed, causing a deadlock.
Preventing Deadlocks:
A mutex (mutual exclusion) is a synchronization primitive that ensures only one thread can
access a critical section of code at a time. In C++, std::mutex is used for this purpose, with
the following common functions:
• lock(): Acquires the lock. If the mutex is already locked, it blocks until the mutex becomes
available.
• unlock(): Releases the lock, allowing other threads to acquire it.
• try_lock(): Attempts to acquire the lock without blocking. Returns immediately with a
success or failure status.
cpp
Copy code
#include <iostream>
#include <thread>
#include <mutex>
void incrementCounter() {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // Locks mtx
++sharedCounter;
// mtx is automatically unlocked when lock goes out of scope
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << sharedCounter << std::endl;
return 0;
}
• The std::mutex (mtx) is used to protect the shared variable sharedCounter from
simultaneous modification.
• A std::lock_guard automatically locks the mutex when it is created and unlocks it when
it goes out of scope, ensuring exception safety.
• std::lock_guard:
o A simpler way to lock a mutex.
o Automatically locks the mutex on creation and unlocks it on destruction (scope-
based locking).
• std::unique_lock:
o More flexible than std::lock_guard.
o Can lock and unlock the mutex multiple times, and supports deferred locking.
cpp
Copy code
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // Create the lock
but don't lock yet
// Perform some operations
lock.lock(); // Lock the mutex
// Critical section
lock.unlock(); // Unlock the mutex
Summary
• Creating and managing threads in C++ involves using std::thread, along with proper
synchronization techniques to avoid race conditions and deadlocks.
• Race conditions occur when multiple threads access shared resources without proper
synchronization, while deadlocks happen when threads wait indefinitely for resources locked
by each other.
• Mutexes and locks are used to control access to shared resources, ensuring that only one
thread executes a critical section at a time. Proper use of synchronization mechanisms like
std::lock_guard and std::unique_lock helps prevent these issues.
Templates in C++ are a powerful feature that allows writing generic, type-independent code.
They enable functions and classes to operate with different data types without rewriting the
entire code for each type. Templates help achieve code reusability and type safety while
avoiding code duplication.
Templates are particularly useful in implementing generic data structures (e.g., linked lists,
stacks) and algorithms (e.g., sorting, searching), where the operations are independent of the
data type.
cpp
Copy code
#include <iostream>
using namespace std;
// Template function to find the maximum of two values
template <typename T>
T getMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << "Max of 3 and 7 is: " << getMax(3, 7) << endl; // Uses int
cout << "Max of 3.5 and 7.2 is: " << getMax(3.5, 7.2) << endl; // Uses
double
return 0;
}
In this example, the getMax function works for both integers and floating-point numbers
without having separate implementations for each type.
• Function Templates:
o Allow functions to operate on different data types while using the same function
definition.
o The template parameter specifies the data type when the function is called.
o Syntax: template <typename T>, where T is the template parameter that can
represent any data type.
cpp
Copy code
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(3, 4) << endl; // Calls add<int>
cout << add(3.5, 4.5) << endl; // Calls add<double>
return 0;
}
• Class Templates:
o Allow the creation of generic classes that can operate with different data types.
o The template parameter is specified when an instance of the class is created.
o Useful for implementing data structures like vector, stack, queue, etc., which should
work with various data types.
cpp
Copy code
template <typename T>
class MyContainer {
private:
T data;
public:
MyContainer(T d) : data(d) {}
T getData() const { return data; }
};
int main() {
MyContainer<int> intContainer(10);
MyContainer<double> doubleContainer(5.5);
cout << "Integer container: " << intContainer.getData() << endl;
cout << "Double container: " << doubleContainer.getData() <<
endl;
return 0;
}
In the example, MyContainer is a class template that can store different data types such as
int or double, based on the template parameter provided.
Template specialization allows customizing the behavior of a template for a specific type. It
is used when the general implementation of a template does not meet the needs for certain
data types, allowing for specific code to be written for those types.
In full template specialization, a template is fully specialized for a specific data type.
cpp
Copy code
#include <iostream>
using namespace std;
// Generic template
template <typename T>
class Printer {
public:
void print(const T& data) {
cout << "Generic printer: " << data << endl;
}
};
int main() {
Printer<double> doublePrinter;
Printer<int> intPrinter;
In this example, the Printer class is fully specialized for int, allowing a different
implementation for integer printing compared to other types.
cpp
Copy code
#include <iostream>
using namespace std;
// Generic template
template <typename T, typename U>
class Pair {
public:
T first;
U second;
Pair(T f, U s) : first(f), second(s) {}
void display() const {
cout << "Pair: " << first << ", " << second << endl;
}
};
// Partial specialization for the case where both types are the same
template <typename T>
class Pair<T, T> {
public:
T first;
T second;
Pair(T f, T s) : first(f), second(s) {}
void display() const {
cout << "Specialized Pair (same type): " << first << ", " << second
<< endl;
}
};
int main() {
Pair<int, double> mixedPair(3, 4.5);
Pair<int, int> intPair(5, 10);
return 0;
}
In this example, the Pair class template is partially specialized for cases where both temp
32. Reading from and Writing to Files in C++
In C++, file handling is done using the fstream library, which provides functionalities to
read from and write to files. The primary classes used for file handling are:
cpp
Copy code
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream outFile("example.txt"); // Open a file for writing
if (!outFile) {
cerr << "Error opening file for writing." << endl;
return 1;
}
return 0;
}
In this example:
• An output file stream (ofstream) is created, which opens "example.txt" for writing.
• Data is written to the file using the stream insertion operator (<<).
• The file is closed using the close() method.
cpp
Copy code
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream inFile("example.txt"); // Open a file for reading
if (!inFile) {
cerr << "Error opening file for reading." << endl;
return 1;
}
string line;
while (getline(inFile, line)) { // Read line by line
cout << line << endl; // Output to the console
}
In this example:
When opening a file in C++, you can specify different modes that dictate how the file will be
accessed. Here are the common modes:
• ios::in: Open the file for reading. The file must exist; otherwise, the open operation
fails.
• ios::out: Open the file for writing. If the file already exists, it is truncated (emptied).
If the file does not exist, it is created.
• ios::app: Open the file for appending. Data is written to the end of the file,
preserving existing data. The file is created if it does not exist.
• ios::ate: Open the file and move the read/write control to the end of the file. This
can be useful for seeking and writing data at the end without truncating the file.
• ios::trunc: If the file is opened for output (ios::out), this mode truncates the file
to zero length, effectively clearing its contents.
• ios::binary: Open the file in binary mode rather than text mode. This mode is
useful for reading or writing non-text files (e.g., images, executables).
Combining Modes
You can combine multiple modes using the bitwise OR operator (|). For example:
cpp
Copy code
std::ofstream outFile("example.txt", ios::out | ios::app | ios::binary);
In this example, the file "example.txt" is opened for appending (ios::app), writing
(ios::out), and in binary mode (ios::binary).
Summary
• Reading and writing to files in C++ is done using the fstream library with ifstream for
reading and ofstream for writing.
• Different file opening modes include ios::in, ios::out, ios::app, ios::ate,
ios::trunc,