Categories
Computer Science / Information Technology Language: C++

Input-Ouput Streams

Overview

In this section, we will explore the utilisation of streams for IO in C++. Firstly, we will delve into the concept of streams and their role in simplifying the handling of diverse input and output devices. Next, we will familiarise ourselves with various C++ stream manipulators. These manipulators are functions that impact the way we read from and write to strings. Following that, we will study a selection of C++ stream manipulators which specifically affect stream-based input and output operations. We will cover manipulators designed for Boolean, integer, and floating-point values. Additionally, we will employ manipulators applicable to any data type to aid us in formatting our output. Such manipulators provide us with capabilities to align text, fill blank spaces with designated characters, and control the width of output fields.

Once we conclude our exploration of stream manipulators, we will shift our focus to input files. This includes topics like file opening, successful opening verification, reading from files, and closing files. We will explore both formatted and unformatted input/output techniques for file processing.

Subsequently, we will delve into output files and their mechanisms. We will learn how to open output files, write to them using formatted and unformatted modes, and close them.

To conclude this section, we will explore string streams. This enables us to treat in-memory strings as streams and utilise the same IO techniques we apply to file-based streams. The power of in-memory string streams will be showcased, including their utility for data validation.

Files, Streams and I/O

Let’s explore how C++ utilises streams and files for input and output (IO) operations. Developing an IO library for any programming language is an exceptionally challenging task due to the multitude of devices that can provide data to a program and receive data from it. These devices encompass physical entities like hard disks, consoles, and keyboards, as well as virtual devices such as connections to web servers.

In C++, a stream abstraction is provided to facilitate working with IO devices. A stream serves as an interface that remains independent of the specific device. Consequently, programmers can code to the stream interface without concerning themselves too much with the connected device. Conceptually, a stream is akin to a sequence of bytes. C++ offers various stream types depending on whether input or output is desired, although there are streams that perform both functions simultaneously.

To visualise this, let’s refer to the following diagram.

On the right side, we see a C++ program. The upper stream represents an input stream that supplies input to the program from an input device. This device could be the keyboard, a file, a connection to a web service, or others. The lower stream represents an output stream that receives output from the program and transmits it to an output device. Similarly, the output device can be a file, the console, a connection to a web service, or others.

The following table showcases three frequently used header files for stream IO:

Header fileDescription
iostreamProvides the necessary definitions for formatted input and output to and from streams
fstreamEquips us with definitions for formatted IO with file streams
iomanipincludes definitions for manipulators that facilitate specific formatting of iostreams

Upon including these header files, we gain access to several C++ classes that facilitate file IO operations. The following table discusses a few of them:

ClassDescription
iosOffers fundamental support for formatted and unformatted IO and serves as the base class for most other classes in the iostream hierarchy.
ifstreamEnables high-level input operations from files. If you intend to read from a file, you can declare an object of the ifstream class.
ofstreamFacilitates high-level output operations to files. When creating a new file or writing to an existing one, you can declare an object of the ofstream class.
fstreamEnables high-level IO on file-based streams. It inherits from both ifstream and ofstream using multiple inheritance. By using an fstream object, we can perform both input and output operations on a file simultaneously.
stringstreamParticularly useful for high-level IO on strings stored in memory. Similar to using insertion and extraction operators with cin and cout, we can employ them with strings to obtain input and produce output within an in-memory string.

Now, let’s revisit familiar entities: cin and cout, and understand their nature. Although we have been using them without worrying about their creation or connection, it is a testament to the design of the C++ IO library and the device independence model.

Global stream objects

cin, cout, cerr, and clog are global objects that are initialised before the execution of the main function. To utilise them, we simply include the “iostream” header file.

ObjectDescription
cinServes as the standard input stream and is connected to the keyboard by default. It is an instance of the istream class.
coutRepresents the standard output stream and is connected to the console by default. It is an instance of the ostream class.
cerrRepresents the standard error stream and is connected to the console by default. It is an instance of the ostream (unbuffered) class.
clogRepresents the standard log stream and is connected to the console by default. It is an instance of the ostream (unbuffered) class.

cin and cout are typically buffered streams. This means that input from cin is not processed automatically until the user presses the Enter key. Similarly, output to cout occurs when the stream buffer becomes full, when we provide a std::endl, or when we explicitly flush the stream.

cerr and clog are unbuffered, meaning that input or output is handled immediately as needed. It is considered best practice to use cerr for error messages and clog for log messages.

For those familiar with the UNIX terminal shell or the Windows command prompt, it is worth noting that these streams can be easily redirected. This allows input to come from a file or output to be directed to a file. While we won’t cover the details here, It is worth it to search online for resources on redirecting IO if interested. You will discover how straightforward and powerful this capability can be.

C++ Stream Manipulators

C++ streams offer a range of helpful member methods that allow us to control formatting. While there are methods available for both input and output streams, formatting output is more commonly used. It’s important to understand that when we manipulate a stream for formatting, the settings we apply can have different durations. Some settings will last for the remainder of the program, while others only affect the next object placed on the stream. There are also settings that take effect immediately.

Most stream formatters come in two versions: a method version and a manipulator version. The method version involves calling a method on the stream object itself. For example, 

std::cout.width(10);

We shall revisit this method in detail later, but for now, let’s focus on the syntax. On the other hand, the manipulator version is designed to be used inline as part of a stream insertion. For instance,

std::cout << std::setw(10);

The manipulator versions leverage the overloading of the insertion operator to provide convenience. To utilise the manipulators, it is necessary to include the iomanip header file.

Boolean stream manipulators

When displaying Boolean values in C++, the default behaviour is to output a 1 for true and a 0 for false. However, there are situations where we might prefer to display the strings “true” or “false” instead. Instead of writing if-else logic every time we need to format Boolean values, C++ provides convenient stream manipulators to control the display format.

Let’s explore examples of formatting Boolean types using cout as the output stream. Keep in mind that these manipulators can be used with any output stream, including file output streams.

// Default behaviour for Boolean values
cout << (10 == 10) << endl;   // Output: 1 (true)
cout << (10 == 20) << endl;   // Output: 0 (false)

To switch to the “true/false” mode, we can use the boolalpha manipulator:

// Formatting Boolean values as "true" or "false"
cout << boolalpha;    // Set the output stream to bool alpha mode
cout << (10 == 10) << endl;   // Output: true
cout << (10 == 20) << endl;   // Output: false

Once we set the boolalpha mode, all subsequent Boolean values will be displayed accordingly. To toggle back to the default behaviour, we can use the noboolalpha manipulator:

// Switching back to default Boolean formatting
cout << noboolalpha;    // Reset the output stream to default
cout << (10 == 10) << endl;   // Output: 1
cout << (10 == 20) << endl;   // Output: 0

Alternatively, we can use the setf method to set the formatting for Boolean types:

// Setting Boolean formatting using setf method
cout.setf(ios::boolalpha);    // Set the bool alpha format flag
cout << (10 == 10) << endl;   // Output: true
cout << (10 == 20) << endl;   // Output: false

To reset the formatting flag back to the default, we can use one of the following 2 ways:

  1. The unsetf method:
// Resetting Boolean formatting using unsetf method
cout.unsetf(ios::boolalpha);    // Reset the bool alpha format flag
cout << (10 == 10) << endl;   // Output: 1
cout << (10 == 20) << endl;   // Output: 0
  1. The resetiosflags method:
// Resetting Boolean formatting using resetiosflags method
cout << resetiosflags (ios::boolalpha);  // Reset the bool alpha format flag
cout << (10 == 10) << endl;   // Output: 1
cout << (10 == 20) << endl;   // Output: 0

While both methods (setf and unsetf) can be used, the stream manipulator version (boolalpha and noboolalpha) is more commonly used and recommended for simplicity and readability.

Here’s an example that demonstrates the usage of setf, unsetf, boolalpha, noboolalpha and resetiosflags:

#include <iostream>
#include <iomanip>

int main() {
    bool value = true;

    // Display the Boolean value using the default formatting
    std::cout << "Default Formatting: " << value << std::endl;

    // Display the Boolean value using boolalpha manipulator
    std::cout << "boolalpha Formatting: " << std::boolalpha << value << std::endl;

    // Toggle the formatting using noboolalpha manipulator
    std::cout << "noboolalpha Formatting: " << std::noboolalpha << value << std::endl;

    // Display the Boolean value using setf method
    std::cout.setf(std::ios::boolalpha);
    std::cout << "setf Formatting: " << value << std::endl;

    // Reset the formatting back to default using unsetf method
    std::cout.unsetf(std::ios::boolalpha);
    std::cout << "unsetf Formatting: " << value << std::endl;
    
    // Display the Boolean value using setf method
    std::cout.setf(std::ios::boolalpha);
    std::cout << "setf Formatting: " << value << std::endl;
    
    // Display the default Boolean value using resetiosflags method
    std::cout << std::resetiosflags(std::ios::boolalpha);
    std::cout << "resetiosflags Formatting: " << value << std::endl;

    return 0;
}

Output:

Default Formatting: 1
boolalpha Formatting: true
noboolalpha Formatting: 1
setf Formatting: true
unsetf Formatting: 1
setf Formatting: true
resetiosflags Formatting: 1

Explanation:

  1. The code starts by including the necessary headers <iostream> and <iomanip> for input/output operations and manipulators.
  2. Inside the main() function, a Boolean variable value is declared and initialized with the value true.
  3. The first output statement uses the default formatting for Boolean values. It displays the message “Default Formatting:” followed by the value of value, which will be 1 for true.
  4. The second output statement demonstrates the use of the std::boolalpha manipulator. By inserting std::boolalpha before value, it enables the boolalpha formatting flag for the subsequent output operation. The output displays the message “boolalpha Formatting:” followed by the value of value, which is now displayed as the string “true”.
  5. The third output statement uses the std::noboolalpha manipulator to toggle the formatting back to the default. The output displays the message “noboolalpha Formatting:” followed by the value of value, which reverts to displaying 1 for true.
  6. The fourth output statement uses the std::cout.setf() method to set the boolalpha formatting flag explicitly. This enables the boolalpha formatting for subsequent output operations without using the manipulator. The output displays the message “setf Formatting:” followed by the value of value, which is displayed as “true”.
  7. The fifth output statement uses the std::cout.unsetf() method to unset the boolalpha formatting flag, effectively resetting the formatting back to the default. The output displays the message “unsetf Formatting:” followed by the value of value, which reverts to displaying 1 for true.
  8. The sixth output statement uses the std::cout.setf() method again to set the boolalpha formatting flag. The output displays the message “setf Formatting:” followed by the value of value, which is displayed as “true” again.
  9. The seventh output statement demonstrates the use of std::resetiosflags() function with the std::ios::boolalpha flag. It resets the boolalpha formatting flag back to the default state for the std::cout stream. The output displays the message “resetiosflags Formatting:” followed by the value of value, which reverts to displaying 1 for true.

Exercises

  1. Write a program that prompts the user to enter a boolean value (true or false) and displays it using the boolalpha manipulator.
  2. Write a program that reads a boolean value from the user and displays it in uppercase using the uppercase manipulator.
  3. Write a program that reads a boolean value from the user and displays it in lowercase using the nouppercase manipulator.
  4. Write a program that prompts the user to enter two boolean values and displays them side by side, separated by a tab character, using the boolalpha manipulator.
  5. Write a program that reads a boolean value from the user and displays it as “Yes” if it’s true and “No” if it’s false using the boolalpha and true/false stream manipulators.

Integer stream manipulators

In C++, there are various formatting options available for displaying integers when outputting to a stream. Let’s explore these options and see how they can be used.

By default, integers are displayed in base 10, which is the decimal system commonly used by humans. However, we have the flexibility to display integers in other bases as well. Octal (base 8) and hexadecimal (base 16) are two commonly used alternate bases in computing. To demonstrate these formatting options, let’s consider the following code snippet:

#include <iostream>
#include <iomanip>

int main() {
    int num = 255;

    // Display integer in decimal (base 10) - default formatting
    std::cout << "Decimal (default): " << num << std::endl;

    // Display integer in hexadecimal (base 16)
    std::cout << "Hexadecimal: " << std::hex << num << std::endl;

    // Display integer in octal (base 8)
    std::cout << "Octal: " << std::oct << num << std::endl;

    return 0;
}

Output:

Decimal (default): 255
Hexadecimal: ff
Octal: 377

In the above code, we initialise the integer num to the value 255. Then, we use manipulators (std::hex and std::oct) to change the base of the output stream.

As you can see, changing the base using the manipulators affects the subsequent output operations on the stream. The std::hex manipulator formats the integer as a hexadecimal value (ff in this case), and the std::oct manipulator formats it as an octal value (377 in this case).

To make it clear that the values are in hexadecimal or octal, we can use the std::showbase manipulator. Let’s modify the code to include this manipulator:

#include <iostream>
#include <iomanip>

int main() {
    int num = 255;

    // Display integer in decimal (base 10) - default formatting
    std::cout << "Decimal (default): " << num << std::endl;

    // Display integer in hexadecimal (base 16) with prefix
    std::cout << "Hexadecimal (with prefix): " << std::showbase << std::hex << num << std::endl;

    // Display integer in octal (base 8) with prefix
    std::cout << "Octal (with prefix): " << std::showbase << std::oct << num << std::endl;

    return 0;
}

Output:

Decimal (default): 255
Hexadecimal (with prefix): 0xff
Octal (with prefix): 0377

With the std::showbase manipulator, the hexadecimal value is displayed with the 0x prefix, indicating that it’s in hexadecimal format. Similarly, the octal value is displayed with a leading 0 to indicate its base. This can be toggled back using std::noshowbase manipulator.

Additionally, we can control the case of hexadecimal digits and the plus sign for positive integers. Let’s modify the code to include these options:

#include <iostream>
#include <iomanip>

int main() {
    int num = 255;

    // Display integer in hexadecimal (base 16) with uppercase letters
    std::cout << "Hexadecimal (uppercase): " << std::showbase << std::uppercase << std::hex << num << std::endl;

    // Display integer in decimal (base 10) with plus sign
    std::cout << "Decimal (with plus sign): " << std::showpos << std::dec << num << std::endl;

    return 0;
}

Output:

Hexadecimal (uppercase): 0XFF
Decimal (with plus sign): +255

By using the std::uppercase manipulator, the hexadecimal digits are displayed in uppercase (FF in this case). When we combine it with std::showpos, a plus sign is added in front of positive integers. We can toggle back this setting using std::noshowpos to not show the positive sign.

Remember that these formatting options persist for future integer output operations unless changed or reset. To reset the formatting flags to their defaults, you can use the std::resetiosflags() function or the std::cout.flags() method. This is demonstrated in the following code snippet:

#include <iostream>
#include <iomanip>

int main() {
    int num = 255;

    // Display integer in hexadecimal (base 16) with prefix
    std::cout << "Hexadecimal (with prefix): " << std::showbase << std::hex << num << std::endl;

    // Reset the formatting flags to default
    std::cout.flags(std::ios_base::dec);

    // Display the same integer using the default formatting
    std::cout << "Default Formatting: " << num << std::endl;

    // Display integer in hexadecimal (base 16) with prefix
    std::cout << "Hexadecimal (with prefix): " << std::showbase << std::hex << num << std::endl;

    // Reset the base formatting flag to default
    std::cout << std::resetiosflags(std::ios_base::basefield);

    // Display the same integer using the default formatting
    std::cout << "Default Formatting: " << num << std::endl;

    return 0;
}

Output:

Hexadecimal (with prefix): 0xff
Default Formatting: 255
Hexadecimal (with prefix): 0xff
Default Formatting: 255

Binary equivalent value generation

As we have seen manipulating integers to hex and octal values in the stream, it is useful to know how to manipulate the given integer to binary too. We can achieve this using bitset. The following example demonstrates the same:

std::cout << "42 in binary:  " << std::bitset<8>{42} << std::endl;
// output : The number 42 in binary:  00101010

Exercises

  1. Write a program that prompts the user to enter an integer and displays it in hexadecimal format.
  2. Create a program that calculates the factorial of a given positive integer. Prompt the user to enter the integer. Display the factorial in decimal format.
  3. Write a program that converts a binary number to its decimal equivalent. Prompt the user to enter a binary number as a string. Display the decimal equivalent using integer stream manipulators. Hint: Use u_long() method of string class.
  4. Create a program that determines whether a given integer is even or odd. Prompt the user to enter an integer. Display “Even” or “Odd” using integer stream manipulators.
  5. Write a program that converts a decimal number to its binary representation. Prompt the user to enter a decimal number. Display the binary representation using integer stream manipulators.

Floating point stream manipulators

Stream formatting options are available for formatting floating-point numbers. One of the key aspects is controlling the precision of the displayed number. By default, the precision is set to 6 digits. Let’s explore some examples and code snippets to understand these concepts better.

Defaults

To begin, let’s consider precision. By default, the precision determines the number of digits displayed after the decimal point. For instance, if we have the number 1234.5678, and we display it in C++, the default precision of 6 will round the number and display it as 1234.57.

#include <iostream>
#include <iomanip>
int main() {
    double num = 1234.5678;
    std::cout << num << std::endl;
    return 0;
}

Output:

1234.57

If the precision is insufficient to represent the number accurately, C++ will resort to scientific notation. Let’s consider another example with a larger number, 123456789.987654321:

#include <iostream>
#include <iomanip>
int main() {
    double num = 123456789.987654321;
    std::cout << num << std::endl;
    return 0;
}

Output:

1.23457e+08

In this case, the number is displayed using scientific notation (1.23457e+08) since using the default precision of 6 would not accurately represent the number. The precision still remains 6, but scientific notation is employed.

std::scientific

C++ provides stream manipulator std::scientific to manipulate the number to be streamed in scientific format. The following example demonstrates the same:

#include <iostream>
#include <iomanip>
int main() {
    double num = 1234.5678;
    std::cout << "Without setting std::scientific: " << num << std::endl;
    std::cout << "After setting std::scientific: " << std::scientific << num << std::endl;
    return 0;
}

Output:

Without setting std::scientific: 1234.57
After setting std::scientific: 1.234568e+03

std::setprecision()

To set a specific precision, we can use the std::setprecision() manipulator. For example, if we want to display the number with a precision of 9, we can modify the code as follows:

#include <iostream>
#include <iomanip>
int main() {
    double num = 123456789.987654321;
    std::cout << std::setprecision(9) << num << std::endl;
    return 0;
}

Output:

123456789

In this case, the number is displayed with a precision of 9, and rounding occurs. Note that no trailing zeros are displayed by default.

std::fixed

Next, let’s explore the std::fixed manipulator. When used, precision is determined from the right side of the decimal point. For example:

#include <iostream>
#include <iomanip>
int main() {
    double num = 1234.5678;
    std::cout << std::fixed << std::setprecision(6) << num << std::endl;
    return 0;
}

Output:

1234.567800

In this case, the number is displayed with exactly 6 digits after the decimal point, and trailing zeros are added if necessary. The std::fixed manipulator ensures precision is applied starting from the decimal point.

std::showpos

Using the std::showpos manipulator with floating-point numbers behaves similarly to integers. It adds a preceding plus sign for positive numbers. Here’s an example:

#include <iostream>
#include <iomanip>
int main() {
    double num = 1234.5678;
    std::cout << std::showpos << num << std::endl;
    return 0;
}

Output:

+1234.56

std::showpoint

By default, trailing zeros are not displayed for floating-point numbers. However, we can use the std::showpoint manipulator to include trailing zeros based on the precision used. Consider the following example:

#include <iostream>
#include <iomanip>
int main() {
    double num = 12.34;
    std::cout << std::showpoint << std::setprecision(6) << num << std::endl;
    return 0;
}

Output:

12.3400

To reset the floating-point format back to the general format, you have two options. One way is to use the std::unsetf method:

#include <iostream>
#include <iomanip>

int main() {
    double num = 1234.5678;
    std::cout << std::setprecision(6) << std::scientific << num << std::endl;
    std::cout.unsetf(std::ios::fixed | std::ios::scientific);
    std::cout << num << std::endl;

    return 0;
}

Output:

1.234568e+03
1234.57

Alternatively, you can use the std::resetiosflags manipulator:

std::cout << std::resetiosflags(std::ios::fixed | std::ios::scientific);

Exercises

Exercise 1:

Write a program that prompts the user to enter a floating-point number and displays it in scientific notation with a precision of 3 digits after the decimal point.

Exercise 2:

Create a program that calculates the average of three test scores. Prompt the user to enter the scores as floating-point numbers. Display the average with a precision of 2 digits after the decimal point and in fixed-point notation.

Exercise 3:

Write a program that converts a distance in kilometres to miles. Prompt the user to enter the distance as a floating-point number. Display the result in fixed-point notation with a precision of 1 digit after the decimal point.

Exercise 4:

Create a program that calculates the compound interest on an investment. Prompt the user to enter the principal amount, interest rate (as a percentage), and the number of years. Display the final amount with a precision of 2 digits after the decimal point and in fixed-point notation.

Exercise 5:

Write a program that prompts the user to enter a temperature in Celsius and converts it to Fahrenheit. Display the result with a precision of 1 digit after the decimal point and in fixed-point notation.

Align and fill stream manipulators

We’ll explore some of the stream formatting options available that work with any type of data. These options allow us to define a field width and specify the justification and fill character for the data items. Let’s go through some examples and code snippets to understand these concepts better.

Default behaviour

By default, there is no field width defined, and output occurs right after the previous output. For example:

#include <iostream>
int main() {
    double num = 1234.5678;
    std::string hello = "hello";
    std::cout << num << " " << hello << std::endl;
    return 0;
}

Output:

1234.5678 hello

Field Width (std::setw()):

The std::setw() manipulator sets the field width for the next data item in the stream. It specifies the minimum number of characters that will be allocated for the data item’s display. If the data item requires fewer characters, it will be padded to meet the specified width.

#include <iostream>
#include <iomanip>

int main() {
    int num = 123;
    std::string text = "Hello";

    std::cout << std::setw(8) << num << std::endl;
    std::cout << std::setw(8) << text << std::endl;

    return 0;
}

Output:

     123
   Hello

In this example, std::setw(8) sets the field width to 8 characters for both the num and text data items. The resulting output ensures that each item is displayed within its respective field width.

To set a field width for multiple data items, we can use std::setw() before each data item. For example:

#include <iostream>
#include <iomanip>

int main() {
    double num = 1234.5678;
    std::string hello = "hello";

    std::cout << std::setw(10) << num << std::setw(10) << hello << std::setw(10) << hello << std::endl;

    return 0;
}

Output:

   1234.57      hello     hello

In this case, each data item is displayed within a field width of 10, right-justified by default. The field width and justification settings only apply to the next data item placed on the stream.

Alignment (std::left and std::right):

The std::left and std::right manipulators control the alignment of data within the specified field width. By default, data is right-aligned within the field. std::left makes the data left-aligned within the field width.

#include <iostream>
#include <iomanip>

int main() {
    int num = 123;
    std::string text = "Hello";

    std::cout << std::setw(8) << std::left << num << std::endl;
    std::cout << std::setw(8) << std::left << text << std::endl;

    return 0;
}

Output:

123
Hello

In this example, std::left is used to left-align the num and text data items within their respective field widths.

Fill Character (std::setfill())

The std::setfill() manipulator sets the fill character for padding the unused space within the specified field width. By default, the fill character is a space.

#include <iostream>
#include <iomanip>

int main() {
    int num = 123;
    std::string text = "Hello";

    std::cout << std::setw(8) << std::setfill('.') << num << std::endl;
    std::cout << std::setw(8) << std::setfill('-') << text << std::endl;

    return 0;
}

Output:

.....123
---Hello

In this example, std::setfill('.') and std::setfill('-') are used to set the fill characters to a dot (.) and a dash (-), respectively. The fill characters are used to pad the unused space within the field widths.

Resetting the manipulation

Similar to the previous manipulators, these stream manipulators also retain the manipulation if not reset. To reset, std::resetiosflags() can be used. The following examples demonstrates its usage:

#include <iostream>
#include <iomanip>

int main() {
    int num = 123;
    std::string text = "Hello";

    // Set field width, alignment, and fill character
    std::cout << std::setw(8) << std::setfill('.') << num << std::endl;
    std::cout << std::setw(8) << std::setfill('-') << text << std::endl;

    // Reset field width, alignment, and fill character
    std::cout << std::resetiosflags(std::ios::adjustfield);  // Reset alignment
    std::cout << std::resetiosflags(std::ios::basefield);    // Reset field width
    std::cout << std::setfill(' ');                          // Reset fill character

    // Output with default settings
    std::cout << num << std::endl;
    std::cout << text << std::endl;

    return 0;
}

Output:

.....123
---Hello
123
Hello

By combining these manipulators, you can achieve precise control over the field width, alignment, and fill character for each data item in the stream. These manipulators help in formatting the output in a visually appealing manner.

Exercises

Exercise 1:

Write a program that asks the user to enter three integers and displays them in a table format. Each number should be right-aligned in a field width of 8 characters, and the empty spaces should be filled with ‘0’.

Example:

Enter three integers: 12 345 6
00000012 00000345 00000006

Exercise 2:

Create a program that calculates the area and perimeter of a rectangle. Prompt the user to enter the length and width of the rectangle as floating-point numbers. Display the results with a precision of 2 digits after the decimal point. The numbers should be left-aligned in a field width of 10 characters, and the empty spaces should be filled with ‘#’.

Exercise 3:

Write a program that displays a countdown from 10 to 1. Each number should be centred in a field width of 5 characters, and the empty spaces should be filled with *.

Exercise 4:

Create a program that converts temperature from Celsius to Fahrenheit. Prompt the user to enter the temperature in Celsius as a floating-point number. Display the result with a precision of 1 digit after the decimal point. The number should be right-aligned in a field width of 6 characters, and the empty spaces should be filled with +.

Exercise 5:

Write a program that prints a pattern of asterisks in a pyramid shape. Prompt the user to enter the number of rows for the pyramid. Each asterisk should be centred in a field width of 3 characters, and the empty spaces should be filled with -.

File operations

For file IO operations using C++ streams, we can use the ifstream, ofstream and fstream classes.

The ifstream class

The ifstream class in C++ is a derived class of the istream (need not be looked at as ifstream is more powerful) class and is specifically designed for reading input from files. It provides a convenient interface to read data from text files, allowing programmers to perform various operations such as reading lines, extracting formatted data, or reading characters from a file.

With the ifstream class, you can easily open a file for input, read data from it, and perform input operations just like you would with the standard input stream cin. It supports input operations such as extracting data using the extraction operator >>, reading lines with getline, or reading characters using member functions like get or getline.

The ifstream class inherits all the functionality of the base istream class and provides additional member functions specifically tailored for file input operations. It allows you to handle file input in a convenient and efficient way, making it an essential tool for reading data from files in C++ programs.

Step 1: Include the necessary header file

To work with file streams, we need to include the <fstream> header file at the beginning of our code. This provides the necessary classes and functions for file input/output operations.

#include <fstream>

Step 2: Declare and connect the stream object

We declare a stream object of either fstream or ifstream type, depending on whether we need both input and output or just input operations. Then, we connect the stream object to the file we want to read from. As we are just reading from the file, we shall use ifstream class.

std::ifstream infile("filename.txt");
// or
std::ifstream infile {"filename.txt"};
// or
std::ifstream infile;
infile.open("filename.txt");

Note that you can also mention the path to the file if the file is not present in the same directory as that of the executable of the program. There is a second optional parameter along with the filename, that is the flag/mode of opening the specified file. They are briefly explained as follows:

  1. std::ios::in

Indicates that the file should be opened for input. It is the default mode for ifstream and allows reading from the file. You can omit this flag when opening an input file.

std::ifstream infile;
infile.open("input.txt", std::ios::in);
  1. std::ios::binary

This flag specifies that the file should be opened in binary mode. It is useful when reading non-text files or when working with raw binary data.

std::ifstream binaryFile;
binaryFile.open("data.bin", std::ios::in | std::ios::binary);

Note that we can specify multiple modes separated by the bitwise OR operator.

  1. std::ios::ate

This flag stands for “at end”. When used in combination with other flags, it seeks to the end of the file immediately after opening it. This can be useful for quickly determining the file size or for efficient file processing.

std::ifstream file;
file.open("data.txt", std::ios::in | std::ios::ate);

Step 3: Check if the file was opened successfully

After opening the file, it’s important to check if the file was opened successfully before proceeding to read from it. We can check this by calling the is_open() method of the stream object.

if (!infile.is_open()) {
    // File opening failed, handle the error
    cout << "Error opening the file." << endl;
    return 1; // or exit the program with an error code
}

Step 4: Read from the file

There are various ways to read from a file using C++ streams. Here are a few examples:

4.1. Reading formatted data using the extraction operator (>>)

We can read formatted data from the text file using the extraction operator, just like we do with cin.

int num;
double total;
string name;

infile >> num >> total >> name;

In this example, the file contains an integer, a double, and a string on separate lines. The extraction operator reads each value from the file and stores it in the corresponding variables.

4.2. Reading an entire line using getline()

If we want to read an entire line of text from the file, we can use the getline() method.

string line;
getline(infile, line);

This reads a single line from the file and stores it in the string variable line. It stops reading when it encounters a newline character \n.

4.3. Reading all lines in the file using a loop

To read all the lines in a text file, we can use a loop and repeatedly call getline() until we reach the end of the file (EOF).

string line;
while (getline(infile, line)) {
    // Process the line
    cout << line << endl;
}

This loop reads each line from the file and processes it. It continues until the getline() function returns false, indicating that we have reached the end of the file.

Alternatively, we can use the get() method to fetch the file contents character by character. This produces the same output as getline() when the starting and ending points of the parsing are the same for both. get() is illustrated in the following snippet:

char c;
while (infile.get(c)) {
        cout << c;
}

Step 5: Close the file

Once we are done reading from the file, it’s important to close it using the close() method.

infile.close();

Closing the file releases any resources associated with it and ensures that the file is properly closed.

Here’s a complete example that incorporates the above steps:

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    ifstream infile("filename.txt");

    if (!infile.is_open()) {
        cout << "Error opening the file." << endl;
        return 1;
    }

    int num;
    double total;
    string name;

    infile >> num >> total >> name;
    cout << "Num: " << num << endl;
    cout << "Total: " << total << endl;
    cout << "Name: " << name << endl;

    string line;
    while (getline(infile, line)) {
        cout << line << endl;
    }

    infile.close();

    return 0;
}

A sample filename.txt for the above example would be as follows:

100
255.67
Larry
This is a line of text.
Another line here.
And a final line.

The output of the program for the above sample would be as follows:

Num: 100
Total: 255.67
Name: Larry

This is a line of text.
Another line here.
And a final line.

The ofstream class

The ofstream class in C++ is a derived class of the ostream class and is specifically designed for writing output to files. It provides a convenient interface to create and write data to text files, allowing programmers to perform various operations such as writing lines, formatted output, or individual characters to a file.

With the ofstream class, you can easily open a file for output, write data to it, and perform output operations just like you would with the standard output stream cout. It supports output operations such as writing data using the insertion operator <<, writing lines with endl, or writing individual characters using member functions like put.

Couple of things to keep in mind regarding the writing to a file operations:

  1. Output files will be created if they do not exist.
  2. Output files will be overwritten (truncated) by default.
  3. Can be opened so that new writes append.
  4. Can be opened in text or binary formats.

Here’s an example that demonstrates the usage of ofstream for writing text to a file:

#include <iostream>
#include <fstream>

int main() {
  std::ofstream outfile("output.txt"); // Alternatively, .open() method can be used.

  if (outfile.is_open()) {
    outfile << "Hello, world!" << std::endl;
    outfile << "This is a sample line." << std::endl;
    outfile.close();
    std::cout << "Data written to the file successfully." << std::endl;
  } else {
    std::cout << "Failed to create the file." << std::endl;
  }

  return 0;
}

In this example, we create an ofstream object named outfile and open the file output.txt for writing. We then use the insertion operator << to write text to the file. Finally, we close the file using the close() member function. If the file was created and written successfully, it displays a success message on the console.

Similar to ifstream, ofstream also supports different flags and modes to control the behaviour of the file stream. These flags and modes determine how the file is opened and how data is written to it. Here are some commonly used flags and modes:

  • std::ios::out (default): This flag indicates that the file should be opened for output. It allows you to write data to the file.
  • std::ios::app: This flag is used to open the file in append mode. When the file is opened in this mode, any data written to it will be appended to the existing contents of the file, rather than overwriting them.
  • std::ios::trunc: This flag is used to truncate the file if it already exists. If the file is opened with this flag, its existing contents will be deleted, and the file will be treated as empty before writing new data to it.
  • std::ios::binary: This flag is used to open the file in binary mode. When the file is opened in binary mode, data is treated as binary data rather than text. This mode is useful when working with non-text files or when you want to perform low-level operations on the file.

These flags can be combined using the bitwise OR operator (|) to specify multiple flags simultaneously. Here are a few examples:

#include <iostream>
#include <fstream>

int main() {
  std::ofstream outfile;

  // Open the file in output mode (default)
  outfile.open("output.txt");

  // Open the file in append mode
  outfile.open("output.txt", std::ios::app);

  // Open the file in truncation mode
  outfile.open("output.txt", std::ios::trunc);

  // Open the file in binary mode
  outfile.open("output.txt", std::ios::binary);

  return 0;
}

The fstream class

The fstream class in C++ provides a convenient way to work with files for both input and output operations. It is derived from the iostream class and combines the functionalities of both ifstream and ofstream. With fstream, you can perform read and write operations on files using the same stream object. Here’s an overview of fstream along with some examples and modes:

Opening Files

To open a file using fstream, you need to declare an fstream object and use the open() member function to specify the file name and open mode. The open modes determine how the file will be accessed. Here are the commonly used modes:

  • std::ios::in: Open the file for reading.
  • std::ios::out: Open the file for writing.
  • std::ios::app: Append to the end of the file.
  • std::ios::trunc: Truncate the file if it exists.
  • std::ios::binary: Open the file in binary mode.

You can combine multiple modes using the bitwise OR operator (|). If no mode is specified, std::ios::in | std::ios::out is used by default. Here’s an example:

#include <iostream>
#include <fstream>

int main() {
  std::fstream file;
  
  // Open file in write mode and truncate if it exists
  file.open("data.txt", std::ios::out | std::ios::trunc);
  
  // Check if file opened successfully
  if (file.is_open()) {
    // File operations
    file << "Hello, World!";
    
    // Close the file
    file.close();
  }
  return 0;
}

In this example, an fstream object named file is created. The file data.txt is opened in write mode with std::ios::out | std::ios::trunc. The file is then written with the text “Hello, World!” using the stream insertion operator (<<). Finally, the file is closed with the close() member function.

Reading from Files

To read from a file using fstream, you can use the stream extraction operator (>>) or the getline() function to extract data from the file. Here’s an example:

#include <iostream>
#include <fstream>
#include <string>

int main() {
  std::fstream file;
  std::string line;
  
  // Open file in read mode
  file.open("data.txt", std::ios::in);
  
  // Check if file opened successfully
  if (file.is_open()) {
    // Read file line by line
    while (getline(file, line)) {
      std::cout << line << std::endl;
    }
    
    // Close the file
    file.close();
  }
  
  return 0;
}

