Introduction to C++

C++ is a general-purpose programming language created by Bjarne Stroustrup as an extension of the C programming language. It was first introduced in 1985 and provides object-oriented features like classes and inheritance. C++ is widely used in various applications like game development, system programming, embedded systems, and high-performance computing.

C++ is a statically-typed language, meaning that the type of a variable is determined during compilation, and has an extensive library called the C++ Standard Library, which provides a rich set of functions, algorithms, and data structures for various tasks.

C++ builds upon the features of C, and thus, most C programs can be compiled and run with a C++ compiler.

Code Example

Here's a simple example of a C++ program that demonstrates some essential features of the language:

#include <iostream>

// A simple function to add two numbers
int add(int a, int b) { return a + b; }

class Calculator {
  public:
    // A member function to multiply two numbers
    int multiply(int a, int b) { return a * b; }
};

int main() {
    int x = 5;
    int y = 3;

    // Using the standalone function 'add'
    int sum = add(x, y);
    std::cout << "Sum: " << sum << std::endl;

    // Using a class and member function
    Calculator calc;
    int product = calc.multiply(x, y);
    std::cout << "Product: " << product << std::endl;

    return 0;
}

meme

C vs C++

C and C++ are two popular programming languages with some similarities, but they also have key differences. C++ is an extension of the C programming language, with added features such as object-oriented programming, classes, and exception handling. Although both languages are used for similar tasks, they have their own syntax and semantics, which makes them distinct from each other.

Syntax and Semantics

C

  • C is a procedural programming language.
  • Focuses on functions and structured programming.
  • Does not support objects or classes.
  • Memory management is manual, using functions like malloc and free.
#include <stdio.h>

void printHello() {
    printf("Hello, World!\n");
}

int main() {
    printHello();
    return 0;
}

C++

  • C++ is both procedural and object-oriented.
  • Supports both functions and classes.
  • Incorporates different programming paradigms.
  • Memory management can be manual (like C) or rely on constructors/destructors and smart pointers.
#include <iostream>

class HelloWorld {
  public:
    void printHello() { std::cout << "Hello, World!" << std::endl; }
};

int main() {
    HelloWorld obj;
    obj.printHello();
    return 0;
}

Code Reusability and Modularity

C

  • Code reusability is achieved through functions and modular programming.
  • High cohesion and low coupling are achieved via structured design.
  • Function libraries can be created and included through headers.

C++

  • Offers better code reusability with classes, inheritance, and polymorphism.
  • Code modularity is enhanced through namespaces and well-designed object-oriented hierarchy.

Error Handling

C

  • Error handling in C is done primarily through return codes.
  • Lacks support for exceptions or any built-in error handling mechanism.

C++

  • Offers exception handling, which can be used to handle errors that may occur during program execution.
  • Enables catching and handling exceptions with try, catch, and throw keywords, providing more control over error handling.

Conclusion

c-vs-c++

Both C and C++ are powerful languages with unique features and capabilities. While C is simpler and focuses on procedural programming, C++ offers the versatility of using different programming paradigms and improved code organization. Understanding the differences between these two languages can help you decide which one is more suitable for your specific needs and programming style.

Basics of C++ Programming

Here are some basic components and concepts in C++ programming:

Including Libraries

In C++, we use the #include directive to include libraries or header files into our program. For example, to include the standard input/output library, we write:

#include <iostream>

Main Function

The entry point of a C++ program is the main function. Every C++ program must have a main function:

int main() {
    // Your code goes here
    return 0;
}

Input/Output

To perform input and output operations in C++, we can use the built-in objects std::cin for input and std::cout for output, available in the iostream library. Here's an example of reading an integer and printing its value:

#include <iostream>

int main() {
    int number;
    std::cout << "Enter an integer: ";
    std::cin >> number;
    std::cout << "You entered: " << number << std::endl;
    return 0;
}

Variables and Data Types

C++ has several basic data types for representing integer, floating-point, and character values:

  • int: integer values
  • float: single-precision floating-point values
  • double: double-precision floating-point values
  • char: single characters

Variables must be declared with a data type before they can be used:

int x;
float y;
double z;
char c;

Control Structures

C++ provides control structures for conditional execution and iteration, such as if, else, while, for, and switch statements.

If-Else Statement

if (condition) {
    // Code to execute if the condition is true
} else {
    // Code to execute if the condition is false
}

While Loop

while (condition) {
    // Code to execute while the condition is true
}

For Loop

for (initialization; condition; update) {
    // Code to execute while the condition is true
}

Switch Statement

switch (variable) {
case value1:
    // Code to execute if variable == value1
    break;
case value2:
    // Code to execute if variable == value2
    break;
// More cases...
default:
    // Code to execute if variable does not match any case value
}

Functions

Functions are reusable blocks of code that can be called with arguments to perform a specific task. Functions are defined with a return type, a name, a parameter list, and a body.

ReturnType functionName(ParameterType1 parameter1, ParameterType2 parameter2) {
    // Function body
    // ...
    return returnValue;
}

For example, here's a function that adds two integers and returns the result:

int add(int a, int b) { return a + b; }

int main() {
    int result = add(3, 4);
    std::cout << "3 + 4 = " << result << std::endl;
    return 0;
}

This basic introduction to C++ should provide you with a good foundation for further learning. Explore more topics such as classes, objects, inheritance, polymorphism, templates, and the Standard Template Library (STL) to deepen your understanding of C++ and start writing more advanced programs.

Loops in C++

Loops are an essential concept in programming that allow you to execute a block of code repeatedly until a specific condition is met. In C++, there are three main types of loops: for, while, and do-while.

For Loop

A for loop is used when you know the number of times you want to traverse through a block of code. It consists of an initialization statement, a condition, and an increment/decrement operation.

Here's the syntax for a for loop:

for (initialization; condition; increment / decrement) {
    // block of code to execute
}

For example:

#include <iostream>
using namespace std;

int main() {
    for (int i = 0; i < 5; i++) {
        cout << "Iteration: " << i << endl;
    }
    return 0;
}

While Loop

A while loop runs as long as a specified condition is true. The loop checks for the condition before entering the body of the loop.

Here's the syntax for a while loop:

while (condition) {
    // block of code to execute
}

For example:

#include <iostream>
using namespace std;

int main() {
    int i = 0;
    while (i < 5) {
        cout << "Iteration: " << i << endl;
        i++;
    }
    return 0;
}

Do-While Loop

A do-while loop is similar to a while loop, with the key difference being that the loop body is executed at least once, even when the condition is false.

Here's the syntax for a do-while loop:

do {
    // block of code to execute
} while (condition);

For example:

#include <iostream>
using namespace std;

int main() {
    int i = 0;
    do {
        cout << "Iteration: " << i << endl;
        i++;
    } while (i < 5);
    return 0;
}

While vs Do-While Loop

while-vs-do-while

Summary

In summary, loops are an integral part of C++ programming that allow you to execute a block of code multiple times. The three types of loops in C++ are for, while, and do-while. Each type has its own specific use case and can be chosen depending on the desired behavior.

Bitwise Operations

Bitwise operations are operations that directly manipulate the bits of a number. Bitwise operations are useful for various purposes, such as optimizing algorithms, performing certain calculations, and manipulating memory in lower-level programming languages like C and C++.

Here is a quick summary of common bitwise operations in C++:

Bitwise AND (&)

The bitwise AND operation (&) is a binary operation that takes two numbers, compares them bit by bit, and returns a new number where each bit is set (1) if the corresponding bits in both input numbers are set (1); otherwise, the bit is unset (0).

Example:

int result = 5 & 3; // result will be 1 (0000 0101 & 0000 0011 = 0000 0001)

Bitwise OR (|)

The bitwise OR operation (|) is a binary operation that takes two numbers, compares them bit by bit, and returns a new number where each bit is set (1) if at least one of the corresponding bits in either input number is set (1); otherwise, the bit is unset (0).

Example:

int result = 5 | 3; // result will be 7 (0000 0101 | 0000 0011 = 0000 0111)

Bitwise XOR (^)

The bitwise XOR (exclusive OR) operation (^) is a binary operation that takes two numbers, compares them bit by bit, and returns a new number where each bit is set (1) if the corresponding bits in the input numbers are different; otherwise, the bit is unset (0).

Example:

int result = 5 ^ 3; // result will be 6 (0000 0101 ^ 0000 0011 = 0000 0110)

Bitwise NOT (~)

The bitwise NOT operation (~) is a unary operation that takes a single number, and returns a new number where each bit is inverted (1 becomes 0, and 0 becomes 1).

Example:

int result = ~5; // result will be -6 (1111 1010)

Bitwise Left Shift (<<)

The bitwise left shift operation (<<) is a binary operation that takes two numbers, a value and a shift amount, and returns a new number by shifting the bits of the value to the left by the specified shift amount. The vacated bits are filled with zeros.

Note: This can be used to multiply numbers by powers of 2

Example:

int result = 5 << 1; // result will be 10 (0000 0101 << 1 = 0000 1010)
int result2 = 5 << 2; // result will be 20 which is 5 * (2^2)

Bitwise Right Shift (>>)

The bitwise right shift operation (>>) is a binary operation that takes two numbers, a value and a shift amount, and returns a new number by shifting the bits of the value to the right by the specified shift amount. The vacated bits are filled with zeros or sign bit depending on the input value being signed or unsigned.

