An Overview of OOP with C++
This article is a crash course in Object-Oriented Programming (OOP). It provides an introduction to OOP core concepts with C++ examples. In addition to elaborating on and demonstrating that instances of classes are objects, and that classes are composed of variables (often called properties) which store the object's data, and functions (often called methods), this article explains the use of three main OOP principles: encapsulation, inheritance, and polymorphism.
Encapsulation
Encapsulation involves bundling variables and functions that operate on those variables into a single unit, which we call a class. It's like enclosing the data and functions within a capsule, hence the term "encapsulation." This concept helps in hiding the internal state of an object and only exposing the necessary functionalities. In OOP, access to the data is typically controlled through methods, which are often referred to as getters and setters.
Inheritance
Inheritance is a mechanism in which a new class inherits properties and behaviors (methods) from an existing class. The existing class is called the base class or superclass, and the new class is called the derived class or subclass. This allows for the creation of a hierarchical classification, where subclasses can specialize or extend the functionality of the superclass. Inheritance promotes code reusability and allows for the creation of a more organized and modular codebase.
Polymorphism
Polymorphism is the ability of objects to take on different forms or behaviors based on the context in which they are used. There are two types of polymorphism: compile-time polymorphism (achieved through method overloading and operator overloading) and runtime polymorphism (achieved through method overriding). Polymorphism allows for code to be written in a more generic and flexible manner, making it easier to extend and maintain.
Roadmap
Encapsulation, inheritance, and polymorphism form the foundation upon which OOP languages are built. In the subsequent sections, these topics will be further discussed alongside classes, objects, constructors, and memory management. The article also covers operator overloading, pointers to objects, and the use of templates for writing generic code. The goal of this article is to provide aspiring programmers with the knowledge and skills needed to utilize OOP principles and features available in C++, enabling them to design and develop robust and scalable software solutions.
Classes and Objects
Classes and objects facilitate modular and reusable code. They are central to OOP as classes serve as blueprints for creating objects, which are instances of that class.
Classes and their data members and member functions are created during the design phase of OOP. Classes not only encapsulate data for the object and operations that can be performed on that data, they also provide a template for objects. Objects created from the same class share the same structure and behavior defined by that class.
In C++, you define a class using the class keyword followed by the class name. The following C++ code defines a simple class representing a car:
- Car is the name of the class.
- make, model, and year are private data members representing the attributes of the car.
- drive() is a public member function that defines the behavior of driving the car.
- The constructor initializes the data members when an object is created.
class Car {
public:
// Constructor
Car(string make, string model, int year) : make(make), model(model), year(year) {}
// Member function
void drive() {
cout << make << " " << model << " is driving." << endl;
}
// Accessors
string getMake() const { return make; }
string getModel() const { return model; }
private:
// Data members
string make;
string model;
int year;
};
Once the class is defined, you can create objects (instances) of that class using the class name followed by parentheses. You then access the data members and member functions of these objects using the dot operator (.).
The following code demonstrates how to create objects from the Car class and access their members with the dot operator:
- car1 and car2 are objects created from the Car class.
- Data members (make, model, year) are accessed using dot notation (object_name.member_name).
- Member function (drive()) is accessed similarly.
int main() {
// Creating objects
Car car1("Toyota", "Camry", 2022);
Car car2("Honda", "Accord", 2023);
// Accessing data members
cout << car1.getMake() << endl; // Output: Toyota
cout << car2.getModel() << endl; // Output: Accord
// Accessing member function
car1.drive(); // Output: Toyota Camry is driving.
return 0;
}
By utilizing classes and objects in C++, you create reusable components, organize your code, and encapsulate data and behavior, which allows for better code organization, maintainability, and scalability.
Encapsulation
This section revisits encapsulation to provide a use-case via an example of its implementation and to emphasize its role in crafting robust and secure software systems. Encapsulation promotes data hiding and abstraction by enclosing data within a class and regulating access through methods. This enhances code maintainability, security, clarity, and control. Its primary objective is to shield the internal implementation details of a class from external manipulation, ensuring precise control over data access and modification.
In C++, the access specifiers—public, private, and protected—are the keywords used to control the visibility of class members (i.e., the member data and functions):
- public: Members declared as public are accessible from outside the class. They form the interface through which external code interacts with the class. Public members can be accessed freely by any part of the program.
- private: Members declared as private are accessible only from within the class itself. They are not accessible from outside the class, including derived classes. Private members encapsulate the internal state of the object and are typically accessed and modified through public member functions. This ensures data hiding and prevents direct manipulation of the class's internal state by external code.
- protected: Members declared as protected are similar to private members, but they are accessible within derived classes. This means that protected members can be accessed by the derived class and its subclasses, but not by code outside the class hierarchy. Protected members are often used when implementing inheritance to allow derived classes to access certain aspects of the base class's implementation.
The following code illustrates the syntax for access specifiers in a C++ class:
- publicMethod() can be accessed from outside the class.
- protectedData is accessible within the class and its derived classes.
- privateData is accessible only within the class.
class MyClass {
public:
// Public member function
void publicMethod() {
cout << "This is a public method." << endl;
}
protected:
// Protected data member
int protectedData;
private:
// Private data member
int privateData;
};
Attempting to directly access protectedData or privateData from outside the class (see main() function below for an example) results in compilation errors due to their restricted visibility.
int main() {
MyClass obj;
// Accessing public member function
obj.publicMethod(); // Output: This is a public method.
/* Attempting to access protected and private members
directly (will cause compilation errors) */
/* Compilation Error:
'protectedData' is protected within this context */
// obj.protectedData = 5;
/* Compilation Error:
'privateData' is private within this context */
// obj.privateData = 10;
return 0;
}
Encapsulation facilitates data abstraction, modularity, and information hiding, therefore fostering software integrity and scalability.
Inheritance
In software design, the use of inheritance allows developers to create new classes based on existing ones. Inheritance establishes hierarchies of classes where derived classes inherit properties and behaviors from a base class. The base class acts as a blueprint, defining common characteristics shared by all derived classes. Furthermore, the derived class inherits both the properties and behaviors of the base class, while also having the ability to add its own unique properties and behaviors. This process extends or specializes the functionality of the base class, promoting code reuse, modularity, and extensibility, while enabling polymorphism.
The following code snippet shows how derived classes inherit properties and behaviors from their base classes in C++, showcasing both code reuse and how class hierarchies are established:
- Vehicle is the base class with attributes brand and year, and a member function drive().
- Car is the derived class that inherits from Vehicle, adding its own attribute model and member function accelerate().
- The Car constructor initializes both the Vehicle attributes and its own model.
- In main(), the Car object is used to demonstrate how it accesses inherited members (brand, year, drive()) and its own members (model, accelerate()).
#include <iostream>
#include <string>
using namespace std;
// Base class (superclass)
class Vehicle {
public:
string brand;
int year;
// Constructor
Vehicle(string br, int yr) : brand(br), year(yr) {}
// Member function
void drive() {
cout << "This " << brand << " vehicle is driving." << endl;
}
};
// Derived class (subclass)
class Car : public Vehicle {
public:
string model;
// Constructor
Car(string br, string mdl, int yr) : Vehicle(br, yr), model(mdl) {}
// Additional member function
void accelerate() {
cout << "The " << brand << " " << model
<< " is accelerating." << endl;
}
};
int main() {
// Create a Car object
Car myCar("Toyota", "Camry", 2022);
// Access inherited members
// Output: Year of manufacture: 2022
cout << "Year of manufacture: " << myCar.year << endl;
// Output: This Toyota vehicle is driving.
myCar.drive();
// Access additional members
// Output: Model: Camry
cout << "Model: " << myCar.model << endl;
// Output: The Toyota Camry is accelerating.
myCar.accelerate();
return 0;
}
Polymorphism
Polymorphism is one of the most powerful features of OOP, as it enables code flexibility and extensibility while allowing objects of different types to be treated uniformly through a common interface. This section focuses on the various forms of polymorphism, beginning with function overloading and overriding, virtual functions, and then showcases their practical applications.
Function overloading occurs when multiple functions in the same scope have the same name but different parameter lists. The compiler determines which function to call based on the number and types of arguments provided. This is resolved at compile time.
Function overriding occurs when a derived class provides a specific implementation of a function that is already defined in its base class. The function in the derived class overrides the function in the base class, allowing different behavior to be executed based on the object's actual type. This is resolved at runtime.
A virtual function is a member function declared in a base class with the virtual keyword. It allows the function to be overridden by derived classes. When a derived class overrides a virtual function, the overridden function is called based on the actual type of the object at runtime.
A pure virtual function is a virtual function declared in a base class with the "= 0" syntax. It has no implementation in the base class and must be overridden by derived classes. Classes containing pure virtual functions are called abstract classes, and they cannot be instantiated. Pure virtual functions provide a way to define a common interface for a group of related classes while forcing each derived class to provide its own implementation.
The following code implements function overloading, function overriding, virtual functions, and pure virtual functions in C++; it illustrates those concepts as follows:
- print() is overloaded with different parameter types (int and double).
- Shape is a base class with a virtual function draw() and a pure virtual function area().
- Circle is a derived class that overrides the draw() function and implements the area() function.
- main() demonstrates function overloading and polymorphism using virtual functions by calling draw() and area() through a base class pointer pointing to a derived class object.
领英推è
#include <iostream>
using namespace std;
// Base class
class Shape {
public:
// Virtual function
virtual void draw() {
cout << "Drawing a shape." << endl;
}
// Pure virtual function
virtual void area() = 0;
};
// Derived class
class Circle : public Shape {
public:
// Override virtual function
void draw() override {
cout << "Drawing a circle." << endl;
}
// Implement pure virtual function
void area() override {
cout << "Calculating area of circle." << endl;
}
};
// Function overloading
void print(int num) {
cout << "Printing an integer: " << num << endl;
}
void print(double num) {
cout << "Printing a double: " << num << endl;
}
int main() {
// Function overloading
print(5); // Output: Printing an integer: 5
print(3.14); // Output: Printing a double: 3.14
// Polymorphism using virtual functions
Shape *shapePtr;
Circle circleObj;
shapePtr = &circleObj;
shapePtr->draw(); // Output: Drawing a circle.
shapePtr->area(); // Output: Calculating area of circle.
return 0;
}
Polymorphism provides an alternative to switch statements. Consider a scenario where various types of shapes require different operations to be performed. In the following code, the function performShapeOperation() accepts a Shape object as an argument and invokes its draw() function. Because draw() is a virtual function, the appropriate implementation based on the object's actual type—be it a Circle or a Rectangle—is automatically selected. Therefore, rather than relying on a switch statement to discern the shape type and execute corresponding operations, polymorphism is leveraged to achieve the same outcome in an object-oriented and adaptable manner. Adding new types of shapes is as simple as introducing new derived classes without needing to alter existing code.
#include <iostream>
// Base class representing a shape
class Shape {
public:
virtual void draw() const = 0; // Pure virtual function
};
// Derived class for a circle shape
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};
// Derived class for a rectangle shape
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
// Function that performs operations based on the type of shape using polymorphism
void performShapeOperation(const Shape& shape) {
shape.draw(); // Calls the appropriate draw() based on the actual type of the object
}
int main() {
Circle circle;
Rectangle rectangle;
performShapeOperation(circle); // Draws a circle
performShapeOperation(rectangle); // Draws a rectangle
return 0;
}
Pointers to Objects
Pointers are essential in C++ for managing dynamic memory and accessing objects stored in the heap, providing flexibility in memory management and object lifetimes. They enable objects to be created on the heap rather than the stack, which is particularly useful when dealing with polymorphic behavior and dynamic memory management.
Using pointers to access objects is illustrated in the following code, which demonstrates accessing members of an object with the arrow operator (->), combining dereferencing a pointer (*) with member access (.) in a single step. Consider the following segments of the code:
- Define a class MyClass with a data member data and a member function display().
- In main(), dynamically allocate memory for an object of type MyClass using the new operator, which returns a pointer to the allocated memory.
- Access the object's members (data and display()) using the arrow operator (->).
- Finally, deallocate the dynamically allocated memory using the delete operator to avoid memory leaks.
#include <iostream>
using namespace std;
// Class definition
class MyClass {
public:
int data;
// Constructor
MyClass(int d) : data(d) {}
// Member function
void display() {
cout << "Data: " << data << endl;
}
};
int main() {
// Create an object dynamically on the heap
MyClass* ptr = new MyClass(10);
// Accessing members using the arrow operator
ptr->data = 20; // Modify data member
ptr->display(); // Call member function
// Deallocate memory to avoid memory leaks
delete ptr;
return 0;
}
Dynamic Memory Allocation
Dynamic memory allocation is crucial in C++ programming, allowing developers to allocate memory at runtime. Improper memory management can result in memory leaks, occurring when memory allocated on the heap is not deallocated correctly. This can lead to gradual depletion of available memory, eventual memory exhaustion, program crashes, or system instability. Additionally, memory leaks can degrade program performance and hinder efficient resource utilization.
In C, dynamic memory allocation is accomplished using functions such as malloc(), calloc(), and realloc(), which allocate memory on the heap and return a pointer to the allocated memory. However, manual memory management is necessary, and failure to deallocate memory properly can cause memory leaks and corruption issues.
In contrast, C++ employs the new and delete operators for dynamic memory allocation and deallocation, respectively. new allocates memory for objects on the heap, while delete deallocates dynamically allocated memory. These operators, used with objects and arrays, automate memory management, reducing the risk of memory leaks and corruption.
The following code demonstrates dynamic memory allocation in C++ using the new and delete operators:
- The new operator is used to dynamically allocate memory for a single integer (ptr) and an array of integers (arr).
- Values are assigned to the dynamically allocated memory.
- The delete operator is used to deallocate the dynamically allocated memory to prevent memory leaks.
#include <iostream>
using namespace std;
int main() {
// Dynamic memory allocation for a single integer
int* ptr = new int;
*ptr = 10; // Assigning a value to the dynamically allocated integer
cout << "Dynamically allocated integer: " << *ptr << endl;
// Dynamic memory allocation for an array of integers
int size = 5;
int* arr = new int[size];
for (int i = 0; i < size; ++i) {
arr[i] = i * 2; // Initializing array elements
}
cout << "Dynamically allocated array: ";
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
// Deallocate dynamically allocated memory to avoid memory leaks
delete ptr;
delete[] arr;
return 0;
}
Constructors and Destructors
Constructors and destructors are special member functions of a class responsible for initializing and cleaning up object state, respectively. They are used for object initialization and resource cleanup. Constructors are called automatically when an object of a class is created. They construct and initialize the object's data members and perform any necessary setup tasks. Constructors have the same name as the class and can be overloaded to accept different parameters. There are three types of constructors: 1. Default Constructor, 2. Parameterized Constructor, and 3. Copy Constructor.
- A default constructor is a constructor with no parameters. If a class does not explicitly define any constructors, the compiler provides a default constructor, which initializes data members to default values.
- A parameterized constructor accepts parameters and initializes data members with the values provided in the constructor's parameter list.
- A copy constructor is a special constructor that initializes an object using another object of the same class. It is called when an object is initialized as a copy of another object, passed by value to a function, or returned by value from a function.
Destructors are called automatically when an object goes out of scope or is explicitly deleted with the delete operator. They destroy an object and perform cleanup tasks, such as releasing dynamically allocated memory, closing files, or releasing other resources acquired during the object's lifetime. Destructors have the same name as the class preceded by a tilde (~).
The following code demonstrates the use of constructors and destructors:
- Define a class MyClass with a default constructor, a parameterized constructor, a copy constructor, and a destructor.
- Create objects obj1, obj2, and obj3 of type MyClass using different constructors.
- Each constructor and the destructor print a message to the console to indicate when they are called.
- The destructor is automatically invoked when objects go out of scope at the end of the main() function.
#include <iostream>
#include <string>
using namespace std;
// Class definition
class MyClass {
public:
// Default constructor
MyClass() {
cout << "Default constructor called." << endl;
data = 0;
}
// Parameterized constructor
MyClass(int d) {
cout << "Parameterized constructor called." << endl;
data = d;
}
// Copy constructor
MyClass(const MyClass& obj) {
cout << "Copy constructor called." << endl;
data = obj.data;
}
// Destructor
~MyClass() {
cout << "Destructor called." << endl;
}
// Member function
void display() {
cout << "Data: " << data << endl;
}
private:
int data;
};
int main() {
// Default constructor
MyClass obj1;
// Parameterized constructor
MyClass obj2(10);
// Copy constructor
MyClass obj3 = obj2;
// Destructor (called automatically when objects go out of scope)
obj1.display();
obj2.display();
obj3.display();
return 0;
}
Operator Overloading
Operator overloading, a powerful feature of C++, allows developers to redefine the behavior of operators for user-defined types. It enables them to use operators with custom classes and structures, providing a more natural and intuitive syntax for working with objects of those types. By overloading operators, developers can write more expressive and concise code, enhancing code readability and maintainability.
Operators that are commonly overloaded include the arithmetic, comparison, and assignment operators. Arithmetic operators such as +, -, *, /, etc., can be overloaded to perform custom operations on class objects. Comparison operators like ==, !=, <, >, etc., can be overloaded to define custom comparison logic for class objects. Assignment operators like =, +=, -=, etc., can be overloaded to perform custom actions when assigning one object to another.
The following code demonstrates operator overloading in C++ as follows:
- Define a class Complex to represent complex numbers with real and imaginary parts.
- Overload the + operator for addition, == operator for equality comparison, = operator for assignment, and << operator for output stream.
- Create objects of the Complex class and demonstrate the use of overloaded operators for addition, comparison, and assignment.
Observe how overloaded operators provide a more intuitive syntax for performing operations on objects of the Complex class.
#include <iostream>
using namespace std;
// Class definition
class Complex {
public:
double real;
double imag;
// Constructor
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// Overloading + operator for addition
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// Overloading == operator for equality comparison
bool operator==(const Complex& other) const {
return (real == other.real && imag == other.imag);
}
// Overloading = operator for assignment
Complex& operator=(const Complex& other) {
real = other.real;
imag = other.imag;
return *this;
}
// Overloading << operator for output stream
friend ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
};
int main() {
// Create complex numbers
Complex c1(3.5, 2.0);
Complex c2(2.0, 1.5);
// Perform addition using overloaded operator
Complex sum = c1 + c2;
cout << "Sum: " << sum << endl;
// Check for equality using overloaded operator
if (c1 == c2) {
cout << "c1 and c2 are equal." << endl;
} else {
cout << "c1 and c2 are not equal." << endl;
}
// Assign one object to another using overloaded operator
Complex c3;
c3 = c1;
cout << "c3: " << c3 << endl;
return 0;
}
Templates
Templates enable developers to write generic code capable of handling various data types. Function templates and class templates are the two forms of templates used in C++ programming. They bring flexibility and code reusability by enabling the same code to be used with different data types without duplication.
Function templates allow you to define a function that can operate with any data type. You specify the generic type parameters within angle brackets (<>) following the function name. Inside the function, you can use these type parameters to define variables, perform operations, and return values. Function templates are instantiated with specific data types when called.
Class templates extend the concept of function templates to classes. They allow you to define a generic class that can work with any data type. Similar to function templates, you specify the generic type parameters within angle brackets (<>) following the class name. Inside the class definition, you can use these type parameters to define member variables, member functions, and nested classes.
The following code demonstrates the use of function and class templates in C++ as follows:
- Define a function template my_max() to find the maximum of two values of any data type.
- Define a class template Container to create a generic container that can hold values of any data type.
- Use the function template my_max() with both integer and floating-point values.
- Instantiate objects of the class template Container with specific data types (int and double) and access their member functions.
#include <iostream>
using namespace std;
// Function template for finding the maximum of two values
template <typename T>
T my_max(T a, T b) {
return (a > b) ? a : b;
}
// Class template for a generic container
template <typename T>
class Container {
private:
T value;
public:
// Constructor
Container(T val) : value(val) {}
// Member function to get the value
T getValue() const {
return value;
}
};
int main() {
// Using function template
cout << "Maximum of 5 and 8: " << my_max(5, 8) << endl;
cout << "Maximum of 3.14 and 2.71: " << my_max(3.14, 2.71) << endl;
// Using class template
Container<int> intContainer(10);
Container<double> doubleContainer(3.14);
cout << "Value in intContainer: " << intContainer.getValue() << endl;
cout << "Value in doubleContainer: " << doubleContainer.getValue() << endl;
return 0;
}
Summary
Encapsulation bundles data (attributes) and methods (behaviors) into objects, such as a "Car" object encapsulating attributes like color and model, along with behaviors like accelerating and braking. Other parts of the program interact with these objects through well-defined interfaces, without needing to know their internal details.
Inheritance establishes relationships between entities. For instance, a "Vehicle" superclass might define common properties and behaviors shared by all vehicles, while subclasses like "Car," "Truck," and "Motorcycle" inherit these while adding their own specific attributes and actions. This mirrors real-world hierarchies, making the code more intuitive and maintainable.
Real-world entities often exhibit different behaviors in different contexts. For example, a "Shape" superclass might have subclasses like "Circle" and "Rectangle," each implementing a "calculateArea" method differently. When interacting with objects through a common interface, the program can rely on polymorphism to execute the appropriate behavior based on the object's actual type, offering flexibility for modeling complex real-world systems.
OOP and C++ provide versatile and powerful approaches to software development. By mastering encapsulation, inheritance, and polymorphism, developers can architect complex systems with clarity, flexibility, and maintainability.
Through the use of classes, objects, constructors, destructors, operator overloading, templates, and dynamic memory allocation, C++ empowers developers to create solutions that meet the demands of modern software development. As technology evolves and software systems grow in complexity, the principles and practices of OOP remain valuable tools in programmer's arsenals.