CS201 — Final Term Summary (Lectures 23–45)
📘 Lecture 23 — Preprocessor
📖 Overview: This lecture covers the C/C++ preprocessor, which modifies source code before compilation. It explains essential directives like
#includeand#define, how to create and use macros, and best practices for conditional compilation and debugging. Understanding the preprocessor is critical for writing efficient, portable, and maintainable C/C++ code.
🗂️ Topics Covered
The lecture begins by introducing the preprocessor and its role in enhancing C. It then covers the #include directive in detail, including angle bracket vs. quotation mark usage. The #define directive is explained for creating symbolic constants. A list of other preprocessor directives is given, with special attention to conditional compilation using #ifdef, #ifndef, #if, #else, #elif, and #endif for debugging. Macros are classified into simple and parameterized types, with detailed examples and precautions about parentheses. The lecture concludes with practical tips on writing header files and ensuring code portability.
📝 Lecture Summary
Preprocessor
The preprocessor is a tool that comes with every C compiler. It enhances the C language by making changes to the source code before the actual compilation. The compiler then receives this modified source code file. All preprocessor directives start with the # sign.
include directive
The #include directive replaces the line where it appears with the entire text of the specified file. There are two ways to use it:
#include <somefile>: The compiler searches for the file in a specific include directory (where standard header files likeiostream.h,stdlib.hare located). These files are typically called header files (extension.h).#include "myHeaderFile.h": The compiler first searches for the file in the current working directory (where the user's source code is located). This is used for user-defined header files.
When the compiler processes #include, it literally inserts the entire content of the included file into the source code at that point. This expanded source code is what the compiler sees. Including header files at points other than the start of the program is legal but not standard practice.
define directive
The #define directive is used to define symbolic constants (also called macros). The syntax is #define NAME value. For example:
#define PI 3.1415926
When the preprocessor encounters this directive, it replaces every occurrence of PI in the code with 3.1415926 before the compiler sees the code. This is a text substitution. Using #define for constants is better than using variables because the value cannot be accidentally changed and it allows for easy updates—changing the value in one place updates the entire program. Symbolic constant names are conventionally written in UPPERCASE.
🔑 Definition — Macro: A special name that is substituted in the code by its definition, resulting in an expanded code block before compilation.
💡 Why this matters: Unlike a variable (double Pi = 3.1415926;), a #define constant cannot be reassigned. This ensures the value remains constant throughout the program. Also, if the same constant is used across multiple files, defining it in a single header file and including that file ensures all files get the updated value when changed.
📐 Formula: #define NAME value → NAME is replaced by value as literal text everywhere in the code.
Other Preprocessor Directives
The lecture lists several preprocessor directives, including #undef, #ifdef, #ifndef, #if, #else, #elif, #endif, #error, #line, #pragma, and #assert. The focus is on conditional compilation.
Conditional Compilation allows parts of the code to be compiled only if certain conditions are met. For example, during debugging, programmers often insert output statements to check variable values. Instead of manually removing all these lines later, they can use #define DEBUG and then enclose debug code within:
#ifdef DEBUG
cout << "Control is in the while loop...";
#endif
If DEBUG is defined, the code inside is compiled; otherwise, it is ignored.
The #undef directive is used to undefine a previously defined symbol (e.g., #undef PI). After this line, PI is no longer available. A symbol cannot be redefined without being undefined first.
The lecture mentions that header files often contain constructs like #ifdef cplusplus ... extern "C" { ... #endif to ensure compatibility between C and C++ code.
Macros
Macros are classified into two categories:
-
Simple Macros: Map a symbolic name to a constant (e.g.,
#define PI 3.1415926). -
Parameterized Macros: Take arguments. For example:
#define square(x) x * xThe
#definestatement is not C code, so no semicolon is needed at the end.⚠️ Critical Pitfall: The simple
#define square(x) x * xhas a precedence problem. Forsquare(i + j), the substitution becomesi + j * i + j, which is incorrect due to operator precedence (*binds before+).Correct Definition: Always put parentheses around the entire macro definition and around each argument.
#define square(x) ((x) * (x))Now
square(i + j)becomes((i + j) * (i + j)), which is correct.💡 Macro vs. Function: A macro is expanded inline at compile time, so there is no function call overhead—no pushing arguments onto the stack, no jumping to a function, no returning. This makes macros faster. However, if a macro is used many times, the code becomes bloated (repeat code at each usage), potentially enlarging the executable. Functions are better for complex operations. Macros are best for simple, one-line substitutions.
Rules for defining parameterized macros:
- No space between the macro name and the opening parenthesis.
- Arguments are separated by commas.
- Always use parentheses around the entire definition and each argument.
📐 Formula — Parameterized Macro: #define MACRO_NAME(ARG1, ARG2) ((ARG1) * (ARG2)) → ARG1 and ARG2 are textually replaced in the definition, with parentheses ensuring correct evaluation.
📌 Example (Parameterized Macro):
#include <iostream.h>
#define square(x) ((x) * (x))
main() {
int x;
cout << "Enter value of x: ";
cin >> x;
cout << "Square of x = " << square(x) << endl; // Replaced: ((x) * (x))
cout << "Square of x+2 = " << square(x+2) << endl; // Replaced: ((x+2) * (x+2))
cout << "Square of 7 = " << square(7); // Replaced: ((7) * (7))
}
Example
A complete program demonstrating macros for calculating the area of a circle:
#include <iostream.h>
#define PI 3.14159
#define CIRCLEAREA(X) (PI * (X) * (X))
main() {
float radius;
cout << "Enter radius of the circle: ";
cin >> radius;
cout << "Area of circle is " << CIRCLEAREA(radius);
}
The macro CIRCLEAREA uses the already defined PI constant. The parentheses ensure correct evaluation even for expressions like CIRCLEAREA(radius + 2).
The lecture also discusses portability. Standard header files (like iostream.h) have consistent names across different compilers and operating systems, making C code highly portable. User-defined header files, containing function prototypes, symbolic definitions, and macros, can be ported along with the source code, centralizing common utilities and easing code maintenance.
Tips
- All preprocessor directives start with the
#sign. - A symbol cannot be redefined without undefining it first.
- Conditional compilation directives help in debugging.
- Do not declare variable names starting with underscore (reserved for compiler internals).
- Always use parentheses when defining macros that take arguments.
⭐ Key Takeaways
The preprocessor modifies source code before compilation, and all its directives begin with #. The #include directive inserts the contents of a file; use angle brackets for standard library headers and quotation marks for user-defined files. The #define directive creates macros for symbolic constants or parameterized code snippets, and for parameterized macros, always wrap the entire definition and each parameter in parentheses to avoid precedence errors. Macros eliminate function call overhead but increase code size, so use them for simple, frequently used operations. Conditional compilation with #ifdef, #ifndef, #if, #else, and #endif is essential for portable code and efficient debugging.
🧠 Quick Revision Questions
- What is the difference between
#include <filename>and#include "filename"in terms of where the preprocessor searches for the file? - Why must all parameters and the entire expression be wrapped in parentheses when defining a parameterized macro like
square(x)? - What is the primary advantage of using a macro over a function for a simple operation like squaring a number?
- How could you use
#define DEBUGand#ifdef DEBUG ... #endifto add debugging output that can be easily disabled? - What does the
#undefdirective do, and why is it used?
📘 Lecture 25 — Default Function Arguments, Inline Functions, and Function Overloading
📖 Overview: This lecture marks the transition from traditional C programming to C++ features. It covers essential C++ enhancements including default function arguments, flexible variable declaration placement, inline functions as safer alternatives to macros, and function overloading for cleaner, more readable code. These concepts form the foundation for understanding object-oriented programming later.
🗂️ Topics Covered
The lecture begins with a brief history of programming languages from machine language to C++. It then covers structured programming principles and their limitations, leading to the introduction of C++ features. Core topics include default function arguments with their right-to-left placement rules, flexible variable declaration placement, inline functions versus macros with side-effect analysis, and function overloading based on parameter type and number.
📝 Lecture Summary
History of C/C++
C was developed by Bell Labs scientists in the 1970s as a lean, powerful language. Major operating systems like Unix were written in C. The evolution of programming languages went from machine language (0s and 1s) to assembly language with symbolic codes, then to high-level languages like COBOL and FORTRAN. These high-level languages led to "spaghetti code" where programs were unstructured with branches growing in every direction, making them difficult to read and manage.
Structured Programming
In structured programming, problems are broken into smaller pieces or modules, each corresponding to a function. This is the top-down structured programming approach. Key rules include: Divide and Conquer — functions should not be longer than 2-3 pages/screens; Single Entry Single Exit rule inside functions for readability and manageability; and well-commented programs explaining what a function does, its parameters, and what it returns.
Limitations of Structured Programming
In functional programming, data exists outside the program and functions process that data. As problems became complex, developers realized data cannot remain outside — it should be part of the program. This led to data-driven programming where data comes first, then functions operate on it. This originated Object Oriented Programming. In the early 1980s, Bjarne Stroustrup at Bell Labs enhanced C to overcome these shortcomings, first called C with Classes, eventually named C++.
Default Function Arguments
When certain parameters of a function are passed the same value most of the time, default function arguments can be used. The default value is provided inside the function prototype or definition.
🔑 Definition — Default Function Arguments: Parameters that take default values when no argument is provided for them during the function call.
void f(int i = 1, double x = 10.5)
{
cout << "The value of i is: " << i;
cout << "The value of x is: " << x;
}
Calling f(); assigns default values: i=1, x=10.5. Calling f(2); assigns i=2, x=10.5 (default). Calling f(1, 2); assigns i=1, x=2.
📐 Rule: Arguments are assigned to parameters from left to right. Parameters with default values must be placed on the extreme right of the parameter list. You cannot skip the first parameter with a default value and provide a value for the second.
Example of Default Function Arguments
#include <iostream.h>
void show(int = 1, float = 2.3, long = 4);
main()
{
show(); // All three arguments default
show(5); // Provide 1st argument
show(6, 7.8); // Provide 1st and 2nd
show(9, 10.11, 12L); // Provide all three arguments
}
📌 Output:
first = 1, second = 2.3, third = 4
first = 5, second = 2.3, third = 4
first = 6, second = 7.8, third = 4
first = 9, second = 10.11, third = 12
Placement of Variable Declarations
In C++, variables can be declared anywhere in a function, not just at the top. The philosophy is to declare variables just before they are actually used, increasing code readability. A variable declared inside a code block is visible only within that block.
for(int i = 0; condition; increment)
{
// i visible here
}
i = 500; // Valid - i declared outside the for loop's braces
Example of Placement of Variable Declarations
#include <iostream.h>
main()
{
for(int lineno = 0; lineno < 3; lineno++)
{
int temp = 22;
cout << "\nThis is line number " << lineno
<< " and temp is " << temp;
}
if(lineno == 4) // lineno still accessible
cout << "\nOops";
// Cannot access temp
}
📌 Output:
This is line number 0 and temp is 22
This is line number 1 and temp is 22
This is line number 2 and temp is 22
Inline Functions
Inline functions are declared using the inline keyword before the function name. This is a directive to the compiler to insert the full function definition at each call location, eliminating function call overhead (loading parameters onto the stack).
🔑 Definition — Inline Function: A function that is expanded at the point of call, avoiding the overhead of a function call. The inline keyword is only a suggestion to the compiler — large functions are not expanded inline even if declared inline.
Disadvantages: If called 100 times and the function is 10 lines long, program size increases by 1000 lines. The compiler may reject the inline request.
Advantages over macros: Inline functions perform automatic type checking on parameters and have no side effects. Macros can cause unexpected behavior due to multiple parameter evaluation.
Macro example with side effect:
#define MAX(A, B) ((A) > (B) ? (A) : (B))
i = MAX(x++, y++); // Larger value incremented TWICE
Inline function (no side effect):
inline int max(int a, int b) {
if(a > b) return a;
return b;
}
i = max(x++, y++); // Works as expected - each variable incremented once
📌 Output comparison:
Macro: x = 24, y = 47 (y incremented twice)
Inline: x = 24, y = 46 (y incremented once, as expected)
💡 Why this matters: Macros can produce subtle bugs when parameters have side effects (like increment operators). Inline functions are safer and should be preferred.
Function Overloading
Function overloading means using the same function name to perform different tasks depending on the situation. The << operator with cout is overloaded for many data types (int, double, float, string) — this is possible because the header file iostream.h contains prototypes for all those functions.
🔑 Definition — Function Overloading: Using the same function name to perform multiple tasks depending on the type and number of arguments passed.
Overloaded functions are differentiated by:
- Type of arguments
- Number of arguments
NOT by return type — you cannot have:
int f(int);
double f(int); // Error: ambiguous declarations
// Overloaded print functions
void print(int i) {
cout << "\nThe value of the integer is: " << i;
}
void print(double d) {
cout << "\nThe value of the double is: " << d;
}
void print(char* s) {
cout << "\nThe value of the string is: " << s;
}
📌 Output:
The value of the integer is: 100
The value of the double is: 123.12
The value of the string is: This is a test string
The compiler uses a technique called name mangling to generate a unique token for each overloaded function based on its name and parameters.
Example of Function Overloading
inline void stringCopy(char *dest, const char *src) {
strcpy(dest, src);
}
inline void stringCopy(char *dest, const char *src, int len) {
strncpy(dest, src, len);
}
⭐ Key Takeaways
Four critical C++ features were introduced in this lecture. First, default function arguments allow parameters to have predefined values, with the rule that parameters having defaults must be on the extreme right of the parameter list. Second, variable declarations can be placed anywhere in C++, not just at the top of blocks, improving readability. Third, inline functions should be preferred over macros because they provide type checking and avoid side effects from multiple parameter evaluations. Fourth, function overloading enables multiple functions with the same name but different parameter types and/or counts, improving code readability — but overloading based only on return type is not allowed.
🧠 Quick Revision Questions
- What is the rule for placing default argument parameters in a function parameter list, and why?
- What is the difference between a macro
#define MAX(A,B)and an inline functioninline int max(int a, int b)regarding side effects? - Can you overload functions based only on return type? Why or why not?
- In the code
for(int i=0; i<10; i++), is the variableiaccessible after the for loop? Explain. - What is name mangling and why is it necessary for function overloading?
📘 Lecture 26 — Classes and Objects
📖 Overview: This lecture introduces the fundamental concepts of classes and objects in C++. It explains how classes extend the language by creating user-defined data types that encapsulate both data and functions, and emphasizes the critical importance of separating interface from implementation for robust, maintainable code.
🗂️ Topics Covered
The lecture covers the definition and structure of classes, the difference between structs and classes regarding visibility, the use of private and public access specifiers to achieve encapsulation, the separation of interface from implementation, how to define member functions using the scope resolution operator, and the concept of constructors including default constructors and constructors with default arguments.
📝 Lecture Summary
Classes and Objects
In today’s lecture, we begin learning about classes and objects. A class is very closely related to a struct, but unlike a struct which groups only data variables, a class groups both data variables and the functions that manipulate that data.
🔑 Definition — Class: A user-defined data type that includes both data members and member functions to manipulate that data.
These functions are called member functions or methods. When we create variables of a class, they are given a special name.
🔑 Definition — Object: An instance of a class.
💡 Why this matters: Just as int i; creates an instance of the built-in type int, Date myDate; creates an instance (object) of the user-defined class Date.
Definition of a class
The structure of a class uses the class keyword, followed by the class name and braces enclosing the definition.
class name_of_class{
// definition of class
};
Consider a Date class:
class Date{
int Day;
int month;
int year;
};
An object is created as Date myDate;.
Separation of Interface from the Implementation
The key difference between a struct and a class is the visibility of data members. In a struct, data is open and visible to every part of the program, which can lead to problems — for example, a variable storing a tax rate could accidentally be changed by a loop.
🔑 Definition — Encapsulation: The concept of hiding data so that it cannot be directly accessed but can still be used through a controlled interface.
The real-world example of a wristwatch illustrates this: we see the hands but cannot touch them; we use a button to adjust the time. The internal mechanism is hidden (implementation), while the button is the interface.
Structure of a class
The default visibility of all data members and member functions of a class is hidden and private.
🔑 Definition — private: A keyword that makes data members and member functions accessible only within the class itself, not from outside.
🔑 Definition — public: A keyword that makes data members and member functions visible and accessible from outside the class.
class Date
{
private:
// private data and functions
public:
// public data and functions
};
Normally, data is kept private, and member functions (which manipulate the data) are kept public. These public functions form the interface of the class. Member functions can see and manipulate the private data members because they are part of the class.
For example, to set the month, we cannot write myDate.month = 10; because month is private. Instead, we write a public function:
void setMonth(int month);
And call it as:
myDate.setMonth(10);
💡 Why this matters: In a struct, we could write myDate.month=13; creating an invalid date. With a class, we can validate data inside the setMonth function. The public part becomes the interface — what we want to show — while the private part is the implementation — what we want to hide.
This separation allows the implementation to change completely while the interface remains the same, just as a car's steering (interface) has remained the same even as the internal steering mechanism evolved from rods to microprocessors.
Sample program: Date class in detail
Here is the complete Date class implementation:
class Date{
public:
void display();
Date(int, int, int);
private:
int day, month, year;
};
🔑 Definition — Scope Resolution Operator (::): A double colon used to define a member function outside the class, telling the compiler which class the function belongs to.
Example of defining a member function:
Date::display()
{
cout << "The date is " << day << "-" << month << "-" << year << endl;
}
The functions are called by objects, not by the class. date1.setMonth(12) manipulates the data of date1, while date2.setMonth(12) manipulates the data of date2.
Setter and getter functions become the public interface:
- setDay(int), setMonth(int), setYear(int) — to set values
- getDay(), getMonth(), getYear() — to retrieve values
Output of the program:
The date is 1-1-2000
The date is 10-12-2002
Constructors
🔑 Definition — Constructor: A special member function with the same name as the class, having no return type, that is automatically called when an object is created. It initializes the object into a consistent and valid state.
If no constructor is written, C++ provides a default constructor that takes no arguments.
Example of a constructor definition:
Date::Date(int theDay, int theMonth, int theYear)
{
day = theDay;
month = theMonth;
year = theYear;
}
Object creation with initialization:
Date myDate(1, 1, 2003);
This creates the object AND initializes day=1, month=1, year=2003.
📌 Example: Without a custom constructor, Date myDate; creates an object with uninitialized data members. The default constructor does not set any values.
Default arguments with constructors
Constructors can use default arguments, providing flexibility in object creation.
Date::Date(int theDay, int theMonth, int theYear = 2002)
{
day = theDay;
month = theMonth;
year = theYear;
}
Now objects can be created in multiple ways:
Date myDate; // Default constructor - uninitialized
Date myDate(1, 1, 2000); // All three arguments provided
Date myDate(1, 1); // Uses default value for year (2002)
Constructors can be overloaded — multiple constructors with different parameters. For example, one constructor could take three integers, while another takes a character string like "01-Jan-2003" and parses it.
Output from the program with constructors:
The date is 1-1-1900
The date is 1-1-2000
The date is 10-12-2002
⭐ Key Takeaways
A class is a user-defined data type that bundles both data members and member functions. The default visibility of class members is private, making data hidden and accessible only through public member functions, which form the class's interface. This separation of interface from implementation allows internal logic to change without affecting external code. Constructors are special functions that initialize objects automatically upon creation, and they can be overloaded or given default arguments for flexible object creation. The scope resolution operator (::) is essential for defining member functions outside the class definition.
🧠 Quick Revision Questions
- What is the difference between a struct and a class regarding default visibility of members?
- What does it mean to separate interface from implementation, and why is this important?
- How do you define a member function outside the class, and what operator is used?
- What is a constructor, and when is it automatically called?
- How can you create an object using default arguments in a constructor? Give an example.
📘 Lecture 27 — Classes and Objects, Constructors, Types of Constructors, Utility Functions, Destructors
📖 Overview: This lecture builds on the foundational concepts of classes and objects in object-oriented programming. It introduces constructors for initializing objects, explains different types including default, parameterized, and overloaded constructors, discusses utility functions as private helpers, and covers destructors for cleanup. Understanding these mechanisms is essential for writing robust, well-structured C++ programs.
🗂️ Topics Covered
This lecture covers Classes and Objects in depth, moving from function-oriented to object-oriented thinking. It explains that a class is a user-defined data type with data members and member functions, with data hiding via encapsulation. It details Constructors as special functions for initialization, including compiler-generated, simple, parameterized, and overloaded constructors. It also introduces Utility Functions (private member functions used internally) and Destructors, which are automatically called when an object is destroyed, primarily for memory deallocation.
📝 Lecture Summary
Classes and Objects
This lecture is a sequel of the previous discussion on 'Classes' and 'Objects'. The use of 'classes and objects' has changed our way of thinking. Instead of having function-oriented programs (getting data and performing functions with it), we now have data that knows how to manipulate itself—this is object-oriented programming. Our programs revolve around data and objects.
In real-world programming, we deal with real-world entities like cycles, cars, buildings, and people, which we perceive as objects. Each object has a behavior (functions or methods) and attributes (data members). For example, a man has attributes like height and weight, and actions like talk, walk, sit, and stand. An aeroplane has attributes like height, width, and number of seats, and actions like takeoff, flying, and landing. In programming terms, attributes are data members, and actions are functions or methods.
Data now encompasses more than just numbers and letters—it includes pictures, images, windows, dialogue boxes, audio, and video (multimedia). With this expanded scope, we can think of an integer that knows how to display itself, or an audio object that knows how to play itself.
Class
A class is a way of defining a user-defined data type. It contains data members and functions that manipulate that data. Encapsulation (data hiding) means that the data of a class cannot be accessed from outside, except through defined member functions. To hide data, we declare it private, making it available only to member functions of the class (except friend functions). A class is divided into the private part (implementation) and the public part (interface).
The analogy of a room with a curtain illustrates this: things behind the curtain (private) are visible only to insiders, while outsiders see only what is in front of the curtain (public interface). A function inside the class (member function) can access and manipulate all things in the class. A function outside the class can only access the public interface. A constructor must be in the public section of the class so it can be called from outside.
Constructors
A constructor is a special function called whenever we instantiate (create) an object of a class. If we do not define a constructor, C++ provides a default constructor that does nothing. Constructors were introduced because the majority of programming bugs (logical errors) occur due to uninitialized data. Using an uninitialized variable like int i; and then j = 2 * i; causes difficult-to-find logical errors. Constructors give us an opportunity to initialize data members so that when a program gets an object, the data is in a known, valid state. A class is a user-defined data type and does not take space in memory until we create an object from it. Constructors create space for data members and put values in them.
Key characteristics of constructors:
- A constructor is a function that has the same name as the class.
- It has no return type, so it contains no return statement.
- Whenever an instance of a class comes into scope, the constructor is executed.
- Constructors can be overloaded.
Types of Constructors
Compiler Generated Constructor If a constructor is not defined by the user, the compiler generates it automatically. This constructor has no parameter and does nothing. The behavior of the compiler-synthesized constructor is rarely what we want—it does no initialization.
Simple Constructor A simple constructor is a user-defined constructor that takes no argument. When written, it assumes the role of the default constructor, and the compiler will not call its own default constructor. It is good programming practice to always provide a default constructor (a constructor with no arguments).
Parameterized Constructors
A parameterized constructor takes arguments. It is automatically called when the required number of arguments are passed to it. Through this, we can easily assign passed values to class data members for a particular object. For example: Date (int, int, int);—a parameterized constructor taking three integer arguments.
Constructor Overloading We can provide more than one constructor using function overloading. The rules for function overloading apply: the function name remains the same, but the argument list must differ in number or type of arguments. We cannot have two functions with the same number and type of arguments. The same concept applies to constructors.
Default Arguments in Constructors
In C++, we can provide default arguments to constructors. For example: Date (int day=1, int month=1, int year=1);. If we provide default values for all arguments, this constructor becomes the default constructor for the class. It is better not to write a separate constructor with no arguments in this case.
💡 Why this matters: Constructors ensure that every object starts in a valid state, preventing the common and hard-to-debug errors caused by uninitialized data.
Example of Constructors:
Consider a Date class with data members day, month, and year (all int). The constructor can initialize these to a known state, such as day=1, month=1, year=1900. We can write a constructor that takes three arguments (int day, int month, int year) and assigns them to the data members. When we write Date myDate;, space for myDate is reserved in memory, then control goes to the constructor which assigns values to the private data members.
📌 Example of Constructor Overloading:
class Date {
public:
Date(); // default constructor
Date(int, int); // two-argument constructor
Date(int, int, int); // three-argument constructor
private:
int day, month, year;
};
When creating objects: Date date1; calls the default constructor, Date date2(12,12); calls the two-argument constructor, and Date date3(25,12,2002); calls the three-argument constructor.
Utility Functions
While member functions are normally in the public part of a class, some functions are private and are called utility functions. These are used by other methods of the class but are not meant to be accessed from outside. For example, a setDate function might be called by the constructor and by other member functions, but we may not want it called directly from outside the class. Utility functions are placed in the private section of the class.
Destructors
A destructor has the same name as the class with a preceding tilde (~), written as a single word (e.g., ~Date). The destructor cannot be overloaded—there is only one destructor per class. It is automatically called when an object is destroyed. An object is destroyed when it goes out of scope (e.g., when a function exits or the main program ends).
The destructor is normally used for memory manipulation purposes. If a constructor allocates memory (e.g., from the heap/free store), the destructor must de-allocate that memory to ensure it is returned to the free store.
Key characteristics of destructors:
- Destructors cannot be overloaded.
- Destructors take no arguments.
- Destructors don't return a value, so they have no return type and no return statement in the body.
Complete Example with Date Class:
The lecture provides a complete program showing constructors, destructor, and set/get functions. The class has three overloaded constructors (default, two-argument, three-argument) and a destructor. Each constructor displays a message when called ("The default constructor is called", etc.), and the destructor displays "The object has destroyed". The program creates three objects and demonstrates when each constructor is called based on the arguments provided.
Memory Management:
When objects of a class are created, the functions of the class take only one copy in memory, shared by all objects. However, the data part of each object takes individual memory locations. When we call a function on a specific object using the dot operator (.), the function operates only on that object's data. For example, date1.setMonth(3); sets only the month of date1, leaving date2 and date3 untouched.
🔑 Definition — Constructor: A special function with the same name as the class, having no return type, that is automatically called when an object is instantiated to initialize its data members.
🔑 Definition — Destructor: A special function with the same name as the class preceded by a tilde (~), taking no arguments and having no return type, automatically called when an object is destroyed, typically used for memory cleanup.
📐 Formula:
- Constructor syntax:
ClassName(parameters);— No return type, same name as class. - Destructor syntax:
~ClassName();— No return type, no parameters, cannot be overloaded.
📌 Example:
class Date {
public:
Date(); // Default constructor
Date(int, int, int); // Parameterized constructor
~Date(); // Destructor
void setDay(int i); // Set function
int getDay(); // Get function
private:
int day, month, year; // Private data members
};
// Constructor with three arguments
Date::Date(int theDay, int theMonth, int theYear) {
day = theDay;
month = theMonth;
year = theYear;
cout << "The constructor with three arguments is called" << endl;
}
// Destructor
Date::~Date() {
cout << "The object has destroyed" << endl;
}
int main() {
Date date1, date3(25,12,2002); // Objects created
// Output: The default constructor is called
// The constructor with three arguments is called
// The object has destroyed (twice, when main ends)
}
⭐ Key Takeaways
Constructors are essential for object initialization, preventing uninitialized data errors by ensuring objects start in a known valid state. They share the class name, have no return type, and can be overloaded with different parameter lists. Destructors, named with a tilde prefix, cannot be overloaded and are used for cleanup, especially deallocating dynamically allocated memory. Utility functions (private member functions) support internal class operations without exposing them to outside code. All objects of a class share a single copy of member functions in memory, while each object has its own data space, with the dot operator ensuring functions operate on the correct object's data.
🧠 Quick Revision Questions
- What is the main purpose of a constructor in C++, and what problem does it solve?
- What are the three types of constructors discussed in this lecture, and how do they differ?
- Why can't a constructor have a return type, and what happens if you include a return statement in its body?
- What is a utility function, and where is it placed within a class?
- When is a destructor called, and why can it not be overloaded?
📘 Lecture 28 — Dynamic Memory Management with Classes in C++
📖 Overview: This lecture explores dynamic memory allocation in C++ using the
newanddeleteoperators, contrasting them with C'smalloc()andfree()functions. It explains how these operators integrate with classes, constructors, and destructors, and covers practical considerations for managing memory in object-oriented programs.
🗂️ Topics Covered
The lecture begins by reviewing C-style memory allocation with malloc(), calloc(), and free(), then introduces C++'s new and delete operators for simpler, type-safe allocation. It demonstrates using new with classes and objects, explains how constructors are called automatically during allocation, and shows the proper use of destructors for deallocation. Memory leak prevention, class abstraction, messaging between objects, and language extension through user-defined types round out the discussion.
📝 Lecture Summary
Memory Allocation in C
In C, memory is dynamically allocated at runtime from a region called the heap using functions like malloc(), calloc(), and realloc(). The malloc() function requires the programmer to specify the exact number of bytes needed and returns a void pointer (void *), which must be cast to the appropriate type before use. For example: datePtr = (Date *) malloc( sizeof( Date ) );. This memory is uninitialized and may contain garbage values. Memory must be freed using the free() function.
🔑 Definition — heap: The region of memory allocated at runtime in C. In C++, this region is called the free store.
📌 Example: To allocate space for 10 integers in C: malloc( 10 * sizeof(int) ); — This returns a void * that must be cast to int *.
Memory Allocation in C++
C++ introduces the new operator for dynamic memory allocation. Unlike malloc(), new is an operator, not a function. It automatically determines the number of bytes needed, returns a pointer of the correct type (no casting required), and initializes the memory. The corresponding deallocation operator is delete.
🔑 Definition — free store: The region of available memory in C++ from which dynamic allocation occurs (equivalent to the heap in C).
📐 Formula: pointer = new data_type; → allocates memory for one variable of data_type
📐 Formula: pointer = new data_type[number_of_elements]; → allocates an array
📐 Formula: delete pointer; → deallocates a single object
📐 Formula: delete [] pointer; → deallocates an array
📌 Example:
int *iptr;
iptr = new int; // allocates one int, returns int*
iptr = new int[10]; // allocates array of 10 ints
delete [] iptr; // deallocates the array
💡 Why this matters: The new operator is safer and simpler than malloc() because it eliminates the need for sizeof(), explicit casting, and manual byte calculations.
When memory in the free store is insufficient, malloc() returns NULL, while the new operator returns 0. Always check the return value of new against 0 for failure.
new Operator and Classes
The new operator works with user-defined class types exactly as with primitive types. When creating an object with new, three actions occur automatically:
- It determines the memory size needed for the object (no
sizeofneeded) - It calls the constructor of the class to initialize data members
- It returns a pointer of the class type (no casting needed)
🔑 Definition — constructor call on new: When new creates an object, the class constructor is invoked automatically to initialize the object with meaningful values rather than garbage.
📌 Example:
Date *dptr;
dptr = new Date; // allocates memory AND calls Date() constructor
int day = dptr->getDay(); // access via arrow operator
For an array of objects: dptr = new Date[10]; — This calls the default (parameterless) constructor for each of the 10 objects and returns a pointer to the first object.
💡 Why this matters: Unlike malloc(), which only allocates raw bytes, new creates a properly initialized object by invoking its constructor.
Classes and Structures in C++
In C++, structures and classes are very similar. Both can contain data members and member functions. The key difference is default visibility: members of a struct are public by default, while members of a class are private by default. Good practice is to explicitly write public: or private: keywords for clarity.
📌 Example:
struct abc { // members are public by default
int integer;
float floatingpoint;
};
class Date { // members are private by default
public:
// public interface
private:
int month, day, year;
};
new Operator and Constructors
The new operator can be called from inside a constructor to dynamically allocate memory for data members. This is common when a class has a pointer data member whose size varies at runtime, such as a student's name.
🔑 Definition — dynamic allocation in constructor: Using new within a constructor to allocate memory for pointer data members whose size is determined at runtime.
📌 Example: In a Student class, the constructor allocates memory for the name string: this->name = new char[strlen(name) + 1]; followed by strcpy(this->name, name);
delete Operator and Classes
When memory is allocated with new inside a constructor, it must be freed with delete inside the destructor. The destructor is the appropriate place because it is called when the object is destroyed. Memory allocated from the free store is a system resource that is NOT automatically returned to the system — it must be explicitly freed.
📐 Rule of thumb: Whenever a class has a pointer data member that allocates memory at runtime, provide a destructor to release that memory.
📌 Example — Complete allocation/deallocation cycle:
class MyDate {
public:
MyDate() { month = day = year = 0; }
~MyDate() { /* cleanup code */ }
private:
int month, day, year;
};
main() {
MyDate *dptr = new MyDate[10]; // allocate 10 objects, constructors called
delete [] dptr; // destructors called, memory freed
}
💡 Why this matters: Without proper deallocation in the destructor, memory leaks occur — allocated memory is never returned to the system even after program termination.
The output from proper code shows constructors called 10 times (for allocation) and destructors called 10 times (for deallocation), with no memory leak.
new, delete outside Constructors and Destructors
The new and delete operators can be called from any function, not just constructors and destructors. For example, you might need to resize a dynamically allocated string within an existing object by first using delete to free the old memory, then new to allocate new space.
📌 Example: Changing a student's name from "Abdul Khaliq" to "Abdul Khaliq Khan" requires: deallocating the old string with delete[], calculating the new string length with strlen(), allocating new memory with new[], and copying the new string.
main() Function and Classes
In C++ programs using classes, the main() function becomes very small — often containing only a few lines to create objects and call methods. Approximately 90% of program code resides inside class definitions. Classes are written first, and main() is written after class definitions.
Class Abstraction
Abstraction means exposing only the interface (method signatures and their purposes) to users while hiding the implementation details (internal variables, data manipulation, function internals). Users only need to know what a method does, not how it does it.
🔑 Definition — class abstraction: The principle of providing only the public interface to users while keeping implementation details private and hidden.
Messages and Methods
Calling a function on an object is equivalent to sending a message to that object. The term "method" comes from the idea that it is a "way of doing something." The entire program operates by sending messages between objects and receiving responses.
🔑 Definition — messaging: The object-oriented concept where function calls to objects are viewed as sending messages, requesting the object to perform some action.
Classes to Extend the Language
C++ allows programmers to create user-defined data types (classes) that extend the language's capabilities. For example, since C++ has no primitive type for complex numbers, a Complex class can be created with double members for real and imaginary parts. Operators like +, *, and / can be overloaded to work naturally with complex numbers.
📌 Example: Without operator overloading, adding complex numbers requires a function like cadd(a, b). With operator overloading, you can write a + b naturally for user-defined complex types.
⭐ Key Takeaways
The new operator in C++ is superior to malloc() because it automatically determines memory size, calls constructors for proper initialization, and returns the correct pointer type without casting. Every new must be paired with a matching delete to prevent memory leaks — typically using constructors for allocation and destructors for deallocation. When allocating arrays with new[], use delete[] with brackets. The free store memory is a system resource that requires explicit deallocation. Classes enable abstraction by exposing interfaces while hiding implementation, and they allow programmers to extend the C++ language with new data types like complex numbers.
🧠 Quick Revision Questions
- What are the three key advantages of the
newoperator overmalloc()when creating class objects? - Why must destructors be provided for classes that have pointer data members allocated with
new? - What is the correct syntax for allocating and deallocating an array of 10
Dateobjects? - What does
newreturn if memory in the free store is insufficient? - How does class abstraction benefit both the programmer and the user of a class?
📘 Lecture 29 — Friend Functions and Friend Classes
📖 Overview: This lecture introduces the concept of friend functions and friend classes in C++ programming. It explains how external functions (non-member functions) and entire classes can be granted special access to the private data members of a class, why this is sometimes necessary, and the important limitations and precautions associated with using friendship in object-oriented programming.
🗂️ Topics Covered
The lecture covers the definition and purpose of friend functions, the syntax for declaring friend functions inside a class, and three sample programs demonstrating friend functions accessing private data of one and two classes. It then extends the concept to friend classes, where an entire class is granted access to another class’s private members, and discusses the critical principles of friendship: it is granted (not taken), one-way, and non-transitive. The lecture concludes with a summary emphasizing the careful use of friendship due to its violation of data encapsulation.
📝 Lecture Summary
6) Friend functions
The lecture begins by reviewing the concepts of class (a user-defined data type), encapsulation, and data hiding. Data members are typically declared private to be visible only from inside the class, while public member functions serve as the interface for the outside world. Normally, private data cannot be accessed directly from outside the class.
Sometimes, however, a need arises to access the private data of a class from outside. This is where the concept of friend functions becomes useful. A friend function of a class has access to the private data members of that class, even though it is not a member function of the class. The analogy used is that just as a friend has access to your inner thoughts and feelings, a friend function has access to the inner (private) data of a class. This is a powerful feature but introduces potential vulnerability, as it partially violates data encapsulation and data hiding.
🔑 Definition — Friend Function: A non-member function that is declared as a friend of a class and, as a result, has access to all private data members and utility functions of that class.
💡 Why this matters: Friend functions allow non-member functions to interact with a class’s private data when needed (e.g., operator overloading, interacting with multiple classes) without making the data public, but they weaken encapsulation and should be used sparingly.
7) Declaration of Friend Functions
To declare a friend function, place its prototype inside the class definition (in either the private or public section) preceded by the keyword friend. Friendship is a strong declaration and is not affected by public or private keywords. The definition of the friend function always appears outside the class (without the friend keyword). Usually, an object of the class is passed as a parameter to the friend function so it can access the object’s private data.
Friend function prototype syntax inside a class:
friend return_type friend_function_name(int, char);
A class declares which functions are its friends – a function cannot declare itself a friend of a class from the outside. Friendship is granted, never taken. The class maintains control over what external functions can access its private members.
🔑 Definition — Friendship is Granted, Never Taken: A class must explicitly declare a function or another class as its friend; an external function or class cannot declare itself a friend of a class.
📌 Example (Declaration):
class myClass {
friend void increment(myClass *, int); // Friend function declaration
private:
int topSecret;
public:
void display();
myClass();
};
8) Sample Program 1
This program demonstrates a friend function increment that accesses and modifies the private data member of a single class, myClass.
The class myClass has a single private data member int topSecret initialized to 100 by the constructor. The friend function increment is declared inside the class. Its definition is outside the class:
void increment(myClass *a, int i) {
a->topSecret += i; // Modify private data
}
In main(), an object x of type myClass is created. After displaying the initial value of topSecret (100), the friend function increment(&x, 10) is called. This directly modifies the private data topSecret of the object x, adding 10 to it. The subsequent display() call will show the new value (110). A non-friend function trying to access topSecret would result in a compiler error.
🔑 Key Concept: Friend functions can directly modify private data members through an object pointer or reference.
📐 Formula (Friend Function Call): friend_function(&object, value); → object.privateData += value;
📌 Example (Program 1 Code):
#include <iostream.h>
class myClass {
friend void increment(myClass *, int);
private:
int topSecret;
public:
void display() { cout << "\n The value of the topSecret is " << topSecret; }
myClass();
};
myClass::myClass() { topSecret = 100; }
void increment(myClass *a, int i) {
a->topSecret += i;
}
main() {
myClass x;
x.display(); // Output: 100
increment(&x, 10);
x.display(); // Output: 110
}
Output:
The value of the topSecret is 100
The value of the topSecret is 110
9) Sample Program 2
This program demonstrates a friend function addBoth that has access to private data members of two different classes, myClass1 and myClass2. This is a common use case for friend functions.
myClass1 and myClass2 each have a private data member int topSecret (initialized to 100 and 200 respectively). A standalone function addBoth is needed to add the topSecret values from objects of both classes. To give it access to both classes' private data, addBoth is declared as a friend function inside both class definitions.
Because myClass2 is referenced in the friend declaration inside myClass1 before myClass2 is defined, a forward declaration of myClass2 is required before the definition of myClass1:
class myClass2; // Forward declaration
🔑 Definition — Forward Declaration: A declaration that tells the compiler that a class name exists (e.g., class myClass2;), allowing it to be referenced before its full definition.
📌 Example (Friend function of two classes):
class myClass2; // Forward declaration
class myClass1 {
private:
int topSecret;
public:
void display() { cout << "\nThe value of the topSecret is " << topSecret; }
myClass1() { topSecret = 100; }
friend void addBoth(myClass1, myClass2);
};
class myClass2 {
private:
int topSecret;
public:
void display() { cout << "\nThe value of the topSecret is " << topSecret; }
myClass2() { topSecret = 200; }
friend void addBoth(myClass1, myClass2);
};
void addBoth(myClass1 a, myClass2 b) {
cout << "\nThe sum of values of topSecret in myClass1 and myClass2 is " << a.topSecret + b.topSecret;
}
Output:
The value of the topSecret is 100
The value of the topSecret is 200
The sum of values of topSecret in myClass1 and myClass2 is 300
10) Sample Program 3
This program expands on Program 2 by creating multiple friend functions (addBoth, subBoth, mulBoth, divBoth) for the same two classes. The program prompts the user to enter an operator (+, -, *, /) and then calls the corresponding friend function to perform the requested arithmetic operation on the private value data members of objects of myClass1 and myClass2.
All friend functions are declared in both classes. Their definitions are placed at the end of the file, returning a float result. This example shows that a class can have multiple friend functions for different purposes.
📌 Example (User interaction with friend functions):
// Inside main()
myClass1 myClass1Obj; // value = 200
myClass2 myClass2Obj; // value = 100
// If user enters '*'
cout << "The multiplication is : " << mulBoth(myClass1Obj, myClass2Obj) << endl;
Output (when '*' is entered):
The multiplication is : 20000
11) Friend Classes
The concept of friendship extends from individual functions to entire classes. A class can be declared as a friend class of another class. When a class is declared as a friend, all member functions of the friend class gain access to the private data members and functions of the class that granted the friendship.
Syntax for declaring a friend class (inside the granting class):
friend class ClassName;
// or
friend ClassName;
The key principles of friend classes mirror those of friend functions:
- Friendship is granted, not taken: The class declares which other classes are its friends.
- Friendship is one-way: If
OtherClassis a friend ofClassOne,OtherClasscan accessClassOne's private members, butClassOnecannot accessOtherClass's private members unlessOtherClassalso declaresClassOneas a friend. - Friendship is not transitive: If
Ais a friend ofB, andBis a friend ofC, it does not meanAis a friend ofC.Amust be explicitly declared as a friend byC.
🔑 Definition — Friend Class: A class whose all member functions have access to the private data members and methods of another class that has declared it as a friend.
📌 Example (Friend class):
class ClassOne {
friend class OtherClass; // OtherClass is a friend of ClassOne
private:
int topSecret;
};
class OtherClass {
public:
void change(ClassOne co) {
co.topSecret++; // Can access private data of ClassOne
}
};
💡 Why this matters: Friend classes can be useful when two classes are tightly related (e.g., a StraightLine class and a Quadratic class that need to interact for mathematical intersection calculations). However, they introduce strong coupling and should be used very sparingly as they violate encapsulation. Changes to the implementation of one class may require changes in the friend class.
12) Summary
The lecture concludes with a summary of key points. Classes allow separation of implementation from interface. Private data members are hidden from outside access. Friend functions are non-member functions that are granted access to a class's private data. Friend classes extend this concept so that all member functions of one class can access the private data of another. Friendship is a useful feature but must be used carefully and sparingly as it negates the core object-oriented principles of encapsulation and data hiding. The fundamental principles of friendship are:
- Friendship is granted, not taken.
- Friendship is not reciprocal (one-way unless explicitly declared both ways).
- Friendship is not transitive.
⭐ Key Takeaways
The most critical concepts from this lecture are: (1) Friend functions are non-member functions that, when declared with the friend keyword inside a class definition, gain full access to that class's private data members and utility functions; (2) Friendship is strictly controlled by the class (the class grants friendship, an external function/class cannot take it); (3) A friend function can be a powerful tool when a function needs to access the private data of two or more different classes (like adding private values from two separate class objects); (4) The concept extends to friend classes where all member functions of one class gain access to another class's private members; (5) Friendship is one-way and non-transitive, and it must be used very sparingly as it directly violates the principles of data hiding and encapsulation by exposing a class's internal structure.
🧠 Quick Revision Questions
- What is the purpose of declaring a friend function, and what access does it have to a class?
- Can a function declare itself a friend of a class from outside the class definition? Explain why or why not.
- In Sample Program 2, why is a forward declaration
class myClass2;needed before definingmyClass1? - Explain the difference between a friend function and a friend class. What are the implications of using a friend class on code maintainability?
- If Class A declares Class B as its friend, and Class B declares Class C as its friend, does Class A automatically have access to Class C's private members? Explain the principle at play here.
📘 Lecture 30 — Reference Data Type
📖 Overview: This lecture introduces the reference data type in C++, explaining how references serve as aliases or synonyms for existing variables. It covers the declaration, initialization, and practical applications of references, particularly in implementing efficient call-by-reference in functions, and compares references with pointers while discussing dangers like dangling references.
🗂️ Topics Covered
The lecture covers reference data type declaration and initialization, examples showing how references work as synonyms, comparing call by value vs call by reference using pointers and references, using const with references for safety, implementing swap function with references, differences between references and pointers, references as return values, dangling references, and how cout statements work by returning references.
📝 Lecture Summary
Reference data type
Today's topic is about references, which is very important from the C++ perspective. C++ defines a way to create an alias or synonym of any data type, called a reference. We declare it using the & operator, but this is confusing because we also use & as the address-of operator. We write int &i; which means that i is a reference to an integer — easier to read from right to left. A reference has to be initialized when declared. For example: int &j = i; declares j as another name for i — it does NOT create a new variable, just a new name for the existing variable. Manipulating either i or j manipulates the same memory location.
🔑 Definition — Reference: A synonym or alias that provides another name for an existing variable. It must be initialized when declared and cannot be reassigned to refer to a different variable.
📐 Declaration Syntax: datatype &reference_name = existing_variable; → Creates an alias for the existing variable
📌 Example:
int i;
int &j = i; // j is a reference to i
i = 123;
cout << i; // displays 123
cout << j; // also displays 123
i++;
cout << i; // displays 124
cout << j; // also displays 124
Both i and j refer to the same memory location.
💡 Why this matters: References provide an elegant way to implement call-by-reference without the cumbersome pointer notation (using * for dereferencing).
Example 1
The lecture presents an example using a structure bigone with a string of 1000 characters, demonstrating three ways to pass it to functions:
- Call by value (
valfunc(bigone v1)) — copies the entire structure onto the stack - Call by pointer (
ptrfunc(const bigone *p1)) — passes address using&bo - Call by reference (
reffunc(const bigone &r1)) — passes reference, looks like call by value
The const keyword with references prevents the function from modifying the original data while maintaining efficiency. The function gets the address but cannot change the original value — providing efficiency of call by reference and safety of call by value.
📌 Example Code Snippet:
struct bigone {
int serno;
char text[1000];
} bo = {123, "This is a BIG structure"};
void valfunc(bigone v1); // Call by value
void ptrfunc(const bigone *p1); // Call by pointer
void reffunc(const bigone &r1); // Call by reference
int main() {
valfunc(bo); // copies entire structure
ptrfunc(&bo); // passes address
reffunc(bo); // passes reference (looks like value call)
}
Difference Between References and Pointers
- References must be initialized when declared; pointers can be declared without initialization
- References cannot be NULL; pointers can be NULL
- Once initialized, a reference cannot be reassigned to refer to another variable
- No arithmetic can be performed on references (no increment, decrement, or reassignment), unlike pointers
- References are primarily used for implementing call by reference with a clean interface
🔑 Definition — Dangling Reference: A reference that points to a memory location that no longer exists (e.g., returning a reference to a local variable that goes out of scope).
Example 2
Functions can return references. The syntax is: datatype& function_name(parameter list). For example: int& num();
When a function returns a reference to a global variable (which exists throughout the program), it can even appear on the left side of an assignment statement, like num() = 200; which is equivalent to assigning to the global variable. However, this is confusing and considered bad practice.
📌 Example:
int myNum = 0; // Global variable
int& num() {
return myNum; // Returns reference to global variable
}
int main() {
int i;
i = num(); // i gets value of myNum (0)
num() = 200; // Equivalent to myNum = 200
cout << myNum; // Displays 200
}
⚠️ Danger: Never return a reference to a local variable — that variable dies when the function returns, creating a dangling reference. Use global or static variables instead.
⭐ Key Takeaways
References are aliases that must be initialized when declared and cannot be reassigned. They provide an elegant way to implement call-by-reference without pointer notation — use const with references to combine efficiency with safety. References cannot be NULL and do not support arithmetic operations. Never return a reference to a local variable to avoid dangling references. Functions returning references should use global or static variables. The & operator acts as a reference declarator in declarations but as address-of operator in code.
🧠 Quick Revision Questions
- What is a reference in C++ and how is it different from a pointer?
- Why must a reference be initialized when declared?
- How does using
constwith references provide both efficiency and safety? - What is a dangling reference and how can it be avoided?
- Can a reference be reassigned to refer to a different variable after initialization?
📘 Lecture 32 — Lecture No. 32
📖 Overview: This lecture continues the discussion on operator overloading in C++, focusing on overloading the minus operator, implementing operators with the Date class, and handling unary operators. It emphasizes the importance of meaningful operator definitions and the careful attention to detail required when designing robust classes.
🗂️ Topics Covered
The lecture covers overloading the minus and minus-equal operators for the Complex class, determining which operators are meaningful for different classes, implementing date arithmetic with a full Date class (including plus and pre-increment operators), and discussing unary operators with the examples of pre/post increment. It also addresses comparison operators, friend functions, and code reuse principles through utility functions.
📝 Lecture Summary
Recap
Before further discussing operator overloading, it is necessary to know that new operators (new symbols) cannot be introduced — only existing symbols can be overloaded. Overloading operators is exactly like writing functions; however, one should remain close to the original meaning of the operator. It is bad practice to define something in opposite terms (e.g., plus operator doing subtraction). Under operator overloading, binary and unary operators will remain unchanged — we cannot make a unary operator work as a binary operator or vice versa. In the case of binary operators, the driving force is the left-hand operand.
Overloading Minus Operator
The process of defining the minus operator (-) for the Complex class is similar to the plus operator. It is a binary operator having two arguments, both complex numbers. When subtracting two complex numbers, it returns a complex number: subtract the real part from real part and subtract the imaginary part from imaginary part.
🔑 Definition — Member Operator: When defining as a member operator, only one argument is passed (the right-hand side operand). The left-hand side calls this and is already available to the function.
📐 Formula:
Complex operator – (Complex c) {
Complex tmp;
tmp.real = real – c.real;
tmp.imag = imag – c.imag;
return tmp;
}
📌 Example: For the minus equal operator (-=), the value of the calling party (left-hand side) will be changed. No temporary complex number is needed:
Complex Complex::operator -= (Complex c) {
real -= c.real;
imag -= c.imag;
}
💡 Why this matters: Overloading minus for the string class does not make sense. Only define operators that are self-explanatory, readable, and understandable. The thing to understand is that every operator does not make sense for every class.
Operators with Date Class
For the Date class, adding a number to a date (e.g., today's date plus 5) makes sense — we get a new date. Subtracting a number also makes sense. However, subtracting two dates requires careful consideration.
Adding an integer to a date involves complex logic because of:
- Different month lengths (30 or 31 days)
- February with 28 or 29 days (leap year)
- End of year transitions
🔑 Definition — Leap Year Rules: If the year is divisible by 4, it is a leap year. For century years, it must be divisible by 400.
📐 Formula: The Date class implementation includes:
const int Date::daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool Date::leapYear(int y) {
if ((y%400 == 0) || (y%100 != 0 && y%4 == 0))
return true;
else
return false;
}
📌 Example: The plus operator implemented using a loop that calls the pre-increment operator:
Date Date::operator + (int numberOfDays) {
for (int i = 1; i <= numberOfDays; i++) {
++(*this);
}
return *this;
}
📌 Example: The pre-increment operator adds one day:
Date Date::operator ++ () {
if (day == daysOfMonth(*this) && month == 12) {
day = 1;
month = 1;
++year;
}
else if(day == daysOfMonth(*this)) {
day = 1;
++month;
}
else {
day++;
}
}
💡 Why this matters: Date arithmetic is very important in business applications. It simplifies calculating vacation end dates, payment periods, and other date-based calculations.
Unary Operators
Unary operators take one argument (like i++ or ++i). You cannot make a unary operator into a binary operator or vice versa.
🔑 Definition — Pre-increment Operator (++): Adds one day to the current date. As a member function, it takes no argument and returns a Date object.
📐 Formula: Prototype for pre-increment: Date operator ++ ();
To distinguish pre-increment from post-increment, an int argument is passed to the post-increment version:
Date operator ++ (int); // post increment operator
The lecture shows how code reuse works: the plus operator uses the pre-increment operator, and the pre-increment operator uses the daysOfMonth utility function. Don't repeat code inside a class — make a function for repeated code and call it where needed.
📌 Example: Comparison operators can also be overloaded. The greater-than operator returns bool:
bool Date :: operator > (Date d) {
if (year > d.year) return true;
else if (year == d.year) {
if (month > d.month) return true;
else if (month == d.month) {
if(day > d.day) return true;
else return false;
}
else return false;
}
else return false;
}
📌 Example: For integer + date, a friend function is needed because the integer is on the left-hand side (not a Date object). The friend function gets both arguments and can access the internal structure of the Date class.
💡 Why this matters: Friend operators can glue two classes together. A classic example is multiplying a vector with a matrix — the multiplication operator will be a friend of both classes.
⭐ Key Takeaways
The most critical things to remember from this lecture are: (1) Only overload operators that make logical sense for the class, and maintain their original meaning to keep code readable. (2) When implementing date arithmetic, pay careful attention to all edge cases including leap years, month lengths, and year transitions. (3) Use code reuse by calling simpler operators from more complex ones (e.g., using ++ inside the + operator). (4) Create utility functions (like daysOfMonth) as private members to centralize logic and avoid code duplication. (5) Use friend functions when the left-hand operand is not an object of the class, such as when adding an integer to a date.
🧠 Quick Revision Questions
- Why should you avoid defining the minus operator for the string class?
- What are the three main cases to handle in a pre-increment operator for the Date class?
- How do you distinguish between pre-increment and post-increment operator prototypes?
- When would you use a friend function instead of a member function for operator overloading?
- What is the logic for comparing two dates using the greater-than operator?
📘 Lecture 33 — Operator Overloading
📖 Overview: This lecture explores advanced operator overloading techniques in C++, focusing on the assignment operator and its proper implementation to handle dynamic memory. It introduces the
thispointer and its role in enabling chained assignments and preventing self-assignment errors. The lecture also covers type conversions between user-defined and built-in types, using constructors and conversion operators, with practical examples from String, Date, and Fraction classes.
🗂️ Topics Covered
The lecture covers the need for and implementation of assignment operators in classes with dynamic memory, using the String class as a primary example. It introduces the this pointer for self-assignment checking and returning values from functions. The discussion extends to conversion techniques between user-defined and built-in types, demonstrating both constructor-based and conversion operator approaches. Practical examples include the Date class and the Fraction class for exact arithmetic.
📝 Lecture Summary
Operator Overloading
Operator overloading enables writing clean, simple code when working with user-defined types. When we add two objects of a class using a + b, we are adding two instances of a user-defined data type. In object-oriented programming, more effort is made in class definitions so that classes know how to manipulate themselves—how to add objects of their own type, how to display themselves, and perform other operations. For example, incrementing a date should be encapsulated within the Date class itself, not in the main program.
Assignment Operator
The question arises whether we need an assignment operator. It is needed when we want to assign one object to another, like a = b. C++ provides a default assignment operator that performs member-wise copy. For a class with three integers and two floats, writing a = b copies each member individually. However, this default behavior can cause problems in special cases.
Consider a String class with a data member *buf (a pointer to character array). If we write s2 = s1, the default assignment operator copies the pointer value (address), not the string content. Both objects end up pointing to the same memory location. If we delete s1, its destructor frees that memory, leaving s2's pointer dangling, pointing to deallocated memory.
The solution is to write our own assignment operator. The code should:
- Delete the existing buffer of the left-hand side object
- Allocate new memory of the right size
- Copy the string content, not just the pointer
void String::operator = ( const String &other )
{
int length;
length = other.length();
delete buf;
buf = new char [length + 1];
strcpy( buf, other.buf );
}
🔑 Definition — Default Assignment Operator: A member-wise copy operator automatically provided by C++ that copies data members individually from one object to another, which can cause problems with pointer members.
📐 Formula: strcpy( buf, other.buf ) → Copies the actual string content from the source object's buffer to the destination object's buffer.
📌 Example:
String myString( "here's my string" );
String yourString( "here's your string" );
yourString = myString; // Calls our custom assignment operator
// After assignment, yourString displays "here's my string" in its own memory location
💡 Why this matters: Whenever objects allocate memory (using new), we must define an assignment operator. Otherwise, the default operator only copies pointer addresses, leading to double deletion and memory corruption.
this Pointer
Whenever an object calls a member function, the function implicitly receives a pointer from the calling object, known as the this pointer. this is a keyword and cannot be used as a variable name. It refers to the calling object.
Three equivalent ways to refer to a member buf of the String class:
buf→ directly refers to the calling object's memberthis->buf→ uses the pointer notation(*this).buf→ dereferences the pointer (parentheses are necessary because dot operator binds stronger than*)
Self Assignment
Self-assignment occurs when an object is assigned to itself, like s = s. While this seems harmless for integers, it's dangerous for classes with dynamic memory. Our assignment operator first deletes the calling object's buffer, then tries to copy from the right-hand side object. In self-assignment, the buffer of s is deleted, and then the code tries to use it for copying—causing unpredictable behavior.
Self-assignment can occur indirectly, for example:
String s, *sptr;
sptr = &s;
s = *sptr; // This is effectively s = s;
To prevent this, we check if the calling object is the same as the passed object:
void String::operator=( const String &other )
{
if( this == &other )
return; // Self-assignment detected, do nothing
delete buf;
length = other.length;
buf = new char[length + 1];
strcpy( buf, other.buf );
}
Returning this Pointer from a Function
For chained assignments like s3 = s2 = s1, the assignment operator must return a value. The expression s2 = s1 should return s2 so it can be assigned to s3. We use the this pointer to return the calling object:
String &String::operator=( const String &other )
{
if( &other == this )
return *this;
delete buf;
length = other.length;
buf = new char[length + 1];
strcpy( buf, other.buf );
return *this;
}
This function returns a reference to the calling object. The this pointer is used in chained statements like cout << a << b << c, where the << operator returns a reference to cout, enabling the chain.
Similarly, for the Date class:
Date& Date::operator+=(int days)
{
for (int i=0; i < days; i++)
*this++;
return *this;
}
Now we can write date2 = date1 + 1 or date2 = date1++ just like with integers.
Conversions
C and C++ have rules for implicitly converting one built-in type to another in various situations:
- Assigning a value (int to long)
- Performing arithmetic (int to float before addition)
- Passing arguments to functions
- Returning values from functions
We can define conversions for our classes too. A class is a user-defined data type, and we can specify conversions between classes or between a class and a built-in type.
Conversion by Constructor: A constructor that takes only one parameter is considered a conversion function. It specifies a conversion from the parameter's type to the class type. For example, if we have a Fraction class and write f = 3, the constructor that takes a single integer is called, converting 3 into a Fraction object with numerator 3 and denominator 1.
Conversion Operators: For converting from our class to another class (especially one in a library we can't modify), we use conversion operators.
Sample Program (conversion by constructor)
The Fraction class stores a fraction as separate numerator and denominator integers, avoiding floating-point precision problems. For example, 1/3 stored as double becomes 0.33333..., and 3 * 0.33333... gives 0.99999..., not 1. The Fraction class keeps 1 and 3 as integers, so arithmetic remains exact.
The constructor provides a default value of 1 for the denominator:
Fraction::Fraction( long num, long den = 1 ) { ... }
This automatically handles conversions. When the program calculates a = b + c where b is 23/11 and c is 2/3, the addition operator uses the greatest common factor (GCF) to produce exact results:
Output: 91/33
💡 Why this matters: Real-world applications like banking require exact arithmetic. Banks store currency as strings or separate integers, not as floating-point numbers, ensuring that 90 paisas added to 9 rupees and 10 paisas equals exactly 10 rupees and 0 paisas, not 9.999...
⭐ Key Takeaways
To properly manage classes with dynamic memory, always define a custom assignment operator that allocates separate memory and copies content rather than pointers. Use the this pointer to check for self-assignment (the first and most important check in the assignment operator) and to return the calling object for chained assignments. Single-argument constructors serve as implicit conversion functions, allowing seamless conversion from built-in types to user-defined types. For exact arithmetic, store values as separate components (like numerator and denominator in Fraction class) to avoid floating-point precision errors found in banks and financial applications.
🧠 Quick Revision Questions
- Why does the default assignment operator cause problems for classes with pointer data members that point to dynamically allocated memory?
- What is the first check that should be performed in a custom assignment operator, and why is it important?
- How does the
thispointer enable chained assignments likes3 = s2 = s1? - What is the difference between conversion by constructor (single-argument constructor) and conversion operators?
- In the Fraction class example, why does storing numerator and denominator separately prevent precision loss compared to using floating-point numbers?
📘 Lecture 35 — Streams
📖 Overview: This lecture provides a comprehensive overview of C++ streams, explaining how they function as ordered sequences of bytes for input and output operations. It covers source and destination concepts, formatted input/output, buffered versus unbuffered streams, and the member functions associated with stream objects, which are essential for managing data flow between programs and external devices.
🗂️ Topics Covered
The lecture covers streams as an ordered sequence of bytes, source and destination of streams including keyboard, screen, files, and memory, state of streams for error checking, formatted input and output capabilities, buffered versus unbuffered I/O with cout and cerr, standard predefined stream objects (cin, cout, cerr, clog), operator overloading for stream insertion and extraction, and various member functions like get(), read(), getline(), put(), write(), unget(), and peek().
📝 Lecture Summary
Streams
Streams are an ordered sequence of bytes that serve as a door or pipe through which a program can communicate with the outside world. There are two types: input streams (e.g., cin) and output streams (e.g., cout). The cin stream reads data from the keyboard and stores it in variables, while cout displays data on the screen.
When you press a key on the keyboard, its ASCII code (binary representation of a character) enters the computer. The cin stream performs implicit conversion, taking the character ASCII code and converting it into the appropriate number before storing it in an integer variable. The stream extractor operator (>>) of cin is heavily overloaded — it knows how to behave with int, char, float, and string data types and what sort of conversion is required.
🔑 Definition — Stream: An ordered sequence of bytes that allows data to move from one part of the computer to another, implemented as objects with member functions and operators.
🔑 Definition — Stream Extractor Operator (>>): The operator associated with cin that gets data from the stream and stores it into a variable; it is overloaded to handle multiple data types.
📌 Example: If we write cin >> i; where i is an integer, cin will take a character in ASCII code from the keyboard, convert it into a number, and store it into the integer variable. If we write cin >> c; where c is a character data type, pressing a key stores it as a character.
Source and Destination of Streams
For every stream, there must be some source and destination. For cin, the source is normally the keyboard, and the destination is typically an ordinary variable (native data type), an area in memory, or a user-defined object. For cout, the source may be a file, memory region, or a variable, and the destination is normally the screen, but can also be a file or printer.
"Every stream has an associated source and a destination"
Every stream has a state that can be checked. If bad input is entered (e.g., typing 'a' when expecting an integer), the stream signals an error and sets its state accordingly. From a program, we can always test whether the state of the stream is correct, allowing for error checking, debugging, and error handling.
📌 Example: When taking two integers from the keyboard, dividing one by the other, and displaying the result:
int i, j;
cin >> i;
cin >> j;
cout << i / j;
If the user enters 0 for j, we should check if j is not zero before carrying out the division.
Formatted Input and Output
Streams provide formatted input and output capabilities. We can format output so that numbers are placed correctly at the correct position, such as in electricity bills or telephone bills. Formatting allows us to specify alignment (left or right justified), decimal precision, and other presentation details. The presented representation (what humans read) can be different from the internal representation.
💡 Why this matters: Formatting ensures output is readable and professional, like displaying matrix columns aligned properly or printing values like pi as 3.141 instead of 3.1415926.
Recap Streams
Streams are an ordered sequence of bytes that allow data to move from one part of the computer to another (screen, keyboard, memory, disk files). They are implemented as objects with member functions and heavily overloaded member operators to handle a variety of data types. Streams have a state that can be checked (e.g., eof for end of file).
The header file iostream.h must be included to use cin and cout. For formatted input and output manipulation, the header file iomanip.h is required.
Standard streams provided by default when including iostream.h include cin (input/reading), cout (output/writing), cerr (standard error), and clog (standard log).
Buffered Input/Output
Due to the speed difference between electronic components (chips, memory, microprocessor) and electro-mechanical devices (keyboard, monitor, disk), buffered input/output is used. A buffer is an area where data is gathered before being written or displayed. Instead of writing each number individually to disk (which would be inefficient), data is collected in a buffer and written in one operation.
cout is a buffered output stream — it gathers data and sends it to the screen. cerr is an unbuffered output stream — it shows data on the screen immediately when it gets it. clog is a buffered standard error stream used for logging detailed program information.
The flush command forces data from the buffer to go to its destination and makes the buffer empty.
📌 Example: In a program with a loop that performs complex calculations taking one minute per iteration, we need unbuffered output on the screen to see iteration numbers after every minute. Using cerr instead of cout for this purpose ensures immediate display.
🔑 Definition — endl: When written as cout << endl;, it not only moves the cursor to the left margin of the new line but also flushes the output, making it appear as if cout is unbuffered.
Methods with Streams
Input stream (cin) methods:
- cin.get() — reads a single character from the keyboard and returns it; has two variants:
c = cin.get()andcin.get(character_variable) - cin.read() — returns a buffer instead of a single character; reads a specified number of characters into a buffer, normally up to a delimiter
- *cin.getline(char buffer, int buff_size, char delimiter = '\n') — reads a complete buffer up to the specified delimiter or number of characters; if SIZE is 100, it reads 99 characters and inserts a null character in the end
- cin.unget() — returns the most recently gotten single character
- cin.peek() — returns the next character that would be read if cin.get() were issued
Output stream (cout) methods:
- cout.put(character_variable) — outputs a single character
- cout.write() — performs raw, unformatted output of a chunk of data from the buffer
- cout.putline() — outputs a buffer (though cout itself knows how to handle character strings)
The stream insertion operator (<<) is overloaded for the output stream and returns a reference of the output stream, allowing chaining. The syntax is: ostream& ostream::operator << (char *text);
💡 Why this matters: Chaining allows statements like cout << "The value is " << i; because the first part returns a reference to cout, which is then used for the next part.
White space is significant — cin and cout are sensitive to white space, treating it as a delimiter. When entering a name like "naveed malik" using cin >> name;, only "naveed" is stored; "malik" remains in the buffer until expected.
📌 Example: Using getline function:
#include <iostream.h>
int main() {
const int SIZE = 80;
char buffer[SIZE];
cout << "\n Enter a sentence: \n";
cin.getline(buffer, SIZE);
cout << " The sentence entered is: \n" << buffer << endl;
return 0;
}
Output:
Enter a sentence:
this is a test
The sentence entered is:
this is a test
📌 Example: Using read and write functions:
#include <iostream.h>
int main() {
const int SIZE = 80;
char buffer[SIZE];
cout << "\n Enter a sentence: \n";
cin.read(buffer, 20);
cout << " The sentence entered was: \n";
cout.write(buffer, cin.gcount());
cout << endl;
return 0;
}
Output:
Enter a sentence:
This is a sample program using read and write functions
The sentence entered was:
This is a sample pro
⭐ Key Takeaways
Streams are ordered sequences of bytes implemented as objects that connect sources and destinations for data movement. cin handles input from the keyboard with implicit ASCII to value conversion, while cout handles output to screen with buffered behavior. The three key output streams are cout (buffered, normal output), cerr (unbuffered, immediate error display), and clog (buffered, logging). Stream operators (>> and <<) are heavily overloaded and return references to the stream object, enabling chaining. Important member functions include get() and getline() for character/line input, read() and write() for buffer handling, and put() for single character output.
🧠 Quick Revision Questions
- What is a stream in C++ and what are the two main types of streams?
- How does cin handle the conversion from keyboard input to stored integer values?
- What is the difference between buffered (cout) and unbuffered (cerr) output streams?
- How does the stream insertion operator (<<) enable chaining in output statements?
- What is the purpose of the getline() function and how does it differ from the get() function?
📘 Lecture 36 — Stream Manipulations
📖 Overview: This lecture explores how to control and format input/output streams in C++. It covers the different types of manipulators, format state flags, and various formatting techniques that allow programmers to control how data is displayed, including base conversion, field width, precision, and alignment.
🗂️ Topics Covered
The lecture covers stream manipulations including state-checking with flags (eof, fail, bad, good), non-parameterized manipulators (dec, hex, oct, endl, ends, flush, ws), parameterized manipulators (setw, setfill, setprecision, setbase), format state flags for controlling output behavior, formatting manipulation for justification, showing the base of numbers, scientific representation, and uppercase/lowercase control.
📝 Lecture Summary
Stream Manipulations
To use stream manipulations in C++, we need to include the iomanip.h header file (in addition to iostream.h and fstream.h). Stream objects have internal flags that can be used to check the state of the stream. These flags can be thought of as integers where each bit position specifies a particular state. For example, cin.eof() returns the state of the end-of-file bit. Similarly, cin.fail() checks if an operation has failed due to a formatting error, and cin.bad() checks if data has been lost during I/O. The cin.good() bit is set when both fail and bad bits are not set. To reset these bits to their normal good state, we use the cin.clear() member function.
Manipulators
Manipulators are inserted into streams to change their behavior. They can be either non-parameterized (taking no arguments) or parameterized (taking arguments). For example, endl is a non-parameterized manipulator that outputs a new line and flushes the buffer. Flush is another manipulator that flushes the output buffer. Manipulators allow us to perform various formatting tasks such as printing a floating-point number with a specific number of decimal places or filling empty spaces with special characters (like asterisks on cheques to prevent tampering).
💡 Why this matters: Manipulators provide an intuitive, stream-based way to control formatting without complex function calls.
Non-Parameterized Manipulators
Non-parameterized manipulators are simple manipulators that take no arguments. The most important ones for number representation convert between different bases (number systems). The oct manipulator converts numbers to octal (base 8), hex converts to hexadecimal (base 16), and dec converts back to decimal (base 10). For example, cout << oct << i displays the octal value of integer i. These manipulators work with both cin and cout. The ws manipulator skips leading whitespace. Other non-parameterized manipulators include endl (new line and flush), ends (terminate string with NULL), and flush (flush the stream).
🔑 Definition — Base: The number system used to represent numbers (binary=2, octal=8, decimal=10, hexadecimal=16).
📐 Formula: 2^n - 1 → The maximum value that can be stored in n bits
💡 Why this matters: Different bases map directly to bit patterns - octal maps to 3 bits, hexadecimal maps to 4 bits, making them useful for low-level programming and hardware interaction.
Parameterized Manipulators
Parameterized manipulators take arguments to control formatting. The setw (set width) manipulator takes an integer argument specifying the field width in spaces. For example, cout << setw(4) << number prints the number in a space of 4 digits with right justification by default. The setfill manipulator takes a character argument to fill empty spaces. When combined with setw, it fills the unused space: cout << setfill('*') << setw(10) << amount prints the amount in 10 spaces with asterisks filling the empty positions. These manipulators can be cascaded because the stream insertion operator (<<) returns a reference to the cout object. The setprecision manipulator takes an integer argument to control the number of decimal places displayed for floating-point numbers: cout << setprecision(2) << pi prints pi (3.1415926) as 3.14. The setbase manipulator takes the base as an argument (0, 8, 10, or 16) and is an alternative to oct, dec, and hex: cout << setbase(8) << number is equivalent to cout << oct << number.
🔑 Definition — setw: Sets the minimum field width for the next output operation only 📐 Example: cout << setw(10) << amount → Prints amount right-justified in 10 spaces 📌 Example: cout << setfill('*') << setw(10) << 4000 → Outputs ******4000
💡 Why this matters: setfill prevents fraud on printed cheques by filling empty spaces with characters that cannot be overwritten.
Format State Flags
Format state flags are a set of flags that control input/output behavior through the flags, setf, and unsetf member functions. These flags are defined in the ios class and include options like ios::skipws (skip whitespace), ios::left (left justify), ios::right (right justify), ios::internal (padding between sign and number), ios::dec/oct/hex (base conversion), ios::showbase (display base notation), ios::showpoint (show decimal point), ios::uppercase (use uppercase letters), ios::showpos (show + sign for positives), ios::scientific (scientific notation), and ios::fixed (fixed-point notation).
As alternatives to manipulators, there are member functions that perform the same tasks. For example, cout.width(10) is equivalent to cout << setw(10), cout.precision(2) is equivalent to cout << setprecision(2), and cout.fill('*') is equivalent to cout << setfill('*'). Member functions are defined in iostream.h, while manipulators require iomanip.h. An important distinction is that inline manipulators like setw apply only to the very next piece of data, not to subsequent output operations.
💡 Why this matters: Multiple ways exist to accomplish the same formatting task, giving programmers flexibility in coding style.
Formatting Manipulation
The setf member function allows us to set format state flags for controlling output justification: cout.setf(ios::flag, ios::adjustfield). The adjustfield has values ios::left (left-justify), ios::right (right-justify), ios::internal (padding between sign and number), and ios::left | ios::right (center output). For example:
cout.setf(ios::left, ios::adjustfield);
cout << "|" << setw(12) << i << "|" << endl; // outputs |-1234 |
cout.setf(ios::right, ios::adjustfield);
cout << "|" << setw(12) << i << "|" << endl; // outputs | -1234|
cout.setf(ios::internal, ios::adjustfield);
cout << "|" << setw(12) << i << "|" << endl; // outputs |- 1234|
Showing the Base
The showbase flag, when set, displays numbers with special notations indicating their base. Using cout.setf(ios::showbase) followed by setting the basefield, numbers are displayed with prefixes: octal numbers get a leading 0, hexadecimal numbers get a leading 0x, and decimal numbers are displayed as usual. For example, the number 77 displayed in octal becomes 0115 (with leading 0), in hexadecimal becomes 0x4d (with 0x prefix), and in decimal remains 77.
📐 Example: cout.setf(ios::oct, ios::basefield) → Displays 77 as 0115
Scientific Representation
The ios::scientific flag sets the output stream to display floating-point numbers in scientific notation (e.g., 1.946000e+009). To restore default notation, use ios::fixed: cout.setf(ios::fixed, ios::floatfield). The ios::uppercase flag controls case: when set, the 'e' in scientific notation becomes uppercase 'E', and hexadecimal letters (A-F) are displayed in uppercase.
⭐ Key Takeaways
There are two types of manipulators: non-parameterized (oct, hex, dec, endl, ws) and parameterized (setw, setfill, setprecision, setbase). The setw manipulator applies only to the next output operation, while setfill and setprecision persist until changed. Format state flags (ios::left, ios::right, ios::internal, ios::scientific, ios::showbase) provide an alternative way to control formatting through the setf member function. Member functions (width, precision, fill) offer yet another approach to accomplish the same tasks as manipulators. The showbase flag adds prefixes (0 for octal, 0x for hexadecimal) to help identify the base of displayed numbers.
🧠 Quick Revision Questions
- What header file must be included to use parameterized manipulators like setw and setprecision?
- Explain the difference between setw and setfill - which one applies only to the next output operation?
- What is the output of
cout << oct << 10and why? - How would you display the number -1234 with the minus sign left-justified and the digits right-justified in a field of 12 spaces?
- What prefix is added to a hexadecimal number when the showbase flag is set?
📘 Lecture 37 — Overloading Insertion and Extraction Operators
📖 Overview: This lecture explains how to overload the stream insertion (
<<) and extraction (>>) operators for user-defined classes. It covers why these operators must be implemented as non-member functions, how to declare them as friends of a class, and how to return references to support cascading operations, with practical examples using aDateclass and aMatrixclass.
🗂️ Topics Covered
The lecture begins by reviewing operator overloading principles (preserving operator spirit and number of operands) and the need to support cascading. It then explains why stream insertion and extraction operators cannot be member functions and must be non-member friends. The return types (ostream& and istream&) and parameter passing (first parameter always by reference, second parameter by reference for extraction) are detailed. Two full examples are provided: a Date class and a Matrix class, both with overloaded << and >>. The lecture concludes with practical tips for implementing these operators.
📝 Lecture Summary
Overloading Insertion and Extraction Operators
When overloading operators, the spirit or behavior of the original operator must be maintained. For stream insertion (<<) and extraction (>>), this means preserving their ability to work with streams like cin and cout. The number of operands cannot be changed: << and >> are binary operators, so they take two operands.
🔑 Definition — Binary operator: An operator that takes two operands. For << and >>, the left operand is the stream object (e.g., cin, cout) and the right operand is the object being inserted or extracted.
The return type of an overloaded operator must support cascading. For example, a + b + c works because each + returns a value. Similarly, cout << a << b works if << returns a reference to ostream.
🔑 Definition — Cascading: The ability to chain multiple uses of an operator in a single expression, e.g., cin >> a >> b or cout << x << y.
The iostream.h file already contains declarations for overloaded << and >> for native data types like int, double, float, char, etc. To use these operators with user-defined classes, we must overload them ourselves.
Stream insertion (<<) and extraction (>>) operators cannot be overloaded as member functions. The reason is that for member operators, the driving object is on the left side of the operator. For these operators, the left side is always a stream object (cin or cout), not an object of our class. Therefore, they must be overloaded as non-member functions.
To access private members of the class from these non-member functions, we can either use the class's public setters and getters (if available) or declare the operator function as a friend of the class. We can only declare friends for our own classes, not for library classes like istream or ostream.
The general prototype for the stream insertion operator is:
ostream & operator << (ostream & output, Vehicle v);
Here, cout is replaced by the reference output. The first parameter must be passed by reference; the compiler does not allow passing it by value. This is because objects passed by value are local to the function and destroyed when the function returns, making it meaningless to return a reference to them. The function returns a reference to ostream to support cascading.
💡 Why this matters: Passing by reference avoids copying the stream object, which is often large, and ensures that the return value remains valid for chaining.
When the operator is declared as a friend of the class, it can directly access private members. For example, if tyre is a private int member of Vehicle, inside the operator function we can write:
output << v.tyre; // This is equivalent to cout << v.tyre
Since tyre is a native type, the built-in << for int handles the output. We are building complex operations on top of already available functionality for native types.
🔑 Formula: Overloaded << return type: ostream& — returns a reference to the output stream to allow cascading.
📌 Example (Date class, overloaded <<):
class Date {
friend ostream& operator << (ostream & os, Date d);
// ...
};
ostream & operator << (ostream & os, Date d) {
os << d.day << "." << d.month << "." << d.year; // access private data as friend
return os;
}
For the stream extraction operator (>>), the rules are similar: it cannot be a member function, must be a non-member (usually a friend), returns istream&, and accepts the first parameter as istream&. An additional restriction is that the second parameter must also be passed by reference because the operator modifies that object (it reads values into it).
🔑 Definition — Stream extraction operator (>>): An operator that extracts (reads) data from an input stream (like cin) and stores it in a variable or object.
🔑 Formula: Overloaded >> prototype: istream & operator >> (istream & input, ClassName & obj);
📌 Example (Date class, overloaded >>):
class Date {
// ...
friend istream & operator >> (istream & is, Date & d);
};
istream & operator >> (istream & is, Date& d) {
cout << "\n\n Enter day of the date: ";
cin >> d.day;
cout << " Enter month of the date: ";
cin >> d.month;
cout << " Enter year of the date: ";
cin >> d.year;
return is;
}
Note: For <<, the second parameter can optionally be passed by reference for performance, but it is not mandatory. For >>, it is mandatory.
Example 1 — Date Class with Overloaded Operators
The full Date class example demonstrates both overloaded operators in action.
#include <iostream.h>
class Date {
public:
Date() {
cout << "\n Parameterless constructor called ...";
month = day = year = 0;
}
~Date() { }
friend ostream & operator << (ostream & os, Date d);
friend istream & operator >> (istream & is, Date & d);
private:
int month, day, year;
};
ostream & operator << (ostream & os, Date d) {
os << d.day << "." << d.month << "." << d.year;
return os;
}
istream & operator >> (istream & is, Date& d) {
cout << "\n\n Enter day of the date: ";
cin >> d.day;
cout << " Enter month of the date: ";
cin >> d.month;
cout << " Enter year of the date: ";
cin >> d.year;
return is;
}
main(void) {
Date date1, date2;
cout << "\n\n Enter two dates";
cin >> date1 >> date2;
cout << "\n Entered date1 is: " << date1 << "\n Entered date2 is: " << date2;
}
The output shows cascading input (cin >> date1 >> date2) and cascading output (cout << date1 << date2), supported by the return of references to istream and ostream.
Example 2 — Matrix Class with Overloaded Operators
This example shows a Matrix class first without overloaded operators, requiring specific functions getMatrix() and displayMatrix(). Then, it is modified to use overloaded << and >>, which improves readability and convenience.
Original version (without overloaded operators):
class Matrix {
// ...
public:
Matrix(int rows = 0, int cols = 0);
void getMatrix();
void displayMatrix();
};
// Usage:
Matrix matrix(2, 2);
matrix.getMatrix();
matrix.displayMatrix();
Modified version (with overloaded operators):
class Matrix {
float elements[30][30];
int numRows, numCols;
public:
Matrix(int rows = 0, int cols = 0) {
numRows = rows;
numCols = cols;
}
friend ostream & operator << (ostream &, Matrix &);
friend istream & operator >> (istream &, Matrix &);
};
istream & operator >> (istream & input, Matrix & m) {
for (int i = 0; i < m.numRows; i++) {
for (int j = 0; j < m.numCols; j++) {
input >> m.elements[i][j];
}
}
return input;
}
ostream & operator << (ostream & output, Matrix & m) {
for (int r = 0; r < m.numRows; r++) {
for (int c = 0; c < m.numCols; c++) {
output << m.elements[r][c] << '\t';
}
output << endl;
}
return output;
}
int main() {
Matrix matrix(3, 3);
cout << "\nEnter a 3 * 3 matrix \n\n";
cin >> matrix;
cout << "\nEntered matrix is: \n";
cout << matrix;
// ...
}
The output shows the matrix is entered and displayed using the familiar cin >> matrix and cout << matrix syntax, rather than calling specialized member functions. This makes the code more intuitive and readable.
💡 Why this matters: Overloading << and >> allows user-defined classes to behave like built-in types, making the interface consistent and reducing the learning curve for other programmers.
Tips
The lecture concludes with essential tips for implementing these operators:
- Stream insertion (
<<) and extraction (>>) operators are always implemented as non-member functions. operator <<returnsostream &andoperator >>returnsistream &to support cascaded operations.- The first parameter to
operator <<is anostream &object (e.g.,cout). Similarly, the first parameter tooperator >>is anistream &object (e.g.,cin). These first parameters are always passed by reference; the compiler won't allow otherwise. - For
operator >>, the second parameter must also be passed by reference. - The second parameter to
operator <<is an object of the class for which we are overloading the operator. The same applies tooperator >>.
⭐ Key Takeaways
Stream insertion (<<) and extraction (>>) operators must be overloaded as non-member functions (typically declared as friends of the class) because the left operand is always a stream object (cin/cout), not an object of the user-defined class. Both operators must return a reference to the appropriate stream type (ostream& for <<, istream& for >>) to support cascading. The first parameter for both operators must be passed by reference to the stream object. For the extraction operator (>>), the second parameter (the object being read into) must also be passed by reference because it is modified; for the insertion operator (<<), passing the second parameter by reference is optional but improves performance. Using these overloaded operators makes user-defined classes behave like native types, improving code readability and programmer convenience.
🧠 Quick Revision Questions
- Why can't stream insertion (
<<) and extraction (>>) operators be overloaded as member functions of a class? - What return type must an overloaded
<<operator have, and why? - In the prototype
istream & operator >> (istream & input, Date & d), why mustdbe passed by reference? - What is the purpose of declaring the overloaded operator function as a
friendof the class? - In the
Matrixexample, why is passing the second parameter (Matrix & m) by reference in the<<operator considered "optional but efficient"?
📘 Lecture 38 — Lecture No. 38
📖 Overview: This lecture covers two important programming concepts: user-defined manipulators and the static keyword. User-defined manipulators allow programmers to create custom formatting tools for output streams, while the static keyword provides a mechanism for maintaining state across function calls and within classes. These concepts are essential for writing more flexible and efficient C++ programs.
🗂️ Topics Covered
The lecture begins with user-defined manipulators, explaining how to create custom manipulators like bell, tab, and matrix formatting tools. It then explores the static keyword thoroughly, covering static variables within functions, static objects, and static data members of a class. The lecture provides detailed examples for each concept, including a complete matrix display program and programs demonstrating the lifetime and behavior of static versus automatic variables.
📝 Lecture Summary
User Defined Manipulators
We have talked a lot about the manipulators that are provided with the streams in the C++. These are similar to setw function, used to set the width of the output. To write our own manipulator, we need to understand parameter-less manipulators like endl, which inserts a new line and flushes the buffer. In case of operator overloading, it is pre-requisite to know where the operator will be used. With manipulators, a stream object (normally cout, an ostream object) appears on the left-hand side. The cout takes the manipulator to carry out some manipulation. These are written in cascading style as cout << manipulator << "some data" << endl.
The left-hand side will be an ostream object that will call the manipulator. Normally, on the right-hand side of the manipulator, we have another stream insertion operator <<. For a parameter-less manipulator, no argument is passed. The manipulator should return a reference to an ostream object, so cascading works. Since the ostream class is built-in and cannot be modified, the manipulator is not a member function — it is a standalone function. The declaration of this manipulator is:
ostream& manipulator_name (ostream& os)
The argument os is the same object which is calling this function. Inside the definition, the manipulator should only do something regarding output and return the ostream reference.
🔑 Definition — Parameter-less manipulator: A manipulator that takes no arguments and operates on an output stream, returning a reference to the stream object to allow cascading.
📐 Formula: ostream& manipulator_name (ostream& os) { return os << 'character'; } → The function receives the output stream, performs an output operation, and returns the stream reference.
📌 Example: To create a bell manipulator:
ostream & bell ( ostream & output ) {
return output << '\a';
}
Usage: cout << "Virtual " << tab << "University" << bell << endLine;
Examples of user defined manipulator
The lecture presents a complete program demonstrating user-defined manipulators for formatting a matrix display. Various manipulators are defined to control spacing, lines, stars, and sounds. The manipulators include spaceFirst (sets width to 33), spaceBetween (sets width to 4), line (inserts "|"), newLine (inserts endl), star (inserts "*"), and sound (inserts beep). These manipulators are used as friend functions of the Matrix class to format the output of a 3x3 matrix with borders and proper spacing.
The output of the matrix program displays:
******************************Displaying The Matrix*****************************
| 3 5 1 |
| 8 7 6 |
| 2 5 2 |
💡 Why this matters: User-defined manipulators allow programmers to create reusable, readable formatting tools that work naturally with C++ stream syntax, making output code cleaner and more maintainable.
Static keyword
The word static refers to something that is stationary, stopped, and not moveable. Static variables exist for a certain amount of time, much longer than ordinary automatic variables. There are different types of variables based on lifetime. Global variables are defined outside of main, exist for the entire program execution, and are accessible from everywhere — but this can cause problems as they are visible in functions that don't need them. Automatic variables (local variables inside functions) are created when a function is called and destroyed when it returns.
Static variables provide a middle ground: they maintain state across function calls but are not visible outside the function. Using the static keyword inside a function creates a variable that is initialized only once during the program's lifetime and retains its value between function calls. The declaration is: static int i;. Unlike ordinary variables, static variables must be initialized at the point of declaration (e.g., static int i = 0;), not through a separate assignment. Static variables are stored in a static memory area, separate from the stack (for automatic variables) and the heap/free store (for dynamic memory).
🔑 Definition — Static variable: A variable declared with the static keyword inside a function that is created and initialized only once during the lifetime of the program, maintaining its value between function calls.
📐 Formula: static data_type variable_name = initial_value; → Creates a static variable that persists for the program's lifetime.
📌 Example: Compare static and automatic variables:
void staticVarFun() {
static int i = 0; // initialized only once
i++;
cout << "The value of i is:" << i << endl;
}
void nonstaticVarFun() {
int i = 0; // initialized every call
i++;
cout << "The value of i is:" << i << endl;
}
When called 10 times, staticVarFun() outputs 1,2,3...10 while nonstaticVarFun() outputs 1,1,1...1.
Static Objects
User-defined data types (classes and objects) can also be declared as static. When we create a static object inside a function, it must be initialized. Since initialization is done in constructors, it is necessary to provide a constructor with default arguments so the object can be properly initialized when created as static. The behavior of a static object is the same as a static variable of a native data type — it maintains its state and exists even outside the function.
The lecture demonstrates the lifetime of static objects using a truck class with constructors and destructors that print messages. The program creates: a global object A('A'), an ordinary object B('B') in main, an automatic object C('C') in function f(), and a static object D('D') in function g(). The output shows:
inside the constructor of A
inside the constructor of B
inside the constructor of C
Inside the destructor of C
inside the constructor of D
Inside the destructor of B
Inside the destructor of D
Inside the destructor of A
Notice that the static object D is NOT destroyed when function g() finishes — it persists until program termination. The destruction order is: local variables of main first (B), then static objects (D), then global objects (A). This shows that static objects remain for a longer period of time.
🔑 Definition — Static object: An object declared with the static keyword inside a function that is created and initialized only once and maintains its state across function calls.
📌 Example: static truck D('D'); — This object persists beyond the function scope and is destroyed only when the program ends.
Static data member of a class
The static data member concept extends static variables to the class level. A static data member is a single copy that belongs to the class itself, not to any individual object. It is created once and initialized once for the entire class, stored in the static memory area. Its lifetime is the lifetime of the program. Static data members are initialized at file scope (outside of main and any functions) using the scope resolution operator: ClassName::static_member = value;
Static data members can be public or private and can be accessed by objects of the class, but it is recommended to access them with the class name, not an object name, to avoid confusion. Changing a static data member through one object changes it for all objects of that class.
🔑 Definition — Static data member: A data member of a class declared with the static keyword that has only one copy shared by all objects of the class, initialized at file scope.
📐 Formula: class ClassName { static int member; }; int ClassName::member = value; → Creates a class-level variable shared by all instances.
📌 Example: For a savingsAccount class:
class savingsAccount {
static double profit_rate; // same for all accounts
// other members...
};
double savingsAccount::profit_rate = 3.0; // initialization at file scope
This profit rate applies to all PLS accounts. If one object changes it via account1.profit_rate = 4.0;, it changes for ALL accounts — which is why accessing it with class name (savingsAccount::profit_rate) is preferred.
Another useful example: counting the number of student objects:
class student {
static int how_many;
public:
student() { how_many++; }
~student() { how_many--; }
};
int student::how_many = 0; // initialize to zero
This automatically tracks the number of student objects as they are created and destroyed.
💡 Why this matters: Static data members provide an elegant way to share state across all objects of a class without using global variables, and can be used for tracking object counts, shared configuration settings, or class-wide constants.
⭐ Key Takeaways
- User-defined manipulators must return a reference to an ostream object and take an ostream reference as a parameter, allowing them to be used in cascading stream expressions like built-in manipulators.
- Static variables inside functions maintain their value between function calls, being initialized only once during the program's lifetime, and are destroyed only when the program ends — unlike automatic variables that are created and destroyed with each function call.
- Static objects follow the same lifetime rules as static variables but require constructors with default arguments for proper initialization; they are not destroyed when the function returns but persist until program termination.
- Static data members are class-level variables shared by all objects, initialized at file scope using the scope resolution operator, and changing them through any object affects all objects of that class.
- Static provides an alternative to global variables by maintaining state while limiting visibility and side effects, making programs more modular and less prone to errors from unintended access.
🧠 Quick Revision Questions
- What is the correct signature and return type for a user-defined parameter-less manipulator?
- Why does a static variable inside a function retain its value between function calls, and what happens if you omit the
statickeyword? - In the truck class example, why was the static object
Dnot destroyed when functiong()finished, and when was it finally destroyed? - How do you initialize a static data member of a class, and why must this be done at file scope rather than in a constructor?
- What is the danger of accessing a static data member through an object name (e.g.,
account1.profit_rate) instead of the class name (e.g.,savingsAccount::profit_rate)?
📘 Lecture 40 — Objects as Class Members
📖 Overview: This lecture explores how classes can contain objects of other classes as data members, focusing on construction and destruction ordering. It demonstrates member initializer lists for efficient initialization and introduces nested classes, explaining how inner classes can be used within outer classes for better code organization and reusability.
🗂️ Topics Covered
The lecture covers objects as class members with construction and destruction sequences, member initializer lists for parameterized constructor calls, examples using Date, Column/Row/Matrix, and VehicleParts structures, advantages of code reuse, structures as class members, nested classes with public/private visibility, friend declarations between nested classes, and practical tips for implementing these concepts.
📝 Lecture Summary
Objects as Class Members
A class is a user-defined data type that can be used inside other classes just like native data types. We can create classes that contain objects of other classes as data members. When one class contains objects of other classes, it is mandatory to understand how and in what sequence the contained and containing objects are constructed. The contained data members of the object (regardless whether they are native or user-defined data types) are constructed before the object itself. The order of destruction of an object is reverse to this construction order, where the containing object is destroyed first before the contained objects.
🔑 Definition — Initializer List: A colon placed after the parameter list of the containing class's constructor, followed by the name of the member and a list of arguments, used to initialize contained objects at construction time.
📐 Construction Order Rule: Inner data members are constructed first → Containing object is constructed last. Destruction is the reverse: Outermost object destroyed first → Inner members destroyed last.
📌 Example: Class A contained in Class B:
class A { /* constructor/destructor */ };
class B {
public:
B() { cout << "B Constructor"; }
~B() { cout << "B Destructor"; }
private:
A a; // contained object
};
// Output: A Constructor → B Constructor → B Destructor → A Destructor
Example 1
A PersonInfo class stores name, address, and birthday of a person, containing an instance of a Date class to store birthday. No arguments are specified in the declaration of birthday, but a member initializer can be used to call a parameterized constructor. The colon is placed after the parameter list of the containing class's constructor, followed by the member name and arguments. Multiple contained objects can be initialized using comma-separated initializers. The order of execution of initializers is the same as the order of declarations of objects inside the outer class, not the order in the initializer list.
💡 Why this matters: This ensures that objects are initialized in a predictable order, preventing dependency issues.
🔑 Definition — Member Initializer Syntax: ClassName::ClassName(params) : memberName(params_for_member) { body }
📌 Example: PersonInfo with birthday and drvLicenseDate:
PersonInfo::PersonInfo(char* nm, char* addr, int month, int day, int year,
int licMonth, int licDay, int licYear)
: drvLicenseDate(licMonth, licDay, licYear), birthday(month, day, year)
{
cout << "\n PersonInfo -- Constructor called ...";
}
// Output: birthday constructor first (declared first), then drvLicenseDate constructor,
// then PersonInfo constructor. Destruction reverses.
Example 2
This example works with the size of a matrix. A Column class stores column size, a Row class contains a Column instance to store number of columns, and a Matrix class contains a Row instance. The construction sequence is: Column object constructed first, then Row object, finally Matrix object. At destruction, the order reverses: Matrix destroyed first, then Row, then Column.
🔑 Definition — Nested Object Construction: The innermost contained object is constructed first, then each containing level, with the outermost object constructed last.
📐 Example: Matrix(3, 4) — Output sequence: Column created → Row created → Matrix created → Matrix destroyed → Row destroyed → Column destroyed
📌 Public member access: Public data members of a contained object can be accessed from outside using the dot operator:
Matrix matrix(4, 5);
Matrix.row.size = 8; // only if row is public and has public size member
Advantages of Objects as Class Members
This is a way of reusing code when we contain objects of already written classes into a new class. The Date class can be used as a data member of Student, Employee, or PersonInfo class. Previously written classes don't need to be tested again — they are added to a components library for later use. This approach gives clarity and better management to source code by breaking problems into smaller components that can be managed independently. When an object is declared as a constant data member inside a class, it is initialized using the initializer list, requiring the contained class to have a parameterized constructor.
Structures as Class Members
Structures and classes are very similar in C++ except the default scope of members. The default scope for members of structures is public, whereas the default visibility for class members is private. All the discussion for class objects as class members applies to structure objects as class members.
📌 Example: Vehicle class with VehicleParts structure:
struct VehicleParts {
int wheels;
int seats;
VehicleParts(int w, int s) : wheels(w), seats(s) { }
};
class Vehicle {
private:
VehicleParts vehicleParts;
public:
Vehicle(int a, int b) : vehicleParts(a, b) { }
};
// Output: VehicleParts - parameterized constructor → Vehicle - parameterized constructor
Classes inside Classes
Classes defined within other classes are called nested classes. A nested class is written exactly like a normal class with data members, member functions, constructors, and destructors, but no memory is allocated unless an instance is created. C++ allows multiple levels of nesting. If a class is nested inside the public section of a class, it is visible outside the outer class. If nested in the private section, it is only visible to the members of the outer class.
🔑 Definition — Nested Class: A class defined within another class, used to keep associated classes together for easier manipulation of objects.
📌 Visibility rules: The outer class has no special privileges regarding the inner class — the inner class has full control over the accessibility of its members. The friend operator can be used to declare the enclosed class as a friend of the inner class to provide access to private members.
📐 Member function definition: Nested class member functions can be defined outside the surrounding class using scope resolution:
Surround::FirstWithin::FirstWithin() { variable = 0; }
📌 Friend declarations example: To allow all three classes (Surround, FirstWithin, SecondWithin) to access private members of each other:
class Surround {
friend class FirstWithin;
friend class SecondWithin;
// ...
class FirstWithin {
friend class Surround;
friend class SecondWithin;
// private members
};
class SecondWithin {
friend class Surround;
friend class FirstWithin;
// private members
};
};
Structures can also be defined inside classes in the same manner, with the exception that default scope of members in structures is public unless explicitly declared otherwise.
⭐ Key Takeaways
A class can contain instances of other classes as its data members, which is a powerful technique for code reuse. The construction order is always inner objects first, then the containing object, while destruction reverses this order. Member initializer lists provide an efficient way to initialize contained objects with parameterized constructors, and the order of initialization follows the declaration order in the class, not the order in the initializer list. Nested classes allow organizing related classes together, with visibility controlled by whether the inner class is declared in the public or private section of the outer class.
🧠 Quick Revision Questions
- What is the construction and destruction order when Class B contains an instance of Class A as a data member?
- How does a member initializer list ensure contained objects are properly initialized with non-default constructors?
- In Example 1 with PersonInfo, why did the birthday constructor execute before drvLicenseDate's constructor even though drvLicenseDate appeared first in the initializer list?
- What are the visibility rules for a nested class declared in the public section versus the private section of the outer class?
- How can the friend operator be used to allow nested classes to access private members of each other?
📘 Lecture 41 — Template Functions
📖 Overview: This lecture introduces the concept of templates in C++ as a powerful code reuse mechanism. It covers function templates, their overloading, explicit type specification, and the integration of template functions with user-defined classes through operator overloading. This approach allows writing generic code once and letting the compiler generate type-specific versions automatically.
🗂️ Topics Covered
The lecture covers template functions as a new code reuse method, including their definition using the template keyword and generic data types. It explores overloading template functions with different argument signatures and explicitly specifying types in angle brackets. The discussion extends to using template functions with class objects, emphasizing the need for operator support within the class, and concludes with a recap combining templates with operator overloading.
📝 Lecture Summary
Template Functions
Templates in C++ provide a way to write generic code that works with any data type. Unlike function overloading where multiple versions of a function are written manually, a function template allows writing the code once, and the compiler automatically generates type-specific versions at compile time. The template is defined using the template<class T> syntax, where T is a generic data type placeholder. At least one function argument must be of the generic type. When the function is called, the compiler detects the argument type, substitutes T with that type, and generates the appropriate function code. For example, a swap function template can work for int, double, char, etc., without rewriting the function for each type.
🔑 Definition — Template Function: A function defined with a generic data type that allows the compiler to generate type-specific versions automatically based on how it is called.
📐 Formula: template<class T> return_type function_name(T arg1, ...) { body } → The compiler replaces T with the actual type when generating the function.
📌 Example: The swap function template takes two references of generic type T and swaps their values using a temporary variable T tmp. Calling swap(a, b) with int a, b generates an int version; with char a, b, a char version is generated.
Overloading Template Functions
Overloaded template functions follow the same rules as ordinary function overloading — functions with the same name but different number or type of arguments. The compiler selects the correct version based on the arguments provided. This allows defining multiple template functions with the same name that perform different operations, as long as their signatures differ. For instance, one inverse template function might swap two values (void inverse(T &x, T &y)), while another returns the negative of a single value (T inverse(T x)). The compiler distinguishes between them by the argument count.
📌 Example: The program defines inverse(i) for a single argument returning its negative, and inverse(i, j) for swapping two values. The compiler generates appropriate versions for each call.
Explicitly Specifying the Type in a Template Function
When calling a template function, the type can be explicitly specified using angle brackets between the function name and the argument list, like function_name<type>(arguments). This is useful when we want to force the compiler to generate a version with a different return type or argument type than what would be inferred automatically. For functions with multiple generic types, types are specified left to right, similar to default arguments — the second type cannot be forced without specifying the first. This technique allows, for example, passing a double argument but generating an int version of the function that returns an int.
🔑 Definition — Explicit Type Specification: Forcing the compiler to generate a specific version of a template function by writing the desired type(s) in angle brackets before the argument list.
📐 Formula: function_name<type1, type2>(arguments) → Forces T to become type1 and U to become type2 for template<class T, class U>.
📌 Example: With template<class T, class U> T reverse(U x), calling reverse<int, double>(amount) forces T = int and U = double, generating int reverse(double x).
Template Functions and Objects
Template functions can also work with user-defined class objects, provided the class supports all operations used inside the template function. When the compiler generates a template function for a class type, it replaces the generic type with the class type. If the template function uses operators like - (unary minus), the class must have that operator overloaded. For example, a reverse template function using -x can work with a PhoneCall class only if that class defines the unary minus operator to reverse the phone call (e.g., changing billCode to 'c' for cancelled). This demonstrates combining templates with operator overloading for powerful, reusable code.
💡 Why this matters: The template function is written once independently, and the class independently defines its operator. The main program connects them, allowing the same generic code to work seamlessly with different classes.
🔑 Definition — Template with Objects: Using a template function with a class object requires that all operations within the template (like -, +, comparison) are defined in the class through operator overloading.
📌 Example: The PhoneCall class defines unary minus operator to set billCode = 'c'. The reverse template function calls -x, which invokes this overloaded operator. Calling reverse(aCall) generates a PhoneCall version, changes the bill code to cancelled, and returns the modified object.
⭐ Key Takeaways
The lecture establishes templates as a fundamental code reuse mechanism in C++. A function template uses template<class T> to define a function that works with any data type, and the compiler automatically generates type-specific versions. Template functions can be overloaded like regular functions, and types can be explicitly specified using angle brackets. When using templates with class objects, all operations in the template must be supported by the class through operator overloading. This combination of templates and operator overloading is extremely powerful for writing once that works across many types.
🧠 Quick Revision Questions
- What is the syntax for defining a template function with one generic type, and what is the minimum requirement regarding arguments?
- How does the compiler handle a call to a template function with an
intargument versus adoubleargument? - Can a template function be overloaded? Under what conditions can two template functions have the same name?
- Write the explicit type specification syntax to force a template function with signature
template<class T, class U> T func(U x)to return anintwhen passed adoubleargument. - If a template function uses the
-operator, what must a class define before a template function can be used with objects of that class?
📘 Lecture 42 — Templates and Standard Template Library
📖 Overview: This lecture introduces class templates, a powerful C++ feature for creating generic classes that work with any data type. It explains how to define template classes, use non-type parameters, handle static members and friend functions within templates, and discusses the advantages, disadvantages, and the Standard Template Library (STL).
🗂️ Topics Covered
The lecture covers class templates including their syntax and creation, class templates with non-type parameters, the behavior of static members in template classes, friend functions in the context of templates, a detailed stack example, a sample program demonstrating template class usage, advantages and disadvantages of using templates, and an introduction to the Standard Template Library (STL).
📝 Lecture Summary
68) Class Templates
While template functions are used for generic data types in functions, class templates allow us to define a complete interface and implementation for a user-defined data type in a generic fashion. The lecture uses a stack data structure as an example, explaining its "Last-In, First-Out" (LIFO) property. A stack class can be made generic so it can hold integers, floats, doubles, or any other data type. When we instantiate a template class with a specific data type, the compiler automatically generates a new version of the class with that data type.
The syntax for a template class is similar to a template function: template <class T> followed by the class definition, where T is the placeholder for the data type. When defining member functions outside the class, we write: template <class T> class-name <T>::function-name (argument list) { // function body }. To create an object of a template class, we specify the data type in angle brackets, for example, Number <int> x; to create a Number object that holds an integer.
🔑 Definition — Class Template: A blueprint for a family of classes that work with a generic data type T, allowing the creation of type-safe, reusable classes for any data type without code duplication.
🔑 Definition — Stack: A data structure that follows the Last-In, First-Out (LIFO) principle, where elements are added (pushed) and removed (popped) only from the top.
💡 Why this matters: Class templates are a cornerstone of generic programming in C++, enabling the creation of type-safe, reusable containers and algorithms without sacrificing performance.
69) Class Templates and Nontype Parameters
Templates can also accept non-type parameters, which are not generic data types but constant values (like integers). The syntax is template <class T, int element>, where element is a constant that can be used within the class definition, for example, to define the size of an array. When instantiating the class, we provide a value for this parameter, like Stack <int, 100> myStack;. This enhances the flexibility of templates by allowing compile-time constant values to be passed.
🔑 Definition — Non-type Parameter: A parameter in a template declaration that is a constant value (such as an int) rather than a data type, used to specify sizes, dimensions, or other constant properties at compile time.
70) Templates and Static Members
When a static member variable is part of a template class, its behavior changes. For an ordinary class, a static variable has a single copy shared by all objects. However, for a template class, a separate copy of the static variable exists for each distinct type with which the class is instantiated. For example, if the Number class has a static variable, objects of Number<int> will share one static copy, while objects of Number<double> will share a completely different static copy. This is because the compiler generates a unique class for each data type.
🔑 Definition — Static Member in Templates: A static data member in a template class is instantiated once for each distinct type used to instantiate the class, meaning Number<int> and Number<double> each get their own static member.
71) Templates and Friend Functions
Friend functions are often needed to overload operators when the left-hand side operand is not an object of the class (e.g., 2 + a), as member functions cannot handle this. In the context of template classes, a friend function declared without <T> (e.g., friend f();) becomes a friend of all classes generated from the template. If we write <T> in the friend function's declaration (e.g., friend f <T>();), it becomes a friend only of classes instantiated with that specific data type T. Similarly, a friend class Y; gives access to all member functions of class Y to all classes generated from the template, while friend A::f(); grants access only to a specific member function of class A.
💡 Why this matters: Friend functions in templates provide controlled access to private data, enabling operator overloading and other external operations while maintaining type safety.
72) Example
The lecture presents a generic Stack class example. The class includes:
- A private integer variable
size - An array
T array[]of the generic typeT - A constructor
Stack() void push(T)to add an elementT pop()to remove an elementbool isEmpty()andbool isFull()for checking stack status
When used in code, Stack<int> x; creates a stack of integers, while Stack<double> y; creates a stack of doubles. The key advantage is that the Stack class is written once and can be reused for any data type.
73) Sample Program
The lecture provides a sample program demonstrating a template class Generic<T> with a constructor and a print() function. It also defines a custom Employee class with a friend function to overload the << operator for output. In main(), objects of Generic are created with int, double, and Employee types, and the output shows that the same template class works correctly for all types:
Generic printing:
7
Generic printing:
6.65
Generic printing:
Employee number 333 Salary 4.9
74) Advantages and Disadvantages of Templates
Advantages:
- Easier to write than multiple versions of similar code for different types
- Easier to understand by abstracting type information
- Type-safe, as types are known at compile time
- Can utilize compiler optimizations to the extreme
Disadvantages:
- Can make code difficult to read and follow
- Can present confusing syntactical problems, especially in large codebases
- Can produce nearly meaningless compiler errors, requiring extra care
75) Standard Template Library (STL)
The Standard Template Library (STL) is a part of the official C++ standard. It is a pre-developed, tested, and compiled library of common use functions and data structures (like arrays, lists, etc.) that use templates. By using the STL, programmers can write smaller, concise, and error-free code, as they are using a tested and tried code base. This promotes code reusability and abstraction.
🔑 Definition — Standard Template Library (STL): A standardized library in C++ that provides a collection of template-based algorithms, containers, and iterators for common programming tasks, promoting code reuse and efficiency.
⭐ Key Takeaways
- Class templates allow you to create a single generic class definition that works with any data type. When instantiated with a specific type (e.g.,
Stack<int>), the compiler generates a type-specific class. This is the core of generic programming in C++. - Non-type parameters enable passing constant values (like array sizes) as part of the template parameter list. Static members in template classes behave uniquely: one copy exists per instantiated data type
T, not for the entire template class. - Friend functions in templates can be declared to be friends of all generated classes (without
<T>) or specific to one type (with<T>). - Templates offer significant advantages like code reuse, type-safety, and performance optimization, but can lead to complex code and confusing compiler errors if misused.
- The Standard Template Library (STL) is a powerful, pre-built, and tested collection of template-based containers and algorithms, enabling the writing of concise, robust, and efficient C++ code.
🧠 Quick Revision Questions
- What is the syntax for declaring a class template named
MyContainerwith a generic typeT? - How does the behavior of a static member variable differ when it belongs to an ordinary class versus a template class?
- Explain the difference between declaring a friend function in a template class as
friend void f();versusfriend void f<T>();. - Write the C++ code to instantiate two objects: one stack of integers and one stack of doubles, assuming a template class
Stackis defined. - What is the primary advantage of using the Standard Template Library (STL) in your C++ programs?
📘 Lecture 43 — Programming Exercise - Matrices
📖 Overview: This lecture focuses on applying object-oriented programming concepts to implement Matrix operations in C++. It walks through the complete design recipe—from problem analysis to class interface design—demonstrating how to create a robust Matrix class with overloaded operators for mathematical operations.
🗂️ Topics Covered
The lecture covers the design and implementation of a Matrix class for performing mathematical operations. It begins with problem analysis, defining matrices and their operations including addition, subtraction, multiplication, division, and transpose. The design recipe is applied to determine data structures, memory allocation, and class interface. Operator overloading is detailed for both matrix-matrix and matrix-scalar operations, distinguishing between member and friend functions. The lecture concludes with the complete class declaration, addressing dynamic memory management, copy constructors, and the need for deep copying.
📝 Lecture Summary
Programming Exercise - Matrices
Mathematics provides an excellent domain for developing classes and programs. This lecture tackles the problem of manipulating and performing different operations on Matrices, which are widely used in real-world applications. The approach follows the design recipe: problem analysis, design, and implementation.
Design Recipe
The design process begins with analysis to create a problem statement. After describing the problem, the next step is to formulate it with examples, paying close attention to details. Data structures are analyzed and selected to best fit the program requirements. Code is then written to implement the program. After implementation, testing verifies correct behavior in all scenarios, and any bugs are fixed. This cycle of testing and bug fixing continues until the program works perfectly.
Problem Analysis
A matrix is a two-dimensional array of numbers, represented in rows and columns. For example, matrix A with 3 rows and 4 columns has an order of 3 * 4:
1 2 3 4
5 6 7 8
9 10 11 12
The operations normally performed on matrices include:
- Addition of two matrices
- Addition of a scalar value to a matrix
- Subtraction of one matrix from another
- Subtraction of a scalar from a matrix
- Multiplication of two matrices
- Multiplication of a matrix by a scalar
- Division of a matrix by a scalar
- Transpose of a matrix
Addition of two matrices of the same order is found by adding the corresponding elements: Aij + Bij, where i varies from 1 to m (max rows) and j varies from 1 to n (max columns). The restriction is that the matrices must have the same number of rows and columns (same order). Scalar addition adds the same number to all elements of the matrix.
Subtraction works similarly: two matrices of the same order participate, and the resultant matrix is obtained by subtracting each element of one matrix from the corresponding element of the other: Cij = Aij - Bij.
🔑 Definition — Matrix Subtraction: Cij = Aij - Bij, where matrices A and B must have the same order m × n.
Division of a matrix by a scalar divides each element of the matrix by the scalar: Cij = Aij / x. Each element of matrix A is divided by the number x to produce the corresponding number in the resultant matrix C.
Multiplication is more complicated. For scalar multiplication: Cij = x * Aij, where each element of matrix A is multiplied by the scalar x.
For matrix-matrix multiplication, there is a restriction: the number of columns of the first matrix must equal the number of rows of the second matrix. The process involves multiplying the first row of the first matrix with the first column of the second matrix. The first element of the row is multiplied with the first element of the column, the second element with the second element, and so on. The results of all these multiplications are added to produce one number, which is placed at the corresponding position in the resultant matrix.
📐 Formula: Matrix Multiplication — If matrix A has order m × n and matrix B has order n × p, then the resultant matrix C has order m × p. The element Cij is calculated as: Cij = Σ(Aik × Bkj) for k = 1 to n.
📌 Example:
[1 2] * [2 4] = [(1)(2)+(2)(1) (1)(4)+(2)(2)]
[5 6] [1 2] [(5)(2)+(6)(1) (5)(4)+(6)(2)]
Result: [4 8; 16 32]
Transpose of a matrix is obtained by interchanging its rows and columns. The first row of the original matrix becomes the first column of the new matrix, the second row becomes the second column, and so on. For a square matrix, there is no change in order. For a non-square matrix, the number of rows of the original becomes the number of columns of the transposed, and vice versa.
📌 Example of transpose:
Original: Transposed:
1 2 3 1 5 9
5 6 7 → 2 6 10
9 10 11 3 7 11
Design Issues and Class Interface
The size of the matrix is specified at creation time, and memory is allocated dynamically. The Matrix class constructor accepts integer parameters for rows and columns:
Matrix(int rows, int cols);
A display function is declared to show elements on the screen:
void display(Matrix &);
Operator overloading is used to perform matrix operations. For addition of two matrices, the + operator is overloaded as a member function:
Matrix operator+(Matrix &) const;
For addition of a matrix and a scalar (A + x):
Matrix operator+(double) const;
For scalar + matrix (x + A), a friend function is needed because the scalar is on the left:
friend Matrix operator+(double, Matrix &);
Similarly, for subtraction:
- Matrix - Matrix:
Matrix operator-(Matrix &) const; - Matrix - Scalar:
Matrix operator-(double) const; - Scalar - Matrix:
friend Matrix operator-(double, Matrix &);
For multiplication:
- Matrix * Matrix:
Matrix operator*(const Matrix &); - Matrix * Scalar:
Matrix operator*(double) const; - Scalar * Matrix:
friend Matrix operator*(const double, const Matrix &);
For division (matrix / scalar):
Matrix operator/(const double);
For transpose:
const Matrix & transpose(void);
Composite operators += and -= are overloaded as member operators.
Stream insertion and extraction operators are overloaded as friend functions:
friend ostream & operator<<(ostream &, Matrix &);
friend istream & operator>>(istream &, Matrix &);
The complete Matrix class declaration includes:
class Matrix {
private:
int numRows, numCols;
double **elements;
public:
Matrix(int=0, int=0); // default constructor
Matrix(const Matrix &); // copy constructor
~Matrix(); // destructor
int getRows(void) const;
int getCols(void) const;
const Matrix & input(istream &is = cin);
const Matrix & input(ifstream &is);
void output(ofstream &os) const;
void output(ostream &os = cout) const;
const Matrix & transpose(void);
const Matrix & operator=(const Matrix &m);
Matrix operator+(Matrix &m) const;
Matrix operator+(double d) const;
const Matrix & operator+=(Matrix &m);
friend Matrix operator+(double d, Matrix &m);
Matrix operator-(Matrix &m) const;
Matrix operator-(double d) const;
const Matrix & operator-=(Matrix &m);
friend Matrix operator-(double d, Matrix &m);
Matrix operator*(const Matrix &m);
Matrix operator*(double d) const;
friend Matrix operator*(const double d, const Matrix &m);
Matrix operator/(const double d);
friend ostream & operator<<(ostream &, Matrix &);
friend istream & operator>>(istream &, Matrix &);
friend ofstream & operator<<(ofstream &, Matrix &);
friend ifstream & operator>>(ifstream &, Matrix &);
void display();
};
💡 Why this matters: Matrix and Matrix objects are passed and returned by reference to avoid overhead from passing by value, which would allocate and de-allocate memory on the stack, affecting performance.
Since dynamic memory allocation is done in the constructor, destructor must free the memory explicitly. The default assignment operator makes a shallow copy, so a custom assignment operator (operator=) must be written for deep copy. Similarly, a copy constructor is needed for deep copying when initializing a new object from an existing one.
The Matrix class can be extended to a template class to handle different data types (int, float, double) without writing separate code for each type. The scalar number can also be templatized.
Checks must be performed inside member function implementations:
- Division operator checks that the divisor is non-zero
- Addition and subtraction check that matrices have equal rows and columns
- Multiplication checks that the number of columns of the first matrix equals the number of rows of the second
An alternative design approach would be to declare a Row class first and then contain multiple Row objects inside the Matrix class, but for simplicity, one Matrix class is used here.
⭐ Key Takeaways
The most critical concepts from this lecture are: (1) Matrices can be represented as 2D arrays with dynamic memory allocation, and operations require specific order restrictions (same order for addition/subtraction, columns of first must equal rows of second for multiplication). (2) Operator overloading is essential for intuitive mathematical notation, using member functions when the left operand is a Matrix and friend functions when the left operand is a scalar. (3) When a class manages dynamic memory, the Rule of Three applies: you must define the destructor, copy constructor, and assignment operator to ensure proper deep copying and prevent memory leaks. (4) Matrices and objects should be passed by reference to avoid performance overhead from copying large data structures. (5) The design recipe—problem analysis, data structure selection, implementation, and testing—provides a systematic approach to software development.
🧠 Quick Revision Questions
- What restriction exists for adding two matrices, and what happens to the order of the resultant matrix?
- Why is a friend function needed for the operation
x + A(scalar + matrix) instead of a member function? - What are the three special member functions that must be written when dynamic memory allocation is used in a class, and why?
- What is the rule for matrix multiplication regarding the orders of the two matrices, and how is the resultant matrix's order determined?
- How does the transpose operation change the order of a matrix that is not square?
📘 Lecture 44 — Matrix Class
📖 Overview: This lecture provides a comprehensive code review of a Matrix class implementation in C++, with special emphasis on constructors, destructors, dynamic memory allocation, and operator overloading. It matters because matrices are fundamental to scientific computing, graphics, and data processing, and this implementation demonstrates proper memory management and encapsulation techniques essential for professional programming.
🗂️ Topics Covered
This lecture covers the complete implementation of a Matrix class including its data structure with private members (numRows, numCols, double **elements), the default and copy constructors with dynamic memory allocation, destructor for memory deallocation, utility functions (getRows, getCols, input, output, transpose), and extensive operator overloading for arithmetic operations (+, -, *, /, +=, -=, =) and stream operators (>>, <<). The lecture also discusses file I/O functions for reading and writing matrices.
📝 Lecture Summary
Matrix Class
The Matrix class data structure is simple but powerful. The private section contains int numRows, numCols; and double **elements; - elements is an array of pointers to double, enabling a two-dimensional dynamic array. The class uses dynamic memory allocation so matrices can be variable-sized (e.g., 2×2, 20×20, or 3×10). This necessitates three things: a constructor that allocates memory using new, a destructor to deallocate memory, and an assignment operator to prevent shallow copying (where only pointer values are copied, not the actual data).
The public interface begins with constructors: Matrix(int=0, int=0); for default construction (zero rows/cols means no memory allocated yet) and Matrix(const Matrix &); for copy constructor. The destructor ~Matrix(); takes no arguments and returns nothing. Utility functions include getRows(), getCols() (both const), input() functions (one for istream/keyboard, one for ifstream/file), output() functions (one for ostream/screen with graphics, one for ofstream/file), and transpose().
💡 Why this matters: The separation of member and friend operators handles cases like matrix + double versus double + matrix - the latter cannot be a member function because the left operand isn't a Matrix object.
The arithmetic operators include member operators for +, -, *, / with various combinations (Matrix+Matrix, Matrix+double), and friend functions for double+Matrix, double-Matrix, double*Matrix. The +=, -= operators are also defined, with += implemented through code reuse: *this = *this + m;.
Definition of Matrix Constructor
The default constructor Matrix::Matrix(int row, int col) assigns row to numRows and col to numCols. It then allocates memory: elements = new (double *) [numRows]; creates an array of row pointers. In a loop, elements[i] = new double [numCols]; allocates each row's column space, and nested loops initialize all elements to 0.0. This creates a zero matrix of the specified dimensions.
The copy constructor Matrix::Matrix(const Matrix &m) copies numRows and numCols from m, allocates the same memory structure, then copies all elements with elements[i][j] = m.elements[i][j];. This performs a deep copy - creating an independent copy of the entire data structure, not just copying the pointer.
🔑 Definition — Copy Constructor: A constructor that creates a new object as a copy of an existing object. It's called when writing Matrix A(B); or Matrix A = B; (declaration, not assignment).
📐 Formula: Copy Constructor Memory Allocation Pattern = allocate row pointers → allocate each row's columns → copy element values.
📌 Example: To create matrix A that is a copy of existing matrix B (a 3×3 matrix), the copy constructor would: set A.numRows = 3, A.numCols = 3, allocate 3 row pointers, allocate 3 columns for each row, then copy all 9 elements from B's elements to A's elements.
Destructor of Matrix Class
The destructor is simple but essential: delete [] elements; The [] indicates it's an array deletion, and the compiler handles the size automatically. This returns the dynamically allocated memory to the free store, preventing memory leaks.
🔑 Definition — Memory Leak: Wastage of memory that occurs when dynamically allocated memory is not deallocated, causing the program to consume increasing amounts of memory.
Utility Functions of Matrix
getRows() and getCols() are simple const member functions that return numRows and numCols respectively. The const keyword ensures they don't modify the object.
The screen output function Matrix::output(ostream &os) creates a visually appealing matrix display using ASCII graphics characters. It starts with formatting: os.setf(ios::fixed,ios::floatfield); to ensure decimal display (no scientific notation), then sets precision to 2 decimal places. The function prints top corners using ASCII codes (218 for top-left, 191 for top-right), vertical bars (179) between rows, and bottom corners (192 for bottom-left, 217 for bottom-right). Each element is displayed with setw(10) for consistent spacing.
The file output function Matrix::output(ofstream &os) stores data without graphics - it first writes numRows and numCols, then all elements with setw(6) and 2 decimal places. This format enables later reading from file.
Input Functions
The keyboard input function Matrix::input(istream &is) politely prompts the user with "Input Matrix size: X rows by Y columns" and for each row asks "Please enter Y values separated by spaces for row no. N:". It reads values using the stream extraction operator into elements[i][j], with spaces as delimiters.
The file input function Matrix::input(ifstream &is) first reads Rows and Cols from the file. If both are positive, it creates a temporary Matrix temp(Rows, Cols), assigns it to the calling matrix with *this = temp; (using the assignment operator to handle dimension changes), then reads all element values from the file. It returns *this as a const reference.
🔑 Definition — Return by Reference vs Return by Value: Return by reference (const Matrix &) is used when returning the this object (the same object calling the function) to avoid copying. Return by value is used when returning a locally created temporary object, as the reference would be invalid after the function ends.
Transpose Function
The transpose function Matrix::transpose() swaps rows with columns. For square matrices (numRows == numCols), it uses an in-place algorithm with a temporary double variable, swapping elements[i][j] with elements[j][i] only for i < j (upper triangle) to avoid double-swapping.
For non-square matrices, it creates a temporary Matrix temp(numCols, numRows) with reversed dimensions, copies elements with temp.elements[j][i] = elements[i][j]; (placing row i, col j of original into row j, col i of temp), then assigns *this = temp;. This changes the original matrix's dimensions permanently.
📌 Example: A 4×5 matrix transposed becomes 5×4. The element originally at position (2,3) (row 2, column 3) moves to position (3,2) in the new matrix.
Code of the Program
The complete code listing includes the entire Matrix class implementation with all operator overloads. The main() function demonstrates usage by creating two matrices (4×5 and 5×4), taking keyboard input, displaying them, performing transpose, addition, subtraction, multiplication by matrices and scalars, and file I/O operations. The output shows the full execution with user interaction and results.
💡 Why this matters: The main function serves as both a test harness and a usage example, showing how all the operators and functions work together in a real program flow.
⭐ Key Takeaways
Students must remember that dynamic memory allocation in classes requires careful implementation of the "Big Three": constructor (allocates memory), destructor (deallocates memory), and copy constructor/assignment operator (ensures deep copy rather than shallow copy). The Matrix class demonstrates how to properly manage a two-dimensional dynamic array using double pointers. Friend functions are necessary when the left operand of an operator is not of the class type (e.g., double + Matrix). The transpose function requires different algorithms for square matrices (in-place swap) versus non-square matrices (create new matrix with swapped dimensions). When returning from functions, local temporary objects must be returned by value while the this pointer can be safely returned by reference.
🧠 Quick Revision Questions
- What are the three essential functions that must be implemented when a class uses dynamic memory allocation, and why is each necessary?
- How does the copy constructor differ from the assignment operator in terms of when each is called?
- Why must a friend function be used for
double + matrixoperation, whilematrix + doublecan be a member function? - Explain the two different algorithms used in the transpose function for square versus non-square matrices.
- What is the purpose of the
constkeyword in the declarationconst Matrix & Matrix::transpose()and how does it affect what the function can do?
📘 Lecture 45 — Example (continued) – Insertion and Extraction Operator Function – Review
📖 Overview: This lecture completes the matrix class implementation by defining assignment, arithmetic, and stream operators in detail. It then provides a comprehensive review of the entire CS201 course, covering fundamental programming concepts, data structures, and language paradigms. This serves as both a practical coding example and a course wrap-up, emphasizing the importance of strong programming fundamentals.
🗂️ Topics Covered
This lecture first demonstrates the complete implementation of operator overloading for a Matrix class, including assignment (=), addition (+), plus-equal (+=), overloaded plus for scalar, minus (-), multiplication (*), and stream insertion/extraction (<<, >>) operators. It then transitions into a comprehensive course review covering programming rules, variables, pointers, arrays, loops, decisions, classes and objects, garbage collection, truth tables, and structured query language (SQL). The review emphasizes the difference between language syntax and core programming concepts, highlighting the importance of fundamental knowledge over language-specific details.
📝 Lecture Summary
Example (continued)
This section continues the discussion on the Matrix class from previous lectures, implementing various operator overloads.
Assignment Operator Function
The assignment operator (=) for the Matrix class is critical because the class uses dynamic memory allocation. The function must handle cases where the source and destination matrices have different sizes. The declaration const Matrix & operator = (const Matrix &m); returns a constant reference to enable chained assignments like a = b = c; while preventing dangerous usage like (a = b) = c;. A self-assignment check is performed using if(&m != this) to avoid accidentally deleting the source matrix when a = a; is written. If sizes differ, the existing memory is freed and reallocated to match the source matrix's dimensions, then elements are copied element-by-element.
🔑 Definition — Self-assignment: Writing an assignment statement where the same object appears on both sides, e.g., a = a;. This requires special checking in classes with dynamic memory to avoid deleting the source before copying.
📐 Formula: Matrix::operator = (const Matrix &m) → The assignment operator copies the source matrix's contents into the destination matrix, handling size changes and self-assignment.
📌 Example: if(&m != this) checks for self-assignment. If true, the function checks if numRows != m.numRows || numCols != m.numCols, then deletes old memory, allocates new memory of correct size, and copies elements using nested loops.
Addition Operator Function
The addition operator (+) for matrices adds two matrices element-wise after checking conformability (same number of rows and columns). It returns a new matrix by value, leaving the original matrices unchanged. The function creates a temporary copy of one matrix using the copy constructor, then adds the corresponding elements of the second matrix to this copy using the += operator. This temporary matrix is returned by value, which means a copy is made on the stack.
🔑 Definition — Conformability: The condition that two matrices must have the same dimensions to be added or subtracted.
📐 Formula: Matrix Matrix::operator+(Matrix &m) const → Returns a new matrix where temp[i][j] = this->elements[i][j] + m.elements[i][j]
📌 Example: For matrices a and b of size 3x3, c = a + b; checks if both have 3 rows and 3 columns, then creates temp(*this) from a, adds b's elements, and returns temp.
Plus-equal Operator Function
The plus-equal operator (+=) modifies the left-hand side matrix directly by adding the right-hand side matrix to it. It returns a reference to the left-hand side matrix (using *this). The function is implemented efficiently by reusing the addition operator: *this = *this + m;. This demonstrates code reuse and returns a constant reference to enable chaining.
🔑 Definition — Code reuse: The practice of using existing functions or operators to implement new functionality, reducing duplication and improving maintainability.
📐 Formula: const Matrix & Matrix::operator += (Matrix &m) → *this = *this + m; return *this;
💡 Why this matters: The += operator is more efficient than + when modifying an existing matrix, as it avoids creating unnecessary temporary matrices.
Overloaded Plus Operator Function
The overloaded plus operator for scalar addition allows expressions like a + d (matrix + double) and d + a (double + matrix). The first is a member function that adds the scalar value d to every element of the matrix and returns a new matrix. The second is a friend function (since the double is on the left side) that performs the same operation. Both return a new matrix by value, using the copy constructor to create a temporary copy.
🔑 Definition — Friend function: A non-member function that has access to the private members of a class, declared using the friend keyword inside the class definition.
📐 Formula: Matrix Matrix::operator+(double d) const and Matrix operator+(double d, Matrix &m) → Returns a new matrix where each element has d added
📌 Example: Matrix result = matrixA + 5.5; adds 5.5 to all elements of matrixA and stores the result in a new matrix result.
Minus Operator Function
The minus operator (-) follows the same pattern as the addition operator. However, unlike addition, the a - d and d - a cases produce different results. The same conformability checking applies, and the function returns a new matrix by value.
📐 Formula: The same structure as addition operator but using subtraction (-=) instead of addition (+=)
Multiplication Operator Function
The multiplication operator (*) for matrices is the most complex arithmetic operator. It checks conformability for multiplication: the number of columns of the first matrix must equal the number of rows of the second matrix. The resultant matrix has dimensions equal to numRows of the first matrix by numCols of the second matrix. The calculation uses a triple nested loop, where each element temp[i][j] = sum over k of elements[i][k] * m.elements[k][j]. Scalar multiplication (matrix * double) follows the same pattern as scalar addition.
🔑 Definition — Conformability for multiplication: The condition that the number of columns in the first matrix equals the number of rows in the second matrix.
📐 Formula: temp.elements[i][j] = sum_{k=0}^{numCols-1} (elements[i][k] * m.elements[k][j]) for i from 0 to numRows-1, j from 0 to m.numCols-1
📌 Example: A 2x3 matrix multiplied by a 3x4 matrix produces a 2x4 matrix. Each element is computed by multiplying and summing corresponding elements from the row of the first and column of the second.
Insertion and Extraction Operator Function
The stream insertion (<<) and extraction (>>) operators are implemented as friend functions to allow syntax like cin >> m and cout << m. These functions demonstrate excellent code reuse by calling the previously defined input() and output() member functions. Two versions are provided for each: one for standard streams (istream, ostream) and one for file streams (ifstream, ofstream). Both versions return a reference to the stream to enable chaining.
🔑 Definition — Stream insertion/extraction operators: Operators (<< and >>) that overload the standard input/output behavior for user-defined types, enabling natural syntax like cout << myObject;.
📐 Formula: istream & operator >> (istream & is, Matrix & m) → m.input(is); return is; and ostream & operator << (ostream & os, Matrix & m) → m.output(); return os;
📌 Example: ofstream & operator << (ofstream & os, Matrix & m) calls m.output(os) and returns the file stream reference.
Exercise
Students are encouraged to study the complete Matrix class code and write test programs. A simple main function can create a 3x3 matrix with Matrix m(3,3);, display it with m.output;, define other matrices, and perform multiplication and addition. The exercise suggests extending the class by adding error messages, changing the data type from double to int, or converting the class into a template using template <class T>.
Review
This section provides a comprehensive review of the entire CS201 course, covering fundamental programming concepts and language-independent principles.
Rules for Programming
Any problem can be solved using three basic constructs: sequential execution (statements executed one after another), decision (if or if-else statements), and loop (repetition structures). Code should be short, concise, self-contained, and understandable. Comments should explain the logic, not the mechanics. Indentation is for human readability, not the compiler. Proper use of braces and semicolons creates the logical syntax structure.
🔑 Definition — Three programming constructs: Sequence (linear execution), decision (conditional branching), and loop (repetition) — the fundamental building blocks of any program.
Variables and Pointers
A variable is a name for a value, like a label on a memory location. A pointer is the address of a memory location. Pointers are specific to C and C++ and allow direct memory manipulation.
🔑 Definition — Pointer: A variable that stores the memory address of another variable.
Arrays
An array is a data structure that stores multiple values of the same data type. In C, C++, and FORTRAN, all elements must be of the same type. Some languages like FoxPro and Visual Basic allow mixed-type arrays (e.g., variant data type in Visual Basic).
🔑 Definition — Array: A collection of variables of the same data type stored in contiguous memory locations.
Loops and Decisions
Decisions in C++ include if, if-else, and switch statements. Loops include while (executes zero or more times), do-while (executes one or more times), and for (similar to while, executes zero or more times). All loops have three basic components: initialization, condition, and body.
🔑 Definition — Loop: A repetition structure that performs a task multiple times with different values.
📌 Example: A while loop may never execute if its condition is false at the start, while a do-while loop always executes at least once.
Classes and Objects
Classes combine data and code into user-defined data types. They implement encapsulation and data hiding. The major advantage is that tested and debugged code can be reused. Polymorphism (discussed in future courses) determines which function to call at runtime. Member functions use sequences, decisions, and loops in their implementation.
🔑 Definition — Encapsulation: The bundling of data with the methods that operate on that data, restricting direct access to some components.
Garbage Collection
Modern languages like Java use references instead of pointers and provide automatic garbage collection. This eliminates problems like dangling pointers (pointing to freed memory) and memory leaks (allocated memory that is never freed). In C and C++, programmers must manually deallocate memory using destructors in classes with dynamic memory.
🔑 Definition — Garbage collection: Automatic memory management that frees memory that is no longer in use. 🔑 Definition — Dangling pointer: A pointer that points to memory that has been deallocated.
Truth Table
Truth tables are tools for analyzing complex logical expressions. They are related to Boolean algebra and minimization techniques used in logic design. These same techniques apply to programming when dealing with complicated decision structures.
🔑 Definition — Truth table: A mathematical table used in logic to compute the functional values of logical expressions for all possible input combinations.
Structured Query Language
SQL (Structured Query Language) is a fourth-generation language used in database programming (Oracle, SQL Server). Unlike conventional languages (C, C++) where programmers must specify what to do and how to do it, SQL only requires specifying what is wanted, leaving the system to determine the optimal execution method. SQL has an ANSI standard and uses built-in optimizers similar to compiler optimizers.
🔑 Definition — SQL: A standard query language for managing data in relational database management systems, where the user specifies what data is needed, not how to retrieve it. 💡 Why this matters: Understanding the difference between 3GL (procedural) and 4GL (declarative) languages broadens programming perspective and highlights different paradigms for problem-solving.
⭐ Key Takeaways
The most critical takeaway is that the assignment operator in a dynamically allocated class must always check for self-assignment to prevent accidentally deleting the source object. Overloaded operators should be designed with proper return types—returning by reference for efficiency in assignment-like operators (returning const to prevent dangerous usage) and returning by value for arithmetic operators that produce new objects. The Matrix class demonstrates excellent code reuse, particularly in the stream operators that delegate to existing input/output functions and the += operator that reuses the + operator. Fundamentally, any programming problem can be solved using just three constructs: sequence, decision, and loop—these language-independent concepts are more important than knowing any specific language syntax. Finally, programming skills are about logical thinking and problem decomposition, not memorizing syntax; strong fundamentals enable adaptation to any new language or paradigm.
🧠 Quick Revision Questions
- Why is the assignment operator for the Matrix class declared with
constin its return type (const Matrix &)? - What is self-assignment and why must it be checked in the assignment operator of a class with dynamic memory?
- When adding two matrices, why does the addition operator return a new Matrix by value rather than a reference?
- How does the multiplication operator determine the dimensions of the resultant matrix, and what conformability check is required?
- What is the fundamental difference between third-generation languages (like C++) and fourth-generation languages (like SQL) in terms of how the programmer specifies a task?