Note: This can be used to divide numbers by powers of 2

Example:

int result = 5 >> 1; // result will be 2 (0000 0101 >> 1 = 0000 0010)
int result2 = 100 >> 2; // result will be 25 which is 100 / (2^2)

These were the most common bitwise operations in C++. Remember to use them carefully and understand their behavior when applied to specific data types and scenarios.

Logical Operators in C++

Logical operators are used to perform logical operations on the given expressions, mostly to test the relationship between different variables or values. They return a boolean value i.e., either true (1) or false (0) based on the result of the evaluation.

C++ provides the following logical operators:

  • AND Operator (&&) The AND operator checks if both the operands/conditions are true, then the expression is true. If any one of the conditions is false, the whole expression will be false.

    (expression1 && expression2)
    

    Example:

    int a = 5, b = 10;
    if (a > 0 && b > 0) {
        cout << "Both values are positive." << endl;
    }
    
  • OR Operator (||) The OR operator checks if either of the operands/conditions are true, then the expression is true. If both the conditions are false, it will be false.

    (expression1 || expression2)
    

    Example:

    int a = 5, b = -10;
    if (a > 0 || b > 0) {
        cout << "At least one value is positive." << endl;
    }
    
  • NOT Operator (!) The NOT operator reverses the result of the condition/expression it is applied on. If the condition is true, the NOT operator will make it false and vice versa.

    !(expression)
    

    Example:

    int a = 5;
    if (!(a < 0)) {
        cout << "The value is not negative." << endl;
    }
    

Using these operators, you can create more complex logical expressions, for example:

int a = 5, b = -10, c = 15;

if (a > 0 && (b > 0 || c > 0)) {
    cout << "At least two values are positive." << endl;
}

This covers the essential information about logical operators in C++.

Arithmetic Operators in C++

Arithmetic operators are used to perform mathematical operations with basic variables such as integers and floating-point numbers. Here is a brief summary of the different arithmetic operators in C++:

1. Addition Operator (+)

It adds two numbers together.

int sum = a + b;

2. Subtraction Operator (-)

It subtracts one number from another.

int difference = a - b;

3. Multiplication Operator (*)

It multiplies two numbers together.

int product = a * b;

4. Division Operator (/)

It divides one number by another. Note that if both operands are integers, it will perform integer division and the result will be an integer.

int quotient = a / b;                 // integer division
float quotient = float(a) / float(b); // floating-point division

5. Modulus Operator (%)

It calculates the remainder of an integer division.

int remainder = a % b;

6. Increment Operator (++)

It increments the value of a variable by 1. There are two ways to use this operator: prefix (++x) and postfix (x++). Prefix increments the value before returning it, whereas postfix returns the value first and then increments it.

int x = 5;
int y = ++x; // x = 6, y = 6
int z = x++; // x = 7, z = 6

7. Decrement Operator (--)

It decrements the value of a variable by 1. It can also be used in prefix (--x) and postfix (x--) forms.

int x = 5;
int y = --x; // x = 4, y = 4
int z = x--; // x = 3, z = 4

These are the basic arithmetic operators in C++ that allow you to perform mathematical operations on your variables. Use them in combination with other control structures, such as loops and conditionals, to build more complex programs.

Functions in C++

A function is a group of statements that perform a specific task, organized as a separate unit in a program. Functions help in breaking the code into smaller, manageable, and reusable blocks.

There are mainly two types of functions in C++:

  • Standard library functions: Pre-defined functions available in the C++ standard library, such as printf(), scanf(), sqrt(), and many more. These functions are part of the standard library, so you need to include the appropriate header file to use them.

  • User-defined functions: Functions created by the programmer to perform a specific task. To create a user-defined function, you need to define the function and call it in your code.

Defining a Function

The general format for defining a function in C++ is:

return_type function_name(parameter list) {
    // function body
}
  • return_type: Data type of the output produced by the function. It can be void, indicating that the function doesn't return any value.
  • function_name: Name given to the function, following C++ naming conventions.
  • parameter list: List of input parameters/arguments that are needed to perform the task. It is optional, and when no parameters are needed, you can leave it blank or use the keyword void.

Example

#include <iostream>
using namespace std;

// Function to add two numbers
int addNumbers(int a, int b) {
    int sum = a + b;
    return sum;
}

int main() {
    int num1 = 5, num2 = 10;
    int result = addNumbers(num1, num2); // Calling the function
    cout << "The sum is: " << result << endl;
    return 0;
}

In this example, the function addNumbers takes two integer parameters, a and b, and returns the sum of the numbers. We then call this function from the main() function and display the result.

Function Prototypes

In some cases, you might want to use a function before actually defining it. To do this, you need to declare a function prototype at the beginning of your code.

A function prototype is a declaration of the function without its body, and it informs the compiler about the function's name, return type, and parameters.

#include <iostream>
using namespace std;

// Function prototype
int multiplyNumbers(int x, int y);

int main() {
    int num1 = 3, num2 = 7;
    int result = multiplyNumbers(num1, num2); // Calling the function
    cout << "The product is: " << result << endl;
    return 0;
}

// Function definition
int multiplyNumbers(int x, int y) {
    int product = x * y;
    return product;
}

In this example, we use a function prototype for multiplyNumbers() before defining it. This way, we can call the function from the main() function even though it hasn't been defined yet in the code.

Functions with trailing return types

In this type of declaration is done in following ways:

auto function_name(parameters) -> return_type {
    // function body
}

This kind of declaration is used in lambdas and also preferred to be used in templates

Lambdas:

auto function_name = []() -> return_type { /*function body*/ }

Templates:

template <typename T, typename U>
auto function_name(T t, U u) -> decltype(t + u) {
    return t + u;
}

Lambda Functions in C++

A lambda function, or simply "lambda", is an anonymous (unnamed) function that is defined in place, within your source code, and with a concise syntax. Lambda functions were introduced in C++11 and have since become a widely used feature, especially in combination with the Standard Library algorithms.

Syntax

Here is a basic syntax of a lambda function in C++:

[capture-list](parameters) -> return_type {
    // function body
};
  • capture-list: A list of variables from the surrounding scope that the lambda function can access.
  • parameters: The list of input parameters, just like in a regular function. Optional.
  • return_type: The type of the value that the lambda function will return. This part is optional, and the compiler can deduce it in many cases.
  • function body: The code that defines the operation of the lambda function.

Usage Examples

Here are a few examples to demonstrate the use of lambda functions in C++:

  • Lambda function with no capture, parameters, or return type.
auto printHello = []() {
    std::cout << "Hello, World!" << std::endl;
};
printHello(); // Output: Hello, World!
  • Lambda function with parameters.
auto add = [](int a, int b) {
    return a + b;
};
int result = add(3, 4); // result = 7
  • Lambda function with capture-by-value.
int multiplier = 3;
auto times = [multiplier](int a) {
    return a * multiplier;
};
int result = times(5); // result = 15
  • Lambda function with capture-by-reference.
int expiresInDays = 45;
auto updateDays = [&expiresInDays](int newDays) {
    expiresInDays = newDays;
};
updateDays(30); // expiresInDays = 30

Note that, when using the capture by reference, any change made to the captured variable inside the lambda function will affect its value in the surrounding scope.

Data Types in C++

In C++, data types are used to categorize different types of data that a program can process. They are essential for determining the type of value a variable can hold and how much memory space it will occupy. Some basic data types in C++ include integers, floating-point numbers, characters, and booleans.

Fundamental Data Types

Integer (int)

Integers are whole numbers that can store both positive and negative values. The size of int depends on the system architecture (usually 4 bytes).

Example:

int num = 42;

There are variants of int that can hold different ranges of numbers:

  • short (short int): Smaller range than int.
  • long (long int): Larger range than int.
  • long long (long long int): Even larger range than long int.

Floating-Point (float, double)

Floating-point types represent real numbers, i.e., numbers with a decimal point. There are two main floating-point types:

  • float: Provides single-precision floating-point numbers. It typically occupies 4 bytes of memory.

Example:

float pi = 3.14f;
  • double: Provides double-precision floating-point numbers. It consumes more memory (usually 8 bytes) but has a higher precision than float.

Example:

double pi_high_precision = 3.1415926535;

Character (char)

Characters represent a single character, such as a letter, digit, or symbol. They are stored using the ASCII value of the symbol and typically occupy 1 byte of memory.

Example:

char letter = 'A';

Boolean (bool)

Booleans represent logical values: true or false. They usually occupy 1 byte of memory.

Example:

bool is_cpp_great = true;

Derived Data Types

Derived data types are types that are derived from fundamental data types. Some examples include:

Arrays

Arrays are used to store multiple values of the same data type in consecutive memory locations.

Example:

int numbers[5] = {1, 2, 3, 4, 5};

Pointers

Pointers are used to store the memory address of a variable.

Example:

int num = 42;
int* pNum = &num;

References

References are an alternative way to share memory locations between variables, allowing you to create an alias for another variable.

Example:

int num = 42;
int& numRef = num;

User-Defined Data Types

User-defined data types are types that are defined by the programmer, such as structures, classes, and unions.

Structures (struct)

Structures are used to group variables of different data types together under a single name.

Example:

struct Person {
    string name;
    int age;
    float height;
};

Person p1 = {"John Doe", 30, 5.9};

Classes (class)

Classes are similar to structures, but they can also have member functions and access specifiers.

Example:

class Person {
  public:
    string name;
    int age;

