Object Oriented Programming with C++ (Part- 1)

Aarthi_Honguthi
18 min readJust now

--

Hello Tech Enthusiasts! If you have opened this article, that means you either want to learn OOP or C++ or maybe both. I was taught OOP in Java in my academic curriculum, but I wasn’t into Java at that point, so I couldn’t relate to the concept of OOP. If you are a fresher, I would suggest you learn OOP with Java because Java is a complete Object-Oriented Language. Java is a popular programming language that is widely used in industry and academia. It is known for its support for object-oriented programming (OOP), which is a programming paradigm that enables you to organize your code into reusable, modular components called objects.

If your goal is specifically to learn OOP at its purest, then Java isn’t actually OOP enough; the language you are looking for is Smalltalk. If your goal is to become a Python developer, learn Python, even if its version of OOP is really dodgy in some aspects. But honestly, to actually know which language you should learn OOP in depends on you and your preferences. Since I have been using C++ since my first year of college, I chose to deepen my expertise in it. This thorough understanding of C++ can be a significant advantage during interviews, as knowing any language in depth sets you apart. Trust me after reading this article you would master the OOP concept and answer all the interview questions

Prerequisites to Learn OOP in C++

  1. Basic Syntax and Structure of C++
  • Understanding how to write a basic C++ program.
  • Familiarity with the main function, headers, and standard input/output.

2. Data Types and Variables

  • Knowledge of basic data types (int, float, double, char, etc.).
  • Ability to declare and initialize variables.

3. Control Structures

  • Proficiency in using control structures like loops (for, while, do-while) and conditional statements (if, else if, switch).

4. Understanding of Scope and Lifetime of Variables

  • Knowing the difference between local and global variables.
  • Understanding the lifetime of variables in different scopes.

5. Basic Understanding of Memory Management

Knowledge of dynamic memory allocation and deallocation using new and delete.

int* ptr = new int;
*ptr = 10;
delete ptr;

& (Address-of Operator)

  • Purpose: Returns the memory address of a variable.

* (Dereference Operator)

  • Purpose: Accesses the value at a given memory address.

. (Member Access Operator)

  • Purpose: Accesses a member of an object or structure.

-> (Arrow Operator)

  • Purpose: Accesses a member of an object through a pointer.
#include <iostream>

using namespace std;

struct Point {
int x;
int y;
};

class MyClass {
public:
int a;

void setA(int a) {
this->a = a; // Using 'this' pointer to access member variable
}

int getA() {
return this->a;
}
};

int main() {
// & (Address-of Operator)
int num = 42;
int* ptr = &num; // ptr now holds the address of num

// * (Dereference Operator)
int value = *ptr; // value is now 42, the value at the address stored in ptr

// . (Member Access Operator)
Point p;
p.x = 10; // Accessing member x of structure Point
p.y = 20; // Accessing member y of structure Point

// -> (Arrow Operator)
Point* ptrP = &p;
ptrP->x = 30; // Accessing member x of structure Point through a pointer
ptrP->y = 40; // Accessing member y of structure Point through a pointer

// Using 'this' pointer in class MyClass
MyClass obj;
obj.setA(100); // Using the setA method to set the value of a

// Output the results
cout << "Address of num (&num): " << ptr << endl;
cout << "Value at address stored in ptr (*ptr): " << value << endl;
cout << "Point p: (" << p.x << ", " << p.y << ")" << endl;
cout << "Point ptrP: (" << ptrP->x << ", " << ptrP->y << ")" << endl;
cout << "Value of a in MyClass obj: " << obj.getA() << endl;

return 0;
}
Output

7. Basic Understanding of Structs and Enums

Knowing how to define and use struct and enum types.

struct Person {
string name;
int age;
};

enum Color { RED, GREEN, BLUE };

Introduction to OOP

Now that we have covered the prerequisites, let’s dive into the core concepts of Object-Oriented Programming (OOP) in C++. Object-oriented programming (OOP) is all about designing and writing software by creating objects that represent real-world things. These objects have attributes (data) and behaviors (functions) and are created using blueprints called classes. One of the coolest things about OOP is that it treats data as a critical element, focusing on structuring your code around the data and the operations that manipulate it.

