Understanding Classes and Objects in C++ with Practical Examples

Evolution and Core Concepts of C++ Classes

C++ introduced object-oriented features on top of C, aiming for high cohesion and low coupling. Encapsulation improves data security, and many operations can be implicitly handled by the compiler, making code more flexible. Compared to traditional C data structures, C++ automatically invokes constructors for initialization and destructors for cleanup, reducing the risk of forgetting to initialize or free memory. Note that functions like malloc and calloc do not trigger constructors for built-in types; explicit constructor logic is required.

When initializing custom types with nested custom members, direct parameterized construction inside the constructor body fails. Instead, initializer lists must be used. Modern C++ allows built-in and custom types to be initialized with Type(args), and initializer lists are the only place where all member variables are initialized.

Deep copies and large object copies are expensive, leading to the use of references. Operator overloading improves readability, and new/delete address memory management issues. As custom types proliferated, printf became insufficient, prompting the creation of stream insertion/extraction operators and related overloads.

class Component {
public:
    Component(int x, int y) : x_(x), y_(y) {}
private:
    int x_;
    int y_;
};

class Container {
public:
    Container(int a = 10, int b = 20, int c = 30, int d = 40)
        : comp1(a, b), comp2(c, d) {
        // Direct calls like comp1(a,b) here are invalid
    }
private:
    Component comp1;
    Component comp2;
};

int main() {
    int val(10); // Built-in type initialization
    return 0;
}

Procedural vs Object-Oriented Programming

C follows a procedural paradigm, focusing on step-by-step processes via function calls. C++ is object-oriented, focusing on objects and their interactions. Procedural programming breaks complex problems into sequential steps, while object-oriented programming models real-world entities in the computer world.

Objects represent real-world entities (e.g., students, teachers) with extracted attributes (name, age, address). Organizing these entities with proper data structures improves efficiency, avoids congestion, and enables parallel task execution instead of serial logic.

This design philosophy, influenced by Unix-like kernel designs, hides low-level details and unifies upper-layer interfaces, aligning with high cohesion and low coupling.

Defining Classes in C++

C++ upgraded struct to a class while maintaining backward compatibility.

Basic Class Syntax

class Entity {
    // Member functions and variables
};

Member functions can be declared in the class and defined outside using the scope operator ::. If defined inside the class, they are implicitly inline. The same class scope allows member functions to be referenced regardless of declaration order.

Naming Conventions for Members

class Date {
public:
    void setYear(int year) {
        year_ = year;
    }
private:
    int year_;
};

Access Specifiers and Encapsulation

Access specifiers include public, private, and protected, effective only during compilation.

  • public: Accessible from outside the class.
  • private/protected: Not directly accessible from outside.
  • Scope starts at the specifier and ends at the next specifier or class closing brace.
  • class defaults to private access; struct defaults to public.

Encapsulation: A Core OOP Principle

Encapsulation bundles data and methods, hiding implementation details and exposing only necessary interfaces. This protects internal state, similar to how OS kernels restrict direct access to core data. In C, such restrictions are not enforced, but C++ uses access specifiers to prevent misuse.

struct Stack {
    void push(int x) {
        data[top++] = x;
    }
    int* data;
    int top;
    int capacity;
};

int main() {
    Stack s;
    s.push(1);       // Valid
    s.data[s.top] = 0; // Invalid direct access (should be restricted)
    return 0;
}

C++ encapsulates attributes and methods, exposing only selected interfaces via access specifiers.

Class Scope

A class defines a new scope. External member definitions require the :: operator. Only local and global scopes affect variable lifetimes. Classes themselves do not allocate individual member space; objects allocate space as a whole.

Class Instantiation

Creating an object from a class type is instantiation. Defining a class does not allocate memory; only instantiated objects occupy space and access members.

Class Size and Memory Layout

Class size includes only member variables, not member functions (stored in a shared code region).

class WithMembers {
public:
    void init() {}
private:
    int value;
};

class EmptyClass {
public:
    void dummy() {}
};

int main() {
    cout << sizeof(WithMembers) << endl; // 4
    cout << sizeof(EmptyClass) << endl;  // 1 (placeholder)
    return 0;
}

Memory Alignment Rules

  1. First member at offset 0.
  2. Other members aligned to the smaller of their size or compiler default (e.g., 8 in VS).
  3. Total size is a multiple of the maximum alignment.
  4. Nested structs align to their own maximum alignment; total size accounts for all alignments.

Alignment improves read efficiency by matching CPU word sizes, trading space for time.

The this Pointer

C++ implicitly passes a this pointer to non-static member functions, pointing to the calling object. All member accesses use this pointer, though it is transparent to the user.

class Date {
public:
    void set(int year, int month, int day) {
        year_ = year;
        month_ = month;
        day_ = day;
    }
    void print(Date* const this) { // Compiler-transformed
        cout << this->year_ << "-" << this->month_ << "-" << this->day_ << endl;
    }
private:
    int year_;
    int month_;
    int day_;
};

int main() {
    Date d1, d2;
    d1.set(2022, 1, 11);
    d2.set(2022, 1, 12);
    d1.print(); // Equivalent to print(&d1)
    return 0;
}

The this pointer is a stack-allocated parameter, passed right-to-left. Dereferencing a null this causes errors, but passing a null pointer itself is not immediately invalid.

Object Lifecycle

Objects are instantiated, constructors are called automatically, and destructors are invoked when the scope ends before destruction.

6 Default Member Functions

Even empty classes have 6 default member functions generated by the compiler: constructor, destructor, copy constructor, copy assignment operator, and two address-of operators. For built-in types, these defaults perform no initialization, cleanup, or shallow copies.