    void printInfo() { cout << "Name: " << name << ", Age: " << age << endl; };
};

Person p1;
p1.name = "John Doe";
p1.age = 30;

Unions (union)

Unions are used to store different data types in the same memory location.

Example:

union Data {
    int num;
    char letter;
    float decimal;
};

Data myData;
myData.num = 42;

Static Typing

In C++, static typing means that the data type of a variable is determined at compile time, before the program is executed. This means that a variable can be used only with data of a specific type, and the compiler ensures that the operations performed with the variable are compatible with its type.

C++ is a statically typed language, which means that it uses static typing to determine data types and perform type checking during compile time. This helps with ensuring type safety and can prevent certain types of errors from occurring during the execution of the program.

Here's a simple code example to demonstrate static typing in C++:

#include <iostream>
#include <string>

int main() {
    int num = 42; // 'num' is statically typed as an integer

    std::string and = "The answer to everything in the Universe";
    num = and; // This assignment would cause a compile-time error as the types
               // don't match

    std::cout << "The value of num is: " << num << std::endl;
    std::cout << "The value of pi is: " << pi << std::endl;

    return 0;
}

In the code above, the variable num is statically typed as an int, and pi is statically typed as a string. If you attempt to assign the value of and to num, you'll get a compile-time error. This is because the static typing system ensures that variables are only used with compatible data types.

Dynamic Typing in C++

C++ is known as a statically-typed language, which means the data types of its variables are determined at compile time. However, C++ also provides concepts to have certain level of dynamic typing, which means determining the data types of variables at runtime.

Here is a brief overview of two ways to achieve dynamic typing in C++:

void* Pointers

A void* pointer is a generic pointer that can point to objects of any data type. They can be used to store a reference to any type of object without knowing the specific type of the object.

Example:

#include <iostream>

int main() {
    int x = 42;
    float y = 3.14f;
    std::string z = "Hello, world!";

    void* void_ptr;

    void_ptr = &x;
    std::cout << "int value: " << *(static_cast<int*>(void_ptr)) << std::endl;

    void_ptr = &y;
    std::cout << "float value: " << *(static_cast<float*>(void_ptr))
              << std::endl;

    void_ptr = &z;
    std::cout << "string value: " << *(static_cast<std::string*>(void_ptr))
              << std::endl;

    return 0;
}

std::any (C++17)

C++17 introduced the std::any class which represents a generalized type-safe container for single values of any type.

Example:

#include <any>
#include <iostream>

int main() {
    std::any any_value;

    any_value = 42;
    std::cout << "int value: " << std::any_cast<int>(any_value) << std::endl;

    any_value = 3.14;
    std::cout << "double value: " << std::any_cast<double>(any_value)
              << std::endl;

    any_value = std::string("Hello, world!");
    std::cout << "string value: " << std::any_cast<std::string>(any_value)
              << std::endl;

    return 0;
}

Keep in mind that both void* pointers and std::any have performance implications due to the additional type checking and casting that take place during runtime. They should be used carefully and only when absolutely necessary.

Pointers

A pointer is a variable that stores the memory address of another variable (or function). It points to the location of the variable in memory, and it allows you to access or modify the value indirectly. Here's a general format to declare a pointer:

dataType* pointerName;

Initializing a pointer:

int num = 10;
int* ptr = &num; // Pointer 'ptr' now points to the memory address of 'num'

Accessing value using a pointer:

int value = *ptr; // Value now contains the value of the variable that 'ptr'
                  // points to (i.e., 10)

References

A reference is an alias for an existing variable, meaning it's a different name for the same memory location. Unlike pointers, references cannot be null, and they must be initialized when they are declared. Once a reference is initialized, it cannot be changed to refer to another variable.

Here's a general format to declare a reference:

dataType& referenceName = existingVariable;

Example:

int num = 10;
int& ref = num; // Reference 'ref' is now an alias of 'num'

Modifying the value of ref will also modify the value of num because they share the same memory location.

Note: References are generally used when you want to pass a variable by reference in function arguments or when you want to create an alias for a variable without the need for pointer syntax. like this:

int a = 0;
// references are aliases to original variable and can be called by
// functions using call by reference which changes the og variable
auto point = [&a]() { return a += 2; };
point();
cout << a << endl; // will output 2 now

Pointers and how to use and operate on them

int a = 0;
int* address = &a;

cout << address << endl;      // will out put memory address of a
cout << *address << endl;     // will output 0 as the pointer is derefrenced
cout << &address << endl;     // will output the memory address of pointer
cout << *address + 1 << endl; // will output 1
cout << *(&address) << endl;  // is same as *address address
cout << address + 2
     << endl; // will output memory_adress + n*(memory byte of int)
cout << address + 'a'
     << endl; // will output memory_adress + n*(memory byte of char)

// pointers and how they interact with arrays
int prime[5] = {2, 3, 5, 7, 11};

// all will return the memory address of first element of the array
cout << "Result using &prime = " << &prime << endl;
cout << "Result using prime = " << prime << endl;
cout << "Result using &prime[0] = " << &prime[0] << endl;

// will return the second elements memory address
cout << "after adding one: " << &prime[0] + 1 << endl;

// will return the second element as the pointer is dereferenced
// this can be used to loop around the array
cout << "after adding one: " << *(&prime[0] + 1) << endl;

for (int i = 0; i < 5; i++) {
    cout << *(prime + i) << " ";
}

Memory Model in C++

The memory model in C++ defines how the program stores and accesses data in computer memory. It consists of different segments, such as the Stack, Heap, Data and Code segments. Each of these segments is used to store different types of data and has specific characteristics.

Stack Memory

Stack memory is used for automatic storage duration variables, such as local variables and function call data. Stack memory is managed by the compiler, and it's allocation and deallocation are done automatically. The stack memory is also a LIFO (Last In First Out) data structure, meaning that the most recent data allocated is the first to be deallocated.

void functionExample() {
    int x = 10; // x is stored in the stack memory
}

Heap Memory

Heap memory is used for dynamic storage duration variables, such as objects created using the new keyword. The programmer has control over the allocation and deallocation of heap memory using new and delete operators. Heap memory is a larger pool of memory than the stack, but has a slower access time.

void functionExample() {
    int* p = new int; // dynamically allocated int in heap memory
    *p = 10;
    // more code
    delete p; // deallocate memory
}

Data Segment

The Data segment is composed of two parts: the initialized data segment and the uninitialized data segment. The initialized data segment stores global, static, and constant variables with initial values, whereas the uninitialized segment stores uninitialized global and static variables.

// Initialized data segment
int globalVar = 10;        // global variables
static int staticVar = 10; // static local variables
const int constVar = 10;   // constant variables with value

// Uninitialized data segment
int globalVar; // uninitialized global variables

Code Segment

The Code segment (also known as the Text segment) stores the executable code (machine code) of the program. It's usually located in a read-only area of memory to prevent accidental modification.

void functionExample() {
    // The machine code for this function is stored in the code segment.
}

In summary, understanding the memory model in C++ helps to optimize the usage of memory resources and improves overall program performance.

Raw Pointers and new and delete operators

Raw pointers in C++ are low-level constructs that directly hold a memory address. They can be used for manually allocating memory, creating dynamic arrays, and passing values efficiently, among other things.

new Operator

The new operator is used to allocate memory on the heap. The memory allocated using new remains available until you explicitly deallocate it using the corresponding delete operator.

Here's an example of using the new operator:

int* ptr = new int; // Dynamically allocates an int on the heap
*ptr = 42;          // Assigns the value 42 to the allocated int

delete Operator

The delete operator is used to deallocate memory that has been allocated using new. After memory is deallocated, it's available to be reallocated for other purposes. Failing to properly deallocate memory can lead to memory leaks.

Here's an example of using the delete operator:

int* ptr = new int; // Dynamically allocates an int on the heap
*ptr = 42;          // Assigns the value 42 to the allocated int

delete ptr; // Deallocates the memory assigned to ptr

new[] and delete[] Operators

The new[] and delete[] operators are used for allocating and deallocating memory for an array of objects. The syntax for new[] and delete[] is very similar to that of new and delete.

Here's an example of using the new[] and delete[] operators:

int n = 10;
int* arr =
    new int[n]; // Dynamically allocates an array of 10 integers on the heap

// Set some values in the array
for (int i = 0; i < n; i++) {
    arr[i] = i;
}

delete[] arr; // Deallocates the memory assigned to the array

In summary, raw pointers, and new and delete operators allow manual memory management in C++, providing control over allocation and deallocation. Make sure to always deallocate memory allocated with new or new[], to avoid memory leaks in your programs.

Memory Leakage

Memory leakage occurs when a program allocates memory in the heap but does not release the memory back to the operating system when it is no longer needed. Over time, this leads to exhaustion of available memory, resulting in low system performance or crashes.

In C++, when you use raw pointers, you need to manage the memory allocation and deallocation manually. In many cases, you will use the new keyword to allocate memory for an object in the heap and use delete keyword to deallocate that memory when it's no longer needed. Forgetting to do this can cause memory leaks.

Here's an example:

void create_memory_leak() {
    int* ptr = new int[100]; // Allocating memory in the heap for an array of integers
    // Some code...
    // Code to deallocate the memory is missing: delete[] ptr;
} // ptr goes out of scope, memory block allocated is not deallocated, causing a memory leak.

