CS304 — Midterm Summary (Lectures 1–22)
📘 Lecture 01 — Object Oriented Programming (CS304)
📖 Overview: This lecture introduces the fundamental concepts of object-oriented programming (OOP) and object-orientation as a technique for visualizing and solving programming problems. It explains why OOP maps naturally to real-world scenarios, making it easier to develop, understand, and implement complex systems.
🗂️ Topics Covered
This lecture covers the introduction to object-orientation, what a model is and why we need models, how object-oriented models represent real-world systems through interacting objects, the advantages of object-orientation, the definition and properties of objects, and the distinction between tangible and intangible objects with examples.
📝 Lecture Summary
01.1. Introduction
Course Objective: The objective of this course is to make students familiar with the concepts of object oriented programming. These concepts will be reinforced by their implementation in C++.
Course Contents: The main topics covered across 45 lectures are: Object Orientation, Objects and Classes, Overloading, Inheritance, Polymorphism, Generic Programming, Exception Handling, and Introduction to Design Patterns.
Recommended Text Book: C++ How to Program by Deitel & Deitel. Reference books include Object-Oriented Software Engineering by Jacobson et al. and The C++ Programming Language by Bjarne Stroustrup.
What is Object-Orientation? It is a technique in which we visualize our programming problems in the form of objects and their interactions, as happens in real life. Examples include a person, a house, a tree, and a car — objects that interact with each other to perform different operations (e.g., a person lives in a house, a person drives a car). In a school, objects include students, teachers, books, pens, school bags, classrooms, parents, and playgrounds. To develop a fee collection system for a school, we find related objects and their interactions as in real life.
In object orientation, we move concentration to objects in contrast to the procedural paradigm where we simply write code in functions and call them in the main program.
01.2. What is a Model?
A model is an abstraction of something real or conceptual. We need models to understand an aspect of reality. Model examples include highway maps, architectural models, and mechanical models.
01.3. OO Models
In the context of programming, models are used to understand the problem before starting development. We make Object Oriented models showing several interacting objects to understand a system given for implementation.
Example 1 – Object Oriented Model: Objects: Ali, Car, House, Tree. Interactions: Ali lives in the house, Ali drives the car.
Example 2 – Object Oriented Model (A School Model): Objects: Teacher, Student, School Bag, Pen, Book, Playground. Interactions: Teacher teaches Student, Student has School Bag, Book, and Pen.
01.4. Object-Orientation - Advantages
As Object Oriented Models map directly to reality, we can easily develop an OO model for a problem, everyone can easily understand an OO model, and we can easily implement an OO model using any object oriented language like C++ with features such as classes, inheritance, and virtual functions.
01.5. What is an Object?
An object is:
- Something tangible (Ali, School, House, Car)
- Something conceptual (that can be apprehended intellectually, e.g., time, date)
An object has:
- State (attributes)
- Well-defined behavior (operations)
- Unique identity
01.6. Tangible and Intangible Objects
Examples of Tangible Objects: Ali is a tangible object with characteristics (attributes): Name, Age; and behavior (operations): Walks, Eats. We identify Ali using his name. A Car has state (attributes): Color, Model; behavior (operations): Accelerate, Start Car, Change Gear. We identify a car using its registration number.
Examples of Intangible Objects (Conceptual Objects): Time is an intangible object with state: Hours, Seconds, Minutes; behavior: Set/Get Hours, Set/Get Seconds, Set/Get Minutes. We assign an own generated unique ID in the model for a Time object. Date is an intangible object with state: Year, Day, Month; behavior: Set/Get Year, Set/Get Day, Set/Get Month. We assign an own generated unique ID for a Date object.
⭐ Key Takeaways
The most critical concepts to remember are: object-orientation visualizes programming problems as interacting real-world objects, making problem-solving more intuitive than the procedural paradigm. A model is an abstraction used to understand reality before implementation. Every object has three essential properties: state (attributes/data), behavior (operations/functions), and unique identity. Objects can be tangible (physical like a car or person) or intangible/conceptual (like time or date). Nouns in a problem description are candidates for becoming objects in the system, and object-oriented models map directly to reality, enabling easier understanding and implementation.
🧠 Quick Revision Questions
- What is the fundamental difference between the object-oriented paradigm and the procedural paradigm?
- What are the three essential properties that every object must have?
- Give two examples each of tangible objects and intangible (conceptual) objects in the context of OOP.
- Why are models important in object-oriented programming? What do they help us achieve?
- What is meant by "nouns in a problem description are candidates for becoming objects"? Provide an example.
📘 Lecture 02 — Information Hiding, Encapsulation, Interface, Implementation, Separation of Interface & Implementation, Messages
📖 Overview: This lecture explores foundational OOP principles that govern how objects manage and expose their data and behavior. It explains how information hiding and encapsulation protect an object's internal state, and how interfaces provide controlled access to that state, while implementation remains hidden. This separation is crucial for building modular, maintainable, and secure object-oriented systems.
🗂️ Topics Covered
The lecture covers Information Hiding as the core principle of restricting access to object details, followed by Encapsulation which bundles data and behavior together. It then defines Interface as the set of exposed functions, and Implementation as the hidden internal data structures and functionality. The Separation of Interface & Implementation is discussed as a key design goal, and finally, Messages are introduced as the mechanism for object communication.
📝 Lecture Summary
02.1. Information Hiding
This is a core OOP principle inspired by real life: not all information should be accessible to everyone. In OOP, it means hiding an object’s details (its state and behavior) from other objects or users (where "users" means other objects or external programs). Information hiding simplifies the model by focusing only on object interactions, not internal workings. It also acts as a barrier against change propagation because changing the internal implementation of a function does not affect other parts of the system that only use the function's name and parameters.
🔑 Definition — Information Hiding: "Showing only those details to the outside world which are necessary for the outside world and hiding all other details from the outside world."
💡 Why this matters: This principle prevents accidental or malicious interference with an object's internal state, making code more robust and secure. It also allows developers to change internal logic without breaking other parts of the program.
Examples:
- A person named Ali keeps his personal information in his brain; we must ask him for it, and he controls how much to share.
- An email server has millions of accounts but only shares our account with us when we request it.
- A phone SIM card stores phone numbers, but the phone-set reads them for us; the owner can restrict access to those numbers.
Achieving Information Hiding:
- All information related to an object is stored within the object.
- It is hidden from the outside world.
- It can only be manipulated by the object itself.
02.2. Encapsulation
Encapsulation is the mechanism that bundles an object's data members (state) and functions (behavior) together within the object. It is closely related to information hiding, as encapsulation is the primary way to achieve it. The data and behavior are tightly coupled inside the object, and both the information structure and the implementation details of its operations are hidden from the outside world.
Advantages of Encapsulation:
- Simplicity and clarity: All data and functions are stored within objects, eliminating free-floating code.
- Low complexity: Functions are not chaotically intertwined; each object has a specific, contained behavior.
- Better understanding: Object diagrams are self-explanatory as each object has a specific role and relations.
Examples:
- The object
Aliencapsulates his personal information (like his name and age) and his behaviors (like walking and eating). Another object cannot use his behavior without Ali’s permission. - A phone encapsulates its stored data (contacts, etc.) and the behavior to show that data; we can only access it through the phone's interface.
🔑 Definition — Encapsulation: "We have enclosed all the characteristics of an object in the object itself."
02.3. Interface
An object’s interface is the set of functions it chooses to expose to other objects. Since data and behavior are hidden, the interface provides the only means for external objects to interact with it. Different objects may need different functions, so an interface can be different for different requesting objects. Interfaces are necessary for object communication.
Examples:
- Interface of a Car: Steer Wheels, Accelerate, Change Gear, Apply Brakes, Turn Lights On/Off.
- Interface of a Phone: Input Number, Place Call, Disconnect Call, Add number to address book, Remove number, Update number.
🔑 Definition — Interface: A set of functions of an object that it wants to expose to other objects.
02.4. Implementation
Implementation is the actual, hidden code and data structures that realize the behavior defined by the interface. It has two main parts:
- Internal data structures: These hold the object's state (the actual values of data members).
- Functionality: This is provided by the member functions that define the object's behavior.
Examples:
- Gear Box in a car system: The implementation includes the mechanical structure of the gear box (its data structure) and the mechanism to change gear (its functionality).
- Address Book in a Phone: The physical structure of the SIM card acts as the data structure, and the phone's read/write operations provide the functionality.
🔑 Definition — Implementation: The actual implementation of the behavior of the object in any Object Oriented language, consisting of internal data structures and functionality.
02.5. Separation of Interface & Implementation
This principle states that we should only show the interface of an object to the outside world and hide the implementation. This makes the interface independent of its internal implementation. This is achieved through encapsulation and information hiding.
Real Life Example: A driver uses a standard interface (steering wheel, pedals, gear shift) to drive a car. Using this interface, they can drive any car, regardless of its engine type, fuel type, or specific model, because the interface is separate from the implementation.
💡 Why this matters: This separation allows developers to change the internal implementation of an object (e.g., improving a sorting algorithm or changing a data structure) without changing any code that uses that object, as long as the interface remains unchanged.
02.6. Messages
Objects communicate by sending messages (also called stimuli) to each other. A message is sent by invoking an appropriate operation on the target object. The number and kind of messages that can be sent to an object are determined by its interface.
Examples- Messages:
- A Person sends a "stop" message to a Car by applying the brakes.
- A Person sends a "place call" message to a Phone by pressing the appropriate button.
🔑 Definition — Messages: The mechanism by which objects communicate, sent by invoking an operation on the target object.
⭐ Key Takeaways
This lecture establishes that objects are self-contained units that hide their internal details (encapsulation and information hiding) and only expose a carefully selected set of operations (interface). The implementation of those operations and the internal data structures are completely separate from the interface, a principle known as separation of interface and implementation. Objects interact not by directly accessing each other's data, but by sending messages that trigger specific behaviors through the interface. These principles are fundamental for building modular, robust, and maintainable software systems.
🧠 Quick Revision Questions
- What is the difference between information hiding and encapsulation as described in this lecture? How are they related?
- Explain the concept of an interface and provide two distinct real-world examples of an object's interface, other than a car or phone.
- What are the two main parts of an object's implementation? Give an example for each.
- Why is the separation of interface and implementation considered a good design principle? What practical benefit does it provide to a developer?
- What is a message in OOP? Using the car example, describe how a driver would send a message to the car and what operation that message invokes.
📘 Lecture 03 — Abstraction, Classes, Inheritance
📖 Overview: This lecture introduces three foundational concepts of Object-Oriented Programming. It explains how abstraction simplifies real-world complexity, how classes serve as blueprints for creating objects, and how inheritance enables code reuse through hierarchical relationships between classes. Understanding these concepts is essential for designing efficient, maintainable, OOP-based systems.
🗂️ Topics Covered
The lecture covers Abstraction as a principle for focusing only on relevant details of objects, using an example of a person who is both a student and a teacher. It then introduces Classes as prototypes or sketches for creating object instances, with examples like Student, Teacher, and Circle classes. Finally, Inheritance is explained as an "IS A" relationship where derived classes inherit characteristics from base classes, with major benefits including Reuse, reduced redundancy, and increased maintainability.
📝 Lecture Summary
Abstraction
Real-life objects have many attributes and behaviors, but most of the time we are only interested in the part relevant to the current problem. For example, when implementing a school system, we don't need to care about the personal life of a student or teacher, as it won't affect the system. This concept is called "Abstraction." Abstraction is a way to cope with complexity and is used to simplify things.
🔑 Definition — Abstraction: "Capture only those details about an object that are relevant to current perspective."
📌 Example: For the statement "Ali is a PhD student and teaches BS students":
- From the student perspective: attributes like Name, Student Roll No, Year of Study, CGPA, Age; behaviors like Study, GiveExam, PlaySports
- From the teacher perspective: attributes like Name, Employee ID, Designation, Salary, Age; behaviors like DevelopExam, TakeExam, DeliverLecture, Walk
- Many attributes (like Name, Age) are common to both perspectives, but others are specific to one role.
Abstraction has major advantages:
- It helps in understanding and solving a problem using OOP by hiding extra irrelevant details.
- Focusing on a single perspective provides freedom to change implementation for other aspects later.
💡 Why this matters: Similar to encapsulation, abstraction achieves information hiding — showing only relevant details to related objects and hiding other details.
Classes
In OOP, we create a general sketch for each kind of object, then create different instances using this sketch. This sketch, prototype, or map is called a "class." All objects of the same kind exhibit identical characteristics (information structure and behavior), though they have data of their own.
🔑 Definition — Class: A blueprint or prototype that defines the attributes and behaviors for a kind of object. Objects are instances of a class.
📌 Example 1 — Student Class: Ali studies mathematics, Anam studies physics, Sohail studies chemistry. Each is a student — they are instances of the Student class.
📌 Example 2 — Teacher Class: Ahsan teaches mathematics, Aamir teaches computer science, Atif teaches physics. Each is a teacher — they are instances of the Teacher class.
Class Representation: A class can be represented as a rectangle with three compartments:
- Normal Form: Shows Class Name (top), Attributes (middle), Operations/behaviors (bottom) — e.g.,
Circlewith attributescenter,radiusand operationsdraw,computeArea - Suppressed Form: Shows only the Class Name — e.g., just
Circle
Inheritance
A child inherits characteristics of its parents. Besides inherited characteristics, a child may have its own unique characteristics.
Inheritance in Classes: If a class B inherits from class A, then B contains all the characteristics (information structure and behavior) of class A. The parent class is called the base class and the child class is called the derived class. Besides inherited characteristics, a derived class may have its own unique characteristics.
🔑 Definition — Inheritance: "IS A" or "IS A KIND OF" relationship between classes. For example: Student IS A Person, Teacher IS A Person, Doctor IS A Person. Similarly, Circle IS A Shape, Line IS A Shape, Triangle IS A Shape.
📌 Example — Person Hierarchy:
- Base class
Person: attributesname,age,gender; behaviorseat,walk - Derived class
Student: adds attributesprogram,studyYear; behaviorsstudy,heldExam - Derived class
Teacher: adds attributesdesignation,salary; behaviorsteach,takeExam - Derived class
Doctor: adds attributesdesignation,salary; behaviorscheckUp,prescribe
📌 Example — Shape Hierarchy:
- Base class
Shape: attributescolor,coord; behaviorsdraw,rotate,setColor - Derived class
Circle: adds attributeradius; behaviorsdraw,computeArea - Derived class
Line: adds attributelength; behaviordraw - Derived class
Triangle: adds attributeangle; behaviorsdraw,computeArea
Inheritance – Advantages:
- Reuse — Main purpose of inheritance is reuse; we can easily add new classes by inheriting from existing ones.
- Less redundancy — Common code is written once in the base class.
- Increased maintainability — Changes to the base class automatically propagate to derived classes.
💡 Why this matters: With reuse, you select an existing class closer to the desired functionality, create a new class, inherit it from the selected class, then add to and/or modify the inherited functionality.
⭐ Key Takeaways
Abstraction is the OOP principle of capturing only relevant details of an object for a given perspective, hiding complexity. Classes act as blueprints or prototypes from which objects (instances) are created; all instances share the same structure and behavior but have their own data. Inheritance creates an "IS A" relationship where a derived class inherits all characteristics from a base class while adding its own unique features. The major benefits of inheritance are reuse, reduced redundancy, and increased maintainability. For the exam, remember that class diagrams use a rectangle with three compartments (name, attributes, operations), with normal and suppressed forms.
🧠 Quick Revision Questions
- What is abstraction, and why is it important in object-oriented programming?
- How does a class differ from an object? Provide an example for each.
- Explain the "IS A" relationship with respect to inheritance using the Shape hierarchy.
- List and explain the three major advantages of inheritance.
- In the Ali example, which attributes belong only to the student perspective and which only to the teacher perspective?
📘 Lecture 04 — Lecture No.04
📖 Overview: This lecture explores advanced inheritance concepts in Object-Oriented Programming, focusing on how classes relate to each other beyond simple parent-child relationships. It introduces generalization, subtyping, specialization, overriding, and the crucial distinction between abstract and concrete classes, which are fundamental for designing flexible and reusable software systems.
🗂️ Topics Covered
The lecture covers concepts related to inheritance including generalization (extracting common features into a base class), subtyping/extension (where derived class is behaviourally compatible with base), specialization/restriction (where derived class is behaviourally incompatible), overriding (redefining base class behaviour), and the difference between abstract classes (cannot be instantiated) and concrete classes (can be instantiated).
📝 Lecture Summary
04.1. Concepts Related with Inheritance
The lecture introduces three key inheritance concepts: Generalization, Subtyping (extension), and Specialization (restriction). These concepts describe different ways derived classes relate to their base classes.
04.2. Generalization
In OO models, some classes may have common characteristics. We extract these features into a new class and inherit original classes from this new class. The Base class encapsulates the idea of commonality of derived classes. This concept is known as Generalization. It reduces redundancy and gives us reusability; using generalization our solution becomes less complex. In generalization there should be “Is a Kind of Relationship” (also called “Is A relationship”) between base and child classes.
Example: Line, Circle and Triangle all share common attributes (color, vertices) and common behaviour (move, setColor). These are extracted into a general Shape class.
Example: Student, Doctor and Teacher all share common attributes (name, age, gender) and common behaviour (eat, walk). These are extracted into a general Person class.
04.3. Sub-typing (Extension)
Sub-typing means that derived class is behaviourally compatible with the base class. Derived class has all the characteristics of base class plus some extra characteristics. Behaviourally compatible means that base class can be replaced by the derived class.
Example: Circle is extending the behaviour of Shape by adding radius and methods like computeCircumference and computeArea. Student has two extra attributes (program, studyYear) and extended behaviour (study, takeExam).
Subtyping and generalization are related concepts. Subtyping (extension) and generalization is a way to look at the same thing in two ways. Sub typing is looking at things from Top to bottom whereas in generalization we look at things from bottom to top.
04.4. Specialization (Restriction)
We want to add a class to existing hierarchy of classes having many similarities to already existing classes but some part of its behaviour is different or restricted. In that case we will use the concept of Specialization. Specialization means that derived class is behaviourally incompatible with the base class. Behaviourally incompatibility means that base class can’t always be replaced by the derived class. Derived class has some different or restricted characteristics than of base class.
Example – Specialization (Restriction): Suppose we want to add a class of Adult for ID card generation such that it is a person but its age is greater than 18. Rather than writing all code again, we derive Adult class from Person class and restrict age in that class.
- Person: age : [0..100]
- Adult: age : [18..100] with setAge method restricted to accept only ages >= 18
Example: Natural Numbers are also Integers with the restriction that natural numbers set can NOT contain zero or negative integers. NaturalSet inherits from IntegerSet but restricts the add method.
- IntegerSet: add(elem) - adds element to set
- NaturalSet: add(elem) - if elem < 1 then error, else add element to set
💡 Why this matters: Add method behaviour is present in both base and derived classes but derived class behaviour is different. Derived class will not exhibit the behaviour of base class but it is overriding behaviour of base class with its own behaviour.
04.5. Overriding
A class may need to override the default behaviour provided by its base class. Derived class overrides the behaviour of its base class.
Reasons for overriding:
- Provide behaviour specific to a derived class (specialization)
- Extend the default behaviour (extension)
- Restrict the default behaviour (restriction)
- Improve performance
Example – Specific Behaviour (Specialization): Shape has a draw method. Circle, Line, and Triangle each override draw with their own specific drawing implementation.
Example – Extension: IntegerSet has add method to add element to set. NaturalSet overrides add with if elem < 1 then give error, else add element to the set.
Example – Extension: Window has draw method. DialogBox overrides draw to first invoke Window's draw, then draw the dialog box.
Example – Restriction: NaturalSet restricts the add method of IntegerSet.
Example – Improve Performance: Class Circle overrides rotate operation of class Shape with a Null operation (doing nothing).
04.6. Abstract Classes
In our examples we made classes for Shape and Person. These are abstract concepts and the classes we make against abstract concepts are called Abstract Classes. They are present at or near the top in the class hierarchy to present most generalized behaviour.
An abstract class:
- Implements an abstract concept
- Main purpose is to be inherited by other classes
- Can’t be instantiated
- Promotes reuse
Abstract Classes - Example I: Shape (abstract class) → Circle, Line, Triangle (concrete classes)
Abstract Classes - Example II: Person (abstract class) → Student, Teacher, Doctor, Engineer, Director (concrete classes)
Abstract Classes - Example III: Vehicle (abstract class) → Car, Bus, Truck (concrete classes)
Abstract Classes cannot exist standalone in an object model. While making object model we start by finding out objects in our object model and then we find out objects having common attributes and make them in the form of general classes at the top of class hierarchies.
04.7. Concrete Classes
The entities that actually we see in our real world are called concrete objects and classes made against these objects are called Concrete Classes.
A concrete class:
- Implements a concrete concept
- These are used to instantiate objects in our programs
- Provides implementation details specific to the domain context
Concrete Classes - Example I: Student, Teacher and Doctor are concrete classes derived from Person.
Concrete Classes - Example II: Car, Bus and Truck are concrete classes derived from Vehicle.
- A concrete class may exist in an object model independently
- Concrete classes mostly lie below the top of class hierarchy in a good object model
If there is an abstract class then hierarchy exists in the object model as there will definitely be some concrete classes as well derived from this abstract class otherwise there is no use of abstract class.
⭐ Key Takeaways
Generalization extracts common features from multiple classes into a base class to reduce redundancy and promote reuse, while subtyping (extension) adds new features and specialization (restriction) modifies or limits inherited behaviour. Overriding allows derived classes to replace base class behaviour for reasons including specialization, extension, restriction, or performance improvement. Abstract classes represent generalized concepts that cannot be instantiated but must be inherited, while concrete classes represent real-world entities that can be instantiated. The key exam concept is understanding that subtyping views relationships top-down while generalization views them bottom-up.
🧠 Quick Revision Questions
- What are the three main concepts related with inheritance discussed in this lecture?
- How does generalization differ from subtyping in terms of direction (top-down vs bottom-up)?
- What does it mean for a derived class to be "behaviourally compatible" with its base class?
- Give two reasons why a derived class might override a method from its base class.
- What is the main difference between an abstract class and a concrete class?
📘 Lecture 05 — Multiple Inheritance
📖 Overview: This lecture introduces multiple inheritance, where a class inherits from more than one parent class. It covers its advantages, such as code reuse, and its significant disadvantages, including increased complexity and the diamond problem, while also shifting focus to association relationships between objects.
🗂️ Topics Covered
The lecture begins by revisiting the purposes of inheritance from the last lecture, then formally introduces multiple inheritance with examples (Mermaid, Amphibious Vehicle) and its C++ syntax. It details the problems associated with multiple inheritance, specifically ambiguity and the diamond problem, and provides solutions like overriding and virtual inheritance. The second half defines association and its two main types: class association (implemented via inheritance) and object association, which includes simple association, composition, and aggregation, along with their properties and UML notation.
📝 Lecture Summary
05.1. Multiple Inheritance
Multiple Inheritance is a feature where a derived class inherits from more than one base class. This allows the derived class to reuse characteristics of multiple parent classes.
🔑 Definition — Multiple Inheritance: The ability of a class to inherit attributes and methods from more than one base class.
📌 Example 1: The Mermaid class inherits from both Woman and Fish classes, gaining the ability to walk() and swim().
📌 Example 2: The AmphibiousVehicle class inherits from both LandVehicle and WaterVehicle classes, gaining the Move() and Float() methods.
Problems with Multiple Inheritance
While multiple inheritance reduces code redundancy, it introduces several problems.
- Increased Complexity: The class hierarchy becomes more complicated and less understandable.
- Reduced Understanding: The object model is difficult to understand for someone seeing it for the first time.
- Duplicate Features: Features (same method name) may appear in multiple parent classes.
Problem 1: Ambiguity
When both parent classes have a method with the same name, the compiler cannot determine which one to inherit. For example, if both Woman and Fish have an eat() method, from which class should Mermaid inherit it?
🔑 Definition — Ambiguity: A situation where a derived class has multiple potential definitions for an inherited member, leading to a compile-time error.
✅ Solution: Override the ambiguous method in the derived class and explicitly call the desired base class method using the scope resolution operator (e.g., Woman::eat()).
Problem 2: Two Instances for Same Function (Diamond Problem)
This occurs in a "diamond" hierarchy where a class inherits from two classes that both inherit from a common base class. For example, AmphibiousVehicle inherits from LandVehicle and WaterVehicle, which both inherit from Vehicle. This results in two copies of the Vehicle class within the AmphibiousVehicle object, causing ambiguity for inherited members like changeGear().
📐 Formula: Diamond Problem: A → (B, C) → D, where both B and C inherit from A, and D inherits from both B and C. This creates two instances of A's members in D.
✅ Solution: The problem is solved through virtual inheritance, which ensures only one copy of the common base class is included in the derived class. Some languages also simply disallow diamond hierarchies.
05.2. Kinds of Association
Association describes the interaction of different objects in an object-oriented model.
🔑 Definition — Association: A relationship between two or more objects that defines how they interact with each other to perform work.
1. Class Association
This is implemented via Inheritance. It represents a relationship between classes.
- Public Inheritance: An "IS-A" relationship.
- Private Inheritance: An "Implemented in terms of" relationship.
2. Object Association
This is the interaction between stand-alone objects of different classes. It has three main types: Simple Association, Composition, and Aggregation.
05.3. Simple Association
The weakest link between two objects that have no intrinsic relationship with each other. It is a reference by which one object can interact with another (e.g., Ali drives a Car).
🔑 Definition — Simple Association: A relationship where two objects interact but have no ownership or part-whole relationship.
Kinds of Simple Association w.r.t Navigation:
- One-way Association: Navigation is possible in only one direction, denoted by an arrow (
→). - Two-way Association: Navigation is possible in both directions, denoted by a simple line (
—).
Kinds of Simple Association w.r.t Cardinality:
- Binary Association: Associates objects of exactly two classes.
- Ternary Association: Associates objects of exactly three classes, denoted by a diamond with lines.
- N-ary Association: An association between 3 or more classes.
05.4. Composition
A strong "part-of" relationship where the composed object is a part of the whole and cannot exist independently.
🔑 Definition — Composition: A strong form of association where an object is composed of other objects, and the "part" objects cannot exist independently of the "whole" object (e.g., Ali is composed of Head, Arm, etc.). Represented by a line with a filled-diamond head towards the composer.
💡 Why this matters: The lifetime of the "part" is tied to the lifetime of the "whole." If the whole is destroyed, the parts are also destroyed.
05.5. Aggregation
A weaker "has-a" relationship where the container object holds a collection of other objects that can exist independently.
🔑 Definition — Aggregation: A form of association where an object contains a collection of other objects, but the contained objects can exist independently of the container (e.g., A Room contains Furniture). Represented by a line with an unfilled-diamond head towards the container.
💡 Why this matters: The container does not own the lifecycle of the contained objects. The contained objects can exist and be part of other containers.
⭐ Key Takeaways
The lecture is split into two core concepts: the mechanics and pitfalls of multiple inheritance, and the various types of association between objects. For multiple inheritance, remember that while it allows powerful code reuse, it introduces ambiguity and the diamond problem, which require explicit solutions like overriding or virtual inheritance. For associations, you must distinguish between class association (inheritance) and object association, and within object association, differentiate simple association (weak, no ownership), composition (strong, part-of, filled diamond), and aggregation (weaker, has-a, unfilled diamond). Understanding the UML notations for these relationships is crucial for conceptual modeling.
🧠 Quick Revision Questions
- What is the diamond problem in multiple inheritance, and what causes it?
- Explain the difference between a one-way and a two-way association, and provide a real-world example for each.
- Describe the key difference between composition and aggregation in terms of object lifecycle and dependency.
- If a Mermaid class inherits from both
WomanandFish, and both base classes have aswim()method, what is the ambiguity problem, and how can it be resolved? - A
LibraryhasBooks. Can aBookexist independently of theLibrary? Which type of association (simple, composition, or aggregation) best describes this relationship?
📘 Lecture 6 — Class Compatibility, Polymorphism & Object-Oriented Modeling
📖 Overview: This lecture introduces two fundamental OOP concepts: class compatibility (subtyping) and polymorphism. It then applies these concepts through a complete, step-by-step example of modeling a graphic editor, demonstrating how to identify classes, associations, attributes, operations, and inheritance from a problem statement.
🗂️ Topics Covered
The lecture covers class compatibility and the definition of a subtype (behavioral compatibility), the general concept of polymorphism and its specific meaning in the OO model with examples and advantages, followed by a comprehensive object-oriented modeling example that demonstrates identifying classes, eliminating irrelevant ones, finding associations, identifying attributes and operations, and identifying inheritance.
📝 Lecture Summary
Class Compatibility
A class is behaviorally compatible with another if it supports all the operations of the other class. Such a class is called a subtype. A class can be replaced by its subtype. A derived class is usually a subtype of the base class because it can handle all the legal messages (operations) of the base class. Therefore, a base class can always be replaced by the derived class. For example, a Shape class with attributes (color, vertices) and operations (move, setColor, draw) has derived classes Circle, Line, and Triangle. All three derived classes are behaviorally compatible with the base class. Similarly, a File class can be replaced by any of its child classes (ASCII File, PDF File, PS File).
Polymorphism
In general, polymorphism refers to the existence of different forms of a single entity. For example, both Diamond and Coal are different forms of Carbon.
Polymorphism in OO Model
In the OO model, polymorphism means that different objects can behave in different ways for the same message (stimulus). Consequently, the sender of a message does not need to know the exact class of the receiver. The sender sends a message to the receiver, and the appropriate method is called on the receiver side.
- Example 1 – Shape Hierarchy: An Editor sends a
drawmessage to a Shape class. Thedrawmethod is called according to the nature of the actual object present (e.g.,Line.draw(),Circle.draw(),Triangle.draw()). - Example 2 – File Hierarchy: An Editor sends a
printmessage to a File class. Theprintmethod is called based on the actual child object of the File class (e.g.,ASCII_File.print(),PDF_File.print(),PS_File.print()). The message is the same, but the appropriate execution will be done based on the receiver.
💡 Why this matters: Polymorphism allows the same message to trigger different behaviors, making systems flexible and extensible.
Polymorphism – Advantages
- Messages can be interpreted in different ways depending upon the receiver class.
- New classes can be added without changing the existing model. For example, if a new Square class is added to the Shape hierarchy, it only needs to implement its own
draw()method. The Editor can still send the samedrawmessage without any code changes. - In general, polymorphism is a powerful tool to develop flexible and reusable systems.
Object-Oriented Modeling an Example
Problem Statement: Develop a graphic editor that can draw different geometric shapes such as line, circle and triangle. User can select, move or rotate a shape. To do so, editor provides user with a menu listing different commands. Individual shapes can be grouped together and can behave as a single shape.
1. Identify Classes: Extract nouns from the problem statement: Editor, Line, Circle, Triangle, Shape, User, Menu, Commands, Group.
- Eliminate irrelevant classes:
- Editor – Very broad scope. It is the name of the overall system, so we will not make its object. It is marked as irrelevant.
- User – Out of system boundary; it is interacting with the system from outside.
- Add classes by analyzing requirements:
- Group (of shapes) – Required to behave as a shape, so it should behave as an object.
- View – A graphic editor must have a display area to show shapes. We add this using domain knowledge.
- Final Classes: Shape, Line, Circle, Triangle, Menu, Group, View.
2. Finding Associations: Find relationships between objects.
- Identify Associations: Extract verbs connecting objects.
- "Individual shapes can be grouped together" → Group consists of Line, Circle, Triangle (and other groups). This is a Composition relationship.
- Verify access paths:
- View contains (draws) shapes (Line, Circle, Triangle, Group). This is an Aggregation relationship.
- Menu sends message to View. This is a Simple One-Way Association.
3. Identify Attributes: Extract properties from domain knowledge.
- Line: Color, Vertices, Length
- Circle: Color, Vertices, Radius
- Triangle: Color, Vertices, Angle
- Shape: Color, Vertices
- Group: noOfObjects
- View: noOfObjects, selected
- Menu: Name, isOpen
4. Identify Operations: Extract verbs connected with an object from the problem statement: "draw", "select", "move", "rotate", "group", "open", "provide".
- Eliminate irrelevant operations: "Develop" is out of system boundary. "Behave" has broad semantics.
- Selected operations per class:
- Line, Circle, Triangle, Shape, Group: draw(), select(), move(), rotate()
- Menu: open(), select(), move(), rotate()
- Extract operations using domain knowledge:
- View: add(), remove(), group(), show(), select(), move(), rotate()
5. Identify Inheritance: Search for "is a kind of" relationships.
- From "shapes such as line, circle and triangle" → Line, Circle, Triangle inherit from Shape.
- From "Individual shapes can be grouped together and can behave as a single shape" → Group inherits from Shape.
Refining the Object Model: Application of inheritance demands an iteration over the whole object model. In the inheritance hierarchy:
- All attributes are shared:
Color,verticesare moved to Shape. - All associations are shared: View contains all kinds of shapes. Group consists of all kinds of shapes.
- Some operations are shared:
select(),move(),rotate()are shared from Shape. - Other operations are overridden: The implementation of
draw()is specific to Line, Circle, Triangle, and Group (they override the basedraw()).
⭐ Key Takeaways
You must understand that class compatibility means a derived class can replace its base class because it supports all base class operations, making it a subtype. Polymorphism is the ability for different objects to respond to the same message in different ways, enabling flexible and reusable systems. The graphic editor example demonstrates the complete OO modeling process: extracting nouns for classes, eliminating irrelevant ones, identifying associations (aggregation, composition, simple association) from verbs, and finding inheritance from "is-a" relationships. Finally, when inheritance is applied, you must refine the object model by sharing attributes, associations, and common operations up the hierarchy, while allowing specific implementations (like draw()) to be overridden in derived classes.
🧠 Quick Revision Questions
- What does it mean for a class to be behaviorally compatible with another class?
- In the context of OOP, what does polymorphism mean?
- What are the two main advantages of polymorphism mentioned in the lecture?
- In the graphic editor modeling example, why was the Editor class considered irrelevant?
- What is the difference between a shared operation and an overridden operation in an inheritance hierarchy?
📘 Lecture 07 — The basic concept “Object” of Object Orientation (thinking in terms of objects) is realized using classes in programming languages.
📖 Overview: This lecture introduces the fundamental concept of classes in C++ as the mechanism to realize objects in programming. It explains how classes allow us to create user-defined types that capture both attributes and behaviours of real-world entities, and covers the syntax, access specifiers, and member access methods essential for implementing Object-Oriented Programming.
🗂️ Topics Covered
This lecture covers the definition and purpose of classes as the implementation mechanism for objects in C++. It explains how classes are used to create user-defined types (like Student or Circle) with data members and member functions. The lecture also discusses abstraction (including only relevant details), the difference between structure and class definitions, and the syntax for declaring class variables and accessing their members using dot and arrow operators. Finally, it covers the three access specifiers (public, private, protected) and the default private access specifier in classes.
📝 Lecture Summary
07.1. Class
A class is the mechanism given by C++ to realize objects in a program. It is the concrete implementation of objects in C++. We capture any object's attributes and behaviour in a programming language using classes. In other words, a class can be defined as a facility given by C++ to create new types according to our requirement. A class is a composite data type made from basic C++ types like integers, chars, and floats.
🔑 Definition — Class: A way (mechanism) given by C++ to realize objects in a program; a concrete implementation of objects; a facility to create new user-defined types according to our requirement.
💡 Why this matters: When we hear the word "student" or think about a student, a sketch comes to mind along with its attributes (name, roll number, class, degree) and behaviour (study, register). We need to capture these characteristic features of any object in the programming language, and the concept of class is used for this purpose.
Uses: Objects are structured in terms of class so our problem becomes easier to understand in terms of a C++ program. We can implement interactions easily in terms of classes. Student objects will interact with each other to take and give services to each other as happens in real life and is mapped in object-oriented programming approach.
07.2. Type in C++
We implement generic concepts using types. We have to model the generic concept of "Student," but there is no built-in type "student" in C++ like built-in C++ types int or float. A class is the mechanism in C++ that will allow us to define Student as a user-defined type. Similarly, the generic concept "Circle" will also be implemented in the same way.
As objects have attributes and behaviour, corresponding classes will also have data members and methods.
Example: For a Person object "Ali", the corresponding class has:
- Characteristics (attributes): Name, Age
- Behaviour (operations): Walks, Eats
class Person {
private: // attributes are generally made private
char name[]; // char array to store name
int age; // int to store age
public: // methods are generally made public
Person(); // constructor used to initialize data members
void walks(); // method walk
void eats(); // method eats
};
07.3. Abstraction
Abstraction means we only include those details in the system that are required for making a functional system. We leave out irrelevant attributes and behaviour from our objects.
Example for Student: Relevant to our problem — Name, Address. Not relevant to our problem — Sibling, Father's Business.
07.4. Defining a New User Defined Type
There are two ways to create user-defined types for objects in C++:
Structure Definition: Partially we can use structures to define an object. In C, we cannot define functions in a structure; however, in C++ we can add functions in both structures and classes.
Class Definition: Uses the keyword class (lowercase) followed by the ClassName.
class ClassName {
Access Specifier: (public, private, or protected)
DataType MemberVariable;
... .... ...
Access Specifier: (public, private, or protected)
ReturnType MemberFunction();
... .... ...
};
Example:
class Student {
private:
int rollNo;
char *name;
float CGPA;
char *address;
public:
void setName(char *newName);
void setRollNo(int newRollNo);
};
Why Member Functions: They model the behaviours of an object. Objects can make their data invisible (in accordance with the principle of data hiding). Setters and getters functions are provided by the class to access its members. This minimizes the chances of moving the objects into an inconsistent state because we can write checks in our setter functions. For example, we can check that whether the user has entered a correct age value and has not entered a negative value for age.
💡 Why this matters: Using setters with validation keeps the object in a consistent state.
Student aStudent;
aStudent.rollNo = 514;
aStudent.rollNo = -514; // Error: this would put object in inconsistent state if allowed
07.5. Object and Class
An object is an instantiation of a user-defined type or class. Once we have defined a class, we can create as many objects for that class as we require.
Declaring class variables: Variables of classes (objects) are declared just like variables of structures and built-in data types.
TypeName VariableName;
int var; // declaring built-in int data type variable
Student aStudent; // declaring user-defined class Student object
07.6. Accessing members
Members of an object can be accessed using:
a. dot operator (.) — to access via the variable name:
Student aStudent; // declaring Student object
aStudent.rollNo = 5;
b. arrow operator (->) — to access via a pointer to an object:
Student * aStudent = new Student(); // declaring and initializing Student pointer
aStudent->rollNo = 5;
Note: It is against the principle of OOP to access the data members directly using an object of a class as we have done above. This code is given for example only. We should write accessor functions (setters and getters) wherever we want to access the members of the class.
Member functions are accessed in the same way using dot or arrow operator.
class Student {
int rollNo;
void setRollNo(int aNo);
};
Student aStudent;
aStudent.setRollNo(5);
Student *ptr_student = new Student();
ptr_student->setRollNo(5);
07.7. Access specifiers
Access specifiers are used to enforce access restrictions to members of a class. There are three access specifiers:
public: Used to tell that a member can be accessed whenever you have access to the object.private: Used to tell that a member can only be accessed from a member function of the same class.protected: To be discussed when we cover inheritance.
Example Program:
class Student {
char * name;
int rollNo;
public:
void setName(char *);
void setRollNo(int aNo);
};
void Student::setName(char * aName) {
if (strlen(aName) > 0) {
name = new char[strlen(aName)];
strcpy(name, aName);
}
}
void Student::setRollNo(int arollNo) {
if (arollNo > 0)
rollNo = arollNo;
}
int main() {
Student aStudent;
aStudent.rollNo = 5;
/* Error: we can not access private member of the class. */
aStudent.name = "Ali";
/* Error: we can not access private member of the class */
aStudent.setRollNo(1);
aStudent.setName("Ali");
/* Correct way to access the data member using public setter functions */
}
Default access specifier: When no access specifier is mentioned, then the default access specifier is private.
Example: The following two class definitions are equivalent:
class Student {
char * name;
int RollNo;
};
// Is equivalent to:
class Student {
private:
char * name;
int RollNo;
};
💡 Why this matters: If you forget to write public: before the methods, they will be treated as private and will not be accessible outside the class.
⭐ Key Takeaways
A class is the fundamental mechanism in C++ for implementing the concept of objects, allowing us to create user-defined types that encapsulate both data members (attributes) and member functions (behaviours). The principle of abstraction requires us to include only relevant attributes and behaviours for the system being built. Classes support data hiding by using access specifiers: public members are accessible outside the class, while private members can only be accessed through member functions (like setters and getters), with private being the default when no specifier is mentioned. Member functions, especially setters, should include validation logic to keep objects in a consistent state and prevent invalid data (like negative roll numbers). Objects are instantiated from classes and their members are accessed using the dot operator (.) for objects or the arrow operator (->) for pointers to objects, though direct access to data members violates OOP principles.
🧠 Quick Revision Questions
- What is the difference between a class and an object in C++?
- What are the three access specifiers in C++ and what does each one mean?
- What is the default access specifier in a C++ class when no specifier is mentioned?
- Why should we use setter functions instead of directly accessing data members of a class?
- Explain the two operators used to access members of a class and when each is used.
📘 Lecture 08 — Member Functions
📖 Overview: This lecture covers how to define member functions in C++ classes, including inline functions for performance optimization. It introduces constructors for object initialization, constructor overloading, default constructors, copy constructors, and the critical difference between shallow copy and deep copy when dealing with dynamic memory.
🗂️ Topics Covered
Member functions and their definition inside and outside the class. Inline functions and their optimization benefits. Constructors, their properties, and default constructors. Constructor overloading with multiple parameters and default parameter values. Copy constructors and when they are called. Shallow copy versus deep copy, including the dangling pointer problem and its solution.
📝 Lecture Summary
08.1. Member Functions
Member functions are the functions that operate on the data encapsulated in the class. Public member functions serve as the interface to the class, allowing external code to interact with the object's data through these functions.
08.2. Defining Member Functions
We can define member functions in two ways:
a. We can define member functions of the class inside the class definition.
b. We can declare member function inside the class definition and define them outside the class. In this case, class definition is added in a .h file and implementation code is added in a .cpp file.
Function definition inside the class:
🔑 Syntax:
class ClassName {
...
public:
ReturnType FunctionName() {
...
}
};
📌 Example:
class Student{
int rollNo;
public:
void setRollNo(int aRollNo){
rollNo = aRollNo;
}
};
Function definition outside the class:
🔑 Syntax: Use the scope resolution operator :: to tell the compiler which class the function belongs to.
class ClassName {
...
public:
ReturnType FunctionName();
};
ReturnType ClassName::FunctionName() {
...
}
📌 Example:
class Student{
int rollNo;
public:
void setRollNo(int aRollNo);
};
void Student::setRollNo(int aRollNo){
rollNo = aRollNo;
}
08.3. Inline Functions
Inline functions are a way used by compilers to improve efficiency of the program. When functions are declared inline, the normal process of function calling (using stack) is not followed; instead, the function code is added by the compiler at all points where these functions have been called. Normally, small size functions that need to be called many times during program execution are declared inline. Inline functions decrease code execution time because the program doesn't involve function call overhead. The keyword inline is used to request the compiler to make a function inline. However, using the inline keyword with a function does not guarantee that the function will definitely be inlined — it depends on the compiler.
📌 Example:
inline int Area(int len, int hi) {
return len * hi;
}
int main() {
cout << Area(10,20);
return 0;
}
💡 Why this matters: The functions defined inside the class are by default inline (whether we mention the keyword inline with them or not). In case we define a function outside the class, then we must use the keyword inline to make the function inline.
📌 Example — Inline inside the class (automatic):
class Student{
int rollNo;
public:
void setRollNo(int aRollNo){ // automatically inline
rollNo = aRollNo;
}
};
📌 Example — Inline outside the class (must use keyword):
class Student{
...
public:
inline void setRollNo(int aRollNo);
};
inline void Student::setRollNo(int aRollNo){
rollNo = aRollNo;
}
08.4. Constructor
A constructor is used to initialize the objects of a class. It ensures that the object is in a well-defined state at the time of creation. The constructor of a class is automatically generated by the compiler; however, we can write it ourselves as well. The constructor is automatically called when the object is created. Constructors are not usually called explicitly by us.
08.5. Constructor Properties
- A constructor is a special function having the same name as the class name
- A constructor does not have a return type (not even
void) - Constructors are commonly public members
📌 Example:
class Student{
int rollNo;
public:
Student(){ // Constructor
rollNo = 0;
}
};
int main() {
Student aStudent; // constructor is implicitly called at this point
}
08.6. Default Constructor
A default constructor is a constructor without any parameters or with all parameters having default values. If we do not define a default constructor, the compiler will generate one. The compiler-generated default constructor is called implicit, and the user-written default constructor is called explicit. The compiler-generated default constructor initializes data members to their default values.
⚠️ Important Rule: If we have given any constructor for a class (whether explicit default or with parameters), then the compiler will not create an implicit default constructor.
📌 Example — No constructor defined, compiler generates one:
class Student {
int rollNo;
char *name;
float GPA;
public:
// no constructors
};
📌 Compiler-generated implicit default constructor code:
{
rollNo = 0;
GPA = 0.0;
name = NULL;
}
08.7. Constructor Overloading
We can write constructors with parameters as well. These parameters are used to initialize the data members with user-supplied data (passed as parameters). This is known as constructor overloading — having multiple constructors with different parameter lists.
📌 Example — Student class with four constructors:
class Student{
int rollNo;
char *name;
float GPA;
public:
Student(); // explicit default constructor
Student(char * aName); // one parameter
Student(char * aName, int aRollNo); // two parameters
Student(int aRollNo, int aRollNo, float aGPA); // three parameters
};
📌 Creating objects using different constructors:
int main() {
Student student1; // default constructor
Student student2("Name"); // one parameter constructor
Student student3("Name", 1); // two parameter constructor
Student student4("Name", 1, 4.0); // three parameter constructor
}
08.8. Constructor Overloading with Default Parameters
We can use default parameter values to reduce the writing effort. In that case, we write only one constructor and it serves the purpose of all constructors.
📌 Example:
Student::Student(char * aName = NULL, int aRollNo = 0, float aGPA = 0.0) {
// initialization code
}
This single constructor is equivalent to all four separate constructors and will use default values if values are not passed as arguments while creating objects.
08.9. Copy Constructor
Copy constructors are used when:
- Initializing an object at the time of creation (creating an object with the state of a pre-existing object)
- When an object is passed by value to a function (a temporary copy of the object is created on the stack)
🔑 Syntax:
Student::Student(const Student &obj) {
/* copying values to newly created object */
rollNo = obj.rollNo;
name = obj.name;
GPA = obj.GPA;
}
📌 Example:
int main() {
Student aStudent; // default constructor called
Student bStudent = aStudent; // copy constructor called
}
void func1(Student student) { // copy constructor called to create temporary student object
...
}
int main() {
Student studentA;
func1(studentA); // copy constructor called here
}
As with the default constructor, the compiler also generates a copy constructor by itself; however, we can override that copy constructor by writing our own.
08.10. Shallow Copy
When we initialize one object with another, the compiler copies the state of one object to the other using the copy constructor by assigning data member values of the previous object to the newly created object. This kind of copying is called shallow copying.
🔑 Shallow copy using default Copy Constructor:
Student::Student(const Student &obj) {
rollNo = obj.rollNo;
name = obj.name; // copies pointer, not the data it points to
GPA = obj.GPA;
}
📌 Example:
Student studentA;
Student studentB = studentA; // Shallow copy
💡 Why this matters: Shallow copy works fine if our class does not include dynamic memory allocation. However, in case of dynamic memory allocation, it leads to the dangling pointer problem. When studentB is created as a shallow copy of studentA, both objects point to the same memory location for the name data member (if it's a char* using dynamic memory). If studentA is deleted, its destructor frees the memory. Now studentB's name pointer still points to that freed memory area — this is a dangling pointer. Also, changing studentA's name would also change studentB's name since they share the same memory.
08.11. Deep Copy
We write our own deep copy code in the copy constructor so that when we create a new object from an existing object using the copy constructor, we also allocate new dynamic memory for data members involving dynamic memory.
🔑 Deep Copy implementation:
Student::Student(const Student &obj) {
int len = strlen(obj.name);
name = new char[len+1]; // allocate new memory
strcpy(name, obj.name); // copy the actual string content
// copy rest of the data members in the same way
rollNo = obj.rollNo;
GPA = obj.GPA;
}
📌 Example — Using deep copy creates separate memory for both objects:
Student studentA;
Student studentB = studentA; // now copy constructor performs deep copy
// studentA and studentB have their own separate memory for name
Rule: In case our class does not involve dynamic memory, the default copy constructor that performs shallow copy works fine. In case our class has any data member involving dynamic memory, we must write our own code to perform deep copy.
⭐ Key Takeaways
Member functions are defined either inside the class (automatically inline) or outside using the scope resolution operator (needing the inline keyword explicitly). Inline functions improve performance by replacing function call overhead with the function's code, but the compiler decides whether to actually inline them. Constructors are special functions with the same name as the class, no return type, and are automatically called when objects are created. If no constructor is written, the compiler generates an implicit default constructor; however, if any constructor is defined, the compiler will not generate one. Constructor overloading allows flexibility in object initialization, and default parameter values can reduce the number of constructors needed. Copy constructors are used when creating objects from existing ones or passing by value, and they can perform either shallow copy (default, copies pointers) or deep copy (user-written, allocates new memory). Shallow copy causes the dangling pointer problem and unintended data sharing when dynamic memory is involved, so deep copy is essential for classes with dynamically allocated data members.
🧠 Quick Revision Questions
- What are the two ways to define member functions in C++ classes, and when does a function become automatically inline?
- What are the three key properties of a constructor, and what happens if you do not define any constructor in a class?
- When does the compiler NOT generate an implicit default constructor?
- What are the two specific situations where a copy constructor is automatically called?
- Explain the difference between shallow copy and deep copy, and describe the problem that arises when shallow copy is used with dynamically allocated data.
📘 Lecture 09 — Review
📖 Overview: This lecture covers the copy constructor in depth, including when it is called, the difference between shallow and deep copy, and the problems that arise with dynamic memory. It also reviews the destructor, its syntax and sequence of calls, accessor functions for data hiding, and the
thispointer which allows member functions to operate on the correct object.
🗂️ Topics Covered
The lecture begins with a review of copy constructors and when they are invoked. It then explains shallow copy and its pitfalls with dynamic memory, followed by deep copy as the solution. Important points about copy constructors are summarized. The destructor is covered with its syntax, limitations (no overloading), and call sequence. Finally, accessor functions (getters and setters) and the this pointer (how it works and is passed) are explained.
📝 Lecture Summary
Copy Constructor
A copy constructor is used to initialize an object at the time of creation using the state of a pre-existing object. It is also called when an object is passed by value to a function, because a temporary copy of the object is created on the stack. The compiler generates a default copy constructor, but we can override it.
🔑 Definition — Copy Constructor: A constructor that initializes an object using another object of the same class. It is called when: (1) initializing an object with another object (e.g., Student studentB = studentA;), and (2) passing an object by value to a function (e.g., func1(studentA);).
📐 Formula/Syntax: Student::Student(const Student &obj) { /* copy data members */ } → The copy constructor takes a const reference to the source object to avoid modification and infinite recursion.
📌 Example: Student studentA; Student studentB = studentA; → Copy constructor is called to create studentB from studentA. Also, void func1(Student student) → when called as func1(studentA), the copy constructor creates the temporary student parameter.
09.1. Shallow Copy
Shallow copy is the default copying mechanism provided by the compiler. It copies the values of data members directly from the source object to the new object. This works fine when the class does not involve dynamic memory allocation. However, when a data member is a pointer to dynamically allocated memory, shallow copy causes both objects to point to the same memory location. This leads to two major problems: dangling pointers (when one object is destroyed, the other's pointer becomes invalid) and unintended data modification (changing the value via one object affects the other).
🔑 Definition — Shallow Copy: A bitwise copy of an object where the data member values of the source object are simply assigned to the new object's data members.
📌 Example (Problem):
Student studentA("AHMAD",1); // name points to heap memory containing "AHMAD"
Student studentB = studentA; // Shallow copy: studentB.name = studentA.name (same address)
// If studentA is deleted, studentB.name becomes a dangling pointer.
💡 Why this matters: Shallow copy with dynamic memory leads to undefined behavior because two objects share the same memory, and the destructor of one will free the memory while the other still points to it.
09.2. Deep Copy
Deep copy solves the problems of shallow copy by writing custom code in the copy constructor that allocates new dynamic memory for pointer data members of the new object and then copies the content (not just the address). This ensures each object has its own independent copy of the data.
🔑 Definition — Deep Copy: A copy operation that allocates separate memory for dynamically allocated data members in the new object and copies the actual data (e.g., using strcpy) rather than just the pointer address.
📐 Formula/Syntax:
Student::Student(const Student &obj) {
int len = strlen(obj.name);
name = new char[len+1]; // allocate new memory
strcpy(name, obj.name); // copy the content
rollNo = obj.rollNo;
}
📌 Example: With deep copy, Student studentB = studentA; now creates studentB.name as a separate block of memory containing "AHMAD". Deletion of studentA does not affect studentB, and vice versa.
09.3. Important points about copy constructor:
- If the class does not involve dynamic memory, the default copy constructor (shallow copy) works fine.
- If the class has any data member involving dynamic memory, we must write our own copy constructor to perform deep copy.
- Copy constructor is normally used to perform deep copy.
- If we do not make a copy constructor, the compiler performs shallow copy.
- Shallow copy performs bitwise copy.
09.4. Destructor
A destructor is a special member function used to free dynamically allocated memory and perform housekeeping operations when an object is destroyed. It has the same name as the class, preceded by a tilde ~. Destructors cannot be overloaded. Constructors and destructors are called automatically: constructors are called in the order objects are declared, while destructors are called in the reverse order.
🔑 Definition — Destructor: A function with the same name as the class, preceded by ~, that is automatically called when an object goes out of scope. It is used to release resources (e.g., memory allocated with new).
📐 Syntax: ~Student() { if(name) { delete []name; } }
📌 Example (Sequence):
int main() {
Student studentB("Ali"); // "Ali Constructor" printed
Student studentA("Ahmad"); // "Ahmad Constructor" printed
return 0;
}
// Output:
// Ali Constructor
// Ahmad Constructor
// Ahmad Destructor
// Ali Destructor
Destructors are called in reverse order of declaration.
09.5. Accessor Functions
According to the principle of information hiding, data members are declared as private. Accessor functions provide a controlled interface to get (getter) and set (setter) the private data. We add error checking code in setter functions to prevent the object from entering an illegal state. A good practice is to never return a handle (reference or pointer) to a data member from a getter, because the caller could modify the private data.
🔑 Definition — Accessor Functions: Public member functions that allow safe access to private data members. Setters modify values (with validation), and getters return values (by value, not by reference).
📌 Example (Setter with validation):
void Student::setRollNo(int aRollNo) {
if(aRollNo < 0) { rollNo = 0; } // error checking
else { rollNo = aRollNo; }
}
📌 Example (Getter):
int getRollNo() { return rollNo; } // returns a copy, not a reference
09.6. this Pointer
When a class is defined, the compiler reserves space for member functions, but memory for data members is allocated only when objects are created. The this pointer is an implicit parameter passed to every non-static member function. It holds the address of the object for which the function was called. This allows the function to access the correct object's data members. The function with n parameters is internally called with n+1 parameters, where the extra parameter is this. The declaration of this is DataType * const this; (the pointer itself is constant, you cannot change its address).
🔑 Definition — this Pointer: A constant pointer that holds the memory address of the current object. It is automatically passed to all non-static member functions.
📌 Example:
Student::Student() { this->rollNo = 0; } // equivalent to rollNo = 0;
And void Student::setName(char *) is internally void Student::setName(char *, const Student *) where the second argument is this.
📌 Memory Layout: Multiple objects (e.g., s1, s2, s3) each have their own data memory. The function code is shared. The this pointer ensures that when s1.setRollNo(5) is called, the function acts on s1's data, not s2's.
⭐ Key Takeaways
For the exam, you must understand when a copy constructor is called and the critical difference between shallow and deep copy. Remember that shallow copy (the default) is only safe for classes without dynamic memory; otherwise, it causes dangling pointers. Deep copy must be explicitly written in the copy constructor to allocate new memory for pointers. Destructors cannot be overloaded and are called in reverse order of construction. Accessor functions (getters/setters) enforce data hiding, and setters should include validation. Finally, the this pointer is an implicit parameter that allows member functions to operate on the correct object; it is always the address of the current object.
🧠 Quick Revision Questions
- What are the two situations where a copy constructor is called?
- What is the core difference between shallow copy and deep copy?
- In the example with
Student studentA("AHMAD"); Student studentB = studentA;(using shallow copy), what happens whenstudentA's destructor runs? - Why can destructors not be overloaded, and in what order are destructors called for objects declared in the same scope?
- Explain why it is considered bad practice to return a reference (handle) to a data member from a getter function.
📘 Lecture 10 — Uses of this Pointer and const Member Functions
📖 Overview: This lecture explores two important C++ concepts: practical uses of the
thispointer, particularly for returning references to the current object, and the separation of interface from implementation in object-oriented programming. It also introduces constant member functions, explaining how they enforce read-only access and prevent accidental modification of object data.
🗂️ Topics Covered
The lecture covers uses of the this pointer for returning *this from functions to enable chaining, the separation of interface and implementation in C++ using header (.h) and implementation (.cpp) files, the Complex number class as an example of interface/implementation separation, constant member functions declared with the const keyword to ensure read-only access, and how the this pointer behaves in constant member functions.
📝 Lecture Summary
10.1. Uses of this Pointer
The this pointer is used when a designer wants to return a reference to the current object from a function. In such cases, the reference is obtained from this using the syntax *this. This is particularly useful for function chaining, where multiple functions can be called sequentially on the same object.
🔑 Definition — this pointer: A special implicit pointer in C++ that points to the object for which a member function is called. It is automatically passed to all non-static member functions. 📌 Example:
class Student {
int rollNo;
char name[50];
public:
Student setRollNo(int aNo) {
rollNo = aNo;
return *this; // returns reference to current object
}
Student setName(char *aName) {
strcpy(name, aName);
return *this; // returns reference to current object
}
};
int main() {
Student aStudent, bStudent;
bStudent = aStudent.setName("Ahmad");
// Chaining example:
bStudent = aStudent.setName("Ali").setRollNo(2);
return 0;
}
💡 Why this matters: The *this pattern enables method chaining, allowing multiple operations to be written concisely in a single line.
10.2. Separation of Interface and Implementation
Public member functions exposed by a class are called its interface. Separation of implementation from the interface is good software engineering practice. The benefit is that we can easily change implementation without changing the interface.
In C++, we generally relate the concept of interface to the header (.h) file and implementation to the .cpp file. Functions are defined in the implementation file (.cpp) while the class definition is given in the header file (.h). This allows users to include only the header file to use the class.
Example of file structure:
// Student.h (interface)
class Student {
int rollNo;
public:
void setRollNo(int aRollNo);
int getRollNo();
};
// Student.cpp (implementation)
#include "student.h"
void Student::setRollNo(int aNo) {
// implementation code
}
int Student::getRollNo() {
// implementation code
}
// Main.cpp (using the class)
#include "student.h"
int main() {
Student aStudent;
return 0;
}
10.3. Complex Number
A complex number is represented as z = x + i y and has two representations:
- Euler form: Standard Cartesian representation with real part (x) and imaginary part (y)
- Phasor form:
z = |z| (cos θ + i sin θ)where|z|is the complex modulus andθis the complex argument or phase
🔑 Definition — UML Notation: A standard way to visualize class structure, showing private data members and public member functions. 📌 Example of two implementations:
Old implementation (Cartesian form):
class Complex { // old
float x; // real part
float y; // imaginary part
public:
void setNumber(float i, float j) {
x = i;
y = j;
}
};
UML: Complex { -x: float; -y: float; +getX(): float; +getY(): float; +setNumber(float i, float j): void }
New implementation (Polar form):
class Complex { // new
float z; // magnitude
float theta; // angle
public:
void setNumber(float i, float j) {
z = sqrt(i*i + j*j);
theta = arctan(j/i);
}
};
UML: Complex { -z: float; -theta: float; +getX(): float; +getY(): float; +setNumber(float i, float j): void }
Advantages of separation:
- User is only concerned about ways of accessing data (interface)
- User has no concern about internal representation and implementation
10.4. const Member Functions
Constant member functions are functions that provide only read-only access to data. They are declared by placing the keyword const at the end of the parameter list. The compiler generates an error if these functions try to change the value of data members.
🔑 Definition — const member function: A member function that cannot modify the state of any object; it is a "read-only" function.
Declaration syntax:
// Inside class:
class ClassName {
ReturnVal Function() const;
};
// Outside class definition:
ReturnVal ClassName::Function() const {
// code
}
📌 Example of correct usage:
class Student {
int rollNo;
public:
int getRollNo() const {
return rollNo; // OK - read-only access
}
};
Importance for error detection: Without const, a common typing mistake (using = instead of ==) can go undetected:
// Without const - buggy code that compiles
bool Student::isRollNo(int aNo) {
if(rollNo = aNo) { // assignment instead of comparison!
return true;
}
return false;
}
// With const - compiler catches the error
bool Student::isRollNo(int aNo) const {
if(rollNo = aNo) { // COMPILER ERROR - cannot modify data
return true;
}
return false;
}
Important rules:
- Constructors and destructors cannot be const because they modify the object to a well-defined state or clean up memory.
class Time { public: Time() const {} // error! ~Time() const {} // error! }; - Constant member functions cannot change data members.
- We cannot call non-constant functions inside constant functions, because non-constant functions may contain code that changes the object's state.
class Student { char *name; public: char *getName(); void setName(char *aName); int ConstFunc() const { name = getName(); // error - getName() is non-const setName("Ahmad"); // error - setName() is non-const } };
10.5. this Pointer and const Member Function
When a class function is called, an implicit this pointer is passed to tell the function which object it operates on. For constant member functions, the this pointer is passed differently:
🔑 Definition — const this pointer in const functions: const Student *const this instead of Student *const this (for ordinary functions). This means the this pointer cannot be used to change the value of data members of the object.
📌 Key difference:
// For ordinary member functions:
Student * const this; // pointer is constant, but object is not
// For constant member functions:
const Student * const this; // both pointer and object are constant
💡 Why this matters: The const version of this ensures that all member access via this is read-only, preventing accidental modification of object state even when using the this pointer directly.
⭐ Key Takeaways
For the exam, you must understand that the this pointer enables method chaining through *this, and that separating interface (.h files) from implementation (.cpp files) is a fundamental software engineering principle that allows changing internal representation without affecting users. Constant member functions, marked with const after the parameter list, enforce read-only access and catch assignment-instead-of-comparison bugs at compile time. Constructors and destructors cannot be const. Non-constant functions cannot be called from within constant functions. Finally, in constant member functions, the implicit this pointer becomes const ClassName * const this, preventing any modification of object data through it.
🧠 Quick Revision Questions
- What is the purpose of returning
*thisfrom a member function, and what pattern does it enable? - How does separating interface from implementation benefit the user of a class, as shown with the Complex number example?
- What is the correct syntax for declaring and defining a constant member function?
- Why can't constructors and destructors be declared as
const? - What is the difference between the
thispointer in an ordinary member function versus a constant member function?
📘 Lecture 11 — Usage Example of Constant Member Functions & Static Data Members
📖 Overview: This lecture covers two crucial C++ topics: how to properly initialize constant data members using member initializer lists, and the concept of static data members that are shared across all class instances. Understanding these mechanisms is essential for writing robust, memory-efficient object-oriented programs.
🗂️ Topics Covered
The lecture begins with a problem scenario requiring a constant roll number in a Student class, then explains the difference between initialization and assignment. It introduces member initializer lists as the solution for initializing constant members. The lecture then covers constant objects and their restriction to constant member functions, followed by a detailed explanation of static variables, static data members, their syntax, definition, initialization, and how they differ from instance variables.
📝 Lecture Summary
11.1. Usage example of Constant member functions
A problem is presented: we need a Student class where a student receives a roll number at object creation that cannot be changed afterwards. The existing class has a simple int rollNo data member. Simply making rollNo constant (const int rollNo) prevents modification, but creates an initialization dilemma — we cannot assign a value to rollNo in the constructor body because by the time constructor code executes, the constant member already exists and cannot be assigned.
🔑 Definition — Constant data member (initialization problem): A data member declared with const cannot be assigned a value after it is created. This means neither declaration-time initialization (not allowed for non-static members in C++) nor constructor body assignment works.
📌 Example (error):
class Student {
const int rollNo;
public:
Student(int aRollNo) {
rollNo = aRollNo; // ERROR: cannot modify constant data member
}
};
💡 Why this matters: Without proper initialization, constant members would be unusable. C++ provides a special syntax to solve this.
11.2. Difference between Initialization and Assignment
- Initialization: Assigning a value when a variable is created.
int i = 2;— memory is allocated and the value is set in one step.
- Assignment: Assigning a value after the variable has been created.
int i;theni = 7;— two separate steps: creation, then value assignment.
🔑 Key insight: Constant members can be initialized (at creation) but cannot be assigned (after creation).
11.3. Member Initializer List
The member initializer list is the mechanism C++ provides to initialize data members at the moment they are created, before the constructor body executes.
- It appears after the closing parenthesis of the constructor's parameter list, preceded by a colon (
:). - For multiple members, use a comma-separated list.
- The syntax is:
ConstructorName(parameters) : member1(value1), member2(value2), ... { ... }
📐 Syntax: ClassName::ClassName(parameters) : member1(value1), member2(value2), ... { constructor body }
📌 Example (Student class solution):
class Student {
const int rollNo;
char *name;
float GPA;
public:
Student(int aRollNo) : rollNo(aRollNo), name(nullptr), GPA(0.0) {
// constructor body — rollNo already initialized
}
};
Order of Initialization: Data members are initialized in the order they are declared in the class, not in the order they appear in the initializer list.
📌 Example demonstrating order:
class ABC {
int x; // declared first
int y; // declared second
int z; // declared third
public:
ABC();
};
ABC::ABC() : y(10), x(y), z(y) {
// Initialization order: x first (junk value, because y not yet initialized)
// y second (10)
// z third (10)
}
Here, x gets a junk value because it is declared before y, even though x(y) appears before y(10) in the initializer list.
11.4. const Objects
Objects can be declared constant using the const keyword. Constant objects cannot change their state (their data members) after construction.
- A
constobject can only call const member functions — functions guaranteed not to modify the object. - Non-const member functions cannot be called on const objects (even if they don't actually modify state).
🔑 Key rule: Make all member functions that do not change object state const by adding the const keyword after the function parameter list.
📌 Example (correct):
class Student {
int rollNo;
public:
int getRollNo() const { // const member function
return rollNo;
}
};
int main() {
const Student aStudent(5);
int a = aStudent.getRollNo(); // OK: getRollNo is const
return 0;
}
📌 Example (incorrect):
class Student {
int rollNo;
public:
int getRollNo() { // non-const member function
return rollNo;
}
};
int main() {
const Student aStudent(5);
int a = aStudent.getRollNo(); // ERROR: cannot call non-const function on const object
return 0;
}
11.5. Static Variables
Static variables inside a function have lifetime throughout the entire program, but are only visible within the function where they are declared.
- They are initialized only once, the first time the function is called.
- If not explicitly initialized, they are automatically initialized to 0.
📌 Example:
void func1(int i) {
static int staticInt = i; // initialized only on first call
cout << staticInt << endl;
}
int main() {
func1(1); // Output: 1 (staticInt initialized to 1)
func1(2); // Output: 1 (staticInt NOT re-initialized; still 1)
return 0;
}
Contrast with assignment version:
void func1(int i) {
static int staticInt; // initialized to 0 only once
staticInt = i; // assignment happens every call
cout << staticInt << endl;
}
int main() {
func1(1); // Output: 1
func1(2); // Output: 2
return 0;
}
Static Data Member
🔑 Definition — Static data member: “A variable that is part of a class, yet is not part of any object of that class.”
Key characteristics:
- Shared by all instances (objects) of the class — one copy exists regardless of how many objects are created.
- Does not belong to any particular instance — it belongs to the class itself.
- Stored in class space (global/data segment), not in individual object memory.
Syntax:
- Declared inside the class with
statickeyword:class ClassName { static DataType VariableName; }; - Defined outside the class (at file scope):
DataType ClassName::VariableName;
Initialization:
- Static data members should be initialized once at file scope (at the point of definition).
- If not explicitly initialized, they are initialized to 0.
- Even private static members can be initialized at file scope using the
ClassName::syntax.
📌 Example:
class Student {
private:
static int noOfStudents; // declaration inside class
public:
// ...
};
int Student::noOfStudents = 0; // definition and initialization at file scope
// Note: private static member can be accessed outside class only for initialization
⭐ Key Takeaways
- Constant data members (
constmembers) cannot be assigned in the constructor body or anywhere else — they must be initialized using a member initializer list, which runs before the constructor body executes. - Member initializer list initialization order follows the declaration order of data members in the class, not the order in the list — this can lead to subtle bugs if not understood.
- Constant objects can only call const member functions; to make a function const, add the
constkeyword after its parameter list. All getter functions that don't modify state should be declared const. - Static variables inside functions are initialized only once and retain their value across function calls. Static data members belong to the class (not objects), are shared by all instances, and must be defined and optionally initialized outside the class.
- The difference between initialization (value set at creation time) and assignment (value set after creation) is fundamental to understanding when member initializer lists are required versus optional.
🧠 Quick Revision Questions
- Why can't you initialize a
constdata member by assigning a value to it inside the constructor body? - What is the syntax for a member initializer list, and how do you initialize multiple data members?
- If a class declares
int x; int y;and the constructor uses the initializer list: y(5), x(y), what value doesxget? Why? - What error occurs when a
constobject tries to call a non-const member function, and how do you fix it? - Where must a static data member be defined (as opposed to declared), and what happens if no explicit initial value is given?
📘 Lecture 12 — Review (Static Data Members and More)
📖 Overview: This lecture reviews the concept of static data members in C++, including their definition, memory allocation, initialization, and access patterns. It also covers static member functions, the
thispointer's relationship with static members, and introduces arrays of objects, which are foundational for managing multiple class instances efficiently.
🗂️ Topics Covered
The lecture covers the definition and syntax of static data members, their allocation and initialization at file scope, two ways to access them (dot operator vs. scope resolution), their lifetime independent of objects, practical uses like counting objects, the problem of public accessibility leading to static member functions, restrictions on static functions (no access to non-static members, no this pointer), comparison with global variables, and finally the rules for creating arrays of objects (requiring a default constructor or explicit initialization).
📝 Lecture Summary
Review: Static Data Member
A static data member is defined as "a variable that is part of a class, yet is not part of an object of that class." Static data members are shared by all instances of a class and do not belong to any particular instance.
Memory for static variables is allocated in class space, whereas for instance variables, memory is separate for each object. For example, with class Student containing static int noOfStudents;, creating objects s1, s2, s3 results in noOfStudents residing once in the class space, while instance-specific data like rollNo is stored separately per object.
Static Data Member (Syntax): The keyword static is used to declare a static data member inside the class:
class ClassName{
...
static DataType VariableName;
};
Defining Static Data Member (allocating memory): The static data member is declared inside the class but must be defined outside the class:
DataType ClassName::VariableName;
Initializing Static Data Member: Static data members should be initialized once at file scope, at the time of definition:
int Student::noOfStudents = 0;
Note: A private static member cannot be accessed outside the class except for this initialization.
If static data members are not explicitly initialized at definition, they are automatically initialized to 0:
int Student::noOfStudents; // equivalent to int Student::noOfStudents = 0;
🔑 Definition — Static Data Member: A class variable shared by all objects of that class, declared with the static keyword, defined outside the class, and initialized once at file scope (defaults to 0).
12.1. Accessing Static Data Member
There are two ways to access a static data member:
- Access like a normal data member (using the dot operator
.) - Access using the scope resolution operator
::
Example:
class Student{
public:
static int noOfStudents;
};
int Student::noOfStudents;
int main(){
Student aStudent;
aStudent.noOfStudents = 1; // via dot operator
Student::noOfStudents = 1; // via scope resolution
return 0;
}
12.2. Life of Static Data Member
- Static data members are created even when there is no object of a class.
- They remain in memory even when all objects of a class are destroyed.
Example (alive without objects):
class Student{
public:
static int noOfStudents;
};
int Student::noOfStudents;
int main(){
Student::noOfStudents = 1; // Works even with no objects created
}
Example (persist after objects destroyed):
class Student{
public:
static int noOfStudents;
};
int Student::noOfStudents;
int main(){
{
Student aStudent;
aStudent.noOfStudents = 1;
} // aStudent destroyed here, but...
Student::noOfStudents = 1; // static member still accessible
return 0;
}
Uses: They can store information required by all objects, like global variables but scoped to the class.
Practical Example — Counting Objects:
class Student{
...
public:
static int noOfStudents;
Student();
~Student();
...
};
int Student::noOfStudents = 0;
Student::Student(){
noOfStudents++;
}
Student::~Student(){
noOfStudents--;
}
int main(){
cout << Student::noOfStudents << endl; // Output: 0
Student studentA;
cout << Student::noOfStudents << endl; // Output: 1
Student studentB;
cout << Student::noOfStudents << endl; // Output: 2
return 0;
}
Problem: In this example, noOfStudents is accessible outside the class, which is a bad design because the local data member is kept public.
💡 Why this matters: Encapsulation is violated when static data members are public. The solution is to make them private and provide static member functions for controlled access.
12.3. Static Member Function
Definition: "The function that needs access to the members of a class, yet does not need to be invoked by a particular object, is called static member function."
- They are used to access static data members.
- Access mechanism for static member functions is the same as for static data members (via class name and
::or via a specific object). - They cannot access any non-static members.
Example:
class Student{
static int noOfStudents;
int rollNo;
public:
static int getTotalStudent(){
return noOfStudents;
}
};
int main(){
int i = Student::getTotalStudents(); // OK — accesses static member
return 0;
}
Error Example — Accessing non-static member from static function:
int Student::getTotalStudents(){
return rollNo; // Error: There is no instance of Student, rollNo cannot be accessed
}
🔑 Definition — Static Member Function: A class-level function declared with static that can only access static data members and other static member functions, called without an object instance.
12.4. this Pointer and Static Member Functions
- The
thispointer is passed implicitly to non-static member functions. - The
thispointer is NOT passed to static member functions. - Reason: Static member functions cannot access non-static data members, so there is no need for a pointer to a specific object.
12.5. Global Variable vs. Static Members
- An alternative to static member is to use a global variable.
- Global variables are accessible to all entities of the program.
- Use of global variables is against the principle of information hiding.
💡 Why this matters: Static members provide the benefits of global data (shared state) while maintaining encapsulation within the class, unlike global variables.
12.6. Array of Objects
- An array of objects can only be created if an object can be created without supplying an explicit initializer.
- There must always be a default constructor if we want to create an array of objects (unless explicitly initialized).
Example — OK (default constructor exists):
class Test{
public:
};
int main(){
Test array[2]; // OK — compiler provides default constructor
return 0;
}
Example — OK (explicit default constructor):
class Test{
public:
Test();
};
int main(){
Test array[2]; // OK
return 0;
}
Example — Error (no default constructor):
class Test{
public:
Test(int i);
};
int main(){
Test array[2]; // Error — no default constructor
return 0;
}
Solution with explicit initialization:
class Test{
public:
Test(int i);
};
int main(){
Test array[2] = {Test(0), Test(0)}; // OK — explicitly initialized
return 0;
}
Alternative with named objects:
class Test{
public:
Test(int i);
};
int main(){
Test a(1), b(2);
Test array[2] = {a, b}; // OK — copy initialization
return 0;
}
🔑 Key Rule: To create an array of objects, either provide a default constructor or initialize each array element explicitly.
⭐ Key Takeaways
Static data members are class-level variables shared across all objects, stored in class space, and persist even when no objects exist. They must be declared inside the class with static and defined outside using scope resolution, defaulting to zero if not explicitly initialized. Static member functions provide controlled access to these members but cannot access non-static data or use the this pointer. Arrays of objects require a default constructor or explicit initialization for each element; without one, compilation fails. These concepts are fundamental for managing shared state and object collections in object-oriented design.
🧠 Quick Revision Questions
- What is the difference in memory allocation between a static data member and an instance data member?
- How do you properly declare, define, and initialize a private static data member in C++?
- Why can't a static member function access a non-static data member like
rollNo? - What happens to a static data member when all objects of its class are destroyed?
- What must be true about a class to create an array of its objects, and what is the alternative if that condition is not met?
📘 Lecture 13 — Pointer to Objects and Date Class
📖 Overview: This lecture covers pointer to objects, dynamic allocation of objects using the
newoperator, and a comprehensive case study on designing aDateclass. It demonstrates how to combine pointer concepts, static members, and proper class design in a practical application.
🗂️ Topics Covered
Pointer to objects and their similarity to pointers to built-in types, using new operator for dynamic object allocation, breakup of new operation into memory allocation and constructor calling, and a complete case study designing a Date class with static default date, getters/setters, and date manipulation functions including leap year validation.
📝 Lecture Summary
13.1. Pointer to Objects
Pointers to objects work similarly to pointers to built-in types. They store the memory address of an object and can be used to access object members using the arrow operator (->). The new operator can be used to dynamically allocate objects at runtime.
🔑 Definition — Pointer to object: A variable that stores the memory address of an object, allowing indirect access to the object's members.
📌 Example 1: Basic pointer to object
Student obj;
Student *ptr;
ptr = &obj;
ptr->setRollNo(10);
📌 Example 2: Dynamic allocation with new
Student *ptr;
ptr = new Student; // allocates memory and calls default constructor
ptr->setRollNo(10);
📌 Example 3: Dynamic allocation with parameterized constructor
Student *ptr;
ptr = new Student("Ali"); // calls constructor with argument
ptr->setRollNo(10);
📌 Example 4: Dynamic array of objects
Student *ptr = new Student[100]; // allocates array of 100 Student objects
for(int i = 0; i < 100; i++) {
ptr->setRollNo(10);
}
13.2. Breakup of new Operation
The new operator is decomposed into two steps:
- Allocating space in memory for the object
- Calling the appropriate constructor to initialize the object
13.3. Case Study: Date Class Design
Design a class Date that allows users to:
- Get and set current day, month, and year
- Increment by x number of days, months, and years
- Set default date
Attributes:
day(int)month(int)year(int)defaultDate(static Date) — shared by all objects
class Date {
int day;
int month;
int year;
static Date defaultDate; // static member shared by all instances
...
};
Interfaces:
getDay,getMonth,getYearsetDay,setMonth,setYearaddDay,addMonth,addYearsetDefaultDate— must be static since it modifies the static member
Constructors and Destructors:
Date(int aDay = 0, int aMonth = 0, int aYear = 0);
~Date(); // Destructor
Implementation of Date Class
Static member initialization:
The static member variable defaultDate must be initialized outside the class definition.
Date Date::defaultDate(07, 3, 2005); // initializes static default date
Constructor implementation:
If any parameter is 0, it uses the corresponding value from defaultDate.
Date::Date(int aDay, int aMonth, int aYear) {
if(aDay == 0) {
this->day = defaultDate.day; // use default date's day
} else {
setDay(aDay);
}
// similarly for month and year
}
Destructor: No housekeeping required in this case.
Date::~Date() { }
Getter and Setter examples:
void Date::setMonth(int a) {
if(a > 0 && a <= 12) {
month = a;
}
}
int Date::getMonth() const {
return month;
}
addYear method: When adding years, if the date is Feb 29 and the new year is not a leap year, adjust to March 1.
void Date::addYear(int x) {
year += x;
if(day == 29 && month == 2 && !leapYear(year)) {
day = 1;
month = 3; // adjust to March 1
}
}
Helper function — leapYear:
bool Date::leapYear(int x) const {
if((x % 4 == 0 && x % 100 != 0) || (x % 400 == 0)) {
return true;
}
return false;
}
💡 Why this matters: A year is a leap year if divisible by 4 but not by 100, OR divisible by 400.
setDefaultDate:
void Date::setDefaultDate(int d, int m, int y) {
if(d >= 0 && d <= 31) {
day = d;
}
// ... similar validation for month and year
}
13.4. Complete Code of Date Class
The complete implementation includes:
- Private data members:
day,month,year, and staticdefaultDate - Public setters/getters for all attributes
addDay,addMonth,addYearmethodssetDefaultDatestatic methodsetDatemethod that sets all three values at once- Constructor with default parameters using 0 to trigger default date usage
- Destructor that prints "Date destructor"
int main() {
Date aDate(0, 0, 0); // uses default date values
aDate.setDate(20, 10, 2011);
system("pause");
}
⭐ Key Takeaways
Pointers to objects work exactly like pointers to built-in types, using the arrow operator -> for member access. The new operator performs two operations: memory allocation and constructor calling. The Date class case study demonstrates how to design a class with static members shared across all instances, proper validation in setters, and the special handling of February 29 when adding years to avoid invalid dates. Static members must be initialized outside the class definition. Constructor parameters set to 0 can trigger the use of default date values, providing flexibility in object creation.
🧠 Quick Revision Questions
- What two operations does the
newoperator perform when creating an object? - Why must the
setDefaultDatemethod be declared asstaticin the Date class? - How does the
addYearmethod handle the case where the current date is February 29 and the new year is not a leap year? - What is the correct way to initialize a static data member of a class?
- In the Date constructor, what happens if a parameter value is 0?
📘 Lecture 14 — Composition
📖 Overview: This lecture introduces the concept of composition in object-oriented programming, where one class contains objects of another class as members, establishing a "has-a" relationship. It demonstrates how composition simplifies code by reusing existing classes, using the example of a
Studentclass that now contains aStringobject instead of managing raw character pointers directly. The lecture also covers the order of constructor and destructor calls in composed objects.
🗂️ Topics Covered
The lecture covers the definition and concept of composition as a "part-whole" relationship, the implementation details of a String class used for composition, the revised Student class that incorporates this String object, how to access methods of composed objects, and the important sequence of constructor and destructor calls between composing and composed objects.
📝 Lecture Summary
14.1. Composition
Composition is a concept where one object is part of another object, establishing a relationship of part and whole. In this relationship, the lifetime of one object depends upon the other, and the part objects are essential components of the whole. For example, a person is composed of hands, eyes, and feet.
The lecture starts with the original Student class implementation from previous lectures that managed a char * name pointer with dynamic memory allocation. The class had private members float gpa, char * name, and int rollNumber, with constructors handling deep copying for the name string.
The instructor introduces composition by stating that in C++, "it is all about code reuse." Composition is defined as creating objects of one class inside another class, representing a "Has a" relationship — for example, "Bird has a beak" or "Student has a name."
The code is modified to replace the char * name with a String object, as it qualifies to be an object because many operations need to be applied to it, such as dynamic creation and deletion, string copy using deep copy, and searching a substring.
String Class Implementation
The String class is presented to show how it simplifies the original Student object and how composition is used.
class String{
private:
char * ptr;
public:
String(); // default constructor
String(const String &); // copy constructor
void SetString(const char *); // setter function
const char * GetString() const; // getter function returning const pointer
~String()
...
};
The String class has a private data member char * ptr. Its default constructor initializes ptr to NULL. The copy constructor performs deep copying by checking if the source's ptr is not NULL, then allocating new memory and copying the string.
The SetString function handles two important issues:
- Memory leakage: If we simply set the pointer, memory will be outside of our object and may cause problems later.
- The user still has a pointer to the passed value and can modify it.
These issues are resolved by allocating new memory and deleting previous memory in SetString. The function checks if ptr is not NULL, deletes it, and sets it to NULL. Then, if the input string is not NULL, it allocates new memory and copies the string.
The GetString function returns ptr as a const char *, ensuring the private data member cannot be modified through the returned pointer.
The destructor deletes the dynamically allocated memory for ptr and displays a message. The instructor notes that any time dynamic memory is deleted, the pointer should also be set to NULL.
Revised Student Class with Composition
The Student class is now rewritten to include a composed String object instead of managing raw character pointers.
class Student{
private:
float gpa;
int rollNumber;
String name;
public:
Student(char* =NULL, int=0, float=0.0);
Student(const Student &);
void SetName(const char *);
String GetName() const;
const char * GetNamePtr() const;
~Student();
...
};
The constructor uses name.SetString(_name) to set the composed object's value. The copy constructor uses a single line: name.SetString(s.name.GetString()); which is explained step by step:
name.SetString(...)— setting composed name of newly created objects.name.GetString()— accessing the composed object string name of object to be copieds.name.GetString()— accessing the value of composed object string name by calling its member functionGetString- Overall result: the value of composed object string of object to be copied will be copied to newly created object's composed object string
The GetNamePtr function returns name.GetString(), and SetName calls name.SetString(n). The destructor simply displays a message; it does not need to manually delete memory because the String destructor handles that automatically.
Important Points about Composition
- Methods of composed objects can be accessed in the same way as methods of other objects, using the syntax:
Name of composed object.MemberFunction - Member functions of a class can access its private data members. For example, in the copy constructor,
gpa = s.gpaandrollNo = s.rollNodirectly access private members of thesobject because the code is within aStudentmember function.
Constructors & Composition
Constructors of the sub-objects are always executed before the constructors of the master class. The output from the example program demonstrates this order:
Constructor::String..
Constructor::Student..
Name: Fakhir
Destructor::Student..
Destructor::String..
🔑 Definition — Composition: A relationship where one object is part of another object, and the lifetime of one depends on the other. The part objects are essential components of the whole.
💡 Why this matters: Understanding composition allows you to build complex objects from simpler, reusable components, promoting code reuse and simplifying memory management.
📌 Example: The Student class contains a String name object. When a Student is created, first the String constructor runs (creating the name part), then the Student constructor runs (creating the whole). When a Student is destroyed, the Student destructor runs first, then the String destructor runs.
Constructor calling order: Constructors are called from composed objects to composing objects (String first, then Student).
Destructor calling order: Destructors are called from composing objects to composed objects (Student first, then String).
⭐ Key Takeaways
Composition establishes a "has-a" relationship where one class contains objects of other classes as members, with the composed object's lifetime depending on the composing object. Constructors are called bottom-up (composed first, composing second), while destructors are called top-down (composing first, composed second). Using composition simplifies code by delegating memory management and operations to the composed class, eliminating the need for the containing class to manage raw pointers and dynamic memory directly. When accessing private data members of composed objects within the containing class's member functions, the dot operator is used to call the composed object's public methods. The GetString function should return a const char * to prevent modification of private data through the returned pointer.
🧠 Quick Revision Questions
- What is composition and what type of relationship does it represent between classes?
- In what order are constructors called for the composing object and the composed object?
- In what order are destructors called for the composing object and the composed object?
- Why does the
Stringclass'sGetStringfunction return aconst char *instead of justchar *? - What two problems does the
SetStringfunction solve by allocating new memory instead of simply assigning the pointer?
📘 Lecture 15 — Composition
📖 Overview: This lecture explores the concepts of composition and aggregation, detailing how objects relate to one another. It introduces the use of member initialization lists to efficiently construct embedded objects. The lecture also covers friend functions and friend classes, which grant non-member functions and other classes access to private data.
🗂️ Topics Covered
The lecture begins by reviewing the composition example from the previous session, focusing on the Student and String classes. It then demonstrates how to initialize embedded objects using a member initialization list instead of explicit setter methods, leading to an overloaded constructor for the String class. The concept is extended by adding a Date object to the Student class, further illustrating composition. Next, the lecture distinguishes composition from aggregation, providing real-world examples like student-teacher and room-chair relationships and showing the C++ implementation of aggregation using pointers. Finally, it introduces friend functions and friend classes, explaining their necessity, how they are declared, and their implications for encapsulation.
📝 Lecture Summary
Composition
The lecture revisits the concept of composition with the Student and String classes. In the previous approach, the Student constructor was forced to call name.SetString(n) to initialize its String sub-object because the String class lacked a parameterized constructor. This is an inefficient overhead.
🔑 Overloaded Constructor — String(char *): A new constructor for the String class that takes a character pointer and initializes the object's internal string data.
String::String(char * str){
if(str != NULL){
ptr = new char[strlen(str)+1];
strcpy(ptr, str);
}
else ptr = NULL;
cout << "Overloaded Constructor::String..\n";
}
🔑 Member Initialization List: A special syntax used in constructor definitions to directly initialize data members and base classes before the constructor body executes.
📐 Formula: Student::Student(char * n, int roll, float g): name(n) { ... } → The Student constructor uses the member initialization list to call the String(char*) constructor for the name object.
📌 Example:
int main(){
Student aStudent("Fakhir", 899, 3.1); // Calls String(char*) for name
return 0;
}
Output:
Overloaded Constructor::String..
Constructor::Student..
Destructor::Student..
Destructor::String..
💡 Why this matters: The member initialization list directly initializes sub-objects, avoiding the need for an explicit setter call within the constructor body. This is more efficient and is the correct way to initialize const members and base classes.
The lecture extends the example by adding a Date member to the Student class.
📌 Example: Student with Date:
Student
- name: String
- birthDate: Date
Student::Student(char * n, const Date & d, int roll, float g): name(n), birthDate(d) {
cout << "Constructor::Student..\n";
rollNumber = roll;
gpa = g;
}
Output for Student aStudent("Fakhir", _date, 899, 3.5);:
Overloaded Constructor::Date..
Copy Constructor::Date..
Overloaded Constructor::String..
Constructor::Student..
Destructor::Student..
Destructor::String..
Destructor::Date..
Destructor::Date..
Note: birthDate(d) calls the Copy Constructor of the Date class.
Aggregation
Aggregation is a weaker relationship than composition. It models a “has-a” relationship where the parts can exist independently of the whole.
🔑 Composition vs. Aggregation:
- Composition (Strong) : The part (e.g.,
String name) is created and destroyed with the composite object (e.g.,Student). The part’s life is dependent on the whole. - Aggregation (Weak) : One object uses the services of another, but both can exist independently. The relationship is typically implemented via pointers or references to the part object.
📐 Implementation: In aggregation, a class contains a pointer or reference to another object. The sub-object's lifetime is NOT dependent on the master class.
📌 Example: Room and Chair
class Room{
private:
float area;
Chair * chairs[50]; // Aggregation: array of pointers to Chair objects
public:
Room();
void AddChair(Chair *, int chairNo);
Chair * GetChair(int chairNo);
bool FoldChair(int chairNo);
};
int main(){
Chair ch1; // Chair object exists independently
{
Room r1; // Room object created
r1.AddChair(&ch1, 1); // Room uses Chair (aggregation)
r1.FoldChair(1);
} // Room r1 is destroyed, but ch1 is NOT destroyed
ch1.UnFoldChair(1); // ch1 still exists
return 0;
}
💡 Why this matters: Aggregation allows for flexible object reuse. Multiple Rooms can share the same Chair, and the Chair can have its own independent lifecycle. The Room destructor does not delete the Chair objects.
Friend Functions
Friend functions are non-member functions that are granted access to all private members of a class.
🔑 Friend Function: A function that is NOT a member of the class but is given the same access privileges as member functions.
Why are they needed? They are useful for operator overloading (e.g., << and >>), for creating functions that operate on objects from multiple classes, or for creating standalone utility functions that require private data.
Are they against OOP? Yes, friend functions violate the principle of encapsulation by allowing outside functions to access a class’s private internals. However, they are sometimes necessary for practical reasons, especially when implementing certain operators.
📌 Example Syntax:
class X{
private:
int a, b;
public:
void MemberFunction();
friend void DoSomething(X obj); // Declaration (friend keyword used here)
};
The definition of the friend function is:
void DoSomething(X obj){ // No 'friend' keyword in definition
obj.a = 3; // No error: private member accessible
obj.b = 4;
}
Important rules:
- The prototype of the friend function appears in the class definition (with
friendkeyword), but it is NOT a member function. - The
friendkeyword can be placed anywhere in the class (private, public, or protected) without affecting it. - The definition must NOT use the
friendkeyword.
Friend Classes
A class can be declared as a friend of another class. All member functions of the friend class then have access to the private members of the other class.
🔑 Friend Class: A class whose member functions can access the private and protected members of the class that declared it as a friend.
📌 Example:
class X{
friend class Y; // Y is a friend of X
private:
int x_var1, x_var2;
};
class Y{
private:
int y_var1, y_var2;
public:
X objX; // Y has an X object as a member
void setX(){
objX.x_var1 = 1; // Y can access X's private members
objX.x_var2 = 2;
}
};
⭐ Key Takeaways
Composition and aggregation are both forms of “has-a” relationships, but they differ fundamentally in ownership and lifecycle management. Composition implies that the part is owned by the whole and cannot exist without it (e.g., a Student's Name), whereas aggregation implies the part can exist independently and is often referenced via a pointer (e.g., a Room’s Chairs). The member initialization list is the correct and most efficient way to construct embedded objects in composition. Friend functions are necessary for specific tasks like operator overloading, but they should be used sparingly as they break encapsulation. A class can also be made a friend of another, granting all its member functions access to that class’s private data.
🧠 Quick Revision Questions
- Why is it better to use a member initialization list (e.g.,
: name(n)) instead of calling a setter method (e.g.,name.SetString(n)) inside the constructor body? - What is the essential difference in ownership between composition and aggregation?
- What C++ construct (pointer, reference, or value) is typically used to implement aggregation, and why?
- A function
void modify(X &obj)is declared asfriendin classX. In its definition, where should thefriendkeyword appear? - If class
Penis declared asfriend class Teacher;, what special access doesTeacherhave regardingPenobjects?
📘 Lecture 16 — Operator Overloading
📖 Overview: This lecture introduces operator overloading in C++, which allows user-defined types (like classes) to use standard operators (+, -, *, etc.) in a natural way. It explains why operator overloading is needed, the rules that govern it, and how to implement it for binary operators using member functions.
🗂️ Topics Covered
The lecture begins with the problem of using function calls like Add() for complex number operations, then introduces operator overloading as the solution. It covers the list of overloadable and non-overloadable operators, explains that precedence, associativity, and arity remain unchanged, and provides the general syntax for overloading operators as member and non-member functions. It concludes with a detailed example of overloading the + operator for a Complex class and discusses the importance of choosing the correct return type.
📝 Lecture Summary
16.1. Operator overloading
Consider a Complex class with real and img data members and functions like Add, Subtract, and Multiply. To add two complex numbers, you would write Complex c3 = c1.Add(c2);. This involves two operations: calling the Add function and copying the result using the copy constructor. However, there are issues: you cannot use the natural + operator (writing c1 + c2 gives an error), and chaining expressions like c1 + c2 + c3 + c4 is cumbersome and requires nested function calls like c1.Add(c2.Add(c3.Add(c4))). This makes code less readable, error-prone, and hard to maintain.
🔑 Definition — Operator Overloading: The ability to define how standard operators (like +, -, *, etc.) behave when applied to objects of user-defined classes.
💡 Why this matters: Operator overloading allows mathematical statements using user-defined objects to be written in a natural, readable way, just like with built-in types.
Operator overloading solves these issues. With an overloaded + operator, you can write c1 + c2 + c3 + c4, which the compiler converts into appropriate function calls like (c1.operator+(c2)).operator+(c3). C++ already overloads operators for predefined types (like int, float, double) and calls the correct low-level function (e.g., Add(int a, int b) for integers). Operator functions are not called directly; they are automatically invoked by the compiler.
🔑 Definition — Precedence: The order in which operators are evaluated in an expression. Precedence is NOT affected by overloading. For example, in c1 * c2 + c3, multiplication is always done first, regardless of overloading.
🔑 Definition — Associativity: The order in which operators of the same precedence are evaluated (left-to-right or right-to-left). Associativity is NOT changed by overloading. For example, c1 + c2 + c3 + c4 is evaluated left-to-right as ((c1 + c2) + c3) + c4.
🔑 Definition — Arity: The number of operands an operator works on (e.g., unary for one, binary for two). Arity is NOT affected by overloading. For example, the division operator / always takes exactly two operands.
Important rules: Always write code that matches the operator's intended meaning (e.g., don't put subtraction code inside the + operator). Creating a new operator (like $) is a syntax error.
🔑 Definition — General syntax for Operator Overloading:
As a member function: return_type class_name::operator operator_symbol( parameters ){ /*code*/ }
As a non-member (friend) function: return_type operator operator_symbol( parameters ){ /*code*/ }
Binary Operators Overloading
Binary operators act on two quantities. The general syntax for a member function is: TYPE class_name::operator operator_symbol( TYPE rhs ){ /*code*/ }. For a non-member function: TYPE operator operator_symbol( TYPE lhs, TYPE rhs ){ /*code*/ }. The operator OP must have at least one formal parameter of a user-defined class type. int operator + (int, int); is an error because you cannot redefine built-in operators for built-in types.
📌 Example — Overloading + for the Complex class:
class Complex {
private:
double real, img;
public:
Complex operator +(const Complex & rhs);
};
Complex Complex::operator +(const Complex & rhs) {
Complex t;
t.real = real + rhs.real;
t.img = img + rhs.img;
return t;
}
The return type is Complex to allow chaining, like Complex t = c1 + c2 + c3;. This is automatically converted to (c1.operator+(c2)).operator+(c3). If the return type were void, you could not chain expressions, would have to store results in one of the existing objects, and the code would be less readable and harder to maintain.
⭐ Key Takeaways
Operator overloading allows user-defined types to use standard operators naturally, solving the readability and maintainability issues of using explicit function calls like Add(). The core rules to remember are: precedence, associativity, and arity of an operator are never changed by overloading; you cannot create new operators; and the overloaded operator must have at least one user-defined type parameter. For chaining expressions (like c1 + c2 + c3), the overloaded operator must return an object (or a reference) rather than void. Always ensure the overloaded operator’s behavior is intuitive (e.g., + should add, not subtract) to avoid code chaos.
🧠 Quick Revision Questions
- What is operator overloading and what problem does it solve?
- Name three operators that cannot be overloaded in C++ and explain why for one of them.
- If you overload
*for a class, can its precedence change compared to the built-in*? Explain. - Why is it important for a binary
+operator to return an object (e.g.,Complex) rather thanvoid? - What is the general syntax for overloading a binary operator as a member function of a class?
📘 Lecture 17 — Binary operators (cont.)
📖 Overview: This lecture continues the discussion on overloading binary operators, focusing on the complexities of adding basic data types to user-defined classes like Complex. It also introduces the critical topic of overloading the assignment operator, explaining why the compiler-generated version fails for classes with dynamic memory and how to implement a correct, deep-copying version.
🗂️ Topics Covered
The lecture covers the invocation mechanism for overloaded binary operators with respect to the left-hand argument. It then details how to modify the Complex class to handle addition with a double value, including the use of friend functions to solve the ordering problem. Finally, it delves into overloading the assignment operator, contrasting shallow and deep copy, identifying issues like memory leaks and dangling pointers, and demonstrating the correct signature and implementation that returns a reference.
📝 Lecture Summary
Binary operators (cont.)
The binary operator is always called with reference to the left-hand argument. For example, in c1+c2, the call becomes c1.operator+(c2), where c1 is the calling object and c2 is the argument. This behavior is fundamental to understanding how operators work from the perspective of an object.
📌 Example:
c1 + c2→c1.operator+(c2)c2 + c1→c2.operator+(c1)
Adding basic data type to complex number class
The standard overloaded + operator for a Complex class can add two Complex objects but cannot handle c1 + 2.325 (adding a double to a Complex). To support this, we modify the class by adding a specific overloaded member function.
class Complex{
// ...
Complex operator+(const Complex & rhs);
Complex operator+(const double& rhs);
};
The implementation for the double version simply adds the double to the real part and keeps the imaginary part unchanged. This now allows c1 + 235.01.
🔑 Definition — Left-hand Argument Issue: When the left-hand operand is not an object of the class (e.g., 450.120 + c1), the compiler cannot find a member operator+ to call on the double. This requires a non-member function.
To solve this, we use friend functions that take both arguments explicitly. This allows us to write code for 450.120 + c1 where neither argument is the calling object.
class Complex{
// ...
friend Complex operator + (const Complex & lhs, const double & rhs);
friend Complex operator + ( const double & lhs, const Complex & rhs);
};
Why friend? Because these functions need access to the private members (real, img) of the Complex class. Alternatively, we could use getters and setters, but that would require four extra functions. The compiler searches for overloaded operators in member functions first, then in non-member (friend) functions.
Other Binary Operators
Other binary operators like *, /, and - are overloaded in a similar manner to the + operator. For a Complex class, these would be implemented as non-member friend functions to perform their respective arithmetic operations on the real and imaginary parts.
📐 Examples:
Complex operator * (const Complex & c1, const Complex & c2);Complex operator / (const Complex & c1, const Complex & c2);Complex operator - (const Complex & c1, const Complex & c2);
Overloading Assignment operator
The compiler can generate a default constructor, copy constructor, and assignment operator for a class. However, when a class has dynamic memory (e.g., a pointer), the compiler-generated assignment operator performs a shallow copy. This copies the pointer value, not the data it points to, leading to problems.
🔑 Definition — Shallow Copy: A bitwise copy of each member, which for a pointer merely copies the address. This results in two pointers pointing to the same memory, causing memory leaks and dangling pointers.
Consider a String class with a char * bufferPtr. A shallow assignment str1 = str2 would copy the pointer bufferPtr from str2 to str1, losing the original memory str1 pointed to (memory leak) and leaving two objects sharing the same memory (dangling pointer issues).
📌 Example (Shallow Copy Problem):
str1points to "Hello",str2points to "World".- After
str1 = str2,str1.bufferPtrnow points to "World", but the memory for "Hello" is lost (leaked). Bothstr1andstr2point to the same "World" string.
Modified Assignment Operator Code
The correct assignment operator must perform a deep copy: delete the existing memory, allocate new memory, and then copy the data. The initial implementation often uses a void return type.
void String::operator = (const String & rhs){
size = rhs.size;
if(rhs.size != 0){
delete [] bufferPtr; // Free existing memory
bufferPtr = new char[rhs.size+1]; // Allocate new memory
strcpy(bufferPtr,rhs.bufferPtr); // Copy data
}
else
bufferPtr = NULL;
}
The Chaining Problem: A void return prevents chaining, e.g., str1 = str2 = str3. This expression is right-associative: str1 = (str2 = str3). First, str2.operator=(str3) is called, but because it returns void, the outer call str1.operator=(void) fails.
Return by Reference
To allow chaining, the assignment operator should return a reference to the object (*this). This allows expressions like str1 = str2 = str3 to work correctly, as the result of the inner assignment (str2 = str3) is the object str2, which is then passed to the outer assignment.
class String{
// ...
String & operator = (const String &);
};
String & String :: operator = (const String & rhs){
size = rhs.size;
delete [] bufferPtr;
if(rhs.size != 0){
bufferPtr = new char[rhs.size+1];
strcpy(bufferPtr,rhs.bufferPtr);
}
else bufferPtr = NULL;
return *this; // Return the current object by reference
}
🔑 Definition — *this: this is a pointer to the current object. Dereferencing it with *this gives the object itself. Returning *this allows chaining.
💡 Why this matters: The assignment operator returning a reference is a standard convention in C++ enabling idiomatic code and is essential for correct operation with standard library containers and algorithms.
⭐ Key Takeaways
- A binary operator like
+is invoked on the left-hand operand. To handle cases where the left operand is not a class object (e.g.,double + Complex), you must use non-member friend functions. - When overloading operators for classes with dynamic memory, the compiler-generated default assignment operator performs a shallow copy, leading to memory leaks and dangling pointers.
- You must overload the assignment operator to perform a deep copy. This involves deleting the old memory, allocating new memory, and copying the data.
- The overloaded assignment operator should always return a reference to the current object (
*this). This supports chaining (e.g.,a = b = c) and is a standard C++ convention. - The order of search for an overloaded operator is: member functions first, then non-member functions.
🧠 Quick Revision Questions
- Explain why the expression
450.120 + c1fails when only a member operator+ is defined forComplex + double. What is the solution? - What is a shallow copy, and what are two specific memory management issues it causes when used with a class that has a dynamically allocated array?
- Why is it necessary to
delete [] bufferPtrinside the overloaded assignment operator before allocating new memory? - What is the purpose of returning
*thisfrom the assignment operator? What problem does it solve? - To add a
doubleto aComplexobject, three overloadedoperator+functions were discussed. List the signatures for all three.
📘 Lecture 18 — Self Assignment Problem and Other Binary Operators
📖 Overview: This lecture addresses the critical self-assignment problem in operator overloading, where assigning an object to itself can cause memory corruption. It then explores overloading compound assignment operators like
+=and demonstrates how to implement binary operators like+using non-member, non-friend functions to maintain encapsulation.
🗂️ Topics Covered
The lecture covers the self-assignment problem in copy assignment operators and its solution using a this pointer check. It discusses making the return type const to prevent assignment to sub-expressions, then moves to overloading the += operator for the Complex class. Finally, it examines friend functions versus non-member functions for operator overloading, showing three versions of the + operator for different operand combinations.
📝 Lecture Summary
18.1. Self assignment problem
When we assign a string object to itself, as done in the main function below, our program will produce unexpected results because the source and destination operands for copying are the same:
int main(){
String str1("Fakhir");
str1 = str1; // Self Assignment problem...
return 0;
}
The result of str1 = str1 leads to memory access violations or incorrect data copy because the program first deletes the buffer of the left-hand side operand, then attempts to copy from the right-hand side which has already been destroyed.
🔑 Definition — Self Assignment Problem: When an object is assigned to itself, the assignment operator may destroy the object's resources (e.g., delete its buffer) before copying from the source, which is the same object, leading to undefined behavior.
We can resolve this issue by adding a simple if condition to ensure that both strings are not the same by comparing the this pointer to the address of the right-hand side operand:
String & String :: operator = (const String & rhs){
if(this != &rhs){
size = rhs.size;
delete [] bufferPtr; // deleting memory of left hand side operand
if(rhs.bufferPtr != NULL){
bufferPtr = new char[rhs.size+1];
strcpy(bufferPtr,rhs.bufferPtr);
// memory access violation or incorrect data copy
}
else bufferPtr = NULL;
}
return *this;
}
Now self-assignment is properly handled:
int main(){
String str1("Fakhir");
str1 = str1;
return 0;
}
We can make the return type const String & to avoid assignment to sub-expressions, such as (str1 = str2) = str3. However, as we can do that with primitive types, we may allow assignment to sub-expressions by keeping the return type as String & only.
💡 Why this matters: The self-assignment check prevents a common bug where an object accidentally assigns to itself, which would corrupt memory. The choice of return type (const or non-const) affects whether chained assignments like (a=b)=c are allowed, matching primitive type behavior.
18.2. Other Binary operators
Overloading += operator:
class Complex{
double real, img;
public:
Complex & operator+=(const Complex & rhs);
Complex & operator+=(const double & rhs);
// ...
};
Complex & Complex::operator += (const Complex & rhs){
real = real + rhs.real;
img = img + rhs.img;
return * this;
}
Complex & Complex::operator += (const double & rhs){
real = real + rhs;
return * this;
}
int main(){
Complex c1, c2, c3;
c1 += c2;
c3 += 0.087;
return 0;
}
🔑 Definition — Compound Assignment Operator: An operator like += that combines an operation with assignment, modifying the left-hand operand in place and returning a reference to it.
📐 Formula: c1 += c2 → c1.real = c1.real + c2.real; c1.img = c1.img + c2.img; → returns reference to c1
📌 Example: With Complex c1(3.0, 4.0) and c2(1.0, 2.0), after c1 += c2, c1.real = 4.0, c1.img = 6.0. With c3 += 0.087 and c3(2.0, 5.0), after operation, c3.real = 2.087, c3.img = 5.0.
18.3. Friend Functions and Operator overloading
Friend functions minimize encapsulation as we can access private data of any class using friend functions. This can result in:
- Data vulnerability
- Programming bugs
- Tough debugging
Hence, use of friend functions must be limited. We can overload operators without declaring them friend functions of a class. For example, the + operator can be defined as a non-member, non-friend function as shown below (three versions to handle three kinds of statements):
obj1 + obj2obj1 + 3.783.78 + obj1
Complex operator + (const Complex & a, const Complex & b){
Complex t = a; // creating temporary object t to store a+b
return t += b; // returning t by reference
}
Complex operator + (const double & a, const Complex & b){
Complex t = b;
return t += a;
}
Complex operator + (const Complex & a, const double & b){
Complex t = a;
return t += b;
}
🔑 Definition — Non-member, Non-friend Function: A function that cannot access private members of a class but can still implement operators by using public member functions like operator+=.
📌 Example: For Complex c1(3.0, 4.0) and c2(1.0, 2.0), c1 + c2 calls operator+(c1, c2) which creates a temporary t = c1, then calls t += c2, returning the temporary. The result is Complex(4.0, 6.0).
💡 Why this matters: Using non-friend functions preserves encapsulation and reduces bugs. The + operator is implemented in terms of +=, which is a common design pattern called "implementing in terms of" that reduces code duplication.
Other Binary operators
The operators -=, /=, *=, |=, %=, &=, ^=, <<=, >>=, != can be overloaded in a very similar fashion, following the same pattern of modifying *this and returning a reference.
⭐ Key Takeaways
The most critical concepts from this lecture are: always check for self-assignment in copy assignment operators by comparing this to &rhs to prevent memory corruption; compound assignment operators like += should return a reference to *this to support chaining; operators can be overloaded as member functions, friend functions, or non-member non-friend functions, with non-friend being preferred to maintain encapsulation; binary operators like + are best implemented in terms of compound assignment operators (+=) to avoid code duplication; and the three versions of binary operators handle different operand type combinations (object+object, object+double, double+object).
🧠 Quick Revision Questions
- What is the self-assignment problem, and how do you fix it in the copy assignment operator?
- Why would you make the return type of
operator=const, and what functionality does it prevent? - How does the
operator+=work for the Complex class, and what does it return? - What are three ways to overload operators in C++, and why are non-friend non-member functions preferred?
- How would you implement
operator+for the Complex class without using friend functions, and why does this approach preserve encapsulation?
📘 Lecture 19 — Overloading stream insertion extraction operators
📖 Overview: This lecture explains how to overload stream insertion (
<<) and extraction (>>) operators for user-defined classes in C++. It covers why these operators need special handling, the limitations of member function overloading, and the correct approach using friend functions to enable natural syntax and cascading.
🗂️ Topics Covered
The lecture begins with an introduction to stream insertion and extraction operators, explaining how cin and cout work with basic data types. It then demonstrates the problem when trying to use these operators with user-defined classes like Complex. The correct overloading technique using friend functions is presented for both insertion and extraction operators, followed by overloading comparison operators (equality and inequality) for the Complex class.
📝 Lecture Summary
19.1. Stream Insertion operator
Often we need to display data on the screen. C++ provides the insertion operator (<<) to put data on the output stream. The default output stream is the console, but it can also be a file or network socket.
📌 Example:
int i=1, j=2;
cout << "i= " << i << "\n";
cout << "j= " << j << "\n";
19.2. Stream Extraction operator
We also need to get data from the console, file, or network. This is achieved through the stream extraction operator (>>), which gets data from the input stream. The default input stream is from the console.
📌 Example:
int i,j;
cin >> i >> j; // getting value of i and j from user
Explanation:
cin and cout are objects of istream and ostream classes respectively. The insertion and extraction operators have been overloaded in istream and ostream classes for all basic types like int, float, long, double, and char*.
When we write cin >> i, the actual call passes objects to the overloaded function:
istream & operator >> (istream & in, int & i)
Here, cin is passed as istream object along with int i. The function returns an istream object by reference & to accommodate multiple input statements in a single line like cin >> i >> j.
Similarly for insertion operator:
ostream & operator << (ostream & os, const int & i)
If we try to use these operators for user-defined data types like our Complex class, the compiler will generate an error as it will not find any overloaded operator code for our Complex class:
Complex c1;
cout << c1; // Error
cout << c1 << 2; // Error cascaded statement
Compiler error: binary '<<' : no operator defined which takes a right-hand operand of type 'class Complex'
💡 Why this matters: The same error will occur for the stream extraction operator, so we need to overload both operators (<< and >>) for our Complex class.
19.3. Overloading Stream Insertion Operator
First, we try to overload insertion << operator as a member function:
class Complex {
// ...
void operator << (const Complex & rhs);
};
When called as cout << c1;, this generates errors because it expects the left operand to be a Complex object, not ostream. The only working syntax would be c1 << cout;, which has two limitations:
- Difficult to understand and remember statement syntax (
c1 << cout ;) - Cascaded statements not possible (
cout << c1 << 2 ;)
The correct approach uses a friend function:
class Complex {
// ...
friend ostream & operator << (ostream & os, const Complex & c);
};
🔑 Definition — Stream Insertion Operator Overloaded: ostream & operator << (ostream & os, const Complex & c)
The implementation:
// we want the output as: (real, img)
ostream & operator << (ostream & os, const Complex & c){
os << '(' << c.real
<< ','
<< c.img << ')';
return os;
}
Important: ostream reference cannot be const as it stores data in its buffer to insert on output stream. However, the Complex reference will be constant as we are only getting data from Complex object.
📌 Example:
Complex c1(1.01, 20.1), c2(0.01, 12.0);
cout << c1 << endl << c2;
Output:
( 1.01 , 20.1 )
( 0.01 , 12.0 )
Now cascading statements are also possible:
cout << c1 << c2;
is equivalent to:
operator<<( operator<<(cout,c1),c2);
Because the insertion operator is left to right associative, first the left part is executed, then the next part — as opposed to the copy assignment operator which is right associative.
19.4. Overloading Stream Extraction Operator
class Complex {
// ...
friend istream & operator >> (istream & i, Complex & c);
};
Important: istream cannot be const because the istream buffer will change as we get data from it. Similarly, Complex object cannot be const for stream extraction operator because we will add data to it and its state will change.
🔑 Definition — Stream Extraction Operator Overloaded: istream & operator >> (istream & in, Complex & c)
Implementation:
istream & operator >> (istream & in, Complex & c){
in >> c.real;
in >> c.img;
return in;
}
📌 Example:
Complex c1(1.01, 20.1);
cin >> c1; // suppose we entered 1.0025 for c1.real and 0.0241 for c1.img
cout << c1;
Output:
( 1.0025 , 0.0241 )
19.5. Other Binary operators
Overloading comparison operators (Equality and Inequality operators)
class Complex {
public:
bool operator == (const Complex & c);
// friend bool operator == (const Complex & c1, const Complex & c2);
bool operator != (const Complex & c);
// friend bool operator != (const Complex & c1, const Complex & c2);
// ...
};
Equality operator as member function:
bool Complex::operator ==(const Complex & c){
if((real == c.real) && (img == c.img)){
return true;
}
else {
return false;
}
}
📐 Formula: (real == c.real) && (img == c.img) → returns true if both real and imaginary parts are equal
As non-member friend function:
bool operator ==(const Complex& lhs, const Complex& rhs){
if((lhs.real == rhs.real) && (lhs.img == rhs.img)){
return true;
}
else {
return false;
}
}
Inequality Operator:
bool Complex::operator !=(const Complex & c){
if((real != c.real) || (img != c.img)){
return true;
}
else {
return false;
}
}
📐 Formula: (real != c.real) || (img != c.img) → returns true if either real or imaginary parts differ
⭐ Key Takeaways
The most critical point is that stream insertion and extraction operators cannot be overloaded as member functions for user-defined classes because the left operand must be an ostream or istream object, not the class object. They must be implemented as friend functions that take the stream object by reference and return it by reference to support cascading. For insertion (<<), the class object is passed as const reference since we only read from it, while for extraction (>>), it must be non-const since we modify it. The insertion operator is left-to-right associative, enabling natural cascading syntax like cout << c1 << c2. Finally, comparison operators like == and != can be overloaded either as member functions or non-member friend functions, comparing the real and imaginary parts component-wise.
🧠 Quick Revision Questions
- Why can't stream insertion and extraction operators be overloaded as member functions of a user-defined class?
- What is the correct function signature for overloading the stream insertion operator, and why must the function return a reference?
- Explain why the Complex object is passed as
constin the insertion operator but as non-const in the extraction operator. - How does left-to-right associativity of the insertion operator enable cascaded statements like
cout << c1 << c2? - Write the implementation of both equality (
==) and inequality (!=) operators for the Complex class, comparing both real and imaginary parts.
📘 Lecture 20 — Modified String Class
📖 Overview: This lecture demonstrates how to extend a user-defined String class with operator overloading to provide intuitive and efficient string manipulation. It covers the subscript operator for character access, the function call operator for both character and substring operations, and the general principles of overloading unary operators.
🗂️ Topics Covered
The lecture begins by presenting a problem with the existing String class where changing a single character requires reallocating the entire buffer. It then presents solutions starting with a SetChar member function, followed by overloading the subscript operator [], the function call operator () for single character and substring operations, and finally an overview of overloading unary operators.
📝 Lecture Summary
Modified String Class
The existing String class has a SetString method that deletes and reallocates a new buffer whenever a string change is made, which is inefficient for large strings or single character changes.
🔑 Definition — SetString: A member function that deletes the current buffer and allocates a new one when changing the entire string value.
📌 Example: Changing "Ping" to "Pong" with str2.SetString("Pong") involves deleting the entire "Ping" buffer and allocating a new buffer for "Pong" — too much overhead for a single character change.
To solve this, a SetChar function can be added:
void SetChar(char c, int pos){
if(bufferPtr != NULL){
if(pos>0 && pos<=size)
bufferPtr[pos] = c;
}
}
📌 Example: str1.SetChar('o', 2); changes "Ping" to "Pong" efficiently by modifying only the character at position 2.
20.1. Subscript [] Operator
An elegant solution is to overload the subscript operator [] to work like it does with built-in arrays, allowing both reading (r-value) and writing (l-value).
🔑 Definition — Subscript operator []: An operator used for direct index-based access to elements of an array or collection.
💡 Why this matters: Overloading [] allows our String class to behave like a built-in character array, making code more intuitive and readable.
📌 Example:
String str2;
str2.SetString("Ping");
str[2] = 'o'; // acting as l-value (writing)
cout << str[2]; // acting as r-value (reading)
20.2. Overloading Subscript [] Operator
The subscript operator must be overloaded as a member function with exactly one integer parameter.
class String{
// ...
public:
char & operator[](int);
// ...
};
char & String::operator[](int pos){
assert(pos>0 && pos<=size);
return stringPtr[pos-1];
}
📐 Formula: operator[](int pos) → returns a reference to char so it can be used on both left and right sides of assignment.
📌 Example:
String s1("Ping");
cout << s1.GetString() << endl; // Output: Ping
s1[2] = 'o'; // Changes 'i' to 'o'
cout << s1.GetString(); // Output: Pong
Output:
Ping
Pong
20.3. Overloading Function () operator
The function call operator () can also be overloaded. It must be a member function, can have any number of parameters, and any return type.
class String{
// ...
public:
char & operator()(int);
// ...
};
char & String::operator()(int pos){
assert(pos>0 && pos<=size);
return bufferPtr[pos-1];
}
🔑 Definition — Function call operator (): An operator that can be overloaded to allow objects to be called like functions, with flexible parameter lists. 📌 Example:
String s1("Ping");
char g = s1(2); // g = 'i' (reading)
s1(2) = 'o'; // writing
cout << g << "\n"; // Output: i
cout << s1.GetString(); // Output: Pong
Output:
i
Pong
20.4. Function Operator performing Sub String operation
The function call operator can be overloaded with two parameters to extract a substring.
class String{
// ...
public:
String operator()(int, int);
// ...
};
String String::operator()(int index, int subLength){
assert(index>0 && index+subLength-1<=size);
char * ptr = new char[subLength+1];
for (int i=0; i < subLength; ++i)
ptr[i] = bufferPtr[i+index-1];
ptr[subLength] = '\0';
String str(ptr);
delete [] ptr;
return str;
}
🔑 Definition — Substring: A contiguous sequence of characters within a string, extracted starting from a given index for a specified length. 📌 Example:
String s("Hello World");
cout << s(1, 5); // Extracts 5 characters starting from position 1
Output:
Hello
20.5. Unary Operators
Unary operators take one operand and act on the object with reference to which they are called.
🔑 Definition — Unary operators: Operators that operate on a single operand, including: &, *, +, -, ++, --, !, ~
General syntax for unary operators:
- As Member Functions:
TYPE & operator OP ();— no argument needed, the object itself is the operand - As Non-member (Friend) Functions:
friend TYPE & operator OP (TYPE & t);— one argument (the object to be operated on)
Overloading unary - for Complex class:
class Complex{
// ...
Complex operator - (); // member function
// friend Complex operator -(Complex &); // alternative as friend
};
Complex Complex::operator -(){
Complex temp;
temp.real = -real;
temp.img = -img;
return temp;
}
Complex c1(1.0, 2.0), c2;
c2 = -c1;
// c2.real = -1.0
// c2.img = -2.0
💡 Why this matters: Unary operator overloading allows user-defined types to use standard operators like -, ++, -- naturally, making code more intuitive and readable.
⭐ Key Takeaways
Students must remember that the subscript operator [] must be overloaded as a member function with one integer parameter and returns a reference to allow both reading and writing. The function call operator () is versatile — it can be overloaded with any number of parameters for different purposes like character access or substring extraction. Unary operators follow a consistent pattern: as member functions they take no arguments, as non-member functions they take one argument. The assert function is used for bounds checking to ensure safe index access. Overloading these operators makes user-defined classes behave more like built-in types, improving code readability and efficiency.
🧠 Quick Revision Questions
- Why is using
SetStringto change a single character in a large string inefficient, and what alternative is presented? - What is the return type of the overloaded subscript operator
[]in the String class, and why is this specific type necessary? - How does the function call operator
()differ from the subscript operator[]in terms of parameter flexibility? - In the substring operation using
operator(), what does the first parameter represent and what does the second parameter represent? - When overloading unary operators as member functions, how many parameters are required, and why?
📘 Lecture 21 — Unary Operators
📖 Overview: This lecture covers the overloading of unary operators, specifically pre and post increment/decrement operators for user-defined classes. It also introduces type conversion mechanisms in C++ including conversion constructors, explicit keyword, and type conversion operators, along with their drawbacks and best practices.
🗂️ Topics Covered
The lecture begins with explaining the behavior of ++ and -- operators for pre-defined types, distinguishing between post-increment and pre-increment semantics. It then demonstrates how to overload these operators as member and non-member functions for user-defined classes. The second major topic is type conversion, covering automatic conversion using single-parameter constructors, the explicit keyword to restrict implicit conversions, and conversion from current type to other types using operator overloading. Finally, it discusses drawbacks of type conversion operators and recommends using separate member functions instead.
📝 Lecture Summary
21.1. Behavior of ++ and -- for pre-defined types
Post-increment operator ++ increments the current value and then returns the previous value. Post-decrement -- works exactly like post-increment.
📌 Example:
int x = 1, y = 2;
cout << y++ << endl; // Output: 2
cout << y; // Output: 3
Post-increment cannot be chained or used as lvalue: y++++; and y++ = x; produce errors.
Pre-increment operator ++ increments the current value and then returns its reference. Pre-decrement works exactly like pre-increment.
📌 Example:
int y = 2;
cout << ++y << endl; // Output: 3
cout << y << endl; // Output: 3
Pre-increment can be chained and used as lvalue:
int x = 2, y = 2;
++++y; // y becomes 4
cout << y; // Output: 4
++y = x; // y becomes 2
cout << y; // Output: 2
For user-defined classes, pre-increment is overloaded as:
🔑 Definition — Pre-increment operator: Returns a reference so the object can be used as an lvalue.
📐 Member function syntax: Complex & operator ++ ();
📐 Non-member function syntax: friend Complex & operator ++(Complex &);
📌 Example (Member function implementation):
Complex & Complex::operator++(){
real = real + 1;
return *this;
}
📌 Example (Non-member function implementation):
Complex & operator ++ (Complex & h){
h.real += 1;
return h;
}
💡 Why this matters: Returning a reference allows chaining and using the result as lvalue, e.g., ++h1 = h2 + ++h3; is valid.
21.2. Post-increment operator
A post-fix unary operator is implemented using a member function with 1 dummy int argument OR a non-member function with two arguments. The dummy parameter tells the compiler it is post-increment.
In post-increment, current value of the object is stored in a temporary variable, current object is incremented, then value of the temporary variable is returned.
🔑 Member function syntax: Complex operator ++ (int);
🔑 Non-member function syntax: friend Complex operator ++(const Complex &, int);
📌 Example (Member function):
Complex Complex::operator ++ (int){
complex t = *this;
real += 1;
return t;
}
📌 Example (Non-member function):
Complex operator ++ (const Complex & h, int){
complex t = h;
h.real += 1;
return t;
}
Post-increment cannot be used as lvalue: h3++ = h2 + h3++; produces an error.
The pre and post decrement operator -- is implemented in exactly the same way.
21.3. Type Conversion
The compiler automatically performs a type coercion of compatible types:
int f = 0.021; // float automatically converted to int, compiler issues warning
double g = 34; // int automatically converted to double
The user can also explicitly convert between types using casting:
int g = (int)0.0210; // C-style cast
double h = double(35); // function-style cast
For user-defined classes, there are two types of conversions:
- From any other type to current type: Requires a constructor with a single parameter
- From current type to any other type: Requires an overloaded operator
Conversion from other type to current type (int to String):
📌 Example:
class String{
public:
String(int a);
char * GetStringPtr() const;
};
String::String(int a){
cout << "String(int) called..." << endl;
char array[15];
itoa(a, array, 10);
size = strlen(array);
bufferPtr = new char [size + 1];
strcpy(bufferPtr, array);
}
int main(){
String s = 345; // implicit conversion using constructor
cout << s.GetStringPtr() << endl; // Output: 345
return 0;
}
🔑 Keyword explicit: Restricts automatic conversions. Only works with constructors. When used, casting must be explicitly performed by the user.
📌 Example:
class String{
public:
explicit String(int); // prevents implicit conversion
};
int main(){
String s;
s = 'A'; // Error: implicit conversion not allowed
s1 = String(101); // valid: explicit casting
s2 = (String)204; // valid: explicit casting
return 0;
}
💡 Why this matters: Without explicit, a character like 'A' (ASCII 65) could be unintentionally converted to integer 65 and then to a String, when the user actually wanted String "A".
Type Conversion Operator — Used for converting from current type (user defined) to any other basic type or user defined type.
📐 General Syntax: TYPE1::operator TYPE2();
Example: String::operator char *(); converts String object to char*.
These functions are written as member functions with NO return type and NO arguments specified. Return type is implicitly taken to be TYPE2 by compiler.
📌 Example:
class String{
public:
operator int();
operator char *();
};
String::operator int(){
if(size > 0)
return atoi(bufferPtr);
else
return -1;
}
String::operator char *(){
return bufferPtr;
}
int main(){
String s("2324");
cout << (int)s << endl << (char *)s; // Output: 2324 \n 2324
return 0;
}
21.4. User Defined types
User-defined types can be converted in exactly the same way. Only prototype is shown:
class String{
operator Complex();
operator HugeInt();
operator IntVector();
};
21.5. Drawbacks of Type Conversion Operator
⛔ Problem: Type conversion operators can cause unexpected implicit conversions.
📌 Example:
class String{
public:
String(char *);
operator int();
};
int main(){
String s("Fakhir");
cout << s; // << is NOT overloaded for String
// compiler automatically converts s to int
// Output: Junk Returned...
return 0;
}
✅ Solution: DO NOT use type conversion operators. Instead, use separate member functions for such type conversions.
📌 Example (Recommended approach):
class String{
public:
String(char *);
int AsInt(); // separate member function instead of operator
};
int String::AsInt(){
if(size > 0)
return atoi(bufferPtr);
else
return -1;
}
int main(){
String s("434");
cout << s; // error: no matching operator
cout << s.AsInt(); // works correctly
return 0;
}
⭐ Key Takeaways
The most critical concepts from this lecture are: pre-increment returns a reference to the object and can be used as lvalue, while post-increment returns a temporary value and cannot. The dummy int parameter in the operator function tells the compiler whether it's post-increment. For type conversion, constructors with single parameters allow conversion from other types to the current type, but the explicit keyword should be used to prevent unintended implicit conversions. Type conversion operators (like operator int()) allow conversion from current type to other types but are dangerous because the compiler may apply them unexpectedly. The recommended practice is to use explicit constructors and separate member functions (like AsInt() or GetStringPtr()) instead of type conversion operators to avoid subtle bugs.
🧠 Quick Revision Questions
- What is the difference between pre-increment and post-increment in terms of what they return?
- Why can post-increment not be used as an lvalue (e.g.,
h3++ = h2 + h3++;is an error)? - How does the compiler distinguish between pre-increment and post-increment operator overloads?
- What is the purpose of the
explicitkeyword and what types of constructors does it work with? - Why is it recommended to use separate member functions like
AsInt()instead of type conversion operators likeoperator int()?
📘 Lecture 22 — Practical Implementation of Inheritance in C++
📖 Overview: This lecture covers the practical implementation of inheritance in C++ programming. It explains how classes inherit characteristics from base classes, the UML notation for inheritance, types of inheritance in C++, the “IS A” relationship, member access, memory allocation, and the order of execution for constructors and destructors. Understanding these concepts is crucial for designing reusable and hierarchical class structures in object-oriented programming.
🗂️ Topics Covered
Inheritance in classes, UML notation of inheritance, types of inheritance in C++, “IS A” relationship, accessing members in inheritance, allocation of derived class objects in memory, constructors and their execution order, base class initializers, initializing members of derived classes, destructors and their order of execution, and practical C++ examples demonstrating inheritance.
📝 Lecture Summary
22.2. Inheritance in Classes
If a class B inherits from class A, then B contains all the characteristics (information structure and behavior) of class A. The class whose behavior is being inherited is called base class (or parent class), and the class that inherits the behavior is called derived class (or child class). Besides inherited characteristics, a derived class may have its own unique characteristics.
22.3. UML Notation
Inheritance is represented in UML by an arrow from the derived class to the parent class. The arrow points toward the parent class.
🔑 Definition — Inheritance Notation: UML uses an open arrow from child to parent to show “IS A” relationship.
22.4. Inheritance in C++
In C++, we can inherit a class from another class in three ways:
- Public — Public and protected members of base class retain their access levels in derived class.
- Private — Public and protected members of base class become private in derived class.
- Protected — Public and protected members of base class become protected in derived class.
🔑 Definition — Access Specifiers: Determines how members of the base class are accessible in the derived class.
22.5. “IS A” Relationship
Inheritance represents an “IS A” relationship. For example, “a student IS A person.” In general, inheritance means “derived class IS A kind of parent class.”
C++ Syntax of Inheritance:
class ChildClass : public BaseClass {
// ...
};
Example:
class Person {
// ...
};
class Student : public Person {
// ...
};
Accessing Members
- Public members of the base class become public members of the derived class.
- Private members of the base class are not accessible from outside the base class, even in the derived class — this is information hiding.
Example:
class Person {
char *name;
int age;
public:
const char *GetName() const;
int GetAge() const;
};
class Student : public Person {
int semester;
int rollNo;
public:
int GetSemester() const;
int GetRollNo() const;
void Print() const;
};
class Teacher : public Person {
char *dept;
int course;
public:
char *GetDept() const;
int GetCourse() const;
void Print() const;
};
Error in Code:
void Student::Print() {
cout << name << “ is in” << “ semester ” << semester; // Error: 'name' is private in Person
}
Corrected Code:
void Student::Print() {
cout << GetName() << “ is in semester ” << semester; // Use public GetName() instead
}
main function:
int main(){
Student stdt;
stdt.semester = 0; // error: private member
stdt.name = NULL; // error: private member in base class
cout << stdt.GetSemester(); // OK
cout << stdt.GetName(); // OK
return 0;
}
🔑 Definition — char data type handling in C++:* char arrays (char[]) can be handled statically (e.g., char name[30];) or dynamically (e.g., char *name; name = new char[30];).
Allocation in Memory
The object of a derived class is represented in memory as containing an anonymous object of the base class followed by the derived class's own data members.
Derived Class Object
|----------------------|
| base member1 |
| base member2 |
| ... | <-- Anonymous base class object
|----------------------|
| derived member1 |
| derived member2 |
| ... | <-- Derived class's own data members
|----------------------|
📐 Memory Layout Rule: Every derived class object has an anonymous base class object embedded within it. Base class members are stored first, then derived class members.
Constructors
- The anonymous object of the base class must be initialized using the constructor of the base class.
- When a derived class object is created, the constructor of the base class is executed before the constructor of the derived class.
Example:
class Parent {
public:
Parent(){ cout << “Parent Constructor...”; }
};
class Child : public Parent {
public:
Child(){ cout << “Child Constructor...”; }
};
int main(){
Child cobj;
return 0;
}
Output:
Parent Constructor...
Child Constructor...
Constructor Rules when No Default Constructor Exists
- If the default constructor of the base class does not exist, the compiler will try to generate a default constructor for the base class and execute it before the derived class constructor.
- If the user has provided only an overloaded constructor for the base class, the compiler will not generate a default constructor for the base class.
Example (Error):
class Parent {
public:
Parent(int i){} // Only non-default constructor exists
};
class Child : public Parent {
public:
Child(){} // ERROR: No default constructor for Parent to call
} Child_Object;
🔑 Definition — Default Constructor: A constructor that has either no parameters or all parameters with default values. It can be used to create an object without passing any arguments.
🔑 Definition — Implicit Default Constructor: The compiler generates this if no constructor is provided for the class.
🔑 Definition — Explicit Default Constructor: A user-defined constructor with no arguments or all arguments having default values.
Solution: Call the base class non-default constructor explicitly using the base class initializer in the derived class constructor's initializer list.
Base Class Initializer
C++ provides a mechanism to explicitly call a constructor of the base class from the derived class. The syntax is similar to member initializer lists and is called base-class initialization.
Example:
class Parent {
public:
Parent(int i){/*...*/};
};
class Child : public Parent {
public:
Child(int i): Parent(i) { /*...*/ } // Base class initializer
};
Another example:
class Parent {
public:
Parent(){ cout << “Parent Constructor...”; }
};
class Child : public Parent {
public:
Child(): Parent() { cout << “Child Constructor...”; }
};
Base Class Initializer with Member Initializer
User can provide base class initializer and member initializer simultaneously.
Example:
class Parent {
public:
Parent(){/*...*/}
};
class Child : public Parent {
int member;
public:
Child(): member(0), Parent() { /*...*/ }
};
Key Rule: The base class constructor is executed before the initialization of data members of the derived class, even if the base class initializer is written after the member initializer.
Initializing Members
- A derived class can only initialize members of the base class using overloaded constructors (via base class initializer).
- A derived class cannot initialize public data members of the base class using member initialization list.
Example (Error):
class Person {
public:
int age;
char *name;
Person();
};
class Student: public Person {
private:
int semester;
public:
Student(int a): age(a) { // error: cannot initialize base class member via member initializer
}
};
Reason: It would be an assignment, not an initialization, and base class members must be initialized by the base class constructor.
Destructors
- Destructors are called in reverse order of constructors.
- The derived class destructor is called before the base class destructor.
Example:
class Parent {
public:
Parent(){ cout << “Parent Constructor”; }
~Parent(){ cout << “Parent Destructor”; }
};
class Child : public Parent {
public:
Child(){ cout << “Child Constructor”; }
~Child(){ cout << “Child Destructor”; }
};
Output:
Parent Constructor
Child Constructor
Child Destructor
Parent Destructor
⭐ Key Takeaways
- Inheritance allows a derived class to reuse base class members, but private members of the base class are never accessible in the derived class — only public/protected members are accessible (depending on inheritance type). This enforces information hiding.
- Memory layout of a derived class object contains an anonymous base class object first, then derived class members. Constructors execute base first, then derived; destructors execute in reverse order (derived first, then base).
- If a base class has no default constructor, the derived class must explicitly call a base class constructor using the base class initializer syntax in its constructor’s initializer list (e.g.,
Child(int i): Parent(i) {...}). - A derived class cannot initialize base class data members directly using member initialization lists — it must rely on the base class constructor.
- The “IS A” relationship is fundamental: inheritance models that a derived class is a specialized version of the base class (e.g.,
Studentis aPerson).
🧠 Quick Revision Questions
- What is the difference between public, private, and protected inheritance in C++, and how does each affect access to base class members?
- Draw the memory layout of a derived class object. Which part gets constructed first: base class members or derived class members?
- Why does the code
Student(int a): age(a) { }cause an error? How would you correctly initialize theagemember inherited fromPerson? - What order are constructors and destructors executed when a derived class object is created and destroyed? Give the output for a base class “Parent” and derived class “Child” with print statements in each.
- How do you explicitly call a non-default base class constructor from a derived class? Write the syntax using a base class initializer.