Bytebook · OOPS
Object-Oriented Programming with C++
Move from C to C++ and master object-oriented design: classes, constructors, inheritance, polymorphism, templates, exceptions and the STL.
Chapter 1 · From C to C++
C++ Introduction
Where C++ came from, what it adds to C, your first program with iostream and namespaces, and how to compile it.
C++ was created by Bjarne Stroustrup at Bell Labs, starting in 1979 as "C with Classes". It keeps almost everything from C — the speed, the pointers, the control over memory — and adds tools for organising large programs: classes and objects, references, function overloading, templates, exceptions and a huge Standard Template Library (STL).
C++ powers game engines (Unreal), browsers (Chrome, Firefox), databases, trading systems, compilers and most performance-critical software you use.
Your first C++ program
#include <iostream>
int main() {
std::cout << "Hello, C++!" << std::endl;
std::cout << "2 + 3 = " << 2 + 3 << "\n";
return 0;
}Hello, C++! 2 + 3 = 5
| Line | Meaning |
|---|---|
#include <iostream> |
the input/output stream library (no .h) |
std::cout |
the standard character output stream — the screen |
<< |
the insertion operator: sends the value on its right into the stream |
std::endl |
ends the line and flushes the output; "\n" just ends the line |
std:: |
everything in the standard library lives in the namespace std |
Namespaces and using
A namespace groups names to avoid clashes — two libraries can both have a sort function if they live in different namespaces. Writing std:: everywhere is precise; using namespace std; lets you drop the prefix:
#include <iostream>
using namespace std;
namespace physics { double g = 9.8; }
namespace games { double g = 1.6; } // same name, different namespace
int main() {
cout << "Earth gravity: " << physics::g << endl;
cout << "Moon gravity in our game: " << games::g << endl;
return 0;
}Earth gravity: 9.8 Moon gravity in our game: 1.6
Compiling
g++ hello.cpp -o hello # g++ is the C++ compiler from GCC
./hello
g++ -std=c++17 -Wall hello.cpp -o hello # pick a standard and turn on warningsWhat C++ adds to C
| Feature | C | C++ |
|---|---|---|
| Programming style | procedural | procedural and object-oriented and generic |
| Input/output | printf, scanf |
cout, cin (type-safe) |
| Data + functions together | no | classes |
| References | no | int &r = x; |
| Function overloading | no | yes |
| Default arguments | no | yes |
| Dynamic memory | malloc / free |
new / delete |
| Strings | char arrays |
std::string |
| Error handling | return codes | exceptions |
| Generic code | macros, void * |
templates |
| Ready-made data structures | none | STL: vector, map, set, … |
Almost any valid C program is also a valid C++ program, so everything from CSE 101 still applies.
Chapter 2 · From C to C++
Input/Output, Strings & References
Read and write with cin and cout, use the std::string class, format output with iomanip, and meet references — C++'s safer alias for a variable.
cin: reading input
cin (character input) reads values with the extraction operator >>. It works out the type from the variable — no format specifiers and no &:
#include <iostream>
#include <string>
using namespace std;
int main() {
string name;
int age;
double cgpa;
cout << "Name, age and CGPA: ";
cin >> name >> age >> cgpa;
cout << name << " is " << age << " with a CGPA of " << cgpa << endl;
return 0;
}Name, age and CGPA: Ravi 19 8.6
Ravi is 19 with a CGPA of 8.6Like scanf("%s"), cin >> name stops at whitespace. To read a whole line, use getline:
#include <iostream>
#include <string>
using namespace std;
int main() {
int roll;
string fullName;
cout << "Roll number: ";
cin >> roll;
cin.ignore(); // skip the newline left after the number
cout << "Full name: ";
getline(cin, fullName);
cout << "#" << roll << ": " << fullName << " (" << fullName.length() << " characters)" << endl;
return 0;
}Roll number: 21 Full name: Ravi Kumar Sharma #21: Ravi Kumar Sharma (17 characters)
The string class
std::string grows automatically, knows its own length, and supports +, == and < directly:
#include <iostream>
#include <string>
using namespace std;
int main() {
string a = "Data", b = "Structures";
string full = a + " " + b; // concatenation
cout << full << " has " << full.size() << " characters" << endl;
cout << "First letter: " << full[0] << ", last: " << full.back() << endl;
cout << "Substring: " << full.substr(5, 6) << endl; // from index 5, length 6
cout << "\"Struct\" found at " << full.find("Struct") << endl;
full.replace(0, 4, "Algo");
cout << "After replace: " << full << endl;
cout << boolalpha << ("apple" < string("banana")) << endl; // dictionary order
for (char c : string("abc")) cout << char(c - 32); // range-based for
cout << endl;
return 0;
}Data Structures has 15 characters First letter: D, last: s Substring: Struct "Struct" found at 5 After replace: Algo Structures true ABC
Formatting with iomanip
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double pi = 3.14159265;
cout << fixed << setprecision(2) << pi << endl; // 2 decimal places
cout << setw(8) << 42 << "|" << endl; // width 8, right-aligned
cout << left << setw(8) << 42 << "|" << endl; // left-aligned
cout << setfill('0') << right << setw(5) << 7 << endl; // pad with zeros
cout << hex << 255 << " " << oct << 8 << dec << endl; // number bases
return 0;
}3.14
42|
42 |
00007
ff 10References
A reference is another name (an alias) for an existing variable. It must be initialised when declared and can never be re-seated to refer to something else.
#include <iostream>
using namespace std;
int main() {
int marks = 70;
int &r = marks; // r is another name for marks
r += 15;
cout << "marks = " << marks << ", r = " << r << endl;
cout << "same address? " << (&marks == &r ? "yes" : "no") << endl;
return 0;
}marks = 85, r = 85 same address? yes
The real power of references is in function parameters — the subject of the next lesson.
| Pointer | Reference |
|---|---|
can be nullptr |
always refers to a valid object |
| can be re-pointed later | bound once, forever |
needs * to read, & to take an address |
used like the variable itself |
| can do arithmetic | no arithmetic |
| has its own memory | usually just an alias |
bool and auto
C++ has a built-in bool type with true and false. And auto lets the compiler deduce a variable's type from its initialiser:
#include <iostream>
#include <vector>
using namespace std;
int main() {
bool passed = true;
auto count = 42; // int
auto ratio = 0.75; // double
auto name = string("Asha"); // std::string
cout << boolalpha << passed << " " << count << " " << ratio << " " << name << endl;
vector<int> v = {3, 1, 4};
for (auto x : v) cout << x * 10 << " ";
cout << endl;
return 0;
}true 42 0.75 Asha 30 10 40
Chapter 3 · From C to C++
Functions: Overloading, Defaults & Inline
What C++ adds to functions: pass by reference, const references, function overloading, default arguments and inline functions.
Pass by reference
A reference parameter lets a function change the caller's variable — without pointers:
#include <iostream>
using namespace std;
void swapValues(int &a, int &b) { // a and b are aliases for the caller's variables
int t = a;
a = b;
b = t;
}
int main() {
int x = 3, y = 8;
swapValues(x, y); // no & needed at the call
cout << "x = " << x << ", y = " << y << endl;
return 0;
}x = 8, y = 3
Compare the three ways of passing an argument:
| By value | By pointer (C style) | By reference | |
|---|---|---|---|
| Declaration | void f(int a) |
void f(int *a) |
void f(int &a) |
| Call | f(x) |
f(&x) |
f(x) |
| Can change caller's variable | no | yes | yes |
| Copies the argument | yes | no (copies an address) | no |
const references
Passing a large object (like a long string or vector) by value copies the whole thing. Pass it as a const reference instead: no copy, and the function promises not to change it.
#include <iostream>
#include <string>
using namespace std;
int countVowels(const string &s) { // fast and read-only
int n = 0;
for (char c : s)
if (string("aeiouAEIOU").find(c) != string::npos) n++;
return n;
}
int main() {
string text = "Object Oriented Programming";
cout << countVowels(text) << " vowels" << endl;
return 0;
}9 vowels
Function overloading
Several functions can share a name if their parameter lists differ (in number or types). The compiler picks the right one from the arguments — this is compile-time polymorphism.
#include <iostream>
using namespace std;
int area(int side) { return side * side; } // square
int area(int length, int width) { return length * width; } // rectangle
double area(double radius) { return 3.14159 * radius * radius; } // circle
int main() {
cout << "square 4: " << area(4) << endl;
cout << "rectangle 4x6: " << area(4, 6) << endl;
cout << "circle 2.0: " << area(2.0) << endl;
return 0;
}square 4: 16 rectangle 4x6: 24 circle 2.0: 12.5664
Default arguments
Parameters can have default values, used when the caller leaves them out. Defaults must come last in the parameter list:
#include <iostream>
using namespace std;
double simpleInterest(double principal, double rate = 7.5, int years = 1) {
return principal * rate * years / 100;
}
int main() {
cout << simpleInterest(10000) << endl; // rate 7.5, 1 year
cout << simpleInterest(10000, 8) << endl; // 1 year
cout << simpleInterest(10000, 8, 3) << endl;
return 0;
}750 800 2400
Inline functions
inline suggests that the compiler paste the function body at each call, avoiding call overhead for tiny functions. It is a safer replacement for function-like macros because arguments are evaluated once and type-checked:
#include <iostream>
using namespace std;
inline int cube(int x) { return x * x * x; }
#define CUBE(x) x * x * x
int main() {
cout << "inline cube(1 + 2) = " << cube(1 + 2) << endl; // 27
cout << "macro CUBE(1 + 2) = " << CUBE(1 + 2) << endl; // 1 + 2*1 + 2*1 + 2 = 7
return 0;
}inline cube(1 + 2) = 27 macro CUBE(1 + 2) = 7
Modern compilers decide on inlining by themselves, so inline is mostly a hint (and today it matters mainly for functions defined in headers).
Chapter 4 · Classes & Objects
OOP Concepts
Why object-oriented programming exists, the difference from procedural code, and the four pillars — encapsulation, abstraction, inheritance and polymorphism.
Procedural programming (what you did in C) organises a program around functions that act on separate data. As programs grow, any function can modify any data, and changes ripple everywhere.
Object-oriented programming (OOP) organises a program around objects — bundles of data (attributes) and the functions that operate on it (methods). A bank account object owns its balance; nothing else may change it except through the account's own deposit and withdraw methods.
| Procedural | Object-oriented |
|---|---|
| program = functions + separate data | program = interacting objects |
| top-down design | bottom-up design: build objects, then combine them |
| data is often global and exposed | data is hidden inside objects |
| harder to reuse and extend | reuse through inheritance and composition |
| C, Pascal, Fortran | C++, Java, Python, C# |
Class and object
- A class is a blueprint: it describes what data and behaviour objects of that kind have.
- An object is an instance of a class: a real thing built from the blueprint, with its own data.
Car is a class; your red Swift with registration KA-01-1234 is an object.
The four pillars
1. Encapsulation
Bundling data with the methods that use it, and hiding the data from outside code (making it private). Outside code must go through public methods, which can check every change.
A capsule hides the medicine inside; you just swallow it.
2. Abstraction
Showing what an object does while hiding how it does it. You call car.start() without knowing about fuel injection. In C++, abstraction comes from public interfaces, abstract classes and header files.
3. Inheritance
Creating a new class from an existing one, reusing its members and adding or changing some. A Student and a Teacher can both inherit from Person. It models an is-a relationship: a Student is a Person.
4. Polymorphism
"Many forms": the same call behaves differently depending on the object. shape->area() computes a circle's area for a Circle and a rectangle's for a Rectangle.
- Compile-time polymorphism: function and operator overloading.
- Run-time polymorphism: virtual functions.
A preview in code
You will understand every line by the end of this unit:
#include <iostream>
#include <string>
using namespace std;
class Shape { // abstraction: what every shape can do
public:
virtual double area() const = 0;
virtual string name() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape { // inheritance
double r; // encapsulation: hidden data
public:
Circle(double r) : r(r) {}
double area() const override { return 3.14159 * r * r; }
string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
string name() const override { return "Rectangle"; }
};
int main() {
Circle c(1.5);
Rectangle r(4, 2.5);
const Shape *shapes[] = {&c, &r};
for (const Shape *s : shapes) // polymorphism: one call, many behaviours
cout << s->name() << " area = " << s->area() << endl;
return 0;
}Circle area = 7.06858 Rectangle area = 10
Other OOP terms
| Term | Meaning |
|---|---|
| Message passing | objects interact by calling each other's methods |
| Dynamic binding | the method to run is chosen at run time (virtual functions) |
| Composition | a class contains objects of other classes — a has-a relationship (a Car has an Engine) |
| Coupling | how much classes depend on each other — keep it low |
| Cohesion | how focused a class is on one job — keep it high |
Chapter 5 · Classes & Objects
Classes & Objects
Define a class with data members and member functions, create objects, control access with public and private, and define methods outside the class with ::.
Defining a class
#include <iostream>
#include <string>
using namespace std;
class Student {
public: // accessible from anywhere
string name;
int roll;
double marks[3];
double average() { // member function (method)
return (marks[0] + marks[1] + marks[2]) / 3;
}
void print() {
cout << roll << " " << name << " avg " << average() << endl;
}
}; // note the semicolon!
int main() {
Student s1; // create an object
s1.name = "Asha";
s1.roll = 7;
s1.marks[0] = 88; s1.marks[1] = 92; s1.marks[2] = 79;
s1.print();
Student s2 = s1; // copy: s2 has its own data
s2.name = "Ravi";
s2.roll = 8;
s1.print();
s2.print();
return 0;
}7 Asha avg 86.3333 7 Asha avg 86.3333 8 Ravi avg 86.3333
Each object has its own copy of the data members; member functions are shared by all objects of the class.
Access specifiers
| Specifier | Accessible from |
|---|---|
public |
anywhere |
private |
only inside the class (and its friends) |
protected |
inside the class and classes derived from it |
In a class, members are private by default. In a struct, they are public by default — that is the only difference between the two in C++.
#include <iostream>
using namespace std;
class BankAccount {
private:
double balance = 0; // hidden: no one outside can set it to -1000
public:
void deposit(double amount) {
if (amount > 0) balance += amount;
}
bool withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
double getBalance() { return balance; }
};
int main() {
BankAccount acc;
acc.deposit(5000);
cout << "withdraw 7000: " << (acc.withdraw(7000) ? "ok" : "refused") << endl;
cout << "withdraw 1200: " << (acc.withdraw(1200) ? "ok" : "refused") << endl;
cout << "balance: " << acc.getBalance() << endl;
// acc.balance = 1000000; // error: 'balance' is a private member
return 0;
}withdraw 7000: refused withdraw 1200: ok balance: 3800
Defining methods outside the class
For longer methods, declare them inside the class and define them outside using the scope resolution operator ::. This is how real projects split code between a header (.h) and a source file (.cpp).
#include <iostream>
using namespace std;
class Rectangle {
double length, width;
public:
void set(double l, double w);
double area();
double perimeter();
};
void Rectangle::set(double l, double w) { length = l; width = w; }
double Rectangle::area() { return length * width; }
double Rectangle::perimeter() { return 2 * (length + width); }
int main() {
Rectangle r;
r.set(7, 3);
cout << "area " << r.area() << ", perimeter " << r.perimeter() << endl;
return 0;
}area 21, perimeter 20
Arrays of objects and objects as arguments
#include <iostream>
#include <string>
using namespace std;
class Item {
public:
string name;
int qty;
double price;
double total() const { return qty * price; }
};
double billTotal(const Item items[], int n) {
double sum = 0;
for (int i = 0; i < n; i++) sum += items[i].total();
return sum;
}
int main() {
Item cart[3] = {{"Notebook", 4, 45.0}, {"Pen", 10, 12.5}, {"Geometry box", 1, 180.0}};
for (const Item &it : cart)
cout << it.name << " x" << it.qty << " = " << it.total() << endl;
cout << "Bill: Rs " << billTotal(cart, 3) << endl;
return 0;
}Notebook x4 = 180 Pen x10 = 125 Geometry box x1 = 180 Bill: Rs 485
Memory for objects
sizeof an object counts only its data members (plus padding). Member functions are stored once, in the code, not inside each object:
#include <iostream>
using namespace std;
class Point {
int x, y;
public:
void move(int dx, int dy) { x += dx; y += dy; }
int getX() { return x; }
};
int main() {
cout << "sizeof(Point) = " << sizeof(Point) << " bytes (two ints)" << endl;
return 0;
}sizeof(Point) = 8 bytes (two ints)
Chapter 6 · Classes & Objects
Constructors & Destructors
Initialise objects automatically: default, parameterised and copy constructors, initialiser lists, constructor overloading, and destructors with the order they run in.
A constructor is a special member function that runs automatically when an object is created. Its job is to put the object into a valid starting state.
- Same name as the class.
- No return type — not even
void. - Can be overloaded.
Default and parameterised constructors
#include <iostream>
#include <string>
using namespace std;
class Student {
string name;
int roll;
public:
Student() { // default constructor: no arguments
name = "Unknown";
roll = 0;
cout << "default constructor" << endl;
}
Student(string n, int r) { // parameterised constructor
name = n;
roll = r;
cout << "parameterised constructor for " << n << endl;
}
void show() { cout << roll << ": " << name << endl; }
};
int main() {
Student a; // calls Student()
Student b("Meera", 12); // calls Student(string, int)
Student c = {"Kiran", 13}; // also the parameterised one
a.show(); b.show(); c.show();
return 0;
}default constructor parameterised constructor for Meera parameterised constructor for Kiran 0: Unknown 12: Meera 13: Kiran
Initialiser lists
The preferred way to initialise members is a member initialiser list, after a colon. It initialises members directly instead of assigning them afterwards, and it is required for const members, reference members and base classes.
#include <iostream>
using namespace std;
class Circle {
const double PI; // const members must use the initialiser list
double radius;
public:
Circle(double r = 1.0) : PI(3.14159), radius(r) {} // also a default constructor
double area() const { return PI * radius * radius; }
};
int main() {
Circle unit, big(10);
cout << unit.area() << " " << big.area() << endl;
return 0;
}3.14159 314.159
Copy constructor
The copy constructor creates a new object as a copy of an existing one. It takes a const reference to an object of the same class. It runs when you:
- initialise one object from another:
Point b = a;orPoint b(a); - pass an object by value to a function;
- return an object by value (often optimised away).
#include <iostream>
using namespace std;
class Point {
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
Point(const Point &other) : x(other.x), y(other.y) { // copy constructor
cout << "copying (" << x << ", " << y << ")" << endl;
}
void show() const { cout << "(" << x << ", " << y << ")" << endl; }
};
void display(Point p) { p.show(); } // by value: copies
int main() {
Point a(3, 4);
Point b = a; // copy constructor
display(a); // copy constructor again
b.show();
return 0;
}copying (3, 4) copying (3, 4) (3, 4) (3, 4)
Destructors
A destructor runs automatically when an object is destroyed — when it goes out of scope, or when you delete it. Its name is the class name with a tilde: ~Student(). It takes no arguments, returns nothing, and cannot be overloaded. Use it to release resources: memory, files, network connections.
#include <iostream>
#include <string>
using namespace std;
class Tracer {
string name;
public:
Tracer(string n) : name(n) { cout << " construct " << name << endl; }
~Tracer() { cout << " destroy " << name << endl; }
};
Tracer global("global");
int main() {
cout << "main starts" << endl;
Tracer a("a");
{
Tracer b("b");
Tracer c("c");
cout << " leaving inner block" << endl;
} // c then b destroyed here
Tracer *d = new Tracer("d (heap)");
delete d; // destroyed only when deleted
cout << "main ends" << endl;
return 0;
} // a destroyed, then globalconstruct global main starts construct a construct b construct c leaving inner block destroy c destroy b construct d (heap) destroy d (heap) main ends destroy a destroy global
Objects are destroyed in the reverse order of construction — like a stack.
Managing a resource: a dynamic array class
#include <iostream>
using namespace std;
class IntArray {
int *data;
int size;
public:
IntArray(int n) : data(new int[n]()), size(n) {} // () zero-initialises
IntArray(const IntArray &o) : data(new int[o.size]), size(o.size) { // deep copy
for (int i = 0; i < size; i++) data[i] = o.data[i];
}
~IntArray() { delete[] data; }
int &at(int i) { return data[i]; }
int length() const { return size; }
};
int main() {
IntArray a(5);
for (int i = 0; i < a.length(); i++) a.at(i) = i * i;
IntArray b = a; // deep copy: b gets its own memory
b.at(0) = 100;
cout << "a[0] = " << a.at(0) << ", b[0] = " << b.at(0) << endl;
return 0;
}a[0] = 0, b[0] = 100
Without the copy constructor, b would share a's pointer (a shallow copy): changing b would change a, and both destructors would delete[] the same memory. More on this in Dynamic Memory & Smart Pointers.
Chapter 7 · Classes & Objects
Encapsulation, this & static Members
Getters and setters with validation, the this pointer, const member functions, and static data and functions shared by every object.
Getters and setters
Encapsulation means keeping data private and exposing controlled access. A setter can validate input; a getter can return a value without exposing the variable itself.
#include <iostream>
#include <string>
using namespace std;
class Employee {
string name;
double salary = 0;
public:
void setName(const string &n) { if (!n.empty()) name = n; }
void setSalary(double s) {
if (s < 0) { cout << "Rejected negative salary " << s << endl; return; }
salary = s;
}
string getName() const { return name; }
double getSalary() const { return salary; }
};
int main() {
Employee e;
e.setName("Divya");
e.setSalary(65000);
e.setSalary(-500); // the class protects its own data
cout << e.getName() << " earns " << e.getSalary() << endl;
return 0;
}Rejected negative salary -500 Divya earns 65000
The this pointer
Inside a member function, this is a pointer to the object the function was called on. You use it to tell members apart from parameters with the same name, and to return the object itself (which allows method chaining):
#include <iostream>
#include <string>
using namespace std;
class Pizza {
string size = "medium";
int toppings = 0;
public:
Pizza &setSize(const string &size) { this->size = size; return *this; } // this->size is the member
Pizza &addTopping() { toppings++; return *this; }
void print() const { cout << size << " pizza with " << toppings << " toppings" << endl; }
};
int main() {
Pizza p;
p.setSize("large").addTopping().addTopping().addTopping(); // chaining
p.print();
return 0;
}large pizza with 3 toppings
const member functions
A member function marked const promises not to modify the object. Only const functions can be called on const objects and through const references — so mark every getter const.
class Account {
double balance;
public:
double getBalance() const { return balance; } // OK: only reads
void reset() const { balance = 0; } // error: modifies a member in a const function
};static data members
A static data member belongs to the class, not to any object: there is exactly one copy shared by all objects. It must be defined once outside the class (or declared inline static in C++17).
static member functions
A static member function can be called without any object, as ClassName::function(). It has no this pointer, so it can only use static members.
#include <iostream>
#include <string>
using namespace std;
class Student {
string name;
int id;
static int count; // one shared counter
public:
Student(const string &n) : name(n), id(++count) {}
~Student() { count--; }
void show() const { cout << "#" << id << " " << name << endl; }
static int total() { return count; } // no object needed
};
int Student::count = 0; // definition of the static member
int main() {
cout << "students: " << Student::total() << endl;
Student a("Asha"), b("Ravi");
{
Student c("Meera");
c.show();
cout << "students: " << Student::total() << endl;
}
a.show(); b.show();
cout << "students: " << Student::total() << endl;
return 0;
}students: 0 #3 Meera students: 3 #1 Asha #2 Ravi students: 2
| Regular member | static member | |
|---|---|---|
| Copies | one per object | one per class |
| Accessed via | an object: s.name |
the class: Student::count |
Function has this? |
yes | no |
| Can use non-static members? | yes | no |
Chapter 8 · Classes & Objects
Friend Functions & Classes
Grant selected functions or classes access to private members — when it is useful, how it works, and why to use it sparingly.
Private members are hidden from everything outside the class. Sometimes a function that is not a member genuinely needs that access — for example, a function that compares two objects of different classes, or an operator like << whose left side is cout. A class can declare such a function its friend.
Friend function
#include <iostream>
using namespace std;
class Box {
double length, width, height;
public:
Box(double l, double w, double h) : length(l), width(w), height(h) {}
friend double volume(const Box &b); // grant access
};
double volume(const Box &b) { // NOT a member: no Box:: and no this
return b.length * b.width * b.height; // but it may read private members
}
int main() {
Box b(2, 3, 4);
cout << "Volume: " << volume(b) << endl;
return 0;
}Volume: 24
Properties of a friend function:
- It is declared inside the class with
friend, but it is not a member — it is called like a normal function,volume(b), notb.volume(). - It can access the class's
privateandprotectedmembers through an object. - The declaration can go in the
publicorprivatesection; it makes no difference.
A friend of two classes
A classic use: one function needs the private data of two classes.
#include <iostream>
using namespace std;
class Rupees; // forward declaration
class Dollars {
double amount;
public:
Dollars(double a) : amount(a) {}
friend bool richer(const Dollars &d, const Rupees &r);
};
class Rupees {
double amount;
public:
Rupees(double a) : amount(a) {}
friend bool richer(const Dollars &d, const Rupees &r);
};
bool richer(const Dollars &d, const Rupees &r) {
const double rate = 84.0; // rupees per dollar
return d.amount * rate > r.amount;
}
int main() {
Dollars savingsUS(150);
Rupees savingsIN(11000);
cout << (richer(savingsUS, savingsIN) ? "Dollar account is bigger" : "Rupee account is bigger") << endl;
return 0;
}Dollar account is bigger
Friend class
A whole class can be a friend: all its member functions get access.
#include <iostream>
using namespace std;
class Engine {
int horsepower = 120;
friend class Mechanic; // Mechanic may see inside
};
class Mechanic {
public:
void tune(Engine &e) { e.horsepower += 15; }
int inspect(const Engine &e) { return e.horsepower; }
};
int main() {
Engine e;
Mechanic m;
cout << "before: " << m.inspect(e) << " hp" << endl;
m.tune(e);
cout << "after: " << m.inspect(e) << " hp" << endl;
return 0;
}before: 120 hp after: 135 hp
Friendship rules
- Not mutual: if A declares B a friend, B can access A, but A cannot access B.
- Not inherited: a friend of the base class is not automatically a friend of derived classes.
- Not transitive: a friend of a friend is not a friend.
Chapter 9 · Inheritance & Polymorphism
Inheritance
Reuse and extend classes: base and derived classes, public/protected/private inheritance, the five types of inheritance, constructor order, and the diamond problem.
Inheritance lets a new class (the derived or child class) take on the members of an existing class (the base or parent class), then add its own or change some. It models an is-a relationship and avoids writing the same code twice.
#include <iostream>
#include <string>
using namespace std;
class Person { // base class
protected:
string name;
int age;
public:
Person(const string &n, int a) : name(n), age(a) {}
void introduce() const { cout << "I am " << name << ", " << age << " years old." << endl; }
};
class Student : public Person { // Student is-a Person
string course;
public:
Student(const string &n, int a, const string &c) : Person(n, a), course(c) {}
void study() const { cout << name << " is studying " << course << "." << endl; } // can use protected name
};
int main() {
Student s("Asha", 19, "Data Structures");
s.introduce(); // inherited from Person
s.study(); // Student's own
return 0;
}I am Asha, 19 years old. Asha is studying Data Structures.
Access in derived classes
A derived class can use the base's public and protected members, but never its private ones. The mode of inheritance (public, protected, private) then decides how inherited members appear to the outside world:
| Base member | public inheritance |
protected inheritance |
private inheritance |
|---|---|---|---|
public |
public | protected | private |
protected |
protected | protected | private |
private |
not accessible | not accessible | not accessible |
public inheritance is by far the most common — it is the one that means "is-a".
Types of inheritance
| Type | Shape | Example |
|---|---|---|
| Single | one base → one derived | Car : Vehicle |
| Multilevel | chain: A → B → C | Vehicle → Car → ElectricCar |
| Multiple | one class, several bases | SmartPhone : Phone, Camera |
| Hierarchical | one base, several derived | Shape → Circle, Square, Triangle |
| Hybrid | a mix of the above | multiple + multilevel together |
Multilevel inheritance and constructor order
Constructors run from the base down to the most derived class; destructors run in the reverse order.
#include <iostream>
using namespace std;
class Vehicle {
public:
Vehicle() { cout << "Vehicle constructor" << endl; }
~Vehicle() { cout << "Vehicle destructor" << endl; }
};
class Car : public Vehicle {
public:
Car() { cout << " Car constructor" << endl; }
~Car() { cout << " Car destructor" << endl; }
};
class ElectricCar : public Car {
public:
ElectricCar() { cout << " ElectricCar constructor" << endl; }
~ElectricCar() { cout << " ElectricCar destructor" << endl; }
};
int main() {
ElectricCar tesla;
cout << "--- object in use ---" << endl;
return 0;
}Vehicle constructor
Car constructor
ElectricCar constructor
--- object in use ---
ElectricCar destructor
Car destructor
Vehicle destructorMultiple inheritance
#include <iostream>
using namespace std;
class Camera {
public:
void takePhoto() const { cout << "Click! Photo saved." << endl; }
};
class Phone {
public:
void call(const char *who) const { cout << "Calling " << who << "..." << endl; }
};
class SmartPhone : public Phone, public Camera { // two bases
public:
void browse() const { cout << "Opening browser." << endl; }
};
int main() {
SmartPhone p;
p.call("Mom");
p.takePhoto();
p.browse();
return 0;
}Calling Mom... Click! Photo saved. Opening browser.
The diamond problem and virtual base classes
In hybrid inheritance, a class can inherit the same base twice:
Without help, TA would contain two copies of Person, and ta.name would be ambiguous. Declaring the shared base virtual makes the derived classes share one copy:
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
Person(const string &n = "") : name(n) { cout << "Person(" << n << ")" << endl; }
};
class Student : virtual public Person { public: Student() { cout << "Student" << endl; } };
class Employee : virtual public Person { public: Employee() { cout << "Employee" << endl; } };
class TA : public Student, public Employee {
public:
TA(const string &n) : Person(n) { cout << "TA" << endl; } // the most derived class builds the shared base
};
int main() {
TA ta("Nikhil");
cout << "name = " << ta.name << " (only one Person inside)" << endl;
return 0;
}Person(Nikhil) Student Employee TA name = Nikhil (only one Person inside)
Overriding (redefining) base functions
A derived class can define a function with the same signature as the base, replacing its behaviour. The base version is still reachable with Base::function():
#include <iostream>
using namespace std;
class Account {
public:
double interestRate() const { return 3.0; }
};
class SeniorCitizenAccount : public Account {
public:
double interestRate() const { return Account::interestRate() + 0.5; } // reuse, then extend
};
int main() {
Account a;
SeniorCitizenAccount s;
cout << "regular: " << a.interestRate() << "%, senior: " << s.interestRate() << "%" << endl;
return 0;
}regular: 3%, senior: 3.5%
Without virtual, which version runs depends on the declared type of the pointer or reference — the problem that virtual functions solve.
Chapter 10 · Inheritance & Polymorphism
Operator Overloading
Make your own types work with +, ==, <<, [] and more: member vs friend operators, unary and binary operators, and the rules you must follow.
Operator overloading lets you define what operators mean for your own classes, so code reads naturally: c1 + c2 instead of add(c1, c2). It is a form of compile-time polymorphism.
An overloaded operator is just a function named operator followed by the symbol: operator+, operator==, operator<<.
Binary operators as members
For a + b written as a member function, a is the object the function is called on and b is the parameter.
#include <iostream>
using namespace std;
class Complex {
double re, im;
public:
Complex(double r = 0, double i = 0) : re(r), im(i) {}
Complex operator+(const Complex &o) const { return Complex(re + o.re, im + o.im); }
Complex operator-(const Complex &o) const { return Complex(re - o.re, im - o.im); }
Complex operator*(const Complex &o) const {
return Complex(re * o.re - im * o.im, re * o.im + im * o.re); // (a+bi)(c+di)
}
bool operator==(const Complex &o) const { return re == o.re && im == o.im; }
void print() const { cout << re << (im < 0 ? " - " : " + ") << (im < 0 ? -im : im) << "i" << endl; }
};
int main() {
Complex a(3, 2), b(1, -4);
Complex sum = a + b; // calls a.operator+(b)
sum.print();
(a - b).print();
(a * b).print();
cout << boolalpha << (a + b == Complex(4, -2)) << endl;
return 0;
}4 - 2i 2 + 6i 11 - 10i true
Unary operators
Prefix and postfix ++ are told apart by a dummy int parameter on the postfix version:
#include <iostream>
using namespace std;
class Counter {
int value;
public:
Counter(int v = 0) : value(v) {}
Counter &operator++() { ++value; return *this; } // prefix: ++c
Counter operator++(int) { Counter old = *this; ++value; return old; } // postfix: c++
Counter operator-() const { return Counter(-value); } // unary minus
int get() const { return value; }
};
int main() {
Counter c(5);
Counter a = c++; // a gets 5, c becomes 6
Counter b = ++c; // c becomes 7, b gets 7
cout << a.get() << " " << b.get() << " " << c.get() << " " << (-c).get() << endl;
return 0;
}5 7 7 -7
Overloading << and >> with friends
cout << obj has cout (an ostream) on the left, so it cannot be a member of your class. Write it as a non-member — usually a friend so it can read private data — and return the stream to allow chaining:
#include <iostream>
using namespace std;
class Fraction {
int num, den;
static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
void reduce() { int g = gcd(num < 0 ? -num : num, den); if (g) { num /= g; den /= g; } }
public:
Fraction(int n = 0, int d = 1) : num(n), den(d) { reduce(); }
Fraction operator+(const Fraction &o) const { return Fraction(num * o.den + o.num * den, den * o.den); }
friend ostream &operator<<(ostream &out, const Fraction &f) { return out << f.num << "/" << f.den; }
friend istream &operator>>(istream &in, Fraction &f) { in >> f.num >> f.den; f.reduce(); return in; }
};
int main() {
Fraction a(1, 2), b;
cout << "Enter a fraction (numerator denominator): ";
cin >> b;
cout << a << " + " << b << " = " << a + b << endl;
cout << "6/8 simplifies to " << Fraction(6, 8) << endl;
return 0;
}Enter a fraction (numerator denominator): 7 3
1/2 + 7/3 = 17/6
6/8 simplifies to 3/4Subscript and comparison operators
#include <iostream>
#include <stdexcept>
using namespace std;
class SafeArray {
int data[5] = {10, 20, 30, 40, 50};
public:
int &operator[](int i) { // returns a reference so a[i] = x works
if (i < 0 || i >= 5) throw out_of_range("index " + to_string(i));
return data[i];
}
};
int main() {
SafeArray a;
a[2] = 99;
cout << a[2] << endl;
try { cout << a[7] << endl; }
catch (const out_of_range &e) { cout << "caught bad " << e.what() << endl; }
return 0;
}99 caught bad index 7
Rules
- You cannot create new operators or change precedence, associativity or the number of operands.
- At least one operand must be a user-defined type (you cannot redefine
int + int). - These cannot be overloaded:
::..*?:sizeoftypeid. =,(),[]and->must be member functions.<<and>>for streams must be non-members (usually friends).
| Member operator | Friend / non-member operator |
|---|---|
left operand is *this |
both operands are parameters |
| one parameter for binary operators | two parameters for binary operators |
| left operand must be your class | left operand can be any type (cout << x, 5 + x) |
Chapter 11 · Inheritance & Polymorphism
Virtual Functions & Runtime Polymorphism
Base-class pointers to derived objects, why virtual changes which function runs, override, virtual destructors, and how the vtable makes it work.
The problem: static binding
A base-class pointer can point to a derived object. But by default, calling a function through it runs the base version — the compiler decides at compile time from the pointer's type. This is static (early) binding.
#include <iostream>
using namespace std;
class Animal {
public:
void speak() const { cout << "..." << endl; } // NOT virtual
};
class Dog : public Animal {
public:
void speak() const { cout << "Woof!" << endl; }
};
int main() {
Dog d;
Animal *p = &d; // base pointer, derived object
d.speak(); // Woof!
p->speak(); // "..." — chosen by the pointer type
return 0;
}Woof! ...
The fix: virtual functions
Mark the base function virtual. Now the function is chosen at run time from the actual object — dynamic (late) binding. This is runtime polymorphism.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Employee {
protected:
string name;
double base;
public:
Employee(const string &n, double b) : name(n), base(b) {}
virtual double pay() const { return base; }
virtual string role() const { return "Employee"; }
virtual ~Employee() {}
void payslip() const { cout << name << " (" << role() << "): Rs " << pay() << endl; }
};
class Manager : public Employee {
double bonus;
public:
Manager(const string &n, double b, double bo) : Employee(n, b), bonus(bo) {}
double pay() const override { return base + bonus; }
string role() const override { return "Manager"; }
};
class Intern : public Employee {
public:
Intern(const string &n) : Employee(n, 15000) {}
string role() const override { return "Intern"; } // keeps Employee::pay
};
int main() {
Manager m("Priya", 90000, 25000);
Intern i("Rahul");
Employee e("Sanjay", 60000);
vector<const Employee *> staff = {&e, &m, &i};
for (const Employee *s : staff) s->payslip(); // one loop, three behaviours
return 0;
}Sanjay (Employee): Rs 60000 Priya (Manager): Rs 115000 Rahul (Intern): Rs 15000
payslip() is written once in the base class, yet it calls the right pay() and role() for each object. Adding a new kind of employee requires no change to payslip — that is the real power of polymorphism.
Virtual destructors
If you delete a derived object through a base pointer and the base destructor is not virtual, only the base destructor runs — the derived part leaks.
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { cout << "~Base" << endl; } // remove 'virtual' and ~Derived is skipped
};
class Derived : public Base {
int *buffer = new int[1000];
public:
~Derived() { delete[] buffer; cout << "~Derived (freed buffer)" << endl; }
};
int main() {
Base *p = new Derived;
delete p; // runs ~Derived, then ~Base
return 0;
}~Derived (freed buffer) ~Base
How it works: the vtable
For each class with virtual functions, the compiler builds a virtual table (vtable): an array of pointers to that class's versions of the virtual functions. Each object gets a hidden pointer (the vptr) to its class's vtable. A virtual call follows object → vptr → vtable → function. That is one extra pointer lookup per call, and one extra pointer per object:
#include <iostream>
using namespace std;
class Plain { int x = 0; public: int f() { return x; } };
class Virtual { int x = 0; public: virtual int f() { return x; } };
int main() {
cout << "sizeof(Plain) = " << sizeof(Plain) << endl;
cout << "sizeof(Virtual) = " << sizeof(Virtual) << " (int + hidden vptr, padded)" << endl;
return 0;
}sizeof(Plain) = 4 sizeof(Virtual) = 16 (int + hidden vptr, padded)
| Static binding | Dynamic binding |
|---|---|
| decided at compile time | decided at run time |
| normal and overloaded functions | virtual functions called through pointers/references |
| faster | tiny overhead (vtable lookup) |
| also called early binding | also called late binding |
Chapter 12 · Inheritance & Polymorphism
Abstract Classes & Interfaces
Pure virtual functions, abstract base classes that cannot be instantiated, interfaces, and the final keyword — designing a clean class hierarchy.
Pure virtual functions
Sometimes a base class cannot sensibly implement a function. What is the area of a generic "Shape"? Declare it pure virtual with = 0: it has no body in the base, and every concrete derived class must override it.
virtual double area() const = 0; // pure virtualAbstract classes
A class with at least one pure virtual function is an abstract class:
- You cannot create objects of it:
Shape s;is a compile error. - You can have pointers and references to it.
- A derived class that does not override every pure virtual function is abstract too.
#include <iostream>
#include <cmath>
using namespace std;
class Shape { // abstract base class
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual const char *name() const = 0;
void describe() const { // concrete function using the abstract ones
cout << name() << ": area " << area() << ", perimeter " << perimeter() << endl;
}
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
explicit Circle(double r) : r(r) {}
double area() const override { return M_PI * r * r; }
double perimeter() const override { return 2 * M_PI * r; }
const char *name() const override { return "Circle"; }
};
class Square : public Shape {
double s;
public:
explicit Square(double s) : s(s) {}
double area() const override { return s * s; }
double perimeter() const override { return 4 * s; }
const char *name() const override { return "Square"; }
};
int main() {
// Shape s; // error: cannot declare variable 's' to be of abstract type
Circle c(1);
Square q(2.5);
const Shape &a = c, &b = q;
a.describe();
b.describe();
return 0;
}Circle: area 3.14159, perimeter 6.28319 Square: area 6.25, perimeter 10
Interfaces
C++ has no interface keyword (Java does). An interface in C++ is simply an abstract class with only pure virtual functions and a virtual destructor. A class can implement several interfaces through multiple inheritance:
#include <iostream>
#include <string>
using namespace std;
class Printable { public: virtual void print() const = 0; virtual ~Printable() = default; };
class Payable { public: virtual double amountDue() const = 0; virtual ~Payable() = default; };
class Invoice : public Printable, public Payable {
string client;
double hours, rate;
public:
Invoice(string c, double h, double r) : client(c), hours(h), rate(r) {}
double amountDue() const override { return hours * rate; }
void print() const override { cout << "Invoice for " << client << ": Rs " << amountDue() << endl; }
};
void printAll(const Printable *items[], int n) { for (int i = 0; i < n; i++) items[i]->print(); }
int main() {
Invoice a("Acme Labs", 12, 1500), b("Nova Foods", 4.5, 2000);
const Printable *docs[] = {&a, &b};
printAll(docs, 2);
const Payable &p = a;
cout << "Total due from Acme: " << p.amountDue() << endl;
return 0;
}Invoice for Acme Labs: Rs 18000 Invoice for Nova Foods: Rs 9000 Total due from Acme: 18000
final
final stops further overriding or inheritance:
class Base { virtual void f(); };
class Mid : public Base { void f() override final; }; // no class may override f again
class Leaf final : public Mid { }; // no class may inherit from LeafAbstract class vs concrete class
| Abstract class | Concrete class |
|---|---|
| has at least one pure virtual function | all functions have bodies |
| cannot be instantiated | can create objects |
| defines a contract for derived classes | provides a full implementation |
| used through pointers/references | used directly or through pointers |
Chapter 13 · Beyond the Basics
Templates
Write code once for any type: function templates, class templates, multiple type parameters, non-type parameters and specialisation — the foundation of the STL.
Suppose you need maximum for int, double and string. Overloading means three copies of identical code. A template lets you write the code once with a placeholder type; the compiler generates each version you use. This is generic programming.
Function templates
#include <iostream>
#include <string>
using namespace std;
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
template <typename T>
void swapAny(T &a, T &b) { T t = a; a = b; b = t; }
int main() {
cout << maximum(3, 9) << endl; // T = int
cout << maximum(2.5, 1.75) << endl; // T = double
cout << maximum(string("apple"), string("mango")) << endl;
cout << maximum<double>(4, 7.5) << endl; // T given explicitly
string a = "left", b = "right";
swapAny(a, b);
cout << a << " " << b << endl;
return 0;
}9 2.5 mango 7.5 right left
typename and class mean the same thing in a template parameter list: template <class T> is equally valid.
Templates with arrays
#include <iostream>
using namespace std;
template <typename T>
T sum(const T arr[], int n) {
T total = T(); // zero for numbers
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
template <typename T>
int linearSearch(const T arr[], int n, const T &key) {
for (int i = 0; i < n; i++) if (arr[i] == key) return i;
return -1;
}
int main() {
int marks[] = {78, 85, 92};
double prices[] = {9.99, 24.5, 3.25};
char grades[] = {'A', 'B', 'O', 'C'};
cout << sum(marks, 3) << " " << sum(prices, 3) << endl;
cout << "'O' at index " << linearSearch(grades, 4, 'O') << endl;
return 0;
}255 37.74 'O' at index 2
Class templates
A class template describes a family of classes. The STL's vector<int> and stack<string> are class templates.
#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;
template <typename T, int CAPACITY = 100> // a type parameter and a non-type parameter
class Stack {
T items[CAPACITY];
int top = -1;
public:
void push(const T &x) {
if (top == CAPACITY - 1) throw overflow_error("stack full");
items[++top] = x;
}
T pop() {
if (top == -1) throw underflow_error("stack empty");
return items[top--];
}
bool empty() const { return top == -1; }
int size() const { return top + 1; }
};
int main() {
Stack<int> numbers;
for (int i = 1; i <= 4; i++) numbers.push(i * i);
cout << "popping ints:";
while (!numbers.empty()) cout << " " << numbers.pop();
cout << endl;
Stack<string, 3> words; // capacity 3
words.push("templates"); words.push("are"); words.push("neat");
try { words.push("overflow!"); }
catch (const overflow_error &e) { cout << "error: " << e.what() << endl; }
cout << "words on stack: " << words.size() << endl;
return 0;
}popping ints: 16 9 4 1 error: stack full words on stack: 3
Two type parameters
#include <iostream>
#include <string>
using namespace std;
template <typename K, typename V>
class Pair {
public:
K key;
V value;
Pair(K k, V v) : key(k), value(v) {}
void show() const { cout << key << " -> " << value << endl; }
};
int main() {
Pair<string, int> p1("Asha", 92);
Pair<int, double> p2(7, 8.75);
p1.show();
p2.show();
return 0;
}Asha -> 92 7 -> 8.75
Template specialisation
Give a type its own special version when the generic code does not fit:
#include <iostream>
#include <cstring>
using namespace std;
template <typename T>
bool isEqual(T a, T b) { return a == b; }
template <> // full specialisation for C strings
bool isEqual<const char *>(const char *a, const char *b) { return strcmp(a, b) == 0; }
int main() {
char x[] = "hello", y[] = "hello";
cout << boolalpha << isEqual(4, 4) << " " << isEqual<const char *>(x, y) << endl;
return 0;
}true true
Without the specialisation, comparing two const char * would compare addresses, not text.
| Templates | Overloading | Macros |
|---|---|---|
| one definition, many types | one definition per type | text substitution |
| type-checked | type-checked | no type checking |
| code generated per type used | written by hand | expanded inline |
Chapter 14 · Beyond the Basics
Exception Handling
Handle errors cleanly with try, throw and catch: standard exception classes, multiple and catch-all handlers, custom exceptions, and stack unwinding.
Programs meet errors at run time: division by zero, a missing file, bad input, memory exhaustion. Checking return codes everywhere clutters code and errors are easy to ignore. Exceptions separate error detection from error handling.
throw— signal an error.try— wrap code that might throw.catch— handle a specific kind of error.
#include <iostream>
using namespace std;
double divide(double a, double b) {
if (b == 0) throw "Division by zero!"; // throw a value
return a / b;
}
int main() {
try {
cout << divide(10, 4) << endl;
cout << divide(1, 0) << endl; // throws: the rest of try is skipped
cout << "never printed" << endl;
} catch (const char *msg) {
cout << "Error: " << msg << endl;
}
cout << "Program continues normally." << endl;
return 0;
}2.5 Error: Division by zero! Program continues normally.
Standard exception classes
The standard library throws objects derived from std::exception. Each has a what() method returning a message.
Class (header <stdexcept>) |
Typical cause |
|---|---|
invalid_argument |
a bad argument, e.g. stoi("abc") |
out_of_range |
index past the end, e.g. vector::at(100) |
length_error |
a container grown beyond its maximum |
overflow_error, underflow_error |
arithmetic range errors |
runtime_error |
general errors detectable only at run time |
bad_alloc (<new>) |
new failed to get memory |
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
try {
cout << v.at(1) << endl;
cout << v.at(10) << endl; // .at() checks bounds and throws
} catch (const out_of_range &e) {
cout << "out_of_range caught" << endl;
}
string inputs[] = {"42", "abc", "99999999999"};
for (const string &s : inputs) {
try {
int n = stoi(s); // may throw
cout << "stoi(\"" << s << "\") = " << n << endl;
} catch (const invalid_argument &) {
cout << "\"" << s << "\" is not a number" << endl;
} catch (const out_of_range &) {
cout << "\"" << s << "\" is too big for an int" << endl;
}
}
return 0;
}2
out_of_range caught
stoi("42") = 42
"abc" is not a number
"99999999999" is too big for an intMultiple catch blocks and catch-all
Handlers are tried in order; the first matching one runs. Put specific types before general ones, and catch (...) (catch anything) last:
#include <iostream>
#include <stdexcept>
using namespace std;
void risky(int option) {
if (option == 1) throw 404;
if (option == 2) throw runtime_error("disk not ready");
if (option == 3) throw 3.14;
cout << "option " << option << " ran fine" << endl;
}
int main() {
for (int option = 0; option <= 3; option++) {
try {
risky(option);
} catch (int code) {
cout << "int error code " << code << endl;
} catch (const exception &e) { // catches every std exception
cout << "std::exception: " << e.what() << endl;
} catch (...) {
cout << "unknown error" << endl;
}
}
return 0;
}option 0 ran fine int error code 404 std::exception: disk not ready unknown error
Custom exceptions
Derive from std::exception (or runtime_error) to describe your own errors:
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class InsufficientFunds : public runtime_error {
public:
double shortBy;
InsufficientFunds(double s) : runtime_error("insufficient funds"), shortBy(s) {}
};
class Account {
double balance = 1000;
public:
void withdraw(double amt) {
if (amt <= 0) throw invalid_argument("amount must be positive");
if (amt > balance) throw InsufficientFunds(amt - balance);
balance -= amt;
}
double getBalance() const { return balance; }
};
int main() {
Account acc;
double amt;
cout << "Withdraw how much? ";
cin >> amt;
try {
acc.withdraw(amt);
cout << "Done. Balance: " << acc.getBalance() << endl;
} catch (const InsufficientFunds &e) {
cout << "Failed: " << e.what() << " (short by Rs " << e.shortBy << ")" << endl;
} catch (const invalid_argument &e) {
cout << "Failed: " << e.what() << endl;
}
return 0;
}Withdraw how much? 2500
Failed: insufficient funds (short by Rs 1500)Stack unwinding
When an exception propagates out of functions, every local object on the way is destroyed properly — its destructor runs. This is why C++ resources should be owned by objects (RAII): cleanup happens automatically even on errors.
#include <iostream>
#include <stdexcept>
using namespace std;
struct Guard {
const char *name;
Guard(const char *n) : name(n) { cout << "acquire " << name << endl; }
~Guard() { cout << "release " << name << endl; }
};
void level2() { Guard g("file"); throw runtime_error("parse failed"); }
void level1() { Guard g("lock"); level2(); cout << "not reached" << endl; }
int main() {
try { level1(); }
catch (const exception &e) { cout << "caught: " << e.what() << endl; }
return 0;
}acquire lock acquire file release file release lock caught: parse failed
Chapter 15 · Beyond the Basics
Dynamic Memory & Smart Pointers
new and delete, arrays on the heap, shallow vs deep copy, the rule of three, and modern smart pointers — unique_ptr and shared_ptr — that free memory for you.
new and delete
C++ replaces malloc/free with new and delete. new allocates memory and calls the constructor; delete calls the destructor and frees the memory.
#include <iostream>
using namespace std;
class Book {
public:
string title;
Book(const string &t) : title(t) { cout << "construct " << title << endl; }
~Book() { cout << "destroy " << title << endl; }
};
int main() {
int *n = new int(42); // one int, initialised to 42
cout << *n << endl;
delete n;
int *arr = new int[5]{1, 2, 3, 4, 5}; // an array
for (int i = 0; i < 5; i++) cout << arr[i] << " ";
cout << endl;
delete[] arr; // arrays need delete[]
Book *b = new Book("C++ Primer"); // constructor runs
cout << "reading " << b->title << endl;
delete b; // destructor runs
return 0;
}42 1 2 3 4 5 construct C++ Primer reading C++ Primer destroy C++ Primer
malloc / free |
new / delete |
|---|---|
function from <cstdlib> |
operator built into the language |
returns void *, needs a cast |
returns the correct pointer type |
| size given in bytes | size computed from the type |
| does not call constructors/destructors | calls constructors and destructors |
returns NULL on failure |
throws bad_alloc on failure |
Shallow copy vs deep copy
If a class owns heap memory through a pointer, the compiler's default copy copies the pointer, not the data — a shallow copy. Two objects then share one buffer, and both destructors free it: a crash.
A deep copy allocates new memory and copies the contents.
The rule of three
If a class needs a custom destructor, it almost certainly also needs a custom copy constructor and copy assignment operator:
#include <iostream>
#include <cstring>
using namespace std;
class Name {
char *text;
public:
Name(const char *s) : text(new char[strlen(s) + 1]) { strcpy(text, s); }
~Name() { delete[] text; } // 1. destructor
Name(const Name &o) : text(new char[strlen(o.text) + 1]) { // 2. copy constructor (deep)
strcpy(text, o.text);
}
Name &operator=(const Name &o) { // 3. copy assignment (deep)
if (this != &o) { // guard against a = a
char *fresh = new char[strlen(o.text) + 1];
strcpy(fresh, o.text);
delete[] text;
text = fresh;
}
return *this;
}
void setFirst(char c) { text[0] = c; }
const char *get() const { return text; }
};
int main() {
Name a("asha");
Name b = a; // copy constructor
Name c("x");
c = a; // copy assignment
b.setFirst('U');
c.setFirst('N');
cout << a.get() << " " << b.get() << " " << c.get() << endl; // each has its own buffer
return 0;
}asha Usha Nsha
(C++11 extends this to the rule of five by adding a move constructor and move assignment. Even better is the rule of zero: let standard types like string, vector and smart pointers own the memory, and write none of these functions.)
Smart pointers
Manual delete is easy to forget, especially when exceptions fly. Smart pointers in <memory> own heap objects and free them automatically — the RAII idea applied to memory.
unique_ptr: sole ownership
#include <iostream>
#include <memory>
using namespace std;
struct Sensor {
int id;
Sensor(int i) : id(i) { cout << "sensor " << id << " on" << endl; }
~Sensor() { cout << "sensor " << id << " off" << endl; }
};
int main() {
unique_ptr<Sensor> a = make_unique<Sensor>(1);
cout << "reading sensor " << a->id << endl;
// unique_ptr<Sensor> b = a; // error: cannot copy a unique_ptr
unique_ptr<Sensor> b = std::move(a); // ownership moves to b
cout << "a is " << (a ? "set" : "empty") << ", b owns sensor " << b->id << endl;
{
auto c = make_unique<Sensor>(2);
} // c's sensor freed here automatically
cout << "end of main" << endl;
return 0;
} // b's sensor freed heresensor 1 on reading sensor 1 a is empty, b owns sensor 1 sensor 2 on sensor 2 off end of main sensor 1 off
shared_ptr: shared ownership
A shared_ptr keeps a reference count; the object is deleted when the last shared_ptr to it is destroyed.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Playlist {
string name;
Playlist(string n) : name(n) {}
~Playlist() { cout << "deleting playlist " << name << endl; }
};
int main() {
shared_ptr<Playlist> mine = make_shared<Playlist>("Coding Lo-fi");
cout << "owners: " << mine.use_count() << endl;
{
shared_ptr<Playlist> friendCopy = mine;
cout << "owners: " << mine.use_count() << endl;
}
cout << "owners: " << mine.use_count() << endl;
mine.reset(); // last owner gone: object deleted
cout << "done" << endl;
return 0;
}owners: 1 owners: 2 owners: 1 deleting playlist Coding Lo-fi done
| Smart pointer | Ownership | Copyable? | Use for |
|---|---|---|---|
unique_ptr |
exactly one owner | no (move only) | the default choice |
shared_ptr |
many owners, reference counted | yes | genuinely shared objects |
weak_ptr |
observes a shared_ptr without owning |
yes | breaking reference cycles |
Chapter 16 · Beyond the Basics
STL Containers
The Standard Template Library's ready-made data structures: vector, list, deque, stack, queue, priority_queue, set, map and their unordered versions — with costs.
The Standard Template Library (STL) gives you tested, fast implementations of every structure from the Data Structures course. It has three parts: containers (data structures), iterators (a uniform way to walk through containers) and algorithms (sort, search and more — next lesson).
vector: the default container
A vector is a dynamic array: contiguous memory that grows as needed (the realloc doubling trick from C, done for you).
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {5, 3, 8};
v.push_back(1); // add at the end: amortised O(1)
v.push_back(9);
cout << "size " << v.size() << ", front " << v.front() << ", back " << v.back() << endl;
v.insert(v.begin() + 1, 42); // insert at index 1: O(n)
v.pop_back(); // remove the last element
v.erase(v.begin()); // remove the first element: O(n)
for (int x : v) cout << x << " ";
cout << endl;
vector<vector<int>> grid(3, vector<int>(4, 0)); // 3x4 matrix of zeros
grid[1][2] = 7;
cout << "grid[1][2] = " << grid[1][2] << ", rows " << grid.size() << endl;
return 0;
}size 5, front 5, back 9 42 3 8 1 grid[1][2] = 7, rows 3
Stack, queue and priority_queue
These are container adapters with a restricted interface — exactly the ADTs from Data Structures:
#include <iostream>
#include <stack>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
int main() {
stack<int> s;
for (int i = 1; i <= 3; i++) s.push(i);
cout << "stack pops:";
while (!s.empty()) { cout << " " << s.top(); s.pop(); }
queue<string> q;
q.push("first"); q.push("second"); q.push("third");
cout << "\nqueue pops:";
while (!q.empty()) { cout << " " << q.front(); q.pop(); }
priority_queue<int> maxHeap; // largest on top
priority_queue<int, vector<int>, greater<int>> minHeap; // smallest on top
for (int x : {40, 10, 70, 30}) { maxHeap.push(x); minHeap.push(x); }
cout << "\nmax-heap top: " << maxHeap.top() << ", min-heap top: " << minHeap.top() << endl;
return 0;
}stack pops: 3 2 1 queue pops: first second third max-heap top: 70, min-heap top: 10
set and map
set stores unique keys in sorted order; map stores sorted key → value pairs. Both are balanced binary search trees (red-black trees): O(log n) insert, find and erase.
#include <iostream>
#include <set>
#include <map>
#include <string>
using namespace std;
int main() {
set<int> s = {5, 1, 5, 3, 1, 9}; // duplicates ignored
s.insert(4);
cout << "set:";
for (int x : s) cout << " " << x; // always sorted
cout << "\ncontains 3? " << (s.count(3) ? "yes" : "no") << endl;
map<string, int> marks;
marks["Asha"] = 91;
marks["Ravi"] = 78;
marks["Meera"] = 85;
marks["Ravi"] += 5; // update
for (const auto &[name, m] : marks) // sorted by key
cout << name << ": " << m << endl;
if (marks.find("Zoya") == marks.end()) cout << "Zoya not found" << endl;
return 0;
}set: 1 3 4 5 9 contains 3? yes Asha: 91 Meera: 85 Ravi: 83 Zoya not found
unordered_map: hash tables
unordered_map and unordered_set are hash tables: O(1) average lookup, but no ordering. Perfect for counting:
#include <iostream>
#include <unordered_map>
#include <sstream>
#include <string>
using namespace std;
int main() {
string text = "the cat and the dog and the bird";
unordered_map<string, int> freq;
stringstream ss(text);
string word;
while (ss >> word) freq[word]++; // missing keys start at 0
for (string w : {"the", "and", "cat", "fish"})
cout << w << ": " << freq[w] << endl;
return 0;
}the: 3 and: 2 cat: 1 fish: 0
Choosing a container
| Container | Underlying structure | Access | Insert/erase | Find |
|---|---|---|---|---|
vector |
dynamic array | O(1) by index | O(1) at end, O(n) elsewhere | O(n) |
deque |
blocks of arrays | O(1) by index | O(1) at both ends | O(n) |
list |
doubly linked list | O(n) | O(1) with an iterator | O(n) |
stack / queue |
adapter (deque by default) | top / front only | O(1) | — |
priority_queue |
binary heap | top only | O(log n) | — |
set / map |
red-black tree | — | O(log n) | O(log n), sorted |
unordered_set / unordered_map |
hash table | — | O(1) average | O(1) average |
Chapter 17 · Beyond the Basics
Iterators, Algorithms & Lambdas
Walk any container with iterators, then use the STL algorithms — sort, find, count, binary_search, accumulate, transform — with lambda functions for custom behaviour.
Iterators
An iterator is an object that points to an element of a container and can move to the next one — a generalised pointer. begin() points to the first element; end() points one past the last.
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main() {
vector<int> v = {10, 20, 30, 40};
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
cout << *it << " "; // dereference like a pointer
cout << endl;
list<string> names = {"Asha", "Ravi", "Meera"};
for (auto it = names.rbegin(); it != names.rend(); ++it) // reverse iterators
cout << *it << " ";
cout << endl;
return 0;
}10 20 30 40 Meera Ravi Asha
Because every container offers iterators, one algorithm works on all of them.
Lambda expressions
A lambda is an unnamed function written inline: [capture](parameters) { body }. It is the easiest way to give an algorithm custom behaviour.
#include <iostream>
using namespace std;
int main() {
auto square = [](int x) { return x * x; };
int bonus = 5;
auto addBonus = [bonus](int m) { return m + bonus; }; // captures bonus by value
int calls = 0;
auto counter = [&calls]() { calls++; }; // captures calls by reference
counter(); counter(); counter();
cout << square(7) << " " << addBonus(80) << " " << calls << endl;
return 0;
}49 85 3
The essential algorithms
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <string>
using namespace std;
void show(const vector<int> &v) { for (int x : v) cout << x << " "; cout << endl; }
int main() {
vector<int> v = {42, 7, 19, 7, 88, 3, 25};
sort(v.begin(), v.end()); // ascending
cout << "sorted: "; show(v);
sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // descending
cout << "descending: "; show(v);
cout << "max element: " << *max_element(v.begin(), v.end()) << endl;
cout << "sum: " << accumulate(v.begin(), v.end(), 0) << endl;
cout << "count of 7: " << count(v.begin(), v.end(), 7) << endl;
cout << "count > 20: " << count_if(v.begin(), v.end(), [](int x) { return x > 20; }) << endl;
auto it = find(v.begin(), v.end(), 19);
cout << "19 found at index " << (it - v.begin()) << endl;
sort(v.begin(), v.end());
cout << "binary_search 25: " << boolalpha << binary_search(v.begin(), v.end(), 25) << endl;
v.erase(unique(v.begin(), v.end()), v.end()); // remove adjacent duplicates
cout << "unique: "; show(v);
reverse(v.begin(), v.end());
cout << "reversed: "; show(v);
vector<int> doubled(v.size());
transform(v.begin(), v.end(), doubled.begin(), [](int x) { return 2 * x; });
cout << "doubled: "; show(doubled);
return 0;
}sorted: 3 7 7 19 25 42 88 descending: 88 42 25 19 7 7 3 max element: 88 sum: 191 count of 7: 2 count > 20: 3 19 found at index 3 binary_search 25: true unique: 3 7 19 25 42 88 reversed: 88 42 25 19 7 3 doubled: 176 84 50 38 14 6
Sorting objects
Pass a comparison lambda to sort by any field:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
struct Student { string name; int marks; };
int main() {
vector<Student> cls = {{"Asha", 88}, {"Ravi", 95}, {"Meera", 88}, {"Kiran", 72}};
stable_sort(cls.begin(), cls.end(), [](const Student &a, const Student &b) {
return a.marks > b.marks; // highest marks first; ties keep their order
});
int rank = 1;
for (const auto &s : cls) cout << rank++ << ". " << s.name << " " << s.marks << endl;
return 0;
}1. Ravi 95 2. Asha 88 3. Meera 88 4. Kiran 72
| Algorithm | Does | Cost |
|---|---|---|
sort |
sort a range (introsort) | O(n log n) |
stable_sort |
sort, keeping equal elements in order | O(n log n) |
find, find_if |
first match | O(n) |
count, count_if |
number of matches | O(n) |
binary_search, lower_bound |
search a sorted range | O(log n) |
accumulate (<numeric>) |
sum or fold | O(n) |
min_element, max_element |
position of the min / max | O(n) |
reverse, rotate, unique |
rearrange | O(n) |
transform |
apply a function to every element | O(n) |
Chapter 18 · Beyond the Basics
File I/O with fstream
Read and write files the C++ way with ifstream, ofstream and fstream: modes, line-by-line reading, parsing records with stringstream, and binary files.
C++ file handling uses streams, exactly like cin and cout. Include <fstream>:
| Class | Purpose |
|---|---|
ofstream |
write to a file (output) |
ifstream |
read from a file (input) |
fstream |
both |
Writing and reading text
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream out("notes.txt"); // creates or truncates the file
if (!out) { cerr << "cannot open file" << endl; return 1; }
out << "Templates make code generic." << endl;
out << "Exceptions separate error handling." << endl;
out << "RAII frees resources automatically." << endl;
out.close();
ifstream in("notes.txt");
string line;
int n = 0;
while (getline(in, line)) // read until end of file
cout << ++n << ": " << line << endl;
return 0; // in closes automatically (RAII)
}1: Templates make code generic. 2: Exceptions separate error handling. 3: RAII frees resources automatically.
File modes
| Flag | Meaning |
|---|---|
ios::in |
open for reading |
ios::out |
open for writing (truncates by default) |
ios::app |
append: every write goes to the end |
ios::ate |
start at the end, but allow moving around |
ios::trunc |
erase existing contents |
ios::binary |
binary mode (no newline translation) |
Combine flags with |: fstream f("data.txt", ios::in | ios::out);
Reading structured records
Write records as text, then read them back with >> or parse each line with a stringstream:
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <iomanip>
using namespace std;
int main() {
ofstream out("students.csv");
out << "roll,name,marks\n101,Asha,88\n102,Ravi,93\n103,Meera,79\n";
out.close();
ifstream in("students.csv");
string line;
getline(in, line); // skip the header
double total = 0;
int count = 0;
cout << left << setw(6) << "Roll" << setw(8) << "Name" << "Marks" << endl;
while (getline(in, line)) {
stringstream ss(line);
string roll, name, marksText;
getline(ss, roll, ','); // split on commas
getline(ss, name, ',');
getline(ss, marksText, ',');
int marks = stoi(marksText);
cout << setw(6) << roll << setw(8) << name << marks << endl;
total += marks;
count++;
}
cout << "Average: " << fixed << setprecision(2) << total / count << endl;
ofstream log("students.csv", ios::app); // append a new record
log << "104,Kiran,85\n";
return 0;
}Roll Name Marks 101 Asha 88 102 Ravi 93 103 Meera 79 Average: 86.67
Binary files
Binary I/O writes raw bytes — compact and fast, but not human-readable:
#include <iostream>
#include <fstream>
using namespace std;
struct Reading { int sensor; double value; };
int main() {
Reading data[3] = {{1, 21.5}, {2, 22.25}, {3, 19.75}};
ofstream out("readings.bin", ios::binary);
out.write(reinterpret_cast<const char *>(data), sizeof(data));
out.close();
ifstream in("readings.bin", ios::binary);
in.seekg(sizeof(Reading) * 2); // jump to the third record
Reading r;
in.read(reinterpret_cast<char *>(&r), sizeof(r));
cout << "sensor " << r.sensor << " read " << r.value << endl;
cout << "file size: " << sizeof(data) << " bytes" << endl;
return 0;
}sensor 3 read 19.75 file size: 48 bytes
seekg / tellg move and report the get (read) position; seekp / tellp do the same for the put (write) position.
Chapter 19 · Beyond the Basics
C++ OOP Cheat Sheet & Exam Prep
The whole OOPS course on one page: syntax for classes, constructors, inheritance and polymorphism, the comparisons examiners love, and programs to practise.
Class skeleton
class Name : public Base { // inheritance
private: // hidden data
int value;
static int count; // shared by all objects
public:
Name(int v = 0) : Base(), value(v) { count++; } // constructor + initialiser list
Name(const Name &o) : Base(o), value(o.value) {} // copy constructor
Name &operator=(const Name &o); // copy assignment
virtual ~Name() {} // virtual destructor
int get() const { return value; } // const getter
void set(int v) { if (v >= 0) value = v; } // validating setter
virtual void show() const override; // runtime polymorphism
static int total() { return count; } // class-level function
friend ostream &operator<<(ostream &, const Name &); // friend operator
};
int Name::count = 0;OOP pillars in one table
| Pillar | Idea | C++ tool |
|---|---|---|
| Encapsulation | bundle data and methods, hide data | private, getters/setters |
| Abstraction | expose what, hide how | public interface, abstract classes |
| Inheritance | reuse and extend (is-a) | class D : public B |
| Polymorphism | one interface, many forms | overloading (compile time), virtual (run time) |
Comparisons examiners ask
| Topic | Key difference |
|---|---|
struct vs class |
default access: public vs private |
| Constructor vs destructor | creates/initialises vs cleans up; destructor has ~, no parameters, no overloading |
| Copy constructor vs assignment | initialise a new object vs overwrite an existing one |
| Shallow vs deep copy | copies the pointer vs copies the pointed-to data |
| Overloading vs overriding | same name, different parameters (compile time) vs same signature in derived class (run time, virtual) |
| Static vs dynamic binding | decided at compile time vs at run time through vtable |
| Abstract class vs interface | has some pure virtual functions vs only pure virtual functions |
| Pointer vs reference | nullable, re-seatable vs always bound, alias |
new/delete vs malloc/free |
calls constructors/destructors, type-safe, throws bad_alloc |
| Function template vs overloading | one generic definition vs one definition per type |
Inheritance access table
| Base member | public inheritance | protected inheritance | private inheritance |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | not accessible | not accessible | not accessible |
Operators you cannot overload
:: . .* ?: sizeof typeid
STL quick picks
| Need | Use |
|---|---|
| growable array | vector<T> |
| LIFO / FIFO | stack<T> / queue<T> |
| always the largest (smallest) | priority_queue<T> (with greater<T>) |
| sorted unique keys | set<T> |
| sorted key → value | map<K, V> |
| fastest lookup by key | unordered_map<K, V> |
| sort / search | sort, binary_search, lower_bound |
Programs to practise
- Class
Student/Employee/BankAccountwith constructors, getters, setters and validation. - Count objects with a
staticmember. - Copy constructor demonstrating deep copy.
ComplexorFractionwith+,-,*,==,<<,>>overloaded.- Multilevel inheritance showing constructor/destructor order.
- Shape hierarchy with an abstract base class and virtual functions.
- Function template
maximum, class templateStack<T>. - Custom exception class with
try/catch. - Word frequency with
maporunordered_map. - Read and write student records with
fstream.