When it comes to building today’s complex software, simply putting together a sequence of programming statements and sets of procedures or modules just won’t cut it. We need to use solid construction techniques and program structures that are easy to understand, implement, and modify. Over the years, many approaches have been tried, like modular programming, bottom-up programming, top-down programming, and structured programming. The main goal of all these methods is to create software that’s both maintainable and reliable.

Let’s Understand the Basic Terms in OOP:

Now I don’t wanna give boring old definitions for you to understand what class, attributes, objects and methods are.

#include <iostream>
using namespace std;
class Car {
public:
// Attributes
string color;
string brand;
int modelYear;
double speed; // Speed in km/h
// Method to display car details
void displayDetails() {
cout << "Brand: " << brand << ", Color: " << color << ", Model Year: " << modelYear << ", Speed: " << speed << " km/h" << endl;
}
// Method to calculate speed in meters per second
double calculateSpeedInMPS() {
return speed * 1000 / 3600; // Converting km/h to m/s
}
// Method to set the speed
void setSpeed(double spd) {
speed = spd;
}
};
int main() {
// Creating an object of the Car class
Car myCar;
// Setting attributes
myCar.color = "Red";
myCar.brand = "Toyota";
myCar.modelYear = 2021;
myCar.setSpeed(120); // Setting speed in km/h
// Calling methods
myCar.displayDetails();
cout << "Speed in meters per second: " << myCar.calculateSpeedInMPS() << " m/s" << endl;
return 0;
}
Output of the above code!
Output of the above code

Accessibility of class members

#include<iostream> 
using namespace std;

// class definition
class Circle
{
public:
double radius;

double compute_area()
{
return 3.14*radius*radius;
}

};

// main function
int main()
{
Circle obj;

// accessing public datamember outside class
obj.radius = 5.5;

cout << "Radius is: " << obj.radius << "\n";
cout << "Area is: " << obj.compute_area();
return 0;
}
PUBLIC Example from GFG
#include<iostream> 
using namespace std;

class Circle
{
// private data member
private:
double radius;

// public member function
public:
double compute_area()
{ // member function can access private
// data member radius
return 3.14*radius*radius;
}

};

// main function
int main()
{
// creating object of the class
Circle obj;

// trying to access private data member
// directly outside the class
obj.radius = 1.5;

cout << "Area is:" << obj.compute_area();
return 0;
}
PRIVATE Example from GFG

The output of the above program is a compile time error because we are not allowed to access the private data members of a class directly from outside the class. Yet an access to obj.radius is attempted, but radius being a private data member, we obtained the above compilation error.

However, we can access the private data members of a class indirectly using the public member functions of the class.

// C++ program to demonstrate private 
// access modifier

#include<iostream>
using namespace std;

class Circle
{
// private data member
private:
double radius;

// public member function
public:
void compute_area(double r)
{ // member function can access private
// data member radius
radius = r;

double area = 3.14*radius*radius;

cout << "Radius is: " << radius << endl;
cout << "Area is: " << area;
}

};

// main function
int main()
{
// creating object of the class
Circle obj;

// trying to access private data member
// directly outside the class
obj.compute_area(1.5);


return 0;
}
PRIVATE Example from GFG
// C++ program to demonstrate 
// protected access modifier
#include <bits/stdc++.h>
using namespace std;

// base class
class Parent
{
// protected data members
protected:
int id_protected;

};

// sub class or derived class from public base class
class Child : public Parent
{
public:
void setId(int id)
{

// Child class is able to access the inherited
// protected data members of base class

id_protected = id;

}

void displayId()
{
cout << "id_protected is: " << id_protected << endl;
}
};

// main function
int main() {

Child obj1;

// member function of the derived class can
// access the protected data members of the base class

obj1.setId(81);
obj1.displayId();
return 0;
}
PROTECTED Example from GFG

