OOPs are actually very simple but we can have some difficult questions on OOPs as well. We will first have short notes of theory(which can be asked in form of questions) and then shift to questions.
I am refering E balaguruswamy book for this. These are short notes and may miss something, if you think something is missing please comment. Also, we would be using this track:
- Introduction to classes and objects
- Constructors and Destructors
- Operator Overloading
- Inheritance
- Polymorphism
We will be discussing major topics here and actually difficult ones. You must know basic OOPs.
Introduction
C structures Vs C++ ClassesWe know structures can be used to create user-defined data types in C and C++. But then why we need classes? We can have functions, constructors, etc in structures as well but what differentiates it from classes are lack of abstraction and inheritance(And actually many other things as well). We can hide certain members from outside world(abstraction) in a class but we can't do anything like this in structures. Also, we cannot overload an operator here. For example
struct complex{
float x,y;
};
// Structure for a complex number of form x+iy(i stands for iota)
complex c1,c2;
c1.x=1;
c1.y=2;
c2.x=3;
c2.y=3;
// c1=1+2i, c2=3+3i
complex c3=c1+c2;//Illegal statement in C because we cannot add these two structures.
General structure of classesclass className{
private:
variable declaration;
function declaration;
public:
variable declaration;
function declaration;
};
Visibility labels(Public, private, protected - private: Data members labelled private are only available withing the class.
- public: Data members are available for the outside world as well.
- protected: Data members are not available for the whole world but are available to the inherited classes.
By default, all members are private.
Some terms and their meaning - Abstraction: Data hiding. All those visibility labels are used for abstraction only. We can achieve data hiding using the visibility labels(private)
- Encapsulation: Data binding. This is just binding various data members and functions into a class. We actually do that, so it's kind of default behaviour.
- Polymorphism:
- Inheritance:
- Overloading:
For CP lovers, here is class based implementation of segment trees and Euler tours, representing inheritance as wellQuestion Link: https://cses.fi/problemset/task/1138
#include<bits/stdc++.h>
using namespace std;
#define int long long int
const int mxN = 200001;
int inf = 1e18;
class segTree
{
private:
vector<int>tree;
vector<int>a;
void buildTree(int i, int st, int end, vector<int> &arr)
{
if(st == end)
{
tree[i] = arr[st];
return;
}
int mid = st + (end - st) / 2;
int lchild = 2 * i + 1;
int rchild = 2 * i + 2;
buildTree(lchild, st, mid, arr);
buildTree(rchild, mid + 1, end, arr);
tree[i] = tree[lchild] + tree[rchild];
}
int queryPrivate(int i, int segStart, int segEnd, int l, int r)
{
if(segEnd < l || segStart > r)return 0;
if(segEnd <= r && segStart >= l)
return tree[i];
int lchild = 2 * i + 1;
int rchild = 2 * i + 2;
int mid = segStart + (segEnd - segStart) / 2;
int resL = queryPrivate(lchild, segStart, mid, l, r);
int resR = queryPrivate(rchild, mid + 1, segEnd, l, r);
return resL + resR;
}
void upd(int i, int st, int end, int ind, int newVal)
{
if(st == end)
{
tree[i] = newVal;
return;
}
int mid = st + (end - st) / 2;
if(ind <= mid)
upd(2 * i + 1, st, mid, ind, newVal);
else
upd(2 * i + 2, mid + 1, end, ind, newVal);
tree[i] = tree[2 * i + 1] + tree[2 * i + 2];
}
public:
void build(vector<int> &arr)
{
int n = arr.size();
tree.resize(4 * n + 1);
for(int i = 0; i < 4 * n + 1; i++)tree[i] = inf;
buildTree(0, 0, n — 1, arr);
a = arr;
}
int queryTree(int l, int r)
{
int n = a.size();
return queryPrivate(0, 0, n — 1, l, r);
}
void updateTree(int indToChange, int newVal)
{
int n = a.size();
upd(0, 0, n - 1, indToChange, newVal);
}
};
// Inherited segTree class to Tree class
class Tree : public segTree
{
vector<int>adj[mxN];
vector<int>val;
vector<int>in;
vector<int>out;
vector<int>eTour;
segTree s;
public:
Tree(vector<int>arr)
{
// cout<<"u";
int n = arr.size();
val.resize(n);
in.resize(n);
out.resize(n);
for(int i = 0; i < n; i++)in[i] = -1;
for(int i = 0; i < n; i++)out[i] = -1;
val = arr;
}
void dfs(int src, vector<bool> &vis)
{
vis[src] = 1;
eTour.push_back(src);
for(auto it : adj[src])
{
if(!vis[it])
dfs(it, vis);
}
eTour.push_back(src);
}
void eulerTour()
{
vector<bool>vis(val.size());
dfs(0, vis);
for(int i = 0; i < eTour.size(); i++)
{
if(in[eTour[i]] == -1)
in[eTour[i]] = i;
else
out[eTour[i]] = i;
}
for(int i = 0; i < eTour.size(); i++)
{
if(in[eTour[i]]==i)
eTour[i] = val[eTour[i]];
else
eTour[i] = -val[eTour[i]];
// cout<<eTour[i]<<" ";
}
// cout<<"\n";
build(eTour);
}
void addEdge(int x, int y)
{
adj[x — 1].push_back(y — 1);
adj[y — 1].push_back(x — 1);
}
int query(int node)
{
node--;
return queryTree(0, in[node]);
}
void update(int node, int x)
{
node--;
updateTree(in[node], x);
updateTree(out[node], -x);
val[node] = x;
}
};
int32_t main()
{
// This is the holy code of the demon AB-DUDE
// This is the code of the question : https://cses.fi/problemset/task/1137/
// We used Euler Tour and Segment Trees for this
// Just do a Euler tour of type 2 on the tree and make a basic segment tree on the array
// Now, we can just use in and out arrays for the positions of the nodes in the euler tour array
int n, q;
cin >> n >> q;
vector<int>a(n);
for(int i = 0; i < n; i++)cin >> a[i];
Tree tr(a);
for (int i = 0; i < n - 1; ++i)
{
int x, y;
cin >> x >> y;
tr.addEdge(x, y);
}
tr.eulerTour();
for(int i = 0; i < q; i++)
{
int t;
cin >> t;
if(t == 1)
{
int s, x;
cin >> s >> x;
tr.update(s, x);
}
else
{
int s;
cin >> s;
cout << tr.query(s) << "\n";
}
}
return 0;
}
Memory allocation of member variables and functions accross objectsMember variables are different for different objects but member functions are the same, which is quite obvious.
Static Data members
As we saw in memory allocation diagram, copies of member variables are made for different objects. But if we want a common variable for all objects of class? For example in an employee class, we need to find the number of employees at a specific time. We can do it using a global variable but that variable can interfere in the whole working of program. We can use a static data member. Static members are initialized to 0 and one important thing to note is that type and scope of static variable is defined outside the class. This is because they are not of every object but are unique to a class. And where we have variables, we do have functions also. Static member functions are also specific to a class, and can access only static members. Please have a code for implementation of both(Here I have kept the static member function outside to generalize the concept that static members are specific to class but they can be defined and implemented inside the class as well)
Sample Code for static data members#include<bits/stdc++.h>
using namespace std;
#define int long long int
class Customers{
static int customer_count;
vector<string> movies;
public:
static void showCount();
Customers()
{
customer_count++;
}
void addMovie(string s)
{
movies.push_back(s);
}
};
int Customers::customer_count;
void Customers::showCount()
{
cout<<customer_count<<"\n";
}
int32_t main()
{
Customers C1;
Customers C2;
C1.addMovie("Shawshank Redemption");
C2.addMovie("Ananda");
C2.addMovie("Bhootnath");
C2.addMovie("Hera Pheri");
C2.addMovie("Phir Hera Pheri");
C2.addMovie("Welcome");
C2.addMovie("Golmaal");
C1.addMovie("Avengers: Infinity Wars");
C1.addMovie("Avengers: Endgame");
C1.addMovie("Deadpool");
C1.addMovie("Godfather");
Customers::showCount();
return 0;
}
All other operations like making an array, vector, or any other STL container works the same for classes(Although you have to make a custom comparator in case of sorting through STL) remains almost same. We can also pass objects as arguments which is self explanatory.
Friendly Functions
There can be a case when we want some function to access private members of a class. For example, suppose there are two classes, Software engineers and Software testers. We want to find maximum income between both of the classes(Although many testers may not be eligible to pay taxes in India(Minimum salary is 5LPA for tax payers)). But we don't want to use the private member "income" as we can't access private members using objects directly. So, what we do is we make a friend function which can access private members of both the classes outside of both of them. This way, it can access both the classes without actually being a part of the class. (Obviously, we have to define that a particular function is friend inside the class as it would be a security threat if any function can be friend)
Code for Friend Functions#include<bits/stdc++.h>
using namespace std;
#define int long long int
class SofTesters;
class SofEngineers{
public:
int income=1000;
friend int greaterIncome(SofEngineers, SofTesters);
void copyCode()
{
cout<<"The deadline is too short!!\n";
}
void pasteCode()
{
cout<<"Why the f it's not working!!!\n";
}
};
class SofTesters{
public:
int income=10;
friend int greaterIncome(SofEngineers, SofTesters);
void curseDev()
{
cout<<"That dev is a dumba**, 5 test scripts failed!";
}
void appologize()
{
cout<<"Oh shoot, Too much work, that intern made a mistake in making test scripts.";
}
};
int greaterIncome(SofEngineers a, SofTesters b)
{
return a.income>=b.income;
}
int32_t main()
{
SofEngineers abdude;
SofTesters lord_invincible;
cout<<greaterIncome(abdude,lord_invincible);
return 0;
}
If a member function do not alter any data, we put a const ahead of its definition: void changeNothing() const;
Pointers to members of a class
Suppose this is the class:
class A{
private:
int x;
};
We define a pointer to X as: int A ::* it=&A :: m;
Now, it contains address of m and we can refer to m using *it.
Constructors and Destructors
Constructor
Special member functions having the same name as the class used to initialize objects. Whenever a new object is created, this member function is called.
Constructor example in segment tree implementationclass segTree{
private:
vector<int>tree;
vector<int>a;
void buildTree(int i,int st,int end,vector<int>&arr)
{
if(st==end){
tree[i]=arr[st];
return;
}
int mid=st+(end-st)/2;
int lchild=2*i+1;
int rchild=2*i+2;
buildTree(lchild,st,mid,arr);
buildTree(rchild,mid+1,end,arr);
tree[i]=tree[lchild]+tree[rchild];
}
int queryPrivate(int i,int segStart,int segEnd,int l,int r)
{
if(segEnd<l||segStart>r)return 0;
if(segEnd<=r&&segStart>=l)
return tree[i];
int lchild=2*i+1;
int rchild=2*i+2;
int mid=segStart+(segEnd-segStart)/2;
int resL=queryPrivate(lchild,segStart,mid,l,r);
int resR=queryPrivate(rchild,mid+1,segEnd,l,r);
return resL+resR;
}
void upd(int i,int st,int end,int ind,int newVal)
{
if(st==end)
{
tree[i]=newVal;
return;
}
int mid=st+(end-st)/2;
if(ind<=mid)
upd(2*i+1,st,mid,ind,newVal);
else
upd(2*i+2,mid+1,end,ind,newVal);
tree[i]=tree[2*i+1]+tree[2*i+2];
}
public:
// Constructor here
segTree(vector<int>&arr)
{
int n=arr.size();
tree.resize(4*n+1);
for(int i=0;i<4*n+1;i++)tree[i]=inf;
buildTree(0,0,n-1,arr);
a=arr;
}
int query(int l,int r)
{
int n=a.size();
return queryPrivate(0,0,n-1,l,r);
}
void update(int indToChange,int newVal){
int n=a.size();
upd(0,0,n-1,indToChange,newVal);
}
};
Constructors can be parametrized or without any parameters as well. Constructors without any parameters are default constructors. Please note that if a parameterized constructor is present and no default constructor is present, this type of initialization will give you an error: segTree s;
because compiler couldn't find any default constructor. But if no constructor is present, compiler supplies one.
Some properties of constructors: - Always in public section
- No return type
- Cannot be inherited
- Constructors cannot be virtual.
The parameters of a constructor can be of any type except for that class only but it can accept a reference to the class. These constructors are called copy constructors. Copy Constructors can be used to create two objects with same member variables.
Example of Copy Constructorsclass A{
public:
A(A&){
}
};
We can also use constructors dynamically. That is, we can use them after initialization of objects also.
Code for Dynamic Initialization of Objects#include<bits/stdc++.h>
using namespace std;
class Problem{
int diff;
string name="";
public:
Problem(){};
Problem(int diff)
{
this->diff=diff;
}
Problem(int diff,string name)
{
this->diff=diff;
this->name=name;
}
void display()
{
cout<<"Problem Name is "<<name<<endl;
cout<<"Problem Difficult is "<<diff<<endl;
}
};
int main()
{
Problem P1,P2,P3;
// After initialization
P1 = Problem(1000);
P2 = Problem(1200,"Nasya And Array");
P3 = Problem(2100,"Nasya And Tree");
P1.display();
P2.display();
P3.display();
}
Destructors
A destructor, as name suggests, is used to delete objects that have been created by a constructor. As we saw in memory allocation, each object has its own memory for its member variables. Suppose we have a vector and we are resizing it inside the constructor. Then we should use a destructor as well to free that memory. (Not in general, but in big firms, yes its necessary)
Destructor Example#include<bits/stdc++.h>
using namespace std;
class Problem{
vector<int>problemRatings;
public:
Problem(){};
Problem(int diff)
{
problemRatings.resize(diff);
}
//Destructor:
~Problem,2021-08-20()
{
problemRatings.clear();
}
};
int main()
{
Problem P1(1000);
}
Operator Overloading
This is perhaps the most interesting part of OOPs. As we saw initially, we can't do a (+) operation on structures, but we can do it in classes using operator overloading. For example, we can use a (+) operator for strings to concatenate them in C++. Similarly, we can modify the meaning of an operator for a class. The possibilities with this are immense. Just imagine, you can actually add maps, sets according to the meaning interpreted by you. You can practically create your own language with this feature. But there are some operators you can't overload.
List of operators you can't overload - Class member access operators( (.) , (*))
- Scope resolution operator( :: )
- Size operator(sizeof)
- Conditional operator (?)
Please note that the precedence of the operators don't change. Multiplication will have a higher precedence than addition.
Let's Overload some Unary and Binary Operators!
- Unary Operator:
Example of Unary operator, in a class complex, which is like an actual complex numberComplex Number of form $$$ a + i*b $$$
#include<bits/stdc++.h>
using namespace std;
#define int long long int
class Complex{
int real,im;
public:
Complex(){}
Complex(int r=0,int i=0)
{
real=r;
im=i;
}
void operator-()
{
real=-real;
im=-im;
}
friend ostream& operator<<(ostream& os, const Complex& a);
};
ostream& operator<<(ostream& os, const Complex& a)
{
os<<a.real<<" + i"<<a.im<<endl;
return os;
}
int32_t main()
{
Complex C1(10,-2);
// 10+2i
// -C1=-10+2i;
// We overloaded the insertion operator as well!
cout<<C1;
// Please Note that we are not doing, C=-C, because we are not returning anything in our implementation.
-C1;
cout<<C1;
return 0;
}
Inheritance
To be updated
Polymorphism
To be updated