Classes

1. Introduction

A class is a user-defined blueprint used to create objects. It defines the properties and behaviors that all objects of that type share.

An object is an instance of a class. It represents a real entity and contains actual values for the class’s attributes.

An instance (instance) is a specific object created from a class.


2. Class Declaration

2.1. Class Syntax

A class can contain:

  • Data members (attributes)
  • Member functions (methods)
  • Constructors and destructors
  • Nested types and other class members

Declaration and Definition

A declaration introduces a class name to the compiler A definition provides the complete class body.

class ClassName;   // declaration
class ClassName { // definition
    private:
        // private members
    public:
        // public members
};

Creating Objects

ClassName employee {};

Accessing Members

The dot operator (.) is used to access members of an object.

employee.print();

2.2. Access Specifiers

Access specifiers control which parts of a class can access its members

Specifier Description
public Accessible from anywhere
private Accessible only within the class
protected Accessible within the class and derived classes
  • By default, all members of a struct are public members.
  • By default, the members of a class are private.

2.3. Members:

Member variables/ functions are the variables/functions that belong to a class type. In C, structs only have data members, not member functions.

2.4. Const Class Objects and Const Member Functions

Const class objects just like with normal variables, we can make our class type objects const or constexpr.

Const member function is a member function that guarantees it will not modify the object or call any non-constant member functions.

  • Syntax: <returnType> <nameFunction> const(<params>){}

2.5. Temporary Class Object

A temporary object (sometimes called an anonymous object or an unnamed object) is an object that has no name and exists only for the duration of a single expression.

class IntPair {
    int m_x{};
    int m_y{};
public:
    IntPair(int x, int y) : m_x{x}, m_y{y}{ }
    int x() const { return m_x; }
    int y() const{ return m_y; }
};

void print(IntPair p) { }

void main() {
    // Case 1: Pass variable
    IntPair p { 3, 4 };
    print(p);

    // Case 2: Construct temporary IntPair and pass to function
    // When the function call returns, the temporary object is destroyed.
    print(IntPair { 5, 6 } );

    // Case 3: Implicitly convert { 7, 8 } to a temporary Intpair and pass to function
    print( { 7, 8 } );
}

3. Constructor

A constructor is a special member function that is automatically invoked when a non-aggregate object of a class type is created.

A constructor is used to:

  • Initialize member variables (typically via a member initialization list).
  • Perform any required setup for the object.

Common setup tasks may include:

  • Validating initialization values.
  • Acquiring resources (e.g., memory, files, database connections).
  • Establishing object invariants.

3.1. Syntax

  • A constructor must have the same name as the class (including capitalization).
  • Constructors have no return type (not even void).
  • Constructors may take parameters.
  • Constructors can be overloaded.
  • For class templates, the constructor name is the class name without template arguments.
    class Employee {
    public:
        Employee(){}            // default constructor
    
        Employee(int id){}      // overloaded constructor
    };
    

3.2. Member Initializer Lists

The member initializer list is defined after the constructor parameters.

  • Begins with a colon (:), and then lists each member to initialize along with the initialization value for that variable, separated by a comma (,).
  • Must use a direct form of initialization here (preferably using braces({}), but parentheses(()) works as well)
    Foo(int x, int y) : m_x { x }, m_y { y } {
        // m_x  = x; this is an assignment
    }
    
  • Prefer using the member initializer list to initialize your members over assigning values because in case where members are required to be initialized (such as for data members that are const or references) assignment will not work.

3.3. Default Constructor and Default Arguments

Default Constructor is a constructor that accepts no arguments. Because constructors are functions, we can:

  • Constructors with default arguments
  • Overloaded constructors

An implicit default constructor is generated by the compiler when the class has no user-declared constructors. This constructor has nothing.

An explicitly default constructor is used in case we already create the constructor ourselves, but also want the compiler to generate the default constructor. Using keyword default

class Foo {
public:
    Foo() = default; // generates an explicitly defaulted default constructor
    Foo(int x, int y): m_x { x }, m_y { y }{}

private:
    int m_x {};
    int m_y {};
};

void main() {
    Foo foo{}; // calls Foo() default constructor
}

3.4. Delegating Constructors

Delegating Constructors allow to delegate (transfer responsibility for) initialization to another constructor from the same class type.

  • Simply call the constructor in the member initializer list
  • Use of the static keyword for the const variables member allows us to have a single member that is shared by all class objects.
    class Employee {
        public:
            Employee(std::string_view name) : Employee{ name, 0 } {} // delegate initialization to Employee(std::string_view, int) constructor
    
            Employee(std::string_view name, int id) : m_name{ name }, m_id { id } {}// actually initializes the members
        private:
            std::string m_name {""};
            int m_id { 0 };
    };
    
    void main() {
        Employee e1{ "James" };
        Employee e2{ "Dave", 42 };
    }
    

