This blog covers some OOPS concepts, and some sample I/O covering the constructor call order. This is basically just my notes after reading about these concepts. I’m kinda saving it here, because CC has nice markdown, and I figured it might also be helpful for others. So let’s go.
What is a Class?
A class in C++ is a user-defined type or data structure declared with keyword class
that has data and functions (also called member variables and member functions) as its members.
Source: Wiki
The member functions are also called ‘methods’, and I will be referring to them as ‘methods’ for the course of this tutorial.
Example Class
class Entity {
int x, y;
string name;
void increment() {
x++;
y++;
}
};
Visibility
- Private: Can be accessed only within the class, or a friend class.
- Protected: Can be accessed by the class, and any derived class.
- Public: Can be accessed from anywhere.
I will not be going through constructors and destructors. You can look them up here.
Difference between Class and Struct
In C++, there is none. They are exactly the one and the same, except for one small difference. In a class, the default visibility is private:
, while in a struct, it is public:
.
Good Stuff
Static members and methods
Static members and methods can be seen as a global variable/function for a class. There is only one instance of static variables/functions for a particular class.
class Entity {
public:
static int x;
int z;
static func() { cout << x << endl; }
};
int main() {
Entity a, b;
a.x = 4;
cout << a.x << endl;
b.x = 27;
cout << a.x << endl;
b.func();
}
OUTPUT:
4
27
27
When we are dealing with static members/methods, it doesn’t make sense to acces them through an object (nevertheless that’s completely allowed).
Entity::x = 4;
cout << a.x << endl;
Entity::x = 27;
cout << a.x << endl;
Entity::func();
Makes more sense.
As there is only one instance of a static method, static methods can only access static variables. Because, if I access z
inside the function func()
, the compiler doesn’t really know which object’s z
to call. Using z
inside func()
will throw a compile error.
Inheritance
You can have a new class, that derives data members and methods from a base class. You can access the members of the base class A
, through an object of B
.
class A {
public:
A() { cout << "Constructed A\n";}
int x, y;
int sum() {return x + y;}
~A() { cout << "Destructed A\n";}
};
class B : public A {
public:
B() { cout << "Constructed B\n";}
int z;
void init() {z = sum();}
~B() { cout << "Destructed B\n";}
};
int main() {
B obj;
obj.x = 1;
obj.y = 2;
obj.init();
cout << obj.z << endl;
}
Constructed A
Constructed B
3
Destructed B
Destructed A
Whenever an object of the inherited class is constructed, a base class is also constructed implicitly, and liked to the the inherited class. We can also declare a base class object, just like any normal class, but we cannot access the members of B
.
A obj;
obj.z = 10;
This code gives a compile error, error: class A has no member z
.
Overriding
A base class function can be overriden, to do something else in a derived class.
class A {
public:
void getName(){ cout << "This is class A\n"; }
};
class B : public A {
public:
void getName(){ cout << "This is class B\n"; }
};
int main() {
A obja;
B objb;
obja.getName();
objb.getName();
}
OUTPUT:
This is class A
This is class B
Polymorphism
C++ allows a derived class pointer to be used in place of a base class pointer. This property of an object oriented programming language is called Polymorphism.
class A {
public:
void getName(){ cout << "This is class A\n"; }
};
class B : public A {
public:
void getName(){ cout << "This is class B\n"; }
};
void print(A* obj) {
obj->getName();
}
int main() {
A* obja;
B* objb;
print(obja);
print(objb);
}
The code compiles successfully. But! Take a look at the output.
OUTPUT:
This is class A
This is class A
When we pass a derived calss pointer, the program still calls the base classs function, and not the overridden function. This is because the bindings are made during compile time. To overcome this, we have to use virtual functions.
class A {
public:
virtual void getName(){ cout << "This is class A\n"; }
};
class B : public A {
public:
void getName() override { cout << "This is class B\n"; }
};
void print(A* obj) {
obj->getName();
}
int main() {
A* obja;
B* objb;
print(obja);
print(objb);
}
OUTPUT:
This is class A
This is class B
The override
keyword is optional, but it gives more clarity while looking at a code.
Note: Override function cannot have a different return type. If it has different arguments then…
It’s not related to the base class anymore. It’s simply a new overloaded function, you might as well treat it as a separate function defined in the derived class.
Initializer List
class Entity {
public:
Entity() { puts("Default Constructor"); }
Entity(int x) { puts("Parameterized Constructor"); }
};
class A {
int x;
Entity e; // Default Constructor NOT called for case 1.
// Case 1:
A() : e(12) {};
// Case 2:
A() { e = Entity(12); }
// Parameterized Constructor called in both cases.
};
OUTPUT:
// Case 1: (Because of initializer list property)
Parameterized Constructor
// Case 2:
Default Constructor
Parameterized Constructor
Memory Allocatiion
Stack:
Stack allocation of memory is what we always do. Any variable that you normally declare is allocated on the stack, and it dies as soon as the scope ends.
// Stack Allocation
int* a;
*a = 3;
// Heap Allocation
int * a = new int;
*a = 3;
...
delete a; // Do not forget to free the heap memory.
Heap:
If you allocate memory in the heap, it stays forever, even after the scope where it was initialized. To de-allocate the memory, we have to manually do it, by calling delete
. Otherwise, we will have memory leaks. In Java or C#, there is no stack. Everything is allocated on the heap. So there is no need to delete it, as these languages are well managed, and they take care of this for you.
Entity* e = new Entity; // Constructor Call
Entity* e = (Entity*)malloc(sizeof(Entity)); // No constructor call
delete e // Destructor Call
free(e); // No Destructor Call
An example:
class Entity {
public:
Entity () { cout << "Constructing\n";;}
~Entity() {cout << "Gone\n"; }
};
int main() {
Entity* ptr;
{
Entity e;
ptr = &e;
}
puts("Case 1:");
{
Entity* e = new Entity;
ptr = e;
}
puts("Case 2:");
delete ptr;
}
OUTPUT:
Constructing
Gone
Case 1:
Constructing
Case 2:
Gone
If you replace delete ptr
with free(ptr)
, the memory will be freed, but the destructor will not be called. (Meaning Gone
will not be printed.)
Similarly, the constructor will not be called if we use malloc(sizeof(Entity))
, instead of new.
Note that the return type of malloc()
is a void*
, and it has to be casted to Entity*
. (To use it properly. You can of course leave it as a void*
, you’ll just get a compile time warning. But…, don’t do that.)
TODO: Copy constructors, Multiple Inheritance, Virtual Inheritance.
(I might add these in a few days).
This is a very small portion mostly based off of Cherno’s Playlist. If you’d like to learn more stuff like this, check it out.