To avoid memory leaks, you should always ensure that memory is deallocated before a pointer goes out of scope or is reassigned. Some ways to achieve this include using the C++ smart pointers (std::unique_ptr, std::shared_ptr), RAII (Resource Acquisition Is Initialization) techniques, and containers from the C++ standard library that manage memory allocation internally (e.g., std::vector, std::string).

For example, this code will not have a memory leak:

#include <memory>

void no_memory_leak() {
    std::shared_ptr<int> ptr = std::make_shared<int[]>(100); // Allocating memory in the heap for an array of integers using shared_ptr
    // Some code...
} // shared_ptr goes out of scope and it will automatically deallocate the memory block assigned to it.

Unique Pointer (unique_ptr)

std::unique_ptr is a smart pointer provided by the C++ Standard Library. It is a template class that is used for managing single objects or arrays.

unique_ptr works on the concept of exclusive ownership - meaning only one unique_ptr is allowed to own an object at a time. This ownership can be transferred or moved, but it cannot be shared or copied.

This concept helps to prevent issues like dangling pointers, reduce memory leaks, and eliminates the need for manual memory management. When the unique_ptr goes out of scope, it automatically deletes the object it owns.

Let's take a look at some basic examples of using unique_ptr:

Creating a unique_ptr

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> p1(new int(5)); // Initialize with pointer to a new integer
    std::unique_ptr<int> p2 = std::make_unique<int>(10); // Preferred method (C++14 onwards)

    std::cout << *p1 << ", " << *p2 << std::endl;
    return 0;
}

Transferring Ownership

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> p1(new int(5));

    std::unique_ptr<int> p2 = std::move(p1); // Ownership is transferred from p1 to p2

    if (p1) {
        std::cout << "p1 owns the object" << std::endl;
    } else if (p2) {
        std::cout << "p2 owns the object" << std::endl;
    }

    return 0;
}

Using unique_ptr with Custom Deleters

#include <iostream>
#include <memory>

struct MyDeleter {
    void operator()(int* ptr) {
        std::cout << "Custom Deleter: Deleting pointer" << std::endl;
        delete ptr;
    }
};

int main() {
    std::unique_ptr<int, MyDeleter> p1(new int(5), MyDeleter());
    return 0; // Custom Deleter will be called when p1 goes out of scope
}

Remember that since unique_ptr has exclusive ownership, you cannot use it when you need shared access to an object. For such cases, you can use std::shared_ptr.

Shared Pointer

A shared_ptr is a type of smart pointer in C++ that allows multiple pointers to share ownership of a dynamically allocated object. The object will be automatically deallocated only when the last shared_ptr that points to it is destroyed.

When using a shared_ptr, the reference counter is automatically incremented every time a new pointer is created, and decremented when each pointer goes out of scope. Once the reference counter reaches zero, the system will clean up the memory.

Code Example

Here's an example of how to use shared_ptr:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "Constructor is called." << std::endl; }
    ~MyClass() { std::cout << "Destructor is called." << std::endl; }
};

int main() {
    // create a shared pointer to manage the MyClass object
    std::shared_ptr<MyClass> ptr1(new MyClass());

    {
        // create another shared pointer and initialize it with the previously created pointer
        std::shared_ptr<MyClass> ptr2 = ptr1;

        std::cout << "Inside the inner scope." << std::endl;
        // both pointers share the same object, and the reference counter has been increased to 2
    }

    std::cout << "Outside the inner scope." << std::endl;
    // leaving the inner scope will destroy ptr2, and the reference counter is decremented to 1

    // the main function returns, ptr1 goes out of scope, and the reference counter becomes 0
    // this causes the MyClass object to be deleted and the destructor is called
}

Output:

Constructor is called.
Inside the inner scope.
Outside the inner scope.
Destructor is called.

In this example, ptr1 and ptr2 share ownership of the same object. The object is only destroyed when both pointers go out of scope and the reference counter becomes zero.

Structuring Codebase

Structuring codebase is an essential part of software development that deals with organizing and modularizing your code to make it more maintainable, efficient, and easier to understand. A well-structured codebase enhances collaboration, simplifies adding new features, and makes debugging faster. In C++, there are various techniques to help you structure your codebase effectively.

Namespaces

Namespaces are one of the tools in C++ to organize your code by providing a named scope for different identifiers you create, like functions, classes, and variables. They help avoid name clashes and make your code more modular.

namespace MyNamespace {
int aFunction() {
    // function implementation
}
} // namespace MyNamespace
// to use the function
MyNamespace::aFunction();

Include Guards

Include guards are a tool for preventing multiple inclusions of a header file in your project. They consist of preprocessor directives that conditionally include the header file only once, even if it's included in multiple places.

#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H

// Your code here

#endif // MY_HEADER_FILE_H

Header and Source Files

Separating your implementation and declarations into header (.h) and source (.cpp) files is a key aspect of structuring your codebase in C++. Header files usually contain class and function declarations, while source files contain their definitions.

// MyClass.h

#ifndef MY_CLASS_H
#define MY_CLASS_H

class MyClass {
  public:
    MyClass();
    int myMethod();
};

#endif // MY_CLASS_H

// MyClass.cpp

#include "MyClass.h"

MyClass::MyClass() {
    // constructor implementation
}

int MyClass::myMethod() {
    // method implementation
}

Code Formatting

Consistent code formatting and indentation play a crucial role in structuring your codebase, making it easier to read and understand for both you and other developers. A style guide such as the Google C++ Style Guide can help you maintain consistent formatting throughout your project.

Scope in C++

Scope refers to the visibility and accessibility of variables, functions, classes, and other identifiers in a C++ program. It determines the lifetime and extent of these identifiers. In C++, there are four types of scope:

  • Global scope: Identifiers declared outside any function or class have a global scope. They can be accessed from any part of the program (unless hidden by a local identifier with the same name). The lifetime of a global identifier is the entire duration of the program.
#include <iostream>

int globalVar; // This is a global variable

int main() {
    std::cout << "Global variable: " << globalVar << std::endl;
}
  • Local scope: Identifiers declared within a function or a block have a local scope. They can be accessed only within the function or the block they were declared in. Their lifetime is limited to the duration of the function/block execution.
#include <iostream>

void localExample() {
    int localVar; // This is a local variable
    localVar = 5;
    std::cout << "Local variable: " << localVar << std::endl;
}

int main() {
    localExample();
    // std::cout << localVar << std::endl; //error: ‘localVar’ was not declared in this scope
}
  • Namespace scope: A namespace is a named scope that groups related identifiers together. Identifiers declared within a namespace have the namespace scope. They can be accessed using the namespace name and the scope resolution operator ::.
#include <iostream>

namespace MyNamespace {
    int namespaceVar = 42;
}

int main() {
    std::cout << "Namespace variable: " << MyNamespace::namespaceVar << std::endl;
}
  • Class scope: Identifiers declared within a class have a class scope. They can be accessed using the class name and the scope resolution operator :: or, for non-static members, an object of the class and the dot . or arrow -> operator.
#include <iostream>

class MyClass {
public:
    static int staticMember;
    int nonStaticMember;

    MyClass(int value) : nonStaticMember(value) {}
};

int MyClass::staticMember = 7;

int main() {
    MyClass obj(10);
    std::cout << "Static member: " << MyClass::staticMember << std::endl;
    std::cout << "Non-static member: " << obj.nonStaticMember << std::endl;
}

Understanding various types of scope in C++ is essential for effective code structuring and management of resources in a codebase.

Namespaces in C++

In C++, a namespace is a named scope or container that is used to organize and enclose a collection of code elements, such as variables, functions, classes, and other namespaces. They are mainly used to divide and manage the code base, giving developers control over name collisions and the specialization of code.

Syntax

Here's the syntax for declaring a namespace:

namespace identifier {
    // code elements
}

Using Namespaces

To access elements within a namespace, you can use the scope resolution operator ::. Here are some examples:

Declaring and accessing a namespace

#include <iostream>

namespace animals {
    std::string dog = "Bobby";
    std::string cat = "Lilly";
}

int main() {
    std::cout << "Dog's name: " << animals::dog << std::endl;
    std::cout << "Cat's name: " << animals::cat << std::endl;

    return 0;
}

Nesting namespaces

Namespaces can be nested within other namespaces:

#include <iostream>

namespace outer {
    int x = 10;

    namespace inner {
        int y = 20;
    }
}

int main() {
    std::cout << "Outer x: " << outer::x << std::endl;
    std::cout << "Inner y: " << outer::inner::y << std::endl;

    return 0;
}

using Keyword

You can use the using keyword to import namespaced elements into the current scope. However, this might lead to name conflicts if multiple namespaces have elements with the same name.

Using a single element from a namespace

#include <iostream>

namespace animals {
    std::string dog = "Bobby";
    std::string cat = "Lilly";
}

int main() {
    using animals::dog;

    std::cout << "Dog's name: " << dog << std::endl;

    return 0;we do be grindin
}

Using the entire namespace

#include <iostream>

namespace animals {
    std::string dog = "Bobby";
    std::string cat = "Lilly";
}

int main() {
    using namespace animals;

    std::cout << "Dog's name: " << dog << std::endl;
    std::cout << "Cat's name: " << cat << std::endl;

    return 0;
}

In conclusion, namespaces are a useful mechanism in C++ to organize code, avoid naming conflicts, and manage the visibility of code elements.

Dont use namespace std

