Skip to main content

C++ Classes

In this section, we will discuss the basics of object-oriented programming (OOP) in C++. A class represents a set of functions (i.e., methods) and data for those methods. An object is an instance of a class.

Object-oriented programming is built on three core ideas, which we define here and then demonstrate throughout this section:

  • Encapsulation — bundling data together with the methods that operate on it inside a class, and controlling what outside code is allowed to access. See Encapsulation.
  • Inheritance — deriving a new class from an existing one so that it reuses and extends the base class's data and behavior. See Inheritance.
  • Polymorphism — letting a single interface operate on many different concrete types, with the correct behavior selected at run time. See Polymorphism.

We will also cover operator overloading.

Defining a Class

In C++, a class is a user-defined data type that combines data members (variables) and member functions (methods) into a single unit. Classes serve as blueprints for creating objects. Here's a simple example of defining a class in C++:

#include <iostream>

class Rectangle {
public:
// Data members
double length_;
double width_;

// Member functions
double Area() {
return length_ * width_;
}

double Perimeter() {
return 2 * (length_ + width_);
}
};

Encapsulation

Encapsulation means grouping data together with the methods that act on it, and then restricting access so outside code can only use the class the way you intend. In C++, access is controlled with three access specifiers:

  • public — members are accessible from anywhere. This is the class's interface.
  • private — members are accessible only from within the class's own methods. This is the default for a class. Use it to hide implementation details and to protect the class's invariants.
  • protected — like private, but also accessible from derived classes (see Inheritance).
class BankAccount {
public:
explicit BankAccount(double balance) : balance_(balance) {}

// Public interface: the only way outside code can change the balance.
void Deposit(double amount) {
if (amount > 0) {
balance_ += amount;
}
}

double balance() const { return balance_; }

private:
// Hidden: outside code cannot set balance_ to a nonsensical value directly.
double balance_;
};

int main() {
BankAccount account(100.0);
account.Deposit(50.0); // OK: uses the public interface
double b = account.balance(); // OK: public getter
// account.balance_ = -999; // ERROR: balance_ is private
return 0;
}

Because balance_ is private, the only way to change it is through Deposit, which can enforce rules (here, rejecting non-positive amounts). This is the main practical benefit of encapsulation: the class controls its own invariants.

struct vs class

A struct and a class in C++ are almost identical — the only difference is the default access level. Members of a struct are public by default, while members of a class are private by default. By convention, use a struct for simple passive data (see structs) and a class when you want encapsulation.

friend

Sometimes another function or class legitimately needs access to your private members. A friend declaration grants that access explicitly. A common use is a stream-output operator that needs to read private data:

#include <iostream>

class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}

// Grant this operator access to Point's private members.
friend std::ostream& operator<<(std::ostream& os, const Point& p);

private:
int x_;
int y_;
};

// Not a member of Point, but allowed to read x_ and y_ because it is a friend.
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x_ << ", " << p.y_ << ")";
return os;
}

int main() {
Point p(3, 4);
std::cout << p << std::endl; // prints (3, 4)
return 0;
}

Use friend sparingly — it deliberately pokes a hole in encapsulation, so only grant it when a function genuinely needs private access.

Constructors

Constructors are special member functions used to initialize objects of a class. Here's an example of a constructor for the Rectangle class:

Rectangle::Rectangle(double l, double w) {
length_ = l;
width_ = w;
}

// Usage
int main() {
Rectangle r(5.0, 3.0); // Creating a Rectangle object with specified dimensions
std::cout << "Area: " << r.Area() << std::endl;
std::cout << "Perimeter: " << r.Perimeter() << std::endl;
return 0;
}

Destructor

In C++, a destructor is a special member function that gets called when an object goes out of scope or is explicitly deleted. Here's an example:

Rectangle::~Rectangle() {
std::cout << "Rectangle object destroyed" << std::endl;
}

// Usage
int main() {
Rectangle r(5.0, 3.0);
// r goes out of scope here, and the destructor is called automatically
return 0;
}

Inheritance

Inheritance lets a new class (the derived class) reuse and extend an existing class (the base class). The derived class automatically gets the base class's members and can add its own or override existing behavior. This models an "is-a" relationship.

#include <iostream>
#include <string>

class Animal {
public:
explicit Animal(const std::string& name) : name_(name) {}

void Eat() const {
std::cout << name_ << " is eating" << std::endl;
}

protected:
// 'protected' data is visible to derived classes but not to outside code.
std::string name_;
};

// Dog "is-a" Animal: it inherits Eat() and name_.
class Dog : public Animal {
public:
explicit Dog(const std::string& name) : Animal(name) {}

void Bark() const {
std::cout << name_ << " says woof" << std::endl; // name_ is protected
}
};

int main() {
Dog d("Rex");
d.Eat(); // inherited from Animal
d.Bark(); // defined by Dog
return 0;
}