In this example, the file data.txt is opened in read mode with std::ios::in. The getline() function is used to read the file line by line, and each line is printed to the console.

Random Access

One of the advantages of fstream is the ability to perform random access operations on files. You can use the seekg() (read pointer seek) and seekp() (write pointer seek) member functions to set the position of the read and write pointers within the file, respectively. Here’s an example:

#include <iostream>
#include <fstream>

int main() {
  std::fstream file;
  int num;
  
  // Open file in read/write mode
  file.open("data.txt", std::ios::in | std::ios::out);
  
  // Check if file opened successfully
  if (file.is_open()) {
    // Read and print the first number
    file >> num;
    std::cout << "First number: " << num << std::endl;
    
    // Set the write pointer to the beginning of the file
    file.seekp(0, std::ios::beg);
    
    // Write a new number
    file << 42;
    
    // Close the file
    file.close();
  }
  
  return 0;
}

In this example, the file data.txt is opened in read/write mode with std::ios::in | std::ios::out. The first number is read from the file and printed. Then, the seekp() function is used to set the write pointer to the beginning of the file, and the number 42 is written to the file, overwriting the existing data.

Exercises

  1. Write a program that reads a text file named “input.txt” and prints its contents to the console.
  2. Write a program that reads a text file named “input.txt” and counts the number of lines in the file.
  3. Write a program that prompts the user to enter a sentence and saves it to a text file named “output.txt”.
  4. Write a program that reads a binary file named “data.bin” and calculates the sum of all the integers in the file.
  5. Write a program that creates a text file named “output.txt” and writes the numbers from 1 to 100, each on a new line.
  6. Write a program that reads a text file named “input.txt” and creates a new file named “output.txt” with all the vowels removed from the text.
  7. Write a program that reads a binary file named “data.bin” and finds the maximum and minimum values in the file.
  8. Write a program that reads a text file named “input.txt” and writes its contents in reverse order to a new file named “output.txt”.
  9. Write a program that reads a text file named “input.txt” and counts the occurrences of a specific word entered by the user.
  10. Write a program that reads a text file named “input.txt” and capitalise the first letter of each word, then saves the modified text to a new file named “output.txt”.

String streams

String Streams provide a convenient way to manipulate strings as if they were streams of input or output. This concept is powerful for tasks like data validation. To work with string streams, we need to include the <sstream> header file. There are three classes we can use: stringstream, istringstream, and ostringstream.

Here’s an example of reading from a stringstream:

#include <sstream>
#include <iostream>

int main() {
  std::string info = "Moe 100 1234.5";
  std::istringstream iss(info);

  std::string name;
  int num;
  double total;

  iss >> name >> num >> total;

  std::cout << "Name: " << name << std::endl;
  std::cout << "Number: " << num << std::endl;
  std::cout << "Total: " << total << std::endl;

  return 0;
}

Output:

Name: Moe
Number: 100
Total: 1234.5

In this example, we initialise the istringstream object iss with the string info. We then use the stream extraction operator (>>) to read the data from the stringstream into the respective variables name, num, and total. Finally, we print the values to the console.

Now let’s look at an example of writing to a stringstream:

#include <sstream>
#include <iostream>

int main() {
  int num = 100;
  double total = 1234.5;
  std::string name = "Moe";

  std::ostringstream oss;
  oss << "Name: " << name << ", Number: " << num << ", Total: " << total;

  std::cout << "Formatted string: " << oss.str() << std::endl;

  return 0;
}

Output:

Formatted string: Name: Moe, Number: 100, Total: 1234.5

In this example, we use the ostringstream object oss to write formatted data to the stringstream. We use the stream insertion operator (<<) to insert the data into the stringstream. The resulting formatted string can be retrieved using the str() method of the stringstream.

Lastly, let’s explore an example of basic data validation using a stringstream:

#include <sstream>
#include <iostream>

int main() {
  std::string input;
  int value;
  bool done = false;

  do {
    std::cout << "Enter an integer: ";
    std::getline(std::cin, input);

    std::istringstream iss(input);
    if (iss >> value) {
      done = true;
    } else {
      std::cout << "Invalid input. Please enter an integer." << std::endl;
    }

    // Clear the input stream and ignore any remaining characters
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // ignore any remaining characters in the input stream until a newline character ('\n') is encountered
  } while (!done);

  std::cout << "Entered integer: " << value << std::endl;

  return 0;
}

In this example, we repeatedly prompt the user to enter an integer until a valid integer is provided. We use a do-while loop to ensure that the input is validated at least once. Inside the loop, we read the user’s input into the string variable input. Then, we create an istringstream object iss and connect it to input. We attempt to extract an integer from iss using the stream extraction operator (>>). If the extraction is successful, we set done to true and exit the loop.

Exercises

Exercise 1: Word Count

Write a program that takes a sentence as input and counts the number of words in it. Use a string stream to split the sentence into individual words.

Exercise 2: String Reversal

Write a program that takes a string as input and reverses its content using string streams.

Exercise 3: Palindrome Check

Write a program that checks if a given string is a palindrome. Use string streams to remove any spaces or punctuation marks from the input string and compare the resulting string with its reverse.

Exercise 4: CSV Parsing

Write a program that reads a CSV (Comma-Separated Values) file and extracts specific data from it. Use string streams to parse the CSV rows and extract the desired values.

Exercise 5: Data Serialization

Write a program that reads data from a file, stores it in a data structure (e.g., a vector or a map), and then serialise the data back into a string using string streams.

Categories
Computer Science / Information Technology Language: C++

Exception Handling

In this section of the course, we will learn about exception handling in C++. Exception handling is a complex topic, but it is essential for writing robust and reliable C++ programs. 

Overview

What is an exception?

An exception is an event that disrupts the normal flow of execution of a program. Exceptions can be caused by a variety of factors, such as invalid input, hardware errors, or programming errors.

What is exception handling?

When an exception occurs, the program can either handle the exception or terminate. Handling an exception means that the program takes steps to recover from the error and continue execution. Terminating the program means that the program stops executing and returns an error code.

Handling exception

In C++, exceptions are handled using the try-catch block. The try block contains the code that is being executed when an exception occurs. In other words, try block contains a block of code that has a possibility to throw an exception. The catch block contains the code that is executed to handle the exception.

Stack unwinding

Stack unwinding is a process that is used to clean up resources when an exception occurs. When an exception occurs, the C++ runtime system unwinds the stack. This means that the runtime system calls the destructors for all of the objects that were created on the stack.

Defining our own exception classes

We can create our own exception classes to provide more information about the exceptions that our programs throw. Exception classes can be derived from the std::exception class.

The Standard Library Exception Hierarchy

The standard exception class hierarchy provides a set of predefined exception classes that can be used to handle common errors. These exception classes include std::out_of_range, std::invalid_argument, and std::runtime_error.

Best practices

Writing exception-safe code is a complex task. Exception-safe code is code that can handle exceptions without causing data corruption. Writing exception-safe code requires careful planning and design.

There are a few best practices for C++ exception handling. These best practices include:

  • Only throw exceptions for exceptional conditions.
  • Always catch exceptions in the smallest scope possible.
  • Rethrow exceptions that cannot be handled.

By following these best practices, you can write C++ programs that are robust and reliable. Do not get overwhelmed with this initial information. We shall see each of these in the section.

Basic concepts of exception handling

In C++, exception handling is a mechanism used to deal with extraordinary situations that may occur during the execution of a program. It allows us to detect and handle errors or exceptional conditions that would otherwise disrupt the normal flow of the program.

Exception handling is specifically designed for synchronous code, where exceptions are thrown and caught within the same thread of execution. It is not meant to handle asynchronous events or multi-threaded scenarios.

The concept behind exception handling is to define what constitutes an extraordinary situation for our application. The definition of such situations depends on the specific requirements and design of the application. Exceptional situations be any of these to start with:

  • Resource limitations, such as running out of memory or storage
  • Missing resources like a required file that doesn’t exist.
  • Invalid operations
  • Range violations
  • Underflows
  • Overflows
  • Illegal data

The goal of exception handling is to detect when an exceptional situation occurs or is about to occur, and then take appropriate actions to handle it. The course of action may vary depending on the nature of the exception and the application itself. In some cases, it may be possible to recover and continue the program execution. However, in more critical situations, termination of the program may be the only feasible option. Even in such cases, we have control over how the program terminates. We can perform cleanup tasks, close files, save data, and ensure a graceful shutdown.

Writing exception-safe code is an important aspect of C++ programming. It means that our code is designed to handle exceptions properly, ensuring that resources are properly released and no memory leaks occur. Achieving complete exception safety is challenging in C++, but by adopting good coding practices and utilising exception handling mechanisms effectively, we can minimise the impact of exceptions on our programs.

Terminology

Let’s delve into the terminology used in C++ when discussing exception handling. While the terminology shares similarities with other programming languages, the way it functions in C++ differs.

Exception

In C++, an exception refers to an object or primitive type (e.g., int, double, boolean) that signals an error condition. This exception object often contains information about the specific problem that occurred. When a code segment determines that something is wrong, it can throw an exception.

Throwing an exception (raising an exception)

The reason code may throw an exception instead of handling the problem itself is that the code might not know how to handle the issue effectively. It throws an exception with the hope that another part of the program can handle it appropriately. This is where catching an exception comes into play. Another section of the program can contain code that catches the thrown exception and performs the necessary actions.

Catching an exception (handle the exception)

Handling an exception can vary depending on the situation. It could involve displaying an error message, logging the error, and terminating the program if it cannot proceed. This is acceptable if the program cannot continue its execution and a graceful failure is desired. However, in other cases, the exception can be handled, allowing for recovery and continued processing.

Example

Let’s consider an example to illustrate this concept. Suppose we want to dynamically allocate memory, but there is insufficient memory available. The code responsible for memory allocation would throw an exception since it couldn’t allocate more memory. However, it doesn’t know how to handle this situation. In another part of the program, we can catch this exception, clear buffers or caches, release some memory, and then attempt the allocation again.

Keywords

C++ employs three keywords to facilitate exception handling: throw, try, and catch.

throw

The throw keyword is used to throw an exception object or primitive type. It is usually followed by an argument representing the exception being thrown.

try { code that may throw an exception }

The try keyword is followed by a code block enclosed in curly braces. This code block contains the segment that may potentially throw an exception, so it is placed within the try block. If no exception is thrown, the code within the try block executes as usual. However, if an exception is thrown, the remaining code in the block is not executed, and C++ searches for a suitable catch block to handle the thrown exception.

catch (Exception ex) { code to handle the exception }

The catch keyword is used to define a catch block, which is followed by the type of exception object it handles. It also has a code block where the code that handles the exception executes.

Catch blocks only execute if an exception is thrown, and the type of the thrown exception matches the parameter in the catch block. We can write multiple catch handlers that expect different types of exceptions.

Example

To better understand exception handling, let’s explore a simple example in C++ that involves division by zero.

Consider the following code snippet, where we calculate the average by dividing the sum by the total:

// Assume sum and total have been declared and initialised
double average;
if (total != 0) {
    average = sum / total;
    // Use the calculated average
}

In this case, we explicitly check if the total variable is zero before performing the division. By doing so, we prevent the program from crashing or producing undefined results. However, if this code exists within a function and the function is expected to return the average, what should we return if the total is zero? This question becomes more challenging to answer.

To handle such situations using exception handling, we can use a try-catch block. Here’s an example to demonstrate the syntax for exception handling:

try {
    // Assume sum, total, and average have been declared and initialised
    if (total == 0) {
        throw 0;  // Throw an exception of type int
    }

    average = sum / total;
    // Use the calculated average

    // Rest of the code within the try block executes normally

    cout << "Program continues" << endl;
} catch (int &exception) {
    // Handle the exception
    cout << "Error: Division by zero" << endl;

    // Code within the catch block executes

    cout << "Program continues" << endl;
}

In the try block, we write the code that may potentially throw an exception. In this case, if total equals 0, we throw an exception of type int with the value 0. It’s worth noting that throwing objects (instead of primitives) is considered a best practice, but we’ll cover that later.

If the total is not equal to 0, the code within the try block executes normally, the division occurs, and we can use the calculated average. After the try block completes, the control transfers to the last statement that displays “Program continues.” Since no exception was thrown, the catch block is skipped entirely.

However, if the total is equal to 0, the code in the try block encounters the throw statement, which immediately transfers the control to the corresponding catch block. Here, the catch block expects an exception object of the same type (int) as the one we threw. The code within the catch block executes, and after it finishes, the control transfers to the last statement that displays “Program continues” again.

By utilising exception handling, we prevent the divide-by-zero error and handle it in a controlled manner. It’s important to note that while the presented code demonstrates the syntax for exception handling, in practice, it’s more common to use if-else statements to handle such cases unless more complex exception handling is required.

Throwing different exception codes

The next step in exploiting the throw-try-catch trio is to throw different error codes for different scenarios. Here’s an example that demonstrates throwing integer exceptions at different places based on various error scenarios, and then handling those exceptions using a switch statement within the catch block.

#include <iostream>
#include <string>

int divideNumbers(int numerator, int denominator) {
    if (denominator == 0) {
        throw 1;  // Throw exception 1 for divide by zero error
    }

    if (numerator < 0) {
        throw 2;  // Throw exception 2 for negative numerator
    }

    if (denominator < 0) {
        throw 3;  // Throw exception 3 for negative denominator
    }

    return numerator / denominator;
}

int main() {
    int numerator, denominator;

    std::cout << "Enter the numerator: ";
    std::cin >> numerator;

    std::cout << "Enter the denominator: ";
    std::cin >> denominator;

    try {
        int result = divideNumbers(numerator, denominator);
        std::cout << "Result: " << result << std::endl;
    } catch (int exception) {
        switch (exception) {
            case 1:
                std::cout << "Error: Divide by zero!" << std::endl;
                break;
            case 2:
                std::cout << "Error: Negative numerator!" << std::endl;
                break;
            case 3:
                std::cout << "Error: Negative denominator!" << std::endl;
                break;
            default:
                std::cout << "Unknown error occurred!" << std::endl;
                break;
        }
    }

    return 0;
}

In this example, we have a function called divideNumbers that takes a numerator and a denominator as input. It performs division and returns the result. However, it can encounter different error scenarios:

  • If the denominator is 0, it throws an exception with the value 1 to indicate a divide-by-zero error.
  • If the numerator is negative, it throws an exception with the value 2 to indicate a negative numerator.
  • If the denominator is negative, it throws an exception with the value 3 to indicate a negative denominator.

In the main function, we prompt the user to enter the numerator and denominator. We then call the divideNumbers function within a try block. If an exception is thrown during the function call, the corresponding catch block executes.

Inside the catch block, we utilise a switch statement to handle different exception scenarios based on their integer values. If exception 1 is caught, it means a divide-by-zero error occurred, and we display the message “Error: Divide by zero!”. If exception 2 is caught, it means a negative numerator was encountered, and we display the message “Error: Negative numerator!”. If exception 3 is caught, it means a negative denominator was encountered, and we display the message “Error: Negative denominator!”. If none of these exceptions are caught, the default case executes and displays the message “Unknown error occurred!”.

By utilising the switch statement within the catch block, we can handle different exception scenarios separately and provide appropriate error messages or actions for each scenario.

Throwing an exception from a function

Let’s explore a common use case in C++ where exception handling can be applied. We’ll examine a function called calculateAverage that takes the sum of integers (assumed to be pre-calculated) and the total number of elements used to obtain that sum. The goal is to determine the average and return it as a double from this function. Additionally, we want to avoid integer division and ensure that division by zero is handled properly.

double calculateAverage(int sum, int total) {
    if (total == 0) {
        throw 0;  // Throw an exception if total is 0
    }
    
    double average = static_cast<double>(sum) / total;
    return average;
}

In the calculateAverage function, we first check if the total is equal to 0. If it is, we throw an exception of type int with the value 0. Throwing an exception causes the division to be bypassed, and the function terminates. The thrown exception will propagate up the call stack until it finds an appropriate exception handler.

Now, let’s see how we can call this function in a way that handles the exception:

try {
    // Assume sum and total have been declared and initialised
    double average = calculateAverage(sum, total);
    cout << "Average: " << average << endl;
} catch (int &exception) {
    cout << "Error: Division by zero" << endl;
}

cout << "Bye!" << endl;

In this code snippet, we encapsulate the function call to calculateAverage within a try block. We anticipate that the function might throw an exception, and if it does, we handle it in the catch block. If no exception is thrown, the code within the try block executes normally.

If an exception is thrown during the function call (i.e., when total is 0), the remaining code within the try block is skipped, and the program searches for a catch block that can handle the thrown exception. In this case, it finds a catch block that expects an integer exception. The code within the catch block executes, displaying an error message indicating division by zero.

After the catch block completes, the control transfers to the last statement, which displays “Bye!” Whether an exception occurs or not, this statement will always execute.

It’s worth noting that we can have multiple catch blocks to handle different types of exceptions or catch-all handlers that can catch any type of exception. However, in this example, we only handle exceptions of type int.

By utilising exception handling, we can ensure that the division by zero scenario is properly handled and the program continues execution in a controlled manner.

The noexcept keyword

In C++, the noexcept keyword is used to specify that a function will not throw any exceptions. It is part of exception specification, which is a way to indicate the exception-handling behaviour of a function.

By using the noexcept keyword, you declare that a function will not throw any exceptions during its execution. This allows the compiler to perform certain optimizations and make assumptions about the function’s behaviour.

Here are some key points about the noexcept keyword:

  1. Exception Specification: The noexcept keyword is used in the function declaration or definition to indicate that the function is declared to be noexcept. It is placed after the parameter list and before the function body.
  2. No Exceptions: A function marked with noexcept guarantees that it will not throw any exceptions. If an exception is thrown from a noexcept function, the std::terminate() function is called, resulting in the termination of the program.
  3. Exception Propagation: If a noexcept function calls another function that may throw exceptions, the exception will propagate to the caller of the noexcept function unless it is caught within the noexcept function.
  4. Exception Specification Dynamic Type: The noexcept keyword can also take an optional argument in parentheses, such as noexcept(expression), where the expression is evaluated at runtime. If the expression evaluates to true, the function is considered noexcept; otherwise, it is not.

Here’s an example demonstrating the usage of noexcept:

void process() noexcept {
    // Function body
}

void foo() noexcept(true) {
    // Function body
}

void bar() noexcept(2 > 1) {
    // Function body
}

int main() {
    process();  // No exceptions thrown
    foo();      // No exceptions thrown
    bar();      // No exceptions thrown

    try {
        process();  // No exceptions thrown
    } catch (...) {
        // This code will not execute because process() is noexcept
    }

    return 0;
}

In the example, the process() function is declared as noexcept and guarantees not to throw any exceptions. It can be called within a try block, but any exceptions thrown from within the function will not be caught.

The foo() and bar() functions also use the noexcept specifier, with the latter demonstrating the usage of an expression in the noexcept argument.

Using noexcept can help improve code optimization and provide guarantees about exception handling. However, it is important to carefully consider the usage of noexcept and ensure that it accurately reflects the function’s behaviour to avoid unexpected program termination.

noexcept vs throw() vs noexcept()

In C++, noexcept, throw(), and noexcept() are used to specify exception handling behaviour and constraints for functions. Here are the differences between these three:

noexcept

noexcept is a C++11 keyword used to declare that a function will not throw any exceptions. When a function is marked as noexcept, it is a guarantee to callers and the compiler that no exceptions will be thrown from that function. If an exception is thrown from a noexcept function, the std::terminate function is called, terminating the program.

Example: void foo() noexcept;

throw()

throw() is an exception specification used in pre-C++11 code to indicate that a function does not throw any exceptions. It is also known as the dynamic exception specification.

In C++11 and later, it is deprecated and replaced by noexcept. It was less reliable than noexcept because it did not provide compile-time checking for exception safety.

Example: void bar() throw();

noexcept()

noexcept() is a C++11 feature that allows you to conditionally specify exception handling behaviour based on expressions.

The expression inside noexcept() determines whether the function is noexcept or not. If the expression evaluates to true, the function is noexcept. If it evaluates to false, the function is not noexcept.

This feature is useful when you want to conditionally specify exception handling based on runtime conditions.

Example: void baz() noexcept(sizeof(T) > 4);

Handling multiple exceptions

Let’s explore the concept of throwing and catching multiple exceptions in C++. Consider a function called calculateMilesPerGallon, which takes the number of miles and the number of gallons as parameters and performs the division to calculate the result. However, this function can encounter multiple error scenarios: division by zero if the number of gallons is 0 and incorrect results if either miles or gallons is negative. We’ll see how we can handle these situations by throwing different types of exceptions.

double calculateMilesPerGallon(int miles, int gallons) {
    if (gallons == 0) {
        throw 0;  // Throw an exception of type int for divide by zero error
    }
    
    if (miles < 0 || gallons < 0) {
        throw std::string("Negative value error");  // Throw an exception of type std::string for negative values
    }
    
    return static_cast<double>(miles) / gallons;
}

In the calculateMilesPerGallon function, we first check if gallons are equal to 0. If it is, we throw an exception of type int with the value 0 to indicate a divide-by-zero error. Additionally, we check if either miles or gallons is negative. If they are, we throw an exception of type std::string initialised with the message “Negative value error” to indicate the presence of negative values. If none of these error scenarios occur, we perform the division and return the result.

Now let’s see how we can handle these exceptions when calling the function:

try {
    // Assume miles and gallons have been declared and initialised
    double milesPerGallon = calculateMilesPerGallon(miles, gallons);
    cout << "Miles per gallon: " << milesPerGallon << endl;
} catch (int &exception) {
    cout << "Error: Divide by zero" << endl;
} catch (std::string &exception) {
    cout << "Error: " << exception << endl;
} catch (...) {
    cout << "Unknown error occurred" << endl;
}

cout << "Buy!" << endl;

In this code snippet, we encapsulate the function call to calculateMilesPerGallon within a try block. We anticipate that the function might throw exceptions of type int or std::string, and we provide separate catch blocks to handle each exception type. Additionally, we have a catch-all handler denoted by the ellipsis (…) that can catch any type of exception.

If no exception is thrown during the function call, the code within the try block executes normally. The result is assigned to milesPerGallon, displayed, and then the statement “Buy!” is executed.

However, if an exception is thrown, the remaining code within the try block is skipped, and the program searches for an appropriate catch block to handle the thrown exception. If the exception is of type int, the corresponding catch block executes and displays the error message “Error: Divide by zero.” If the exception is of type std::string, the respective catch block executes and displays the error message provided in the exception. If none of the catch blocks match the thrown exception type, the catch-all handler executes, displaying the message “Unknown error occurred.”

By utilising multiple catch blocks and a catch-all handler, we can handle different types of exceptions that may occur within the function and provide appropriate error messages or actions for each scenario.

Stack unwinding

When an exception is thrown in a function and the function does not handle the exception itself, the function terminates and is removed from the call stack. At this point, C++ examines the call stack to identify the function that is now at the top, as it must have called the terminated function. If this calling function has a try block, the catch handlers within that block are checked to find a match for the thrown exception. If a match is found, the corresponding catch block is executed, and the program continues as demonstrated in the previous examples. However, if there is no try block in the calling function or the try block does not contain a matching catch handler, the calling function is also removed from the call stack, and the process of stack unwinding continues. Stack unwinding refers to the sequential removal of functions from the call stack in search of an appropriate exception handler.

Let’s enhance the explanation with a code snippet to illustrate stack unwinding:

#include <iostream>

void innerFunction() {
    std::cout << "Inside innerFunction" << std::endl;
    throw "Exception occurred in innerFunction";  // Throw an exception of type const char*
}

void middleFunction() {
    std::cout << "Inside middleFunction" << std::endl;
    innerFunction();
}

void outerFunction() {
    std::cout << "Inside outerFunction" << std::endl;
    try {
        middleFunction();
    } catch (int exception) {
        std::cout << "Caught exception: " << exception << std::endl;
    }
}

int main() {
    std::cout << "Inside main" << std::endl;
    try {
        outerFunction();
    } catch (const char* exception) {
        std::cout << "Caught exception: " << exception << std::endl;
    }
    std::cout << "Exiting main" << std::endl;
    return 0;
}

In this example, we have four functions: main, outerFunction, middleFunction, and innerFunction. The innerFunction throws an exception of type const char* with a message.

When the program starts executing in the main function, it calls outerFunction, which in turn calls middleFunction. Inside middleFunction, the exception is thrown in innerFunction.

Since innerFunction does not handle the exception itself, the function terminates and is removed from the call stack. C++ then examines the call stack and identifies middleFunction as the top function that called the terminated innerFunction. As middleFunction has a try block, it checks the catch handlers for a match. However, as there is no catch handler for const char* in the try block, middleFunction is also removed from the call stack.

Finally, the program control reaches the catch block in main, which matches the thrown exception type const char*. The catch block is executed, and the appropriate message is displayed.

The output of the program would be:

Inside main
Inside outerFunction
Inside middleFunction
Inside innerFunction
Caught exception: Exception occurred in innerFunction
Exiting main

As shown in the example, stack unwinding occurs when a function throws an exception that is not handled within the function itself. The process continues until an appropriate catch handler is found or the program terminates.

User defined exception classes

It is recommended to create our own exception classes to make the type of the exception explicit and specific to our application. Best practice dictates that we throw objects instead of primitive types, and it is advisable to throw by value and catch by reference or const reference. There are additional best practices that we will cover in later discussions. Now, let’s explore how to create our own exception classes.

In this example, we will create two exception classes: DivideByZeroException and NegativeValueException. These classes will represent specific exceptions for our application. We can provide constructors, attributes, and methods for these classes as we would for any other class, but for simplicity, let’s keep them basic.

class DivideByZeroException {
};

class NegativeValueException {
};

With our custom exception classes defined, let’s modify the calculateMilesPerGallon function from the previous section to throw these exception objects:

double calculateMilesPerGallon(int miles, int gallons) {
    if (gallons == 0) {
        throw DivideByZeroException();
    }

    if (miles < 0 || gallons < 0) {
        throw NegativeValueException();
    }

    return static_cast<double>(miles) / gallons;
}

In this updated code, we check if gallons are zero and throw a DivideByZeroException object if it is. Additionally, if either miles or gallons is negative, we throw a NegativeValueException object.

Now, let’s see how we can handle these exceptions when calling the calculateMilesPerGallon function:

int main() {
    try {
        double result = calculateMilesPerGallon(300, 0);
        std::cout << "Miles per gallon: " << result << std::endl;
    } catch (const DivideByZeroException& exception) {
        std::cout << "Error: Division by zero." << std::endl;
    } catch (const NegativeValueException& exception) {
        std::cout << "Error: Negative value encountered." << std::endl;
    }

    std::cout << "Program continues after exception handling." << std::endl;
    return 0;
}

In the main function, we wrap the call to calculateMilesPerGallon inside a try block. If an exception is thrown during the execution of calculateMilesPerGallon, it will be caught by the appropriate catch block. In this case, we have catch blocks for both DivideByZeroException and NegativeValueException.

By catching the exceptions using const references, we have the option to access any attributes or methods of the exception objects if we had defined them in our custom exception classes.

If none of the catch blocks match the thrown exception type, the program will terminate or continue searching for a suitable handler up the call stack.

In summary, by creating our own exception classes and throwing them, we can provide more specific and meaningful exceptions for our application. By catching these exceptions, we can handle them appropriately and continue the execution of our program.

Populating user defined exception classes for added features

When implementing a user-defined exception class in C++, you have the flexibility to include various features to enhance the functionality and information provided by the exception. Here are some things you can implement in a user-defined exception class:

Custom error message

You can provide a descriptive error message that explains the nature of the exception. This can be done by overriding the what() function inherited from the base std::exception class. Here’s the syntax to do that:

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

Output:

Exception caught: Custom error message.

Additional attributes

You can add additional attributes to the exception class to provide more specific information about the error. These attributes can be accessed in the catch block to handle the exception accordingly. Here’s the syntax to do that:

class MyException : public std::exception {
private:
    int errorCode;
public:
    MyException(int code) : errorCode(code) {}
    
