Classes

Class as Object

Like struct in C, class is a user-defined data type that can hold data members. Unlike struct, class can also hold member functions, known as method.

class Point {
// access specifier: only accessible within the class
private:
    double x;
    double y;

// access specifier: accessible outside the class
public:
    void set(double x, double y) {
        // `this` pointer pointes to the caller object
        this->x = x;
        this->y = y;
    }

    // const method promises not to modify the object
    double dist(const Point& that) const {
        return sqrt(
            // `this` is optional
            (x - that.x) * (x - that.x) +
            (y - that.y) * (y - that.y)
        );
    }
};  // don't forget the semicolon

Point p;   // create an object of class Point
p.dist();  // access the member function

Constructor & Destructor

A constructor is a special member function that is called when an object is created, used to initialize the object.

A destructor on the other hand is called when an object is destroyed, used to release allocated resources.

class Vector {
private:
    double* elem;
    int sz;

public:
    // default constructor
    Vector() {
        elem = new double[10];
        sz = s;
    }

    // constructor with parameter
    Vector(int s) : // `:` is the initializer list
        elem{new double[s]},
        sz{s}
    {
        for (int i = 0; i < s; ++i) elem[i] = 0;
    }

    // destructor
    ~Vector() { delete[] elem; }

    // copy constructor
    Vector(const Vector& a) : elem{new double[a.sz]}, sz{a.sz} {
        for (int i = 0; i < sz; ++i) elem[i] = a.elem[i];
    }

    // move constructor
    Vector(Vector&& a) : elem{a.elem}, sz{a.sz} {
        a.elem = nullptr;
        a.sz = 0;
    }
};

class Point {
private:
    double x;
    double y;

public:
    Point() = default;            // default constructor (compiler generated)
    Point(const Point&) = delete; // no copy constructor (prevent copy)
    Point(Point&&) = delete;      // no move constructor (prevent move)
};

Sharing

friend keyword grants access to private members of a class to another class or function.

// forward declaration
class List;
class Node;

class Node {
    friend class List; // List can access private members of Node
private:
    int data;
    Node* next;
};

class List {
private:
    Node* head;
    Node* tail;
};

static keyword makes a member shared by all instances of the class.

class Counter {
private:
    static int count; // shared by all instances of Counter
public:
    Counter() { ++count; }
    ~Counter() { --count; }
    static int getCount() { return count; }
};

int Counter::count = 0; // initialize the static member
Counter a;
Counter::getCount(); // => 1
Counter b, c;
Counter::getCount(); // => 3

Inheritance

// base class
class Animal {
// access specifier: accessible within the class and derived class
protected:
    int age;
public:
    void type() { std::cout << "I'm an animal!" << std::endl; }
    void eat() { std::cout << "yum yum~" << std::endl; }
};

// derived class
class Cat : public Animal {
public:
    // override the base class method
    void type() { std::cout << "I'm a cat!" << std::endl; }
    // implement a new method
    void meow() {
        std::cout << "mew mew~ I'm "
                  << age  // inherited from Animal
                  << " years old!"
                  << std::endl;
    }
};

Cat pet;
pet.eat();  // inherited from Animal
pet.meow(); // defined in Cat

Sometimes multiple classes share a common base class, and they need to access the common base class members. In this case, virtual inheritance is used, to avoid ambiguity (the common base class is inherited only once).

class Animal { /* ... */ };
class Dog : virtual public Animal { /* ... */ };
class Cat : virtual public Animal { /* ... */ };
class SiameseCat : public Dog, public Cat { /* ... */ };

When defining a base class, we can use virtual keyword to make a method overridable by derived classes; override keyword to ensure that the method is indeed overriding a base class method; and final keyword to mark a method as not overridable.

It's also possible to define a pure virtual method, which has no implementation and must be overridden by derived classes.

class Animal {
public:
    // virtual method (with default implementation)
    virtual void info() { std::cout << "I'm an animal!" << std::endl; }
    // pure virtual method (no implementation, must be overridden)
    virtual void eat() = 0;
};

class Feline : public Animal {
public:
    // override the base class method
    void info() override { std::cout << "I'm a feline!" << std::endl; }
    // override the pure virtual method
    void eat() override { std::cout << "I eat meat!" << std::endl; }
};

class Cat : public Feline {
public:
    // make final overrides
    void info() override final { std::cout << "I'm a cat!" << std::endl; }
    void eat() override final { std::cout << "I eat fish!" << std::endl; }
};

Polymorphism

Polymorphism is the ability to treat objects of different derived classes through a common interface (base class).

// dynamic dispatch (runtime polymorphism)
Cat saplyn;
Animal* ani_ptr = &saplyn;
Animal& ani_ref = saplyn;

ani_ptr->type(); // calls Cat::type()
ani_ref.type();  // calls Cat::type()

// however, the following will result in slicing
Animal ani = saplyn; // only the base part of saplyn is copied
ani.type();          // calls Animal::type()

Operator Overloading

The operators that can be overloaded are listed below:

  • Binary Operators: +, -, *, /, %, ^, &, |, =, <<, >>, ,, ->*, ->, []
  • Comparison Operators: <, >, <=, >=, ==, !=
  • Short-Circuit Operators: &&, ||
  • Unary Operators: ~, !
  • Pre/Post Unary Operators: ++, --
  • Function Call Operator: ()

Note that . (dot operator) and ? : (ternary operator) cannot be overloaded. Additionally, the behavior of some operators (like the short-circuit behavior of logical operators (&& and ||)) cannot be changed.

class Point {
private:
    double x;
    double y;
public:
    Point() = default;
    Point(double x = 0, double y = 0) : x(x), y(y) {}

    double get_x() const { return x; }
    double get_y() const { return y; }
};

// overload the `+` operator
Point operator+(const Point& a, const Point& b) {
    return Point{a.get_x() + b.get_x(), a.get_y() + b.get_y()};
}

// overload the `<<` operator
std::ostream& operator<<(std::ostream& os, const Point& p) {
    return os << '(' << p.get_x() << ", " << p.get_y() << ')';
}

In C++, some operators have multiple forms (pre/post-incr, etc.), they are overloaded with different signatures.

// pre-increment (++a)
Type& operator++();
// post-increment (a++)
Type operator++(int);

// l-value []
Type& operator[](int i);
// r-value []
Type operator[](int i) const;