<br

3.5. Copy Constructor

Copy constructor is a constructor that is used to initialize an object with an existing object of the same type. After the copy constructor executes, the newly created object should be a copy of the object passed in as the initializer.

An implicit copy constructor is generated by the compiler if we do not provide a one.

An explicitly copy constructor by explicitly define our own copy constructor

Fraction(const Fraction& fraction) 
    // Initialize our members using the corresponding member of the parameter
    : m_numerator{ fraction.m_numerator }, m_denominator{ fraction.m_denominator } {}

Using = default to generate a default copy constructor. Using = delete to prevent copies.

// Explicitly request default copy constructor
Fraction(const Fraction& fraction) = default;
Fraction fCopy { f };

// Delete the copy constructor so no copies can be made
Fraction(const Fraction& fraction) = delete;
Fraction f { 5, 3 };
Fraction fCopy { f }; // compile error: copy constructor has been deleted

The rule of three is a well known C++ principle that states that if a class requires a user-defined copy constructor, destructor, or copy assignment operator, then it probably requires all three. In C++11, this was expanded to the rule of five, which adds the move constructor and move assignment operator to the list. Not following the rule of three/rule of five is likely to lead to malfunctioning code. We’ll revisit the rule of three and rule of five when we cover dynamic memory allocation

3.6. Class Initialization and Copy Elision

Class initialization is the process of creating and initializing an object. Copy elision is a compiler optimization that eliminates unnecessary copying of objects.

  • For variables:

    int a;          // no initializer (default initialization)
    int b = 5;      // initializer after equals sign (copy initialization)
    int c(6);       // initializer in parentheses (direct initialization)
    
    // List initialization methods (C++11)
    int d {7};      // initializer in braces (direct list initialization)
    int e = {8};    // initializer in braces after equals sign (copy list initialization)
    int f {};       // initializer is empty braces (value initialization)
    
  • For object with class types:

    class Foo {
    public:
        Foo(){ /**/ }           // Default constructor
        Foo(int x){ /**/ }      // Normal constructor
        Foo(const Foo&){/**/}   // Copy constructor
    };
    
    void main(){
        // Calls Foo() default constructor
        Foo f1;           // default initialization
        Foo f2{};         // value initialization (preferred)
    
        // Calls foo(int) normal constructor
        Foo f3 = 3;       // copy initialization (non-explicit constructors only)
        Foo f4(4);        // direct initialization
        Foo f5{ 5 };      // direct list initialization (preferred)
        Foo f6 = { 6 };   // copy list initialization (non-explicit constructors only)
    
        // Calls foo(const Foo&) copy constructor
        Foo f7 = f3;      // copy initialization
        Foo f8(f3);       // direct initialization
        Foo f9{ f3 };     // direct list initialization (preferred)
        Foo f10 = { f3 }; // copy list initialization
    }
    
  • For all types of initialization:

    • When initializing a class type, the set of constructors for that class are examined, and overload resolution is used to determine the best matching constructor. This may involve implicit conversion of arguments.
    • When initializing a non-class type, the implicit conversion rules are used to determine whether an implicit conversion exists.
    • List initialization disallows narrowing conversions.
    • Copy initialization only considers non-explicit constructors/conversion functions.
    • List initialization prioritizes matching list constructors over other matching constructors.

3.7. Converting Constructors and The explicit Keyword

A converting constructor is a constructor that can be called with a single argument and allows implicit conversion from one type to another.

class Foo {
private:
    int x_{};
public:
    Foo(int x) : x_{x}{ }
    int get_y() const { return x_; }
};

void print_foo(Foo f) {} // has a Foo parameter

void main() {
    print_foo(5); // we're supplying an int argument
}
  • The compiler will check whether there is a constructor that can convert the value in (5) to a Foo object.
  • By default, constructors that can be called with a single argument are converting constructors.
  • Only one user-defined conversion is allowed in an implicit conversion sequence.

The explicit keyword is used to to tell the compiler that a constructor should not be used as a converting constructor.

  • For constructors with a separate declaration (inside the class) and definition (outside the class), the explicit keyword is used only on the declaration.
  • Explicit constructors can be used for direct and direct list initialization
  • Prefer use this key work for constructors that take a single argument.
    class Dollars {
        int dollars_{};
    
    public:
        explicit Dollars(int d) : dollars_{d}{}
        int get_dollars() const { return dollars_; }
    };
    
    void print(Dollars d) {}
    
    void main() {
        print(5); // compilation error because Dollars(int) is explicit
        Dollars d1(5); // ok
        Dollars d2{5}; // ok
    }
    

Return by value and explicit constructors when we return a value from a function, if that value does not match the return type of the function, an implicit conversion will occur. Just like with pass by value, such conversions cannot use explicit constructors.