    int getErrorCode() const {
        return errorCode;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>

class MyException : public std::exception {
private:
    int errorCode;
public:
    MyException(int code) : errorCode(code) {}
    
    int getErrorCode() const {
        return errorCode;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

int main() {
    try {
        throw MyException(42);
    } catch (const MyException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
        std::cout << "Error code: " << e.getErrorCode() << std::endl;
    }

    return 0;
}

Output:

Exception caught: Custom error message.
Error code: 42

Contextual information

You can include additional data or context information that helps in understanding the cause of the exception. This can be achieved by defining member variables and providing appropriate constructors to initialise them. Here’s the syntax to do that:

class MyException : public std::exception {
private:
    std::string context;
public:
    MyException(const std::string& ctx) : context(ctx) {}
    
    const std::string& getContext() const {
        return context;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>
#include <string>

class MyException : public std::exception {
private:
    std::string context;
public:
    MyException(const std::string& ctx) : context(ctx) {}
    
    const std::string& getContext() const {
        return context;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

int main() {
    try {
        throw MyException("An error occurred in the function foo()");
    } catch (const MyException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
        std::cout << "Context: " << e.getContext() << std::endl;
    }

    return 0;
}

Output:

Exception caught: Custom error message.
Context: An error occurred in the function foo()

Nested exceptions

You can include a nested exception within your user-defined exception class. This allows you to capture and propagate multiple exceptions, providing more comprehensive error handling. Here’s the syntax to do that:

class MyException : public std::exception {
private:
    std::exception_ptr nestedException;
public:
    MyException(std::exception_ptr nested) : nestedException(nested) {}
    
    std::exception_ptr getNestedException() const {
        return nestedException;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>
#include <stdexcept>

class MyException : public std::exception {
private:
    std::exception_ptr nestedException;
public:
    MyException(std::exception_ptr nested) : nestedException(nested) {}
    
    std::exception_ptr getNestedException() const {
        return nestedException;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

void innerFunction() {
    throw std::runtime_error("Inner exception");
}

void outerFunction() {
    try {
        innerFunction();
    } catch (const std::exception& e) {
        std::exception_ptr nested = std::current_exception();
        throw MyException(nested);
    }
}

int main() {
    try {
        outerFunction();
    } catch (const MyException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
        std::exception_ptr nested = e.getNestedException();
        try {
            std::rethrow_exception(nested);
        } catch (const std::exception& nestedException) {
            std::cout << "Nested exception: " << nestedException.what() << std::endl;
        }
    }

    return 0;
}

Output

Exception caught: Custom error message.
Nested exception: Inner exception

As you can see, it is possible to throw an exception from within an exception handler. This allows for the chaining or nesting of exceptions, where an outer exception captures and includes an inner exception. The above example is bit complex and here is the breakdown of the code:

  1. We define a custom exception class called MyException, derived from std::exception. This class has an additional member variable nestedException of type std::exception_ptr, which will hold the nested exception.
    1. std::exception is a base class in C++ for defining user-defined exception classes. It provides a standard interface for handling exceptions and serves as the parent class for all standard exceptions in C++.
    2. std::exception_ptr is a C++ type that represents a pointer to an exception object. It allows capturing and storing an exception for later rethrowing or inspection.
  2. The innerFunction throws a std::runtime_error exception.
  3. The outerFunction calls innerFunction within a try-catch block. If an exception is thrown, it captures the exception using std::current_exception() to obtain an std::exception_ptr to the nested exception.
    1. std::current_exception() is a C++ function that captures the currently thrown exception and returns an std::exception_ptr pointing to it.
  4. The MyException constructor takes the nested exception as a parameter and stores it in the nestedException member variable.
  5. In the main function, we call outerFunction within a try-catch block. If a MyException is thrown, we catch it and retrieve the nested exception using getNestedException().
  6. We then rethrow the nested exception using std::rethrow_exception and catch it again. In this case, the nested exception is a std::runtime_error that was thrown in the innerFunction.
    1. std::rethrow_exception is a C++ function that rethrows an exception represented by an std::exception_ptr.
    2. std::runtime_error is a C++ standard exception class that represents errors that can occur during runtime.

By using nested exceptions, we can capture more detailed information about the error and propagate it up the call stack. It allows us to handle exceptions at different levels of the program and gain insights into the origin and nature of the error.

In the provided code, the MyException class acts as a wrapper for the nested exception, providing a custom error message. By using the std::exception_ptr type, we can capture and propagate exceptions of any type, making it a flexible mechanism for error handling and reporting.

These are just a few examples of what you can implement in a user-defined exception class. The specific features and attributes depend on the requirements of the application and the information you want to convey when an exception occurs. Remember to consider best practices, such as throwing objects instead of primitive types and catching exceptions by reference or const reference to ensure proper exception handling and avoid unnecessary object slicing.

Exceptions in the context of c++ class

Exceptions can indeed be thrown from class methods, constructors, and destructors. However, it is important to follow best practices for each scenario.

Destructors should not throw exceptions. By default, destructors are marked as noexcept in C++, which means they are not expected to throw exceptions. If a destructor throws an exception while being called as a result of another exception, the original catch block will not be reached, leading to a problematic situation. It is highly discouraged to throw exceptions from destructors, unless the destructor can handle and resolve the exception internally, although such cases are rare.

For class methods, exception handling works similarly to regular functions. Exceptions can be thrown, caught, and handled appropriately using try-catch blocks.

Constructors, on the other hand, can also throw exceptions. Constructors may fail for various reasons, such as failing to allocate memory dynamically or encountering errors during initialization. Since constructors do not have return values, exceptions provide a mechanism to handle these failure scenarios. For example, in a class like Account, if the constructor detects a negative balance, it can throw a custom exception like IllegalBalanceException.

Here’s an example that demonstrates throwing an exception from a constructor:

#include <iostream>
#include <memory>

class IllegalBalanceException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Illegal balance detected";
    }
};

class Account {
private:
    double balance;

public:
    Account(double initialBalance) : balance(initialBalance) {
        if (balance < 0)
            throw IllegalBalanceException();
    }
};

int main() {
    try {
        Account moeAccount (-1000.0);
    }
    catch (const IllegalBalanceException& ex) {
        std::cout << "Exception caught: " << ex.what() << std::endl;
    }

    std::cout << "Program continues after exception handling." << std::endl;

    return 0;
}

Output:

Exception caught: Illegal balance detected
Program continues after exception handling.

In this example, the Account class’s constructor takes an initial balance as a parameter. If the provided balance is negative, the constructor throws an IllegalBalanceException. In the main function, we use a try-catch block to catch this exception. The caught exception is then displayed, and the program continues execution.

By handling exceptions in constructors, we can gracefully handle construction failures and provide appropriate error handling or feedback to the caller.

C++ standard exception class hierarchy

In C++, the standard exception class hierarchy is defined in the <stdexcept> header and provides a set of predefined exception classes that represent common error conditions. These exception classes are derived from the base class std::exception. The following diagram shows the C++ standard exception class hierarchy:

Some of the important and most frequently used classes are as follows:

  1. std::exception: This is the base class for all standard exceptions. It defines the basic interface for exceptions, including a virtual member function what() that returns a C-style string describing the exception.
  2. std::logic_error: This class is used to represent errors that are caused by logical errors in the program. It has several derived classes:
  3. std::invalid_argument: This exception is thrown when an invalid argument is passed to a function or constructor.
  4. std::domain_error: This exception is thrown when a value is outside the valid domain for a mathematical function or operation.
  5. std::length_error: This exception is thrown when a length or size exceeds the maximum allowed limit.
  6. std::out_of_range: This exception is thrown when an index or value is out of the valid range.
  7. std::runtime_error: This class is used to represent errors that occur during runtime. It has several derived classes:
  8. std::range_error: This exception is thrown when an attempt is made to store a value outside the range of the data type.
  9. std::overflow_error: This exception is thrown when an arithmetic operation results in an overflow.
  10. std::underflow_error: This exception is thrown when an arithmetic operation results in an underflow.
  11. std::system_error: This exception is used to represent errors related to the operating system or external libraries.
  12. std::bad_alloc: This exception is thrown when a dynamic memory allocation fails, typically due to insufficient memory.

These standard exception classes provide a convenient way to handle common error scenarios in a consistent and standardised manner. We can create subclasses of the exception classes and then implement the what virtual function so that it displays whatever exception message we want. This can be very useful in some situations since by being derived publicly from std::exception, our classes are now part of this hierarchy and can be used wherever a std::exception is expected since your class is an exception.

Also thanks to dynamic polymorphism. All you have to do is implement the what() virtual function and it will be bound dynamically at runtime.

By catching specific exception types, you can handle different types of errors appropriately in the code.

Here’s an example demonstrating the usage of some standard exception classes:

#include <iostream>
#include <stdexcept>

int main() {
    try {
        // Throwing an exception of type std::invalid_argument
        throw std::invalid_argument("Invalid argument!");
    } catch (const std::exception& ex) {
        std::cout << "Caught exception: " << ex.what() << std::endl;
    }
    return 0;
}

Output

Caught exception: Invalid argument!

In this example, an exception of type std::invalid_argument is thrown and caught. The what() member function is used to retrieve the error message associated with the exception, which is then printed to the console.

User-defined exception by inheriting from a standard exception class

Let’s explore an example that demonstrates the creation of a user-defined exception by inheriting from a standard exception class.

#include <iostream>
#include <exception>
#include <memory>

class IllegalBalanceException : public std::exception {
    public:
        IllegalBalanceException() = default;
        ~IllegalBalanceException() noexcept = default;
        const char* what() const noexcept override {
            return "Illegal balance exception";
        }
};

class Account {
    private:
        double balance;
    public:
        Account(double initialBalance) {
            if (initialBalance < 0)
                throw IllegalBalanceException();
            balance = initialBalance;
        }
        double getBalance() const {
            return balance;
        }
};

int main()
{
    try {
        std::unique_ptr<Account> moeAccount;
        moeAccount = std::make_unique<Account>(-100);
        std::cout << "Moe's account balance: " << moeAccount->getBalance() << std::endl;
    }
    catch (const std::exception& ex) {
        std::cout << "Exception caught: " << ex.what() << std::endl;
    }
    return 0;
}

Output:

Exception caught: Illegal balance exception

In this example, we define a user-defined exception class called IllegalBalanceException, which publicly inherits from std::exception. The class provides a default constructor and a default destructor. We also override the what() virtual function to return a C-style string description of the exception.

Next, we have the Account class that uses the IllegalBalanceException to handle the case when an account is created with a negative balance. In the constructor, if the initial balance is less than 0, we throw an IllegalBalanceException object.

In the main() function, we create a std::unique_ptr to an Account object, attempting to create Moe’s account with a negative balance of -100. Since this is an illegal balance, the Account constructor throws an IllegalBalanceException. We catch the exception in the catch block, where we can access the exception object and call its what() function to display the exception message.

Exercises

  1. Write a program that prompts the user to enter two integers and performs division. Handle the division by zero exception and display an error message if the second number is zero.
  2. Create a class called “TemperatureConverter” that converts temperatures between Celsius and Fahrenheit. Add a member function named “convertToFahrenheit” that takes a temperature in Celsius as input and converts it to Fahrenheit. Handle any invalid temperature values (e.g., below absolute zero) by throwing a custom exception. Write a program that utilises the class and catches the exception to display an error message.
  3. Write a program that reads a file name from the user and attempts to open and read the contents of the file. Handle any file-related exceptions, such as file not found or permissions error, and display an appropriate error message.
  4. Implement a stack class that supports push, pop, and top operations. Handle the exception when popping an element from an empty stack and display an error message.
  5. Write a program that asks the user to enter a positive integer and calculates its factorial. Handle the exception when a negative integer is entered and display an error message.
  6. Write a program that prompts the user to enter a positive number. If the user enters a negative number, throw an instance of std::domain_error with an appropriate error message.
  7. Create a function that takes two integers as input and divides them. Handle the case where the second number is zero by throwing an instance of std::runtime_error with a descriptive error message.
  8. Write a program that reads a file name from the user and attempts to open and read the contents of the file. Handle the case where the file does not exist by throwing an instance of std::invalid_argument with an appropriate error message.
  9. Implement a vector class that supports indexing. Handle the case where an index is out of bounds by throwing an instance of std::out_of_range with a descriptive error message.
  10. Write a program that asks the user to enter their age. If the user enters a value less than 0 or greater than 120, throw an instance of std::range_error with a suitable error message.
  11. Create a user-defined exception class named NegativeValueException that is derived from std::logic_error. This exception should be thrown when a negative value is encountered. Write a program that asks the user to enter a positive number, and if they enter a negative number, throw an instance of NegativeValueException with an appropriate error message.
  12. Implement a class named Circle that represents a circle. Include a constructor that takes a radius as a parameter. Throw an instance of std::invalid_argument with an appropriate error message if a negative radius is provided.
  13. Create a user-defined exception class named FileReadException that is derived from std::runtime_error. This exception should be thrown when a file cannot be read. Write a program that attempts to read the contents of a file specified by the user, and if the file cannot be read, throw an instance of FileReadException with a descriptive error message.
  14. Implement a class named Student that represents a student. Include a constructor that takes a student’s age as a parameter. Throw an instance of std::range_error with an appropriate error message if an age outside the valid range (18-25) is provided.
  15. Create a user-defined exception class named OverflowException that is derived from std::runtime_error. This exception should be thrown when an arithmetic operation results in an overflow. Write a program that performs arithmetic operations on large numbers and throws an instance of OverflowException if an overflow occurs.
  16. Write a program that prompts the user to enter two integers and performs division operation. Handle the following exceptions:
    1. If the user enters non-numeric input, throw a std::invalid_argument exception with an appropriate error message.
    2. If the user enters a divisor of 0, throw a std::runtime_error exception with an appropriate error message.
    3. If the result of the division is too large to fit in an int variable, throw a std::overflow_error exception with an appropriate error message.
  17. Implement a function called calculateSquareRoot that takes an integer as input and calculates its square root. Handle the following exceptions:
    1. If the input is negative, throw a std::domain_error exception with an appropriate error message.
    2. If the input is 0, throw a std::runtime_error exception with an appropriate error message.
    3. If the input is too large to fit in an int variable, throw a std::overflow_error exception with an appropriate error message.
  18. Create a class called FileReader that reads the contents of a file. Implement a member function called readFile that takes a file path as input and handles the following exceptions:
    1. If the file does not exist, throw a std::runtime_error exception with an appropriate error message.
    2. If the file cannot be opened for reading, throw a std::ios_base::failure exception with an appropriate error message.
  19. Write a program that asks the user to enter a character. Handle the following exceptions:
    1. If the user enters more than one character, throw a std::length_error exception with an appropriate error message.
    2. If the user enters a non-alphabetic character, throw a std::invalid_argument exception with an appropriate error message.
  20. Implement a function called divideNumbers that takes two integers as input and performs division operation. Handle the following exceptions:
    1. If the first number is negative, throw a std::range_error exception with an appropriate error message.
    2. If the second number is zero, throw a std::logic_error exception with an appropriate error message.
    3. If the division result is not an integer, throw a std::runtime_error exception with an appropriate error message.

Interview questions

  1. What is exception handling in C++? How does it work?
  2. What are the benefits of using exceptions in a program?
  3. Explain the difference between the try block, catch block, and throw statement in C++.
  4. How do you handle exceptions in C++? Explain the catch hierarchy and how multiple catch blocks are evaluated.
  5. What is the purpose of the std::exception class in C++? How is it used in exception handling?
  6. Can you give an example of how to create a user-defined exception in C++?
  7. Explain the concept of stack unwinding in exception handling.
  8. What are the best practices for exception handling in C++?
  9. Can you throw an exception from a destructor? Why or why not?
  10. How do you handle exceptions in constructors? Are there any special considerations?
  11. What are the differences between noexcept, throw(), and noexcept() in C++?
  12. How do you handle exceptions that are not caught by any catch block?
  13. What are the standard exception classes provided by C++? Can you give examples of when you might use them?
  14. Explain the concept of object slicing in the context of exception handling.
  15. Can you explain how to use the try-catch construct in C++?
Categories
Computer Science / Information Technology Language: C++

Smart Pointers

Overview

Raw pointers are a powerful tool in C++. They provide us with absolute flexibility with memory management. With this flexibility comes complexity. We must explicitly allocate and deallocate storage for heap dynamic variables as well as manage their lifetimes. And that’s where we often see problems. Infact, most defects in programs written in programming languages that provide raw pointers are pointer-related defects.

Some of the issues with raw pointers include:

  • Uninitialised (wild pointers): Pointers can point anywhere in memory. If such pointers are dereferenced, it can have catastrophic consequences.
  • Memory leaks: If a raw pointer is not properly deleted, it can lead to a memory leak. Memory leaks can cause programs to run out of memory and crash.
  • Dangling pointers: A dangling pointer is a pointer that points to an object that has been deleted. Dangling pointers can cause undefined behaviour, which can lead to crashes or other unexpected results.
  • Null pointers: A null pointer is a pointer that does not point to any object. Trying to dereference a null pointer can cause a crash.
  • Not exception safe: Our code might throw an exception and the code that releases our allocated memory may never execute leading to memory leak.

Smart pointers can help us prevent all of these types of errors. They can help us be more clear about who owns the pointer, and when a pointer should be deleted to free up allocated memory while being intuitive to use.

Smart pointers are a type of pointer that automatically manages the lifetime of the object it points to. This means that the smart pointer will automatically delete the object when it is no longer needed, preventing memory leaks. There are a few different types of smart pointers in C++, some of the most common are:

  • std::unique_ptr: A smart pointer that owns the object it points to. Only one std::unique_ptr can own an object at a time.
  • std::shared_ptr: A smart pointer that shares ownership of the object it points to. Multiple std::shared_ptrs can own the same object.
  • std::weak_ptr: A smart pointer that does not own the object it points to. A std::weak_ptr can only be used to check if the object it points to is still alive.

The best type of smart pointer to use depends on the specific situation. In general, std::unique_ptr should be used when an object should only be owned by one thing, std::shared_ptr should be used when an object should be shared by multiple things, and std::weak_ptr should be used when an object does not need to be owned. We shall see each of these in detail in upcoming sections.

Here are some of the disadvantages of using smart pointers:

  • Can be more complex than raw pointers: Smart pointers can be more complex to use than raw pointers, especially when multiple smart pointers are used to point to the same object.
  • Can be slower than raw pointers: Smart pointers can be slower than raw pointers, especially when they are used to share ownership of an object.
  • Cannot do pointer arithmetic.

Overall, smart pointers are a powerful tool that can help to improve the safety and reliability of C++ programs. However, it is important to be aware of the potential disadvantages of using smart pointers before using them in the code.

RAII: Resource Acquisition Is Initialisation

RAII stands for Resource Acquisition is Initialization. It is a programming idiom that can be used to automatically manage resources in C++. RAII works by associating a resource with a scope. When the scope exits, the resource is automatically released. This ensures that resources are always released correctly, even if there are errors in the program.

There are a few different ways to implement RAII in C++. One common way is to use smart pointers. Smart pointers are objects that automatically manage the lifetime of the resources they point to. When a smart pointer goes out of scope, it will automatically delete the resource it points to.

Another way to implement RAII is to use RAII classes. RAII classes are classes that encapsulate a resource, usually allocate the necessary resources in a constructor and automatically release it when the class goes out of scope. This usually happens in the class destructor. For example, the following class encapsulates a file handle:

class File {
public:
  File(const char* filename) : handle(fopen(filename, "r")) {}
  ~File() {
    if (handle) {
      fclose(handle);
    }
  }

private:
  FILE* handle;
};

This class can be used to open a file and automatically close it when the class goes out of scope. For example:

{
  File file("myfile.txt");
  // Do something with the file
}

When the File object goes out of scope, the fclose() function will be called to close the file. This ensures that the file is always closed correctly, even if there are errors in the program.

RAII is a powerful tool that can help to improve the safety and reliability of C++ programs. By using RAII, we can avoid memory leaks and other resource management problems.

Unique pointer

A unique pointer is an efficient and the fastest smart pointer (however, slower than the raw pointers). It owns the object it points to. Only one unique pointer can own an object at a time. When the unique pointer goes out of scope, the object it points to will be automatically deleted. The syntax to create a unique pointer is as follows:

std::unique_ptr<MyClass> my_ptr(new MyClass());

Here,

  • MyClass is the type of object that the unique pointer will point to.
  • new MyClass() creates a new MyClass object and returns a pointer to it.
  • The unique pointer my_ptr is then initialised with this pointer.

Once a unique pointer is created, it can be used to access the object it points to just like a raw pointer. For example:

// Access the object
my_ptr->some_method();

// Get the value of a member variable
int value = my_ptr->some_member_variable;

When the unique pointer goes out of scope, the object it points to will be automatically deleted. This means that you don’t have to worry about memory leaks.

Here is an example of how to use a unique pointer:

#include <iostream>
#include <memory>

class MyClass {
public:
  void some_method() {
    std::cout << "Hello, world!" << std::endl;
  }
};

int main() {
  std::unique_ptr<MyClass> my_ptr(new MyClass());
  my_ptr->some_method();
  return 0;
}

Output:

Hello, world!

The my_ptr unique pointer will automatically delete the MyClass object when it goes out of scope at the end of main(). This ensures that the MyClass object is properly cleaned up.

The std::make_unique()

std::make_unique is a C++14 library function template that creates a std::unique_ptr object and initialises it with a dynamically allocated object of the specified type. It is a convenient way to allocate and initialise an object using smart pointers.

The std::make_unique function simplifies the process of creating a std::unique_ptr by automatically allocating memory and constructing the object in a single step. It ensures exception safety by guaranteeing that the allocated memory will be properly deallocated if an exception occurs during object construction.

Here’s an example that demonstrates the usage of std::make_unique:

#include <memory>

class MyClass {
public:
    MyClass() {
        // Constructor logic
    }
};

int main() {
    // Using make_unique to create a unique_ptr object
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();

    // Accessing members of the dynamically allocated object
    ptr->someMethod();

    return 0;
}

In the above example, std::make_unique is used to create a std::unique_ptr object named ptr that points to a dynamically allocated MyClass object. The std::make_unique function automatically handles memory allocation and construction of the object. The ptr object can then be used to access members and methods of the allocated object.

Using std::make_unique instead of directly calling new and constructing the object separately provides several benefits, such as improved code readability, exception safety, and prevention of memory leaks. It is generally considered a good practice to use std::make_unique whenever possible to manage dynamically allocated objects with std::unique_ptr.

Shared pointer

A shared pointer is a smart pointer that shares ownership of the object it points to. Multiple shared pointers can own the same object. When the last shared pointer that owns an object goes out of scope, the object will be automatically deleted.

The syntax to create a shared pointer is as follows:

std::shared_ptr<MyClass> my_ptr(new MyClass());

Here, MyClass is the type of object that the shared pointer will point to. new MyClass() creates a new MyClass object and returns a pointer to it. The shared pointer my_ptr is then initialised with this pointer.

Once a shared pointer is created, it can be used to access the object it points to just like a raw pointer. For example:

// Access the object
my_ptr->some_method();

// Get the value of a member variable
int value = my_ptr->some_member_variable;

When the last shared pointer that owns an object goes out of scope, the object will be automatically deleted. This means that you don’t have to worry about memory leaks.

Here is an example of how to use a shared pointer:

#include <iostream>
#include <memory>

class MyClass {
public:
  void some_method() {
    std::cout << "Hello, world!" << std::endl;
  }
};

int main() {
  std::shared_ptr<MyClass> my_ptr(new MyClass());
  my_ptr->some_method();
  return 0;
}

Output:

Hello, world!

The my_ptr shared pointer will automatically delete the MyClass object when it goes out of scope at the end of main(). This ensures that the MyClass object is properly cleaned up. An added advantage of shared pointer is that it allows Multiple ownership: Multiple shared pointers can own the same object. This can be useful for objects that need to be shared between different parts of a program.

Weak pointer

A  weak pointer does not own the object it points to. A weak pointer can only be used to check if the object it points to is still alive. When the object that a weak pointer points to is deleted, the weak pointer will become invalid and will not be able to access the object.

The syntax to create a weak pointer is as follows:

std::weak_ptr<MyClass> my_ptr(new MyClass());

Here, MyClass is the type of object that the weak pointer will point to. new MyClass() creates a new MyClass object and returns a pointer to it. The weak pointer my_ptr is then initialised with this pointer.

Once a weak pointer has been created, it can be used to check if the object it points to is still alive using the expired() function. For example:

if (!my_ptr.expired()) {
  // The object is still alive.
} else {
  // The object has been deleted.
}

A weak pointer can be used to obtain a shared pointer to the object it points to if the object is still alive. For example:

std::shared_ptr<MyClass> my_shared_ptr = my_ptr.lock();

If the object is no longer alive, then my_shared_ptr will be a null pointer. Here is an example of how to use a weak pointer:

#include <iostream>
#include <memory>

class MyClass {
public:
  ~MyClass() {
    std::cout << "Object deleted." << std::endl;
  }
};

int main() {
  std::weak_ptr<MyClass> my_ptr(new MyClass());
  if (!my_ptr.expired()) {
    // The object is still alive.
  } else {
    // The object has been deleted.
  }

  std::shared_ptr<MyClass> my_shared_ptr = my_ptr.lock();
  if (my_shared_ptr) {
    // The object is still alive.
  } else {
    // The object has been deleted.
  }

  return 0;
}

Output:

Object deleted.

The my_ptr weak pointer will become invalid when the MyClass object is deleted. This will cause the if (!my_ptr.expired()) condition to evaluate to false. The my_shared_ptr shared pointer will also become invalid when the MyClass object is deleted. This will cause the if (my_shared_ptr) condition to evaluate to false.

Here are some of the advantages of using weak pointers:

  • Can be used to check if an object is still alive: Weak pointers can be used to check if an object is still alive. This can be useful for things like garbage collection and deferred cleanup.
  • Do not prevent objects from being deleted: Weak pointers do not prevent objects from being deleted. This means that they can be used to safely share ownership of an object with other objects.
  • Can be used to obtain a shared pointer to an object: Weak pointers can be used to obtain a shared pointer to an object if the object is still alive. This can be useful for things like accessing the object’s members or calling its methods.

Common operations

Here are some of the common operations that can be performed on smart pointers:

Move assignment

The move assignment operator can be used to move the ownership of an object from one smart pointer to another. For example:

std::unique_ptr<MyClass> my_new_ptr = std::move(my_old_ptr);

This will move the ownership of the object pointed to by my_old_ptr to my_new_ptr. my_old_ptr will no longer point to any object.

Reset

The reset() function can be used to reset a smart pointer to point to a new object. For example:

my_ptr.reset(new MyClass());

This will reset my_ptr to point to a new MyClass object. The old object that my_ptr was pointing to will be deleted.

Get

The get() function can be used to get the raw pointer that is encapsulated by a smart pointer. For example:

MyClass* raw_ptr = my_ptr.get();

This will return the raw pointer that my_ptr is pointing to. It is important to note that you should not delete the object that the raw pointer points to. This will be done automatically by the smart pointer when it goes out of scope.

Release

The release() function can be used to release ownership of an object from a smart pointer. For example:

MyClass* raw_ptr = my_ptr.release();

This will release ownership of the object that my_ptr is pointing to. The smart pointer will no longer point to any object. It is important to note that you are responsible for deleting the object that raw_ptr points to.

Custom deleter

A custom deleter is a function or a callable object that is used to release the memory allocated to an object managed by a smart pointer when the pointer goes out of scope. It is an optional parameter that can be passed to the constructor of the smart pointer. The custom deleter is executed instead of the default destructor of the smart pointer when the pointer is destroyed.

The syntax of custom deleter is as follows:

std::shared_ptr<T> ptr(new T, Deleter);

where T is the type of the object that the smart pointer is pointing to, and Deleter is a function object or a function pointer that is used to delete the object when it is no longer needed.

The syntax for defining a custom deleter function is:

void customDeleter(T* ptr) {
    // Custom deletion code here
}

Here, T is the type of the object that the custom deleter function will delete. The custom deletion code can be anything that is appropriate for the object being deleted, such as calling a member function, deallocating dynamic memory, or doing nothing at all.

To use the custom deleter function with a smart pointer, you pass it as the second argument to the std::shared_ptr constructor:

std::shared_ptr<T> ptr(new T, customDeleter);

This creates a std::shared_ptr object that owns the dynamically allocated object of type T, and uses the customDeleter function to delete it when the std::shared_ptr object goes out of scope or is reset.

Here is an example of creating a custom deleter for a smart pointer:

#include <iostream>
#include <memory>

class Person {
public:
    Person(const std::string& name) : name_(name) {
        std::cout << name_ << " created." << std::endl;
    }
    ~Person() {
        std::cout << name_ << " destroyed." << std::endl;
    }
private:
    std::string name_;
};

void custom_deleter(Person* p) {
    std::cout << "Custom deleter called for " << p << std::endl;
    delete p;
}

int main() {
    std::shared_ptr<Person> p1(new Person("Alice"), custom_deleter);
    std::unique_ptr<Person, decltype(&custom_deleter)> p2(new Person("Bob"), &custom_deleter);
    return 0;
}

Output:

Bob destroyed.
Custom deleter called for 0x55f996f4aeb0
Alice destroyed.

In this example, we define a Person class with a custom constructor and destructor that prints a message when the object is created or destroyed. We also define a custom_deleter function that takes a Person pointer as its argument and deletes it.

In main(), we create two smart pointers p1 and p2, one std::shared_ptr and one std::unique_ptr. We pass custom_deleter as the second argument to the std::shared_ptr constructor and as the third argument to the std::unique_ptr constructor.

When the program runs and the main() function returns, both smart pointers will go out of scope and the custom deleter will be called for each one, instead of the default destructor. The custom_deleter function will print a message and delete the Person object that the smart pointer is managing.

Circular referencing

Circular reference with shared pointers occurs when two or more objects hold shared pointers to each other, creating a cycle that prevents the memory from being released properly. This leads to memory leaks and can cause the program to crash if the memory usage becomes too high.

Here’s an example:

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

In this example, objects of class A and B hold shared pointers to each other, creating a circular reference.

To break this cycle, we can use weak_ptr instead of shared_ptr for one of the objects. Since weak_ptr doesn’t increment the reference count, it doesn’t prevent the object from being deleted when no other shared_ptr references exist.

Here’s the modified example:

class A;

class B {
public:
    std::shared_ptr<A> a_ptr;
};

class A {
public:
    std::weak_ptr<B> b_ptr;
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);

    a_ptr->b_ptr = b_ptr;
    b_ptr->a_ptr = a_ptr;

    return 0;
}

In this modified example, class A holds a weak_ptr to class B, which breaks the circular reference and allows the memory to be released properly when the last shared_ptr reference is destroyed.

Exercises

  1. Write a C++ program using smart pointers that creates an integer variable dynamically using new keyword, assign the value to it, print the value using smart pointer.
  2. Write a C++ program using smart pointers that creates a dynamic array of size 5, assigns values to each element of the array, prints each element using smart pointer.
  3. Write a C++ program using smart pointers that creates two integer variables dynamically using new keyword, assigns values to them, adds the values of the variables and prints the result using smart pointer.
  4. Write a C++ program using smart pointers that creates a class named “Rectangle” with member variables “length” and “width”. Create a dynamic object of the class using smart pointer, assign values to the member variables and print them using smart pointer.
  5. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and calculates its area. Use smart pointer functions get(), reset(), and release() to manipulate the object.
  6. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and uses smart pointers to copy its value to a new object. Then use the smart pointer functions to release the old object.
  7. Write a C++ program using smart pointers that creates a dynamic array of size 10 using new keyword, assigns values to each element of the array, prints each element using smart pointer. Then use smart pointer to reset the array to size 5 and print the elements using smart pointer.
  8. Write a C++ program using smart pointers that creates two dynamic objects of class “Rectangle”, assigns values to their member variables, adds their areas and prints the result using smart pointer.
  9. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and calculates its perimeter. Use smart pointer function reset() to update the member variables and calculate the new perimeter. Print both the original and updated perimeters using a smart pointer.
  10. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and calculates its diagonal. Use smart pointer function release() to pass the object to a function that calculates the diagonal and returns it. Print the diagonal using a smart pointer.

Interview questions

  1. What is a smart pointer?
  2. What is the difference between a unique pointer and a shared pointer?
  3. How does a unique pointer ensure that a dynamically allocated object is deleted?
  4. How does a shared pointer ensure that a dynamically allocated object is deleted?
  5. What is a weak pointer?
  6. What is the difference between a weak pointer and a shared pointer?
  7. Can you have a circular reference with shared pointers? If so, how can you break the cycle?
  8. What is the purpose of the get() function in a smart pointer?
  9. What is the purpose of the reset() function in a smart pointer?
  10. What is the purpose of the release() function in a smart pointer?
  11. How can you create a custom deleter for a smart pointer?
  12. What is the purpose of a deleter in a smart pointer?
  13. Can you use a raw pointer as an argument to a smart pointer constructor? If so, why might you do this?
  14. What are some advantages of using smart pointers instead of raw pointers?
  15. What are some disadvantages of using smart pointers?
Categories
Computer Science / Information Technology Language: C++

Polymorphism

Overview

In this section, we will explore how polymorphism and inheritance can empower us to develop reusable and adaptable programs. Our journey begins with an examination of the various types of polymorphism available in C++. Specifically, we will focus on dynamic polymorphism.

Next, we will delve into the utilisation of base class pointers, which will elevate our class hierarchies to new heights. By employing this technique, we can embrace abstract thinking and alleviate concerns over intricate details.

Throughout this course section, we will encounter examples that illustrate both static and dynamic binding of function calls. By analysing their advantages and disadvantages, we will gain a comprehensive understanding of each approach.

Subsequently, we will leverage virtual functions to establish polymorphic functions, allowing for runtime binding. Additionally, we will explore the significance of virtual destructors and utilise the C++11 override and final specifiers.

To further harness dynamic polymorphism, we will explore the use of base class references. This technique offers additional flexibility and adaptability in our programs.

Finally, we will uncover the concepts of pure virtual functions and abstract classes, highlighting their importance in both standalone implementations and interfaces.

Introduction to polymorphism

Polymorphism, in simple terms, refers to the ability of an object to take on many forms. It allows you to treat different objects in a similar way, even if they belong to different classes. Think of polymorphism as a way of interacting with objects without knowing their specific types. You can perform actions on objects based on their common characteristics or behaviours, rather than their specific implementations.

For example, imagine you have different shapes, such as a square, a circle, and a triangle. Each shape has its own unique properties, but they all share a common characteristic of being able to calculate their area. Polymorphism allows you to write a piece of code that can calculate the area of any shape, regardless of its specific type. You can treat each shape object as a “shape” and call a common function like calculateArea(), which is implemented differently in each shape class.

By leveraging polymorphism, you can write generic code that works with a variety of objects without explicitly knowing their individual details. This flexibility makes the code more adaptable, maintainable, and reusable.

Types of polymorphism

There are two main types of polymorphism: compile-time polymorphism and runtime polymorphism.

Compile-time Polymorphism

Compile-time polymorphism (also known as static binding or early binding polymorphism) is determined during the compilation phase of a program. It involves the use of function overloading and operator overloading (discussed in earlier sections). The specific function or operator to be executed is resolved by the compiler based on the arguments provided at compile-time.

Runtime Polymorphism

Runtime polymorphism (also known as dynamic binding/late binding polymorphism) is determined during the runtime or execution phase of a program. It involves the use of inheritance and virtual functions. As discussed in the previous section, Inheritance allows a derived class to inherit properties and behaviours from a base class.

Virtual functions are functions declared in a base class and overridden in derived classes. When a base class pointer or reference is used to refer to an object of a derived class, the virtual function call is resolved at runtime, and the appropriate derived class implementation is executed. This allows different objects of derived classes to be treated as objects of the base class, and the appropriate functions are called dynamically based on the actual object type.

We shall delve into virtual function in greater detail in the upcoming sections.

Contrast between compile time and runtime polymorphism

First, let’s examine a non-polymorphic example that utilises static binding. In the scenario, we have an account hierarchy implemented using public inheritance, as depicted in the following figure.

Each account class has its own unique version of the withdraw method, which varies depending on the account type. We’ll create four objects, one for each account type, and invoke their respective withdraw methods.

Account a;
a.withdraw(1000); // Account::withdraw()

Savings b;
b.withdraw(1000); // Savings::withdraw()

Checking c;
c.withdraw(1000); // Checking::withdraw()

Trust d;
d.withdraw(1000); // Trust::withdraw()

When we invoke the withdraw method of object ‘a’, it calls the withdraw method of the account class. This is logical since ‘a’ is an account object. The compiler is aware of this and binds the method call during compile-time or statically. The same principle applies to objects ‘b’, ‘c’, and ‘d’. In each case, the compiler binds the withdraw calls based on the object information provided when they were declared in the source code. Thus, ‘b’ invokes the withdraw method of the savings class, ‘c’ invokes the withdraw method of the checking class, and ‘d’ invokes the withdraw method of the trust class. So far, everything aligns with our expectations. Now consider the following snippet:

Account *p = new Trust();
p->withdraw(1000);

p is a pointer to an account object, holding the address of an account. Now, we dynamically create a trust account object on the heap and assign its address to p. Is this permissible? Indeed, it is. p can hold addresses of accounts, and trust is an account according to the inheritance hierarchy.

But what happens when we invoke the withdraw method on the object pointed to by p? Since static binding is the default behaviour, the compiler doesn’t possess knowledge of the account type that p is pointing to during runtime. It simply doesn’t concern itself with it. All it knows is that p is pointing to an account. As a result, it calls the withdraw method of the account class. This outcome is likely not what we expected or desired since we want the trust object on the heap to employ its own version of the withdraw method. All the compiler knows is that p is a pointer to an account so it will bind the withdraw method to the account class’s withdraw method at compile time.

Let’s consider another example where we have the same class hierarchy as before. Let’s assume that each account class possesses its own implementation of a display method, which is responsible for showcasing account information based on the account type.

To illustrate this, we can create a simple C++ function called displayAccount. This function expects a reference to an account object as input. Since all the derived classes are considered accounts, we can pass any of them into this function. Consequently, the function will invoke the appropriate display method based on the account object provided.

Account a;
displayAccount(a);

Savings b;
displayAccount(b);

Checking c;
displayAccount(c);

Trust d;
displayAccount(d);

void displayAccount(const Account &acc) {
    acc.display();
    // will always use Account::display
}

The code snippet demonstrates the creation of four objects: a, b, c, and d, each representing a different account type. These objects are then passed to the displayAccount function. However, the behaviour observed from the displayAccount function may not align with initial expectations.

By default, C++ adheres to static binding, which means that when the compiler encounters the call to acc.display within the displayAccount function, it binds the call to the display method of the account class. As a result, the account’s display method will be invoked regardless of the object passed in. Consequently, the display will only showcase the contents of the account section.

It is possible for C++ to inquire about the type of account object being passed in by asking, “Hey, what kind of account are you?” and based on the response, if-else statements could be employed to invoke the appropriate display methods. However, relying on such conditional statements is considered poor coding practice as it hampers program abstraction. In such cases, we would need to determine the object’s type explicitly and call its functions accordingly.

Using a Base Class Pointer

We have already learned that in C++, dynamic binding of method calls requires an inheritance hierarchy, the use of base class pointers or references, and the declaration of the desired methods as virtual. In this section, we will focus on the importance and power of using base class pointers. Let’s consider the following account class hierarchy:

First, let’s assume that this class hierarchy now utilises dynamic polymorphism. We will explore how to achieve this later in the section. For now, let’s concentrate on its effects and the advantages it offers. We will create four pointers to account objects and initialise each one with a different type of account created on the heap.

Account* p1 = new Account();
Account* p2 = new SavingsAccount();
Account* p3 = new CheckingAccount();
Account* p4 = new TrustAccount();

This is valid because all these objects are accounts due to the “is-a” relationship established through public inheritance. Next, using these base class pointers, we can call the withdraw method, and C++ will determine which method to bind at runtime based on the type of the object being pointed to by each pointer.

p1->withdraw(1000);   // Account::withdraw
p2->withdraw(1000);   // SavingsAccount::withdraw
p3->withdraw(1000);   // CheckingAccount::withdraw
p4->withdraw(1000);   // TrustAccount::withdraw

// delete the pointers

This dynamic binding of method calls is incredibly powerful. Once the hierarchy is set up, we don’t need to do anything else. However, it’s essential to deallocate the memory allocated by the pointers when we are finished using them.

Now, let’s explore a more compelling use case for base class pointers. In the following example, we have the same four pointers as before, but this time, I’ve declared an array that holds pointers to account objects.

Account* accountArray[] = { p1, p2, p3, p4 };

Here, the elements of the array are base class pointers. We can loop through the array and call the withdraw method for each element. The correct withdraw method will be called based on the type of the object each pointer points to.

for (int i = 0; i < 4; ++i) {
    accountArray[i]->withdraw();
}

This demonstrates the power of using base class pointers. It doesn’t matter how many pointers we initialise the array with or what types of accounts they point to; it will work as expected. Even if we replace an array element with another, it will still work correctly. This is programming more abstractly or more generally. We can simply think of calling the withdraw method for each account in the array without worrying about the specific details. The same concept applies to other collections such as vectors.

std::vector<Account*> accountVector { p1, p2, p3, p4 };

for (auto account : accountVector) {
    account->withdraw();
}

Consider what would happen if we added another class to our account hierarchy, like a bond account. None of the existing code that works with account objects would need to be modified. Since a bond account is an account, it will seamlessly work with our existing code, allowing us to leverage the benefits of polymorphism and abstraction.

Virtual functions

Let’s delve into the concept of virtual functions and the usage of the virtual keyword to achieve dynamic binding in class hierarchies. When we derive a class from a base class, we have the ability to redefine the behaviour of base class functions in the derived class, creating specialised versions of those functions. By default, these overridden functions are statically bound at compile time. However, by using the virtual keyword, we can make them dynamically bound, enabling runtime binding based on the actual type of the object.

To declare a function as virtual, we use the virtual keyword in the base class. Let’s take a look at the syntax. In the code snippet below, we have the account base class, and we declare the withdraw method as virtual:

class Account {
public:
    virtual void withdraw(double amount) {
        // Base implementation
    }
};

By marking the withdraw function as virtual, we indicate that it can be overridden in derived classes. This enables us to treat objects in the class hierarchy abstractly, using base class pointers or references. Once a function is declared as virtual in the base class, it remains virtual throughout the derived classes.

Let’s consider an example where we override the withdraw function in a derived class called Checking:

class Checking : public Account {
public:
    void withdraw(double amount) {
        // Derived class implementation
    }
};

In this example, the withdraw function in the Checking class is implicitly virtual, thanks to the virtual declaration in the base class. However, it’s good practice to explicitly use the override keyword to make it clear that we are overriding a base class function. This makes the compiler throw an error if there is any deviation in the function signatures of the parent class virtual function from the derived class virtual function. The syntax to do so is as follows:

class Checking : public Account {
public:
    void withdraw(double amount) override {
        // Derived class implementation
    }
};

It’s important to note that when overriding a base class function, the function signature and return type must match exactly. Any deviation will be treated as a redefinition rather than an override, resulting in static binding.

Remember, virtual functions are dynamically bound only when they are called through a base class pointer or reference. If called directly through an object, they are statically bound.

Let’s consider an elaborate example using the account class hierarchy to demonstrate dynamic polymorphism, base class pointers, and the power of abstraction. We will create a banking system that manages different types of accounts: regular accounts, savings accounts, checking accounts, and trust accounts.

First, let’s define the base class Account:

#include <iostream>

class Account {
protected:
    double balance;

public:
    Account(double balance = 0.0) : balance(balance) {}

    virtual void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            std::cout << "Withdrew $" << amount << " from Account. New balance: $" << balance << std::endl;
        } else {
            std::cout << "Insufficient funds in Account." << std::endl;
        }
    }
};

Next, we’ll create derived classes: SavingsAccount, CheckingAccount, and TrustAccount. Each of these classes will provide its own implementation of the withdraw method:

class SavingsAccount : public Account {
public:
    SavingsAccount(double balance = 0.0) : Account(balance) {}

    void withdraw(double amount) override {
        // Additional logic specific to SavingsAccount
        std::cout << "Withdrawing $" << amount << " from Savings Account." << std::endl;
        Account::withdraw(amount);
    }
};

class CheckingAccount : public Account {
public:
    CheckingAccount(double balance = 0.0) : Account(balance) {}

    void withdraw(double amount) override {
        // Additional logic specific to CheckingAccount
        std::cout << "Withdrawing $" << amount << " from Checking Account." << std::endl;
        Account::withdraw(amount);
    }
};

class TrustAccount : public Account {
public:
    TrustAccount(double balance = 0.0) : Account(balance) {}

    void withdraw(double amount) override {
        // Additional logic specific to TrustAccount
        std::cout << "Withdrawing $" << amount << " from Trust Account." << std::endl;
        Account::withdraw(amount);
    }
};

Now, let’s simulate the banking system. We’ll create instances of different account types and demonstrate dynamic polymorphism by using base class pointers:

int main() {
    // Create account objects
    Account* account = new Account(1000.0);
    Account* savingsAccount = new SavingsAccount(2000.0);
    Account* checkingAccount = new CheckingAccount(1500.0);
    Account* trustAccount = new TrustAccount(5000.0);

    // Call withdraw method using base class pointers
    account->withdraw(200.0);
    savingsAccount->withdraw(500.0);
    checkingAccount->withdraw(1000.0);
    trustAccount->withdraw(3000.0);

    // Clean up memory
    delete account;
    delete savingsAccount;
    delete checkingAccount;
    delete trustAccount;

    return 0;
}

When we run this program, we’ll see the appropriate logging messages based on the type of account and the amount being withdrawn. The dynamic binding will ensure that the correct version of the withdraw method is called for each account type.

Output:

Withdrew $200 from Account. New balance: $800
Withdrawing $500 from Savings Account.
Withdrew $500 from Account. New balance: $1500
Withdrawing $1000 from Checking Account.
Insufficient funds in Account.
Withdrawing $3000 from Trust Account.
Withdrew $3000 from Account. New balance: $2000

This example demonstrates the use of dynamic polymorphism, base class pointers, and the power of abstraction. We can handle different account types through a common base class interface, allowing us to write code that works with accounts in a general and abstract manner, without needing to know the specific account types involved.

Virtual destructors

Virtual destructors are an essential aspect when working with polymorphic objects and inheritance hierarchies in C++. In the previous example, you might have encountered warning messages during compilation regarding the absence of a virtual destructor in our simple account class hierarchy.

warning: deleting object of polymorphic class type 'Account' which has non-virtual destructor might cause undefined behaviour

These warnings indicate that deleting a polymorphic object without a virtual destructor can lead to unpredictable behaviour. According to the C++ standard, if a derived class object is destroyed through a base class pointer and the base class does not have a virtual destructor, the behaviour is undefined.

To address these warnings and ensure proper destruction of derived objects, we need to provide a virtual destructor in the base class. This ensures that the destructors of both the base and derived classes are called in the correct order. Declaring a destructor as virtual is straightforward and intuitive. Once the base class has a virtual destructor, all derived classes automatically have virtual destructors as well, without the need to explicitly declare them as virtual (although it’s considered best practice to do so).

Here’s an example of the code snippet for the account base class, showcasing the addition of a virtual destructor:

class Account {
public:
  // Other member functions

  virtual ~Account() {
    // Destructor implementation
  }
};

In the code snippet above, we have added the virtual keyword before the destructor declaration in the base class. This ensures that when an object of a derived class is deleted through a base class pointer, the appropriate destructor for the derived class will be called, followed by the destructor of the base class.

Contrast between overriding virtual functions and overriding normal functions

Inheritance and Polymorphism:

  • Virtual functions are used in the context of inheritance and polymorphism, where a derived class can override the implementation of a virtual function inherited from a base class. This enables objects of different derived classes to be treated uniformly as objects of the base class, while still invoking their specific implementations.
  • Normal functions are not involved in inheritance and polymorphism. They are standalone functions defined in a class or globally, and their behaviour remains the same regardless of any derived classes.

Function Signature:

  • When overriding a virtual function, the derived class must have the exact same function signature (i.e., return type, name, and parameters) as the base class’s virtual function. If the function signatures don’t match, it will be considered a different function and won’t override the base class function.
  • Normal functions can have different function signatures, as they are not intended to be overridden. It is possible to have multiple functions with the same name but different parameters within a class.

Binding:

  • Virtual functions use dynamic binding, also known as late binding or runtime polymorphism. The appropriate function to be executed is determined at runtime based on the actual type of the object.
  • Normal functions use static binding, also known as early binding or compile-time polymorphism. The function to be executed is determined at compile-time based on the declared type of the object or the function call.

Keyword Usage:

  • Virtual functions are declared using the virtual keyword in the base class. This keyword indicates that the function can be overridden by derived classes.
  • Normal functions are not explicitly declared with any special keyword. They are defined as regular member functions or global functions without any specific indication of being overridden.

Base Class Function Execution:

  • When a virtual function is overridden in a derived class and called through a base class pointer or reference, the overridden implementation in the derived class is executed.
  • When calling a normal function, the implementation in the class where the function is defined is executed, irrespective of the derived classes.

The C++11 final specifier

The C++11 final specifier provides us with the ability to prevent derivation and subclassing of classes, as well as overriding methods in derived classes. This specifier can be used in two different contexts.

Context of classes

When used at the class level, the final specifier prevents a class from being derived from or subclassed. This is useful in scenarios where we want to create final classes that should not be extended. It can be done to improve compiler optimization or ensure object copying safety without the risk of object slicing. The syntax for using the final specifier at the class level is straightforward. We simply add the final specifier after the class name during declaration. For example:

class MyClass final {
    // Class definition
};

class YoutClass final {
    // Class definition
};

In the above examples, both MyClass and YourClass classes are marked as final, indicating that they cannot be derived from. If an attempt is made to derive a class from these final classes, the compiler will generate an error, preventing subclassing.

Context of methods

Additionally, the final specifier can be used at the method or function level to prevent a method from being overridden in derived classes. This can be done to improve compiler optimization or to enforce a specific behaviour that should not be modified further down in the inheritance hierarchy. Let’s consider a simple class hierarchy with classes A, B, and C. Class A declares the virtual function doSomething(), and class B, derived from A, overrides it and marks it as final. Here’s an example:

class A {
public:
    virtual void doSomething() {
        // Base class implementation
    }
};

class B : public A {
public:
    void doSomething() final {
        // Derived class implementation, marked as final
    }
};

class C : public B {
public:
    // Error: Attempting to override a final function
    void doSomething() {
        // Derived class implementation
    }
};

In this example, the doSomething() function in class B is overridden and marked as final, indicating that it cannot be further overridden in any derived classes. If class C attempts to override doSomething(), the compiler will generate an error, indicating that the function is final and cannot be overridden.

Categories
Computer Science / Information Technology Language: C++

Inheritance

Overview

Inheritance is a powerful feature of object-oriented programming that allows you to reuse code from existing classes. In C++, inheritance is used to create new classes (derived classes) from existing classes (base classes). The derived class inherits all the properties and methods of the base class, and can also add its own properties and methods.

For example, let’s say you have a class called Animal that has properties like name, age, and species, and methods like eat() and sleep(). You could then create a derived class called Dog that inherits all the properties and methods of Animal, and adds its own properties and methods, like breed and colour.

Inheritance is a powerful tool that can help us to write more efficient and maintainable code. It can also help us to organise our code in a more logical way.

Here are some of the benefits of using inheritance in C++:

  • Code reuse: Inheritance allows you to reuse code from existing classes, which can save you time and effort.
  • Code organisation: Inheritance can help you to organise code in a more logical way, making it easier to understand and maintain.
  • Flexibility: Inheritance gives you the flexibility to customise classes to meet specific needs.
  • Reusability: Inheritance makes it easy to create new classes from existing classes, which can save you time and effort when developing new applications.

Inheritance is a powerful feature of C++ that can be used to improve the quality, efficiency, and maintainability of code. Inheritance can also be thought of as a way of grouping classes together based on their common attributes. A base class can be created that contains all of the common attributes, and then derived classes can be created that inherit from the base class. This allows us to reuse code and keep our code organised.

In the following exercise, identify which class could be the base class, what could be the attributes of the base class and the derived classes:

  1. Line, Oval, Shape, Circle, Square.
  2. Savings Account, Account, Trust Account, Checking Account, Current Account.
  3. Employee, Student, Faculty, Staff, Administrator, Person.
  4. Level Boss, Hero, Super Player, Player.

Syntax

Considering a basic example of a base class getting inherited by a derived class, the following is the syntax that realises it:

class BaseClass {
  // Base class members
};

class DerivedClass : public BaseClass {
  // Derived class members
};

Example

Let’s consider an example of a simple Bank Account system. The classes to be written would be Savings Account, Checking Account and Trust Account. We can see that all of these accounts have the following things in common:

  1. Balance
  2. Transaction details
  3. Account number

Hence, the following would be a possible structure of Inheritance for this example:

class Account {
	// balance, transaction details, account number, ...
};

class Savings_Account : public Account {
	// Interest rate, specialised withdrawal, ...
};

class Checking_Account : public Account {
	// minimum_balance, per check fee, specialised withdrawal, ...
};

class Trust_Account : public Account {
	// interest rate, specialised withdrawal, ...
};

Types of Inheritance

There are five types of inheritance in C++:

Single inheritance

Single inheritance is a type of inheritance in which a derived class inherits from a single base class. The derived class inherits all of the members of the base class, including its methods, variables, and constructors.

For example, let’s say we have a class called Animal that has a method called eat(). We could then create a derived class called Dog that inherits from Animal. The Dog class would also have the eat() method, even though it is not defined in the Dog class itself.

Here is an example of single inheritance in C++:

class Animal {
 public:
  void eat() {
    std::cout << "I am eating." << std::endl;
  }
};

class Dog : public Animal {
};

int main() {
  Dog myDog;
  myDog.eat(); // Prints "I am eating."
  return 0;
}

In this example, the Animal class is the base class, and the Dog class is the derived class. The Dog class inherits the eat() method from the Animal class.

Single inheritance is a powerful feature of object-oriented programming that can be used to reuse code and organise code in a more logical way.

Multiple inheritance

Multiple inheritance is a type of inheritance in which a derived class inherits from multiple base classes. The derived class inherits all of the members of the base classes, including its methods, variables, and constructors.

For example, let’s take up the same example, a class called Animal that has a method called eat(). We could also have a class called Mammal that has a method called giveBirth(). We could then create a derived class called Dog that inherits from both Animal and Mammal. The Dog class would have both the eat() and giveBirth() methods, even though they are not defined in the Dog class itself.

Here is an example of multiple inheritance in C++:

class Animal {
 public:
  void eat() {
    std::cout << "I am eating." << std::endl;
  }
};

class Mammal {
 public:
  void giveBirth() {
    std::cout << "I am giving birth." << std::endl;
  }
};

class Dog : public Animal, public Mammal {
};

int main() {
  Dog myDog;
  myDog.eat(); // Prints "I am eating."
  myDog.giveBirth(); // Prints "I am giving birth."
  return 0;
}

In this example, the Dog class inherits the eat() method from the Animal class and the giveBirth() method from the Mammal class.

Multilevel inheritance

Multilevel inheritance is a type of inheritance in which a derived class inherits from a base class that itself inherits from another base class. The derived class inherits all of the members of the base classes, including its methods, variables, and constructors.

For example, let’s say we have a class called Animal that has a method called eat(). We could also have a class called Mammal that inherits from Animal and has a method called giveBirth(). We could then create a derived class called Dog that inherits from Mammal. The Dog class would have both the eat() and giveBirth() methods, even though they are not defined in the Dog class itself.

Here is an example of multilevel inheritance in C++:

class Animal {
 public:
  void eat() {
    std::cout << "I am eating." << std::endl;
  }
};

class Mammal : public Animal {
 public:
  void giveBirth() {
    std::cout << "I am giving birth." << std::endl;
  }
};

class Dog : public Mammal {
};

int main() {
  Dog myDog;
  myDog.eat(); // Prints "I am eating."
  myDog.giveBirth(); // Prints "I am giving birth."
  return 0;
}

In this example, the Dog class inherits the eat() method from the Animal class and the giveBirth() method from the Mammal class.

Hierarchical inheritance

Hierarchical inheritance is a type of inheritance in which a derived class inherits from a single base class, and that base class is also inherited from another derived class. The derived classes are like branches on a tree, and the base class is like the trunk of the tree.

For example, let’s say we have a class called Animal that has a method called eat(). We could also have a class called Mammal that inherits from Animal and has a method called giveBirth(). We could then create two derived classes called Dog and Cat that both inherit from Mammal. The Dog class would have the eat() and giveBirth() methods, and the Cat class would have the eat() and giveBirth() methods.

Here is an example of hierarchical inheritance in C++:

class Animal {
 public:
  void eat() {
    std::cout << "I am eating." << std::endl;
  }
};

class Mammal : public Animal {
 public:
  void giveBirth() {
    std::cout << "I am giving birth." << std::endl;
  }
};

class Dog : public Mammal {
};

class Cat : public Mammal {
};

int main() {
  Dog myDog;
  myDog.eat(); // Prints "I am eating."
  myDog.giveBirth(); // Prints "I am giving birth."

  Cat myCat;
  myCat.eat(); // Prints "I am eating."
  myCat.giveBirth(); // Prints "I am giving birth."
  return 0;
}

In this example, the Dog class and the Cat class both inherit the eat() and giveBirth() methods from the Mammal class.

Hierarchical inheritance is a powerful feature of object-oriented programming that can be used to reuse code and organise code in a more logical way. It is a good choice when you have a hierarchy of classes, such as animals, mammals, dogs, and cats.

Hybrid inheritance

Hybrid inheritance is a combination of two or more types of inheritance. For example, a class could inherit from multiple base classes, or it could inherit from a base class that itself inherits from another base class.

Here is an example of hybrid inheritance in C++:

class Animal {
 public:
  void eat() {
    std::cout << "I am eating." << std::endl;
  }
};

class Mammal : public Animal {
 public:
  void giveBirth() {
    std::cout << "I am giving birth." << std::endl;
  }
};

class Dog : public Mammal {
};

class FlyingAnimal : public Animal {
 public:
  void fly() {
    std::cout << "I am flying." << std::endl;
  }
};
class Bird : public FlyingAnimal {
};

int main() {
  Dog myDog;
  myDog.eat(); // Prints "I am eating."
  myDog.giveBirth(); // Prints "I am giving birth."

  Bird myBird;
  myBird.eat(); // Prints "I am eating."
  myBird.fly(); // Prints "I am flying."
  return 0;
}

In this example, the Dog class inherits from both the Mammal class and the Animal class. The Bird class inherits from both the FlyingAnimal class and the Animal class. This is a type of hybrid inheritance.

Hybrid inheritance can be a powerful tool for organising code. It can be used to reuse code, to create more complex hierarchies of classes, and to model real-world relationships between objects.

Summary

Each type of inheritance has its own advantages and disadvantages. Single inheritance is the simplest type of inheritance, but it can be limiting if you need to reuse code from multiple classes. Multiple inheritance can be more flexible, but it can also be more complex and error-prone. Multilevel inheritance can be useful for organising code, but it can also make it more difficult to keep track of the relationships between classes. Hierarchical inheritance can be useful for representing hierarchies of objects, but it can also make it more difficult to find the code that you need. Hybrid inheritance can be the most flexible type of inheritance, but it can also be the most complex and error-prone.

The best type of inheritance to use depends on the specific needs of the project. If you are not sure which type of inheritance to use, it is best to start with single inheritance and then add more complex types of inheritance as needed.

Exercises

Single inheritance

  • Create a class called Shape with a method called draw().
  • Create a derived class called Circle that inherits from Shape.
  • Create a derived class called Rectangle that inherits from Shape.
  • In the draw() method of Circle, print “I am a circle.”
  • In the draw() method of Rectangle, print “I am a rectangle.”
  • Create an object of type Circle and call the draw() method.
  • Create an object of type Rectangle and call the draw() method.

Multiple inheritance

  • Create a class called Animal with a method called eat().
  • Create a class called Mammal with a method called giveBirth().
  • Create a class called Dog that inherits from both Animal and Mammal.
  • In the eat() method of Dog, print “I am a dog and I am eating.”
  • In the giveBirth() method of Dog, print “I am a dog and I am giving birth.”
  • Create an object of type Dog and call the eat() and giveBirth() methods.

Multilevel inheritance

  • Create a class called Animal with a method called eat().
  • Create a class called Mammal that inherits from Animal and has a method called giveBirth().
  • Create a class called Dog that inherits from Mammal.
  • In the eat() method of Dog, print “I am a dog and I am eating.”
  • In the giveBirth() method of Dog, print “I am a dog and I am giving birth.”
  • Create an object of type Dog and call the eat() and giveBirth() methods.

Hierarchical inheritance

  • Create a class called Animal with a method called eat().
  • Create a derived class called Mammal that inherits from Animal and has a method called giveBirth().
  • Create two more derived classes called Dog and Cat that both inherit from Mammal.
  • In the eat() method of Dog, print “I am a dog and I am eating.”
  • In the giveBirth() method of Dog, print “I am a dog and I am giving birth.”
  • In the eat() method of Cat, print “I am a cat and I am eating.”
  • In the giveBirth() method of Cat, print “I am a cat and I am giving birth.”
  • Create an object of type Dog and call the eat() and giveBirth() methods.
  • Create an object of type Cat and call the eat() and giveBirth() methods.

Hybrid inheritance

  • Create a class called Shape with a method called draw().
  • Create a class called Color with a method called setColor().
  • Create a derived class called Circle that inherits from both Shape and Color.
  • In the draw() method of Circle, print “I am a circle.”
  • In the setColor() method of Circle, print “I am setting the colour of the circle.”
  • Create an object of type Circle and call the draw() and setColor() methods.

Inheritance vs Composition

Inheritance and composition are two different ways to achieve code reuse in object-oriented programming.

Inheritance is a relationship between two classes where one class, the derived class, inherits the properties and methods of another class, the base class. The derived class is said to extend the base class.

Composition is a relationship between two classes where one class, the container class, contains an instance of another class, the contained class. The container class is said to compose the contained class.

The main difference between inheritance and composition is the relationship between the two classes. In inheritance, the derived class is a specialisation of the base class. This means that the derived class inherits all of the properties and methods of the base class, and it can also add its own properties and methods. In other words, the relationship between derived class and base class in inheritance is a “Is-A relationship”. For example,

Derived classRelationshipBase class
EmployeeIs aPerson
SavingsAccountIs aBankAccount
DogIs anAnimal
CircleIs aShape

Whereas, in composition, the container class is a collection of the contained class. This means that the container class can have multiple instances of the contained class, and it can also access the properties and methods of the contained class. In other words, the relationship between 2 classes in composition is “Has-a”. For example,

Container classRelationshipContained class
EmployeeHas aSavingsAccount
PersonHas aHouse
CircleHas aLocation
CarHas anEngine

Inheritance is a powerful tool for code reuse, but it can also lead to tight coupling between classes. This means that changes to the base class can break the derived class. Composition is a loser coupling approach to code reuse, and it is less likely to lead to problems when the base class changes.

In general, inheritance is a good choice when the derived class is a specialisation of the base class. Composition is a good choice when the container class is a collection of the contained class.

Here is a table that summarises the differences between inheritance and composition:

FeatureInheritanceComposition
Relationship between classesDerived class is a specialisation of base classContainer class is a collection of contained class
CouplingTight couplingLoose coupling
When to useWhen the derived class is a specialisation of the base classWhen the container class is a collection of the contained class

A practical example:

Exercise

Develop the following program:

  • Create a class called Animal with the following properties:
    • Name (string)
    • Age (Float)
    • Species (String)
  • Create a class called Mammal that inherits from Animal and has the following properties:
    • gives birth (Boolean)
    • nurses young (Boolean)
  • Create a class called Dog that inherits from Mammal and has the following properties:
    • Barks (Boolean)
    • wags tail (Boolean)
  • Create a class called Cat that inherits from Mammal and has the following properties:
    • Purrs (Boolean)
    • Scratches (Boolean)
  • Create a class called Zoo that contains a collection of animals.
  • Add a method to the Zoo class that allows you to add an animal to the zoo.
  • Add a method to the Zoo class that allows you to remove an animal from the zoo.
  • Add a method to the Zoo class that allows you to print a list of all the animals in the zoo.

Public, private, and protected inheritance in C++

  • Public inheritance is a type of inheritance in which the derived class inherits the public members of the base class. This means that the derived class can access the public members of the base class, and it can also add its own public members.
  • Private inheritance is a type of inheritance in which the derived class inherits the private members of the base class. This means that the derived class can access the private members of the base class, but the public and protected members of the base class are not accessible to the derived class.
  • Protected inheritance is a type of inheritance in which the derived class inherits the protected members of the base class. This means that the derived class can access the protected members of the base class, and the public members of the base class are also accessible to the derived class.

The main difference between public, private, and protected inheritance is the accessibility of the members of the base class. In public inheritance, the derived class can access all of the members of the base class. In private inheritance, the derived class can only access the private members of the base class. In protected inheritance, the derived class can access the protected members of the base class, and the public members of the base class are also accessible to the derived class.

Public inheritance is the most common type of inheritance. It is used when the derived class needs to have access to all of the members of the base class. Private inheritance is used when the derived class needs to have complete control over the base class. Protected inheritance is used when the derived class needs to have access to the base class, but it does not need to have complete control over the base class.

Here are some examples of public, private, and protected inheritance in C++:

Public inheritance:

class Base {
  public:
    int public_member;

  private:
    int private_member;

  protected:
    int protected_member;
};

class Derived : public Base {
  public:
    void derived_member() {
      std::cout << public_member; // This will compile.
      std::cout << private_member; // This will not compile.
      std::cout << protected_member; // This will compile.
    }
};

In this example, the Derived class inherits the public_member variable from the Base class. The public_member variable is accessible to the Derived class, and it is also accessible to any other classes. The private_member and protected_member variables are not accessible to the Derived class.

Private inheritance

class Base {
  public:
    int public_member;

  private:
    int private_member;

  protected:
    int protected_member;
};

class Derived : private Base {
  public:
    void derived_member() {
      std::cout << public_member; // This will not compile.
      std::cout << private_member; // This will compile.
      std::cout << protected_member; // This will not compile.
    }
};

In this example, the Derived class inherits the private_member variable from the Base class. The public_member and protected_member variables are not accessible to the Derived class.

Protected inheritance

class Base {
  public:
    int public_member;

  private:
    int private_member;

  protected:
    int protected_member;
};

class Derived : protected Base {
  public:
    void derived_member() {
      std::cout << public_member; // This will compile.
      std::cout << private_member; // This will not compile.
      std::cout << protected_member; // This will compile.
    }
};

In this example, the Derived class inherits the protected_member variable from the Base class. The public_member and private_member variables are also accessible to the Derived class.

Exercises

Private inheritance

  • Create a class called Shape with the following properties:
    • name
    • colour
  • Create a class called Circle that inherits from Shape and has the following properties:
    • radius
  • Create a class called Rectangle that inherits from Shape and has the following properties:
    • width
    • height
  • Create a method in the Circle class that calculates the area of the circle.
  • Create a method in the Rectangle class that calculates the area of the rectangle.
  • Create a main function that creates a Circle object and a Rectangle object and prints the area of each object.

Protected Inheritance

  • Create a class called Account with the following properties:
    • name
    • balance
  • Create a class called CheckingAccount that inherits from Account and has the following properties:
    • monthly fee
  • Create a class called SavingsAccount that inherits from Account and has the following properties:
    • interest rate
  • Create a method in the CheckingAccount class that deducts the monthly fee from the balance.
  • Create a method in the SavingsAccount class that adds interest to the balance.
  • Create a main function that creates a CheckingAccount object and a SavingsAccount object and deposits $100 into each account. The main function should then print the balance of each account.

Constructors and destructors in Inheritance

When a class inherits from another class, the constructors and destructors of the base class are called automatically before the constructors and destructors of the derived class are called. This is called constructor chaining. For example, consider the following code:

#include <iostream>

class Base {
    public:
        Base() {
            std::cout << "Base constructor called" << std::endl;
        }
        ~Base() {
            std::cout << "Base destructor called" << std::endl;
        }
};

class Derived : public Base {
    public:
        Derived() {
            std::cout << "Derived constructor called" << std::endl;
        }
    
        ~Derived() {
            std::cout << "Derived destructor called" << std::endl;
        }
};

int main() {
    Derived *der = new Derived();
    std::cout << std::endl << "Object is alive" << std::endl <<std::endl;
    delete der;
    std::cout << std::endl << "Object is destroyed";
    return 0;
}

Output:

Base constructor called
Derived constructor called

Object is alive

Derived destructor called
Base destructor called

Object is destroyed

When a Derived object is created, the following happens:

  1. The Base constructor is called.
  2. The Derived constructor is called.
  3. The Base destructor is called.

When a Derived object is destroyed, the following happens:

  1. The Derived destructor is called.
  2. The Base destructor is called.

Important rules

Note that a derived class does NOT inherit:

  • Base class constructor
  • Base class destructor
  • Base class overloaded assignment operators
  • Base class friend functions

However, the derived class constructors, destructors and overloaded assignment operators can invoke the base class versions. One may ask what is the difference when we just saw that the base class constructors and destructors are called implicitly? This is a key feature when the base class has overloaded constructors and the Derived class needs to choose which version of the base class constructor needs to be called. This also comes with the fact that the derived class can come with its own set of overloaded constructors. These overloaded constructors of derived class will want to invoke specific overloaded constructor in the base class. The syntax to call the base class constructor from the derived class constructor is as follows:

class Derived : public Base {
public:
  Derived() : Base() {
    // Derived class constructor body
  }
};

It is important to note that the Base class constructor must be declared before the Derived class constructor. Otherwise, the compiler will not be able to find the Base class constructor.

The syntax to call a parameterized base class constructor from a parameterized derived class constructor is as follows:

class Derived : public Base {
public:
  Derived(int x, int y) : Base(x, y) {
    // Derived class constructor body
 }
};

C++11 allows explicit inheritance of base ‘non-special’ constructors with

  • using Base::Base; anywhere in the derived class declaration.
  • Lots of rules involved, often better to define constructors yourself.

Copy, move constructors and assignment operator with derived classes

When a derived class inherits from a base class, the compiler will automatically generate a copy constructor, move constructor, and assignment operator for the derived class. However, these generated copy constructors, move constructors, and assignment operators may not be what you want. For example, the generated copy constructor may make a shallow copy of the object, which can lead to problems if the object contains pointers or references to dynamically allocated memory.

To avoid these problems, you can override the copy constructor, move constructor, and assignment operator for the derived class. When you override these functions, you need to make sure that they correctly copy or move the data from the source object to the destination object.

Here is an example of how to override the copy constructor for a derived class:

class Base {
public:
  Base() {
    std::cout << "Base constructor called\n";
  }

  Base(const Base& other) {
    std::cout << "Base copy constructor called\n";
  }

  ~Base() {
    std::cout << "Base destructor called\n";
  }
};

class Derived : public Base {
public:
  Derived() {
    std::cout << "Derived constructor called\n";
  }

  Derived(const Derived& other) : Base(other) {
    std::cout << "Derived copy constructor called\n";
  }

  ~Derived() {
    std::cout << "Derived destructor called\n";
  }
};

In this example, the Derived class overrides the Base class copy constructor. The Derived class copy constructor first calls the Base class copy constructor to copy the base class data. Then, the Derived class copy constructor copies the derived class data.

Here is an example of how to override the move constructor for a derived class:

class Base {
public:
  Base() {
    std::cout << "Base constructor called\n";
  }

  Base(Base&& other) {
    std::cout << "Base move constructor called\n";
  }

  ~Base() {
    std::cout << "Base destructor called\n";
  }
};

class Derived : public Base {
public:
  Derived() {
    std::cout << "Derived constructor called\n";
  }

  Derived(Derived&& other) : Base(std::move(other)) {
    std::cout << "Derived move constructor called\n";
  }

  ~Derived() {
    std::cout << "Derived destructor called\n";
  }
};

In this example, the Derived class overrides the Base class move constructor. The Derived class move constructor first calls the Base class move constructor to move the base class data. Then, the Derived class move constructor moves the derived class data.

Here is an example of how to override the assignment operator for a derived class:

class Base {
public:
  Base& operator=(const Base& other) {
    std::cout << "Base assignment operator called\n";
    return *this;
  }

  ~Base() {
    std::cout << "Base destructor called\n";
  }
};

class Derived : public Base {
public:
  Derived& operator=(const Derived& other) {
    std::cout << "Derived assignment operator called\n";
    Base::operator=(other);
    // Copy derived class data
    return *this;
  }

  ~Derived() {
    std::cout << "Derived destructor called\n";
  }
};

In this example, the Derived class overrides the Base class assignment operator. The Derived class assignment operator first calls the Base class assignment operator to copy the base class data. Then, the Derived class assignment operator copies the derived class data.

Method overriding: Redefining base class methods

Method overriding is a feature of object-oriented programming that allows a derived class to provide a different implementation of a method that is defined in its base class. This is done by defining a method in the derived class with the same name, parameter list, and return type as the method in the base class.

For example, consider the following code:

class Base {
public:
  void print() {
    std::cout << "Base::print()\n";
  }
};

class Derived : public Base {
public:
  void print() {
    std::cout << "Derived::print()\n";
  }
};

In this code, the Base class has a method called print(). The Derived class also has a method called print(). The two methods have the same name, parameter list, and return type. Therefore, the Derived class is overriding the print() method from the Base class.

When an object of the Derived class is created, the print() method from the Derived class will be called when the print() method is invoked on the object.

Method overriding is a powerful feature of object-oriented programming that allows for greater flexibility and code reuse.

Here are some additional things to keep in mind about method overriding:

  • The method in the derived class must have the same name, parameter list, and return type as the method in the base class.
  • The method in the derived class can have a different implementation than the method in the base class.
  • The method in the derived class will be called when the method is invoked on an object of the derived class.
  • The method in the base class will not be called when the method is invoked on an object of the derived class.

Static binding is a technique in which the type of a variable or expression is known at compile time. This means that the compiler can resolve the call to a method or function at compile time, rather than at runtime.

In C++, static binding is used in conjunction with inheritance. When a derived class inherits from a base class, the compiler can resolve the call to a method or function that is defined in the base class, even if the object is actually of the derived class type. The example we just saw is of static binding.

Prevention of inheritance in C++

Inheritance is a powerful feature of C++, but it can also be abused. In some cases, it may be desirable to prevent a class from being inherited from. There are a few different ways to do this in C++.

One way to prevent inheritance is to use the final keyword. The final keyword can be used to mark a class as final, which means that it cannot be inherited from. For example:

class MyClass final {
  // ...
};

Another way to prevent inheritance is to make the constructor private. If a class has a private constructor, then it cannot be inherited from. For example:

class MyClass {
  private:
    MyClass() {}
};

Finally, it is also possible to prevent inheritance by using a friend class. A friend class is a class that has access to the private and protected members of another class. This can be used to create a class that can access the private members of a base class, but cannot be inherited from. For example:

class MyClass {
  friend class MyFriendClass;

  private:
    MyClass() {}
};

class MyFriendClass {
  // ...
};

It is important to note that preventing inheritance can sometimes be useful, but it can also make it more difficult to design and implement classes. It is important to carefully consider the pros and cons of preventing inheritance before using it.

Here are some of the reasons why you might want to prevent inheritance in C++:

  • To prevent the accidental creation of subclasses that have unexpected or undesirable behaviour.
  • To enforce a strict hierarchy of classes.
  • To prevent the modification of the base class by subclasses.
  • To improve the performance of the class by preventing the creation of unnecessary objects.

Here are some of the drawbacks of preventing inheritance in C++:

  • It can make it more difficult to reuse code.
  • It can make it more difficult to implement polymorphism.
  • It can make it more difficult to test code.

Ultimately, the decision of whether or not to prevent inheritance in C++ is a design decision that should be made on a case-by-case basis.

Diamond problem of Inheritance

The diamond problem of inheritance is a situation that can arise in multiple inheritance when a derived class inherits from two or more base classes that share a common base class. It can lead to ambiguity in the derived class about which inherited function or data member to use.

Here’s an example to illustrate the diamond problem:

class A {
public:
    void foo() { std::cout << "A::foo()" << std::endl; }
};

class B : public A {
public:
    void foo() { std::cout << "B::foo()" << std::endl; }
};

class C : public A {
public:
    void foo() { std::cout << "C::foo()" << std::endl; }
};

class D : public B, public C {
public:
    // This class inherits from both B and C,
    // which both inherit from A.
};

int main() {
    D d;
    d.foo(); // This line will not compile because of ambiguity.
    return 0;
}

In this example, class A is the common base class, while class B and class C both inherit from A. Class D inherits from both B and C, which results in the diamond shape. The foo() function is defined in both B and C, and is inherited by D. When we call d.foo(), the compiler doesn’t know which implementation of foo() to use – should it use B::foo() or C::foo()? This ambiguity is the diamond problem.

To solve the diamond problem, C++ offers two methods: virtual inheritance and scope resolution. We shall see virtual inheritance in the next section. Let’s see how we can use the scope resolution and solve the diamond inheritance problem

The idea is to specify the class from which a particular function is to be called using the scope resolution operator. We can specify the base class from which we want to call the function. For example, to call the foo() function from class B, we can use the following syntax:

D::B::foo();

Similarly, to call the foo() function from class C, we can use:

D::C::foo();

By using the scope resolution operator, we can resolve the ambiguity and specify exactly which function to call. In the example we saw previously, we can use scope resolution and solve the diamond inheritance problem the following way:

class A {
public:
    void foo() { std::cout << "A::foo()" << std::endl; }
};

class B : public A {
public:
    void foo() { std::cout << "B::foo()" << std::endl; }
};

class C : public A {
public:
    void foo() { std::cout << "C::foo()" << std::endl; }
};

class D : public B, public C {
public:
    // This class inherits from both B and C,
    // which both inherit from A.
};

int main() {
    D d;
    d.B::A::foo(); // specify that we want to call the foo() function from class A in the B branch of the diamond
    return 0;
}

Difference between overloading and overriding

FeatureMethod overloadingMethod overriding
DefinitionA technique that allows a class to have multiple functions with the same name but with different parameters.A technique that allows a derived class to have a function with the same name, parameter list, and return type as a function in its base class.
InheritanceNot requiredRequired
ResolutionCompile timeRun time
AccessFunctions can be overloaded in the same classFunctions can be overridden only in the derived class
Examplevoid print (int x) and void print (char *x)void print(int x) in the base class and void print(int x) in the derived class.

Exercises

  1. Create a class called Shape with methods to calculate the perimeter and area of a shape. Create subclasses of Shape for different types of shapes, such as Circle, Rectangle, and Triangle. Each subclass should override the perimeter() and area() methods to calculate the perimeter and area of the specific shape.
  2. Create a class called Animal with a method called makeSound(). Create subclasses of Animal for different types of animals, such as Dog, Cat, and Bird. Each subclass should override the makeSound() method to make the sound of the specific animal.
  3. Create a class called Vehicle with methods to calculate the fuel efficiency, distance travelled, and maximum speed of a vehicle. Create subclasses of Vehicle for different types of vehicles, such as Car, Truck, and Motorcycle. Each subclass should override the fuelEfficiency(), distanceTraveled(), and maximumSpeed() methods to calculate the specific values for the type of vehicle.

Interview Questions

  1. What is inheritance in C++ and why is it useful?
  2. What are the different types of inheritance in C++?
  3. What is the syntax for inheriting a class in C++?
  4. Can multiple inheritance be achieved in C++? If so, how?
  5. What is the difference between private, protected, and public inheritance in C++?
  6. How do you prevent inheritance in C++?
  7. What is the diamond problem in C++ inheritance and how do you solve it?
  8. How do you access the members of a base class in a derived class?
  9. What is function overriding in C++ inheritance?
  10. What is the difference between function overloading and function overriding in C++?
  11. Can a derived class be used as a base class in C++? If so, how?
  12. What are the advantages and disadvantages of using inheritance in C++?
  13. How does C++ inheritance differ from Java inheritance?
  14. What is the output of the following code:
#include <iostream>
class A {
public:
  void foo() {
    std::cout << "This is foo in A\n";
  }
};

class B : public A {
public:
  void foo() {
    std::cout << "This is foo in B\n";
  }
};
class C : public B {};

int main() {
  C c;
  c.foo();
  return 0;
}
Categories
Computer Science / Information Technology Language: C++

Operator overloading

Overview

Operator overloading is a feature in C++ that allows operators to be given new meanings when applied to user-defined data types. This means that operators such as +, -, *, /, etc. can be used with objects of a class in the same way as they are used with built-in data types. This can make code more readable and writable. These are not implicit, i.e., not done automatically (except for the assignment operator), they must be explicitly defined.

Operator overloading in perspective of using objects

Let’s consider an example of a class Point that represents a point in a two-dimensional plane with coordinates x and y. We want to be able to add two Point objects using the + operator.

Without operator overloading, we would need to define a member function to add two Point objects, such as:

class Point {
public:
    Point(int x, int y) : x(x), y(y) {}
    Point add(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }
private:
    int x, y;
};

To add two Point objects, we would have to call the add member function explicitly:

Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1.add(p2);

Or worse off, there could be a static method or a function called add() that takes two parameters: objects of Point class that need to be added. we would have to call the add member function explicitly:

Point p3 = add(p1, p2);

With operator overloading, we can define the + operator to perform the addition of two Point objects:

Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1 + p2;

As we can see, operator overloading provides a more natural and intuitive way of expressing operations on objects. It allows us to use familiar syntax for built-in types with user-defined types, making our code more readable and easier to understand. In this section, we’ll extensively dive into details of operator overloading.

Operators that support overloading

In C++, the following operators can be overloaded using operator overloading:

  1. Arithmetic operators: +, -, *, /, %, +=, -=, *=, /=, %=
  2. Comparison operators: ==, !=, <, >, <=, >=
  3. Logical operators: !, &&, ||
  4. Bitwise operators: &, |, ^, ~, <<, >>
  5. Assignment operators: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  6. Increment and decrement operators: ++, --
  7. Function call operator: ()
  8. Subscript operator: []
  9. Member selection operator: ->
  10. Comma operator: ,

Also, in C++, not all operators can be overloaded. The following operators cannot be overloaded:

  1. :: – Scope resolution operator
  2. . – Member selection operator
  3. .* – Pointer-to-member selection operator
  4. sizeof – Size-of operator
  5. typeid – Type information operator
  6. ?: – Ternary conditional operator (conditional operator can be overloaded, but ?: specifically cannot)
  7. # – Preprocessor operator
  8. ## – Token pasting operator (used in macro definitions)

These operators are either completely built into the language (e.g., sizeof, typeid) or have a specific syntax that cannot be changed (e.g., . for member selection and :: for scope resolution).

Basic rules of operator overloading

The basic rules of operator overloading in C++ are:

  1. Only existing operators can be overloaded, and new operators cannot be created.
  2. The number of operands (‘arity’) cannot be changed. For example, the addition operator + always takes two operands, and this cannot be changed.
  3. Precedence and associativity cannot be changed.
  4. Overloaded operators must obey the same syntax and precedence rules as the built-in operators.
  5. Overloaded operators should not modify their operands, except when it makes sense to do so, like in the case of the assignment operator =.
  6. Some operators, such as the conditional operator ?:, require multiple overloaded versions to cover all possible operand types.
  7. [], (), ->, and the assignment operator (=) must be declared as member method while other operators can be declared as member methods or global functions.

By following these rules, C++ allows programmers to redefine the behaviour of built-in operators to work with user-defined types.

Syntax of operator overloading

The syntax of operator overloading in C++ is as follows:

ReturnType operator Symbol (ParameterList) {
    // code to perform the operation
    return Result;
}

Here, ReturnType is the return type of the overloaded operator, Symbol is the symbol of the operator being overloaded (such as +, -, *, /, ==, etc.), ParameterList is the list of parameters that the operator takes, and the body of the function contains the code that performs the operation.

Overloading Arithmetic operators

Overloading the addition operator (+)

Overloading the addition operator (+) in a class allows objects of that class to be added using the + operator. This is useful when the addition of objects of a certain class has a specific meaning or operation.

The syntax for overloading the addition operator in a class is as follows:

ReturnType operator+(const ClassName& obj) const;

Here,

  • ReturnType is the return type of the overloaded operator.
  • operator+ is the addition operator.
  • ClassName is the name of the class.
  • obj is the object to be added to the current object.
  • The const keyword is used to indicate that the operator function does not modify the state of the object.

Let’s consider an example of a class Complex that represents complex numbers:

class Complex {
    private:
        double real;
        double imag;
    public:
        Complex(double r = 0, double i = 0) : real(r), imag(i) {}
        Complex operator+(const Complex& obj) const {
            Complex res;
            res.real = real + obj.real;
            res.imag = imag + obj.imag;
            return res;
        }
        void display() {
            cout << real << " + " << imag << "i" << endl;
        }
};

In this example, we have overloaded the addition operator using the operator+ function. This function takes an object of the class Complex as an argument and returns an object of the same class. The function creates a new object res, sets its real and imag members to the sum of the corresponding members of the two input objects, and returns the new object. The display function is used to print the complex number in the format real + imagi.

Now, we can use the addition operator to add two Complex objects as follows:

Complex c1(2, 3);
Complex c2(4, 5);
Complex c3 = c1 + c2;
c3.display();   // Output: 6 + 8i

In this example, we have created two Complex objects c1 and c2 and added them using the + operator. The result is stored in a new Complex object c3, which is then displayed.

Overloading subtraction operator

Let’s consider the same example of the Complex class used earlier. To overload the subtraction operator - for this class, we can define a member function named operator- that takes another Complex object as an argument and returns a Complex object as the result of the subtraction operation.

Here’s an example implementation of the subtraction overloading method:

Complex operator-(const Complex& other) const {
    return Complex(real - other.real, imag - other.imag);
}

In this implementation, the operator- function takes a const Complex& parameter named other, which represents the Complex object to be subtracted from the current object. The function returns a new Complex object that represents the result of the subtraction.

For example, if we have two Complex objects c1 and c2, we can subtract c2 from c1 using the overloaded - operator as follows:

Complex c1(2.0, 3.0);
Complex c2(1.0, 2.0);
Complex result = c1 - c2;  // calls operator- function

In this case, the result object will have a real part of 1.0 and an imaginary part of 1.0, which is the result of subtracting c2 from c1.

Note that we can also overload the - operator as a non-member function or a friend function of the Complex class, just like the + operator. The syntax for this would be:

Complex operator-(const Complex& c1, const Complex& c2) {
    return Complex(c1.real - c2.real, c1.imag - c2.imag);
}

This function can be used in the same way as the member function overload.

Overloading other arithmetic operators

Similar to the examples seen above for addition and subtraction operator overloading, other arithmetic operators such as multiplication, division and modulus operators can be overloading. The following code does the same:

#include <iostream>

class Complex {
private:
    double real;
    double imag;

public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // Overload * operator
    Complex operator*(const Complex& c) const {
        double r = real * c.real - imag * c.imag;
        double i = real * c.imag + imag * c.real;
        return Complex(r, i);
    }

    // Overload / operator
    Complex operator/(const Complex& c) const {
        double denominator = c.real * c.real + c.imag * c.imag;
        double r = (real * c.real + imag * c.imag) / denominator;
        double i = (imag * c.real - real * c.imag) / denominator;
        return Complex(r, i);
    }

    // Overload % operator
    Complex operator%(const Complex& c) const {
        double denominator = c.real * c.real + c.imag * c.imag;
        double r = (real * c.real + imag * c.imag) / denominator;
        double i = (imag * c.real - real * c.imag) / denominator;
        return Complex(real - c.real * r - c.imag * i, imag - c.real * i + c.imag * r);
    }

    // Display Complex number
    void display() const {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(2, 3);
    Complex c2(4, 5);

    Complex c3 = c1 * c2;
    Complex c4 = c1 / c2;
    Complex c5 = c1 % c2;

    std::cout << "c1 = ";
    c1.display();

    std::cout << "c2 = ";
    c2.display();

    std::cout << "c1 * c2 = ";
    c3.display();

    std::cout << "c1 / c2 = ";
    c4.display();

    std::cout << "c1 % c2 = ";
    c5.display();

    return 0;
}

In the above example, we have overloaded the *, / and % operators using member functions. Here are some key points:

  • We define the operator*, operator/, and operator% functions as member functions of the Complex class.
  • We use the const keyword to specify that these functions do not modify the objects they are called on.
  • The operator* function returns a new Complex object that represents the product of two Complex objects.
  • The operator/ function returns a new Complex object that represents the quotient of two Complex objects.
  • The operator% function returns a new Complex object that represents the remainder of two Complex objects.
  • In the main function, we create two Complex objects c1 and c2, and then use the overloaded operators to perform arithmetic operations on them.
  • Finally, we display the results using the display function that we defined earlier.

Overall, by overloading these arithmetic operators as member functions, we can use them with Complex objects just like we would use the built-in arithmetic operators with primitive types.

Overloading assignment and compound assignment operators

Let’s continue with the complex numbers example we used before. The assignment operator, operator=, is used to assign one object to another object of the same class. By default, the compiler provides a default assignment operator which performs a shallow copy of the object. However, if the class contains any dynamically allocated memory, this default behaviour may not be sufficient and a custom assignment operator may need to be defined.

The default copy using the ‘=’ operator will only copy the pointer, not the actual data, resulting in two objects pointing to the same memory location. This can lead to problems if one of the objects is deleted, as the other object would be left with a dangling pointer.

To avoid such issues, we can define our own assignment operator that performs a deep copy of the object’s member variables, copying the actual data instead of just the pointer.

Syntax for overloading assignment operator for deep copying

class MyClass {
public:
  //...

  MyClass& operator=(const MyClass& other) {
    if (this != &other) { // check for self-assignment
      // Delete the existing resources held by the object
      // and copy the resources of the other object to this object
      // using deep copy
    }
    return *this;
  }

  //...
};

In this syntax, we define the assignment operator as a member function of the class MyClass and it takes a const reference to another object of the same class as its argument. Inside the function, we first check if the object being assigned to is not the same as the other object. If they are different, we delete the existing resources held by the object and copy the resources of the other object to this object using deep copy. Finally, we return a reference to the object itself.

Note that for deep copy, we need to define a copy constructor and a destructor that implement the logic of copying the resources held by an object and deleting them, respectively.

Example of overloading assignment operator for deep copying

Here is an example of how to overload the assignment operator for a Mystring class which is our minimal implementation of string class in C++:

#include <iostream>
#include <cstring>

class MyString {
private:
    char* str;
    int len;
public:
    MyString() : str(nullptr), len(0) {}
    MyString(const char* s) : str(nullptr), len(0) {
        if (s) {
            len = std::strlen(s);
            str = new char[len+1];
            std::strcpy(str, s);
        }
    }
    MyString(const MyString& other) : str(nullptr), len(other.len) {
        if (other.str) {
            str = new char[len+1];
            std::strcpy(str, other.str);
        }
    }
    MyString& operator=(const MyString& other) {
        std::cout << "Called overloaded method for assignment operator" << std::endl;
        if (this != &other) {
            delete[] str;    // Deallocate memory of the str string of current class to avoid memory leak/overflow/underflow
            len = other.len;
            if (other.str) {
                str = new char[len+1];
                std::strcpy(str, other.str);
            }
            else {
                str = nullptr;
            }
        }
        return *this;
    }
    ~MyString() {
        delete[] str;
    }
    const char* c_str() const {
        return str;
    }
};

int main() {
    MyString s1("hello");
    MyString s2("world");
    std::cout << "s1: " << s1.c_str() << std::endl;
    std::cout << "s2: " << s2.c_str() << std::endl;
    s1 = s2;
    std::cout << "s1 after assignment: " << s1.c_str() << std::endl;
    return 0;
}

Output:

s1: hello
s2: world
Called overloaded method for assignment operator
s1 after assignment: world

In this example, MyString is a class that holds a C-style string (char*) and its length. The constructor that takes a const char* argument allocates dynamic memory to hold the string and copies it into the class member. The copy constructor and destructor are also defined to handle dynamic memory allocation and deallocation.

The assignment operator overloading function is defined as a member function that takes a const MyString& argument and returns a reference to MyString. Inside the function, the code checks if the object being assigned to is not the same as the current object (i.e. it’s not a self-assignment). If it’s not a self-assignment, the function first deallocates the current memory pointed to by str and then allocates new memory for the incoming string, and copies the incoming string into the class member. Finally, it returns a reference to the current object.

The main() function demonstrates how the assignment operator can be used to assign one MyString object to another, and how the copy constructor ensures that dynamic memory is correctly allocated and deallocated for each object.

Difference between copy constructor and overloading assignment operator

The copy constructor and the overloaded assignment operator are two ways of copying one object to another, but they have some differences.

The copy constructor is used to create a new object by initialising it with an existing object. It is called automatically whenever a new object is created from an existing object, either by assignment or by passing the object by value.

On the other hand, the overloaded assignment operator is used to copy the contents of one object to another object that already exists. It is called explicitly by the user, using the assignment operator (=).

Here are some differences between the two:

  1. Parameters: The copy constructor takes a single parameter of the same class type as the object being copied, while the overloaded assignment operator takes a single parameter of the same class type as the object being copied to.
  2. Return value: The copy constructor does not return anything, while the overloaded assignment operator returns a reference to the object being assigned to.
  3. Initialization: The copy constructor initialises the new object using the values of the existing object’s data members, while the overloaded assignment operator copies the values of the existing object’s data members to the data members of the object being assigned to.
  4. Usage: The copy constructor is used when a new object is being created from an existing object, while the overloaded assignment operator is used when an existing object is being assigned a new value.

In summary, the copy constructor is used to create a new object, while the overloaded assignment operator is used to copy the contents of an existing object to another object. Both are important for creating and manipulating objects in C++.

Overloading assignment operator for moving

std::move()

Before jumping into this section of overloading assignment operator for moving, we need to understand the std::move() function. std::move is a utility function in C++ defined in the <utility> header. It is used to indicate that an object should be moved instead of copied. When an object is passed as an argument to std::move, it is converted into an rvalue, which makes it eligible for move operations.

The std::move function simply returns its argument as an rvalue reference, allowing the move constructor or move assignment operator to be called instead of the copy constructor or copy assignment operator. This can be useful when you want to move the contents of an object to a new location rather than copying it, which can be more efficient in certain situations.

For example, consider a vector of string objects. If you want to remove an element from the vector and move its contents to a new location, you can use std::move to move the contents instead of copying them:

std::vector<std::string> myVector = {"foo", "bar", "baz"};
std::string myString = std::move(myVector[1]); // move "bar" to myString
myVector.erase(myVector.begin() + 1); // remove "bar" from the vector

In this example, std::move is used to move the contents of the myVector[1] element to myString instead of copying them. This is more efficient than copying the contents because it avoids the overhead of allocating and deallocating memory twice.

Overloading assignment operator for move

When overloading the assignment operator, it is possible to distinguish between copying and moving an object. If an object is being moved, it means that the resources (such as dynamically allocated memory) owned by the source object are transferred to the target object, and the source object is left in a valid but unspecified state. This can be more efficient than copying the resources.

To overload the assignment operator for moving, we need to define a move assignment operator, which has the following syntax:

class MyClass {
public:
    // ...

    // move assignment operator
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            // move resources from other to this
            // ...

            // release resources from this
            // ...
        }
        return *this;
    }
    // ...
};

Do not worry about the keyword noexcept yet, we shall see it in detail in exception handling.

In the move assignment operator, the other object is an rvalue reference to the source object that is being moved. The noexcept specifier indicates that the function does not throw any exceptions.

Here is an example of a Vector class that has a move assignment operator:

#include <iostream>
#include <algorithm>

class Vector {
public:
    // default constructor
    Vector() : data(nullptr), size(0), capacity(0) {}

    // constructor with size and value
    Vector(int size, int value) : data(new int[size]), size(size), capacity(size) {
        std::fill_n(data, size, value);
    }

    // destructor
    ~Vector() {
        delete[] data;
    }

    // copy constructor
    Vector(const Vector& other) : data(new int[other.size]), size(other.size), capacity(other.size) {
        std::copy(other.data, other.data + other.size, data);
    }

    // copy assignment operator
    Vector& operator=(const Vector& other) {
        if (this != &other) {
            int* new_data = new int[other.size];
            std::copy(other.data, other.data + other.size, new_data);
            delete[] data;
            data = new_data;
            size = other.size;
            capacity = other.size;
        }
        return *this;
    }

    // move assignment operator
    Vector& operator=(Vector&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            capacity = other.capacity;
            other.data = nullptr;
            other.size = 0;
            other.capacity = 0;
        }
        return *this;
    }

    // ...

private:
    int* data;
    int size;
    int capacity;
};

int main() {
    Vector v1(3, 1);  // create a vector with size 3 and value 1
    Vector v2;        // create an empty vector

    v2 = std::move(v1);  // move v1 to v2

    std::cout << "v1 size: " << v1.size << '\n';  // should print 0
    std::cout << "v2 size: " << v2.size << '\n';  // should print 3

    return 0;
}

In the example, we create two Vector objects v1 and v2, and then move v1 to v2 using the move assignment operator. After the move, v1 has size 0 and v2 has size 3.

Overloading compound assignment operators

Compound assignment operators are operators that perform a binary operation and then store the result in the left-hand operand. For example, += operator performs an addition operation on its operands and stores the result in the left-hand operand.

In C++, we can overload compound assignment operators like +=, -=, *=, /=, etc. to work with user-defined classes. These operators are typically implemented as member functions.

The syntax for overloading compound assignment operators is similar to overloading the binary operator. Here is an example of overloading the += operator for a Vector class:

class Vector {
private:
    double *data;
    int size;
public:
    Vector(int size) : size(size) {
        data = new double[size];
        for (int i = 0; i < size; i++) {
            data[i] = 0.0;
        }
    }

    Vector(const Vector& other) : size(other.size) {
        data = new double[size];
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
    }

    ~Vector() {
        delete[] data;
    }

    // Compound assignment operator overloading
    Vector& operator+=(const Vector& other) {
        if (size != other.size) {
            throw std::invalid_argument("Sizes do not match");
        }
        for (int i = 0; i < size; i++) {
            data[i] += other.data[i];
        }
        return *this;
    }
};

In this example, we overload the += operator for the Vector class. The operator adds the corresponding elements of two Vector objects and stores the result in the left-hand operand. The function returns a reference to the left-hand operand to enable chaining of operators.

Here is an example of how to use the overloaded += operator:

int main() {
    Vector v1(3);
    v1[0] = 1.0;
    v1[1] = 2.0;
    v1[2] = 3.0;

    Vector v2(3);
    v2[0] = 4.0;
    v2[1] = 5.0;
    v2[2] = 6.0;

    v1 += v2; // Adds v1 and v2 and stores the result in v1

    // Outputs: [5, 7, 9]
    std::cout << v1 << std::endl;

    return 0;
}

In this example, we create two Vector objects v1 and v2. We add v2 to v1 using the overloaded += operator and store the result in v1. The result is [5, 7, 9].

Using the Complex class example, we can overload all the other compound assignment operators as follows:

#include <iostream>
#include <cmath>
using namespace std;

class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // compound assignment operator overloading
    Complex& operator+=(const Complex& rhs) {
        real += rhs.real;
        imag += rhs.imag;
        return *this;
    }
    
    Complex& operator-=(const Complex& rhs) {
        real -= rhs.real;
        imag -= rhs.imag;
        return *this;
    }
    
    Complex& operator*=(const Complex& rhs) {
        double temp_real = real * rhs.real - imag * rhs.imag;
        double temp_imag = real * rhs.imag + imag * rhs.real;
        real = temp_real;
        imag = temp_imag;
        return *this;
    }
    
    Complex& operator/=(const Complex& rhs) {
        double div = rhs.real * rhs.real + rhs.imag * rhs.imag;
        double temp_real = (real * rhs.real + imag * rhs.imag) / div;
        double temp_imag = (imag * rhs.real - real * rhs.imag) / div;
        real = temp_real;
        imag = temp_imag;
        return *this;
    }
    
    Complex& operator%=(const Complex& rhs) {
        real = fmod(real, rhs.real);
        imag = fmod(imag, rhs.imag);
        return *this;
    }
    
    // display function
    void display() {
        cout << real << " + " << imag << "i" << endl;
    }
};

// non-member functions for other compound assignment operators
Complex operator+(Complex lhs, const Complex& rhs) {
    return lhs += rhs;
}

Complex operator-(Complex lhs, const Complex& rhs) {
    return lhs -= rhs;
}

Complex operator*(Complex lhs, const Complex& rhs) {
    return lhs *= rhs;
}

Complex operator/(Complex lhs, const Complex& rhs) {
    return lhs /= rhs;
}

Complex operator%(Complex lhs, const Complex& rhs) {
    return lhs %= rhs;
}

Now, we can use the following code to test all the compound assignment operator overloads:

int main() {
    Complex c1(3.0, 4.0);
    Complex c2(2.0, 1.0);
    
    c1 += c2;
    c1.display();  // expected output: 5 + 5i
    
    c1 -= c2;
    c1.display();  // expected output: 3 + 4i
    
    c1 *= c2;
    c1.display();  // expected output: 2 + 11i
    
    c1 /= c2;
    c1.display();  // expected output: 3 + 4i
    
    c1 %= c2;
    c1.display();  // expected output: 1 + 0i
    
    return 0;
}

Output:

5 + 5i
3 + 4i
2 + 11i
3 + 4i
1 + 0i

Overloading operator using member functions

In all the examples we have seen so far, you might have observed that there are 3 ways in which we can overload a given operator: using member functions of the class, using global function and using a friend function. Let’s see it formally now.

Using member function

In this case, the left-hand operand is an object of the class, and the right-hand operand can be any compatible data type or another object of the class. The operator function is declared inside the class definition and is treated as a member function.

The syntax for overloading operators using member functions is as follows:

MyClass MyClass::operator+(const MyClass& rhs) const {
    // implementation
}

Or, if outside of the class:

In this example, the + operator is overloaded using a member function of the class MyClass. The function takes a reference to the right-hand operand as a constant reference (const MyClass& rhs) and returns a new object of the class MyClass.

Using global functions

Operator overloading using global functions involves defining the overloaded operator function as a global function outside the class. The syntax for overloading binary operators using global functions is as follows:

return_type operator symbol (operand_type1 operand1, operand_type2 operand2) {
    // code to perform the operation on the operands
}

Here, return_type is the type of the value returned by the operator function, symbol is the operator being overloaded, and operand_type1 and operand_type2 are the types of the two operands being operated on. The operator function takes two arguments of these operand types, which represent the two operands on which the operator is applied.

For example, to overload the + operator for a class MyClass using a global function, the syntax would be:

MyClass operator+ (const MyClass& obj1, const MyClass& obj2) {
    // code to perform the addition of obj1 and obj2
}

Here, MyClass is the return type of the overloaded operator function, + is the symbol for the addition operator, and const MyClass& obj1 and const MyClass& obj2 are the two operands being added, passed by reference to avoid copying. The operator function returns a new MyClass object that represents the result of the addition operation.

Using friend function

Operator overloading using friend functions allows a non-member function to access the private data members of a class, which is useful in cases where the operator function needs to modify the private data members directly. The syntax for operator overloading using friend functions is similar to overloading using global functions, with the addition of the friend keyword in the class declaration.

Here is an example of overloading the + operator using a friend function:

class MyClass {
private:
    int value;
public:
    MyClass(int v) : value(v) {}
    friend MyClass operator+(const MyClass& lhs, const MyClass& rhs);
};

MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
    int sum = lhs.value + rhs.value;
    return MyClass(sum);
}

In this example, the operator+ function is declared as a friend function inside the MyClass class. This allows it to access the private data member value. The function takes two constant references to MyClass objects as its parameters, and returns a new MyClass object that contains the sum of the value data members of the two input objects.

Note that the operator+ function is defined outside of the class definition, and is declared as a global function. The friend keyword in the class declaration tells the compiler that this function has access to the private data members of the class.

Overloading stream insertion and extraction extraction

Stream insertion (<<) and extraction (>>) operators are used for input and output operations on streams, such as reading from or writing to files, or displaying output on the console. When we overload these operators, we can customise the way objects of our classes are displayed and read from the console or other input/output streams.

The advantages of overloading stream insertion and extraction operators are as follows:

  1. Readability: It makes the code more readable by allowing the use of standard input/output operations on user-defined types.
  2. Consistency: Overloading these operators for a class makes it consistent with other built-in types, which can also be input/output using these operators.
  3. Code reusability: We can define how the insertion and extraction of streams behaves once and that works the same every time. This can reduce and isolate errors.
  4. Flexibility: We can customise the input and output of the object according to our specific requirements.
  5. Ease of use: We can use the standard input/output stream classes (cin and cout) to input and output our objects, which makes the code shorter and more concise.

Overall, overloading stream insertion and extraction operators helps us to write more expressive and maintainable code, as well as providing a consistent and intuitive interface for interacting with user-defined types.

Let’s take an example to illustrate how overloading stream insertion and extraction operators help. Suppose we have a class called Person which has private members name and age.

class Person {
public:
    Person(std::string name = "", int age = 0) : name(name), age(age){} 
    std::string getName() const { return name; }
    int getAge() const { return age; }
    
private:
    std::string name;
    int age;
};

Inorder to read the name and age and create a Person’s object and then print the member values, we need to do something of the following sort:

int main() {
    std::string name;
    int age;
    std::cout << "Enter the name of the person: ";
    std::cin >> name;
    std::cout << "Enter the age of the person: ";
    std::cin >> age;
    Person p(name, age);
    std::cout << "The name of the person is " << p.getName() << " and his/her age is " << p.getAge() << std::endl;
    return 0;
}

If we were to overload the stream insertion and extraction operators, performing the above operation would be as simple as follows:

int main() {
    Person p;
    std::cin >> p;
    std::cout << p;
    return 0;
}

The syntax for overloading the stream insertion operator is as follows:

std::ostream& operator<<(std::ostream& out, const MyClass& obj) {
    // Code to output obj to out
    return out;
}

Here, std::ostream& is the return type of the function. The out argument is a reference to the stream object that we want to output to, and the const MyClass& argument is a reference to the object that we want to output.

The syntax for overloading the stream extraction operator is as follows:

std::istream& operator>>(std::istream& in, MyClass& obj) {
    // Code to input obj from in
    return in;
}

Here, std::istream& is the return type of the function. The in argument is a reference to the stream object that we want to input from, and the MyClass& argument is a reference to the object that we want to input into.

To demonstrate this practically, we can take up the same example of Person’s class:

#include <iostream>
#include <string>

class Person {
public:
    Person(std::string name = "", int age = 0) : name(name), age(age){}
    std::string getName() const { return name; }
    int getAge() const { return age; }
    
private:
    std::string name;
    int age;
};

// Overload stream insertion operator
std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "The name of the person is " << p.getName() << " and his/her age is " << p.getAge() << std::endl;
    return os;
}

// Overload stream extraction operator
std::istream& operator>>(std::istream& is, Person& p) {
    std::string name;
    int age;
    std::cout << "Enter the name of the person: ";
    is >> name;
    std::cout << "Enter the age of the person: ";
    is >> age;
    p = Person(name, age);
    return is;
}

int main() {
    Person p;
    std::cin >> p;
    std::cout << p;
    return 0;
}

Output:

Enter the name of the person: Prajwal
Enter the age of the person: 25
The name of the person is Prajwal and his/her age is 25

Rules of overloading stream insertion and extraction operators

  1. The stream insertion operator << must be overloaded as a non-static member function or a non-member function, while the stream extraction operator >> must be overloaded as a non-static member function only.
  2. Overloaded stream insertion and extraction operators must accept two arguments: the stream object on the left-hand side of the operator, and the object being read from or written to the stream on the right-hand side.
  3. The stream insertion operator << should return the stream object by reference, while the stream extraction operator >> should return the stream object by value or reference.
  4. Overloaded stream insertion and extraction operators should be declared as friend functions of the class whose objects are being inserted into or extracted from the stream. This is to allow the overloaded operator function to access the private members of the class.
  5. The overloaded stream insertion and extraction operators should be defined outside the class definition, typically in the same source file as the class implementation.
  6. Overloaded stream insertion and extraction operators should follow the same syntax and formatting conventions as the standard operators. For example, when overloading the stream insertion operator, the object being inserted should be written to the stream in a formatted manner.

By following these rules, we can ensure that the overloaded stream insertion and extraction operators behave consistently with the standard operators and provide a convenient and intuitive way to read from and write to streams.

The default keyword

In C++, the default keyword is used to explicitly specify that a member function (constructor, destructor, or assignment operator) should be generated by the compiler with its default behaviour. It is primarily used in relation to special member functions of a class.

When a special member function is defined with the default keyword, it means that the compiler-generated default implementation should be used for that function. The default behaviour is typically the one that would be automatically generated by the compiler if the function was not explicitly defined by the programmer.

Here are some use cases for the default keyword:

  1. Default Constructor: If a class needs a default constructor but also has other constructors defined, the default keyword can be used to explicitly request the compiler to generate the default constructor.
class MyClass {
public:
    MyClass() = default;  // Compiler-generated default constructor
    MyClass(int value) {
        // Constructor with parameter
    }
};
  1. Copy Constructor and Copy Assignment Operator: By using default, you can instruct the compiler to generate the copy constructor and copy assignment operator with their default behaviour.
class MyClass {
public:
    MyClass(const MyClass& other) = default; // Compiler-generated copy constructor
    MyClass& operator=(const MyClass& other) = default;  // Compiler-generated copy assignment operator
};
  1. Destructor: The default keyword can be used to explicitly define the destructor as the default destructor generated by the compiler.
class MyClass {
public:
    ~MyClass() = default;  // Compiler-generated destructor
};

Using the default keyword can be useful when you want to explicitly indicate that the default behaviour is desired, or when you want to make it clear that a default implementation is being used.

Note that the default keyword can only be used with special member functions, and it cannot be used for regular member functions or non-member functions.

What next?

This document has tried to provide explanations and examples to understand the basics of operator overloading, its need and contribution to writing good programs and its design language. It has left out redundant information such as describing the overloading of all the operators such as logical, comparison and others. Overloading those operators is also pretty much similar and should be fairly straightforward once you have understood the explanation and examples given in this document thoroughly.

It is strongly encouraged to practise overloading all the operators to get a good grip over the concept.

Exercises

Example 1: Fraction class

Create a Fraction class that contains two integer data members, numerator and denominator. Overload the following arithmetic operators using member functions: +, -, *, /, +=, -=, *=, /=, ++ (prefix), -- (prefix), ++ (postfix), -- (postfix). Also overload the stream insertion and extraction operators.

Example Usage:

Fraction f1(3, 4);
Fraction f2(1, 2);
Fraction f3 = f1 + f2;
std::cout << f3 << std::endl; // Output: 5/4

Fraction f4 = f1 * f2;
std::cout << f4 << std::endl; // Output: 3/8

f1 += f2;
std::cout << f1 << std::endl; // Output: 5/4

f1--;
std::cout << f1 << std::endl; // Output: 1/4

Exercise 2: Matrix

Create a Matrix class that contains a dynamic 2D array of integers and two integer data members, rows and columns. Overload the following arithmetic operators using member functions: +, -, *, /, +=, -=, *=, /=, ++ (prefix), -- (prefix), ++ (postfix), -- (postfix). Also overload the stream insertion and extraction operators.

Example Usage:

Matrix m1(2, 2);
Matrix m2(2, 2);
m1[0][0] = 1;
m1[0][1] = 2;
m1[1][0] = 3;
m1[1][1] = 4;
m2[0][0] = 4;
m2[0][1] = 3;
m2[1][0] = 2;
m2[1][1] = 1;

Matrix m3 = m1 + m2;
std::cout << m3 << std::endl; // Output: 5 5
                              //         5 5

Matrix m4 = m1 * m2;
std::cout << m4 << std::endl; // Output: 8 5
                              //         20 13

m1 += m2;
std::cout << m1 << std::endl; // Output: 5 5
                              //         5 5

m1--;
std::cout << m1 << std::endl; // Output: 4 4
                              //         4 4

Exercise 3

  1. Define a class Person with the following private member variables:
    • std::string name: stores the name of the person
    • int age: stores the age of the person
  1. Define the following public member functions for the Person class:
    • A constructor that takes in name and age as arguments and initialises the corresponding member variables.
    • A default constructor that initialises name to an empty string and age to 0.
    • void setName(const std::string& name): sets the name of the person.
    • std::string getName() const: returns the name of the person.
    • void setAge(int age): sets the age of the person.
    • int getAge() const: returns the age of the person.
  1. Overload the following comparison operators as member functions of the Person class:
    • bool operator==(const Person& other) const: returns true if the name and age of this Person object are the same as the name and age of the other Person object.
    • bool operator!=(const Person& other) const: returns true if the name and age of this Person object are not the same as the name and age of the other Person object.
    • bool operator<(const Person& other) const: returns true if the age of this Person object is less than the age of the other Person object. If the age is the same, then the name is used for comparison.
  1. Create several Person objects and test the overloaded comparison operators using if statements and cout statements. For example:
Person p1("Alice", 25);
Person p2("Bob", 30);

if (p1 == p2) {
    std::cout << "p1 and p2 are the same person\n";
} else {
    std::cout << "p1 and p2 are different people\n";
}

if (p1 < p2) {
    std::cout << "p1 is younger than p2\n";
} else {
    std::cout << "p1 is older than or the same age as p2\n";
}

Exercise 4

Create a class Person with the following private member variables:

  • name of type std::string
  • age of type int
  • income of type double

Define the following public member functions:

  • A default constructor that sets the name to an empty string, age to 0, and income to 0.
  • A constructor that takes in three parameters (name, age, income) and initialises the member variables accordingly.
  • Overload the == operator to compare two Person objects for equality based on all three member variables.
  • Overload the < operator to compare two Person objects based on their age.
  • Overload the && operator to return true if both Person objects have an income greater than 50000, and false otherwise.
  • Overload the || operator to return true if at least one Person object has an age greater than 50, and false otherwise.

In main(), create several Person objects and test out the overloaded operators to ensure they are working as expected.

Interview questions

The following are a finite set of possible interview questions that could be asked from operator overloading. The answer to some of the following interview questions might not be in this document but as said earlier in the “What’s next” section, this document provides enough data to streamline the understanding of the operator overloading. Answering the questions in this section will also help you broaden your understanding on this concept. The following are the questions:

  1. What is operator overloading, and why is it useful?
  2. What are the advantages and disadvantages of overloading operators using member functions vs global functions?
  3. Which operators cannot be overloaded in C++?
  4. What are the basic rules of operator overloading in C++?
  5. Can you explain the difference between the copy constructor and the assignment operator in C++?
  6. Can you provide an example of overloading the assignment operator for deep copying?
  7. How can you overload arithmetic operators in C++?
  8. What is the difference between overloading the increment operator as a prefix vs postfix operator?
  9. Can you explain how to overload stream insertion and extraction operators in C++?
  10. What is the syntax for overloading the subscript operator in C++?
  11. How can you overload the function call operator in C++?
  12. What is the difference between the new and new[] operators in C++, and how can you overload them?
  13. How can you overload the comma operator in C++?
  14. Can the scope of an overloaded operator be limited to a particular class?
  15. Can you overload the ternary conditional operator in C++?
  16. What is the role of the const keyword in operator overloading?
  17. Can you overload the new and delete operators in C++?
  18. What is the difference between the unary and binary operators in C++?
  19. Can you overload the type conversion operator in C++?
  20. Can you provide an example of overloading the comparison operators to compare two objects of a user-defined type?
  21. How can you overload the member selection operator to access a specific member of a user-defined type?
Categories
Computer Science / Information Technology Language: C++

Object Oriented Programming using C++

Introduction

To understand what is the need of object oriented programming, first we need to understand what are the limitations of structural/procedural oriented programming.

  • Functions need to know about the structure of the data. If the structure of the data changes, many functions must be changed.
    • Suppose we have a program with hundreds of functions and many of those functions accept data with a specific data type as argument.
    • If the structure or format for that data structure being passed around changes, then this requires changes in all of the functions written to support the new format of the data.
    • This can have a domino effect in the program and the amount of effort needed to change and then test all of the updates would be huge.
  • As the programs get larger, they become more:
    • Difficult to understand since the number of connections in the program becomes very hard to trace.
    • Difficult to maintain.
    • Difficult to extend.
    • Difficult to debug.
    • Difficult to reuse.
    • Fragile and easier to break.

Object oriented programming is a paradigm designed parallel to the real world. In structure oriented programming we would mainly focus on the data and the operations performed on the data. Whereas in object oriented programming, the main focus is on the entities to which the data is related to.

Object oriented programming is driven by classes and objects at its rudimentary level. Object oriented programming is all about modelling the software being written in terms of classes and objects. These classes and objects model real world entities in the problem domain being solved. A class is used to define a user defined data type and can be considered as a blueprint for maintaining the data.

The fact that the objects contain data and operations that work on that data is called encapsulation. And it’s an extension of the abstract data type in computer science.Now the data and the operations are together in the class where they belong and not spread across many functions, each passing and receiving data. Encapsulation is another mechanism used by object oriented programming to help us deal with complexity.

We can take the idea of encapsulation even further with information hiding. Object oriented programming allows us to hide implementation-specific logic in a class so that it’s available only within the class. This is a super powerful concept since it allows us to provide a public interface to the class and hide everything else. Now we know that the user of the class can’t mess with the implementation-specific code since they never knew about it in the first place! This makes the code easier to maintain, debug and extend.

Object-oriented programming is also all about reusability. Reusability is defined in terms of using something from one program in another program. Since classes are pretty much encapsulated units of data and operations, reusing them in other applications is much easier and since the class has already been tested, we get that benefit as well. This leads to faster development and better quality software.

Inheritance enables us to generate a new class from an existing class by adding or modifying only the elements required to create the new class. For instance, let’s assume we have an account class that represents a basic bank account, which includes a balance and regular withdrawal and deposit methods. Now, suppose we require a special trust account class with the following business logic: the account type is restricted to three withdrawals per year, and each withdrawal cannot exceed 10% of the current account balance. We could insert this logic into the existing account class and implement an enumeration or another discriminant to differentiate between the account types and execute the appropriate if-else logic. This approach is feasible, but it may become complex when several account variations exist, such as a money market account, a demat account, etc. Each account variation has its unique business logic, not just for withdrawal, but also for deposit. Consequently, the original account class could become an unwieldy and unmanageable behemoth.

In this case, we can create a new class from the existing account class and add the appropriate behaviour in the new class without modifying the original class at all. This approach enables reusability and the creation of polymorphic classes. Since all the derived classes that we produce are accounts, this approach is beneficial.

Differences between C and C++

CC++
Was developed by Dennis Ritchie between the years 1969 and 1973 at AT&T Bell Labs.Was developed by Bjarne Stroustrup in 1979.
Strictly structure-oriented / procedural language.Supports both structure oriented as well as an object-oriented language.
C is a subset of C++.C++ is a superset of C.
Contains 32 keywords.C++14 contains 90 keywords.
Function and operator overloading is not supported in C.Function and operator overloading is supported by C++.
Functions in C are not defined inside structures.Functions can be used inside a structure in C++.
Namespace features are not present.Namespace is used by C++, which avoids name collisions.
Reference variables are not supported.Reference variables are supported.
Virtual and friend functions are not supported.Virtual and friend functions are supported.
Does not support inheritance.C++ supports inheritance.
Instead of focusing on data, C focuses on method or process.C++ focuses on data instead of focusing on method or procedure.
C provides malloc(), calloc(), realloc() functions for Dynamic memory allocation, and free() for memory deallocation.C++ provides ‘new’ operator for memory allocation and ‘delete’ operator for memory deallocation.
Direct support for exception handling is not supported.Exception handling is supported.
cin() and cout() are not supported; instead scanf() and printf() functions are used for input/output in C.Almost all functions in C are supported in C++.
C structures don’t have access modifiers.C ++ structures have access modifiers.

Differences between object oriented and structure oriented programming

ParameterObject Oriented ProgrammingProcedural Programming
DefinitionUses classes and objects to create models based on the real world environment. It makes it easier to maintain and modify existing code as new objects are created inheriting characteristics from existing ones.Follows a step-by-step approach to break down a task into a collection of variables and routines (or subroutines) through a sequence of instructions.
ApproachThe concept of objects and classes is introduced and hence the program is divided into small chunks called objects which are instances of classes.The main program is divided into small parts based on the functions.
Access modifiersIn OOPs access modifiers are introduced namely as Private, Public, and Protected.No such modifiers are introduced in procedural programming.
SecurityDue to abstraction in OOPs data hiding is possible and hence it is more secure than POP.Procedural programming is less secure as compared to OOPs.
ComplexityDue to modularity, are less complex and hence new data objects can be created easily from existing objects making programs easy to modifyThere is no simple process to add data in procedural programming, at least not without revising the whole program.
Program divisionOOP divides a program into small parts and these parts are referred to as objects.Procedural programming divides a program into small programs and each small program is referred to as a function.
ImportanceOOP gives importance to data rather than functions or procedures.Procedural programming does not give importance to data. In POP, functions along with sequence of actions are followed.
InheritanceOOP provides inheritance in three modes i.e. protected, private, and publicProcedural programming does not provide any inheritance.
ExamplesC++, C#, Java, Python, etc. are examples of OOP languages.C, BASIC, COBOL, Pascal, etc. are examples of POP languages.

Advantages of object oriented programming:

  1. Improved software-development productivity: Object-oriented programming is modular, as it provides separation of duties in object-based program development. It is also extensible, as objects can be extended to include new attributes and behaviours. Objects can also be reused within and across applications. Because of these three factors – modularity, extensibility, and reusability – object-oriented programming provides improved software-development productivity over traditional procedure-based programming techniques.
  2. Improved software maintainability: For the reasons mentioned above, object oriented software is also easier to maintain. Since the design is modular, part of the system can be updated in case of issues without a need to make large-scale changes.
  3. Faster development: Reuse enables faster development. Object-oriented programming languages come with rich libraries of objects, and code developed during projects is also reusable in future projects.
  4. Lower cost of development: The reuse of software also lowers the cost of development. Typically, more effort is put into the object-oriented analysis and design, which lowers the overall cost of development.
  5. Higher-quality software: Faster development of software and lower cost of development allows more time and resources to be used in the verification of the software. Although quality is dependent upon the experience of the teams, object-oriented programming tends to result in higher-quality software.

Limitations of object oriented programming:

  1. Not a panacea
    1. Object oriented programming won’t make bad code better. It’s most likely to make it worse.
    2. Not suitable for all types of problems. There are problems that lend themselves well to functional-programming style, logic-programming style, or procedure-based programming style, and applying object-oriented programming in those situations will not result in efficient programs.
    3. Not everything decomposes to a class.
  2. Learning curve
    1. Usually a steeper learning curve. The thought process involved in object-oriented programming may not be natural for some people, and it can take time to get used to it. It is complex to create programs based on interaction of objects. Some of the key programming techniques, such as inheritance and polymorphism, can be challenging to comprehend initially.
    2. Many object oriented languages, many variations of object oriented concepts.
  3. Design
    1. Usually more up-front design is necessary to create good models and hierarchies.
  4. Programs can be
    1. Larger in size
    2. Slower: Object-oriented programs are typically slower than procedure-based programs, as they typically require more instructions to be executed.
    3. More complex

Classes and objects

Classes

  • Classes are blueprints from which objects are created. Classes are user defined types.
  • One of the goals in object oriented programming is to make the user defined types feel like they are part of the programming language. So when we create our classes, we want to be able to use them, just like we use basic data types such as integers, doubles.
  • Classes have attributes that are data. And also, classes have functions, which are called methods.
  • Classes can hide data and methods that are only used internally by the class. This is done using the private and public access modifiers.
  • The goal of a class is to provide a well-defined public interface that the user of the class can easily use to solve their problem.

Objects

  • Objects are created from classes and represent a specific instance of the class they are created from. So if I have an account class, I can create Prajwal’s account object that’s a specific instance of an account that models Prajwal’s account information.
  • We can have as many objects as we need. If we were to model a real banking application, we could have hundreds of thousands of account objects, each representing an individual instance of an account.
  • Each object has its own identity, and each can use the methods defined in the class.

Examples

Let’s assume only for the sake of this example, that int is a class and not a primitive data type.

int max_value;

int min_value;

The above statement tells us about the valid values max_value and min_value can hold. And it also tells us the operations we can perform on those variables such as addition, subtraction etc. As we have assumed int as a class and hence max_value and min_value are objects of this class. Notice that the max_value and min_value are the instances of int. They each have a value and an identity, perhaps a location in the memory.

Let’s consider the following two examples:

Account prajwal_account;

Account chandra_account;

Let’s assume that we’ve already written the Account class. Notice the syntax and how similar it is to the syntax of declaration of integer variables above. Account is a user-defined type. Hence, prajwal_account and chandra_account are the instances/objects of the Account class.

We have been using objects for a long time now. In the following example, marks is an object of class vector and address is an object of class string. vector and string classes provided by the standard C++ library.

std::vector<int> marks;

std::string address;

Class declaration

In C++, you can declare a class using the following syntax:

class ClassName {
  // member variables and functions
  // access specifier (public, private, protected) for member functions and variables
public:
  // public member functions and variables
private:
  // private member functions and variables
protected:
  // protected member functions and variables
};

The keyword class is used to indicate that a new class is being declared. ClassName is the name of the class you are defining, and the body of the class follows inside the curly braces {}.

You can declare member functions and variables inside the class, and they can be public, private, or protected. The access specifiers control the visibility of the member functions and variables. Public members are accessible from outside the class, private members are only accessible from within the class, and protected members are accessible from within the class and its derived classes.

Here’s an example of a class declaration in C++:

class MyClass {
public:
  int public_variable;
  void public_function();
private:
  int private_variable;
  void private_function();
protected:
  int protected_variable;
  void protected_function();
};

In this example, MyClass is the name of the class being declared. It has three types of member variables and functions, each with its own access specifier. The public variables and functions can be accessed from outside the class, the private variables and functions can only be accessed from within the class, and the protected variables and functions can be accessed from within the class and its derived classes. More on the access specifiers in upcoming sections.

Object creation

To create objects of a class in C++, you need to use the class name followed by the object name and parentheses. The parentheses can either be empty or contain arguments, depending on whether the class has a default constructor or not.

Here’s an example of how to create an object of the MyClass class that we declared earlier:

MyClass my_object; // creates an object named "my_object" of the "MyClass" class

In this example, we’re creating an object of the MyClass class using the default constructor, which doesn’t take any arguments. The object is named my_object.

The object creation syntax heavily depends on whether the corresponding class has an overloaded constructor or not. We shall deal with overloading and constructors in the upcoming sections.

Accessing class members

To access class members (variables or functions) via objects in C++, you use the dot operator (.) to separate the object name and the member name. The syntax for accessing a member variable or function is:

object_name.member_name; // to access a member variable
object_name.member_function(); // to call a member function

Here’s an example of how to access the member variables and functions of the MyClass class using an object:

class MyClass {
public:
  int public_variable;
  void public_function() {
    cout << "Hello from public_function!" << endl;
  }
private:
  int private_variable;
  void private_function() {
    cout << "Hello from private_function!" << endl;
  }
};

MyClass my_object;
my_object.public_variable = 42; // accessing a public member variable
my_object.public_function(); // calling a public member function

In this example, we’re creating an object named my_object of the MyClass class. We’re then accessing the public member variables public_variable and public_function using the dot operator (.). We set the value of the public_variable to 42, and we call the public_function, which outputs the message “Hello from public_function!” to the console.

Note that you can only access the public members of a class using an object. Private members can only be accessed from within the class itself, not from outside the class.

Class members access specifiers

In C++, access specifiers are keywords used to control the visibility and accessibility of class members (variables and functions) from outside the class. There are three access specifiers in C++: public, private, and protected.

Public Access Specifier:

Members declared as public are accessible from anywhere in the program, both inside and outside the class. Any function or object can access and modify public members of the class. Here’s an example of a class with a public member variable and function:

class MyClass {
public:
    int public_variable;
    void public_function() {
        cout << "This is a public function." << endl;
    }
};

In this example, public_variable and public_function are declared as public members of the MyClass class. These members can be accessed from any function or object in the program.

Private Access Specifier:

Members declared as private are only accessible from within the class itself. They cannot be accessed or modified from outside the class. This provides encapsulation, which is an important concept in object-oriented programming.

Here’s an example of a class with a private member variable and function:

class MyClass {
private:
    int private_variable;
    void private_function() {
        cout << "This is a private function." << endl;
    }
public:
    void set_private_variable(int x) {
        private_variable = x;
    }
};

In this example, private_variable and private_function are declared as private members of the MyClass class. These members can only be accessed from within the class itself, but the set_private_variable function is declared as a public member, which allows external code to set the value of private_variable.

Protected Access Specifier:

Members declared as protected are accessible from within the class and its derived classes. They cannot be accessed from outside the class or its derived classes.

Here’s an example of a class with a protected member variable and function:

class MyClass {
protected:
    int protected_variable;
    void protected_function() {
        cout << "This is a protected function." << endl;
    }
public:
    void set_protected_variable(int x) {
        protected_variable = x;
    }
};

class DerivedClass : public MyClass {
public:
    void derived_function() {
        protected_variable = 42; // accessing the protected member from a derived class
       protected_function(); // accessing the protected function from a derived class
    }
};

In this example, protected_variable and protected_function are declared as protected members of the MyClass class. These members can be accessed from within the class and its derived classes, but not from outside the class or its derived classes. The DerivedClass class is derived from MyClass and can access the protected members of MyClass.

To summarise, access specifiers in C++ provide control over the visibility and accessibility of class members. public members are accessible from anywhere in the program, private members are only accessible from within the class, and protected members are accessible from within the class and its derived classes.

Exercises on access specifiers

  1. Create a class called Rectangle with the following private member variables: length and width. Add public member functions to set and get the values of these member variables.
  2. Create a class called BankAccount with a private member variable called balance and a public member function called withdraw. The withdraw function should subtract the given amount from the balance variable and return true if the withdrawal was successful, or false if the withdrawal amount is greater than the balance.
  3. Create a class called Student with a private member variable called name and a public member function called printName. The printName function should print the name of the student to the console.
  4. Create a class called Animal with a protected member variable called age and a public member function called getAge. The getAge function should return the value of the age variable.

Implementing member methods

In C++, member methods are functions that are defined inside a class and are used to manipulate the class’s data members. They are sometimes referred to as member functions or methods.

To define a member method in C++, you must first declare it in the class definition using the following syntax:

class MyClass {
public:
    void myMethod(); // function declaration
};

In this example, myMethod is declared as a public member function of the MyClass class. It does not have any parameters and returns void.

To implement this method, you must define it outside of the class definition using the following syntax:

void MyClass::myMethod() {
    // implementation code here
}

Note that the method definition must include the class name followed by the scope resolution operator :: and the method name. This tells the compiler that you are defining a member function of the class, not a standalone function.

Within the method implementation, you can access the class’s data members and call other member methods using this keyword. For example:

class MyClass {
private:
    int myInt;
public:
    void setInt(int value) {
        this->myInt = value;
    }
    int getInt() {
        return this->myInt;
    }
};

In this example, the setInt and getInt methods are defined to set and retrieve the value of the private myInt member variable. The this keyword is used to refer to the current object, allowing you to access its data members and call its methods.

You can also pass parameters to member methods and return values from them, just like regular functions. For example:

class MyClass {
public:
    int addNumbers(int a, int b) {
        return a + b;
    }
};

In this example, the addNumbers method takes two int parameters and returns their sum. It can be called on an object of the MyClass class like this:

MyClass myObject;
int result = myObject.addNumbers(5, 10);

This will create an instance of MyClass called myObject and call its addNumbers method with the parameters 5 and 10. The resulting sum 15 will be returned and assigned to the result variable.

Overall, member methods are a powerful feature of C++ classes that allow you to encapsulate data and behaviour within a single object-oriented construct. By declaring and implementing these methods properly, you can write robust, efficient, and reusable code that solves a wide variety of programming problems.

Best practice: code separation

In C++, it’s common to separate the class declaration from the implementation by putting the declaration in a header file (often with a .h or .hpp extension) and the method definitions in a separate implementation file (often with a .cpp extension). This makes the code more modular and easier to maintain.

Here’s an example of how to separate a class into separate files:

MyClass.h:

class MyClass {
private:
    int myInt;
public:
    MyClass();
    void setInt(int value);
    int getInt();
};

In this header file, we have declared the MyClass class and its member methods setInt, getInt, and a constructor. Constructors will be dealt in great detail in upcoming sections. For simplicity, consider constructor as any other method in a class. Note that we only provide method declarations, not the full method implementations.

MyClass.cpp:

#include "MyClass.h"

MyClass::MyClass() {
    myInt = 0;
}

void MyClass::setInt(int value) {
    myInt = value;
}

int MyClass::getInt() {
    return myInt;
}

In this implementation file, we include the header file and provide the full implementations of the class methods declared in the header file. Note that we use the scope resolution operator :: to link the method definitions to the class.

main.cpp

#include <iostream>
#include "MyClass.h"

int main() {
    MyClass myObject;
    myObject.setInt(42);
    std::cout << myObject.getInt() << std::endl;
    return 0;
}

In the main driver file, we include the header file and use the MyClass class just like we would normally. Note that we don’t need to include the implementation file, because it’s already included in the header file.

To compile and run this program, we can use the following command:

g++ MyClass.cpp main.cpp -o myProgram
./myProgram

This will compile both source files and link them together to create an executable called myProgram. When we run it, it will create an instance of MyClass, set its integer value to 42, and print it out to the console.

Separating class declaration from implementation in this way is a common practice in C++ programming, as it helps to keep the code organised and maintainable. By using header files for class declarations and implementation files for method definitions, you can write clear and concise code that is easy to read, modify, and reuse.

Exercises

  1. Create a Rectangle class with private member variables width and height, and public methods setWidth, setWidth, getArea, and getPerimeter. Implement the methods to set and retrieve the values of the width and height, and calculate the area and perimeter of the rectangle.
  2. Create a Person class with private member variables name and age, and public methods setName, setAge, getName, and getAge. Implement the methods to set and retrieve the values of the name and age, and validate the input (e.g. age must be a positive integer).
  3. Create a BankAccount class with private member variables accountNumber, balance, and interestRate, and public methods deposit, withdraw, getBalance, and calculateInterest. Implement the methods to deposit and withdraw money from the account, retrieve the current balance, and calculate the interest earned based on the current balance and interest rate.
  4. Create a Student class with private member variables name, id, grades (an array of doubles), and public methods setName, setId, setGrades, getName, getId, getGrades, and getAverageGrade. Implement the methods to set and retrieve the values of the name and id, set the grades, retrieve the grades, and calculate the average grade.
  5. Create a Car class with private member variables make, model, year, speed, and public methods setMake, setModel, setYear, setSpeed, getMake, getModel, getYear, getSpeed, accelerate, and brake. Implement the methods to set and retrieve the values of the make, model, year, and speed, and increase or decrease the speed by a certain amount when accelerating or braking.

Constructors

In C++, constructors are special member functions that are called automatically when an object of a class is created. Constructors are responsible for initialising the member variables of the class to specific values, and they can also perform additional initialization tasks if needed. Constructors have the same name as the class and no return type, not even void.

Here’s an example to illustrate how constructors work:

class Rectangle {
public:
    Rectangle(); // Constructor declaration

    int getWidth() const;
    int getHeight() const;
    void setWidth(int width);
    void setHeight(int height);

private:
    int width_;
    int height_;
};

Rectangle::Rectangle() {
    width_ = 0;
    height_ = 0;
}

int Rectangle::getWidth() const {
    return width_;
}

int Rectangle::getHeight() const {
    return height_;
}

void Rectangle::setWidth(int width) {
    width_ = width;
}

void Rectangle::setHeight(int height) {
    height_ = height;
}

In this example, we have defined a class called Rectangle with private member variables width_ and height_, and public methods getWidth, getHeight, setWidth, and setHeight to retrieve and modify these values. We have also declared a constructor for the class called Rectangle, which takes no parameters.

In the implementation of the constructor, we have initialised the width_ and height_ member variables to 0. This means that whenever an object of the Rectangle class is created, its width_ and height_ will be set to 0 by default.

Now, let’s say we want to create an object of the Rectangle class and set its width and height to specific values. We can do this as follows:

Rectangle rect; // Create a Rectangle object using the default constructor/ zero constructor
rect.setWidth(10); // Set the width to 10
rect.setHeight(20); // Set the height to 20

In this code, we have created an object of the Rectangle class called rect using the default constructor. We have then set its width_ and height_ member variables to 10 and 20, respectively, using the setWidth and setHeight methods.

Constructors can also take parameters, which can be used to initialise member variables to specific values. Here’s an example:

class Person {
public:
    Person(const std::string& name, int age);

    const std::string& getName() const;
    int getAge() const;

private:
    std::string name_;
    int age_;
};

Person::Person(const std::string& name, int age)
    : name_(name), age_(age) {
}

const std::string& Person::getName() const {
    return name_;
}

int Person::getAge() const {
    return age_;
}

In this example, we have defined a class called Person with private member variables name_ and age_, and public methods getName and getAge to retrieve these values. We have also declared a constructor for the class called Person, which takes two parameters: a const std::string& representing the person’s name, and an int representing the person’s age. Constructors that take parameters are called Parameterised constructors.

The following syntax shows how to pass arguments to constructor:

ClassName objName (arg1, arg2, …, argn);		// Local object
ClassName *objName = new ClassName(arg1, arg2, …, argn);	// Object pointer

Note that if we define any other constructor for the class, then the default constructor will not be automatically generated. In that case, if we want to create objects using the default constructor, we need to explicitly define it.

#include <iostream>

class A {
public:
    int x; // data member
    A(int a) { // parameterized constructor
        std::cout << "Parameterized constructor called" << std::endl;
        x = a; // initialize x with given value
    }
};

int main() {
    A a; // error: no matching function for call to 'A::A()'
    return 0;
}

In this example, we have defined a parameterized constructor for class A. Since we have defined a constructor, the compiler will not generate the default constructor. Therefore, when we try to create an object of class A without any arguments, we get a compilation error.

Constructor overloading

In C++, constructor overloading is a technique that allows us to define multiple constructors with different parameters in a class. This means that we can create objects of the same class in different ways, depending on the parameters we provide when creating the object.

Constructor overloading is useful when we want to create objects of a class in different ways. For example, suppose we have a Person class that has two member variables – name and age. We might want to create a Person object with just a name, or with just an age, or with both name and age. We can achieve this by overloading the constructor of the Person class with different parameter combinations.

Here’s an example:

#include <iostream>
#include <string>

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

    // Default constructor
    Person() {
        name = "Unknown";
        age = 0;
        std::cout << "Default constructor called" << std::endl;
    }

    // Constructor with name parameter
    Person(std::string n) {
        name = n;
        age = 0;
        std::cout << "Constructor with name parameter called" << std::endl;
    }

    // Constructor with age parameter
    Person(int a) {
        name = "Unknown";
        age = a;
        std::cout << "Constructor with age parameter called" << std::endl;
    }

    // Constructor with both name and age parameters
    Person(std::string n, int a) {
        name = n;
        age = a;
        std::cout << "Constructor with name and age parameters called" << std::endl;
    }
};

int main() {
    // Creating objects with different constructors
    Person p1;  // Default constructor called
    Person p2("John");  // Constructor with name parameter called
    Person p3(25);  // Constructor with age parameter called
    Person p4("Jane", 30);  // Constructor with name and age parameters called
    return 0;
}

In the above code, we have defined a Person class with four constructors. The first constructor is the default constructor, which initialises the name and age member variables to default values. The second constructor takes a name parameter and initialises the name member variable with the passed value, while the age member variable is set to a default value. The third constructor takes an age parameter and initialises the age member variable with the passed value, while the name member variable is set to a default value. The fourth constructor takes both name and age parameters and initialises the member variables with the passed values.

In the main() function, we create objects of the Person class using different constructors. We create an object p1 with the default constructor, an object p2 with the constructor that takes a name parameter, an object p3 with the constructor that takes an age parameter, and an object p4 with the constructor that takes both name and age parameters. Depending on the constructor used, the corresponding message is printed to the console.

Constructor overloading makes it easier and more flexible to create objects of a class, by providing different ways to initialise the member variables.

Destructors

In C++, a destructor is a member function of a class that is called when an object of that class is destroyed, typically when it goes out of scope or is explicitly deleted. A destructor has the same name as the class preceded by a tilde (~) symbol.

The purpose of a destructor is to perform cleanup tasks such as freeing dynamically allocated memory, closing files, or releasing other resources that the object has acquired during its lifetime.

Here is an example of a class with a destructor:

#include <iostream>

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

