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;
}
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
andfree
.
#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
, andthrow
keywords, providing more control over error handling.
Conclusion
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 valuesfloat
: single-precision floating-point valuesdouble
: double-precision floating-point valueschar
: 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
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 bevoid
, 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 keywordvoid
.
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 thanint
. - long (
long int
): Larger range thanint
. - long long (
long long int
): Even larger range thanlong 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 = #
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 = # // 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 fromBase
)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
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 theabort
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:
- 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;
- 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:
- 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
- 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"
- 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
- 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