Using access modifiers helps encapsulate the data and functionality, promoting better control over how class members are accessed and modified. This brings us to the concept of getters and setters, which provide controlled access to private data members.

GETTERS AND SETTERS

Now that we have learned about access modifiers, let’s delve into getters and setters, which are methods that allow controlled access and modification of private data members. They are crucial in implementing the principles of encapsulation and data hiding.

Getter Methods

Getters are methods that return the value of a private data member. They allow read-only access to the data. A getter method usually has a public access modifier to make it accessible outside the class.Setter Methods

Setter Methods

Setters are methods that set or modify the value of a private data member. They allow controlled write access to the data. A setter method usually has a public access modifier and often includes validation to ensure the data remains consistent and valid.

#include <iostream>
#include <string>

using namespace std;

class Treasure {
private:
int diamonds;
int goldCoins;
int silver;

public:
// Getter for diamonds
int getDiamonds() const {
return diamonds;
}

// Setter for diamonds
void setDiamonds(int d) {
if (d >= 0) { // Validation
diamonds = d;
} else {
cout << "Invalid value for diamonds. Must be non-negative." << endl;
}
}

// Getter for goldCoins
int getGoldCoins() const {
return goldCoins;
}

// Setter for goldCoins
void setGoldCoins(int g) {
if (g >= 0) { // Validation
goldCoins = g;
} else {
cout << "Invalid value for gold coins. Must be non-negative." << endl;
}
}

// Getter for silver
int getSilver() const {
return silver;
}

// Setter for silver
void setSilver(int s) {
if (s >= 0) { // Validation
silver = s;
} else {
cout << "Invalid value for silver. Must be non-negative." << endl;
}
}
};

int main() {
Treasure treasure;

// Setting values using setters
treasure.setDiamonds(50);
treasure.setGoldCoins(100);
treasure.setSilver(200);

// Getting values using getters
cout << "Diamonds: " << treasure.getDiamonds() << endl;
cout << "Gold Coins: " << treasure.getGoldCoins() << endl;
cout << "Silver: " << treasure.getSilver() << endl;

return 0;
}
  • Private Data Members: diamonds, goldCoins, and silver are private data members of the Treasure class.
  • Getters: Methods like getDiamonds, getGoldCoins, and getSilver allow read access to the private data members.
  • Setters: Methods like setDiamonds, setGoldCoins, and setSilver allow controlled modification of the private data members, with validation to ensure non-negative values.
  • Main Function: Demonstrates how to use the setter methods to set values and the getter methods to retrieve those values.
// Attempt to access private members directly (this will cause compilation errors)
treasure.diamonds = 100; // Error: 'int Treasure::diamonds' is private within this context
cout << treasure.goldCoins << endl; // Error: 'int Treasure::goldCoins' is private within this context
 // Attempt to set invalid values using setters
treasure.setDiamonds(-10); // Invalid value, setter will reject it
treasure.setGoldCoins(100);
treasure.setSilver(-50); // Invalid value, setter will reject it

// Getting values using getters
cout << "Diamonds: " << treasure.getDiamonds() << endl; // Expected to be 0 as invalid value was set
cout << "Gold Coins: " << treasure.getGoldCoins() << endl;
cout << "Silver: " << treasure.getSilver() << endl; // Expected to be 0 as invalid value was set

Congrats! You’ve made it this far. Now you know how to write a class and object methods along with access modifiers. You’ve indirectly learned about encapsulation.

ENCAPSULATION

Wrapping Up of Data and Member Functions in a Single Unit Class

Encapsulation is one of the fundamental principles of object-oriented programming (OOP). It refers to the bundling of data (attributes) and methods (functions) that operate on that data into a single unit or class. Encapsulation also involves restricting direct access to some of an object’s components, which is where the concept of data hiding comes into play. Encapsulation is better for UNIT TESTING.

CONSTRUCTOR

Class is just a blueprint; it does not have memory but its objects do. The data members use the memory of the computer. Compilers will automatically build constructors by default if you want to allocate values to the data members at the time of object creation.