    ~MyClass() {
        std::cout << "Destructor called" << std::endl;
    }
};

int main() {
    MyClass obj; // create an object of MyClass
    // obj goes out of scope here and its destructor is called
    return 0;
}

In this example, the constructor of MyClass is called when an object of the class is created. The constructor simply outputs a message to the console. The destructor of MyClass is called automatically when the object goes out of scope at the end of the main function. The destructor also outputs a message to the console.

When a class has dynamically allocated memory or other resources, it is important to ensure that these resources are released properly. Here is an example of a class with a destructor that frees dynamically allocated memory:

#include <iostream>

class MyClass {
private:
    int* ptr;

public:
    MyClass() {
        ptr = new int[10]; // allocate memory
    }

    ~MyClass() {
        delete[] ptr; // free memory
    }
};

int main() {
    MyClass obj; // create an object of MyClass
    // obj goes out of scope here and its destructor is called
    return 0;
}

In this example, the constructor of MyClass allocates memory using the new operator. The destructor of MyClass frees the memory using the delete[] operator. This ensures that the memory allocated by the constructor is released when the object is destroyed.

Note that in C++, a class can have only one destructor, and it cannot be overloaded with different parameter lists. The destructor has no return type, and its parameters cannot be modified.

Order of invocation of constructors and destructors