class Foo {
public:
    explicit Foo() {} // note: explicit
    explicit Foo(int x){} // note: explicit
};

Foo getFoo() {
    // explicit Foo() cases
    return Foo{ };   // ok
    return { };      // error: can't implicitly convert initializer list to Foo

    // explicit Foo(int) cases
    return 5;        // error: can't implicitly convert int to Foo
    return Foo{ 5 }; // ok
    return { 5 };    // error: can't implicitly convert initializer list to Foo
}

4. Destructor

Destructor is a special member function that is called automatically when an object of a non-aggregate class type is destroyed. It provides a reliable mechanism for performing such cleanup automatically, reducing the risk of resource leaks and other errors.

For example, the classes that use a resource (most often memory, but sometimes files, databases, network connections, etc…) often need to be explicitly sent or closed before the class object using them is destroyed. In other cases, we may want to do some record-keeping prior to the destruction of the object, such as writing information to a log file, or sending a piece of telemetry to a server. The term “clean up” is often used to refer to any set of tasks that a class must perform before an object of the class is destroyed in order to behave as expected. If we have to rely on the user of such a class to ensure that the function that performs clean up is called prior to the object being destroyed, we are likely to run into errors somewhere.

Syntax:

  • A destructor has the same name as the class, prefixed with a tilde (~).
  • A class can have only one destructor.
  • A destructor cannot take parameters and has no return type

An implicit destructor: If a class does not declare a destructor, the compiler automatically generates one.


5. this

C++ utilizes a hidden pointer named this. this is a const pointer that stores the address of the current implicit object.

// static void set_id(Simple* const this, int id) { this->m_id = id; }
void set_id(int id) { m_id = id; }

// Simple::set_id(&simple, 2); // note that simple has been changed from an object prefix to a function argument!
simple.set_id(2); 
  • When we call simple.set_id(2), the compiler actually calls Simple::setID(&simple, 2), and simple is passed by address to the function.
  • The function has a hidden parameter named this which receives the address of simple.
  • Member variables inside set_id are prefixed with this->, which points to simple. So when the compiler evaluates this->m_id, it’s actually resolving to simple.m_id.
  • All non-static member functions have a this const pointer that holds the address of the implicit object. this always points to the object being operated on

6. Static member

Static member variables are static duration members that are shared by all objects of the class. Static members exist even if no objects of the class have been instantiated.

  • Syntax: static <type> <name>{<value>}
  • They are shared by all objects of the class and not associated with class objects.
  • They are global variables that live inside the scope region of the class.
  • Access static members using the class name and the scope resolution operator (::).
  • Defining and initializing static member variables:
    • Initialization of static member variables inside the class definition
    • Make your static members inline or constexpr so they can be initialized inside the class definition (.h).

Static member functions are member functions that can be called with no object.

  • Can access to the static members via non-static function but requires us to instantiate an object to call
  • Because static member functions are not attached to an object, they have no this pointer
  • Can directly access other static members (variables or functions), but not non-static members.
    class Counter {
        int id_ {0}; // non-static member variable
    public:
        // Static member variable (shared by all instances)
        static inline int count {0};                ///< C++17+ inline initialization
        static constexpr const char* name {"App"};  ///< compile-time constant
    
        Counter() { ++count; }
        ~Counter() { --count; }
    
        // Static member function
        static void showCount() {
            std::cout << "Count: " << count << '\n';
            // std::cout << id_;  not allowed - no access to non-static members
        }
    
        // Non-static function accessing static member
        void showInfo() const {
            std::cout << "Instance -> " << name << " | Current count: " << count << '\n';
        }
    };
    
    // int Counter::count = 0;  ///< (Alternative old-style definition, no longer needed with inline)
    
    void main() {
        Counter c1, c2;
        c1.showInfo();     // Access static via non-static method
        Counter::showCount(); // Access static function via class name
    
        Counter c3;
        Counter::showCount();
    }
    

7. Nested Types - Nested Class

A nested type is any type declared inside a class, such as a nested class, struct, enum, or type alias.

  • To create a nested type, define the type inside the class under the appropriate access specifier.
  • Outside the enclosing class, a nested type must be referred to using its fully qualified name OuterClass::InnerClass p{};
  • Nested classes are members of the enclosing class and therefore follow normal access control rules.
  • A nested type cannot be forward declared prior to the definition of the enclosing class.
    class Outer {
        class Inner;
    };
    
    class Outer::Inner {
        void print() {}
    };
    

8. Classes and Header Files

Member functions can be defined outside the class definition, just like non-member functions.

  • The function name must be qualified with the class name using the scope resolution operator (::) so the compiler knows the function belongs to the class.
  • Prefer to put your class definitions in a header file with the same name as the class.
  • Trivial member functions (such as access functions, constructors with empty bodies,Default arguments for member functions, etc…) can be defined inside the class definition.