A constructor is a special method invoked automatically at the time of object creation. It is primarily used for initialization. Here are some key points about constructors:

  • Same Name as Class: A constructor has the same name as the class.
  • No Return Type: Unlike other methods, constructors do not have a return type.
  • Called Once: Constructors are called only once, automatically, at the time of object creation.
  • Memory Allocation: Memory allocation for the object happens when the constructor is called.
#include <iostream>

using namespace std;

class Treasure {
private:
int diamonds;
int goldCoins;
int silver;

public:
// Default constructor
Treasure() : diamonds(0), goldCoins(0), silver(0) {
cout << "Default constructor called!" << endl;
}

// Parameterized constructor
Treasure(int d, int g, int s) : diamonds(d), goldCoins(g), silver(s) {
cout << "Parameterized constructor called!" << endl;
}
//

// Copy constructor
Treasure(const Treasure &t) : diamonds(t.diamonds), goldCoins(t.goldCoins), silver(t.silver) {
cout << "Copy constructor called!" << endl;
}

// Method to display treasure details
void display() {
cout << "Diamonds: " << diamonds << endl;
cout << "Gold Coins: " << goldCoins << endl;
cout << "Silver: " << silver << endl;
}
};

int main() {
// Creating object using default constructor
Treasure t1;
t1.display();

// Creating object using parameterized constructor
Treasure t2(50, 100, 200);
t2.display();

// Creating object using copy constructor
Treasure t3 = t2;
t3.display();

return 0;
}
  1. Default Constructor: Initializes diamonds, goldCoins, and silver to 0.
  2. Parameterized Constructor: Initializes diamonds, goldCoins, and silver to the values passed as arguments.
  3. Copy Constructor: Initializes a new object by copying the values from an existing object. (Default copy Constructor & Custom Copy Constructor)
Output of the above
Special Constructor (Custom since I’ve defined the constructor Treasure with &) used to copy properties of one object into another
Btw, Initialization can be done in anyway!

Can we allocate values to object without constructor?

Yes, you can allocate values to an object without using a constructor by directly accessing and modifying its public data members or using member functions after creating the object. However, this approach is generally not recommended because it can lead to inconsistent or invalid states of the object.

#include <iostream>

using namespace std;

class Treasure {
public:
int diamonds;
int goldCoins;
int silver;

// Method to display treasure details
void display() {
cout << "Diamonds: " << diamonds << endl;
cout << "Gold Coins: " << goldCoins << endl;
cout << "Silver: " << silver << endl;
}
};

int main() {
// Creating object without using a constructor
Treasure t1;

// Allocating values to the object's data members directly
t1.diamonds = 50;
t1.goldCoins = 100;
t1.silver = 200;

// Displaying the values
t1.display();

return 0;
}
int main() {
// Creating object using parameterized constructor
Treasure t1(50, 100, 200);

// Displaying the values
t1.display();

return 0;
}

CONSTRUCTOR OVERLOADING

Constructor overloading in C++ allows a class to have more than one constructor, each with different parameters. This enables the creation of objects with various initialization options(Polymorphism)