Objects containing other objects

In C++, the order in which constructors and destructors are called depends on the order in which objects are created and destroyed. The general rule is that the constructor of an object is called before the constructor of any objects that it contains, and the destructor of an object is called after the destructor of any objects that it contains.

Let’s consider an example to illustrate this:

#include <iostream>

class A {
public:
    A() {
        std::cout << "A constructor called" << std::endl;
    }

    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
private:
    A a;

public:
    B() {
        std::cout << "B constructor called" << std::endl;
    }

    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    B b; // create an object of class B
    return 0;
}

In this example, we have two classes, A and B, where B contains an object of class A. In the main function, we create an object of class B named b.

The output of this program will be:

A constructor called
B constructor called
B destructor called
A destructor called

As you can see, the constructor of A is called first because it is a member of B. Next, the constructor of B is called. When the program ends and B goes out of scope, the destructor of B is called first, followed by the destructor of A.

It is important to note that the order of destruction is the reverse of the order of construction. This is because the destruction of objects follows the opposite path of construction. In the example above, a was constructed before b, but a was destroyed after b.

Multiple objects of same class

When we create multiple objects of the same class, the constructor and destructor are invoked in the order of creation and destruction of the objects respectively.

Let’s consider an example to illustrate this:

#include <iostream>

class A {
public:
    A() {
        std::cout << "A constructor called" << std::endl;
    }

    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

int main() {
    A a1; // create first object of class A
    A a2; // create second object of class A
    A a3; // create third object of class A
    return 0;
}

In this example, we have a class A with a constructor and destructor. In the main function, we create three objects of class A named a1, a2, and a3.

The output of this program will be:

A constructor called
A constructor called
A constructor called
A destructor called
A destructor called
A destructor called

As you can see, the constructor of A is called three times in the order of creation of the objects, and the destructor of A is called three times in the reverse order of creation of the objects.

This means that when the first object a1 is created, its constructor is called, followed by the constructors of a2 and a3 respectively. When the program ends and the objects go out of scope, the destructor of a3 is called first, followed by the destructors of a2 and a1 respectively.

So, the order of invocation of constructor and destructor of multiple objects of the same class is determined by the order of creation and destruction of the objects, respectively.

Exercises