Code Splitting

Code splitting refers to the process of breaking down a large code base into smaller, more manageable files or modules. This helps improve the organization, maintainability, and readability of the code. In C++, code splitting is generally achieved through the use of separate compilation, header files, and source files.

Header Files (.h or .hpp)

Header files, usually with the .h or .hpp extension, are responsible for declaring classes, functions, and variables that are needed by multiple source files. They act as an interface between different parts of the code, making it easier to manage dependencies and reduce the chances of duplicated code.

Example of a header file:

// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H

class Example {
public:
    void printMessage();
};

#endif

Source Files (.cpp)

Source files, with the .cpp extension, are responsible for implementing the actual functionality defined in the corresponding header files. They include the header files as needed and provide the function and class method definitions.

Example of a source file:

// example.cpp
#include "example.h"
#include <iostream>

void Example::printMessage() {
    std::cout << "Hello, code splitting!" << std::endl;
}

Separate Compilation

C++ allows for separate compilation, which means that each source file can be compiled independently into an object file. These object files can then be linked together to form the final executable. This provides faster build times when making changes to a single source file since only that file needs to be recompiled, and the other object files can be reused.

Example of separate compilation and linking:

# Compile each source file into an object file
g++ -c main.cpp -o main.o
g++ -c example.cpp -o example.o

# Link object files together to create the executable
g++ main.o example.o -o my_program

By following the code splitting technique, you can better organize your C++ codebase, making it more manageable and maintainable.

Forward Declaration

Forward declaration is a way of declaring a symbol (class, function, or variable) before defining it in the code. It helps the compiler understand the type, size, and existence of the symbol. This declaration is particularly useful when we have cyclic dependencies or to reduce compilation time by avoiding unnecessary header inclusions in the source file.

Class Forward Declaration

To use a class type before it is defined, you can declare the class without defining its members, like this:

class ClassA; // forward declaration

You can then use pointers or references to the class in your code before defining the class itself:

void do_something (ClassA& obj);

class ClassB {
public:
    void another_function(ClassA& obj);
};

However, if you try to make an object of ClassA or call its member functions without defining the class, you will get a compilation error.

Function Forward Declaration

Functions must be declared before using them, and a forward declaration can be used to declare a function without defining it:

int add(int a, int b); // forward declaration

int main() {
    int result = add(2, 3);
    return 0;
}

int add(int a, int b) {
    return a + b;
}

Enum and Typedef Forward Declaration

For enum and typedef, it is not possible to forward declare because they don't have separate declaration and definition stages.

Keep in mind that forward declarations should be used cautiously, as they can make the code more difficult to understand.

Structures and Classes in C++

Structures and classes are user-defined data types in C++ that allow for the grouping of variables of different data types under a single name. They make it easier to manage and organize complex data by creating objects that have particular attributes and behaviors. The main difference between a structure and a class is their default access specifier: members of a structure are public by default, while members of a class are private.

Structures

A structure is defined using the struct keyword, followed by the structure's name and a set of curly braces {} enclosing the members (variables and/or functions) of the structure. The members can be of different data types. To create an object of the structure's type, use the structure name followed by the object name.

Here's an example of defining a structure and creating an object:

struct Employee {
    int id;
    std::string name;
    float salary;
};

Employee e1; // create an object of the 'Employee' structure

You can access the members of a structure using the dot operator .:

e1.id = 1;
e1.name = "John Doe";
e1.salary = 40000;

Classes

A class is defined using the class keyword, followed by the class's name and a set of curly braces {} enclosing the members (variables and/or functions) of the class. Like structures, class members can be of different data types. You can create objects of a class using the class name followed by the object name.

Here's an example of a class definition and object creation:

class Student {
    int roll_no;
    std::string name;
    float marks;

public:
    void set_data(int r, std::string n, float m) {
        roll_no = r;
        name = n;
        marks = m;
    }

    void display() {
        std::cout << "Roll no: " << roll_no
                  << "\nName: " << name
                  << "\nMarks: " << marks << std::endl;
    }
};

Student s1; // create an object of the 'Student' class

Since the data members of a class are private by default, we cannot access them directly using the dot operator from outside the class. Instead, we use public member functions to set or get their values:

s1.set_data(1, "Alice", 95.0);
s1.display();

That's a brief summary of structures and classes in C++. Remember that while they may seem similar, classes provide more control over data encapsulation and can be used to implement more advanced features like inheritance and polymorphism.

Object-Oriented Programming (OOP) in C++

Object-oriented programming (OOP) is a programming paradigm that uses objects, which are instances of classes, to perform operations and interact with each other. In C++, you can achieve OOP through the use of classes and objects.

Classes

A class is a blueprint for creating objects. It defines the structure (data members) and behavior (member functions) for a type of object. Here's an example of a simple class:

class Dog {
public:
    std::string name;
    int age;

    void bark() {
        std::cout << name << " barks!" << std::endl;
    }
};

This Dog class has two data members: name and age, and one member function bark. You can create an object of this class and access its members like this:

Dog myDog;
myDog.name = "Fido";
myDog.age = 3;
myDog.bark(); // Output: Fido barks!

Encapsulation

Encapsulation is the concept of bundling data and functions that operate on that data within a single unit, such as a class. It helps to hide the internal implementation details of a class and expose only the necessary information and functionalities. In C++, you can use access specifiers like public, private, and protected to control the visibility and accessibility of class members. For example:

class Dog {
private:
    std::string name;
    int age;

public:
    void setName(std::string n) {
        name = n;
    }

    void setAge(int a) {
        age = a;
    }

    void bark() {
        std::cout << name << " barks!" << std::endl;
    }
};

In this example, we've made the name and age data members private and added public member functions setName and setAge to modify them. This way, the internal data of the Dog class is protected and only accessible through the provided functions.

Inheritance

Inheritance is the concept of deriving new classes from existing ones, which enables code reusability and organization. In C++, inheritance is achieved by using a colon : followed by the base class' access specifier and the base class name. For example:

class Animal {
public:
    void breathe() {
        std::cout << "I can breathe" << std::endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        std::cout << "Dog barks!" << std::endl;
    }
};

In this example, the Dog class inherits from the Animal class, so the Dog class can access the breathe function from the Animal class. When you create a Dog object, you can use both breathe and bark functions.

Dog myDog;
myDog.breathe(); // Output: I can breathe
myDog.bark(); // Output: Dog barks!

Polymorphism

Polymorphism allows you to use a single interface to represent different types. In C++, it's mainly achieved using function overloading, virtual functions, and overriding. For example:

class Animal {
public:
    virtual void makeSound() {
        std::cout << "The Animal makes a sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows!" << std::endl;
    }
};

In this example, we have an Animal base class with a virtual makeSound function. We then derive two classes, Dog and Cat, which override the makeSound function. This enables polymorphic behavior, where an Animal pointer or reference can be used to access the correct makeSound function depending on the derived class type.

Animal *animals[2] = {new Dog, new Cat};
animals[0]->makeSound(); // Output: Dog barks!
animals[1]->makeSound(); // Output: Cat meows!

That's a brief overview of object-oriented programming concepts in C++.

A sample program

A damn simple program to demonstrate concepts of OOP.

Program

#include <iostream>
#include <memory>
#include <string>

using std::cout;

class Doctor {
  private:
    std::string name;
    int age;
    int id_no;

  public:
    Doctor(std::string name, int age, int id_no)
        : name(name), age(age), id_no(id_no) {}

    std::string getName() const { return name; }
    void setName(const std::string &name) { this->name = name; }
    int getAge() const { return age; }
    void setAge(int age) { this->age = age; }
    int getIdNo() const { return id_no; }
    void setIdNo(int id_no) { this->id_no = id_no; }

    virtual void get_name() {
        cout << "Name of Doctor is " << name << std::endl;
    }

    virtual void task() { cout << "Treat patients" << std::endl; }

    Doctor(){};
};

class surgeon : public Doctor {
  public:
    surgeon(std::string name): Doctor(name,0,0){};
    void task() override { cout << "Do surgery" << std::endl; }
};

class vet : public Doctor {
  private:
    int no_of_animals;

  public:
    vet(std::string name, int no_of_animals)
        : Doctor(name, 0, 0), no_of_animals(no_of_animals) {}

    void task() override { cout << "Operates on animals" << std::endl; }

    void no_animals() const {
        cout << "No of Animals: " << no_of_animals << std::endl;
    }
};

int main() {
    std::unique_ptr<Doctor> rishabh =
        std::make_unique<Doctor>("Rishabh", 20, 100);
    rishabh->get_name();
    rishabh->task();

    std::unique_ptr<surgeon> amit = std::make_unique<surgeon>("Amit");
    amit->get_name();
    amit->task();

    std::unique_ptr<vet> sumit = std::make_unique<vet>("Sumit", 1231);
    sumit->get_name();
    sumit->no_animals();
    sumit->task();

    return 0;
}

Multiple Inheritance

Multiple inheritance is a feature in C++ where a class can inherit characteristics (data members and member functions) from more than one parent class. The concept is similar to single inheritance (where a class inherits from a single base class), but in multiple inheritance, a class can have multiple base classes.

When a class inherits multiple base classes, it becomes a mixture of their properties and behaviors, and can override or extend them as needed.

Syntax

Here is the syntax to declare a class with multiple inheritance:

class DerivedClass : access-specifier BaseClass1, access-specifier BaseClass2, ...
{
    // class body
};