int main() {
// Using the default constructor
Treasure t1;
cout << "Default Constructor: Diamonds = " << t1.getDiamonds()
<< ", Gold Coins = " << t1.getGoldCoins()
<< ", Silver = " << t1.getSilver() << endl;

// Using the parameterized constructor
Treasure t2(10, 20, 30);
cout << "Parameterized Constructor: Diamonds = " << t2.getDiamonds()
<< ", Gold Coins = " << t2.getGoldCoins()
<< ", Silver = " << t2.getSilver() << endl;

// Using the parameterized constructor with only diamonds
Treasure t3(50);
cout << "Parameterized Constructor with only diamonds: Diamonds = " << t3.getDiamonds()
<< ", Gold Coins = " << t3.getGoldCoins()
<< ", Silver = " << t3.getSilver() << endl;

return

This demonstrates how you can use constructor overloading to initialize objects of the Treasure class in different ways.

Shallow Copy Vs Deep Copy

A shallow copy creates a new object that is a duplicate of the original object, but the new object only copies the references to the objects in the original object, not the actual objects themselves.

A deep copy creates a new object that is a duplicate of the original object and also creates copies of the objects that the original object references. This means that the new object and the original object are entirely independent.

Shallow Copy’s issue comes to the picture when dealing with dynamic memory allocation. In programming, there are two main types of memory used for storing data: stack memory and heap memory.

Stack Memory

Stack memory is a region of memory that stores temporary variables created by each function, including the main function. The stack is managed in a last-in, first-out (LIFO) manner, meaning that the last function called is the first one to return. Variables allocated on the stack are automatically freed when the function that created them returns. Stack memory is fast to allocate and deallocate, but it is limited in size and typically used for local variables, function parameters, and return addresses.

Heap Memory

Heap memory, on the other hand, is a region of memory used for dynamic memory allocation. Unlike stack memory, variables allocated on the heap are not automatically deallocated when a function returns. Instead, the programmer must manually allocate and free heap memory using functions like new and delete in C++. Heap memory is typically larger than stack memory and is used for variables that need to persist beyond the scope of a single function call, such as objects created with dynamic memory allocation. However, heap memory management can be more complex and prone to issues like memory leaks and dangling pointers if not handled correctly.

Shallow Copy vs. Deep Copy

When a shallow copy is made, the new object created will have pointers that reference the same memory locations as the original object. This can be problematic when the original and copied objects both attempt to manage the same dynamically allocated memory, leading to issues like double deletion or unexpected changes in data.

In contrast, a deep copy duplicates the actual data being pointed to, allocating separate memory on the heap for the new object. This ensures that the original and copied objects are independent, each managing its own memory. Deep copying avoids the pitfalls associated with shallow copying by ensuring that changes to one object do not affect the other, and that each object’s memory can be safely deallocated without impacting the other.

Example

Consider a class that uses dynamically allocated memory:

class Treasure {
private:
int* diamonds;
int* goldCoins;
int* silver;

public:
// Default constructor
Treasure() {
diamonds = new int(0);
goldCoins = new int(0);
silver = new int(0);
cout << "Default constructor called!" << endl;
}

// Parameterized constructor
Treasure(int d, int g, int s) {
diamonds = new int(d);
goldCoins = new int(g);
silver = new int(s);
cout << "Parameterized constructor called!" << endl;
}

// Shallow copy constructor
Treasure(const Treasure &t) {
diamonds = t.diamonds;
goldCoins = t.goldCoins;
silver = t.silver;
cout << "Shallow copy constructor called!" << endl;
}

// Destructor
~Treasure() {
delete diamonds;
delete goldCoins;
delete silver;
cout << "Destructor called!" << endl;
}

// Method to display treasure details
void display() const {
cout << "Diamonds: " << *diamonds << endl;
cout << "Gold Coins: " << *goldCoins << endl;
cout << "Silver: " << *silver << endl;
}
};

int main() {
// Creating object using parameterized constructor
Treasure t1(50, 100, 200);
t1.display();

// Creating object using shallow copy constructor
Treasure t2 = t1;
t2.display();

return 0;
}

In this example, t2 is a shallow copy of t1, meaning both objects share the same dynamically allocated memory. If one object modifies the memory, it affects the other object, and when either object is destructed, it deletes the shared memory, leading to potential errors. Using a deep copy constructor instead would allocate separate memory for t2, ensuring both objects are independent and preventing such issues.

#include <iostream>

using namespace std;

class Treasure {
private:
int* diamonds;
int* goldCoins;
int* silver;

public:
// Default constructor
Treasure() {
diamonds = new int(0);
goldCoins = new int(0);
silver = new int(0);
cout << "Default constructor called!" << endl;
}

// Parameterized constructor
Treasure(int d, int g, int s) {
diamonds = new int(d);
goldCoins = new int(g);
silver = new int(s);
cout << "Parameterized constructor called!" << endl;
}

// Deep copy constructor
Treasure(const Treasure &t) {
diamonds = new int(*t.diamonds);
goldCoins = new int(*t.goldCoins);
silver = new int(*t.silver);
cout << "Deep copy constructor called!" << endl;
}

// Destructor
~Treasure() {
delete diamonds;
delete goldCoins;
delete silver;
cout << "Destructor called!" << endl;
}

// Method to display treasure details
void display() const {
cout << "Diamonds: " << *diamonds << endl;
cout << "Gold Coins: " << *goldCoins << endl;
cout << "Silver: " << *silver << endl;
}
};

int main() {
// Creating object using parameterized constructor
Treasure t1(50, 100, 200);
t1.display();

// Creating object using deep copy constructor
Treasure t2 = t1;
t2.display();

return 0;
}

By using a deep copy constructor, you ensure that each Treasure object is independent and manages its own dynamically allocated memory, preventing the problems associated with shallow copying.

this -> prop or *(this).prop

this is a special pointer in C++ that points to the current object.

The code on the left side is really confusing while you are initializing parameters for the constructor.

diamonds = diamonds;

The diamonds on the left side represents object’s property of class Treasure while the right side diamonds represent the constructor’s parameter. Really confusing right? And yes! That’s how you should understand the concept of this. The usage of this keyword to differentiate between the object's properties and constructor parameters when they have the same name.

1. Referring to the current class instance variable:

this->variable = parameter;

2. Invoking current class method:

this->method();

3. Using this in constructors:

ClassName(int a, int b) {
this->a = a;
this->b = b;
}

4. Passing this as an argument in a method call:

method(this);

5. Passing this as an argument in a constructor call:

OtherClass obj(this);

6. Returning the current class instance from the method:

ClassName* getThis() {
return this;
}
#include <iostream>
using namespace std;

class Example {
private:
int a, b;

public:
// Constructor
Example(int a, int b) {
this->a = a; // Referring to the current class instance variable
this->b = b;
}

// Method to set values
void setValues(int a, int b) {
this->a = a;
this->b = b;
}

// Method to display values
void display() const {
cout << "a: " << a << ", b: " << b << endl;
}

// Method to invoke another method
void invokeDisplay() const {
this->display(); // Invoking current class method explicitly
}

// Method that returns the current class instance
Example* getThis() {
return this;
}

// Method that takes the current instance as an argument
void compare(const Example* obj) const {
if (this->a == obj->a && this->b == obj->b) {
cout << "Objects are equal." << endl;
} else {
cout << "Objects are not equal." << endl;
}
}
};

// Function that takes the current instance as an argument
void compareObjects(const Example* obj1, const Example* obj2) {
if (obj1->getThis()->a == obj2->getThis()->a && obj1->getThis()->b == obj2->getThis()->b) {
cout << "Objects are equal." << endl;
} else {
cout << "Objects are not equal." << endl;
}
}

int main() {
Example obj1(10, 20);
Example obj2(10, 20);

// Using this to refer to instance variables
obj1.setValues(30, 40);
obj1.display();

// Using this to invoke a method
obj1.invokeDisplay();

// Using this to return the current instance
Example* objPtr = obj1.getThis();
objPtr->display();

// Passing this as an argument in a method call
obj1.compare(&obj2);

// Passing this as an argument in a function call
compareObjects(&obj1, &obj2);

return 0;
}

Congratulations! You’ve mastered classes, objects, methods, attributes, constructors, getters, setters along with the essential pillar of Encapsulation. This is just the beginning of your OOP journey. In Part 2, we’ll explore Inheritance, Polymorphism, and Abstraction, unlocking powerful techniques to enhance your C++ skills.
I would recommend you to code along with the article. The treasure example code which I have come up with is completely my idea. You can come up with such exciting examples which will make your understanding even more better.

Stay tuned for more insights and advanced concepts. Get ready for more exciting examples and hands-on coding that will take your skills to the next level.

Happy coding, and see you in Part 2!

--

--