  1. Write a C++ class Rectangle with two private data members length and width of type double. Implement a parameterized constructor that takes two arguments len and wid and initialises the data members with the given values. Also, implement a destructor that prints a message “Rectangle object destroyed”.
#include <iostream>

class Rectangle {
private:
    double length;
    double width;
public:
    Rectangle(double len, double wid) {
        length = len;
        width = wid;
    }
    ~Rectangle() {
        std::cout << "Rectangle object destroyed" << std::endl;
    }
};

int main() {
    Rectangle r(5, 10);
    return 0;
}
  1. Write a C++ class Person with two private data members name and age of type string and int respectively. Implement a default constructor that initialises the data members with default values “Unknown” and 0. Also, implement a parameterized constructor that takes two arguments n and a and initialises the data members with the given values. Also, implement a destructor that prints a message “Person object destroyed”.
#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;
public:
    Person() {
        name = "Unknown";
        age = 0;
    }
    Person(std::string n, int a) {
        name = n;
        age = a;
    }
    ~Person() {
        std::cout << "Person object destroyed" << std::endl;
    }
};

int main() {
    Person p1;
    Person p2("John", 30);
    return 0;
}
  1. Write a C++ class Circle with a private data member radius of type double. Implement a default constructor that initialises the radius to 0.0. Also, implement a parameterized constructor that takes one argument r and initialises the radius with the given value. Also, implement a function area that returns the area of the circle.
#include <iostream>

class Circle {
private:
    double radius;
public:
    Circle() {
        radius = 0.0;
    }
    Circle(double r) {
        radius = r;
    }
    double area() {
        return 3.14 * radius * radius;
    }
};

int main() {
    Circle c1;
    Circle c2(5);
    std::cout << "Area of c1: " << c1.area() << std::endl;
    std::cout << "Area of c2: " << c2.area() << std::endl;
    return 0;
}
  1. Write a C++ class Student with two private data members name and roll of type string and int respectively. Implement a default constructor that initialises the data members with default values “Unknown” and 0. Also, implement a parameterized constructor that takes two arguments n and r and initialises the data members with the given values. Also, implement a function display that displays the name and roll number of the student.