The DerivedClass will inherit members from both BaseClass1 and BaseClass2. The access-specifier (like public, protected, or private) determines the accessibility of the inherited members.

Example

Here is an example of multiple inheritance in action:

#include <iostream>

// Base class 1
class Animal {
public:
    void eat(){
        std::cout << "I can eat!" << std::endl;
    }
};

// Base class 2
class Mammal {
public:
    void breath(){
        std::cout << "I can breathe!" << std::endl;
    }
};

// Derived class inheriting from both Animal and Mammal
class Dog : public Animal, public Mammal {
public:
    void bark(){
        std::cout << "I can bark! Woof woof!" << std::endl;
    }
};

int main(){
    Dog myDog;

    // Calling members from both base classes
    myDog.eat();
    myDog.breath();

    // Calling a member from the derived class
    myDog.bark();

    return 0;
}

Note

In some cases, multiple inheritance can lead to complications such as ambiguity and the "diamond problem". Ensure that you use multiple inheritance judiciously and maintain well-structured and modular classes to prevent issues.

For more information on C++ multiple inheritance and related topics, refer to C++ documentation or a comprehensive C++ programming guide.

Diamond Inheritance

Diamond inheritance is a specific scenario in multiple inheritance where a class is derived from two or more classes, which in turn, are derived from a common base class. It creates an ambiguity that arises from duplicating the common base class, which leads to an ambiguous behavior while calling the duplicate members.

To resolve this ambiguity, you can use virtual inheritance. A virtual base class is a class that is shared by multiple classes using virtual keyword in C++. This ensures that only one copy of the base class is inherited in the final derived class, and thus, resolves the diamond inheritance problem.

Example:

#include<iostream>
using namespace std;

class Base {
public:
    void print() {
        cout << "Base class" << endl;
    }
};

class Derived1 : virtual public Base {
public:
    void derived1Print() {
        cout << "Derived1 class" << endl;
    }
};

class Derived2 : virtual public Base {
public:
    void derived2Print() {
        cout << "Derived2 class" << endl;
    }
};

class Derived3 : public Derived1, public Derived2 {
public:
    void derived3Print() {
        cout << "Derived3 class" << endl;
    }
};

int main()
{
    Derived3 d3;
    d3.print(); // Now, there is no ambiguity in calling the base class function
    d3.derived1Print();
    d3.derived2Print();
    d3.derived3Print();

    return 0;
}

In the code above, Derived1 and Derived2 are derived from the Base class using virtual inheritance. So, when we create an object of Derived3 and call the print() function from the Base class, there is no ambiguity, and the code executes without any issues.

Static Polymorphism

Static polymorphism, also known as compile-time polymorphism, is a type of polymorphism that resolves the types and method calls at compile time rather than at runtime. This is commonly achieved through the use of function overloading and templates in C++.

Function Overloading

Function overloading is a way to create multiple functions with the same name but different parameter lists. The compiler determines the correct function to call based on the types and number of arguments used when the function is called.

Example:

#include <iostream>

void print(int i) {
    std::cout << "Printing int: " << i << std::endtreel;
}

void print(double d) {
    std::cout << "Printing double: " << d << std::endl;
}

void print(const char* s) {
    std::cout << "Printing string: " << s << std::endl;
}

int main() {
    print(5);          // Calls print(int i)
    print(3.14);       // Calls print(double d)
    print("Hello");    // Calls print(const char* s)

    return 0;
}

Templates

Templates are a powerful feature in C++ that allows you to create generic functions or classes. The actual code for specific types is generated at compile time, which avoids the overhead of runtime polymorphism. The use of templates is the main technique to achieve static polymorphism in C++.

Example:

#include <iostream>

// Template function to print any type
template<typename T>
void print(const T& value) {
    std::cout << "Printing value: " << value << std::endl;
}

int main() {
    print(42);           // int
    print(3.14159);      // double
    print("Hello");      // const char*

    return 0;
}

In conclusion, static polymorphism achieves polymorphic behavior during compile time using function overloading and templates, instead of relying on runtime information like dynamic polymorphism does. This can result in more efficient code since method calls are resolved at compile time.

Dynamic Polymorphism

Dynamic polymorphism is a programming concept in object-oriented languages like C++ where a derived class can override or redefine methods of its base class. This means that a single method call can have different implementations based on the type of object it is called on.

Dynamic polymorphism is achieved through virtual functions, which are member functions of a base class marked with the virtual keyword. When you specify a virtual function in a base class, it can be overridden in any derived class to provide a different implementation.

Example

Here's an example in C++ demonstrating dynamic polymorphism.

#include <iostream>

// Base class
class Shape {
  public:
    virtual void draw() { std::cout << "Drawing a shape" << std::endl; }
};

// Derived class 1
class Circle : public Shape {
  public:
    void draw() override { std::cout << "Drawing a circle" << std::endl; }
};

// Derived class 2
class Rectangle : public Shape {
  public:
    void draw() override { std::cout << "Drawing a rectangle" << std::endl; }
};

int main() {
    Shape* shape;
    Circle circle;
    Rectangle rectangle;

    // Storing the address of circle
    shape = &circle;

    // Call circle draw function
    shape->draw();

    // Storing the address of rectangle
    shape = &rectangle;

    // Call rectangle draw function
    shape->draw();

    return 0;
}

This code defines a base class Shape with a virtual function draw. Two derived classes Circle and Rectangle both override the draw function to provide their own implementations. Then in the main function, a pointer of type Shape is used to call the respective draw functions of Circle and Rectangle objects. The output of this program will be:

Drawing a circle
Drawing a rectangle

As you can see, using dynamic polymorphism, we can determine at runtime which draw method should be called based on the type of object being used.

Never use malloc in C++

In this code calling of f() with the a2 object will cause a segmentation fault as a2 is instantiated using malloc So don't use malloc use new keyword instead in C++

class A {
  public:
    int x = 3;
    virtual void f() { std::cout << "abc"; }
};

A* a1 = new A;
A* a2 = (A*)malloc(sizeof(A));

std::cout << a1->x; // print "3"
std::cout << a2->x; // undefined value!!
a1->f();            // print "abc"
a2->f();            // segmentation fault

Virtual Methods

Virtual methods are a key aspect of dynamic polymorphism in C++. They allow subclass methods to override the methods of their base class, so the appropriate method is called depending on the actual type of an object at runtime.

To declare a method as virtual, simply use the virtual keyword in the method's declaration in the base class. This tells the compiler that the method should be treated as a virtual method, allowing it to be overridden by derived classes.

Code Example

Here's an example demonstrating virtual methods:

#include <iostream>

// Base class
class Shape {
public:
    virtual double area() const {
        return 0;
    }
};

// Derived class
class Circle : public Shape {
public:
    Circle(double r) : radius(r) {}

    // Override the base class method
    double area() const override {
        return 3.14 * radius * radius;
    }

private:
    double radius;
};

// Derived class
class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    // Override the base class method
    double area() const override {
        return width * height;
    }

private:
    double width;
    double height;
};

int main() {
    Circle c(5);
    Rectangle r(4, 6);

    Shape* shape = &c;
    std::cout << "Circle's area: " << shape->area() << std::endl;

    shape = &r;
    std::cout << "Rectangle's area: " << shape->area() << std::endl;

    return 0;
}

In this example, we define a base class Shape that has a virtual method area. This method is then overridden by the derived classes Circle and Rectangle. By using a virtual method and a base class pointer to the derived objects, we can invoke the appropriate area method based on the actual object type at runtime.

Virtual Tables

Virtual Tables (or Vtable) are a mechanism used by C++ compilers to support dynamic polymorphism. In dynamic polymorphism, the appropriate function is called at runtime, depending on the actual object type.

When a class contains a virtual function, the compiler creates a virtual table for that class. This table contains function pointers to the virtual functions defined in the class. Each object of that class has a pointer to its virtual table (vptr, virtual pointer), which is automatically initialized by the compiler during object construction.

Example

Let's consider the following example:

class Base {
  public:
    virtual void function1() { std::cout << "Base::function1" << std::endl; }

    virtual void function2() { std::cout << "Base::function2" << std::endl; }
};

class Derived : public Base {
  public:
    void function1() override {
        std::cout << "Derived::function1" << std::endl;
    }

    void function3() { std::cout << "Derived::function3" << std::endl; }
};

int main() {
    Base* obj = new Derived(); // create a Derived object and assign a pointer
                               // of type Base*
    obj->function1(); // calls Derived::function1, due to dynamic polymorphism
    obj->function2(); // calls Base::function2

    delete obj;
    return 0;
}

In this example, when a Derived object is created, the compiler generates a Vtable for Derived class, containing pointers to its virtual functions:

  • Derived::function1 (overridden from Base)
  • Base::function2 (inherits from Base)

The _vptr_ pointer in the Derived object points to this Vtable. When the function1 is called on the Base pointer pointing to the Derived object, the function pointer in the Vtable is used to call the correct function (in this case, Derived::function1). Similarly, the call to function2 calls Base::function2, since it's the function pointer stored in the Vtable for Derived class.

Note that function3 is not part of the Vtable, as it is not a virtual function.

Diagram explanation

image

Overview

Exception handling in C++ is a mechanism to handle errors, anomalies, or unexpected events that can occur during the runtime execution of a program. This allows the program to continue running or exit gracefully when encountering errors instead of crashing abruptly.