The derived class calls the base class's constructor in its initializer list (Dog(...) : Animal(name)). Note how Dog can read the protected member name_, which ordinary outside code could not.

Polymorphism

Polymorphism ("many forms") lets you use a base-class pointer or reference to operate on objects of different derived types, with the correct method chosen at run time. You enable this by marking methods virtual and overriding them in the derived classes.

#include <iostream>

class Shape {
public:
virtual ~Shape() = default; // a polymorphic base class needs a virtual dtor

// A pure virtual method (= 0) makes Shape an abstract interface: you cannot
// instantiate Shape directly, only classes that implement Area().
virtual double Area() const = 0;
};

class Circle : public Shape {
public:
explicit Circle(double radius) : radius_(radius) {}
double Area() const override { return 3.14159 * radius_ * radius_; }

private:
double radius_;
};

class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
double Area() const override { return side_ * side_; }

private:
double side_;
};

// Works for ANY Shape; the correct Area() is selected at run time.
void PrintArea(const Shape& shape) {
std::cout << "Area: " << shape.Area() << std::endl;
}

int main() {
Circle c(2.0);
Square s(3.0);
PrintArea(c); // calls Circle::Area
PrintArea(s); // calls Square::Area
return 0;
}

The override keyword tells the compiler you intend to override a base-class virtual method, so it will error if the signature doesn't match. The factory pattern and mixins build directly on inheritance and polymorphism.

Operator Overloading

Operator overloading allows you to define how operators like +, -, *, and others behave with objects of your class. Here's an example of overloading the + operator for the Rectangle class:

Rectangle operator+(const Rectangle& r1, const Rectangle& r2) {
Rectangle result;
result.length_ = r1.length_ + r2.length_;
result.width_ = r1.width_ + r2.width_;
return result;
}

// Usage
int main() {
Rectangle r1(5.0, 3.0);
Rectangle r2(2.0, 4.0);
Rectangle sum = r1 + r2; // Using the overloaded + operator
std::cout << "Sum of rectangles: Area=" << sum.Area() << ", Perimeter=" << sum.Perimeter() << std::endl;
return 0;
}

Assignment Operators

In C++, you can overload assignment operators (=, +=, -=) to define custom behavior for objects of your class during assignment. This allows you to control how your objects are copied or modified.

Example:

class MyNumber {
private:
int value_;

public:
explicit MyNumber(int v) : value_(v) {}

// Overloading the assignment operator '='
MyNumber& operator=(const MyNumber& other) {
if (this == &other) // Self-assignment check
return *this;

value_ = other.value_;
return *this;
}
};

In this example, we overload the assignment operator to ensure that self-assignment is handled gracefully.

Arithmetic Operators

Arithmetic operators like +, -, *, /, and % can be overloaded to define custom arithmetic operations for objects of your class.

Example:

class Complex {
private:
double real_;
double imaginary_;

public:
Complex(double r, double i) : real_(r), imaginary_(i) {}

// Overloading the addition operator '+'
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_, imaginary_ + other.imaginary_);
}
};

In this example, we overload the addition operator to perform complex number addition.

Relational Operators

Relational operators (==, !=, <, >, <=, >=) can be overloaded to define custom comparison logic for objects of your class.

Example:

class Student {
private:
int id_;

public:
explicit Student(int student_id) : id_(student_id) {}

// Overloading the equality operator '=='
bool operator==(const Student& other) const {
return id_ == other.id_;
}
};

In this example, we overload the equality operator to compare student objects based on their IDs.

Subscript Operator

The subscript operator ([]) can be overloaded to customize how you index into objects of your class.

Example:

#include <cstddef>

class MyString {
private:
char* str_;

public:
explicit MyString(char* s) : str_(s) {}

// Overloading the subscript operator '[]'
char operator[](std::size_t index) const {
return str_[index];
}
};

In this example, we overload the subscript operator to access individual characters of a string-like object.

Function Call

You can overload the function call operator () to make objects of your class callable like functions.

Example:

class MyFunction {
public:
int operator()(int x, int y) const {
return x + y;
}
};

In this example, we overload the function call operator to create an object that behaves like a function, adding two integers.

Bitwise Operators

Bitwise operators (&, |, ^, ~, <<, >>) can be overloaded to define custom bitwise operations for objects of your class.

Example:

class Bitset {
private:
unsigned int data_;

public:
explicit Bitset(unsigned int data) : data_(data) {}

// Overloading the bitwise OR operator '|'
Bitset operator|(const Bitset& other) const {
return Bitset(data_ | other.data_);
}

// Overloading the bitwise AND operator '&'
Bitset operator&(const Bitset& other) const {
return Bitset(data_ & other.data_);
}

unsigned int Value() const {
return data_;
}
};

In this example, we overload the bitwise OR and AND operators to combine the underlying bits of two Bitset objects.