#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int roll;

public:
    // Default constructor
    Student() : name("Unknown"), roll(0) {}

    // Parameterized constructor
    Student(std::string n, int r) : name(n), roll(r) {}

    // Display method
    void display() {
        std::cout << "Name: " << name << "\nRoll: " << roll << std::endl;
    }
};

int main() {
    // Create default student object
    Student s1;
    s1.display(); // Output: Name: Unknown, Roll: 0

    // Create parameterized student object
    Student s2("John Doe", 123);
    s2.display(); // Output: Name: John Doe, Roll: 123
    return 0;
}

Constructor initialiser lists

In C++, constructor initialization lists provide a way to initialise the member variables of a class before the constructor body is executed. Initialization lists use a special syntax to initialise the members of the class, and can be used to initialise both primitive and non-primitive data types.

Constructor initialization lists are used in the constructor’s definition and are placed after the constructor’s parameters and before the constructor’s body. Here’s an example of a constructor initialization list:

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

public:
    // Constructor with initialization list
    Person(std::string n, int a) : name(n), age(a) {}
};

In this example, the Person class has two member variables, name and age. The constructor takes two parameters, n and a, and initialises the name and age member variables using an initialization list.

The syntax of an initialization list is a colon : followed by a comma-separated list of member variable names, each one followed by its initial value in parentheses. For example, in the above Person class, the initialization list : name(n), age(a) initialises the name member variable with the value of the n parameter, and the age member variable with the value of the a parameter.

Initialization lists can also be used to initialise non-primitive data types such as arrays, pointers, and other classes. Here’s an example:

class Rectangle {
private:
    int* dimensions;

public:
    // Constructor with initialization list
    Rectangle(int length, int width) : dimensions(new int[2]) {
        dimensions[0] = length;
        dimensions[1] = width;
    }

    // Destructor
    ~Rectangle() {
        delete[] dimensions;
    }
};

In this example, the Rectangle class has a member variable dimensions, which is a pointer to an integer array. The constructor initialises dimensions with a new integer array of size 2, and sets the first and second elements of the array to length and width, respectively. The destructor deallocates the memory allocated for the integer array.

Using constructor initialization lists is preferred over initialising member variables inside the constructor’s body, because it is more efficient and can prevent undefined behaviour when initialising const or reference member variables.

Delegating constructors

Delegating constructors are a feature introduced in C++11 that allow one constructor of a class to call another constructor of the same class to perform initialization tasks. This helps to avoid code duplication and simplify the code.

Delegating constructors are invoked using the colon (:) syntax and are placed before the constructor body. The syntax for delegating constructors is:

Classname(params) : Constructor_to_call(args)
{
    // constructor body
}

Here, Classname(params) is the constructor being defined, and Constructor_to_call(args) is the constructor being delegated to. The constructor being delegated to must have already been defined in the class before the constructor being defined. The delegated constructor is called before the constructor being defined and initialises the members of the class.

Let’s take an example to understand delegating constructors in detail:

#include <iostream>
using namespace std;

class MyClass {
  private:
    int num1;
    int num2;

  public:
    MyClass() : MyClass(0, 0) {
        cout << "Default Constructor" << endl;
    }

    MyClass(int n) : MyClass(n, 0) {
        cout << "Single Parameter Constructor" << endl;
    }

    MyClass(int n1, int n2) : num1(n1), num2(n2) {
        cout << "Two Parameter Constructor" << endl;
    }

    void display() {
        cout << "Number 1: " << num1 << endl;
        cout << "Number 2: " << num2 << endl;
    }
};

int main() {
    MyClass obj1;
    obj1.display();

    MyClass obj2(5);
    obj2.display();

    MyClass obj3(10, 20);
    obj3.display();

    return 0;
}

In this example, we have defined three constructors for the MyClass class. The first constructor is the default constructor that initialises the data members with default values. The second constructor is a single-parameter constructor that takes an integer value and calls the two-parameter constructor with the given value and 0. The third constructor is a two-parameter constructor that initialises the data members with the given values.

Notice how the two-parameter constructor is being called in the delegating constructor syntax in both the default and single-parameter constructors. This helps to avoid code duplication and simplify the code.

Output:

Two Parameter Constructor
Default Constructor
Number 1: 0
Number 2: 0
Two Parameter Constructor
Single Parameter Constructor
Number 1: 5
Number 2: 0
Two Parameter Constructor
Number 1: 10
Number 2: 20

In the output, we can see that the constructors are being called in the correct order, and the correct values are being passed to the constructors. This is an example of how delegating constructors can simplify the code and avoid code duplication.

Default arguments in Constructors

Default arguments in constructors allow us to define parameters with default values. These default values are used when no argument is provided for that parameter. The syntax for defining default arguments in constructors is the same as in functions.

Consider the following example:

class Person {
private:
    std::string name;
    int age;
public:
    Person(std::string name = "Unknown", int age = 0) {
        this->name = name;
        this->age = age;
    }
    void display() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    // Using default values
    Person p1; // name = "Unknown", age = 0

    // Providing values for all parameters
    Person p2("John", 25); // name = "John", age = 25

    // Providing value for one parameter, using default value for the other
    Person p3("Alice"); // name = "Alice", age = 0

    return 0;
}

In this example, the Person class has a constructor with two parameters: name and age. Both parameters have default values of “Unknown” and 0, respectively. This means that if no arguments are provided when creating a Person object, the object will be initialised with these default values.

The rules for default arguments in constructors are as follows:

  1. Default arguments must be specified in the declaration of the constructor in the class definition, not in the implementation.
  2. If a constructor has multiple parameters, default arguments can only be specified for the trailing parameters.
  3. Default arguments can be used in constructors that are also overloaded.

Here is an example that illustrates rules 2 and 3:

class Rectangle {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h = 0) {
        width = w;
        height = h;
    }
    Rectangle() : Rectangle(0) {}
    void display() {
        std::cout << "Width: " << width << ", Height: " << height << std::endl;
    }
};

int main() {
    // Using default values
    Rectangle r1; // width = 0, height = 0

    // Providing values for all parameters
    Rectangle r2(10, 20); // width = 10, height = 20

    // Providing value for one parameter, using default value for the other
    Rectangle r3(5); // width = 5, height = 0

    return 0;
}

In summary, default arguments in constructors provide a convenient way to initialise objects with default values, while still allowing for customization by providing arguments for the parameters.

Copy constructor

In C++, a copy constructor is a special constructor that is used to create a new object as a copy of an existing object of the same class. It is a member function of a class that has the same name as the class and takes a single argument of a const reference to the same class.

Syntax of declaring the copy constructor:

Type::Type(const Type &source);

The syntax for a copy constructor is:

ClassName(const ClassName &old_obj)
{
    // Code to copy the old object to new object
}

Here, old_obj is the existing object that is being copied, and ClassName is the name of the class. The copy constructor takes a reference to old_obj and creates a new object with the same data as old_obj.

The copy constructor is called automatically when an object is created from an existing object, such as:

ClassName obj1;  // Calling default constructor
ClassName obj2(obj1);  // Calling copy constructor

In the above example, obj2 is created using the copy constructor and is initialised with the values of obj1.

Here is an example of a copy constructor in action:

#include <iostream>
using namespace std;

class MyClass {
    int x, y;
  public:
    MyClass(int a, int b) {
        x = a;
        y = b;
    }
    MyClass(const MyClass &old_obj) {
        x = old_obj.x;
        y = old_obj.y;
    }
    void display() {
        cout << "x = " << x << ", y = " << y << endl;
    }
};

int main() {
    MyClass obj1(10, 20);
    MyClass obj2(obj1);  // Copy constructor called
    obj1.display();  // Output: x = 10, y = 20
    obj2.display();  // Output: x = 10, y = 20
    return 0;
}

In the above example, MyClass has a copy constructor that takes a reference to an existing object and initialises the new object with the same values. The display function is used to output the values of the data members.

When obj2 is created using obj1 as the argument, the copy constructor is called and a new object is created with the same values as obj1. The display function is then called on both objects to show that they have the same values.

It is important to note that if a copy constructor is not defined explicitly, the compiler generates a default copy constructor, which performs a shallow copy of the data members. This may not be sufficient for classes that contain dynamically allocated memory or have other complex data structures, which is why it is often necessary to define a copy constructor explicitly.

This is particularly important when an object contains dynamically allocated memory, such as arrays or pointers, as simply copying the memory address would result in both objects pointing to the same memory location.

To implement copying of dynamically allocated memory members in a copy constructor, we need to ensure that a deep copy is made. This means that we allocate new memory for the copy and copy the contents of the original memory into the new memory. Here is an example:

#include <iostream>

using namespace std;

class MyClass {
private:
    size_t size;
public:
    int* ptr;
    // Constructor
    MyClass(int s) {
        size = s;
        ptr = new int[size];
    }
    // Copy constructor
    MyClass(const MyClass& obj) {
        size = obj.size;
        ptr = new int[size];
        for (int i = 0; i < size; i++) {
            ptr[i] = obj.ptr[i];
        }
    }
    size_t getSize() { return size; }
    int getPtr(size_t idx) { return ptr[idx]; }
    // Destructor
    ~MyClass() {
        delete[] ptr;
    }
};

int main() {
    MyClass obj1(5); // Create object with size 5
    obj1.ptr[0] = 1;
    obj1.ptr[1] = 2;
    obj1.ptr[2] = 3;
    obj1.ptr[3] = 4;
    obj1.ptr[4] = 5;
    
    MyClass obj2 = obj1; // Copy object
    obj2.ptr[0] = 10; // Modify copy
    obj2.ptr[1] = 20;
    
    // Display original and copy
    for (int i = 0; i < obj1.getSize(); i++) {
        cout << obj1.getPtr(i) << " ";
    }
    cout << endl;
    for (int i = 0; i < obj2.getSize(); i++) {
        cout << obj2.getPtr(i) << " ";
    }
    cout << endl;
    
    return 0;
}

Ouput:

1 2 3 4 5
10 20 3 4 5

Output if the copy constructor is commented out:

10 20 3 4 5
10 20 3 4 5

In this example, the MyClass class contains a dynamically allocated array ptr of size size. The copy constructor is defined to create a deep copy of the object by allocating a new array and copying the contents of the original array into the new array. The getSize() and getPtr() functions are defined to access the private member variables size and ptr, respectively.

In the main() function, we create an object obj1 with size 5 and assign values to its elements. We then create a copy of obj1 using the copy constructor, modify the copy, and display both objects. Since a deep copy is made, the modifications made to the copy do not affect the original object.

Copy constructors get called even when the objects are passed to the function or if the function is returning the object. This is because, when an object is communicated from or to the function, internally the object is being copied from one scope to the other, hence the copy constructor is called. Here’s an example that demonstrates the use of copy constructor when passing and returning objects from a function:

#include <iostream>
using namespace std;

class MyClass {
private:
    int* arr;
    int size;
public:
    MyClass(int s) {
        size = s;
        arr = new int[size];
        for(int i = 0; i < size; i++) {
            arr[i] = i;
        }
    }

    // Copy constructor
    MyClass(const MyClass& obj) {
        cout << "Copy constructor called" << endl;
        size = obj.size;
        arr = new int[size];
        for(int i = 0; i < size; i++) {
            arr[i] = obj.arr[i];
        }
    }

    ~MyClass() {
        delete[] arr;
    }

    void print() {
        for(int i = 0; i < size; i++) {
            cout << arr[i] << " ";
        }
        cout << endl;
    }
};

// Function that returns an object of MyClass
MyClass func(MyClass obj) {
    obj.print();
    cout << "Returning the object back to caller" << endl;
    return obj;
}

int main() {
    MyClass obj1(5);
    
    // Pass obj1 to func
    cout << "Passing the object to the function" << endl;
    MyClass obj2 = func(obj1);
    
    return 0;
}

Output:

Passing the object to the function
Copy constructor called
0 1 2 3 4 
Returning the object back to caller
Copy constructor called

In this example, we have a MyClass that dynamically allocates an array of integers of size size in the constructor. We also have a copy constructor to deep copy the array. In the main function, we first create an object obj1 of MyClass with size 5 and print its contents using the print function.

Next, we pass obj1 to the func function. Since the parameter of func is an object of MyClass, a copy constructor is called to create a new object inside the function. The function then prints the contents of this new object and returns it.

Finally, we create another object obj2 and assign it the returned object of func. Since the copy constructor is called again to create obj2, the dynamically allocated memory is deep copied to the new object. We then print the contents of obj2.

In this way, we can see that the copy constructor is called both when passing and returning objects from a function.

A copy constructor can also have an initialiser list. Here is an example of the same:

class Car {
  private:
    string model;
    int year;

  public:

    // Copy constructor with initializer list
    Car(const Car& otherCar) : model{otherCar.model}, year{otherCar.year} {
        cout << "Copy constructor called." << endl;
    }
};

The above example basically pics up the attributes from the source object and assigns it to the attributes of the assigned object.

Best practices

  1. Provide a copy constructor when your class has raw pointer members.
  2. Provide the copy constructor with a const reference parameter.
  3. Use STL classes as they already provide copy constructors.
  4. Avoid using raw pointer data members if possible.

Shallow copying with copy constructor

Shallow copying is a type of object copying where only the values of the data members are copied from the source object to the destination object. This means that if the data member is a pointer, then only the address of the memory location pointed to by the pointer is copied, not the contents of the memory location itself. As a result, both the source and destination objects will be pointing to the same memory location, which can lead to issues like memory leaks and data corruption.

When using the copy constructor for shallow copying, the default copy constructor provided by the compiler is sufficient. Here’s an example:

#include <iostream>
#include <cstring>

class Person {
public:
    char* name;
    int age;
    Person(const char* n, int a) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
        age = a;
    }

    ~Person() {
        delete[] name;
    }

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

int main() {
    Person p1("John", 30);
    Person p2 = p1; // using default copy constructor

    p1.display(); // output: Name: John, Age: 30
    p2.display(); // output: Name: John, Age: 30

    // changing name of p1 will also change name of p2
    p1.name[0] = 'M';

    p1.display(); // output: Name: Mon, Age: 30
    p2.display(); // output: Name: Mon, Age: 30

    return 0;
}

In this example, the Person class has a char* data member name, which is dynamically allocated using the new operator in the constructor. When the default copy constructor is used to copy p1 to p2, the pointer name is copied, but not the contents of the memory location it points to. This means that both p1 and p2 are pointing to the same memory location, and any changes made to name in one object will affect the other object as well. This can be seen when we change the first character of name in p1 to ‘M’, and then display both objects using the display() function. Both objects have the same name “Mon”, which is the modified name of p1. This is an example of shallow copying.

But there is a problem with shallow copy which might crash the program written. The problem is explained in the following example: Consider a class named Person which has a dynamic member name of type char*. The Person class has a constructor which allocates memory for name using new and a destructor which frees the allocated memory using delete. Here’s the class definition:

#include <cstring>

class Person {
private:
    char* name;

public:
    Person(const char* n) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }

    Person(const Person& p) {
        name = p.name; // Shallow copy
    }

    ~Person() {
        delete[] name;
    }

    const char* getName() const {
        return name;
    }
};

In the Person copy constructor, we are performing a shallow copy of the name member. This means that both the original object and the new object will have the same value for the name pointer, and they will both point to the same memory location.

Now let’s write a function named printPerson which takes a Person object by value and prints its name:

void printPerson(Person p) {
    std::cout << "Name: " << p.getName() << std::endl;
}

Notice that printPerson takes a Person object by value, which means that a copy of the original object will be made when the function is called.

Now let’s create a Person object and call printPerson with it:

int main() {
    Person p1("Alice");
    printPerson(p1);
    return 0;
}

In this code, we are creating a Person object p1 with the name “Alice”, and then passing it to the printPerson function. Remember that printPerson takes a Person object by value, so a copy of p1 will be made. Since we are performing a shallow copy in the copy constructor, both the original object p1 and the copy created for the printPerson function will have the same name pointer, pointing to the same memory location.

Now let’s look at what happens when the printPerson function returns. When a function returns, all the variables created inside the function go out of scope and their destructors are called. This means that the destructor of the copy created for the printPerson function will be called, and it will free the memory pointed to by the name pointer. However, the original object p1 still has a name pointer pointing to that memory location, even though it has already been freed by the destructor of the copy. This is a problem, because the memory has already been freed and accessing it can cause undefined behaviour.

In summary, the problem with shallow copying is that if multiple objects have pointers to the same dynamically allocated memory, deleting the memory once can cause problems when the other objects try to access that memory.

Exercises

Exercise 1: Copy constructor for a class with dynamic memory allocation

Create a class named MyArray which has a dynamically allocated integer array as a member variable. Implement the following methods:

  1. A default constructor which initialises the array with a size of 0.
  2. A parameterized constructor which takes an integer value n and initialises the array with the given size.
  3. A copy constructor which creates a new instance of MyArray and copies the contents of the original array to the new array.
  4. A destructor which frees the memory allocated to the array.

Example usage:

MyArray a(5);  // create an array of size 5
MyArray b = a; // create a copy of a

Exercise 2: Initializer list for a class with non-primitive data members

Create a class named Person which has two string member variables: name and address. Implement the following methods:

  1. A default constructor which initialises the name and address variables with empty strings.
  2. A parameterized constructor which takes two string values and initialises the name and address variables with the given values.
  3. A copy constructor which creates a new instance of Person and copies the name and address variables from the original instance.
  4. A destructor which does nothing.

Use an initializer list to initialise the member variables in the constructors.

Example usage:

Person p1("John", "123 Main St.");
Person p2 = p1; // create a copy of p1

Exercise 3: Delegating constructors

Create a class named Rectangle which has two member variables: length and width. Implement the following methods:

  1. A default constructor which initialises length and width to 0.
  2. A parameterized constructor which takes two integer values and initialises length and width with the given values.
  3. A constructor which takes a single integer value and delegates to the parameterized constructor, setting both length and width to the given value.
  4. A destructor which does nothing.

Example usage:

Rectangle r1(5, 10); // create a rectangle with length 5 and width 10
Rectangle r2(7);     // create a square with length 7

Move constructors

Move semantics are one of the most powerful features introduced in C++ 11. It is quite complicated hence we shall try to dissect the topic one step at a time. First let’s revisit lvalues and rvalues. In C++, an expression refers to a piece of code that produces a value. There are two types of expressions in C++: lvalues and rvalues.

Lvalues and rvalues

An lvalue refers to an object that has a name, a memory address, and a value. An lvalue can be assigned a value or passed as an argument to a function. For example:

int x = 10; // 'x' is an lvalue
int y = x; // 'x' is an lvalue, 'y' is an lvalue

In the above example, x and y are lvalues because they are objects with names, memory addresses, and values.

On the other hand, an rvalue refers to an object that has a value but no name or memory address. An rvalue cannot be assigned a value or passed as an argument by non-const reference. For example:

int x = 10; // '10' is an rvalue
int y = x + 5; // '(x + 5)' is an rvalue, 'y' is an lvalue

In the above example, 10 and (x + 5) are rvalues because they are objects with values but no names or memory addresses. y is an lvalue because it is an object with a name, a memory address, and a value.

Lvalue references and rvalue references

An lvalue reference is a reference to an lvalue object. It is denoted by the & symbol. The main use case of lvalue references is to modify the object referred to by the reference. Here is an example of an lvalue reference:

int x = 5;
int& ref = x;
ref = 10; // changes the value of x to 10

In this example, ref is an lvalue reference to the int object x. When we assign 10 to ref, it modifies the value of x as well.

On the other hand, an rvalue reference is a reference to an rvalue object. It is denoted by the && symbol. The main use case of rvalue references is to enable move semantics in C++. Here is an example of an rvalue reference:

int&& rref = 5;
int x = 6;
int&& rref1 = x; // Error

In this example, rref is an rvalue reference to the int object 5.

Lvalues and rvalues as function parameters

A function parameter can be declared as either an lvalue reference or an rvalue reference. This allows functions to distinguish between lvalues and rvalues passed as arguments. Consider the following example:

#include <iostream>

void func(int& lref) {
    std::cout << "lvalue reference: " << lref << std::endl;
}

void func(int&& rref) {
    std::cout << "rvalue reference: " << rref << std::endl;
}

int main() {
    int a = 5;
    int& lref = a;
    func(lref); // calls lvalue reference version of func
    func(10); // calls rvalue reference version of func
    return 0;
}

Output:

lvalue reference: 5
rvalue reference: 10

In this example, we have two versions of the func function – one that takes an lvalue reference parameter and one that takes an rvalue reference parameter. In the main function, we create an integer variable a and a reference lref to a. We then call func with lref as an argument, which will call the lvalue reference version of func. Finally, we call func with the integer literal 10 as an argument, which will call the rvalue reference version of func.

Move constructor

Sometimes copy constructors are called many times automatically due to the copy semantics of C++. Copy constructors doing deep copying can have a significant performance bottleneck. C++11 introduced move semantics and the move constructor. Move constructor moves an object rather than copying it. 

In C++, a move constructor is a special constructor that allows efficient transfer of resources (such as memory) from a temporary object to a new object. It is used to implement move semantics, which improve the performance of certain operations by avoiding unnecessary copying of data.

When an object is moved, the resources owned by the object (such as memory allocated on the heap) are transferred to the new object, leaving the original object in a “valid but unspecified” state. This can be more efficient than copying the data, especially for large objects or when copying is expensive.

The syntax for a move constructor is similar to that of a copy constructor, but it uses an rvalue reference parameter (denoted by &&) instead of a const reference parameter:

class MyClass {
public:
    // Move constructor
    MyClass(MyClass&& other) {
        // Transfer resources from 'other' to 'this'
        // ...
    }
};

Here’s an example of how a move constructor can be used to improve the performance of a function that returns a large object:

class LargeObject {
public:
    // Constructor that allocates a large amount of memory
    LargeObject(size_t size) {
        data_ = new char[size];
    }
    
    // Move constructor
    LargeObject(LargeObject&& other) {
        data_ = other.data_;
        other.data_ = nullptr;
    }
    
    // Destructor
    ~LargeObject() {
        delete[] data_;
    }
    
private:
    char* data_;
};

// Function that returns a large object by value
LargeObject createLargeObject() {
    LargeObject obj(1000000);
    return obj;  // Move-constructs a new object from 'obj'
}

int main() {
    LargeObject obj = createLargeObject();  // Move-constructs 'obj' from temporary object
    // ...
}

In this example, the createLargeObject() function returns a LargeObject by value. Without a move constructor, this would result in a copy of the LargeObject being made when the function returns, which could be expensive for large objects. With a move constructor, the new object can be constructed by transferring the memory owned by the temporary object returned by createLargeObject(), resulting in better performance.

Exercise 1: Implement a class with a move constructor

Write a C++ class Person that has a move constructor. The class should have the following private data members:

  • name of type std::string
  • age of type int

The class should have the following public member functions:

  • a constructor that takes name and age as arguments and initialises the corresponding data members
  • a move constructor that moves the name and age from the source object to the destination object
  • a getName function that returns the name of the person
  • an getAge function that returns the age of the person

Exercise 2: Implement a class with a move constructor that moves a dynamically allocated array

Write a C++ class IntArray that has a move constructor. The class should have the following private data members:

  • size_ of type int, the size of the dynamically allocated array
  • data_ of type int*, a pointer to the dynamically allocated array

The class should have the following public member functions:

  • a constructor that takes size as an argument and dynamically allocates an array of size size
  • a destructor that frees the dynamically allocated array
  • a move constructor that moves the size and data from the source object to the destination object
  • a getSize function that returns the size of the array
  • an getElement function that takes an index i as argument and returns the i-th element of the array

The `this` pointer

In C++, the this pointer is a pointer to the current object. It is an implicit parameter that is passed to all non-static member functions of a class. The this pointer is used to refer to the calling object inside a member function. It is particularly useful when there are multiple objects of the same class, and the member function needs to know which object it is currently operating on. Here is an example of how the this pointer is used in a class called Person:

#include <iostream>
#include <string>

class Person {
public:
    Person(std::string name, int age) {
        this->name = name;
        this->age = age;
    }

    void printInfo() {
        std::cout << "Name: " << this->name << std::endl;
        std::cout << "Age: " << this->age << std::endl;
    }

private:
    std::string name;
    int age;
};

int main() {
    Person person1("John", 30);
    Person person2("Jane", 25);

    person1.printInfo();
    person2.printInfo();

    return 0;
}

In the above example, the this pointer is used to refer to the name and age members of the calling object inside the printInfo function. When person1.printInfo() is called, this will point to person1. Similarly, when person2.printInfo() is called, this will point to person2. Note that this is particularly of great importance when the function parameter and the member objects have the same name. In the above example, this can be seen in the constructor. The constructor has 2 parameters: name and age. The class Person has 2 member objects of the same name. The this pointer helps the compiler distinguish between the two.

The const keyword in context of classes

Consider a class named Person with two member variables: name which is a string and age which is unsigned short int. A constructor that initialises both of these attributes. The class contains a method named set_name() that accepts an argument: new_name that replaces the value of the current name attribute to that of the received new attribute.

class Person {
public:
    std::string name;
    int age;
    Person(std::string name, int age) {
        this->name = name;
        this->age = age;
    }
    void set_name(std::string new_name) {
        this->name = new_name;
    }
};

The following statement creates a const object of Person:

const Person p {"Prajwal", 32};

If set_name() is called from p to change name, the compiler will throw an error. Obviously this is done to prevent accidental modification of member objects of p. Let’s consider another method to print_name() that prints the name of the caller object. Consider the following definition of the function print_name():

void print_name() {
    std::cout << this->name << std::endl;
}

The above method would also make the compiler throw error.The reason is that there is a possibility that the object’s member values might get altered inside the method. The C++ compiler takes utmost care to prevent any accidental modification of the object’s member values. To solve this problem, we need to introduce const keyword to methods too which make them compatible to be called from const objects. The following is the syntax to do the same:

void print_name() const{
    std::cout << this->name << std::endl;
}

Note that it is not enough to use const keyword in both object creation and in the function. We need to make sure that we are actually not changing/modifying the object values inside of the methods to prevent any errors. Also, a const method can be called by a non-const object but the converse is not possible as we just saw.

Exercises

Exercise 1: Circle class

Create a class Circle with a private data member radius of type double. Implement a public member function setRadius to set the radius of the circle and a public member function getArea to calculate and return the area of the circle. Make sure that the getArea function is a const function as it does not modify the state of the object.

Solution

class Circle {
private:
    double radius;

public:
    void setRadius(double r) {
        radius = r;
    }

    double getArea() const {
        return 3.14159 * radius * radius;
    }
};

Exercise 2: Car class

Create a class Car with a private data member speed of type int. Implement a public member function drive that takes an int parameter time and increases the speed of the car by time. Implement a public member function getSpeed to return the current speed of the car. Make sure that the drive function is a const function as it does not modify the state of the object.

Solution

class Car {
private:
    int speed;

public:
    void drive(int time) {
        speed += time;
    }

    int getSpeed() const {
        return speed;
    }
};

Exercise 3: Person class

Create a class Person with private data members name and age of type string and int respectively. Implement a public member function getName to return the name of the person and a public member function getAge to return the age of the person. Make sure that both the getName and getAge functions are const functions as they do not modify the state of the object.

Solution

class Person {
private:
    string name;
    int age;

public:
    string getName() const {
        return name;
    }

    int getAge() const {
        return age;
    }
};

Exercise 4: Rectangle

Create a class Rectangle with private data members length and width of type double. Implement a public member function setDimensions to set the dimensions of the rectangle and a public member function getPerimeter to calculate and return the perimeter of the rectangle. Make sure that the getPerimeter function is a const function as it does not modify the state of the object.

Solution

class Rectangle {
private:
    double length;
    double width;

public:
    void setDimensions(double l, double w) {
        length = l;
        width = w;
    }

    double getPerimeter() const {
        return 2 * (length + width);
    }
};

Exercise 5: Book class

Create a class Book with private data members title and author of type string. Implement public member functions getTitle and getAuthor to return the title and author of the book respectively. Make sure that both the getTitle and getAuthor functions are const functions as they do not modify the state of the object.

Solution

class Book {
private:
    string title;
    string author;

public:
    string getTitle() const {
        return title;
    }

    string getAuthor() const {
        return author;
    }
};

static class members

A static member variable is a member variable that belongs to the class itself rather than to instances of the class. In other words, there is only one instance of a static member variable, regardless of how many objects of the class are created. A static member variable is declared using the static keyword inside the class definition, like this:

class MyClass {
public:
  static int myStaticMember;
  // ...
};

The static member variable can then be defined and initialised outside the class definition, like this:

int MyClass::myStaticMember = 0;

A static member function is a member function that is associated with the class rather than with individual objects of the class. Like a static member variable, a static member function is declared using the static keyword inside the class definition. A static member function can be called using the class name and the scope resolution operator, like this:

class MyClass {
public:
  static void myStaticFunction();
  // ...
};

void MyClass::myStaticFunction() {
  // ...
}

int main() {
  MyClass::myStaticFunction();
  return 0;
}

The main advantage of using static class members is that they can be accessed without creating an instance of the class. This is particularly useful for defining constants that are associated with the class, or for implementing utility functions that operate on the class. By using a static member variable or function, you can avoid the overhead of creating unnecessary objects, and you can make the code more readable by indicating that the variable or function is associated with the class as a whole rather than with individual objects.

Exercises: static members

Create a class named “Person” with the following private members:

  • A static integer member variable named “count”.
  • A string member variable named “name”.
  • An integer member variable named “age”.
  • The “Person” class should have the following public member functions:

A constructor that takes two parameters: a string “name” and an integer “age”. This constructor should increment the “count” static member variable by 1 each time it is called.

  • A destructor that decrements the “count” static member variable by 1 each time it is called.
  • A function named “getCount” that returns the value of the “count” static member variable.
  • A function named “getName” that returns the value of the “name” member variable.
  • A function named “getAge” that returns the value of the “age” member variable.

Create a program that creates several “Person” objects with different names and ages, and then displays the total count of “Person” objects using the “getCount” member function. Finally, display the names and ages of all the “Person” objects using the “getName” and “getAge” member functions.

Example code:

#include <iostream>
#include <string>

class Person {
private:
    static int count;
    std::string name;
    int age;
public:
    Person(std::string n, int a) {
        name = n;
        age = a;
        count++;
    }
    ~Person() {
        count--;
    }
    static int getCount() {
        return count;
    }
    std::string getName() {
        return name;
    }
    int getAge() {
        return age;
    }
};

int Person::count = 0;
int main() {
    Person p1("Alice", 25);
    Person p2("Bob", 30);
    Person p3("Charlie", 35);
    std::cout << "Total count of Person objects: " << Person::getCount() << std::endl;
    std::cout << "Names and ages of all Person objects:" << std::endl;
    std::cout << p1.getName() << " (" << p1.getAge() << ")" << std::endl;
    std::cout << p2.getName() << " (" << p2.getAge() << ")" << std::endl;
    std::cout << p3.getName() << " (" << p3.getAge() << ")" << std::endl;
    return 0;
}

Structs vs Classes

Both structs and classes are used to define custom data types, but there are some differences between them.

  1. Default Access Specifier: The default access specifier for members of a struct is public, whereas for a class, it is private.
  2. Inheritance: By default, inheritance of a struct is public, whereas for a class, it is private.
  3. Member Functions: A struct can have member functions defined inside it or outside it, whereas for a class, member functions are typically defined outside of the class declaration using the scope resolution operator (::).
  4. Usage: Structs are often used for simple data structures, where the emphasis is on the data itself rather than on the behaviour of the data. On the other hand, classes are often used for more complex data structures that have both data and behaviour.

Here’s an example to demonstrate the differences between struct and class:

#include <iostream>

// struct example
struct Person {
    std::string name;
    int age;
};

// class example
class Car {
private:
    std::string brand;
    std::string model;
public:
    void setBrand(std::string b) {
        brand = b;
    }
    void setModel(std::string m) {
        model = m;
    }
    void printInfo() {
        std::cout << "Brand: " << brand << "\nModel: " << model << std::endl;
    }
};
int main() {
    // struct example
    Person p1;
    p1.name = "John";
    p1.age = 25;

    std::cout << "Name: " << p1.name << "\nAge: " << p1.age << std::endl;

    // class example
    Car c1;
    c1.setBrand("Toyota");
    c1.setModel("Camry");

    c1.printInfo();

    return 0;
}

In this example, we define a Person struct and a Car class. We create an object of each type and set their member variables using their respective member functions. We then print out the member variables using std::cout. This example demonstrates how the syntax for defining and using structs and classes is similar, but the differences between them lie in their default access specifiers, inheritance, and usage.

Friends of a class

A friend of a class is a function or another class that is given special permission to access the private and protected members of the class. It is declared within the class definition and defined outside of the class definition. There are two types of friends:

  1. Function Friend: A non-member function can be declared as a friend of a class.
  2. Class Friend: A class can also be declared as a friend of another class. This allows the friend class to access the private and protected members of the other class.

Here’s an example to illustrate the concept of friend classes:

#include <iostream>

// forward declaration of class B
class B;

// class A
class A {
private:
    int x;

public:
    A() {
        x = 0;
    }

    void setX(int a) {
        x = a;
    }

    // declaration of friend function
    friend void sum(A obj1, B obj2);
};

// class B
class B {
private:
    int y;

public:
    B() {
        y = 0;
    }

    void setY(int b) {
        y = b;
    }

    // declaration of friend function
    friend void sum(A obj1, B obj2);
};

// definition of friend function
void sum(A obj1, B obj2) {
    std::cout << "Sum of x and y is " << obj1.x + obj2.y << std::endl;
}

int main() {
    A obj1;
    B obj2;

    obj1.setX(10);
    obj2.setY(20);

    sum(obj1, obj2);

    return 0;
}

In this example, class A and class B are two independent classes. The friend function sum is declared in both classes, allowing it to access the private data members of both classes. The sum function takes objects of class A and B as arguments and calculates the sum of their private data members x and y. The main function creates objects of class A and B, sets their private data members, and calls the sum function to print the sum of x and y.

In summary, friends of a class provide a way to share private and protected data between classes or functions. However, it should be used with caution as it can lead to a loss of encapsulation and increase coupling between classes.

Section exercise

Consider the following requirements for implementing a simple bank account management system:

  1. A BankAccount class needs to be defined that stores the following information:
    • Account number (integer)
    • Account holder name (string)
    • Account type (string)
    • Account balance (float)
  1. The BankAccount class should have the following member functions:
    • Constructor that takes account number, account holder name, account type and account balance as arguments and initialises the member variables.
    • Destructor that deallocates any dynamically allocated memory.
    • Setter and getter functions for all member variables.
    • Function to deposit money into the account.
    • Function to withdraw money from the account.
    • Function to display the account details.
  2. The BankAccount class should also have a static member variable that keeps track of the total number of accounts created.
  3. The BankAccount class should also have a friend function named transferMoney that transfers money from one bank account to another.
  4. The BankAccount class should also have a move constructor and a move assignment operator that transfers the ownership of dynamically allocated memory from one object to another.
  5. The BankAccount class should ensure that the account balance cannot be negative.
  6. The BankAccount class should also have a default constructor that sets all member variables to default values.
  7. The BankAccount class should use constructor initializer lists wherever possible.
  8. The BankAccount class should have a static member function that returns the total number of accounts created.
  9. The BankAccount class should have a const member function that displays the account details.