Inline member functions

  • Any member function defined inside the class definition is implicitly inline.
  • Member functions defined outside the class definition are not implicitly inline. (and thus are subject to the one definition per program part of the one-definition rule).
  • If a member function is defined outside the class definition but remains in a header file, it should generally be marked inline.
    /// @brief myclass.h
    class MyClass {
    public:
        void print();
    };
    
    inline void MyClass::print(){}
    
    /// @brief myclass.h / myclass.cpp
    class MyClass
    {
    public:
        void print();
    };
    
    #include "myclass.h"
    void MyClass::print()
    {
        std::cout << "Hello\n";
    }
    

9. Ref Qualifiers (C++11)

A ref-qualifier allows overloading a member function based on whether it is being called on an lvalue or an rvalue implicit object. Using this feature, we can create two versions of getName() – one for the case where our implicit object is an lvalue, and one for the case where our implicit object is an rvalue.

class Employee {
private:
    std::string name_{};
public:
    Employee(std::string_view name): name_ { name } {}

    const std::string& getName() const &  { return name_; } //  & qualifier overloads function to match only lvalue implicit objects
    std::string        getName() const && { return name_; } // && qualifier overloads function to match only rvalue implicit objects
};

// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name) {
    Employee e { name };
    return e;
}

void main() {
    Employee joe { "Joe" };
    std::cout << joe.getName() << '\n'; // Joe is an lvalue, so this calls std::string& getName() & (returns a reference)

    std::cout << createEmployee("Frank").getName() << '\n'; // Frank is an rvalue, so this calls std::string getName() && (makes a copy)
}

10. Friend

A friend declaration uses the friend keyword to grant another class or function access to a class’s private and protected members. A friend can be:

  • A non-member function
  • A member function of another class
  • An entire class

Friends have full access to the private and protected members of the class that grants friendship. Friendship grants access, but does not make the friend a member of the class.

10.1. Friend Non-member Function

A friend non-member function is a regular function that is not a member of a class, but has been granted access to the class’s private and protected members using the friend keyword.

class Accumulator {
private:
    int m_value { 0 };
public:
    // Here is the friend declaration that makes non-member function void print(const Accumulator& accumulator) a friend of Accumulator
    // member function but it have friend keyword, it is instead treated as a non-member function
    friend void print(const Accumulator& accumulator);
};

/// @brief Friend Non-member Function
void print(const Accumulator& accumulator) {
    // it can access the private members of Accumulator
    std::cout << accumulator.m_value;
}

void main() {
    Accumulator acc{};
    print(acc); // call the print() non-member function
    return 0;
}

A friend non-member function can be defined inside the class definition or declared inside the class and defined later outside the class.

Multiple friends: A single function can be a friend of multiple classes simultaneously.

10.2. Friend Member Function

A friend member function is a specific member function of one class that is granted access to the private and protected members of another class.

class Storage;
class Display {
private:
public:
    void displayStorage(const Storage& storage);
};

class Storage {
    bool value_ {};
public:
    // Make the Display::displayStorage member function a friend of the Storage class
    friend void Display::displayStorage(const Storage& storage);
};

void Display::displayStorage(const Storage& storage) {
    std::cout << storage.value_ << '\n';    // access Storage members
}

void main() {
    Storage store{};
    Display display {};
    display.displayStorage(storage);
}

10.3. Friend Class

A friend class is a class that can access the private and protected members of another class.

class Storage {
private:
    int value_ {};
public:
    Storage(int value): value_{value}{}
    friend class Display; ///< Make the Display class a friend of Storage
};

class Display {
    void displayStorage(const Storage& storage) {
        std::cout << storage.value_ << '\n';    // access Storage members
    }
};

void main() {
    Storage store{};
    Display display {};
    display.displayStorage(storage);
}


11. Shallow copying & Deep copying

Shallow copying:

  • C++ does not know much about our class, so the default copy constructor and default assignment operators use memberwise copy and then copy each member of the class individually.
  • Simple classes => work well.
  • Classes handling dynamically allocated memory => just copy the address of the pointer => does not allocate any memory => causes problems.

Deep copying:

  • Allocates memory for the copy and then copies the actual values, so that the copy lives in memory distinct from the source.
  • The original and the copy will not affect each other in any way.
  • This requires write our own default copy constructor and default assigment operators

Role of three: If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.This ensures proper resource management and avoids shallow copy problems.

Role of five: Extends the Rule of Three in C++11 and later. In addition to the destructor, copy constructor, and copy assignment operator, it includes the move constructor and move assignment operator. This allows efficient transfer of resources instead of copying.

Role of zero: The best practice is to write classes that do not manage resources directly, letting the compiler generate all special member functions automatically. This avoids the need to define destructors or copy/move operations manually.


12. TODO:

Copy Assignment Operator Move Constructor / Move Assignment Operator