C++ provides a set of keywords and constructs for implementing exception handling:

  • try: Defines a block of code that should be monitored for exceptions.
  • catch: Specifies the type of exception to be caught and the block of code that shall be executed when that exception occurs.
  • throw: Throws an exception that will be caught and handled by the appropriate catch block.
  • noexcept: Specifies a function that doesn't throw exceptions or terminates the program if an exception is thrown within its scope.

Example

Here's an example demonstrating the basic usage of exception handling:

#include <iostream>

int divide(int a, int b) {
    if (b == 0) {
        throw "Division by zero!";
    }
    return a / b;
}

int main() {
    int num1, num2;

    std::cout << "Enter two numbers for division: ";
    std::cin >> num1 >> num2;

    try {
        int result = divide(num1, num2);
        std::cout << "The result is: " << result << std::endl;
    } catch (const char* msg) {
        std::cerr << "Error: " << msg << std::endl;
    }

    return 0;
}

In this example, we define a function divide that throws an exception if b is zero. In the main function, we use a try block to call divide and output the result. If an exception is thrown, it is caught inside the catch block, which outputs an error message. This way, we can handle the error gracefully rather than letting the program crash when attempting to divide by zero.

Standard Exceptions

C++ provides a standard set of exception classes under the <stdexcept> library which can be used as the exception type for more specific error handling. Some of these classes include:

  • std::exception: Base class for all standard exceptions.
  • std::logic_error: Represents errors which can be detected statically by the program.
  • std::runtime_error: Represents errors occurring during the execution of a program.

Here's an example showing how to use standard exceptions:

#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return a / b;
}

