Advanced Programming c412
Advanced Programming c412
Advanced Programming
C412
For Fourth Level
2023-2024
2
Advanced Programming (C412)
Compiled by
Dr. Mostafa Herajy
Faculty of Science, Port Said University
Lecture 1: Introduction 5
Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Polymorphism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Advantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Lecture 2: Pointers 7
Allocating and Deallocating Pointers in C++ . . . . . . . . . . . . . . . . . . . . 8
Allocating Memory: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Deallocating Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
C++ pointers and arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Pointers and Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Pointer to Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Arrays of Structures and Pointers . . . . . . . . . . . . . . . . . . . . . . . 12
Structures Containing Pointers . . . . . . . . . . . . . . . . . . . . . . . . 12
Smart Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
std::unique_ptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
std::shared_ptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
std::weak_ptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
More examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Pointers to functions in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Declaring a Pointer to a Function: . . . . . . . . . . . . . . . . . . . . . . 15
Initializing a Pointer to a Function: . . . . . . . . . . . . . . . . . . . . . 15
Calling a Function Through a Pointer: . . . . . . . . . . . . . . . . . . . . 15
Passing Pointers to Functions as Arguments: . . . . . . . . . . . . . . . . 16
Lab 2: C++ Pointers Practice . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Practice Questions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Objects
Objects are instances of classes and serve as the building blocks of OOP. They encapsulate
both data (attributes or properties) and behavior (methods or functions) related to
a specific entity or concept. Objects are self-contained, allowing for modularity and
reusability.
Classes
Classes are blueprints or templates for creating objects. They define the structure and
behavior that objects of the class will have. Classes encapsulate data and methods,
providing a clear and organized way to model entities in a program.
Encapsulation
Encapsulation is the concept of bundling data (attributes) and methods (functions) that
operate on that data into a single unit (i.e., an object or class). It enforces data protec-
tion by specifying access control (public, private, protected), allowing for controlled
interaction with object internals.
Inheritance
Inheritance allows one class (the derived or child class) to inherit properties and be-
haviors from another class (the base or parent class). It promotes code reuse and the
creation of hierarchical class structures.
Polymorphism
Polymorphism enables objects of different classes to be treated as objects of a common
base class. It allows for dynamic method invocation based on the actual type of the
object, leading to flexible and extensible code.
6 L E C T URE 1: INTRODUCTION
Advantages
Object-Oriented Programming offers several benefits, including:
• Modularity: OOP promotes breaking down complex systems into smaller, manage-
able pieces (objects and classes), making it easier to develop and maintain large
software projects.
• Reusability: By creating reusable classes and objects, OOP reduces redundant code
and accelerates development, as well as reducing the likelihood of errors.
• Flexibility: OOP allows for easy modification and extension of code by adding new
classes and altering existing ones without affecting other parts of the system.
OOP is not limited to any specific programming language and can be implemented
in various languages, including C++, Java, Python, and many others. Its principles and
concepts provide a versatile and scalable approach to software development, making it
a cornerstone of modern programming.
Lecture 2: Pointers
In C++, a pointer is a variable that stores the memory address of another variable. Point-
ers allow you to indirectly manipulate data by referencing the memory location where
data is stored. They are a powerful feature in C++ and provide fine-grained control
over memory allocation and data manipulation. Here are some key points about C++
pointers:
2. Declaration: To declare a pointer, you use an asterisk (*) in the declaration. For
example, int* ptr; declares a pointer to an integer.
4. Assignment: You can assign the address of a variable to a pointer using the address-
of operator &. For example, ptr = &myVariable; assigns the address of myVariable
to ptr.
7. Pointer Types: Pointers have types that indicate the type of data they point to. For
instance, int* is a pointer to an integer, char* is a pointer to a character, and so on.
8. Null Pointers: C++ allows you to create null pointers (pointers that don’t point to
any valid memory location) using nullptr or NULL.
9. Dynamic Memory Allocation: Pointers are often used in dynamic memory allo-
cation with operators like new and delete. For instance, int* arr = new int[10];
dynamically allocates an array of 10 integers on the heap.
10. Pointers to Objects: You can have pointers to objects and classes. These pointers
allow you to access and manipulate objects indirectly.
i n t main ( ) {
int x = 42; / / D e c l a r e an i n t e g e r v a r i a b l e
int * ptr ; / / D e c l a r e a p o i n t e r t o an i n t e g e r
return 0 ;
}
In this example, ptr is a pointer to an integer, and it’s assigned the address of the x
variable. By dereferencing ptr with *ptr, we can access the value stored in x
The next program demonstrates the usage of a pointer to a float variable, allowing
you to indirectly access and manipulate the float data.
# include <iostream >
i n t main ( ) {
f l o a t myFloat = 3 . 1 4 ; / / Declare a f l o a t variable
float * ptr ; / / Declare a pointer to a f l o a t
p t r = &myFloat ; / / A s s i g n t h e a d d r e s s o f myFloat t o p t r
return 0 ;
}
In this example:
1. We declare a float variable called myFloat and initialize it with the value 3.14.
3. We assign the address of the myFloat variable to the ptr pointer using the address-
of operator &
4. Finally, we access the value of myFloat through the ptr pointer by dereferencing it
with *ptr and print it.
/ / A l l o c a t e memory f o r an a r r a y o f i n t e g e r s
i n t * i n t A r r a y = new i n t [ 1 0 ] ;
Remember to check if memory allocation was successful, as new can throw an ex-
ception (std::bad_alloc) if memory is not available.
Deallocating Memory
Using delete Operator: The delete operator frees the memory allocated with new. When
you’re done with the dynamically allocated memory, you should use delete to release it
to prevent memory leaks.
/ / D e a l l o c a t e memory f o r a s i n g l e i n t e g e r
delete s i n g l e I n t ;
/ / D e a l l o c a t e memory f o r an a r r a y o f i n t e g e r s
delete [ ] intArray ;
Not deallocating memory correctly can lead to memory leaks, where your program
consumes memory that it can’t release, potentially causing your program to run out of
memory.
Using delete vs. delete[]: It’s crucial to use delete[] for arrays allocated with new[]
and delete for single objects allocated with new. Mixing them can lead to undefined
behavior. Here’s an example illustrating dynamic memory allocation and deallocation:
# include <iostream >
i n t main ( ) {
/ / A l l o c a t e memory f o r a s i n g l e i n t e g e r
i n t * s i n g l e I n t = new i n t ;
* singleInt = 42;
/ / A l l o c a t e memory f o r an a r r a y o f i n t e g e r s
i n t * i n t A r r a y = new i n t [ 5 ] ;
f o r ( i n t i = 0 ; i < 5 ; ++ i ) {
intArray [ i ] = i * 10;
}
/ / D e a l l o c a t e memory f o r t h e s i n g l e i n t e g e r
delete s i n g l e I n t ;
/ / D e a l l o c a t e memory f o r t h e a r r a y o f i n t e g e r s
delete [ ] intArray ;
return 0 ;
}
10 L E C T URE 2: POINTERS
In this example, we allocate memory for a single integer and an array of integers
using new. After using the memory, we deallocate it using delete and delete[] to release
the allocated memory.
Proper memory allocation and deallocation are essential for efficient and reliable
C++ programming, as it helps prevent memory leaks and ensures that memory is used
efficiently.
Pointer Arithmetic: You can use pointer arithmetic to access elements of an array by
incrementing or decrementing a pointer. For example:
i n t * p t r = myArray ; / / P o i n t s t o t h e f i r s t e l e m e n t
int value1 = * ptr ; / / A c c e s s e s the f i r s t element ( value1 = 1 )
/ / Move t o t h e n e x t e l e m e n t
ptr ++;
int value2 = * ptr ; / / A c c e s s e s the second element ( value2 = 2)
Array Access Using Pointer Notation: You can also use pointer notation to access
array elements.
i n t * p t r = myArray ;
int value3 = ptr [ 2 ] ; / / A c c e s s e s the t h i r d element ( value3 = 3)
Pointer and Array Size: The size of the array affects pointer arithmetic. If you move a
pointer beyond the bounds of the array, it can lead to undefined behavior or segmenta-
tion faults.
Pointers and Dynamic Arrays: Pointers are commonly used with dynamic arrays
allocated on the heap. For example
i n t * dynamicArray = new i n t [ 1 0 ] ;
/ / A c c e s s and manipulate dynamicArray u s i n g p o i n t e r n o t a t i o n
d e l e t e [ ] dynamicArray ; / / D e a l l o c a t e t h e dynamic a r r a y
Passing Arrays to Functions: When you pass an array to a function, it’s actually
passing a pointer to the first element of the array. This allows functions to work with
arrays efficiently.
void p r o c e s s A r r a y ( i n t * a r r , i n t s i z e ) {
/ / Process the elements of the array using pointer notation
}
i n t main ( ) {
i n t myArray [ 5 ] = { 1 , 2 , 3 , 4 , 5 } ;
p r o c e s s A r r a y ( myArray , 5 ) ; / / P a s s myArray t o t h e f u n c t i o n
return 0 ;
}
Pointers and Multidimensional Arrays: Pointers can be used to work with multidi-
mensional arrays by creating pointers to arrays. This is particularly useful for dynamically
allocated multidimensional arrays.
i n t * * m a t r i x = new i n t * [ 3 ] ;
for ( i n t i = 0 ; i < 3 ; i ++) {
m a t r i x [ i ] = new i n t [ 3 ] ;
}
/ / A c c e s s matrix elements using p o i n t e r notation
Understanding the relationship between pointers and arrays is crucial for effective
memory management and data manipulation in C++. It allows you to work efficiently
with arrays, both static and dynamic, and pass arrays to functions without copying large
amounts of data.
Pointer to Structure
Pointer Declaration: You can declare a pointer to a structure by using the structure’s type
name followed by an asterisk (*). For example, if you have a structure named Person,
you can declare a pointer to it like this:
s t r u c t Person {
s t d : : s t r i n g name ;
i n t age ;
};
Person * p e r s o n P t r ;
Dynamic Allocation: Pointers to structures are often used with dynamic memory
allocation (e.g., new operator) to create instances of structures on the heap:
Person * p e r s o n P t r = new Person ; / / A l l o c a t e memory f o r a P e r s o n s t r u c t u r e
Dynamic Allocation of Arrays: You can dynamically allocate arrays of structures using
pointers:
Person * p e o p l e A r r a y = new Person [ 5 ] ; / / A l l o c a t e an a r r a y o f 5 P e r s o n s t r u c t u r e s
Accessing Array Elements: You can access individual elements of an array of struc-
tures through pointers:
p e o p l e A r r a y [ 2 ] . name = " C h a r l i e " ;
( p e o p l e A r r a y + 3)−>name = " David " ;
Student student1 ;
s t u d e n t 1 . g r a d e s = new i n t [ 5 ] ; / / A l l o c a t e an a r r a y f o r g r a d e s
Using pointers with structures allows you to create more flexible and memory-
efficient data structures, particularly when working with dynamic data or arrays of
structures. It’s important to manage memory properly, especially when using pointers
within structures, to avoid memory leaks and undefined behavior.
Smart Pointers
Smart pointers are a C++ feature that helps manage dynamic memory allocation and
deallocation more safely and conveniently. They are objects that act like pointers but
provide automatic memory management. Smart pointers handle the allocation and
deallocation of memory, which can help prevent common issues like memory leaks and
dangling pointers. In C++, there are three primary types of smart pointers:
std::unique_ptr
std::unique_ptr: This smart pointer type represents ownership of a dynamically allocated
object. It ensures that there is only one unique pointer pointing to the object. When a
std::unique_ptr goes out of scope, it automatically deallocates the memory associated
with the object it owns. Example:
s t d : : u n i q u e _ p t r < i n t > u n i q u e P t r = s t d : : make_unique < i n t > ( 4 2 ) ;
std::shared_ptr
std::shared_ptr: This smart pointer type allows multiple shared pointers to share owner-
ship of the same dynamically allocated object. The object is only deallocated when the
last shared pointer that owns it is destroyed. Example:
s t d : : s h a r e d _ p t r < i n t > s h a r e d P t r 1 = s t d : : make_shared < i n t > ( 4 2 ) ;
s t d : : s h a r e d _ p t r < i n t > s h a r e d P t r 2 = s h a r e d P t r 1 ; / / Both s h a r e d P t r 1 and s h a r e d P t r 2 s h a r e o
std::weak_ptr
std::weak_ptr: This smart pointer type is used in conjunction with std::shared_ptr to
break potential reference cycles. A std::weak_ptr does not increase the reference count
of the object it points to, and it allows you to check if the object is still alive. Example:
s t d : : s h a r e d _ p t r < i n t > s h a r e d P t r = s t d : : make_shared < i n t > ( 4 2 ) ;
s t d : : weak_ptr < i n t > weakPtr = s h a r e d P t r ;
i n t main ( ) {
s t d : : u n i q u e _ p t r < i n t > u n i q u e P t r = s t d : : make_unique < i n t > ( 4 2 ) ;
return 0 ;
}
Smart pointers are an essential part of modern C++ and contribute to safer and more
robust code by automating many aspects of memory management.
14 L E C T URE 2: POINTERS
More examples
std::shared_ptr with Basic Data Types:
# include <iostream >
# i n c l u d e <memory>
i n t main ( ) {
/ / C r e a t e a s h a r e d _ p t r t o an i n t e g e r
s t d : : s h a r e d _ p t r < i n t > s h a r e d I n t = s t d : : make_shared < i n t > ( 4 2 ) ;
/ / C r e a t e a n o t h e r s h a r e d _ p t r t h a t s h a r e s owner ship
std : : shared_ptr < int > sharedInt2 = sharedInt ;
return 0 ;
}
i n t main ( ) {
/ / C r e a t e a u n i q u e _ p t r f o r an i n t e g e r a r r a y
s t d : : u n i q u e _ p t r < i n t [ ] > u n i q u e I n t A r r a y ( new i n t [ 5 ] ) ;
f o r ( i n t i = 0 ; i < 5 ; ++ i ) {
uniqueIntArray [ i ] = i * 1 0 ;
}
f o r ( i n t i = 0 ; i < 5 ; ++ i ) {
s t d : : c o u t << " V a l u e a t i n d e x " << i << " : " << u n i q u e I n t A r r a y [ i ] << s t d : : e n d l ;
}
/ / Memory f o r t h e i n t e g e r a r r a y i s a u t o m a t i c a l l y d e a l l o c a t e d when u n i q u e I n t A r r a y g o
return 0 ;
}
i n t main ( ) {
s t d : : s h a r e d _ p t r < i n t > s h a r e d I n t = s t d : : make_shared < i n t > ( 4 2 ) ;
s t d : : weak_ptr < i n t > weakInt = s h a r e d I n t ;
/ / Check i f t h e s h a r e d _ p t r i s s t i l l a l i v e
i f ( auto l o c k e d I n t = weakInt . l o c k ( ) ) {
s t d : : c o u t << "Weak p o i n t e r i s a l i v e . " << s t d : : e n d l ;
s t d : : c o u t << " V a l u e : " << * l o c k e d I n t << s t d : : e n d l ;
} else {
s t d : : c o u t << "Weak p o i n t e r i s e x p i r e d . " << s t d : : e n d l ;
}
/ / When s h a r e d I n t g o e s out o f s c o p e , t h e i n t e g e r i s d e a l l o c a t e d
return 0 ;
}
In this example, a std::weak_ptr is used to check if a std::shared_ptr is still alive and
access the integer it points to.
i n t Add ( i n t a , i n t b ) {
return a + b ;
}
i n t main ( ) {
P r o c e s s ( 5 , 3 , Add ) ; / / C a l l s Add ( 5 , 3 ) t h r o u g h P r o c e s s
Process (5 , 3 , Subtract ) ; / / C a l l s S u b t r a c t ( 5 , 3) through P r o c e s s
return 0 ;
}
1. Problem 1: Basic Pointer Manipulation Write a C++ program that does the following:
• Write a C++ program that defines a function square which takes an integer
as a parameter and returns the square of that integer. Then, use a pointer to
this function to calculate the squares of several integers and print the results.
7. Problem 8: Pointer Casting Write a C++ program that demonstrates pointer casting:
9. Problem 10: Memory Leak Detection (Optional) Incorporate a memory leak de-
tection tool like Valgrind to check your program for memory leaks in any of the
previous problems. Make any necessary adjustments to your code to ensure proper
memory deallocation.
18 L E C T URE 2: POINTERS
Practice Questions
1. How do you declare a pointer variable for an integer?
9. How can you handle errors related to dynamic memory allocation with pointers?
10. Explain common pitfalls and best practices to avoid errors when working with
pointers.
12. Write a C++ program that uses a double pointer to dynamically create a 2D integer
array (matrix). Prompt the user for the number of rows and columns, allocate
memory for the matrix, fill it with values, and then print the matrix.
13. Write a C++ program that defines a function square which takes an integer as
a parameter and returns the square of that integer. Then, use a pointer to this
function to calculate the squares of several integers and print the results.
14. Write a C++ program that does the following: Declares an integer variable x and
initializes it with a value. Declares a pointer to an integer and assigns the address
of x to it. Prints the value of x and the value pointed to by the pointer.
Lecture 3: C++ Classes
• Objects: Objects are instances of classes, and they encapsulate both data (attributes
or properties) and behavior (methods or functions). These objects represent real-
world entities and can interact with each other.
• Classes: Classes are blueprints or templates for creating objects. They define the
structure and behavior that objects of the class will have. A class specifies the
attributes and methods that its objects will possess.
• Inheritance: Inheritance allows you to create a new class (a derived or child class)
based on an existing class (a base or parent class). The derived class inherits
the attributes and methods of the base class, enabling code reuse and creating a
hierarchical structure of classes.
• OOP promotes code reusability, modularity, and a more intuitive way of thinking
about and structuring software, making it a widely used and powerful programming
paradigm. It is commonly employed in languages like Java, C++, Python, and many
others.
five fundamental principles of OOP are often referred to as SOLID, an acronym formed
from the first letter of each principle:
• Single Responsibility Principle (SRP): This principle states that a class should
have only one reason to change, meaning it should have a single responsibility or
purpose. A class should encapsulate a single piece of functionality, and if it has
multiple responsibilities, it may become difficult to maintain and modify.
These SOLID principles guide the design and development of object-oriented sys-
tems, helping developers create software that is more modular, extensible, and main-
tainable. By adhering to these principles, developers can reduce the complexity of their
code, improve its readability, and make it more resilient to changes and updates.
1. Class Definition: A class in C++ is a user-defined data type that defines a template
for creating objects. It defines the properties (data members or attributes) and
behaviors (member functions or methods) that objects of the class will have.
2. Encapsulation One of the key principles of OOP is encapsulation, and classes
facilitate this. Encapsulation means bundling data and methods that operate on
that data into a single unit (i.e., an object). In C++, you can specify the access level
of data members (public, private, protected) to control who can access them.
5. Member Functions: Member functions are functions defined within a class that
operate on the class’s data members. These functions can be public, private, or
protected. Public member functions can be called from outside the class and are
used to interact with objects.
6. Data Members: Data members are variables declared within a class that represent
the object’s state. They define what data an object of the class can store.
7. Access Specifiers: C++ provides three access specifiers: public, private, and pro-
tected. These specifiers determine the visibility and accessibility of class members.
/ / Member f u n c t i o n
void s t a r t E n g i n e ( ) {
c o u t << " Engine s t a r t e d ! " << e n d l ;
}
private :
/ / Data members
int year ;
s t r i n g make ;
};
22 L E C T URE 3: C++ CL ASSES
In this example, we define a Car class with a constructor, a member function startEngine,
and private data members year and make. You can create Car objects, call member func-
tions, and access data members to work with instances of the class.
Objects are instances of a class. To create an object, you declare a variable of the
class type. For example:
Car myCar ; / / C r e a t e s an o b j e c t o f t h e c l a s s Car
You can access the data members and member functions of an object using the dot
(.) operator. For example:
Car myCar ; / / C r e a t e s an o b j e c t o f t h e c l a s s Car
myCar . s t a r t E n g i n e ( ) ; / / C a l l s a member f u n c t i o n
Here is a complete C++ class and how the class is called from inside main():
# include <iostream >
# include < s t r i n g >
using namespace s t d ;
/ / Class definition
c l a s s Car {
public :
/ / Constructor
Car ( i n t year , s t r i n g make ) {
t h i s −>y e a r = y e a r ;
t h i s −>make = make ;
engineRunning = f a l s e ;
}
/ / Member f u n c t i o n t o s t a r t t h e e n g i n e
void s t a r t E n g i n e ( ) {
engineRunning = t r u e ;
c o u t << " Engine s t a r t e d ! " << e n d l ;
}
/ / Member f u n c t i o n t o s t o p t h e e n g i n e
void s t o p E n g i n e ( ) {
engineRunning = f a l s e ;
c o u t << " Engine stopped . " << e n d l ;
}
/ / Member f u n c t i o n t o d i s p l a y c a r i n f o
void d i s p l a y I n f o ( ) {
c o u t << " Y e a r : " << y e a r << e n d l ;
c o u t << "Make : " << make << e n d l ;
c o u t << " Engine s t a t u s : " << ( engineRunning ? " Running " : " Stopped " ) << e n d l ;
}
private :
/ / Data members
int year ;
s t r i n g make ;
bool engineRunning ;
};
i n t main ( ) {
/ / C r e a t e an o b j e c t o f t h e Car c l a s s
Car myCar ( 2 0 2 3 , " Toyota " ) ;
/ / S t a r t the engine
myCar . s t a r t E n g i n e ( ) ;
c o u t << " Car I n f o a f t e r s t a r t i n g t h e e n g i n e : " << e n d l ;
myCar . d i s p l a y I n f o ( ) ;
return 0 ;
}
In this example, we define a Car class with a constructor, member functions (startEngine,
stopEngine, and displayInfo), and private data members (year, make, and engineRun-
ning). In the main() function, we create an instance of the Car class, call its member
functions, and display information about the car object.
When you run this program, it will create a Car object, manipulate its state (start and
stop the engine), and display information about the car’s year, make, and engine status.
– Classes act as blueprints or templates for creating objects. A class defines the
structure (data members or attributes) and behavior (member functions or
methods) that objects of that class will have.
– Objects are instances of classes. When you create an object based on a class,
you’re essentially creating a specific instance that adheres to the blueprint
defined by that class.
Constructor: revisited
In C++, constructors are special member functions used for initializing objects when they
are created. Constructors have the same name as the class and are called automatically
when an object is instantiated. There are several types of constructors in C++, each
serving different purposes. Here are the main types of constructors with examples:
Default Constructor:
A default constructor is called when an object is created without any arguments. It
initializes the object’s data members to default values.
c l a s s MyClass {
public :
MyClass ( ) {
/ / Default constructor
value = 0 ;
message = " H e l l o , World ! " ;
}
void d i s p l a y ( ) {
c o u t << message << " V a l u e : " << v a l u e << e n d l ;
}
private :
int value ;
s t r i n g message ;
};
Example usage:
MyClass o b j ; / / C a l l s t h e d e f a u l t c o n s t r u c t o r
o b j . d i s p l a y ( ) ; / / Output : H e l l o , World ! Value : 0
Parameterized Constructor:
A parameterized constructor takes one or more arguments and initializes the object’s
data members with the provided values.
c l a s s Student {
public :
Student ( s t r i n g n , int r ) {
/ / Parameterized constructor
name = n ;
rollNumber = r ;
}
void d i s p l a y ( ) {
c o u t << "Name : " << name << " R o l l Number : " << rollNumber << e n d l ;
}
private :
s t r i n g name ;
i n t rollNumber ;
};
Example usage:
Student s 1 ( " A l i c e " , 1 0 1 ) ; / / C a l l s the parameterized c o n s t r u c t o r
s 1 . d i s p l a y ( ) ; / / Output : Name : A l i c e R o l l Number : 1 0 1
Copy Constructor:
A copy constructor creates a new object as a copy of an existing object of the same class.
It is used for deep copying data members.
c l a s s Person {
public :
Person ( s t r i n g n , i n t a ) {
name = n ;
age = a ;
}
/ / Copy c o n s t r u c t o r
Person ( const Person& o t h e r ) {
name = o t h e r . name ;
age = o t h e r . age ;
}
void d i s p l a y ( ) {
c o u t << "Name : " << name << " Age : " << age << e n d l ;
}
private :
s t r i n g name ;
i n t age ;
};
Example usage:
Person p1 ( " Bob " , 3 0 ) ;
Person p2 = p1 ; / / C a l l s t h e copy c o n s t r u c t o r t o c r e a t e p2 a s a copy o f p1
p2 . d i s p l a y ( ) ; / / Output : Name : Bob Age : 30
26 L E C T URE 3: C++ CL ASSES
void d i s p l a y ( ) {
c o u t << " T i t l e : " << t i t l e << " Author : " << a u t h o r << e n d l ;
}
private :
string t i t l e ;
s t r i n g author ;
};
Example usage:
Move Constructor
A move constructor is a special constructor in C++ that allows for the efficient transfer
of ownership of resources from one object to another. It is used to move the contents
of one object (often a temporary or soon-to-be-destroyed object) into another object,
typically without copying data. This is particularly useful for optimizing memory and
performance when working with expensive-to-copy resources like dynamically allocated
memory or large data structures.
The move constructor has the following signature:
c l a s s MyClass {
public :
MyClass ( MyClass&& o t h e r ) noe xc ep t {
/ / Move c o n s t r u c t o r
/ / T r a n s f e r owners hip o f r e s o u r c e s from ’ o t h e r ’ t o ’ t h i s ’
/ / U s u a l l y i n v o l v e s s h a l l o w c o p y i n g and r e s e t t i n g ’ o t h e r ’
}
};
Here’s an example illustrating the use of a move constructor with a simple class:
# include <iostream >
# include < s t r i n g >
c l a s s M yS t r i n g {
public :
M y S t r i n g ( const char * s ) : d a t a ( n u l l p t r ) {
size = strlen ( s ) ;
d a t a = new char [ s i z e + 1 ] ;
s t r c p y ( data , s ) ;
}
/ / Move c o n s t r u c t o r
M y S t r i n g ( M y S t r i n g&& o t h e r ) no exc ep t : d a t a ( n u l l p t r ) , s i z e ( 0 ) {
s t d : : swap ( data , o t h e r . d a t a ) ; / / E f f i c i e n t l y t r a n s f e r owners hip
s t d : : swap ( s i z e , o t h e r . s i z e ) ;
}
~ MyString ( ) {
delete [ ] data ;
}
void d i s p l a y ( ) {
s t d : : c o u t << " S t r i n g : " << d a t a << " Length : " << s i z e << s t d : : e n d l ;
}
private :
char * d a t a ;
size_t size ;
};
i n t main ( ) {
M y S t r i n g s t r 1 ( " H e l l o , World ! " ) ;
return 0 ;
}
In this example, we define a MyString class with a move constructor. When str2 is
constructed using std::move(str1), the move constructor efficiently transfers ownership
of the data and size resources from str1 to str2, leaving str1 in a valid but unspecified
state.
Move constructors are particularly valuable when working with large data structures,
managing memory, and optimizing performance in C++ programs. They play a crucial
role in enabling the efficient use of move semantics and avoiding unnecessary data
copying.
Destructor: revisited
In C++, a destructor is a special member function of a class that is responsible for
cleaning up resources used by objects of that class when they go out of scope or are
explicitly destroyed. Destructors have the same name as the class, preceded by a tilde
( ).
Here’s more information about C++ destructors:
1. Purpose of Destructors:
• The primary purpose of a destructor is to release any resources held by an
object, such as dynamic memory allocations, file handles, or other external
resources.
28 L E C T URE 3: C++ CL ASSES
2. Destructor Syntax:
/ / Destructor
~ MyClass ( ) { / * D e s t r u c t o r c o d e * / }
};
• Destructors do not take any parameters, and their name is the class name
preceded by a tilde ( ).
3. Resource Cleanup:
• Inside the destructor, you should release any resources that the object ac-
quired during its lifetime.
• Common tasks in a destructor include deleting dynamically allocated memory
(e.g., with delete or delete[]), closing files, releasing network connections, or
freeing any other acquired resources.
4. Automatic Invocation:
5. Default Destructor:
• If you don’t provide a destructor in your class, C++ will generate a default
destructor that does nothing. This is usually sufficient for classes that do not
manage any resources.
6. Destructor in Inheritance:
• When working with inheritance, derived classes can have their own destruc-
tors, which are called after the base class destructor. Make sure to call the
base class destructor explicitly if necessary.
c l a s s ResourceHolder {
public :
ResourceHolder ( ) {
/ / Constructor acquires a resource
r e s o u r c e = new i n t [ 1 0 0 ] ;
s t d : : c o u t << " Resource a c q u i r e d . " << s t d : : e n d l ;
}
~ ResourceHolder ( ) {
/ / Destructor r e l e a s e s the resource
delete [ ] resource ;
s t d : : c o u t << " Resource r e l e a s e d . " << s t d : : e n d l ;
}
private :
int * resource ;
};
i n t main ( ) {
{
ResourceHolder holder ; / / O b j e c t c r e a t e d within a scope
} / / D e s t r u c t o r c a l l e d when ’ h o l d e r ’ g o e s out o f s c o p e
// ...
return 0 ; / / Program ends , and ’ h o l d e r ’ d e s t r u c t o r i s c a l l e d a g a i n
}
In this example, the ResourceHolder class acquires and releases a dynamically allo-
cated resource in its constructor and destructor, respectively. When the holder object
goes out of scope, its destructor is automatically called, releasing the resource. The
destructor is called a second time when the program exits.
Pointer to Objects:
You can declare pointers to objects of a class just like you declare pointers to other data
types. For example:
MyClass * my Ob je ctP tr ; / / P o i n t e r t o an o b j e c t o f c l a s s MyClass
Once you have a pointer, you can use it to dynamically allocate objects on the heap:
my Ob je ctP tr = new MyClass ; / / D y n a m i c a l l y a l l o c a t e an o b j e c t on t h e heap
You can then access the object’s members (data members and member functions)
through the pointer:
30 L E C T URE 3: C++ CL ASSES
Example usage:
MyClass myObj ;
m o d i f y O b j e c t (&myObj ) ; / / P a s s a p o i n t e r t o myObj
Example usage:
MyClass myObj ;
myObj . * dataMemberPtr = 4 2 ; / / A c c e s s data member t h r o u g h t h e p o i n t e r
( myObj . * f u n c P t r ) ( ) ; / / C a l l member f u n c t i o n t h r o u g h t h e p o i n t e r
• Usage for Data Containers: Structures are often used when you need a simple
data container to group related data elements together. They don’t typically have
member functions but focus on storing and organizing data.
Point p ;
p. x = 10;
p. y = 20;
• No Member Functions: While C++ allows you to add member functions to struc-
tures, it’s not a common practice. By convention, structures are primarily used for
holding data.
struct Rectangle {
i n t width ;
int height ;
int area ( ) {
return width * h e i g h t ; / / Member f u n c t i o n t o c a l c u l a t e a r e a
}
};
Classes:
• Default Member Accessibility: In a C++ class, all data members have private acces-
sibility by default. This means that data members are not directly accessible from
outside the class. You need member functions (getters and setters) to access or
modify them.
c l a s s Person {
private :
s t d : : s t r i n g name ; / / P r i v a t e by d e f a u l t
i n t age ; / / P r i v a t e by d e f a u l t
public :
/ / Member f u n c t i o n s t o a c c e s s and modify p r i v a t e members
void setName ( const s t d : : s t r i n g & n ) { name = n ; }
void s e t A g e ( i n t a ) { age = a ; }
};
Person p ;
p . setName ( " A l i c e " ) ;
p . setAge ( 3 0 ) ;
• Encapsulation: Classes support encapsulation, a key OOP concept. This means you
can control access to data members, hiding implementation details and providing
a public interface for interacting with objects.
Person p ;
p . setName ( " A l i c e " ) ; / / A c c e s s t h r o u g h a p u b l i c member f u n c t i o n
In summary, structures are primarily used for grouping data members together, and
their members are typically public by default. Classes, on the other hand, are used for
defining complex data types with private data members and member functions that
provide an interface for interacting with the data. Classes are central to object-oriented
programming and encapsulation, allowing you to model real-world entities with behavior
and data.
Static Members
Static Member Variables
In C++, a static member variable is a variable that is associated with a class rather
than with instances (objects) of the class. It is shared among all instances of the class
and can be accessed without creating an object of the class. Here are some important
characteristics and usage guidelines for static member variables:
1. Declaration and Initialization: Static member variables are declared inside a class
like any other class member but with the static keyword. They are typically initial-
ized outside the class definition in a source file.
c l a s s MyClass {
public :
s t a t i c i n t count ; / / Declaration
};
2. Shared Among All Objects: All instances (objects) of the class share the same static
member variable. Modifying the static member variable in one object will affect
its value in all other objects of the class.
3. Accessing Static Member Variables: You can access a static member variable using
the class name followed by the scope resolution operator (::) or by using an object
of the class. However, it’s a good practice to access static members through the
class name to emphasize that they are shared among all instances.
i n t main ( ) {
MyClass o b j 1 , o b j 2 ;
MyClass : : count = 1 0 ; / / A c c e s s t h r o u g h c l a s s name
o b j 1 . count = 2 0 ; / / A l s o v a l i d but not recommended f o r s t a t i c members
c o u t << o b j 2 . count << e n d l ; / / A c c e s s t h r o u g h o b j e c t ( not recommended )
return 0 ;
}
5. Use Cases: Static member variables are often used to maintain data that is shared
among all instances of a class. For example, you might use a static member variable
to keep count of the number of objects created from a class.
c l a s s MyClass {
public :
s t a t i c i n t o b j e c t C o u n t ; / / S t a t i c member v a r i a b l e
MyClass ( ) {
objectCount ++; / / Increment count in the c o n s t r u c t o r
}
};
i n t MyClass : : o b j e c t C o u n t = 0 ; / / I n i t i a l i z e t h e s t a t i c member v a r i a b l e
6. Constant Static Member Variables: You can also have static member variables
that are constants, declared as static const. These values are fixed and cannot be
modified once initialized.
c la ss Constants {
public :
s t a t i c const i n t MAX_VALUE = 1 0 0 ; / / C o n s t a n t s t a t i c member
};
Static member variables are a powerful feature in C++ and can be used for various
purposes, including counting instances, sharing configuration settings, and managing
shared resources among class instances. They provide a way to store class-level data
that is independent of object-specific data.
1. A static member function is declared inside a class like any other member function,
but it is also preceded by the static keyword. It is defined outside the class just like
a regular function.
34 L E C T URE 3: C++ CL ASSES
c l a s s MyClass {
public :
s t a t i c void s t a t i c F u n c t i o n ( ) ; / / Declaration
};
void MyClass : : s t a t i c F u n c t i o n ( ) {
/ / Definition
/ / S t a t i c member f u n c t i o n s do not have a c c e s s t o i n s t a n c e − s p e c i f i c data
}
2. No Access to Instance Data: Static member functions do not have access to instance-
specific data (non-static data members) or this pointer because they are not called
on instances. They can only access static members (variables or functions) of the
class.
3. Accessing Static Member Functions: You can call a static member function using
the class name followed by the scope resolution operator (::) or using an object
of the class. However, it’s more common to call them through the class name to
emphasize that they are class-level functions.
i n t main ( ) {
MyClass : : s t a t i c F u n c t i o n ( ) ; / / A c c e s s t h r o u g h c l a s s name
MyClass o b j ;
o b j . s t a t i c F u n c t i o n ( ) ; / / A l s o v a l i d but not common
return 0 ;
}
4. Use Cases: Static member functions are often used for tasks that are not related to
specific instances but are associated with the class as a whole. Common use cases
include utility functions, factory methods, and functions that operate on static
data members.
class MathUtility {
public :
s t a t i c i n t add ( i n t a , i n t b ) {
return a + b ;
}
};
i n t r e s u l t = M a t h U t i l i t y : : add ( 5 , 3 ) ; / / C a l l i n g a s t a t i c member f u n c t i o n
Static member functions are a valuable feature in C++ for organizing code and pro-
viding class-level functionality. They are a key part of the language’s support for encap-
sulation and object-oriented design.
Constant Member Functions
In C++, a const member function is a member function of a class that promises not
to modify the state of the object on which it is called. It is a way of declaring that the
function does not modify the data members of the object. This promise is enforced
by the compiler, which generates an error if you attempt to modify non-mutable data
members within a const member function.
Here’s how const member functions work and why they are useful:
1. Declaration and Syntax: To declare a member function as const, you add the
const keyword after the function’s parameter list but before its body in the class
declaration.
c l a s s MyClass {
public :
i n t g e t D a t a ( ) const ; / / D e c l a r a t i o n o f a c o n s t member f u n c t i o n
};
When defining the function outside the class, you include const again to indicate
that it’s a const member function.
i n t MyClass : : g e t D a t a ( ) const {
/ / I m p l e m e n t a t i o n o f t h e c o n s t member f u n c t i o n
/ / Cannot modify non−mutable data members h e r e
}
3. Access to const and Non-const Objects: You can call a const member function
on both const and non-const objects of the class. However, you cannot call a
non-const member function on a const object, as it could potentially modify the
object’s state.
const MyClass o b j 1 ;
MyClass o b j 2 ;
4. Use Cases: const member functions are particularly useful when you want to
provide read-only access to the object’s data without allowing modifications. They
are commonly used for getter methods that return the value of data members
without altering them.
c l a s s TemperatureSensor {
public :
double g e t T e m p e r a t u r e ( ) const {
return t e m p e r a t u r e ;
}
36 L E C T URE 3: C++ CL ASSES
private :
double t e m p e r a t u r e ;
};
5. Mutable Data Members: If you have data members that you want to modify even
within const member functions, you can declare them as mutable. These mutable
members are exempt from the promise of non-modification.
c l a s s MyClass {
public :
i n t g e t D a t a ( ) const {
return d a t a + mutableData ;
}
private :
int data ;
mutable i n t mutableData ; / / Can be m o d i f i e d i n c o n s t member f u n c t i o n s
};
const member functions play a crucial role in ensuring data integrity and providing
safe access to object data within C++ classes. They are an important feature for designing
classes with well-defined interfaces and controlling object behavior.
1. Define a class named Student with the following private data members:
– std::string name (Student’s name)
– int age (Student’s age)
– double gpa (Student’s GPA)
2. Add public member functions to set and get the values of these data members.
1. Create two Student objects: one using the default constructor and another
using the parameterized constructor.
2. Set and display values for the data members of these objects using member
functions.
• Task 4: Encapsulation
1. Implement a member function void study() in the Student class that prints a
message indicating that the student is studying.
2. Implement a member function void displayInfo() in the Student class that
displays the student’s name, age, and GPA.
1. In the main() function, create a menu-driven program that allows the user to
perform the following actions:
– Create a new Student object using the parameterized constructor and set
its values.
– Display the information of a student using the displayInfo() function.
– Make a student study using the study() function.
– Make a student study using the study() function.
2. Lab Completion:
– Test your program thoroughly by creating and manipulating Student
objects.
– Ensure that constructors and destructors are called correctly.
– Verify that encapsulation principles are followed.
– Demonstrate the use of member functions to access and modify data
members.
38 L E C T URE 3: C++ CL ASSES
Practice Questions
1. Define a C++ class named Rectangle with private data members for length and
width. Provide public member functions to set and get the values of these data
members. Create objects of this class and demonstrate the use of these member
functions.
2. Create a C++ class named Person with private data members for name and age.
Implement a parameterized constructor to initialize these members and a member
function to display the person’s details. Create objects of this class and display the
details of multiple persons.
3. Define a C++ class called BankAccount with private data members for account-
Number, accountHolderName, and balance. Implement member functions to:
4. Create a C++ class called Circle with a private data member for the radius. Imple-
ment member functions to calculate and return the area and circumference of the
circle. Demonstrate the use of this class to calculate the area and circumference of
multiple circles.
5. Define a C++ class named Book to represent a book with private data members
for title, author, and publicationYear. Implement a parameterized constructor
to initialize these members and a member function to display the book’s details.
Create an array of Book objects and display the details of all the books.
6. Create a C++ class called Date with private data members for day, month, and
year. Implement a parameterized constructor to initialize these members. Write a
member function to display the date in the format "dd/mm/yyyy." Create objects
of this class to represent various dates and display them.
7. Implement a C++ class called Employee with private data members for name,
employeeId, and salary. Provide a parameterized constructor to initialize these
members. Implement a member function to give an annual salary raise to an
employee. Create objects of this class and apply a salary raise to some employees.
8. Define a C++ class named Car with private data members for make, model, and
year. Implement a parameterized constructor to initialize these members. Write
a member function to display the car’s details. Create objects of this class to
represent different cars and display their details.
9. Create a C++ class called Student with private data members for name, rollNumber,
and marks. Implement a parameterized constructor to initialize these members.
Write a member function to calculate and return the grade based on the marks (A,
B, C, D, or F). Create objects of this class to represent students and display their
grades.
10. Implement a C++ class named Complex to represent complex numbers with private
data members for the real and imaginary parts. Provide member functions to
perform addition and multiplication of complex numbers. Create objects of this
class and demonstrate these operations.
40 L E C T URE 3: C++ CL ASSES
Lecture 4: Inheritance and Polymorphism
In C++, classes are the building blocks of object-oriented programming, and they play a
pivotal role in inheritance. Inheritance allows you to create new classes (derived or child
classes) by inheriting the properties and behaviors of existing classes (base or parent
classes). Let’s explore how classes are used in inheritance within C++:
void s l e e p ( ) {
/ / Common b e h a v i o r f o r a l l animals
}
};
c l a s s Cat : p u b l i c Animal {
public :
void meow ( ) {
/ / Unique b e h a v i o r f o r c a t s
}
};
42 L E C T URE 4: INHERITANCE AND POLYMORPHISM
Access Specifiers
In C++, you can specify the access level for the inherited members from the base class in
the derived class. The most common access specifiers are public, protected, and private.
The choice of access specifier determines how the derived class and external code can
access the inherited members.
public: Inherited members are accessible to everyone. protected: Inherited members
are accessible within the derived class and its subclasses but not directly from external
code. private: Inherited members are not accessible in the derived class (unless a friend
relationship is established), making them effectively hidden.
~Dog ( ) {
/ / D e s t r u c t o r f o r Dog
}
};
Method Overriding:
Method Overriding: Derived classes can override (replace) base class methods with their
own implementations. This allows for polymorphism, where different objects of the
same base class type can exhibit different behavior based on their actual derived class
type.
c l a s s B i r d : p u b l i c Animal {
public :
void e a t ( ) {
/ / O v e r r i d e t h e e a t method f o r b i r d s
/ / Custom b e h a v i o r f o r b i r d s
}
};
/ / Base c l a s s : Shape
c l a s s Shape {
public :
/ / Common method t o c a l c u l a t e a r e a ( v i r t u a l , t o be o v e r r i d d e n )
v i r t u a l double c a l c u l a t e A r e a ( ) {
return 0 . 0 ; / / D e f a u l t i m p l e m e n t a t i o n f o r b a s e c l a s s
}
};
/ / D e r i v e d c l a s s : C i r c l e ( i n h e r i t s from Shape )
c l a s s C i r c l e : p u b l i c Shape {
private :
double r a d i u s ;
public :
/ / Constructor for Circle
C i r c l e ( double r ) : r a d i u s ( r ) { }
/ / O v e r r i d e t h e c a l c u l a t e A r e a method f o r c i r c l e s
double c a l c u l a t e A r e a ( ) o v e r r i d e {
return 3 . 1 4 1 5 9 2 6 5 3 5 9 * r a d i u s * r a d i u s ; / / Area o f a c i r c l e
}
};
/ / D e r i v e d c l a s s : R e c t a n g l e ( i n h e r i t s from Shape )
c l a s s R e c t a n g l e : p u b l i c Shape {
private :
double l e n g t h ;
double width ;
public :
/ / Constructor for Rectangle
R e c t a n g l e ( double l , double w) : l e n g t h ( l ) , width (w) { }
/ / O v e r r i d e t h e c a l c u l a t e A r e a method f o r r e c t a n g l e s
double c a l c u l a t e A r e a ( ) o v e r r i d e {
return l e n g t h * width ; / / Area o f a r e c t a n g l e
}
};
i n t main ( ) {
/ / C r e a t e o b j e c t s o f C i r c l e and R e c t a n g l e
Circle circle ( 5 . 0 ) ;
Rectangle rectangle ( 4 . 0 , 6 . 0 ) ;
/ / C a l c u l a t e and d i s p l a y a r e a s
s t d : : c o u t << " Area o f t h e C i r c l e : " << c i r c l e . c a l c u l a t e A r e a ( ) << s t d : : e n d l ;
s t d : : c o u t << " Area o f t h e R e c t a n g l e : " << r e c t a n g l e . c a l c u l a t e A r e a ( ) << s t d : : e n d l ;
return 0 ;
}
In this example:
1. We define a base class called Shape, which includes a virtual method calculateArea().
This method will be overridden in the derived classes to calculate the area of spe-
44 L E C T URE 4: INHERITANCE AND POLYMORPHISM
cific shapes.
2. We create two derived classes, Circle and Rectangle, each inheriting from the
Shape base class. These derived classes have their own specific attributes (radius
for circles and length and width for rectangles) and override the calculateArea()
method to provide their own implementation of area calculation.
3. In the main() function, we create instances of Circle and Rectangle. When we call
the calculateArea() method on these objects, the appropriate overridden method
is executed, and it calculates and returns the area specific to each shape.
Multiple Inheritance
Multiple inheritance is a feature in object-oriented programming languages, including
C++, that allows a class to inherit properties and behaviors from more than one base
class. This means a derived class can inherit from multiple parent classes. While multiple
inheritance can be powerful, it also introduces complexities and challenges that need
to be managed carefully. In C++, you can achieve multiple inheritance by specifying
multiple base classes separated by commas in the declaration of a derived class.
Here’s a basic example to illustrate multiple inheritance in C++:
# include <iostream >
/ / F i r s t base c l a s s
class A {
public :
void methodA ( ) {
s t d : : c o u t << " Method A from c l a s s A" << s t d : : e n d l ;
}
};
/ / Second b a s e c l a s s
class B {
public :
void methodB ( ) {
s t d : : c o u t << " Method B from c l a s s B" << s t d : : e n d l ;
}
};
i n t main ( ) {
C cObj ;
/ / A c c e s s i n g a method from t h e d e r i v e d c l a s s
cObj . methodC ( ) ; / / C a l l s methodC from c l a s s C
return 0 ;
}
In this example:
• We have two base classes, A and B, each with its own method.
• The C class is derived from both A and B using multiple inheritance. As a result, it
inherits the methods methodA from class A and methodB from class B.
• In the main() function, we create an instance of the C class and can access methods
from both base classes (A and B) as well as its own method (methodC).
Multiple inheritance can be a powerful tool, allowing you to model complex rela-
tionships between classes and reuse code effectively. However, it can also introduce
ambiguity and the "diamond problem" (a situation where multiple base classes have a
common ancestor) in more complex scenarios. Careful design and usage of multiple in-
heritance, along with virtual inheritance and explicit scope resolution, can help mitigate
these issues when they arise.
Polymorphism
Polymorphism is a fundamental concept in object-oriented programming, and it plays
a significant role in C++. It allows objects of different classes to be treated as objects
of a common base class, providing a way to work with objects in a more abstract and
generic manner. There are two primary forms of polymorphism in C++: compile-time
(or static) polymorphism and runtime (or dynamic) polymorphism.
• Operator Overloading: C++ allows you to redefine the behavior of operators for
user-defined classes by overloading them. This enables you to use operators like +,
-, *, etc., with user-defined types.
c l a s s Complex {
public :
Complex operator + ( const Complex& o t h e r ) {
Complex r e s u l t ;
r e s u l t . r e a l = t h i s −> r e a l + o t h e r . r e a l ;
r e s u l t . i m a g i n a r y = t h i s −>i m a g i n a r y + o t h e r . i m a g i n a r y ;
return r e s u l t ;
}
};
c l a s s C i r c l e : p u b l i c Shape {
public :
void draw ( ) o v e r r i d e {
/ / S p e c i f i c implementation f o r c i r c l e s
}
};
c l a s s Square : p u b l i c Shape {
public :
void draw ( ) o v e r r i d e {
/ / S p e c i f i c implementation f o r squares
}
};
• Polymorphic Function Calls: You can use pointers or references to the base class
to achieve polymorphism. When a base class pointer/reference is used to call a
virtual function, the appropriate derived class function is invoked at runtime.
Shape * s h a p e P t r ;
Circle circle ;
Square s q u a r e ;
shapePtr = & c i r c l e ;
s h a p e P t r −>draw ( ) ; / / C a l l s C i r c l e ’ s draw ( ) method
s h a p e P t r = &s q u a r e ;
s h a p e P t r −>draw ( ) ; / / C a l l s Square ’ s draw ( ) method
• Dynamic Binding: The mechanism by which the correct function is called at run-
time is called dynamic binding or late binding. It allows for more flexible and
extensible code as new derived classes can be added without changing existing
code that uses the base class interface.
Shape drawing
A common real-world example of dynamic polymorphism in C++ is the use of polymor-
phic behavior through virtual functions when dealing with various shapes in a drawing
application. Let’s create a simplified example to illustrate dynamic polymorphism with
shapes:
# include <iostream >
# include < vector >
c l a s s Shape {
public :
v i r t u a l void draw ( ) {
s t d : : c o u t << " Drawing a g e n e r i c shape " << s t d : : e n d l ;
}
};
c l a s s C i r c l e : p u b l i c Shape {
public :
void draw ( ) o v e r r i d e {
s t d : : c o u t << " Drawing a c i r c l e " << s t d : : e n d l ;
}
};
c l a s s Square : p u b l i c Shape {
public :
void draw ( ) o v e r r i d e {
s t d : : c o u t << " Drawing a s q u a r e " << s t d : : e n d l ;
}
};
i n t main ( ) {
s t d : : v e c t o r <Shape * > shapes ;
Circle circle ;
Square s q u a r e ;
return 0 ;
}
In this example:
• We define a base class Shape with a virtual draw() method. This method has a
common interface for all shapes but is meant to be overridden by derived classes.
• We create two derived classes, Circle and Square, each of which overrides the
draw() method to provide a specific implementation for drawing a circle or square.
• In the main() function, we create instances of Circle and Square. We then store
pointers to these objects in a vector of Shape*, which is possible because of the
polymorphic relationship between the base class and derived classes.
We loop through the vector of Shape* and call the draw() method on each object.
Because the draw() method is marked as virtual and overridden in the derived classes, the
appropriate draw() method for each shape is called at runtime, demonstrating dynamic
polymorphism.
Abstract Classes
Abstract classes are a fundamental concept in object-oriented programming, and they
play an essential role in C++ and similar languages. An abstract class is a class that
cannot be instantiated directly; instead, it is meant to serve as a blueprint for other
classes. Abstract classes are often used to define a common interface and provide some
default behavior that derived classes must implement.
Here are some key characteristics and uses of abstract classes in C++:
• Cannot be Instantiated: You cannot create objects of an abstract class. Attempting
to do so will result in a compilation error. Abstract classes are meant to be used as
base classes for other classes.
• May Contain Pure Virtual Functions: Abstract classes often contain at least one
pure virtual function. A pure virtual function is declared with the virtual keyword
and is followed by = 0;. It has no implementation in the abstract class and must be
implemented by any concrete (i.e., non-abstract) derived class. This enforces that
derived classes provide their own implementation of these functions.
class AbstractShape {
public :
v i r t u a l void draw ( ) = 0 ; / / Pure v i r t u a l f u n c t i o n
v i r t u a l double a r e a ( ) const = 0 ; / / A n o t h e r pure v i r t u a l f u n c t i o n
};
• Can Contain Concrete (Non-Virtual) Functions: Abstract classes can have regular
(non-virtual) member functions that provide a default implementation that can
be shared among derived classes. These functions can be overridden in derived
classes if needed.
class AbstractShape {
public :
/ / C o n c r e t e ( non−v i r t u a l ) f u n c t i o n with d e f a u l t i m p l e m e n t a t i o n
void s e t C o l o r ( const s t d : : s t r i n g & c o l o r ) {
t h i s −> c o l o r = c o l o r ;
}
/ / Pure v i r t u a l f u n c t i o n s
v i r t u a l void draw ( ) = 0 ;
v i r t u a l double a r e a ( ) const = 0 ;
protected :
std : : s t r i n g color ;
};
• Derived Classes Must Implement Pure Virtual Functions: Any class derived from
an abstract class must implement all the pure virtual functions declared in the
base class. Failure to do so will also result in a compilation error.
c l a s s C i r c l e : public AbstractShape {
public :
void draw ( ) o v e r r i d e {
/ / Implement drawing f o r c i r c l e s
}
double a r e a ( ) const o v e r r i d e {
/ / Implement a r e a c a l c u l a t i o n f o r c i r c l e s
}
};
• Useful for Creating Common Interfaces: Abstract classes are useful for defining
common interfaces in situations where multiple classes share a set of functions
but implement them differently. They help ensure consistency and conformity
among derived classes.
Interface Classes
In C++, there is no direct language construct called "interface" as you might find in
some other programming languages like Java or C#. However, you can achieve similar
functionality using abstract classes and pure virtual functions, which serve as interfaces
in C++. These classes act as contracts that derived classes must adhere to by providing
implementations for all the pure virtual functions declared in the base class. This is
often referred to as "interface-like" behavior in C++.
Here’s how you can create an interface-like structure in C++:
50 L E C T URE 4: INHERITANCE AND POLYMORPHISM
c l a s s Shape {
public :
virtual void draw ( ) = 0 ; / / Pure v i r t u a l f u n c t i o n ( i n t e r f a c e method )
virtual double a r e a ( ) const = 0 ; / / A n o t h e r pure v i r t u a l f u n c t i o n ( i n t e r f a c e method
virtual ~Shape ( ) { } / / V i r t u a l d e s t r u c t o r ( recommended f o r polymorphism )
};
In this example:
• Shape is an abstract class that defines an interface with two pure virtual functions,
draw() and area(). Any class inheriting from Shape must provide implementations
for these methods.
Here’s how you can create a concrete class that implements the Shape interface:
c l a s s C i r c l e : p u b l i c Shape {
public :
void draw ( ) o v e r r i d e {
/ / Implement drawing f o r c i r c l e s
}
double a r e a ( ) const o v e r r i d e {
/ / Implement a r e a c a l c u l a t i o n f o r c i r c l e s
}
};
In this example, the Circle class inherits from Shape and provides implementations
for both draw() and area(), adhering to the interface defined by the Shape class.
You can create other classes like Rectangle, Triangle, etc., following the same pattern
to implement the Shape interface.
Although C++ doesn’t have a native keyword for defining interfaces like some other
languages, this approach using abstract classes and pure virtual functions provides simi-
lar functionality. It allows you to define and enforce contracts for classes that implement
specific interfaces, promoting code consistency and polymorphism.
3. Prefer Protected or Private Inheritance: If you don’t want to expose the interface of
the base class to the outside world, consider using protected or private inheritance.
This is often a better choice for implementation inheritance.
4. Avoid Deep Inheritance Hierarchies: Deep hierarchies can lead to complexity and
make code harder to understand and maintain. Aim for shallow hierarchies with a
clear and focused purpose.
5. Declare Virtual Functions Properly: Declare virtual functions in the base class with
a clear understanding of which functions should be overridden by derived classes.
7. Use the override Keyword: In derived classes, use the override keyword when
overriding virtual functions. This helps catch errors at compile time and makes
the code more readable.
8. Avoid Hiding Inherited Functions: When inheriting functions with the same name
as those in the derived class, use the using keyword to bring them into scope rather
than hiding them. Hiding can lead to confusion.
9. Implement Pure Virtual Functions Completely: Derived classes must provide com-
plete implementations for pure virtual functions. Failure to do so results in a
compilation error.
10. Don’t Rely on the Order of Constructor Calls: The order in which constructors are
called for base and derived classes can be complex. Avoid relying on a specific
order and design your classes to be independent of constructor order.
11. Use Static Polymorphism Where Appropriate: Not all polymorphism requires dy-
namic dispatch. If performance is critical, consider using function overloading
and templates for static polymorphism.
12. Document Your Inheritance Hierarchy: Provide clear documentation for your
inheritance hierarchy, including the purpose of each class and its relationship with
others.
13. Consider the Liskov Substitution Principle (LSP): Ensure that derived classes can be
substituted for their base classes without altering the correctness of the program.
14. Test and Verify: Thoroughly test your derived classes to ensure they behave as
expected when inheriting from a base class.
15. Refactor When Necessary: Be open to refactoring your inheritance hierarchy if you
discover design flaws or changing requirements.
16. Use Access Specifiers Thoughtfully: Choose access specifiers (public, protected,
private) carefully to control the visibility of inherited members based on your
design intent.
52 L E C T URE 4: INHERITANCE AND POLYMORPHISM
Adhering to these best practices and guidelines when using inheritance in C++
will help you create maintainable, extensible, and robust code while minimizing
potential pitfalls and design issues.
Summary
Inheritance and polymorphism are two fundamental concepts in C++ that enable the
creation of robust, reusable, and extensible object-oriented code. They play key roles in
structuring class hierarchies and enabling dynamic behavior in C++ programs.
• Inheritance:
• Polymorphism
2. Task 2: Create Derived Classes (Circle and Rectangle) Create two derived classes,
Circle and Rectangle, that inherit from the Shape class.
In the Circle class, declare an attribute radius (double) and a constructor that
initializes it.
In the Rectangle class, declare attributes length and width (both double) and a
constructor that initializes them.
Override the display() function in both the Circle and Rectangle classes to display
specific information about each shape, including its name, color, and relevant
attributes (radius for circles and length/width for rectangles).
Questions
1. What is inheritance in C++? Explain its significance in object-oriented program-
ming.
2. How do you declare a base class in C++? Provide an example of a simple base class.
54 L E C T URE 4: INHERITANCE AND POLYMORPHISM
3. What is a derived class in C++? How does it relate to the base class?
4. Explain the "is-a" relationship between base and derived classes. Provide an ex-
ample to illustrate this relationship.
7. Describe the access specifiers (public, protected, private) in C++ inheritance. How
do they affect the visibility of inherited members?
8. What is the purpose of the virtual keyword in C++ when used with functions in a
base class?
9. How do you prevent a base class function from being overridden in a derived class
in C++?
10. Explain the concept of multiple inheritance in C++. What challenges and ambigui-
ties can arise when using it?
11. What is the "diamond problem" in the context of multiple inheritance, and how
can it be resolved in C++?
12. How can you create an instance of a derived class in C++, and what happens when
you call a base class method on it?
13. What is dynamic polymorphism, and how does it relate to virtual functions and
inheritance in C++?
14. Explain the role of destructors in C++ inheritance. When should a base class
destructor be declared as virtual?
15. Give an example of a real-world scenario where inheritance in C++ can be applied
to model relationships between objects.
Lecture 5: Operator Overloading
• Intuitive Syntax: Operator overloading allows you to use familiar operators (e.g.,
+, -, *, ==) with user-defined types. This makes code more intuitive and read-
able, as it resembles natural mathematical or logical expressions.
• Simplifies Complex Operations: For complex data types or mathematical structures
like matrices, complex numbers, or custom containers, operator overloading can
simplify complex operations, reducing the need for verbose function calls.
• Code Clarity: Overloaded operators can lead to code that closely resembles the
problem domain, making it easier for developers to understand and maintain.
This can improve code clarity and reduce the chances of errors.
• Consistency: Operator overloading allows you to maintain consistency in your
code. When you use operators like +, ==, or « with your custom types, you apply
the same logical meaning as with built-in types.
• Reusability: Well-designed operator overloads can be reused across different projects
and codebases. This reusability saves time and effort when implementing similar
functionality.
56 L E C T URE 5: OPERATOR OVERLOADING
• Custom Behavior: You can tailor the behavior of operators to your specific needs.
For example, you can redefine the + operator to perform vector addition for a
physics simulation or concatenate strings in a custom way.
• Expressiveness: Operator overloading can make code more expressive. For exam-
ple, overloading the « operator for output can allow you to print complex data
structures with a single line of code, enhancing the expressiveness of your code.
• Elegant Syntax: Operator overloading can lead to elegant and concise code, which
can be especially beneficial in math-heavy or scientific applications.
However, it’s important to use operator overloading judiciously and maintain clear
and consistent semantics. Overloading operators in a non-standard or confusing way
can lead to code that is difficult to understand and debug. Therefore, it’s crucial to follow
best practices
and conventions when employing operator overloading in your C++ code.
1. Choose the Operator to Overload: Decide which unary operator you want to over-
load based on the requirements of your class and the desired functionality.
For example, to overload the unary minus operator -, you would define the function
as follows:
MyClass operator −() {
/ / D e f i n e custom b e h a v i o r f o r t h e unary minus o p e r a t o r
}
3. Implement the Custom Behavior: Inside the operator function, implement the
custom behavior you want for the unary operator. This behavior should make
sense in the context of your class.
4. Return the Result: Ensure that the operator function returns an object of the same
type as your class. This typically involves creating a new object with the modified
values or returning the current object with its values changed.
public :
Complex ( double r , double i ) : r e a l ( r ) , imag ( i ) { }
/ / O v e r l o a d t h e unary minus o p e r a t o r ( −)
Complex operator −() {
return Complex(− r e a l , −imag ) ;
}
In this example, when you apply the unary minus operator to a Complex object, it
negates both the real and imaginary parts of the complex number.
6. Usage Example:
Complex a ( 3 . 0 , 4 . 0 ) ;
Complex b = −a ; / / Now b c o n t a i n s ( − 3 .0 , −4 .0 )
For example, to overload the addition operator +, you would define the function
as follows:
Y o u r C l a s s operator + ( const Y o u r C l a s s& r h s ) const {
/ / D e f i n e custom b e h a v i o r f o r t h e a d d i t i o n o p e r a t o r
}
1. Implement the Custom Behavior: Inside the operator function, implement the
custom behavior you want for the binary operator. This behavior should make
sense in the context of your class and provide meaningful results when applied to
objects of your class.
2. Return the Result: Ensure that the operator function returns an object of the
same type as your class. This typically involves creating a new object with the
modified values or returning the current object with its values changed based on
the operation.
public :
V e c t o r ( double xV al , double y V a l ) : x ( x V a l ) , y ( y V a l ) { }
In this example, when you apply the addition operator to two Vector objects, it
performs vector addition by adding their respective x and y components.
4. Usage Example:
Vector v1 ( 1 . 0 , 2 . 0 ) ;
V e c t o r v2 ( 2 . 0 , 3 . 0 ) ;
V e c t o r v3 = v 1 + v2 ; / / Now v3 c o n t a i n s ( 3 . 0 , 5 . 0 )
1. Choose the Operator to Overload: Decide which comparison operator you want to
overload based on the requirements of your class and the desired functionality.
For example, to overload the equality operator ==, you would define the function
as follows:
bool operator = = ( const Y o u r C l a s s& r h s ) const {
/ / D e f i n e custom b e h a v i o r f o r t h e e q u a l i t y o p e r a t o r
}
3. Implement the Custom Behavior: Inside the operator function, implement the
custom behavior you want for the comparison operator. This behavior should
make sense in the context of your class and provide meaningful results when
comparing objects of your class.
4. Return the Result: Ensure that the operator function returns a bool value (true or
false) based on the result of the comparison.
public :
Person ( const s t d : : s t r i n g & n , i n t a ) : name ( n ) , age ( a ) { }
In this example, the operator== function checks whether two Person objects have
the same name and age.
6. Usage Example:
Person p e r s o n 1 ( " A l i c e " , 3 0 ) ;
Person pe rs on 2 ( " Bob " , 2 5 ) ;
Person p erson3 ( " A l i c e " , 3 0 ) ;
bool r e s u l t 1 = ( p e r s o n 1 == pe rs on 2 ) ; // false
bool r e s u l t 2 = ( p e r s o n 1 == pe rson3 ) ; / / true
7. Overloading Other Comparison Operators: You can similarly overload other com-
parison operators like !=, <, >, <=, and >= following the same principles.
1. Choose the Data Structure: First, determine the data structure that your class
represents and define it internally. It could be an array, a vector, a linked list, or
any other data structure you need.
Here, T is the type of the elements you want to access, and index is the position of
the element you want to access.
3. Implement the Custom Behavior: Inside the operator function, implement the
custom behavior for accessing elements based on the given index. Ensure that
your implementation correctly handles out-of-range indices, if applicable.
4. Return the Result: The subscript operator should return a reference to the element
you want to access. This allows you to both read and modify the element through
the subscript operator.
public :
DynamicArray ( s i z e _ t s z ) : s i z e ( s z ) {
d a t a = new i n t [ s i z e ] ;
}
/ / D e s t r u c t o r t o r e l e a s e memory
~ DynamicArray ( ) {
delete [ ] data ;
}
};
In this example, the subscript operator allows you to access elements of the Dy-
namicArray as if it were a regular C++ array.
6. Usage Example:
DynamicArray a r r ( 5 ) ;
a r r [ 0 ] = 1 0 ; / / Assign value to the f i r s t element
i n t v a l u e = a r r [ 0 ] ; / / R e t r i e v e t h e v a l u e from t h e f i r s t e l e m e n t
7. Error Handling: Ensure that you handle out-of-range indices gracefully, either by
throwing an exception (as shown in the example) or using an alternative error-
handling mechanism that suits your needs.
Here, return_type is the type of value you want the "function" to return, and
parameter_list represents any parameters you want to pass to the "function."
2. Implement the Custom Behavior: Inside the operator function, implement the
custom behavior that you want your object to exhibit when it is called like a
function. The behavior should be meaningful and relevant to your class’s purpose.
3. Return the Result: Ensure that the operator function returns a value of the specified
return_type if applicable
4. Example - Overloading the Function Call Operator (()): Here’s an example of over-
loading the function call operator for a Multiplier class, which multiplies an integer
by a specified factor:
class Multiplier {
private :
int factor ;
public :
Multiplier ( int f ) : factor ( f ) { }
In this example, an object of the Multiplier class can be used as a function that
multiplies an integer by the factor specified during its construction.
5. Usage Example:
Mu lt ip li er multiplyBy2 ( 2 ) ;
int r e s u l t = multiplyBy2 ( 5 ) ; / / C a l l s t h e M u l t i p l i e r o b j e c t a s i f i t were a f u n c t i o
/ / r e s u l t i s now 1 0
6. Customization: Overloading the function call operator allows you to customize the
behavior of your class objects in a way that makes sense for your specific applica-
tion. This can be particularly useful in scenarios where you want to encapsulate
complex or specialized functionality within an object.
7. Functor Use Cases: Functors are often used in algorithms that accept a function-
like object as a parameter, such as standard library algorithms (std::sort, std::transform)
or custom callback mechanisms.
Overloading Type Conversion Operators
Overloading type conversion operators in C++ allows you to define custom type conver-
sions between your user-defined class and other data types. This feature lets you control
how objects of your class can be implicitly or explicitly converted to other data types,
enhancing the usability and versatility of your class.
Here’s how to overload type conversion operators in C++:
1. Choose the Target Data Type: Determine the data type to which you want to
convert objects of your class. This could be a built-in type, another user-defined
type, or even a standard library type.
3. Implement the Custom Conversion Logic: Inside the conversion operator function,
implement the custom logic to convert an object of your class to the target data
type. Ensure that the conversion makes sense in the context of your class.
4. Return the Result: Return the value of the target data type after the conversion
public :
F r a c t i o n ( i n t num, i n t denom ) : numerator (num ) , denominator ( denom ) { }
In this example, when an object of the Fraction class is used in a context that
expects an int, the conversion operator is automatically invoked to return the
integer part of the fraction.
6. Usage Example:
64 L E C T URE 5: OPERATOR OVERLOADING
Fraction fraction (5 , 2 ) ;
int intValue = fraction ; / / Implicit conversion to int
/ / i n t V a l u e i s now 2
3. Ambiguity: Avoid creating ambiguous situations where the compiler cannot deter-
mine which operator to use. This can occur when overloading multiple operators
with similar parameter types.
5. Overloading Too Many Operators: Overloading many operators for a class can
lead to code that is difficult to understand and maintain. Focus on overloading
operators that make the most sense for your class’s purpose.
6. Incorrect Return Types: Ensure that the return type of the overloaded operator
matches the expected type based on the operator’s semantics.
Best Practices:
1. Follow Standard Semantics: Overloaded operators should maintain the standard
semantics of the operator as closely as possible. Users of your class should not be
surprised by unexpected behavior.
3. Overload with Purpose: Overload operators with a clear purpose that aligns with
the class’s intended use. Avoid overloading operators just for the sake of doing so.
4. Keep Operator Overloads Simple: Overloaded operators should have concise and
readable implementations. If the logic is complex, consider moving it to a separate
function and calling that function from the operator overload.
5. Use const Correctness: Whenever possible, use const correctly to indicate that an
operator overload does not modify the state of the object.
6. Test Thoroughly: Write comprehensive unit tests to ensure that overloaded opera-
tors work as expected in various scenarios.
7. Avoid Hidden Conversions: Be cautious when overloading operators that can lead
to hidden type conversions, as these can lead to unexpected behavior.
10. Use Existing Conventions: Follow established naming conventions for operator
overloads to make your code more readable and maintainable.
By adhering to these best practices and avoiding common pitfalls, you can effectively
and safely overload operators in your C++ classes, enhancing code clarity and user
experience.
Summary
Operator overloading in C++ is a powerful feature that allows you to redefine the behavior
of standard C++ operators for user-defined types, such as classes and structures. Here’s
a summary of key points regarding operator overloading:
• Operators That Can Be Overloaded: Common operators that can be overloaded in-
clude arithmetic operators (+, -, *, /), comparison operators (==, !=, <, >,
<=, >=), logical operators (&&, ||, !), and many others.
• Type Conversion: You can use operator overloading to define custom type conver-
sions between your class and other data types.
• Code Clarity: Properly overloaded operators can enhance code clarity by allowing
you to write code that resembles natural mathematical or logical expressions.
Practice Lab
1. Exercise 1: Overloading the Arithmetic Operators In this exercise, you will create a
Complex class to represent complex numbers and overload arithmetic operators
(+, -, *, /) for it. Implement the following:
Define the Complex class with real and imaginary parts. Overload the arithmetic
operators to perform complex number operations. Implement a display() method
to print complex numbers.
2. Exercise 2: Overloading the Equality and Inequality Operators
In this exercise, you will create a Person class to represent individuals and overload
the equality (==) and inequality (!=) operators for it. Implement the following:
Define the Person class with name and age attributes. Overload the equality and
inequality operators to compare Person objects based on their attributes.
3. Exercise 3: Overloading the Index Operator In this exercise, you will create a Matrix
class to represent 2D matrices and overload the index operator ([]) to access matrix
elements. Implement the following:
Define the Matrix class with a 2D array. Overload the index operator to access
elements using row and column indices.
Questions
1. What is operator overloading in C++? Provide a brief explanation.
2. Why is operator overloading useful in C++? Give an example scenario where oper-
ator overloading can improve code readability.
68 L E C T URE 5: OPERATOR OVERLOADING
4. Create a Complex class to represent complex numbers (with real and imaginary
parts). Overload the addition operator (+) to perform complex number addition.
5. In the Complex class, overload the subtraction operator (-) to perform complex
number subtraction.
6. Define a Date class to represent dates with day, month, and year. Overload the
equality (==) operator to compare two Date objects for equality.
7. In the Date class, overload the less-than operator (<) to compare two Date objects
to determine which date comes first.
8. Create a Book class to represent books with attributes like title, author, and ISBN.
Overload the stream insertion operator («) to display book details.
9. Write a code snippet that demonstrates the use of the overloaded stream insertion
operator to display book details.
10. Design a Matrix class to represent 2D matrices with a dynamic array. Overload the
index operator ([]) to access matrix elements.
11. Provide an example of how to access an element in a Matrix object using the
overloaded index operator.
12. Define a Counter class that keeps track of a count value. Overload the function
call operator () to increment the count value by a specified amount.
13. Write a code snippet that demonstrates the use of the overloaded function call
operator to increment the count value.
15. Provide an example where an object of the Temperature class is implicitly con-
verted to Celsius.
16. Design a class representing a 2D vector and overload the multiplication operator
(*) to implement scalar multiplication of vectors.
17. Create a class representing a complex number and overload the division operator
(/) to perform complex number division.
18. List three best practices to follow when overloading operators in C++.
• Purpose: Header files serve as the interface to your code. They provide a way for
other parts of your program to access and use the functionality you’ve defined.
• Extensions: Header files often have extensions like .h or .hpp. The choice of exten-
sion is not standardized, so you can use what you prefer.
• Include Guards: To prevent multiple inclusions of the same header file, you should
use include guards. These are preprocessor directives that ensure the header is
included only once in a source file. For example:
# i f n d e f MYHEADER_H
# d e f i n e MYHEADER_H
# e n d i f / / MYHEADER_H
• Purpose: Implementation files provide the details of how functions and classes
work. They contain the executable code that implements the behavior declared in
the header files.
• Contents: Implementation files include the function definitions, method imple-
mentations, variable initializations, and any other code that isn’t part of the inter-
face.
• Extensions: Implementation files often use the .cpp extension, although it’s not
strictly required.
Example
Let’s say you have a class called Calculator that you want to use in your program. You
would typically organize your code as follows:
Calculator.h (Header File):
# i f n d e f CALCULATOR_H
# d e f i n e CALCULATOR_H
class Calculator {
public :
Calculator ( ) ; / / Constructor
i n t add ( i n t a , i n t b ) ;
int subtract ( int a , int b ) ;
int multiply ( int a , int b ) ;
double d i v i d e ( i n t a , i n t b ) ;
};
# e n d i f / / CALCULATOR_H
Calculator.cpp (Implementation File):
# include " C a l c u l a t o r . h"
Calculator : : Calculator () {
/ / C o n s t r u c t o r implementation
}
i n t C a l c u l a t o r : : add ( i n t a , i n t b ) {
return a + b ;
}
double C a l c u l a t o r : : d i v i d e ( i n t a , i n t b ) {
i f ( b == 0 ) {
throw " D i v i s i o n by z e r o ! " ;
}
return s t a t i c _ c a s t <double > ( a ) / b ;
}
i n t main ( ) {
Calculator calc ;
i n t r e s u l t = c a l c . add ( 5 , 3 ) ;
s t d : : c o u t << " 5 + 3 = " << r e s u l t << s t d : : e n d l ;
return 0 ;
}
In this example:
• Calculator.h defines the class Calculator and its member functions’ declarations.
• Main.cpp uses the Calculator class by including its header file and creating an
instance of the class.
1. target: The target is the name of the file or action you want to build. It could be an
executable file, an object file, or even just a label for a particular task.
Example Makefile
Here’s a simple example of a Makefile for a C++ program with two source files, main.cpp
and utils.cpp, and one header file, utils.h:
CXX = g++
CXXFLAGS = −s t d =c + + 1 1 −W a l l
SRCDIR = s r c
BINDIR = bin
TARGET = myapp
SOURCES = $ ( SRCDIR ) / main . cpp $ ( SRCDIR ) / u t i l s . cpp
OBJECTS = $ ( SOURCES : $ ( SRCDIR ) / % . cpp=$ ( BINDIR ) / % . o )
clean :
rm −f $ ( BINDIR ) / * . o $ ( BINDIR ) / $ ( TARGET )
In this example:
• CXX specifies the C++ compiler to use (in this case, g++).
• CXXFLAGS specifies compiler flags, such as the C++ version and warning options.
• SRCDIR and BINDIR define source code and binary directories, respectively.
• The first rule ((BIN DIR)/(TARGET)) specifies how to build the final executable
from object files.
• The second rule specifies how to compile each source file into an object file.
This will compile your program and create the executable. You can also use specific
targets, such as clean, to perform other tasks:
make c l e a n
CMake
CMake is an open-source cross-platform build system and project management tool
widely used in the C and C++ programming communities (although it supports many
other languages as well). CMake simplifies the process of configuring, building, testing,
and packaging software projects. It generates build files and scripts (e.g., Makefiles,
Visual Studio project files) for various build tools and platforms, allowing developers to
work on their projects across different operating systems with ease.
Here are the key features and concepts of CMake:
• Multiple Generators: CMake can generate build files for various build systems,
including Makefiles, Ninja, Visual Studio, and Xcode. Developers can choose the
generator that best suits their development environment.
• Testing: CMake includes built-in support for testing. It allows you to define test
cases, run tests, and generate test reports as part of the build process.
• Packaging: CMake supports packaging your project for distribution. It can generate
installer packages, compressed archives, or other distribution formats.
• Extensibility: CMake can be extended with custom CMake modules and scripts to
support specific project requirements.
cmake_minimum_required ( VERSION 3 . 1 0 )
p r o j e c t ( MyProject )
s e t ( CMAKE_CXX_STANDARD 1 7 )
# Define source f i l e s
s e t ( SOURCES
main . cpp
u t i l s . cpp
)
# S p e c i f y dependencies ( e . g . , l i b r a r i e s )
t a r g e t _ l i n k _ l i b r a r i e s ( myapp
$ { DEPENDENCY_LIBRARIES }
)
# C o n f i g u r a t i o n o p t i o n s ( e . g . , compile f l a g s )
t a r g e t _ c o m p i l e _ o p t i o n s ( myapp PRIVATE
−W a l l
)
To build a project using CMake, you would typically run the following commands in
your project directory:
mkdir b u i l d # C r e a t e a b u i l d d i r e c t o r y ( out−of−s o u r c e b u i l d )
cd b u i l d
cmake . . # Generate build f i l e s
make # B u i l d t h e p r o j e c t ( or use t h e a p p r o p r i a t e b u i l d t o o l )
Lecture 7: Design Patterns
Design patterns are reusable solutions to common software design problems that occur
during the development of software applications. They provide best practices, guidelines,
and templates for structuring code and solving recurring design challenges. Design pat-
terns help improve code readability, maintainability, and extensibility while promoting
good software engineering practices. Here are some important design patterns:
Creational Patterns
Singleton Pattern
Ensures that a class has only one instance and provides a global point of access to that
instance.
The Singleton Pattern is a creational design pattern that ensures a class has only one
instance and provides a global point of access to that instance. It is one of the simplest
design patterns but can be quite powerful in scenarios where exactly one object should
coordinate actions across the system, such as a configuration manager, a logging service,
or a database connection pool.
Key characteristics of the Singleton Pattern:
• Single Instance: A Singleton class should have only one instance, which is created
lazily or at the time of the first request.
• Global Access: It provides a way to access the single instance from any part of the
application, typically through a static method.
• Initialization Control: The pattern allows for careful control over how and when
the singleton instance is created, ensuring that it meets specific requirements.
public :
/ / S t a t i c method t o a c c e s s t h e s i n g l e t o n i n s t a n c e
static Singleton * getInstance ( ) {
i f ( ! instance ) {
i n s t a n c e = new S i n g l e t o n ( ) ;
76 L E C T URE 7: DESIGN PAT TERNS
}
return i n s t a n c e ;
}
In this example:
2. The getInstance method is used to get the single instance of the Singleton class. If
an instance does not exist, it is created lazily.
3. The static instance variable holds the reference to the single instance, and it is
initialized as nullptr.
/ / Both p o i n t e r s p o i n t t o t h e same i n s t a n c e
i f ( s i n g l e t o n 1 == s i n g l e t o n 2 ) {
s t d : : c o u t << " S i n g l e t o n i n s t a n c e i s t h e same . " << s t d : : e n d l ;
} else {
s t d : : c o u t << " S i n g l e t o n i n s t a n c e i s d i f f e r e n t . " << s t d : : e n d l ;
}
/ / Clean up ( o p t i o n a l )
delete singleton1 ;
return 0 ;
}
It’s important to note that the Singleton Pattern can be implemented in various ways,
and the example provided above is a simple one. Depending on your requirements
and the complexity of your application, you may need to consider thread safety, lazy
initialization, or other aspects when implementing a Singleton. Additionally, some
modern C++ standards and libraries provide alternative approaches to ensure thread
safety and lazy initialization.
1. Abstract Creator: The pattern defines an abstract class (or interface) known as the
"Creator" that declares a factory method. This method is responsible for creating
objects of a related "Product" class.
3. Product Class: The Product class is the common interface for all objects created
by the factory method. It defines the properties and behaviors that all concrete
product classes must implement.
Here’s a typical implementation of the Factory Method Pattern in C++:
/ / Product i n t e r f a c e
c l a s s Product {
public :
v i r t u a l void doSomething ( ) = 0 ;
v i r t u a l ~ Product ( ) { }
};
/ / Concrete Product A
c l a s s ConcreteProductA : public Product {
public :
void doSomething ( ) o v e r r i d e {
s t d : : c o u t << " P r o d u c t A does something . " << s t d : : e n d l ;
}
};
/ / Concrete Product B
c l a s s ConcreteProductB : public Product {
public :
void doSomething ( ) o v e r r i d e {
s t d : : c o u t << " P r o d u c t B does something . " << s t d : : e n d l ;
}
};
/ / Creator abstract c l a s s
class Creator {
public :
v i r t u a l Product * factoryMethod ( ) = 0 ;
};
/ / Concrete Creator A
c l a s s ConcreteCreatorA : public Creator {
public :
Product * factoryMethod ( ) o v e r r i d e {
return new C o n c r e t e P r o d u c t A ( ) ;
}
};
78 L E C T URE 7: DESIGN PAT TERNS
/ / Concrete Creator B
c l a s s ConcreteCreatorB : public Creator {
public :
Product * factoryMethod ( ) o v e r r i d e {
return new C o n c r e t e P r o d u c t B ( ) ;
}
};
In this example:
1. The Product interface defines the common interface for all products created by
the factory method.
3. The Creator abstract class declares the factory method, which returns a Prod-
uct object. Concrete creators, such as ConcreteCreatorA and ConcreteCreatorB,
implement this method to create specific products.
C r e a t o r * c r e a t o r B = new C o n c r e t e C r e a t o r B ( ) ;
P r o d u c t * productB = c r e a t o r B −>f a c t o r y M e t h o d ( ) ;
productB −>doSomething ( ) ;
delete creatorA ;
delete creatorB ;
delete productA ;
delete productB ;
return 0 ;
}
1. Abstract Factory: The pattern defines an abstract factory interface that declares a
set of creation methods for creating related objects, often organized into families.
2. Concrete Factories: Concrete subclasses of the abstract factory implement the
factory interface. Each concrete factory is responsible for creating a specific family
of related objects.
3. Abstract Products: The pattern defines abstract product interfaces for each type of
object that the factories can create. These interfaces declare the common methods
that all concrete products must implement.
4. Concrete Products: Concrete product classes implement the abstract product
interfaces. Each concrete factory creates its own set of concrete products.
/ / C o n c r e t e P r o d u c t A1
c l a s s ConcreteProductA1 : public AbstractProductA {
public :
void o p e r a t i o n A ( ) o v e r r i d e {
s t d : : c o u t << " P r o d u c t A1 does o p e r a t i o n A . " << s t d : : e n d l ;
}
};
/ / C o n c r e t e P r o d u c t A2
c l a s s ConcreteProductA2 : public AbstractProductA {
public :
void o p e r a t i o n A ( ) o v e r r i d e {
s t d : : c o u t << " P r o d u c t A2 does o p e r a t i o n A . " << s t d : : e n d l ;
}
};
/ / A b s t r a c t Product B
class AbstractProductB {
public :
v i r t u a l void o p e r a t i o n B ( ) = 0 ;
virtual ~AbstractProductB ( ) { }
};
/ / C o n c r e t e P r o d u c t B1
c l a s s ConcreteProductB1 : public AbstractProductB {
public :
void o p e r a t i o n B ( ) o v e r r i d e {
s t d : : c o u t << " P r o d u c t B1 does o p e r a t i o n B . " << s t d : : e n d l ;
}
};
/ / C o n c r e t e P r o d u c t B2
c l a s s ConcreteProductB2 : public AbstractProductB {
public :
void o p e r a t i o n B ( ) o v e r r i d e {
80 L E C T URE 7: DESIGN PAT TERNS
/ / Abstract Factory
class AbstractFactory {
public :
virtual AbstractProductA * createProductA ( ) = 0;
virtual AbstractProductB * createProductB ( ) = 0 ;
virtual ~AbstractFactory () { }
};
/ / Concrete Factory 1
c l a s s ConcreteFactory1 : public A b s t r a c t F a c t o r y {
public :
AbstractProductA * createProductA ( ) override {
return new C o n c r e t e P r o d u c t A 1 ( ) ;
}
/ / Concrete Factory 2
c l a s s ConcreteFactory2 : public A b s t r a c t F a c t o r y {
public :
AbstractProductA * createProductA ( ) override {
return new C o n c r e t e P r o d u c t A 2 ( ) ;
}
In this example:
1. There are two families of products, A and B, each with two concrete implemen-
tations (e.g., ConcreteProductA1, ConcreteProductA2, ConcreteProductB1, Con-
creteProductB2).
2. The AbstractFactory interface declares the creation methods for product A and
product B.
/ / Use t h e c r e a t e d p r o d u c t s . . .
delete factory1 ;
delete factory2 ;
delete productA1 ;
delete productB1 ;
delete productA2 ;
delete productB2 ;
return 0 ;
}
Builder Pattern
Separates the construction of a complex object from its representation, allowing the
same construction process to create different representations.
The Builder Pattern is a creational design pattern that separates the construction
of a complex object from its representation, allowing the same construction process to
create different representations of the object. It is particularly useful when you need to
create objects with many optional components or configurations.
Key characteristics of the Builder Pattern:
• Director: The pattern includes a director class that orchestrates the construction
process. The director uses a builder interface to construct the object step by step.
• Builder Interface: The builder interface defines a set of methods for building
various parts of the complex object. These methods typically include functions for
setting attributes, adding components, and finalizing the object.
• Concrete Builders: Concrete builder classes implement the builder interface and
provide specific implementations for constructing objects with different configu-
rations.
• Client: The client code interacts with the director to initiate the construction
process, specifying which concrete builder to use. The client can also access the
constructed product when the builder is done.
/ / Product c l a s s
c l a s s Computer {
82 L E C T URE 7: DESIGN PAT TERNS
public :
void setCPU ( const s t d : : s t r i n g & cpu ) {
cpu_ = cpu ;
}
void d i s p l a y I n f o ( ) {
s t d : : c o u t << "CPU : " << cpu_ << s t d : : e n d l ;
s t d : : c o u t << "RAM : " << ram_ << s t d : : e n d l ;
s t d : : c o u t << " S t o r a g e : " << s t o r a g e _ << s t d : : e n d l ;
}
private :
std : : s t r i n g cpu_ ;
std : : s t r i n g ram_ ;
std : : string storage_ ;
};
/ / Builder interface
c l a s s ComputerBuilder {
public :
v i r t u a l void buildCPU ( ) = 0 ;
v i r t u a l void buildRAM ( ) = 0 ;
v i r t u a l void b u i l d S t o r a g e ( ) = 0 ;
v i r t u a l Computer * g e t R e s u l t ( ) = 0 ;
};
/ / C o n c r e t e b u i l d e r f o r a gaming computer
c l a s s GamingComputerBuilder : p u b l i c ComputerBuilder {
public :
void buildCPU ( ) o v e r r i d e {
computer_ −>setCPU ( " High−end Gaming CPU" ) ;
}
void buildRAM ( ) o v e r r i d e {
computer_ −>setRAM ( " 1 6GB Gaming RAM" ) ;
}
void b u i l d S t o r a g e ( ) o v e r r i d e {
computer_ −> s e t S t o r a g e ( " 5 1 2GB SSD" ) ;
}
Computer * g e t R e s u l t ( ) o v e r r i d e {
return computer_ ;
}
GamingComputerBuilder ( ) {
computer_ = new Computer ( ) ;
}
private :
Computer * computer_ ;
};
/ / Director
c l a s s Co mpu te rDi rec tor {
public :
Computer * b u i l d ( ComputerBuilder * b u i l d e r ) {
b u i l d e r −>buildCPU ( ) ;
b u i l d e r −>buildRAM ( ) ;
b u i l d e r −> b u i l d S t o r a g e ( ) ;
return b u i l d e r −> g e t R e s u l t ( ) ;
}
};
i n t main ( ) {
Co mpu te rDi rec tor d i r e c t o r ;
GamingComputerBuilder g a m i n g B u i l d e r ;
d e l e t e gamingComputer ;
return 0 ;
}
In this example:
2. The ComputerBuilder interface defines methods for building various parts of the
computer.
5. The client code (in main) interacts with the director to create a gaming computer.
Structural Patterns
Adapter Pattern
Allows the interface of an existing class to be used as another interface.
The Adapter Pattern is a structural design pattern that allows objects with incompati-
ble interfaces to work together. It acts as a bridge between two interfaces, enabling them
to collaborate without changing their source code. The Adapter Pattern is particularly
84 L E C T URE 7: DESIGN PAT TERNS
useful when integrating existing classes or libraries with new code or when dealing with
legacy systems.
Key characteristics of the Adapter Pattern:
• Target: The "target" is the interface that the client code expects to work with. It
defines the operations or methods that the client code uses.
• Adaptee: The "adaptee" is the class or component that has a different interface
from the target. It is the class that needs to be integrated or adapted to work with
the client code.
• Adapter: The "adapter" is a class that implements the target interface while con-
taining an instance of the adaptee. It acts as an intermediary, translating requests
from the client code into calls to the adaptee’s methods.
/ / Target i n t e r f a c e
class Target {
public :
v i r t u a l void r e q u e s t ( ) = 0 ;
};
/ / Adaptee
c l a s s Adaptee {
public :
void s p e c i f i c R e q u e s t ( ) {
s t d : : c o u t << " Adaptee ’ s s p e c i f i c r e q u e s t . " << s t d : : e n d l ;
}
};
/ / Adapter
c l a s s Adapter : public Target {
private :
Adaptee * a d a p t e e _ ;
public :
A d a p t e r ( Adaptee * a da pt ee ) : a d a p t e e _ ( a da pt ee ) { }
void r e q u e s t ( ) o v e r r i d e {
s t d : : c o u t << " A d a p t e r i s a d a p t i n g t h e r e q u e s t . " << s t d : : e n d l ;
adaptee_ −> s p e c i f i c R e q u e s t ( ) ;
}
};
i n t main ( ) {
Adaptee * a da pt ee = new Adaptee ( ) ;
T a r g e t * a d a p t e r = new A d a p t e r ( a da pt ee ) ;
a d a p t e r −>r e q u e s t ( ) ;
d e l e t e ad a pt ee ;
delete adapter ;
return 0 ;
}
In this example:
• Target is the interface that the client code expects, and it defines the request
method.
• Adaptee is the class with a different interface that needs to be integrated. It has a
specificRequest method.
• Adapter is a class that implements the Target interface and contains an instance
of Adaptee. It adapts the request method to call specificRequest internally.
• In the main function, an Adaptee object is created, and then an Adapter object
is created, passing the Adaptee as a parameter. The Adapter is used to make the
request.
Decorator Pattern
Attaches additional responsibilities to an object dynamically, providing a flexible alter-
native to subclassing.
The Decorator Pattern is a structural design pattern that allows you to attach addi-
tional responsibilities to an object dynamically. It is used to extend the functionality of
objects in a flexible and reusable way without altering their source code. This pattern is
based on the principle of composition rather than inheritance, which promotes open-
closed principle compliance (classes are open for extension but closed for modification).
Key characteristics of the Decorator Pattern:
• Decorator: The "decorator" is an abstract class that also implements the compo-
nent interface. It has an association with the component and can add or override
behaviors.
• Concrete Decorators: "Concrete decorators" are classes that extend the decorator
class. They provide specific implementations for adding responsibilities to the
component.
/ / Component i n t e r f a c e
class Coffee {
public :
v i r t u a l s t d : : s t r i n g g e t D e s c r i p t i o n ( ) const = 0 ;
v i r t u a l double c o s t ( ) const = 0 ;
};
/ / C o n c r e t e Component
c l a s s Espresso : public Coffee {
public :
s t d : : s t r i n g g e t D e s c r i p t i o n ( ) const o v e r r i d e {
return " E s p r e s s o " ;
}
double c o s t ( ) const o v e r r i d e {
return 1 . 9 9 ;
}
};
/ / Decorator abstract c l a s s
c l a s s CoffeeDecorator : public Coffee {
protected :
Coffee * coffee ;
public :
CoffeeDecorator ( Coffee * coffee ) : coffee ( coffee ) { }
s t d : : s t r i n g g e t D e s c r i p t i o n ( ) const o v e r r i d e {
return c o f f e e −> g e t D e s c r i p t i o n ( ) ;
}
double c o s t ( ) const o v e r r i d e {
return c o f f e e −> c o s t ( ) ;
}
};
/ / Concrete Decorator 1
c l a s s MilkDecorator : public CoffeeDecorator {
public :
MilkDecorator ( Coffee * c o f f e e ) : CoffeeDecorator ( c o f f e e ) { }
s t d : : s t r i n g g e t D e s c r i p t i o n ( ) const o v e r r i d e {
return c o f f e e −> g e t D e s c r i p t i o n ( ) + " , Milk " ;
}
double c o s t ( ) const o v e r r i d e {
return c o f f e e −> c o s t ( ) + 0 . 5 0 ;
}
};
/ / Concrete Decorator 2
c l a s s SugarDecorator : public CoffeeDecorator {
public :
SugarDecorator ( Coffee * c o f f e e ) : CoffeeDecorator ( c o f f e e ) { }
s t d : : s t r i n g g e t D e s c r i p t i o n ( ) const o v e r r i d e {
return c o f f e e −> g e t D e s c r i p t i o n ( ) + " , Sugar " ;
}
double c o s t ( ) const o v e r r i d e {
return c o f f e e −> c o s t ( ) + 0 . 2 5 ;
}
};
i n t main ( ) {
C o f f e e * e s p r e s s o = new E s p r e s s o ( ) ;
s t d : : c o u t << " Order : " << e s p r e s s o −> g e t D e s c r i p t i o n ( )
<< " , Cost : $ " << e s p r e s s o −> c o s t ( ) << s t d : : e n d l ;
C o f f e e * m i l k E s p r e s s o = new M i l k D e c o r a t o r ( e s p r e s s o ) ;
s t d : : c o u t << " Order : " << m i l k E s p r e s s o −> g e t D e s c r i p t i o n ( )
<< " , Cost : $ " << m i l k E s p r e s s o −> c o s t ( ) << s t d : : e n d l ;
C o f f e e * s u g a r M i l k E s p r e s s o = new S u g a r D e c o r a t o r ( m i l k E s p r e s s o ) ;
s t d : : c o u t << " Order : " << s u g a r M i l k E s p r e s s o −> g e t D e s c r i p t i o n ( )
<< " , Cost : $ " << s u g a r M i l k E s p r e s s o −> c o s t ( ) << s t d : : e n d l ;
delete espresso ;
delete milkEspresso ;
delete sugarMilkEspresso ;
return 0 ;
}
In this example:
In the main function, decorators are used to enhance an Espresso object with addi-
tional responsibilities.
Composite Pattern
Composes objects into tree structures to represent part-whole hierarchies. Clients can
treat individual objects and compositions of objects uniformly.
The Composite Pattern is a structural design pattern that allows you to compose
objects into tree structures to represent part-whole hierarchies. It lets clients treat
individual objects and compositions of objects uniformly. The Composite Pattern is
88 L E C T URE 7: DESIGN PAT TERNS
particularly useful when you need to work with complex structures that have a recursive
or hierarchical nature.
Key characteristics of the Composite Pattern:
2. Leaf: A "leaf" is a concrete class that implements the component interface. Leaf
objects represent the individual objects that make up the composite.
/ / Component i n t e r f a c e
c l a s s Component {
public :
v i r t u a l void o p e r a t i o n ( ) const = 0 ;
v i r t u a l ~Component ( ) { }
};
/ / Leaf
c l a s s L e a f : p u b l i c Component {
public :
void o p e r a t i o n ( ) const o v e r r i d e {
s t d : : c o u t << " L e a f does something . " << s t d : : e n d l ;
}
};
/ / Composite
c l a s s Composite : p u b l i c Component {
private :
s t d : : v e c t o r <Component * > c h i l d r e n ;
public :
void add ( Component * component ) {
c h i l d r e n . push_back ( component ) ;
}
void o p e r a t i o n ( ) const o v e r r i d e {
s t d : : c o u t << " Composite does something . " << s t d : : e n d l ;
f o r ( const auto& c h i l d : c h i l d r e n ) {
c h i l d −>o p e r a t i o n ( ) ;
}
}
~ Composite ( ) {
f o r ( auto& c h i l d : c h i l d r e n ) {
delete child ;
}
}
};
i n t main ( ) {
L e a f * l e a f 1 = new L e a f ( ) ;
L e a f * l e a f 2 = new L e a f ( ) ;
Composite * composite = new Composite ( ) ;
composite −>add ( l e a f 1 ) ;
composite −>add ( l e a f 2 ) ;
composite −>o p e r a t i o n ( ) ;
d e l e t e composite ;
return 0 ;
}
In this example:
2. Leaf is a concrete leaf class that implements the Component interface. It represents
individual objects.
4. In the main function, leaf and composite objects are created and organized into
a hierarchical structure. The composite’s operation method traverses the entire
hierarchy, executing the operation method of each component.
Proxy Pattern
Provides a surrogate or placeholder for another object to control access to it.
The Proxy Pattern is a structural design pattern that provides a surrogate or place-
holder for another object to control access to it. It allows you to add an extra layer of
control over the access to an object, typically to implement lazy initialization, access
control, logging, monitoring, or other functionality.
Key characteristics of the Proxy Pattern:
1. Real Subject: The "real subject" is the actual object that the proxy represents. It is
the object to which access is being controlled or augmented.
2. Proxy: The "proxy" is an interface or class that provides the same interface as the
real subject. It acts as an intermediary between the client and the real subject,
controlling access and adding behavior as needed.
90 L E C T URE 7: DESIGN PAT TERNS
3. Lazy Initialization: Proxies are often used for lazy initialization, meaning that the
real subject is only created or initialized when it is actually needed, not when the
proxy is created.
4. Access Control: Proxies can enforce access control rules, such as checking permis-
sions or authentication, before allowing the client to access the real subject.
5. Monitoring and Logging: Proxies can be used to monitor and log the interactions
between the client and the real subject for debugging or auditing purposes.
/ / Subject interface
class Subject {
public :
v i r t u a l void r e q u e s t ( ) = 0 ;
virtual ~Subject ( ) { }
};
/ / Real Subject
c l a s s RealSubject : public Subject {
public :
void r e q u e s t ( ) o v e r r i d e {
s t d : : c o u t << " R e a l S u b j e c t h a n d l e s t h e r e q u e s t . " << s t d : : e n d l ;
}
};
/ / Proxy
c l a s s P ro x y : p u b l i c S u b j e c t {
private :
RealSubject * realSubject ;
public :
P ro x y ( ) : r e a l S u b j e c t ( n u l l p t r ) { }
void r e q u e s t ( ) o v e r r i d e {
/ / Lazy i n i t i a l i z a t i o n : C r e a t e t h e R e a l S u b j e c t o n l y when needed
if ( ! realSubject ) {
r e a l S u b j e c t = new R e a l S u b j e c t ( ) ;
}
/ / A d d i t i o n a l b e h a v i o r , i f n e c e s s a r y , can be added h e r e
s t d : : c o u t << " P r ox y h a n d l e s t h e r e q u e s t . " << s t d : : e n d l ;
~ P r ox y ( ) {
delete realSubject ;
}
};
i n t main ( ) {
P ro x y proxy ;
return 0 ;
}
In this example:
Subject is the subject interface that both RealSubject and Proxy implement. It defines
the request method.
RealSubject is the actual object that the proxy represents. It implements the Subject
interface and performs the real operation when requested.
Proxy is a proxy class that also implements the Subject interface. It contains an
instance of RealSubject and controls access to it. It performs lazy initialization and
additional behavior (printing a message) before delegating the request to the RealSubject.
In the main function, the client interacts with the Proxy object, which in turn controls
access to the RealSubject.
Behavioral Patterns
Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes
state, all its dependents are notified and updated automatically.
The Observer Pattern is a behavioral design pattern that defines a one-to-many
dependency between objects. It allows multiple objects (observers) to be notified and
updated automatically when the state of a subject (the object being observed) changes.
This pattern is commonly used in software architectures where changes to one object
need to be reflected in other objects without them being tightly coupled.
Key characteristics of the Observer Pattern:
1. Subject: The "subject" (also known as the "observable") is the object being ob-
served. It maintains a list of observers and notifies them when its state changes.
4. Notification: When the subject’s state changes, it notifies all registered observers
by calling their update methods.
5. Loose Coupling: The Observer Pattern promotes loose coupling between subjects
and observers. Subjects don’t need to know the specific classes of their observers,
and observers are not tightly coupled to the subject they observe.
92 L E C T URE 7: DESIGN PAT TERNS
/ / Observer i n t e r f a c e
c l a s s Observer {
public :
v i r t u a l void update ( const s t d : : s t r i n g & message ) = 0 ;
};
/ / Concrete Observer
c l a s s ConcreteObserver : public Observer {
private :
s t d : : s t r i n g name ;
public :
C o n c r e t e O b s e r v e r ( const s t d : : s t r i n g & _name ) : name ( _name ) { }
/ / Subject interface
class Subject {
private :
std : : vector < Observer * > observers ;
std : : s t r i n g state ;
public :
void a t t a c h ( O b s e r v e r * o b s e r v e r ) {
o b s e r v e r s . push_back ( o b s e r v e r ) ;
}
void d e t a c h ( O b s e r v e r * o b s e r v e r ) {
/ / Implement o b s e r v e r removal l o g i c h e r e ( not shown i n t h i s s i m p l e example )
}
void n o t i f y ( ) {
for ( Observer * observer : observers ) {
o b s e r v e r −>update ( s t a t e ) ;
}
}
};
i n t main ( ) {
Subject subject ;
s u b j e c t . s e t S t a t e ( "New s t a t e ! " ) ;
return 0 ;
}
In this example:
3. Subject is the subject class that maintains a list of observers and notifies them when
its state changes. It provides methods for attaching observers, detaching observers
(not shown in this simple example), setting its state, and notifying observers.
4. In the main function, two observers are created, attached to the subject, and
notified when the subject’s state changes.
Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
It allows the algorithm to vary independently from clients that use it.
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms,
encapsulates each one, and makes them interchangeable. It allows a client to choose an
algorithm from a family of algorithms at runtime, providing flexibility in selecting the
appropriate strategy without altering the client code.
Key characteristics of the Strategy Pattern:
1. Context: The "context" is the class that contains the strategy and maintains a
reference to the selected strategy. The context is responsible for invoking the
strategy.
2. Strategy: The "strategy" is an interface or abstract class that defines a set of meth-
ods or operations. Concrete strategy classes implement these methods to provide
different algorithmic behaviors.
4. Dynamic Switching: The Strategy Pattern allows the client (context) to switch
between different strategies at runtime, making it possible to choose the most
appropriate strategy for a given situation.
/ / Strategy interface
class PaymentStrategy {
public :
v i r t u a l void pay ( i n t amount ) = 0 ;
};
/ / Concrete Strategy 1
c l a s s CreditCardPayment : p u b l i c P a y m e n t S t r a t e g y {
public :
void pay ( i n t amount ) o v e r r i d e {
s t d : : c o u t << " P a i d " << amount << " v i a C r e d i t Card . " << s t d : : e n d l ;
}
};
/ / Concrete Strategy 2
c l a s s PayPalPayment : p u b l i c P a y m e n t S t r a t e g y {
public :
void pay ( i n t amount ) o v e r r i d e {
s t d : : c o u t << " P a i d " << amount << " v i a P a y P a l . " << s t d : : e n d l ;
}
};
/ / Context
c l a s s ShoppingCart {
private :
PaymentStrategy * paymentStrategy ;
public :
void s e t P a y m e n t S t r a t e g y ( P a y m e n t S t r a t e g y * s t r a t e g y ) {
paymentStrategy = s t r a t e g y ;
}
i n t main ( ) {
ShoppingCart c a r t ;
P a y m e n t S t r a t e g y * c r e d i t C a r d S t r a t e g y = new CreditCardPayment ( ) ;
P a y m e n t S t r a t e g y * p a y p a l S t r a t e g y = new PayPalPayment ( ) ;
delete creditCardStrategy ;
delete paypalStrategy ;
return 0 ;
}
In this example:
3. ShoppingCart is the context class that maintains a reference to the selected pay-
ment strategy and uses it to perform the payment when the checkout method is
called.
4. In the main function, two different payment strategies are created, and the Shop-
pingCart object switches between them at runtime to complete the checkout
process.
Command Pattern
Encapsulates a request as an object, thereby allowing for parameterization of clients
with queues, requests, and operations.
The Command Pattern is a behavioral design pattern that encapsulates a request
as an object, thereby allowing for parameterization of clients with queues, requests,
and operations. It enables the separation of the sender (invoker) of a request from the
receiver (handler) of that request, providing flexibility and extensibility in how requests
are processed.
Key characteristics of the Command Pattern:
3. Receiver: The "receiver" is the object that knows how to perform the action. It has
the necessary methods and state to carry out the request.
5. Client: The "client" creates and assembles the commands, receivers, and invokers,
thereby defining how the system’s behavior should be configured.
/ / Command i n t e r f a c e
c l a s s Command {
public :
v i r t u a l void e x e c u t e ( ) = 0 ;
};
/ / Receiver
class Light {
public :
void turnOn ( ) {
s t d : : c o u t << " L i g h t i s ON. " << s t d : : e n d l ;
}
void t u r n O f f ( ) {
s t d : : c o u t << " L i g h t i s OFF . " << s t d : : e n d l ;
}
};
/ / C o n c r e t e Command 1
c l a s s TurnOnCommand : p u b l i c Command {
private :
Light * l i g h t ;
public :
TurnOnCommand ( L i g h t * l ) : l i g h t ( l ) { }
void e x e c u t e ( ) o v e r r i d e {
l i g h t −>turnOn ( ) ;
}
};
/ / C o n c r e t e Command 2
c l a s s TurnOffCommand : p u b l i c Command {
private :
Light * l i g h t ;
public :
TurnOffCommand ( L i g h t * l ) : l i g h t ( l ) { }
void e x e c u t e ( ) o v e r r i d e {
l i g h t −> t u r n O f f ( ) ;
}
};
/ / Invoker
c l a s s RemoteControl {
private :
Command * command ;
public :
void setCommand (Command * cmd ) {
command = cmd ;
}
void p r e s s B u t t o n ( ) {
command−>e x e c u t e ( ) ;
}
};
i n t main ( ) {
Light livingRoomLight ;
Light kitchenLight ;
RemoteControl remote ;
/ / C o n f i g u r e t h e remote c o n t r o l
remote . setCommand(& livingRoomOn ) ;
remote . p r e s s B u t t o n ( ) ;
remote . setCommand(& l i v i n g R o o m O f f ) ;
remote . p r e s s B u t t o n ( ) ;
return 0 ;
}
In this example:
State Pattern
Allows an object to alter its behavior when its internal state changes. The object will
appear to change its class.
98 L E C T URE 7: DESIGN PAT TERNS
The State Pattern is a behavioral design pattern that allows an object to alter its
behavior when its internal state changes. The pattern represents each possible state
as an object and delegates the state-specific behavior to these objects. This enables an
object to appear as if it changes its class when its internal state changes.
Key characteristics of the State Pattern:
1. Context: The "context" is the class that contains the state and maintains a reference
to the current state object. The context delegates state-specific behavior to the
current state object.
2. State: "State" objects are classes that represent specific states and implement the
behavior associated with those states. Each state typically provides methods for
handling events or operations.
3. Context Interface: The context defines an interface that allows clients to request
operations. These operations are typically delegated to the current state object.
4. Concrete States: "Concrete states" are classes that implement the state interface.
They provide the specific behavior associated with a particular state.
/ / State interface
class State {
public :
v i r t u a l void handle ( ) = 0 ;
};
/ / Concrete State 1
c l a s s S t a t e 1 : public S t a t e {
public :
void handle ( ) o v e r r i d e {
s t d : : c o u t << " S t a t e 1 i s h a n d l i n g t h e r e q u e s t . " << s t d : : e n d l ;
}
};
/ / Concrete State 2
c l a s s S t a t e 2 : public S t a t e {
public :
void handle ( ) o v e r r i d e {
s t d : : c o u t << " S t a t e 2 i s h a n d l i n g t h e r e q u e s t . " << s t d : : e n d l ;
}
};
/ / Context
c l a s s Context {
private :
State * currentState ;
public :
Context ( S t a t e * i n i t i a l S t a t e ) : c u r r e n t S t a t e ( i n i t i a l S t a t e ) { }
void s e t S t a t e ( S t a t e * newState ) {
c u r r e n t S t a t e = newState ;
}
void r e q u e s t ( ) {
c u r r e n t S t a t e −>handle ( ) ;
}
};
i n t main ( ) {
State1 state1 ;
State2 state2 ;
C o n t e x t c o n t e x t (& s t a t e 1 ) ;
context . request ( ) ;
/ / Change t h e s t a t e d y n a m i c a l l y
c o n t e x t . s e t S t a t e (& s t a t e 2 ) ;
context . request ( ) ;
return 0 ;
}
In this example:
1. State is the state interface that defines the handle method for handling requests.
2. State1 and State2 are concrete state classes that implement the State interface.
They provide specific behavior associated with different states.
3. Context is the context class that maintains a reference to the current state and
delegates requests to the current state object.
4. In the main function, we create instances of State1, State2, and Context to demon-
strate how the context can change its behavior dynamically by switching between
different states.
Practice Lab
1. Exercise 1: Singleton Pattern
Implement a Singleton pattern to create a configuration manager that loads ap-
plication settings from a file. Ensure that only one instance of the configuration
manager exists.
Containers
Containers are classes that hold collections of objects or data. STL provides a variety of
container classes, including:
• Lists: Doubly linked lists that allow efficient insertions and deletions.
• Deques: Double-ended queues that offer fast insertions and deletions at both
ends.
Algorithms
Algorithms in the STL are a set of reusable functions for performing various operations on
containers or sequences. These include sorting, searching, and manipulation operations.
Some commonly used algorithms include sort(), find(), for_each(), and more.
Iterators
Iterators are objects that provide a way to access elements in a container in a consistent
and abstract manner. They act as a bridge between algorithms and containers, allowing
you to traverse and manipulate container elements. Common types of iterators include
begin(), end(), rbegin(), and rend().
102 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
Allocators
Allocators are used to manage memory allocation and deallocation for containers. While
they are typically abstracted away in most cases, advanced users can customize memory
management using allocators.
Code Reusability
The STL provides a wide range of generic classes and functions, reducing the need to
write custom data structures and algorithms. Developers can leverage pre-implemented
solutions for common tasks.
Standardization
The STL is part of the C++ Standard Library, ensuring that it is available and consistent
across different C++ compilers and platforms. This standardization simplifies cross-
platform development.
Performance
STL containers and algorithms are optimized for efficiency and speed. The library is
designed to be highly efficient, making it suitable for various applications, including
performance-critical ones.
Safety
STL containers and algorithms are designed to be safe to use. They perform bounds
checking and ensure memory safety, helping to prevent common programming errors.
i n t main ( ) {
/ / Create a vector of i n t e g e r s
s t d : : v e c t o r < i n t > numbers = { 5 , 2 , 9 , 1 , 5 , 6 } ;
/ / Use an STL a l g o r i t h m ( s o r t ) t o s o r t t h e v e c t o r
s t d : : s o r t ( numbers . b e g i n ( ) , numbers . end ( ) ) ;
/ / Use an STL i t e r a t o r t o p r i n t t h e s o r t e d v e c t o r
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
return 0 ;
}
• Auto-Sizing: When you insert elements into a std::vector, it expands itself as needed
to accommodate the new data. You don’t have to specify the size upfront; the vector
adapts on the fly.
• Easy Access: You can access elements by their position just like in a regular array,
using indices. It’s like having a collection of numbered drawers, and you can open
any drawer to get what you want.
• Efficient Packing: std::vector packs its elements tightly in memory, making it ef-
ficient for tasks like looping through all the items. Elements are neighbors in
memory, so it’s like flipping through pages in a book.
• Self-Cleaning: When you’re done with a std::vector, it takes care of cleaning up the
memory it used. It’s like having a magical butler who cleans up your room when
you leave.
• Customizable: You can tell the vector how much space you expect to need (without
putting things in it), so it can prepare in advance. It’s like reserving seats in a
restaurant before you arrive.
• Adaptable: If you decide you need more room, you can add more elements, and
the vector expands to fit. If you want to slim it down, you can remove elements,
and it shrinks accordingly.
i n t main ( ) {
/ / Create a std : : vector to s t o r e a l i s t of i n t e g e r s
s t d : : v e c t o r < i n t > numbers ;
/ / C a l c u l a t e t h e sum o f t h e numbers
i n t sum = 0 ;
f o r ( const i n t & num : numbers ) {
sum += num ;
}
s t d : : c o u t << "Sum o f t h e numbers : " << sum << s t d : : e n d l ;
/ / Remove t h e f i r s t e l e m e n t from t h e v e c t o r
numbers . e r a s e ( numbers . b e g i n ( ) ) ;
/ / D i s p l a y t h e updated v e c t o r
s t d : : c o u t << " Numbers a f t e r e r a s i n g t h e f i r s t element : " ;
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
s t d : : c o u t << s t d : : e n d l ;
return 0 ;
}
In this example:
• We include the necessary headers for std::vector.
• We create a std::vector called numbers to store a list of integers.
• We use push_back() to add numbers to the vector.
• We iterate through the vector to display its contents, calculate the sum, and find
the maximum number.
This example showcases the basic operations you can perform with std::vector, such
as adding, accessing, iterating, and modifying elements. std::vector is a flexible and
convenient container for working with dynamic lists of data in C++.
Arrays
std::array is a container class in C++ that represents a fixed-size, static array with several
advantages over traditional arrays. It is part of the Standard Library and provides type
and bounds safety, making it a safer alternative to C-style arrays. Here are some key
characteristics and features of std::array:
• Fixed Size: std::array has a fixed size determined at compile time. Once you create
an std::array, its size cannot be changed. This feature ensures compile-time safety
and predictability
• Type and Bounds Safety: std::array provides type safety, meaning you can’t acci-
dentally mix different data types. It also performs bounds checking, preventing
you from accessing elements outside the valid range.
• Efficient: Like traditional arrays, std::array elements are stored in contiguous mem-
ory, making element access and iteration efficient.
• Iterators: You can use iterators with std::array just like with other STL containers,
allowing for a consistent way to traverse and manipulate elements.
i n t main ( ) {
/ / D e c l a r e and i n i t i a l i z e an s t d : : a r r a y o f i n t e g e r s
s t d : : a r r a y < i n t , 5> numbers = { 1 , 2 , 3 , 4 , 5 } ;
/ / A c c e s s e l e m e n t s by i n d e x
s t d : : c o u t << " Element a t i n d e x 2 : " << numbers [ 2 ] << s t d : : e n d l ;
106 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
/ / Modify an e l e m e n t
numbers [ 3 ] = 1 0 ;
/ / C a l c u l a t e t h e sum o f e l e m e n t s
i n t sum = 0 ;
f o r ( const i n t & num : numbers ) {
sum += num ;
}
s t d : : c o u t << "Sum o f e l e m e n t s : " << sum << s t d : : e n d l ;
/ / S i z e and i t e r a t i o n
s t d : : c o u t << " S i z e o f t h e a r r a y : " << numbers . s i z e ( ) << s t d : : e n d l ;
s t d : : c o u t << " Elements i n t h e a r r a y : " ;
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
s t d : : c o u t << s t d : : e n d l ;
return 0 ;
}
In this example:
std::array combines the safety of a fixed-size array with the convenience and com-
patibility of STL containers, making it a valuable tool for managing arrays in C++.
List
std::list is a doubly linked list container class provided by the C++ Standard Library as
part of the Standard Template Library (STL). Unlike std::vector, which uses dynamic
arrays, std::list uses linked nodes to store its elements. Here are some key characteristics
and features of std::list:
• Doubly Linked List: std::list is implemented as a doubly linked list, which means
that each element in the list has pointers to both the next and the previous ele-
ments. This allows for efficient insertions and deletions at both the beginning and
the end of the list, as well as at arbitrary positions.
• Efficient Insertions and Deletions: Inserting or removing an element at the be-
ginning or end of a std::list is a constant-time operation, O(1). Insertions and
deletions at arbitrary positions also have good time complexity, O(N), where N is
the distance from the beginning or end of the list.
• Iterators: std::list provides bidirectional iterators, which allow you to traverse the
list in both forward and backward directions. This makes it suitable for scenarios
where you need to access elements in reverse order.
• Dynamic Sizing: Unlike std::array, which has a fixed size, or std::vector, which
dynamically resizes itself, std::list can dynamically grow or shrink as elements
are inserted or removed. This makes it useful for scenarios where the number of
elements is not known in advance.
• Memory Overhead: Due to its doubly linked nature, std::list has higher mem-
ory overhead compared to arrays and vectors. Each element in the list requires
additional memory for two pointers (next and previous).
Here’s a basic example of using std::list to store and manipulate a list of integers:
# include <iostream >
# include < l i s t >
i n t main ( ) {
/ / D e c l a r e and i n i t i a l i z e an s t d : : l i s t o f i n t e g e r s
s t d : : l i s t < i n t > numbers = { 1 , 2 , 3 , 4 , 5 } ;
/ / I n s e r t an e l e m e n t a t t h e b e g i n n i n g o f t h e l i s t
numbers . p u s h _ f r o n t ( 0 ) ;
/ / I n s e r t an e l e m e n t a t t h e end o f t h e l i s t
numbers . push_back ( 6 ) ;
/ / I t e r a t e t h r o u g h t h e l i s t u s i n g an i t e r a t o r
s t d : : c o u t << " Elements i n t h e l i s t : " ;
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
s t d : : c o u t << s t d : : e n d l ;
/ / Remove t h e f i r s t and l a s t e l e m e n t s
numbers . p o p _ f r o n t ( ) ;
numbers . pop_back ( ) ;
/ / D i s p l a y t h e updated l i s t
s t d : : c o u t << " Elements a f t e r removal : " ;
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
108 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
s t d : : c o u t << s t d : : e n d l ;
return 0 ;
}
In this example:
• We use push_front() and push_back() to insert elements at the beginning and end
of the list.
• We use an iterator to iterate through the list and display its contents.
• We use pop_front() and pop_back() to remove the first and last elements.
std::list is a useful container when you need efficient insertions and deletions, espe-
cially at both ends, and when you don’t require constant-time random access.
Set
std::set is a container class provided by the C++ Standard Library as part of the Standard
Template Library (STL). It represents a collection of unique, sorted elements. Here are
some key characteristics and features of std::set:
• nsertions and Deletions: Inserting and deleting elements in a std::set are also effi-
cient operations, performed in logarithmic time. The set automatically maintains
its sorted order after these operations.
• Iterators: std::set provides bidirectional iterators, allowing you to traverse the set in
both forward and backward directions. This is useful for tasks that involve iterating
through elements.
• . Custom Comparisons: You can customize the comparison function used for
sorting elements in the set. By default, elements are sorted using the less-than (<)
operator, but you can provide your own comparison function for more complex
data types.
• Memory Overhead: std::set has a memory overhead due to the need to maintain
unique and sorted elements. Each element is typically associated with additional
memory for internal data structures.
Here’s a basic example of using std::set to store and manipulate a set of integers:
# include <iostream >
# include < set >
i n t main ( ) {
/ / D e c l a r e and i n i t i a l i z e an s t d : : s e t o f i n t e g e r s
s t d : : s e t < i n t > numbers = { 5 , 2 , 9 , 1 , 5 , 6 } ;
/ / I n s e r t an e l e m e n t i n t o t h e s e t
numbers . i n s e r t ( 8 ) ;
/ / Check i f an e l e m e n t i s p r e s e n t in the s e t
i f ( numbers . count ( 5 ) > 0) {
s t d : : c o u t << " 5 i s i n t h e s e t . " << s t d : : e n d l ;
} else {
s t d : : c o u t << " 5 i s not i n t h e s e t . " << s t d : : e n d l ;
}
/ / I t e r a t e t h r o u g h t h e s e t u s i n g an i t e r a t o r
s t d : : c o u t << " Elements i n t h e s e t : " ;
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
s t d : : c o u t << s t d : : e n d l ;
/ / Remove an e l e m e n t from t h e s e t
numbers . e r a s e ( 1 ) ;
/ / D i s p l a y t h e updated s e t
s t d : : c o u t << " Elements a f t e r removal : " ;
f o r ( const i n t & num : numbers ) {
s t d : : c o u t << num << " " ;
}
s t d : : c o u t << s t d : : e n d l ;
return 0 ;
}
In this example:
std::set is a valuable container when you need a collection of unique, sorted elements
and efficient search operations.
Maps
std::map is a container class provided by the C++ Standard Library as part of the Standard
Template Library (STL). It represents an associative container that stores a collection of
key-value pairs, where each key is unique, and the keys are sorted in ascending order.
Here are some key characteristics and features of std::map:
• Unique Keys: In a std::map, each key is unique. This ensures that there are no
duplicate keys in the map.
• Sorted Keys: Keys in a std::map are automatically sorted in ascending order. This
allows for efficient key-based searches and traversals.
• Custom Comparisons: You can customize the comparison function used for sorting
keys in the map. By default, keys are sorted using the less-than (<) operator, but
you can provide your own comparison function for more complex data types.
• Memory Overhead: std::map has a memory overhead due to the need to maintain
unique and sorted keys. Each key-value pair is typically associated with additional
memory for internal data structures.
Here’s a basic example of using std::map to store and manipulate a map of names
and ages:
# include <iostream >
# i n c l u d e <map>
i n t main ( ) {
/ / D e c l a r e and i n i t i a l i z e an s t d : : map o f names ( k e y s ) and a g e s ( v a l u e s )
s t d : : map< s t d : : s t r i n g , i n t > ageMap ;
/ / I n s e r t key−v a l u e p a i r s i n t o t h e map
ageMap [ " A l i c e " ] = 2 5 ;
ageMap [ " Bob " ] = 3 0 ;
ageMap [ " C h a r l i e " ] = 2 2 ;
/ / I t e r a t e t h r o u g h t h e map u s i n g an i t e r a t o r
s t d : : c o u t << " C o n t e n t s o f t h e map : " << s t d : : e n d l ;
f o r ( const auto& e n t r y : ageMap ) {
s t d : : c o u t << e n t r y . f i r s t << " : " << e n t r y . second << " y e a r s " << s t d : : e n d l ;
}
return 0 ;
}
In this example:
• We include the necessary headers for std::map.
• We declare and initialize an std::map called ageMap to store names as keys and
ages as values.
• We use the subscript operator ([]) to insert key-value pairs into the map.
• We use find() to check if a key exists in the map before accessing its value.
• We use an iterator to iterate through the map and display its contents.
std::map is a versatile container when you need to maintain a collection of key-value
pairs with unique keys and sorted keys for efficient lookups.
Pairs
std::pair is a simple but versatile utility class provided by the C++ Standard Library. It
allows you to create a pair of two objects, which can be of different types. This class is
frequently used for various purposes, especially in scenarios where you need to group
two values together. Here are some key characteristics and features of std::pair:
• Two-Element Container: std::pair can hold two values, referred to as "first" and
"second" elements. These elements can be of different types.
• Value Semantics: Like most C++ objects, std::pair exhibits value semantics, mean-
ing that copies of pairs are created with distinct copies of the stored elements.
• Flexible Types: You can use std::pair to store values of any data type, including
user-defined types. For example, you can use it to store a pair of integers, a pair of
strings, or even a pair of custom objects.
112 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
• Convenient for Functions: Pairs are often used to return multiple values from
functions. Instead of defining a custom struct or class for this purpose, you can
use std::pair to bundle the values together.
Here’s a basic example of using std::pair to store and manipulate a pair of integers:
# include <iostream >
# include < u t i l i t y > / / Required f o r s t d : : pair
i n t main ( ) {
/ / D e c l a r e and i n i t i a l i z e a p a i r o f i n t e g e r s
s t d : : p a i r < i n t , i n t > c o o r d i n a t e s = s t d : : make_pair ( 3 , 4 ) ;
/ / A c c e s s and modify t h e e l e m e n t s o f t h e p a i r
s t d : : c o u t << " C o o r d i n a t e s : ( " << c o o r d i n a t e s . f i r s t << " , " << c o o r d i n a t e s . second <<
/ / Compare p a i r s b a s e d on t h e i r f i r s t e l e m e n t s
s t d : : p a i r < i n t , i n t > o t h e r C o o r d i n a t e s = s t d : : make_pair ( 2 , 5 ) ;
i f ( coordinates < otherCoordinates ) {
s t d : : c o u t << " These c o o r d i n a t e s a r e c l o s e r t o t h e o r i g i n . " << s t d : : e n d l ;
} else {
s t d : : c o u t << " These c o o r d i n a t e s a r e f a r t h e r from t h e o r i g i n . " << s t d : : e n d l ;
}
return 0 ;
}
In this example:
• We access and modify the elements of the pair using .first and .second.
• We compare pairs based on their first elements to determine which set of coordi-
nates is closer to the origin.
std::pair is a convenient and widely used tool in C++ for working with pairs of values.
It’s often employed in a wide range of scenarios, from simple data grouping to returning
multiple values from functions.
Tuples
std::tuple is a versatile container provided by the C++ Standard Library that allows you to
store a fixed-size collection of elements. Each element in a tuple can be of a different type,
making it useful for scenarios where you need to bundle together values of different data
types. Tuples are indexed, ordered, and allow you to access their elements by position.
Key Features of std::tuple
• Heterogeneous Elements: One of the primary advantages of std::tuple is that it
can hold elements of different types. This is in contrast to arrays or vectors, where
all elements must have the same data type.
• Fixed Size: The size of a tuple is determined at compile time. Once you define the
number and types of elements in a tuple, it cannot be changed without altering
the code.
• Ordered: Elements in a tuple are ordered and can be accessed by their position
within the tuple. This order is established at compile time and remains consistent.
• Value Semantics: Tuples have value semantics, meaning they create copies of their
elements when copied. Modifications to one tuple do not affect another tuple.
• Structured Binding (C++17 and later): C++17 introduced structured binding, which
allows you to easily decompose a tuple into individual variables. This makes
working with tuples more convenient.
• Compile-Time Type Safety: The types of elements in a tuple are known at compile
time, providing type safety. This means you can’t accidentally access elements
with the wrong data type.
• Comparison Operators: Tuples support comparison operators (e.g., ==, <, >), mak-
ing it possible to compare tuples for equality or order.
i n t main ( ) {
/ / C r e a t i n g a t u p l e with e l e m e n t s o f d i f f e r e n t t y p e s
s t d : : t u p l e < i n t , double , s t d : : s t r i n g > myTuple ( 4 2 , 3 . 1 4 , " H e l l o " ) ;
/ / A c c e s s i n g e l e m e n t s by p o s i t i o n
i n t i n t V a l u e = s t d : : g e t <0 >( myTuple ) ;
double d o u b l e V a l u e = s t d : : g e t < 1 > ( myTuple ) ;
s t d : : s t r i n g s t r i n g V a l u e = s t d : : g e t <2 >( myTuple ) ;
s t d : : c o u t << " Tuple e l e m e n t s : " << i n t V a l u e << " , " << d o u b l e V a l u e << " , " << s t r i n g
return 0 ;
}
In this example:
Queues
std::queue is a container adapter provided by the C++ Standard Library that represents a
queue data structure. A queue follows the First-In-First-Out (FIFO) principle, meaning
that the element added first is the one that will be removed first. std::queue is typically
implemented using other standard containers, such as std::deque (double-ended queue)
or std::list. Here are some key characteristics and features of std::queue:
• FIFO Behavior: Elements are inserted at the back (enqueue) and removed from
the front (dequeue) of the queue. This ensures that the element that has been in
the queue the longest is the first to be removed.
• Container Adapter: std::queue is a container adapter, which means that it is built
on top of an underlying container (usually std::deque by default). The choice of
the underlying container can be customized if needed.
• Limited Access: Access to elements in a queue is limited. You can only access the
front element (the one to be dequeued next) and the back element (the one most
recently enqueued).
• No Random Access: Unlike arrays or vectors, you cannot directly access elements
at arbitrary positions within a queue. It’s optimized for efficient enqueuing and
dequeuing.
• Constant-Time Operations: Enqueuing and dequeuing operations are typically per-
formed in constant time, O(1), making queues efficient for managing a collection
of elements where order matters.
• . Empty Queue: It provides methods like empty() to check if the queue is empty,
which is useful for handling boundary cases.
i n t main ( ) {
/ / D e c l a r e a queue o f i n t e g e r s
s t d : : queue < i n t > myQueue ;
/ / Enqueue e l e m e n t s
myQueue . push ( 1 0 ) ;
myQueue . push ( 2 0 ) ;
myQueue . push ( 3 0 ) ;
/ / Dequeue and p r i n t e l e m e n t s
while ( ! myQueue . empty ( ) ) {
s t d : : c o u t << " Dequeued : " << myQueue . f r o n t ( ) << s t d : : e n d l ;
myQueue . pop ( ) ; / / Remove t h e f r o n t e l e m e n t
}
return 0 ;
}
std::queue is commonly used in various applications where elements are processed in
a specific order, such as task scheduling, breadth-first search algorithms, and managing
a print job queue in a printer.
Optional
std::optional is a relatively new addition to the C++ Standard Library, introduced in C++17.
It provides a way to represent values that may or may not be present, and it’s particu-
larly useful in situations where you need to handle optional or nullable values without
resorting to null pointers or special sentinel values. Here are some key characteristics
and features of std::optional:
• Optional Values: std::optional allows you to represent values that may or may not
exist. It encapsulates the idea of optionality in a type-safe manner.
• Type Safety: The type of the value contained in the optional is known at com-
pile time, providing strong type safety. This eliminates common runtime errors
associated with nullable pointers.
• Accessing Values: You can access the value within an optional using the * derefer-
ence operator or the -> arrow operator, similar to working with pointers.
• Checking for Presence: You can check if an optional has a value using the has_value()
member function or the bool conversion operator.
• Value Initialization: You can initialize an optional without a value, making it empty.
This can be useful when you don’t have a valid value to provide initially.
• Use Cases: std::optional is useful for function return values that may fail, configu-
ration settings that may or may not be present, and handling nullable values in
general.
i n t main ( ) {
/ / D e c l a r e an o p t i o n a l i n t e g e r
std : : optional < int > optionalValue ;
/ / Check i f t h e o p t i o n a l has a v a l u e
i f ( optionalValue . has_value ( ) ) {
116 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
return 0 ;
}
In this example:
• We include the necessary header for std::optional.
• . We declare an std::optional called optionalValue to hold an integer.
• We check if the optional has a value using has_value().
• We assign a value to the optional using the assignment operator.
• We access the value within the optional using *optionalValue.
std::optional is a powerful tool for handling optional values in a type-safe and ex-
pressive manner, reducing the risk of null pointer-related errors in your code.
Here’s a basic example of using std::find to search for an integer within a vector:
# include <iostream >
# include < vector >
# include <algorithm >
i n t main ( ) {
s t d : : v e c t o r < i n t > numbers = { 1 , 2 , 3 , 4 , 5 } ;
int target = 3;
/ / Use s t d : : f i n d t o s e a r c h f o r t h e t a r g e t v a l u e
auto r e s u l t = s t d : : f i n d ( numbers . b e g i n ( ) , numbers . end ( ) , t a r g e t ) ;
i f ( r e s u l t ! = numbers . end ( ) ) {
s t d : : c o u t << " V a l u e " << t a r g e t << " found a t p o s i t i o n " << s t d : : d i s t a n c e ( numbe
} else {
s t d : : c o u t << " V a l u e " << t a r g e t << " not found . " << s t d : : e n d l ;
}
return 0 ;
}
In this example:
• We create a vector of integers called numbers and initialize it with some values.
• We specify the target value we want to find (in this case, 3).
• We use std::find to search for the target value within the range defined by num-
bers.begin() and numbers.end().
• If the value is found (result is not equal to numbers.end()), we print its position
using std::distance.
Find_if
std::find_if is another C++ Standard Library algorithm provided by the <algorithm>
header. It’s an extension of std::find and is used for searching a range based on a specified
predicate or condition. std::find_if allows you to search for an element that satisfies a
specific condition rather than just looking for an exact value. Here are its key features:
• Search with a Predicate: std::find_if searches for the first element in a range that
satisfies a given predicate or condition.
118 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
• Type-Agnostic: You can use std::find_if to search for elements of any data type as
long as you define a suitable predicate.
• Linear Search: Like std::find, this algorithm performs a linear search, checking
each element in the range against the predicate until a match is found or the end
of the range is reached.
Here’s a basic example of using std::find_if to search for an element greater than a
specific threshold in a vector of integers:
# include <iostream >
# include < vector >
# include <algorithm >
bool i s G r e a t e r T h a n T h r e s h o l d ( i n t v a l u e , i n t t h r e s h o l d ) {
return v a l u e > t h r e s h o l d ;
}
i n t main ( ) {
s t d : : v e c t o r < i n t > numbers = { 1 , 2 , 3 , 4 , 5 } ;
int threshold = 3;
i f ( r e s u l t ! = numbers . end ( ) ) {
s t d : : c o u t << " F i r s t v a l u e g r e a t e r than " << t h r e s h o l d << " found : " << * r e s u l t
} else {
s t d : : c o u t << "No v a l u e g r e a t e r than " << t h r e s h o l d << " found . " << s t d : : e n d l ;
}
return 0 ;
}
In this example:
• . We create a vector of integers called numbers and initialize it with some values.
for_each
std::for_each is a C++ Standard Library algorithm provided by the <algorithm> header.
It is used for performing a specified operation on each element within a given range,
such as an array, a container, or any sequence that provides iterators. std::for_each is
often used when you need to apply a function or perform an action on every element in
a collection. Here are its key features:
• Type-Agnostic: You can use std::for_each to process elements of any data type as
long as you define a suitable function or callable object.
• Returns Nothing: std::for_each does not return any value. It is typically used for its
side effects, such as modifying elements in place or performing some action on
each element.
void p r i n t V a l u e ( i n t v a l u e ) {
s t d : : c o u t << v a l u e << " " ;
}
i n t main ( ) {
s t d : : v e c t o r < i n t > numbers = { 1 , 2 , 3 , 4 , 5 } ;
/ / Use s t d : : f o r _ e a c h t o a p p l y t h e p r i n t V a l u e f u n c t i o n t o each e l e m e n t
s t d : : f o r _ e a c h ( numbers . b e g i n ( ) , numbers . end ( ) , p r i n t V a l u e ) ;
s t d : : c o u t << s t d : : e n d l ;
return 0 ;
}
120 L E C T URE 9: STANDARD TEMPL ATE LIBRARY (STL)
In this example:
• We define a custom function printValue that takes an integer and prints it.
• We define a custom function printValue that takes an integer and prints it.
• We create a vector of integers called numbers and initialize it with some values.
• We use std::for_each to apply the printValue function to each element within the
range defined by numbers.begin() and numbers.end().
The result of running this code will be the values in the vector printed to the console.
std::for_each is a convenient algorithm when you want to perform a consistent
operation on each element in a collection without manually writing loops. It promotes
a more declarative and functional style of programming.
Count
std::count is a C++ Standard Library algorithm provided by the <algorithm> header. It
is used for counting the occurrences of a specific value within a given range, such as
an array, a container, or any sequence that provides iterators. std::count is a simple yet
powerful tool for finding out how many times a particular value appears in a collection.
Here are its key features:
• Count Occurrences: std::count counts the number of times a specific value appears
within a given range.
• Linear Search: The algorithm performs a linear search, comparing each element
in the range with the target value and incrementing the count when a match is
found.
i n t main ( ) {
s t d : : v e c t o r < i n t > numbers = { 1 , 2 , 2 , 3 , 2 , 4 , 2 , 5 } ;
int target = 2;
/ / Use s t d : : c o u n t t o c o u n t t h e o c c u r r e n c e s o f t h e t a r g e t v a l u e
i n t count = s t d : : count ( numbers . b e g i n ( ) , numbers . end ( ) , t a r g e t ) ;
s t d : : c o u t << " V a l u e " << t a r g e t << " a p p e a r s " << count << " t i m e s . " << s t d : : e n d l ;
return 0 ;
}
In this example:
• We create a vector of integers called numbers and initialize it with some values.
• We specify the target value we want to count (in this case, 2).
• We use std::count to count the occurrences of the target value within the range
defined by numbers.begin() and numbers.end().
The result of running this code will be the number of times the target value appears
in the vector.
std::count is a convenient algorithm when you need to quickly determine how many
times a specific value occurs in a collection, and it’s commonly used in tasks like data
analysis and statistics.
• Create a C++ program that includes the necessary headers (nclude <iostream>
and <vector>).
• Declare a std::vector<int> called numbers and initialize it with the integers 1
to 5.
• Use a for loop to print the contents of the numbers vector.
• Add two more integers to the numbers vector using the push_back method.
• Print the modified numbers vector.
• Use an iterator to print the contents of the characters list in reverse order
(from ’E’ to ’A’).
• Insert a new character ’F’ at the beginning of the characters list.
• Print the modified characters list.
Practice Questions
1. Explain the primary difference between std::vector and std::list in terms of memory
allocation and element access.
2. What is the purpose of std::unordered_set, and what is the key advantage of using
it over std::set?
Lecture 10: Files and Streams in C++
In computer programming, dealing with data is a fundamental task. Data can be read
from various sources, processed, and then written back to storage. C++ provides a
versatile and powerful mechanism for working with external files through the concept
of files and streams. Streams provide a unified interface for reading and writing data,
whether it’s coming from a file, the console, or any other source.
This chapter explores the basics of files and streams in C++, including file input and
output operations, stream classes, and error handling. By the end of this chapter, you
will have a solid understanding of how to work with files and streams in C++.
Understanding Streams
What Are Streams?
In C++, a stream is an abstraction that represents a sequence of characters or bytes.
Streams are a flexible way to handle input and output operations. They can be associated
with various devices, such as the standard input (keyboard), standard output (screen),
and external files.
Streams can be categorized into two main types:
• Input Streams: These are used for reading data from a source, such as a file or the
keyboard. The standard input stream is represented by cin.
• Output Streams: These are used for writing data to a destination, such as a file or
the screen. The standard output stream is represented by cout.
C++ provides a set of predefined stream objects for common input and output oper-
ations. These include cin (standard input), cout (standard output), cerr (standard error
output), and clog (standard logging output).
Stream Classes
Streams in C++ are implemented as classes within the Standard Library. The primary
stream classes are:
• istream: This class is used for input operations and includes functions for reading
data.
• ostream: This class is used for output operations and includes functions for writing
data.
• ifstream: This class is derived from istream and is used specifically for file input
operations.
124 L E C T URE 10: FILES AND STREAMS IN C++
• ofstream: This class is derived from ostream and is used specifically for file output
operations.
• fstream: This class is derived from both ifstream and ofstream and is used for both
input and output operations on files.
These stream classes provide member functions that allow you to perform various
operations like reading and writing data, positioning the file pointer, and handling errors.
i n t main ( ) {
/ / Open a f i l e f o r r e a d i n g
ifstream i n p u t F i l e ( " input . t x t " ) ;
/ / Perform o p e r a t i o n s on t h e f i l e s . . .
/ / Close the f i l e s
inputFile . close ( ) ;
outputFile . close ( ) ;
return 0 ;
}
Always remember to close files when you’re done with them to release system re-
sources.
Reading from Files
To read data from a file, use the » operator with an ifstream object.
# include <iostream >
# include <fstream >
using namespace s t d ;
i n t main ( ) {
ifstream i n p u t F i l e ( " input . t x t " ) ;
i n t number ;
i f ( i n p u t F i l e . is_open ( ) ) {
while ( i n p u t F i l e >> number ) {
c o u t << " Read : " << number << e n d l ;
}
inputFile . close ( ) ;
} else {
c e r r << " F a i l e d t o open t h e f i l e . " << e n d l ;
}
return 0 ;
}
Writing to Files
To write data to a file, use the « operator with an ofstream object.
# include <iostream >
# include <fstream >
using namespace s t d ;
i n t main ( ) {
ofstream o u t p u t F i l e ( " output . t x t " ) ;
i f ( outputFile . is_open ( ) ) {
o u t p u t F i l e << " H e l l o , F i l e ! " << e n d l ;
outputFile . close ( ) ;
} else {
c e r r << " F a i l e d t o open t h e f i l e . " << e n d l ;
}
return 0 ;
}
Error Handling
Handling errors when working with files is crucial to ensure the reliability of your pro-
grams. C++ provides mechanisms to check and handle errors during file operations.
Exception Handling
C++ also supports exception handling for more robust error management. You can use
try, catch, and throw statements to handle exceptions that occur during file operations.
try {
ifstream i n p u t F i l e ( " input . t x t " ) ;
if ( ! inputFile ) {
throw r u n t i m e _ e r r o r ( " F a i l e d t o r e ad from t h e f i l e . " ) ;
}
/ / Read from t h e f i l e . . .
} catch ( const r u n t i m e _ e r r o r& e ) {
c e r r << " E r r o r : " << e . what ( ) << e n d l ;
}
i n t main ( ) {
/ / Open a b i n a r y f i l e f o r w r i t i n g
o f s t r e a m o u t p u t F i l e ( " b i n a r y _ d a t a . d a t " , i o s : : out | i o s : : b i n a r y ) ;
i f ( outputFile . is_open ( ) ) {
/ / Data t o be w r i t t e n
int integerData = 42;
double doubleData = 3 . 1 4 1 5 9 ;
/ / W r i t e b i n a r y data
o u t p u t F i l e . w r i t e ( r e i n t e r p r e t _ c a s t < char * >(& i n t e g e r D a t a ) , s i z e o f ( i n t ) ) ;
o u t p u t F i l e . w r i t e ( r e i n t e r p r e t _ c a s t < char * >(& doubleData ) , s i z e o f ( double ) ) ;
/ / Close the f i l e
outputFile . close ( ) ;
c o u t << " Data w r i t t e n t o b i n a r y f i l e . " << e n d l ;
} else {
c e r r << " F a i l e d t o open t h e f i l e . " << e n d l ;
}
return 0 ;
}
In this example:
2. We prepare some binary data (an integer and a double) that we want to write to
the file.
3. We use the write() method to write the binary data. Note the use of reinterpret_cast
to convert the data pointers to char* pointers, allowing us to write raw bytes to
the file.
i n t main ( ) {
/ / Open t h e b i n a r y f i l e f o r r e a d i n g
i f s t r e a m i n p u t F i l e ( " binary_data . dat " , i o s : : in | i o s : : binary ) ;
i f ( i n p u t F i l e . is_open ( ) ) {
/ / Data t o s t o r e t h e r e a d v a l u e s
int readInteger ;
double readDouble ;
/ / Read b i n a r y data
i n p u t F i l e . r ea d ( r e i n t e r p r e t _ c a s t < char * >(& r e a d I n t e g e r ) , s i z e o f ( i n t ) ) ;
i n p u t F i l e . r ea d ( r e i n t e r p r e t _ c a s t < char * >(& readDouble ) , s i z e o f ( double ) ) ;
if ( ! inputFile . f a i l ( ) ) {
c o u t << " Read I n t e g e r : " << r e a d I n t e g e r << e n d l ;
c o u t << " Read Double : " << readDouble << e n d l ;
} else {
c e r r << " F a i l e d t o r ea d d a t a from t h e f i l e . " << e n d l ;
}
/ / Close the f i l e
128 L E C T URE 10: FILES AND STREAMS IN C++
inputFile . close ( ) ;
} else {
c e r r << " F a i l e d t o open t h e f i l e . " << e n d l ;
}
return 0 ;
}
In this example:
1. We open the same file "binary_data.dat" for reading, using ios::in | ios::binary as
the mode flags.
2. We declare variables to store the data we will read from the file.
3. We use the read() method to read the binary data, again using reinterpret_cast to
convert the data pointers to char* pointers.
4. We check for errors using the fail() method. If the read was successful, we print
the read values.
• Endianness (byte order) can be an issue when reading/writing binary data across
different systems. Be cautious when sharing binary files between systems with
different endianness.
• Binary files may not be portable between different compilers or platforms due to
differences in data representation and padding.
• Binary files are not human-readable, so ensure proper error handling and version
control when working with them.
Working with binary files in C++ allows you to handle non-text data efficiently. How-
ever, it requires careful management of data types, sizes, and endianness to ensure data
integrity when reading and writing binary files.
Ensure proper error handling for file opening. Create another program that opens
"text_file.txt" for reading and displays its contents on the console.
• Binary File Operations
1. Create a program that opens a binary file named "binary_data.dat" for writing.
Write the following data to the file:n integer with the value 42. A double with
the value 3.14159.
2. Create another program that opens "binary_data.dat" for reading. Read the
data and display it on the console
• Error Handling
• Modify the programs in Task 1 and Task 2 to include proper error handling for file
opening, reading, and writing. Ensure that error messages are displayed if any
errors occur.
• User Interaction
1. Create a program that allows the user to enter text interactively and write it
to a text file named "user_text.txt." Allow the user to enter multiple lines of
text, and write them to the file. Use a loop to continue taking input until the
user decides to stop.
2. Create another program that reads the contents of "user_text.txt" and displays
them on the console.
• Binary File Extension
1. Extend the binary file program from Task 2 to write and read additional data
types such as float, char, and bool.
2. Ensure proper error handling for all file operations, and display the read data
on the console.
• Lab Completion:
1. Test each program thoroughly to ensure proper functionality and error han-
dling.
2. Verify that text and binary files are created, written to, and read from correctly.
3. Ensure that all error messages are informative and helpful.
4. Demonstrate understanding of both text and binary file operations and error
handling.
130 L E C T URE 10: FILES AND STREAMS IN C++
Practice Questions
1. What is the purpose of C++ streams, and why are they essential in programming?
2. What is the difference between text mode and binary mode when working with
C++ streams?
3. How do you open a text file for reading in C++ using streams? Provide an example.
4. How can you check if a file stream (ifstream or ofstream) was opened successfully?
What happens if the stream fails to open?
5. Describe the process of writing text to a file in C++ using an ofstream object.
Provide an example.
6. How do you read text from a file in C++ using an ifstream object? Provide an
example.
7. Explain the concept of end-of-file (EOF) when reading from a file. How do you
detect EOF in C++?
8. What are the common stream states that can be checked using member functions
like good(), eof(), fail(), and bad()? How are they used?
9. How do you open a binary file for writing in C++ using streams? Provide an exam-
ple.
10. What is the purpose of write() and read() methods when working with binary files
in C++? Provide examples of using these methods.
11. Explain the importance of proper error handling when working with file streams
in C++. What mechanisms can be used for error handling?
12. Describe how you can use C++ streams to read user input from the console and
write it to a text file. Provide an example.
13. What precautions should be taken when sharing binary files between systems with
different endianness (byte order)?
14. How do you close a file stream in C++? Why is it important to close files properly?
15. What are the advantages of using C++ streams over traditional C-style file handling
functions (e.g., fopen, fread, fwrite)?
TODO:
• add a chapter about UML and how this is related to OOP
• add part of the project after each chapter, should be progressed while chapters are
moving
• add a chapter about writing GUI applications using C++