Implement the BankAccount class according to the above requirements and test it with a driver program that creates at least two BankAccount objects, transfers money between them, displays the account details, and displays the total number of accounts created. Use dynamic memory allocation wherever required.

Bonus: Implement the BankAccount class as a template class that can store account balance in different data types (float, double, int, etc.).

Categories
Computer Science / Information Technology Language: C++

Functions in C++

Please note that in this section, we shall be only looking at the extra features C++ provides than the ones which are provided by C. C++ supports all the features that C provides and provides extra features upon the ones C has. Please make sure that you have gone through the C notes on functions before going ahead with this section.

Functions

A function is a set of instructions that can perform a specific task. This aids in reducing the size of the program by calling and using the functions any number of times from different places in the program instead of rewriting the same set of instructions everywhere. The advantages of functions are reusability, modularity and increased simplicity.

Types of functions

Functions are broadly classified into 2 types:

  • Library functions: These functions come with the C++ library and are ready to be used in the programs without worrying about internal implementation. Eg: iostream, string etc
  • User-defined functions: A function defined by the user.

Library functions

C++ has many built-in libraries. One such library is the cmath library (similar to math.h in C programming). Inorder to use this library, one must include cmath. This can be done using the following statement:

#include <cmath>

The cmath library includes functions to perform mathematical operations. A detailed list of all the functions can be found in the following link: https://cplusplus.com/reference/cmath/
Some of the most commonly used functions are listed below along with information on how to use them:

Sl. NoFunctionParametersReturn ValueDescription
1pow()Base, exponentThe result of raising base to the power exponent.Returns base raised to the power exponent
2sqrt()+ve value whose square root is computed.Square root of the parameterReturns the square root of x.
3ceil()Value to round upThe smallest integral value that is not less than x (as a floating-point value).Rounds x upward, returning the smallest integral value that is not less than x.
4floor()Value to round down.The value of x rounded downward (as a floating-point value).Rounds x downward, returning the largest integral value that is not greater than x.
5round()Value to round.The value of x rounded to the nearest integral (as a floating-point value).Returns the integral value that is nearest to x, with halfway cases rounded away from zero.
6fabs()Value whose absolute value is returned.The absolute value of x. Always returns a floating point.Returns the absolute value of x: |x|.
7abs()Value whose absolute value is returned.The absolute value of x. Floating point or integer depending on the parameter.Returns the absolute value of x: |x|.

Example

#include <iostream>
#include <cmath>

int main ()
{
    std::cout << "abs (3.1416) = " << std::abs (3.1416) << std::endl;
    std::cout << "abs (-10.6) = "   << std::abs (-10.6) << std::endl;
    std::cout << "7 ^ 3 = "        << pow (7.0, 3.0) << std::endl;
    std::cout << "4.73 ^ 12 = "    << pow (4.73, 12.0) << std::endl;
    std::cout << "32.01 ^ 1.54 = " << pow (32.01, 1.54) << std::endl;

    double param, result;
    param = 1024.0;
    result = sqrt (param);
    std::cout << "sqrt(" << param << ") = " << result << std::endl;

    std::cout << "value\tround\tfloor\tceil\tfabs\tabs" << std::endl;
    std::cout << "-----\t-----\t-----\t----\t----\t---" << std::endl;
    std::cout <<  2.3 << "\t" << round( 2.3) << "\t" << floor( 2.3) << "\t" << ceil( 2.3) << "\t" << fabs( 2.3) << "\t" <<  abs( 2) << std::endl;
    std::cout <<  3.8 << "\t" << round( 3.8) << "\t" << floor( 3.8) << "\t" << ceil( 3.8) << "\t" << fabs( 3.8) << "\t" <<  abs( 3.8) << std::endl;
    std::cout <<  5.5 << "\t" << round( 5.5) << "\t" << floor( 5.5) << "\t" << ceil( 5.5) << "\t" << fabs( 5.5) << "\t" <<  abs( 5.5) << std::endl;
    std::cout << -2.3 << "\t" << round(-2.3) << "\t" << floor(-2.3) << "\t" << ceil(-2.3) << "\t" << fabs(-2.3) << "\t" <<  abs( -2.3) << std::endl;
    std::cout << -3.8 << "\t" << round(-3.8) << "\t" << floor(-3.8) << "\t" << ceil(-3.8) << "\t" << fabs(-3.8) << "\t" <<  abs( -3.8) << std::endl;
    std::cout << -5.5 << "\t" << round(-5.5) << "\t" << floor(-5.5) << "\t" << ceil(-5.5) << "\t" << fabs(-5.5) << "\t" <<  abs( -5.5) << std::endl;

    return 0;
}

Output:

abs (3.1416) = 3.1416
abs (-10.6) = 10.6
7 ^ 3 = 343
4.73 ^ 12 = 1.2541e+08
32.01 ^ 1.54 = 208.037
sqrt(1024) = 32
value   round   floor   ceil    fabs    abs
-----   -----   -----   ----    ----    ---
2.3     2       2       3       2.3     2
3.8     4       3       4       3.8     3
5.5     6       5       6       5.5     5
-2.3    -2      -3      -2      2.3     2
-3.8    -4      -4      -3      3.8     3
-5.5    -6      -6      -5      5.5     5

The cmath library given above is an example of built in libraries and the utilities that comes with them. The programmer can easily import them and exploit the functions they provide.

User defined functions

As indicated earlier, one must continue reading this section only if he/she has gone through the functions in C notes completely.
C++ provides much more flexibility to the programmer to define her own functions that come along with the features provided by C. We will see only the features provided by C++ over that came with C.

Default argument values

As we know, when a function is called, all arguments must be supplied. However, sometimes some arguments have the same values most of the time. C++ provides a feature called default argument values that tells the compiler to use default values if the arguments are not supplied.

Default values can be in the prototype or in the definition, not in both. However, it is recommended as a best practice to have it in the prototype. It is also mandatory for the default argument values to appear at the tail end of the parameter list.

There could be multiple default values, all of which must appear consecutively at the tail end of the parameter list.
The following is an example that show a program with no default arguments:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment);

// Function definition
double calc_profit(double revenue, double investment) {
    return revenue - investment;
}

int main () {
    double revenue = 5000;
    double investment = 2000;
    std::cout << "Profit: " << calc_profit(revenue, investment);
    return 0;
}

The same program could be written in the following way assuming that the investment will be always a same value of 2000:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment = 2000);

// Function definition
double calc_profit(double revenue, double investment) {
    return revenue - investment;
}
int main () {
    double revenue = 5000;
    std::cout << "Profit: " << calc_profit(revenue);
    return 0;
}

Note that the function calc_profit() can still accept investment value. When explicitly sent, the default argument of 2000 will be overridden with another value sent. This can be seen in the following example:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment = 2000);

// Function definition
double calc_profit(double revenue, double investment) {
    return revenue - investment;
}

int main () {
    double revenue = 5000;
    double investment = 200;
    std::cout << "Profit: " << calc_profit(revenue, investment);
    return 0;
}

In the examples so far, we have seen only one default parameter. However, we can have multiple default arguments for a function. The only condition is that the default argument list needs to be put at the tail end. Consider the following example:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment = 2000, double tax = 18);

// Function definition
double calc_profit(double revenue, double investment, double tax) {
    double profit = revenue - investment;
    return profit - profit * tax / 100;
}

int main () {
    double revenue = 5000;
    std::cout << "Profit: " << calc_profit(revenue) << std::endl;
    std::cout << "Profit: " << calc_profit(revenue, 200) << std::endl;
    std::cout << "Profit: " << calc_profit(revenue, 200, 20);
    return 0;
}

Output:

Profit: 2460
Profit: 3936
Profit: 3840

Function overloading

Function overloading is a feature given by C++ that enables programmers to have the same function name for 2 or more functions of the same program but with a different signature, that is, different parameter list in terms of order of the data types, return type or the number of the parameters in the function.

This feature enables programmers to write readable, intuitive code that is easy to maintain. This feature comes as a type of polymorphism: same name with different data types to execute similar behaviour. The compiler must be able to tell the functions apart based on the parameter lists and arguments supplied.

Consider the following simple example of adding two numbers. As C++ is a strictly typed language (meaning, the data type of a variable once declared cannot be changed over the course of execution), and there can be multiple permutations and combinations of addition of numbers such as:

  • Float + Float
  • Int + Int
  • Float + Int
  • Int + Float

Naming differently for functions defining logic for the above combinations is firstly cumbersome, and also abstraction is badly hit. Using the feature of function overloading, a single name can be used to define all the four functions and this is demonstrated in the following example:

#include <iostream>

using namespace std;

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

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

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

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

int main() {
    int a = 10, b = 11;
    float c = 10.5, d = 11.8;
    cout << add(c, d) << endl;
    cout << add(a, b) << endl;
    cout << add(c, b) << endl;
    cout << add(a, d) << endl;
    return 0;
}

Notice that the name of the function is the same but each of the functions are unique with respect to their return type and/or the parameter list. Having the exact same name, parameter list and order of the data types in the parameter list will give rise to a compile time error as there is no way for the compiler to tell the functions apart when called. Consider the following function declarations.

void display(float n);
void display(char ch);
void display(std::string s);
void display(std::vector<std::string> v);

Once these functions are implemented, one can call display and pass in the data. The compiler will check the data type of the argument and match it to one of the available overloaded display functions. If it can’t match it or if it can’t convert the argument to one that matches, then we get a compiler error.

There is one restriction to function overloading. The return type is not considered when the compiler is trying to determine which function to call. Consider the following example where there are two overloaded functions with no parameters but with different return type:

int fetch_value();
float fetch_value();

// Compile time error
cout << fetch_value();

Pass by reference

At times, we want to be able to change the actual parameter from within the function body. In order to achieve this we need to have access to the location/address of the actual parameter inside of the called function. There are two ways to achieve this: using pointers and using references. In this section, we will see how to do this by passing values by reference. The syntax to call by reference is as follows:

ret_type called_function (data_type &formal_parameter) {
    // Body of the function
}

ret_type caller_function () {
    called_function(actual_parameter)
}

Consider the following example:

#include <iostream>
using std::cout;

int square(int num) {
    return num * num;
}

int main() {
    int num {10};
    int sq_num = square(num);
    cout << num << " * " << num << " = " << sq_num;
    return 0;
}

In the above example, the formal parameter is a copy of the actual parameter. The function square() squares the variable num and returns it. The returned value is copied to the variable sq_num and then printed.

We might want the function to modify the actual parameter instead of making a copy of it and then operating on the copy. Let’s see how to do it using references in C++:

#include <iostream>
using std::cout;

void square(int &num) {
    num = num * num;
}

int main() {
    int num {10};
    int original_num {num};
    square(num);
    cout << original_num << " * " << original_num << " = " << num;
    return 0;
}

Notice that the formal parameter is an alias of the actual parameter and both of them refer to the same object in the memory. Any operation on the formal parameter inside of the function will reflect in the caller function too.

Pass by reference is advantageous for various reasons as follows:
It allows us to change the actual parameter if we need to.
We do not make a copy of the parameter, which could be large and take time. Pass by reference is far more efficient than passing by value.
Let’s see an example where we can swap two numbers.

#include <iostream>
using std::cout;

void swap(int &a, int &b) {
    a += b;
    b = a - b;
    a -= b;
}

int main() {
    int a {10}, b {11};
    cout << "Before swapping: a: " << a << " b: " << b << "\n";
    swap(a, b);
    cout << "After swapping: a: " << a << " b: " << b << "\n";
    return 0;
}

However, we need to be aware of potentially unwanted changes. Consider the following example:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;
using std::endl;

void print(vector <int> &v) {
    for (auto num: v)
        cout << num << " ";
    cout << endl;
}

int main() {
    vector <int> vec {1,2,3,4,5};
    print(vec);
    return 0;
}

In the above example, the function print() has absolute control over the vector and has all privileges to modify it. One needs to follow the least privileges principle: if the permission is not needed to perform an operation, the permission need not be given. The function print() need not have write permission. Hence it can receive the vector as a const vector to prevent accidental modification. This is demonstrated in the following example:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;
using std::endl;

void print(const vector <int> &v) {
    v[1] = 5;         // Error
    for (auto num: v)
        cout << num << " ";
    cout << endl;
}

int main() {
    vector <int> vec {1,2,3,4,5};
    print(vec);
    return 0;
}

C++ Function scope rules

C++ uses scope rules to determine where an identifier can be used. C++ uses static or lexical scoping. C++ has 2 main scopes:

Local or block scope
  1. Identifiers are declared in a block: {}, hence only visible within the block {} where declared.
#include <iostream>

void function() {
    int num = 100;
}

int main() {
    std::cout<<num;     // Error as num is not in scope of main
    return 0;
}
  1. Function parameters have block scope, hence they cannot be accessed outside of the function as the block is the function itself. (Refer C notes on functions)
#include <iostream>

void function(int num) {
    std::cout << num;
}

int main() {
    int n = 100;
    std::cout<<num; // num is local only to function()
    return 0;
}
  1. Function local variables are only active and available while the function is executing.
  2. Local variables are not preserved between function calls.
  3. With nested blocks inner blocks can access outer variables but outer blocks cannot access inner variables.
#include <iostream>
using std::cout;

void function() {
    int num = 100;
    {
        int num = 200;
        cout << num << "\n";
    }
    cout << num;
}

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

Output:

200
100
#include <iostream>
using std::cout;

void function() {
    int num = 100;
    {
        int n = 200;
        cout << num << "\n";
        cout << n << "\n";
    }
    cout << n;  // Error
}

int main() {
    function();
    return 0;
}
  1. The only type of variable whose value is preserved between function calls is a static qualifier. However, these are only initialised the first time the function is called. The life of static variables is throughout the execution of the program. But these variables can be accessed only inside of their respective blocks.
#include <iostream>
using std::cout;

void function() {
    static int n {100};
    cout << n << "\n";
    n++;
}

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

Output:

100
101
102
Global Scope
  1. These identifiers are declared outside any function or class.
  2. Identifiers with global scope are visible to all parts of the program after they have been declared.
  3. The best practice is not to use global variables. However, declaring constants such as PI, size of a fixed buffer are okay to be done in global scope.
#include <iostream>
using std::cout;
const int global_const {100};

void function() {
    cout << global_const << "\n";
}

int main() {
    function();
    cout << global_const << "\n";
    return 0;
}

Output:

100
100

Inline functions

As we have seen in C notes, function calls have significant overhead associated with them. The activation record has to be created, push it on the stack, deal with parameters and execute the logic, pop off the activation record when the function terminates and deal with the return addresses and return values. Although this can happen very quickly and very efficiently, this happens. Sometimes, we have a very simple function where the function call overhead might be greater than the function execution time itself.

In such cases, we can suggest the compiler to generate inline code and embed the function logic into the caller function itself. Inline code is basically inline assembly code that avoids the function overhead. Inline functions are generally faster but this needs to be used judiciously. If these are used frequently for the same piece of logic, you are violating the DRY principle of coding: Do not repeat yourself. Repetition results in larger binaries. However, compiler technology has grown significantly that they are now capable of detecting which are the functions which can be made inline for maximum efficiency.

The following is the syntax for the inline functions:

inline <return type> <function name> ([parameter list]) {
    // function definition
}

The following is an example of inline function

#include <iostream>
using std::cout;

inline int subtract(int op1, int op2) {
    return op1 - op2;
}

int main() {
    cout << subtract(4, 3);
    return 0;
}

Output:

1

Categories
Computer Science / Information Technology Language: C++

References

A reference is an alias for a variable and must be initialised to a variable at declaration. A reference cannot be null and once initialised, it cannot be made to refer to a different variable. A reference can be thought of as a constant pointer that is automatically dereferenced once out of scope.

Declaration and initialisation

Syntax:

data_type &ref_var = var;

Where,

  • data_type is any data type; user-defined or primitive.
  • var is a variable of data type data_type.

Note that the ampersand (&) before the reference variable is compulsory during the declaration of a reference variable.

Use of references in Range-based for loop

When one needs to modify the variable in a loop, using references in a range-based for loop is the best combination. The following program is an example of how to do it:

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector <int> int_num {1, 2, 3, 4, 5};
    for (int &var: int_num)
        var = var * var;
    for (int var: int_num)
        cout << var << " ";
    return 0;
}
Output1 4 9 14 25
ConclusionReferences can be used to modify the contents of a collection in a range-based for loop

Using references is very efficient as the extra cost of copying each collection element is not incurred. Sometimes this approach can be deadly as this is prone to accidental modifications of the collection elements inside of the loop. To solve this, one can use a const modifier which prohibits modifying the collection elements which are being iterated upon.

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector <int> int_num {1, 2, 3, 4, 5};
    for (const int &var: int_num)
        var = var * var;
    return 0;
}
OutputCompilation error
ConclusionThe values in const references cannot be modified. It’s a safer way to use references when no modification is intended.

Using references in function calls

Similar to the use of references in range-based for loop, using references in function parameters is very efficient. The use of references in this context can be done in 2 scenarios described as follows:

  1. When the data of the parameters have to be accessed and not modified
    1. In this scenario, the parameters in the function should be made constant references to the argument passed by the calling function. This prevents accidental modification of the values and reduces the overhead of copying contents to the called function scope as the reference variables are used.
    2. This is illustrated in the following set of examples:
#include <iostream>
#include <vector>
using namespace std;
void print_num(int &num) {
    cout << "Received num: " << num;
}
int main() {
    int num {5};
    print_num(num);
    return 0;
}
OutputReceived num: 5
ConclusionThe syntax of passing the arguments remains the same but the parameter list has to be changed to references. 
#include <iostream>
#include <vector>
using namespace std;
void print_num(const int &num) {
    cout << "Received num: " << num;
}
int main() {
    int num {5};
    print_num(num);
    return 0;
}
OutputReceived num: 5
ConclusionThe value num is not being modified in print_num() function, the best practice is to make the reference constant.
#include <iostream>
#include <vector>
using namespace std;
void print_num(const int &num) {
    cout << "Received num: " << num;
    num++;
}
int main() {
    int num {5};
    print_num(num);
    return 0;
}
OutputCompilation error
ConclusionThe values in constant references cannot be modified. This is a safer way to use references when no modification is intended.
  1. When the data of the parameters have to be modified
    1. Previously, pointers were an optimal way to modify the values in the functions to reduce the overhead of copying. References provide a cleaner way to achieve the same goal. This is illustrated in the following set of examples:
#include <iostream>
#include <vector>
using namespace std;
void square(int &num) {
    num *= num;
}
int main() {
    int num {5};
    square(num);
    cout << "Square of num: " << num;
    return 0;
}
OutputSquare of num: 25
ConclusionThe syntax of passing the arguments remains the same but the parameter list has to be changed to references. 
#include <iostream>
#include <vector>
using namespace std;
void square(vector<int> &num) {
    for (int &val : num)
        val *= val;
}
int main() {
    vector <int> num {1, 2, 3, 4, 5};
    square(num);
    for (int &val : num)
        cout << val << " ";
    return 0;
}
Output1 4 9 16 25
ConclusionNot only primitive data types but references of all data types can be used in function calls. This is far more efficient than making a separate local copy of values in the called function.

Difference between a reference and a pointer

It is common for beginners to get confused between a reference and a pointer. The following table lists the differences between a reference and a pointer.

ReferencePointer
Initialisation:data_type &ref_var = var;Initialisation:data_type *ptr_var = &var;
References only available in C++Pointers are available both in C and C++
Cannot be reassigned to different variableCan be reassigned to point to different variables at a different point in time.
Cannot hold NULLCan hold NULL as a value
Does not need dereferencing operator to access the valueNeeds dereferencing operator (asterisk – ‘*’) to access the value pointed by the pointer
After the initialisation, the reference variable and the original variable are effectively the same but with different names. A reference variable is an alias to the original variableA variable and the pointer to the variable are two different entities.
A reference to a reference (multilevel referencing/nested referencing) is illegal in C++A pointer to pointer (double pointer/multilevel pointer/nested pointers) is valid
To access the address of the variable, ampersand (&) can be suffixed to the variable or the referenceJust the pointer without the ampersand or the dereferencing symbol returns the address of the variable it is pointing to
References cannot be used during dynamic memory allocationPointers must be used during dynamic memory allocation

Reference of pointers

It is possible to have references of pointers. The following example shows the same:

#include <iostream>
using namespace std;
void swap(int * &ptr_var1, int * &ptr_var2){
    int temp = *ptr_var1;
    *ptr_var1 = *ptr_var2;
    *ptr_var2 = temp;
}
int main(){
    int var1 = 5, var2 = 6;
    int *ptr_var1 = &var1, *ptr_var2 = &var2;
    swap(ptr_var1, ptr_var2);
    cout << "var1 is " << var1 << endl;
    cout << "var2 is " << var2;
    return 0;
}
Outputvar1 is 6
var2 is 5
Explanationptr_var1 and ptr_var2 are pointers containing addresses of var1 and var2 respectively.
The two pointers are passed to swap() function as arguments.
The swap function receives the pointer arguments as a reference of pointer parameters and swaps the contents pointed by the pointers.

Exercises

  1. Write a function which has a reference int variable as a parameter and returns void. The function name should be ‘cube’ and it should save the cube of the parameter to itself.
  2. Write a function which has a reference vector variable and returns void. This function should square all the elements of the vector.

L-values and R-values

L-values

  • It is an object that occupies a location in memory and is addressable.
  • These are modifiable if they are not constants.
  • All variables with valid memory allocation are l-values.
  • The name l-value comes from the fact that these entities can be kept in the left hand side of an equality operator: x = 100;

R-values

  • The constant literals with no memory are r-values. These essentially cannot be placed in the left hand side of the equality operator, hence the name r-value.
  • Example “String Literal”, 1, (10 + 20).
  • Assigning values to r-values is illegal:
    • "String literal" = str;
  • Hence we cannot create a reference to an r-value. This is especially important when we are passing values to a function. Consider the following program:
#include <iostream>
#include <vector>

using std::cout;

int double_it(int &a) {
    return a * 2;
}

int main() {
    int n = 5;
    cout << double_it(n); // Legal
    cout << double_it(5); // Illegal
    return 0;
}
Categories
Computer Science / Information Technology Language: C++

Strings

Introduction

  • To use strings in C++, one must include the string library as follows: #include <string>
  • C++ string and its rich set of methods are in the standard namespace (std).
  • Similar to C strings, C++ strings are stored in contiguous memory locations.
  • However, unlike C strings which are fixed in size, the C++ strings have dynamic size. That is, C++ strings can grow or shrink as needed in runtime.
  • C++ strings work with input and output streams.
  • Some of the arithmetic and comparison operators are compatible with C++ strings. Some of them are as follows:
    • Addition (+) for concatenation
    • Assignment (=)
    • Lesser than (<)
    • Lesser than or equal to (<=)
    • Greater than (>)
    • Greater than or equal to (>=)
    • Shorthand addition and assignment (+=) for concatenation and assignment
    • Equals to (==) for string matching
    • Not equals to (!=) for string matching
  • C++ gives the flexibility to convert C++ strings to C strings and vice-versa easily.
  • C++ are safer as it provides bound checking, dynamic sizing and various other features.

Declaration and Initialization

As noted earlier, to use C++ strings, one must include the string library and use the standard namespace. The syntax of the declaration of a C++ string is as follows:

string str_var;

Unlike C strings, the C++ strings are initialised by default at the time of declaration to an empty string, if nothing is explicitly specified.

There are various ways in which a C++ string can be initialised. They are described as follows:

  1. Initialising with a string literal.
string str_var {"string_literal"};
  1. Initialising with another string
string str_var {s2};

This makes a deep copy of s2 in str_var

  1. Initialise with the substring of a string literal
string str_var {“string_literal”, 3};

This initialises the first 3 characters of the string literal to str_var. That is, it initialises "str" to str_var.

  1. Initialise with a substring of another string
string str_var {s2, start_index, num_of_char};

This initialises str_var to substring of s2 from start_index and the length of the substring will be num_of_char

  1. Initialise with a character
string str_var (3, 'A');

This initialises str_var to a string of length 3 characters and contains only 'A'. That is "AAA".

Operators used on strings

The assignment operator (=)

The assignment operator can be used to deep copy the contents of one string to another. This is illustrated in the following example:

#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "Hello World";
    string s2 = s1;
    s2[2] = 'z';
    cout << s1 << endl;
    cout << s2;
    return 0;
}

Output:

Hello World
Hezlo World

Conclusion:

The assignment operator deep copies the contents of one string to another. Hence the changes made to one string don’t reflect in the other string.

Addition operator (+) for concatenation

The addition operator can be used to concatenate one C++ string to another. This is illustrated in the following examples:

Example 1
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "Hello";
    string s2 = " World";
    string s3 = s1 + s2;
    cout << s3;
    return 0;
}

Output: Hello World

Conclusion: The addition operator can be used to concatenate two strings.

Example 2
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "Hello";
    string s2 = s1 + " World!";
    cout << s2;
    return 0;
}

Output: Hello World!

Conclusion: The addition operator can concatenate a string variable with a string literal too.

Example 3
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "Hello";
    string s2 = s1 + " World!" + " C++ is fun.";
    cout << s2;
    return 0;
}

Output: Hello World! C++ is fun.

Conclusion:

  • The addition operator can concatenate a string variable with a string literal too.
  • The addition operator can be used to concatenate multiple strings.

It is important to note that we cannot concatenate 2 C strings using the addition operator. At least one of the operands to the addition operator should be strictly C++ string.

Example 4

#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "Hello" + " World!";
    cout << s1;
    return 0;
}

Output: ERROR

Conclusion: The addition operator can concatenate a string variable with a string literal too.

The subscript operator ([])

To access individual characters of strings, one can use the subscript operator. This is illustrated in the following examples:

Example 1
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "str";
    cout << "Character at index #0: " << s1[0] << endl;
    cout << "Character at index #1: " << s1[1] << endl;
    cout << "Character at index #2: " << s1[2];
    return 0;
}

Output: Hello World!

Example 2
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "str";
    s1[1] = 'i';
    cout << s1;
    return 0;
}

Output: sir

Conclusion: The subscript notation can be used to modify strings at specific indices.

The at() method can also be used for the exact purpose for which the subscript operator is used. The syntax is as follows:

str_var.at(index);

The only difference between using the subscript notation and at() method is that the at() method is safe and if the index given as a parameter exceeds the size of the string, then it throws an exception.

Comparison operators

The C++ strings support the following comparison operators:

  • Lesser than (<)
  • Lesser than or equal to (<=)
  • Greater than (>)
  • Greater than or equal to (>=)
  • Equals to (==) for string matching
  • Not equals to (!=) for string matching

When these operators are used on C++ strings, the strings are compared character-wise lexically and the corresponding boolean values are returned.

The comparison operators have certain limitations. They can compare only certain combinations of string objects and C strings. The compatible combinations are listed as follows:

  1. Two std::string objects.
  2. std::string object and C-style string literal (character constant).
  3. std::string object and C-style string variable (character array).

Using a range-based for loop to iterate through a string

The range-based for loop can be used to conveniently iterate through the string. The following examples illustrate the same:

Example 1
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "str";
    for (char ch: s1)
        cout << ch << " ";
    return 0;
}

Output: s t r

Example 2
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "str";
    for (int ch: s1)
        cout << ch << " ";
    return 0;
}

Output: 115 116 114

Conclusion: The values from the iterable placed in a range-based for loop are type cased to the type of the iterator variable. In this case, it is the integer which depicts the ASCII values of the corresponding characters.

C++ string methods

MethodDescriptionReturn typeParameters
sizeReturn length of stringsize_tNone
lengthReturn length of stringsize_tNone
max_sizeReturns the maximum length the string can reachsize_tNone
resizeResizes the string to a length of n characters. If c is specified, the new elements are initialized as copies of c, otherwise, they are value-initialized characters (null characters)void(size_t n, char c)
Where c is optional
emptyTest whether the string is emptybooleanNone
capacityReturns the size of the storage space currently allocated for the string, expressed in terms of bytes.size_tNone
clearErases the contents of the string, which becomes an empty string (with a length of 0 characters)voidNone
reserveRequests that the string capacity is at least enough to contain 'n' elements.voidn
shrink_to_fitRequests the string to reduce its capacity to fit its size.voidNone
atAccess character at the specified indexcharIndex value between 0 and length – 1 inclusive
frontReturns a reference to the first character of the string.Char ReferenceNone
backReturns a reference to the last character of the string.Char ReferenceNone
appendExtends the string by appending additional characters at the end of its current valueString referenceTypical: (string str)

There are many versions of append. Please refer to the official documentation for details.
assignAssigns a new value to the string, replacing its current contents, and modifying its size accordingly.String referenceTypical: (string str)

There are many versions of append. Please refer to the official documentation for details.
push_backAppends character c to the end of the string, increasing its length by one.voidchar
pop_backErases the last character of the string, effectively reducing its length by one.voidNone
insertInserts additional characters into the string right before the character indicated by posAn iterator that points to the first of the newly inserted elements.Typical: (size_t pos, string str)

There are many versions of append. Please refer to the official documentation for details.
eraseRemoves from the string either a single char at a position or a range of elements ([start_idx, length))An iterator referring to the character that now occupies the position of the first character erased, or string::end if no such character exists.If no parameters are present, the method erases all characters in the string. Possible parameters:

1. (size_t position, size_t len)

2. Iterator pointing character to be removed.

3. Iterator to the first character to be removed and the last character to be removed
swapExchanges the content of the container by the content of str, which is another string object. Lengths may differvoidString
replaceReplaces the portion of the stringReference to stringTypical: (start_pos, length, string)

There are many versions of append. Please refer to the official documentation for details.
c_strReturns a pointer to an array that contains a null-terminated sequence of characters (i.e., a C-string) representing the current value of the string object.Pointer to a character arrayNone
dataReturns a pointer to an array that contains a null-terminated sequence of characters (i.e., a C-string) representing the current value of the string object.Pointer to character arrayNone
get_allocatorReturns a copy of the allocator object associated with the string.Allocator typeNone
copyCopies a substring of the current value of the string object into the array pointed by s. This substring contains the len characters that start at position pos.size_tchar* s, size_t len, size_t pos = 0
findSearches the string for the first occurrence of the sequence specified by its arguments.size_tA string or pointer to a character array or a character and a starting position
rfindFind last occurrence of content in stringsize_tA string or pointer to a character array or a character and a starting position
find_first_ofSearches the string for the first character that matches any of the characters specified in its arguments.size_tA string or pointer to a character array or a character and a starting position
find_last_ofSearches the string for the last character that matches any of the characters specified in its arguments.size_tA string or pointer to a character array or a character and a starting position
find_first_not_ofSearches the string for the first character that does not match any of the characters specified in its arguments.size_tA string or pointer to a character array or a character and a starting position
find_last_not_ofSearches the string for the last character that does not match any of the characters specified in its arguments.size_tA string or pointer to a character array or a character and a starting position
substrReturns a newly constructed string object with its value initialized to a copy of a substring of this object.stringstart position and length
compareCompares the value of the string object (or a substring) to the sequence of characters specified by its arguments.intTypically takes a string.

There are many versions of append. Please refer to the official documentation for details.
String methods

Reading strings from the console

Using cin

To read the string using the input stream through cin, the following syntax should be used:

string str;
cin >> str;

The above statement accepts the input string up to the first white space character. Consider the following examples:

Example 1
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1;
    cin >> s1;
    cout << "Entered string: " << s1;
    return 0;
}

Input: Hello

Output: Entered string: Hello

Example 2
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1;
    cin >> s1;
    cout << "Entered string: " << s1;
    return 0;
}

Input: Hello World

Output: Entered string: Hello

Conclusion: cin reads only up to the first whitespace character found.

Using getline() function

To read the entire line till the new line character, one can use getline() function. The syntax is as follows:

getline(cin, str);

The getline() function also has an optional 3rd parameter called delimiter. The delimiter is a character that when found in the input, the function getline() stops reading the input there. This is by default set to newline character if not mentioned. Consider the following examples:

Example 1
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1;
    getline(cin, s1);
    cout << "Entered string: " << s1;
    return 0;
}

Input: Hello World

Output: Entered string: Hello World

Conclusion: getline() reads the entire line, that is, up to the first occurrence of the newline character.

Example 2
#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1;
    getline(cin, s1, 'r');
    cout << "Entered string: " << s1;
    return 0;
}

Input: Hello World

Output: Entered string: Hello World

Conclusion: If the delimiter is passed to getline(), it reads up to the first occurrence of the mentioned delimiter or the newline character, whichever comes first.

Note to readers

This note has provided content sufficient to start working with C++ strings. The reader has to work by solving examples to get familiarised with the methods the string class provides to get familiarised and comfortable with the C++ strings.

Exercises

  1. Write a function that accepts a string and deletes the last character.
  2. Write a function that accepts a string and deletes all the trailing spaces at the end of the string.
  3. Write a function that accepts a string and deletes all the leading spaces.
  4. Write a function that returns the number of times the character is found in a string. The function has two parameters: the first parameter is a pointer to a string and the second parameter is the character to be found.
  5. Write a function that inserts a string into another string at a specified position. It should return the new string. The first parameter is the receiving string. The second parameter is the string to be inserted. And the third parameter is the index of the insertion position in the first string.
  6. Write a program that extracts part of the given string from the specified position. For example, if the string is “Working with strings is fun”, then if from position 4, four characters are to be extracted, then the program should print the string as “king”. If the number of characters to be extracted is 0, then the program should print the entire string from the specified position. Make use of functions. The function should take the string, start index and the end index as parameters and should return the substring.
  7. Write a program that converts a string like “123” to an integer 123.
  8. To uniquely identify a book a 10 digit ISBN (international standard book number) is used. The rightmost digit is a checksum digit. This digit is determined from the other 9 digits using the condition that d1 + 2d2 + 3d3 + … + 10d10 must be a multiple of 11 (where di denotes the ith digit from the right). The checksum digit d1 can be any value from 0 to 10: the ISBN convention is to use the value x to denote 10. Write a program that receives a 10 digit integer, computes the checksum, and reports whether the ISBN number is correct or not.
  9. A credit card number is usually a 16 digit number. A valid credit card number could satisfy a rule explained below with the help of a dummy credit card number- 4567 1234 5678 9129. Start with the rightmost – 1 digit and multiply every other digit by 2.
4567123456789129
812261014184
  • Then, perform the following operations:
    • Subtract 9 from any number larger than 10. Thus we get:
      • 8 3 2 6 1 5 9 4
    • Add them all up to get 38.
    • Add all the other digits to get 42.
    • The sum of 38 and 42 is 80. Since 80 is divisible by 10, the credit card number is valid.
  • Write a program that receives a credit card number and checks using the above rule whether the credit card number is valid.

You cannot copy content of this page