int main() {
    int num1, num2;

    std::cout << "Enter two numbers for division: ";
    std::cin >> num1 >> num2;

    try {
        int result = divide(num1, num2);
        std::cout << "The result is: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}

In this example, we modified the divide function to throw a std::runtime_error instead of a simple string. The catch block now catches exceptions derived from std::exception and uses the member function what() to display the error message.

Examples

Very Simple Example

#include <iostream>
using namespace std;
int main() {
    double numerator, denominator, divide;
    cout << "Enter numerator: ";
    cin >> numerator;
    cout << "Enter denominator: ";
    cin >> denominator;
    try {
        // throw an exception if denominator is 0
        if (denominator == 0)
            throw 0;
        // not executed if denominator is 0
        divide = numerator / denominator;
        cout << numerator << " / " << denominator << " = " << divide << endl;
    }
    catch (int num_exception) {
        cout << "Error: Cannot divide by " << num_exception << endl;
    }
    return 0;
}

Somewhat complex example that handles multiple exception

#include <iostream>
using namespace std;
int main() {
    double numerator, denominator, arr[4] = {0.0, 0.0, 0.0, 0.0};
    int index;

    cout << "Enter array index: ";
    cin >> index;

    try {
        // throw exception if array out of bounds
        if (index >= 4)
            throw "Error: Array out of bounds!";

        // not executed if array is out of bounds
        cout << "Enter numerator: ";
        cin >> numerator;

        cout << "Enter denominator: ";
        cin >> denominator;
        // throw exception if denominator is 0
        if (denominator == 0)
            throw 0;
        // not executed if denominator is 0
        arr[index] = numerator / denominator;
        cout << arr[index] << endl;
    }
    // catch "Array out of bounds" exception
    catch (const char* msg) {
        cout << msg << endl;
    }
    // catch "Divide by 0" exception
    catch (int num) {
        cout << "Error: Cannot divide by " << num << endl;
    }
    // catch any other exception
    catch (...) {
        cout << "Unexpected exception!" << endl;
    }

    return 0;
}

Exception handling in C++

C++ exception handling is a mechanism that allows you to handle errors that occur during the execution of a program. It is built on three keywords: try, catch, and throw.

  • The try keyword is used to define a block of code that can throw an exception.
  • The catch keyword is used to define a block of code that is executed when a particular exception is thrown.
  • The throw keyword is used to throw an exception.

The following is an example of a simple try-catch block:

int main() {
  try {
    // This code might throw an exception.
    throw(something can be of any type)
  } catch (int e) {
    // This code is executed if an exception of type int is thrown.
    std::cout << "An integer exception was thrown: " << e << std::endl;
  } catch (std::string e) {
    // This code is executed if an exception of type std::string is thrown.
    std::cout << "A string exception was thrown: " << e << std::endl;
  }
}

In this example, the try block contains the code that might throw an exception. The catch blocks are executed if a particular type of exception is thrown. In this case, there are two catch blocks: one for exceptions of type int and one for exceptions of type std::string.

If an exception is thrown in the try block, the first catch block that matches the type of the exception is executed. If no catch block matches the type of the exception, the program terminates abnormally.

You can also have multiple catch blocks for the same type of exception. In this case, the catch blocks are executed in the order in which they appear in the code.

The throw keyword is used to throw an exception. The syntax for the throw keyword is:

throw expression;

The expression can be any value. When the throw keyword is executed, the expression is passed to the first catch block that matches its type.

Here is an example of a throw statement:

throw 10;

This statement throws an exception of type int with the value of 10.

The try, catch, and throw keywords can be used to handle a wide variety of errors. For example, you can use them to handle errors that occur when opening files, reading from files, or writing to files. You can also use them to handle errors that occur when dividing by zero, accessing invalid memory, or calling a function that does not exist.

Exit codes

In C++, the exit function is used to terminate the execution of a program. The value supplied as an argument to exit is returned to the operating system as the program's exit code. By convention, a return code of zero means that the program completed successfully.

Here are some of the commonly used exit codes in C++:

  • EXIT_SUCCESS: 0, indicates successful termination of the program.
  • EXIT_FAILURE: 1, indicates abnormal termination of the program.
  • SIGINT: 2, indicates that the program was terminated by a user interrupt (Ctrl+C).
  • SIGSEGV: 11, indicates that the program attempted to access a memory location that is not accessible.
  • SIGABRT: 6, indicates that the program was aborted by a call to the abort function.
  • SIGKILL: 9, indicates that the program was killed by the operating system.

You can also use user-defined exit codes. However, it is important to avoid using the following exit codes, as they have special meanings:

  • 1, 2, 126-165, and 255

The exit function is defined in the <stdlib.h> header file.

Here is an example of how to use the exit function:

#include <stdlib.h>

int main() {
    if (some_error_occurred) {
        exit(1); // Terminate the program with an exit code of 1
    }

    // Do something else

    return 0; // Terminate the program with an exit code of 0
}

Example: Using return in main

#include <iostream>

int main() {
    // Some code here...

    if (/*some error condition*/) {
        std::cout << "An error occurred." << std::endl;
        return 1;
    }

    // More code here...

    if (/*another error condition*/) {
        std::cout << "Another error occurred." << std::endl;
        return 2;
    }

    return 0; // Successful execution
}

Access Violation in C++

An access violation in C++ is an error that occurs when a program tries to access a memory address that it does not have permission to access. This can happen for a variety of reasons, such as:

  • Trying to access a memory address that is outside of the allocated memory space for the program.
  • Trying to access a memory address that is already in use by another program or process.
  • Trying to access a memory address that has been marked as invalid.

Code Examples

Here are some code examples of access violations:

Trying to access a memory address that is outside of the allocated memory space for the program:

int* p = new int[10]; // Allocate memory for an array of 10 integers.
*p = 10; // This is fine.
p[10] = 20; // This will cause an access violation because p is pointing to an address outside of the allocated memory space.

Trying to access a memory address that is already in use by another program or process:

int* q = (int*)malloc(sizeof(int)); // Allocate memory for an integer on the heap.
*q = 10; // This is fine.
p = q; // This is also fine.
*p = 20; // This will cause an access violation because p is now pointing to the same memory address as q, which is still in use by another program or process.

Trying to access a memory address that has been marked as invalid:

int* r = new int; // Allocate memory for an integer.
delete r; // This marks the memory address pointed to by r as invalid.
*r = 20; // This will cause an access violation because the memory address pointed to by r is no longer valid.

How to Avoid Access Violations

To avoid access violations, it is important to be careful about how you access memory in C++. Here are a few tips:

  • Always use the new keyword to allocate memory for your program. This will help to prevent you from accidentally accessing memory that is outside of your allocated space.
  • Use the delete keyword to free up memory when you are finished with it. This will help to prevent memory leaks, which can also lead to access violations.
  • Be careful about using pointers. Pointers can be used to access memory addresses directly, which can make it easier to accidentally cause an access violation.
  • Use the assert() macro to check for errors in your code. This can help to catch access violations early on, before they cause a crash.

How to Debug Access Violations

If you do encounter an access violation, there are a few things you can do to debug the problem:

  • Use a debugger to step through your code and see where the access violation is occurring.
  • Check the values of your variables to see if they are pointing to invalid memory addresses.
  • Use the valgrind tool to scan your code for memory errors.

By following these tips, you can help to prevent access violations in your C++ code.

Type Casting

Type casting in C++ is a way of converting an object of one data type into another. It allows you to change the data type of a variable or expression. There are two types of type conversion: implicit and explicit.

Implicit Type Conversion: Also known as 'automatic type conversion', it is done by the compiler on its own, without any external trigger from the user. It generally takes place when in an expression more than one data type is present. In such conditions, type conversion (type promotion) takes place to avoid loss of data. All the data types of the variables are upgraded to the data type of the variable with the largest data type. However, it is possible for implicit conversions to lose information, signs can be lost (when signed is implicitly converted to unsigned), and overflow can occur (when long long is implicitly converted to float)

Explicit Type Conversion: This process is also called type casting and it is user-defined. Here the user can typecast the result to make it of a particular data type. In C++, it can be done by two ways:

  1. Converting by assignment: This is done by explicitly defining the required type in front of the expression in parenthesis. This can be also considered as forceful casting.
double x = 1.2;
// Explicit conversion from double to int
int sum = (int)x + 1;
  1. Conversion using Cast operator: A Cast operator is an unary operator which forces one data type to be converted into another data type.
float f = 3.5;
// using cast operator
int b = static_cast<int>(f);

C++ supports four types of casting:

  1. static_cast: This is the most commonly used type of casting in C++. It can be used for conversions between related types (like from a base class pointer to a derived class pointer), among other things. It can also be used for conversions between unrelated types, such as from an int to a float. However, it does not perform any runtime checks, so it's up to the programmer to ensure the cast is safe.
double x = 10.3;
int y;
y = static_cast<int>(x); // y is now 10
  1. const_cast: This is used to add or remove the const or volatile qualifier from a variable. It can be used to modify a const object, which is undefined behavior in C++.
const char* c = "sample text";
char* nonConst = const_cast<char*>(c); // nonConst now points to "sample text"
  1. dynamic_cast: This is used for downcasting in the context of inheritance. It can be used to safely downcast a pointer or reference to a base class to a derived class. If the object is not of the target type, dynamic_cast returns null for pointers or throws a std::bad_cast exception for references.
class Base {};
class Derived : public Base {};
Base* a = new Base;
Derived* b = dynamic_cast<Derived*>(a); // b is null
  1. reinterpret_cast: This is the most powerful type of cast. It can convert any pointer type to any other pointer type, regardless of the classes they point to. It can also be used to convert any pointer type to an integer type and vice versa. It's the least safe type of cast and should be used sparingly.
int* pi = new int(3);
char* pc =
    reinterpret_cast<char*>(pi); // pc now points to the same memory as pi

It's important to note that while these casts can be very useful, they should be used judiciously. Incorrect use of type casting can lead to bugs that are hard to detect and fix. Always prefer safer casts (like static_cast and dynamic_cast) over more dangerous ones (like reinterpret_cast), and only use const_cast when necessary to modify a const object.

Standard Template Library

The C++ Standard Template Library (STL) is a powerful feature of the C++ language that provides a set of well-structured, generic C++ components that work together in a seamless way . It enables generic programming in C++, a programming paradigm in which algorithms are written in terms of generic types, which are instantiated when needed for specific types provided as parameters .

The STL is divided into three main components: containers, algorithms, and iterators.

Containers

Containers in STL are holder objects that store a collection of other objects (its elements). They are implemented as class templates, which allows great flexibility in the types supported as elements. There are several types of containers in STL:

  • Sequence containers: Implement data structures that can be accessed sequentially.
  • Associative containers: Implement sorted data structures that can be quickly searched.
  • Unordered associative containers: Implement unsorted (hashed) data structures that can be quickly searched.
  • Container adapters: Provide a different interface for sequential containers.

Here are some examples of containers:

std::vector<int> vec;         // Vector container
std::set<int> s;              // Set container
std::map<int, std::string> m; // Map container
std::unordered_set<int> us;   // Unordered set container
std::stack<int> st;           // Stack container
std::queue<int> q;            // Queue container

Algorithms

Algorithms in STL are functions that operate on containers. They provide a way to perform common tasks like sorting, searching, and manipulating data. Here's an example of using the sort algorithm with a vector:

std::vector<int> vec = {5, 3, 2, 1, 4};
std::sort(vect.begin(), vect.end()); // Sorts the elements in a range in ascending order
std::reverse(vect.begin(), vect.end()); // Reverses the order of elements in a range
auto it = std::find(vect.begin(), vect.end(), value); // Searches for a specific element in a range and returns an iterator pointing to the first occurrence of the element
int count = std::count(vect.begin(), vect.end(), value); // Counts the number of elements in a range that match a specific value
auto max_it = std::max_element(vect.begin(), vect.end()); // Finds the maximum element in a range
auto min_it = std::min_element(vect.begin(), vect.end()); // Finds the minimum element in a range
int sum = std::accumulate(vect.begin(), vect.end(), 0); // Calculates the sum of elements in a range
bool found = std::binary_search(vect.begin(), vect.end(), value); // Determines whether a sorted range contains a specific value
std::next_permutation(vect.begin(), vect.end()); // Generates the next lexicographically greater permutation of a range
std::prev_permutation(vect.begin(), vect.end()); // Generates the previous lexicographically smaller permutation of a range

Iterators

Iterators in STL are objects that can navigate or iterate over elements in a container. They are essentially a generalization of pointers and provide similar, but more advanced, behavior. Here's an example of using an iterator with a vector:

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << ' ';
}

The STL is highly beneficial for several reasons:

  • Reuse: STL hides complex, tedious, and error-prone details, ensuring type-safe plug compatibility between STL components.
  • Flexibility: Iterators decouple algorithms from containers, allowing for unanticipated combinations easily.
  • Efficiency: Templates & inlining avoid virtual function overhead, and strict attention to time complexity of algorithms is maintained.
  • Competitive programming: They can really help you in CP.

Templates

Templates in C++ are a powerful feature that allows you to write generic code. This means you can write a function or a class that can work with different data types, without having to write separate versions of the function or class for each data type.

Function Templates

Function templates are functions that can work with different data types. Here's an example of a function template that prints out the value of an object of any type:

template <typename T> void print(T value) { std::cout << value << std::endl; }

print(1);
print("Hello");
print(5.3);

In the above code, T is a placeholder for any data type. When you call the print function, you can pass an object of any type for which operator << is overloaded, and the print function will print out the value of the object.

Class Templates

Class templates are classes that can work with different data types. Here's an example of a class template that holds an object of any type:

template <typename T> class Box {
    T value;

  public:
    Box(T value) : value(value) {}
    void print() { std::cout << value << std::endl; }
};

In the above code, T is a placeholder for any data type. When you create an object of the Box class, you can specify the type of the object, and the Box class will hold an object of that type.

Template Specialization

Template specialization is a way to provide a different implementation of a function or a class for a specific data type. Here's an example of a function template and a specialized version of the function for the int data type:

// Function template
template <typename T> T add(T a, T b) { return a + b; }

// Specialized version for int
template <> int add<int>(int a, int b) { return a + b; }

In the above code, the add function template can add two objects of any type. The specialized version of the add function for the int data type adds two int objects.

Class Template Specialization

Similarly, you can provide a different implementation of a class for a specific data type. Here's an example of a class template and a specialized version of the class for the int data type:

// Class template
template <typename T> class Box {
    T value;

  public:
    Box(T value) : value(value) {}
    void print() { std::cout << value << std::endl; }
};

// Specialized version for int
template <> class Box<int> {
    int value;

  public:
    Box(int value) : value(value) {}
    void print() {
        std::cout << "This is an integer box. Value: " << value << std::endl;
    }
};

In the above code, the Box class template can hold an object of any type. The specialized version of the Box class for the int data type holds an int object and prints a different message when the print method is called.

Name Mangling

Name mangling in C++ is a technique used to resolve conflicts between identifiers that have the same name but belong to different scopes or have different types. This process transforms function names into unique identifiers that can be distinguished by the linker during the linking phase of compilation.

However, it's important to note that name mangling is platform-specific. The C++ ABI (Application Binary Interface) of the platform you are running on defines the name mangling. There are two common C++ ABIs, which are the Itanium ABI and Microsoft's ABI. Depending on the platform and the compiler being used, different name mangling schemes may be applied .

The C++ standard does not define a specific naming convention for mangled names, so different compilers may implement name mangling differently. However, mangled names typically start with _Z followed by the mangled name of the function or variable, including namespace, classes, and name, with a length and the identifier 1.

Here's an example of name mangling in action:

namespace MyNamespace {
class MyClass {
  public:
    void myMethod(int param);
};
} // namespace MyNamespace

The mangled name of myMethod might look something like _ZN11MyNamespace7MyClass9myMethodEi, which includes the namespace (MyNamespace), the class (MyClass), the method name (myMethod), and the parameter type (int).

To understand the mangled names, you can use a tool like c++filt. c++filt is a command-line utility that comes with the GNU Binutils package and is used to demangle C++ symbols. By piping the output of nm (another command-line utility that lists symbols from object files) through c++filt, you can see the demangled names of the symbols.

Here's how you might use c++filt:

nm myprogram.o | c++filt

This command will print out the names of all symbols in myprogram.o, with the mangled names replaced by their demangled equivalents.

Output:

0000000000003df0 d _DYNAMIC
0000000000003fe8 d _GLOBAL_OFFSET_TABLE_
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000002004 r __GNU_EH_FRAME_HDR
0000000000004010 D __TMC_END__
0000000000004010 B __bss_start
                 w __cxa_finalize@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004008 D __dso_handle
                 w __gmon_start__
                 U __libc_start_main@GLIBC_2.34
0000000000004010 D _edata
0000000000004018 B _end
000000000000112c T _fini
0000000000001000 T _init
0000000000001020 T _start
0000000000004000 W data_start
0000000000001119 T main