Constructor

Constructors initialize objects (not create them) when instantiated, called once per object lifetime. They can also implement patterns like RAII.

Characteristics

  1. Same name as the class, no return value.
  2. Automatically called on instantiation.
  3. Can be overloaded for multiple initialization modes.
  4. If no constructor is defined, the compiler generates a default no-argument constructor. Once user-defined, no default is generated.
  5. Default constructors include no-arg, fully defaulted, and compiler-generated versions. Only one default constructor is allowed to avoid ambiguity.
class Example {
public:
    Example(int a = 10, int b = 20) : a_(a), b_(b) {}
private:
    int a_;
    int b_;
};

int main() {
    Example e; // Calls default constructor
    // Example e(); // Misinterpreted as function declaration
    return 0;
}

Compiler-generated defaults do not initialize built-in types but call default constructors for custom members. C++11 allows in-class default values for members, used by the compiler-generated constructor via initializer lists.

Destructor

Destructors clean up resources (not destroy the object itself; local object destruction is compiler-managed). They are called automatically at the end of the object’s lifetime.

Characteristics

  1. Named ~ClassName, no parameters or return value.
  2. Only one destructor per class, cannot be overloaded.
  3. Compiler-generated destructors ignore built-in types but call destructors for custom members.
  4. Destruction order is LIFO (last in, first out) to avoid dependency issues.
  5. Double-free of heap memory is invalid; freeing a null pointer is safe.

Copy Constructor

A copy constructor takes a single reference to an object of the same class (usually const-qualified) and initializes a new object with an existing one.

Characteristics

  1. Overloads the constructor.
  2. Parameter must be a reference to avoid infinite recursion (pass-by-value would trigger another copy constructor).
  3. Built-in types use shallow copy; custom types must call their copy constructors. Shallow copies of pointers cause double-free or unintended modifications, requiring user-defined deep copies.
  4. Compiler-generated defaults perform byte-wise shallow copies.

Copy Assignment Operator

Operator Overloading Basics

Operator overloading enhances readability with special functions named operator@ (where @ is an operator). Key rules:

  • Cannot create new operators (e.g., operator@ is invalid).
  • Atleast one operand must be a class/enum type.
  • Cannot change built-in operator behavior for built-in types.
  • Class member overloads have one fewer parameter (due to implicit this).
  • Certain operators (., ::, sizeof, ?:, .*) cannot be overloaded.
class Date {
public:
    Date(int y = 2024, int m = 1, int d = 1) : y_(y), m_(m), d_(d) {}
    int y_, m_, d_;
};

bool operator<(const Date& a, const Date& b) {
    if (a.y_ != b.y_) return a.y_ < b.y_;
    if (a.m_ != b.m_) return a.m_ < b.m_;
    return a.d_ < b.d_;
}

int main() {
    Date d1(2021, 1, 1), d2(2021, 1, 2);
    cout << (d1 < d2) << endl; // Uses overloaded operator
    return 0;
}

Copy Assignment vs Copy Construction

Copy construction initializes an uninitialized object with a existing one. Copy assignment assigns an already initialized object to another initialized object.

class Date {
public:
    Date(int y = 2024, int m = 1, int d = 1) : y_(y), m_(m), d_(d) {}
    Date(const Date& other) {
        cout << "Copy ctor" << endl;
        y_ = other.y_; m_ = other.m_; d_ = other.d_;
    }
    Date& operator=(const Date& other) {
        cout << "Copy assignment" << endl;
        y_ = other.y_; m_ = other.m_; d_ = other.d_;
        return *this;
    }
private:
    int y_, m_, d_;
};

int main() {
    Date a(1, 1, 1);
    Date b = a;       // Copy constructor
    Date c(2, 2, 2);
    c = a;            // Copy assignment
    return 0;
}

Assignment Operator Characteristics

  1. Returns a reference to support chaining (e.g., a = b = c).
  2. Must be a class member function to avoid conflicts with compiler-generated defaults.
  3. Compiler-generated defaults perform shallow copies, similar to copy constructors.

Const Member Functions and Address-of Operators

Const member functions modify the implicit this pointer to const, preventing modification of member variables.

class Date {
public:
    void print() { cout << "Non-const" << endl; }
    void print() const { cout << "Const" << endl; }
};

int main() {
    Date d1;
    d1.print(); // Calls non-const version
    const Date d2;
    d2.print(); // Calls const version
    return 0;
}

Address-of operators are rarely overridden unless restricting access:

class Date {
public:
    Date* operator&() { return this; }
    const Date* operator&() const { return this; }
};

Separating Class Declaration and Definition

When splitting class code, use scope operators for definitions, and place default arguments only in declarations.

typedef int ItemType;

class Stack {
public:
    Stack(size_t cap = 10);
    void push(const ItemType& item);
    ~Stack();
private:
    ItemType* data_;
    size_t size_;
    size_t capacity_;
};

Stack::Stack(size_t cap) {
    data_ = (ItemType*)malloc(cap * sizeof(ItemType));
    if (!data_) { perror("malloc failed"); return; }
    size_ = 0;
    capacity_ = cap;
}

void Stack::push(const ItemType& item) {
    data_[size_++] = item;
}

Stack::~Stack() {
    free(data_);
    data_ = nullptr;
    size_ = capacity_ = 0;
}

int main() {
    Stack* s = new Stack;
    delete s; // Calls destructor then frees memory
    return 0;
}

Tags: C++ Object-Oriented Programming Classes Constructors Operator Overloading

Posted on Wed, 10 Jun 2026 18:51:21 +0000 by gluck