Last updated: December 2025

C++ Classes and Objects: Complete Guide — Constructors, Destructors, Encapsulation & DSA (2025)

By CoodeVerse Editorial Team ✓ 2025 Verified ⏱ 22 min read 🎯 Beginner–Intermediate 📦 C++11/17
Difficulty:
Beginner–Intermediate — Prerequisites: Variables & Types, Functions

⚡ Quick Answer: Classes and Objects in C++

Classes and objects are the heart of C++ OOP — they are the building blocks for every data structure you'll implement: linked lists, stacks, queues, trees, and graphs. This guide covers everything from the absolute basics to the Rule of Three/Five, with complete annotated examples, a visual class anatomy diagram, and five DSA implementations.

🏗️

Class Basics

Declaration & syntax

🔐

Access Specifiers

public / private / protected

⚙️

Constructors

All 4 types explained

🧹

Destructors

Cleanup & memory

🛡️

Encapsulation

Getters, setters, this

📜

Rule of Three/Five

Deep copy & move

⚖️

class vs struct

When to use which

🏆

DSA Examples

5 complete classes

🏗️ Section 1

Class Declaration, Anatomy & First Object

A class is a user-defined type that bundles data (member variables) and behavior (member functions/methods). By itself a class definition allocates no memory — only when you create an object does memory get allocated.

🔬 Anatomy of a C++ class
string name; // data member — encapsulated
int id; // only own methods can access this
Student(string n, int i); // constructor
~Student(); // destructor
void display() const; // const method — won't modify object
void setName(string n); // setter with validation
string getName() const; // getter — returns copy
student_class.cpp — declaration, objects, dot & arrow operatorsC++
#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int    id;

public:
    // Parameterized constructor with initializer list
    Student(string n, int i) : name(n), id(i) {
        cout << "Constructor: " << name << endl;
    }

    // Destructor
    ~Student() {
        cout << "Destructor:  " << name << endl;
    }

    // Const method — cannot modify *this
    void display() const {
        cout << "Name: " << name << ", ID: " << id << endl;
    }

    void   setName(string n) { name = n; }
    string getName() const  { return name; }
};

int main() {
    // Stack object — destroyed when scope ends
    Student s1("Alice", 101);
    s1.display();               // dot operator

    // Heap object — destroyed when we delete
    Student* s2 = new Student("Bob", 102);
    s2->display();              // arrow operator
    s2->setName("Robert");
    s2->display();
    delete s2;                  // destructor called here

    // s1 destructor called here (scope end)
    return 0;
}
Output
Constructor: Alice
Constructor: Bob
Name: Alice, ID: 101
Name: Bob, ID: 102
Name: Robert, ID: 102
Destructor: Robert
Destructor: Alice
🔐 Section 2

Access Specifiers: public, private, protected

public

Accessible from anywhere — inside the class, from objects, from derived classes.

Use for: methods, constructors, the public interface

private

Accessible only within the class's own methods. Default for class.

Use for: data members, internal helpers

protected

Accessible within the class AND derived (child) classes. Not from outside.

Use for: members derived classes need to extend
Encapsulation rule: Make data members private. Expose what callers need through public methods. This lets you change the internal representation without breaking code that uses your class — the definition of a stable API.
⚙️ Section 3

Constructors — All 4 Types With Examples

A constructor is a special member function called automatically when an object is created. It has the same name as the class, no return type, and can be overloaded.

Default constructor
No parameters. Called by MyClass obj;. Compiler auto-generates one if you define no constructors. MyClass() : x(0) {}
Parameterized constructor
Takes arguments. MyClass(int v) : x(v) {}. Called by MyClass obj(5); or MyClass obj{5};
Copy constructor
Creates from existing object. MyClass(const MyClass& o) : x(o.x) {}. Called by MyClass b = a;
Move constructor (C++11)
Transfers resources from temporary. MyClass(MyClass&& o) : x(o.x) { o.x=nullptr; }. Enables efficient vector resizing.
all_constructors.cppC++
#include <iostream>
using namespace std;

class Point {
public:
    double x, y;

    // 1. Default constructor
    Point() : x(0), y(0) {
        cout << "Default ctor: (0,0)\n";
    }

    // 2. Parameterized constructor
    Point(double px, double py) : x(px), y(py) {
        cout << "Param ctor: (" << x << "," << y << ")\n";
    }

    // 3. Copy constructor
    Point(const Point& o) : x(o.x), y(o.y) {
        cout << "Copy ctor: (" << x << "," << y << ")\n";
    }

    void print() const { cout << "("<<x<<","<<y<<")\n"; }
};

int main() {
    Point p1;          // default ctor
    Point p2(3.0,4.0); // parameterized
    Point p3 = p2;     // copy ctor
    Point p4(p2);      // also copy ctor
    return 0;
}
Output
Default ctor: (0,0)
Param ctor: (3,4)
Copy ctor: (3,4)
Copy ctor: (3,4)
initializer list — why it mattersC++
class Circle {
    const double PI;  // const member — MUST use initializer list
    double&       ref; // reference member — MUST use initializer list
    double        r;

public:
    // Initializer list runs BEFORE the constructor body
    // Order matches declaration order, not list order!
    Circle(double& external, double radius)
        : PI(3.14159),   // const — can only be init, not assigned
          ref(external),  // reference — can only be init, not assigned
          r(radius)       // preferred: direct init, not default+assign
    { }

    double area() const { return PI * r * r; }
};

// WITHOUT initializer list these would be compile errors:
// Circle(...) { PI = 3.14; }   ← error: assignment to const
// Circle(...) { ref = x; }     ← error: reference must be initialized
Always use initializer lists. For const and reference members, initializer lists are mandatory. For all other members, they are preferred: a member initialized in the list is constructed directly from the value; one assigned in the body is first default-constructed, then assigned — two operations instead of one.
🧹 Section 4

Destructors and Resource Management

The destructor (~ClassName()) runs automatically when an object is destroyed — when a stack object leaves scope, or when delete is called on a heap object. Its job: release any resource the object owns (heap memory, file handles, network connections, locks).

destructor_demo.cpp — dynamic array with proper cleanupC++
#include <iostream>
using namespace std;

class DynamicArray {
    int* data;
    int  size;
public:
    DynamicArray(int n) : size(n), data(new int[n]()) {
        // new int[n]() zero-initializes the array
        cout << "Allocated " << n << " ints\n";
    }

    ~DynamicArray() {
        delete[] data;  // [] required for arrays — not delete data
        cout << "Freed array of size " << size << "\n";
    }

    int& operator[](int i) { return data[i]; }
    int  getSize() const    { return size; }
};

int main() {
    {   // inner scope
        DynamicArray arr(5);
        arr[0] = 10; arr[1] = 20;
        cout << arr[0] << " " << arr[1] << endl;
    }   // ← destructor called here automatically
    cout << "Back in outer scope\n";
    return 0;
}
Output
Allocated 5 ints
10 20
Freed array of size 5
Back in outer scope
Always use delete[] for arrays allocated with new[]. Using delete (without []) on an array is undefined behavior — typically only the first element's destructor runs and the rest of the memory is leaked. Pair new T[n] with delete[] and new T with delete. Better yet: use std::vector<T> which handles this automatically.
🛡️ Section 5

Encapsulation, Getters/Setters, const Methods & this

encapsulation_demo.cpp — validated access + const methods + thisC++
#include <iostream>
#include <stdexcept>
using namespace std;

class BankAccount {
private:
    double balance;
    string owner;

public:
    BankAccount(string owner, double initial)
        : owner(owner), balance(initial) {}

    // const getter — returns a copy; const objects can call this
    double getBalance() const { return balance; }
    string getOwner()  const { return owner; }

    // Validated setter — encapsulation enforces invariant
    void deposit(double amount) {
        if (amount <= 0) throw invalid_argument("Deposit must be positive");
        balance += amount;
    }

    void withdraw(double amount) {
        if (amount > balance) throw runtime_error("Insufficient funds");
        balance -= amount;
    }

    // 'this' used to disambiguate and for method chaining
    BankAccount& setOwner(string owner) {
        this->owner = owner;  // 'this->owner' = member, 'owner' = param
        return *this;         // enables chaining: acc.setOwner("X").deposit(100)
    }

    void display() const {
        cout << owner << ": $" << balance << endl;
    }
};

int main() {
    BankAccount acc("Alice", 1000.0);
    acc.deposit(500.0);
    acc.withdraw(200.0);
    acc.display();

    // Method chaining using *this
    acc.setOwner("Alice Smith").deposit(100.0);
    acc.display();

    try {
        acc.withdraw(99999.0);  // throws — balance protected
    } catch(const exception& e) {
        cout << "Error: " << e.what() << endl;
    }
    return 0;
}
Output
Alice: $1300
Alice Smith: $1400
Error: Insufficient funds
📜 Section 6

Rule of Three & Rule of Five

When a class manually manages a resource (heap memory, file handle, mutex), the compiler- generated copy/move operations do the wrong thing. The Rule of Three (C++03) and Rule of Five (C++11) tell you when to define your own.

Special functionCalled whenDefault behaviorMust define when…
DestructorObject destroyedDoes nothingClass owns heap resources
Copy constructorMyClass b = a;Shallow copyShallow copy causes double-free
Copy assignmentb = a; (after both exist)Shallow copySame as above
Move constructor (C++11)MyClass b = move(a);Shallow copyPerformance: transfer, not copy
Move assignment (C++11)b = move(a);Shallow copySame as move constructor
rule_of_three.cpp — deep copy to prevent double-freeC++
#include <iostream>
#include <cstring>   // memcpy
using namespace std;

class Buffer {
    int* data;
    int  size;
public:
    Buffer(int n) : size(n), data(new int[n]()) {}

    // 1. Destructor
    ~Buffer() { delete[] data; }

    // 2. Copy constructor — DEEP copy
    Buffer(const Buffer& o) : size(o.size), data(new int[o.size]) {
        memcpy(data, o.data, size * sizeof(int));
    }

    // 3. Copy assignment — deep copy + self-assignment guard
    Buffer& operator=(const Buffer& o) {
        if (this == &o) return *this;  // self-assignment guard
        delete[] data;
        size = o.size;
        data = new int[size];
        memcpy(data, o.data, size * sizeof(int));
        return *this;
    }

    int& operator[](int i) { return data[i]; }
    int  getSize() const   { return size; }
};

int main() {
    Buffer a(3); a[0]=1; a[1]=2; a[2]=3;
    Buffer b = a;    // copy constructor — DEEP copy
    b[0] = 99;       // modifying b should NOT affect a
    cout << a[0] << endl;  // 1 — independent copy ✓
    cout << b[0] << endl;  // 99
    return 0;
}
Output
1
99
Modern C++ shortcut: Instead of implementing the Rule of Three manually, use std::vector, std::unique_ptr, or std::shared_ptr as member types. These handle deep copying and cleanup automatically — your class can use the compiler-generated defaults and get correct behavior for free.
⚖️ Section 7

class vs struct — When to Use Which

Featureclassstruct
Default accessprivatepublic
Default inheritanceprivatepublic
Supports methods✓ Yes✓ Yes
Supports constructors✓ Yes✓ Yes
Supports inheritance✓ Yes✓ Yes
ConventionComplex objects with encapsulationPlain data grouping, competitive programming
class vs struct — conventional usageC++
// struct: plain data record, no invariants to enforce
struct Point {
    double x, y;              // public by default — fine for plain data
};

// struct in competitive programming — custom comparator for Kruskal's
struct Edge {
    int u, v, w;
    bool operator<(const Edge& e) const { return w < e.w; }
};

// class: complex object with invariants — private data, public interface
class BankAccount {
private:
    double balance;  // invariant: balance >= 0 enforced by methods
public:
    void deposit(double a);    // validates before modifying
    void withdraw(double a);   // validates before modifying
};
🏆 Section 8

DSA Applications: 5 Complete Class-Based Data Structures

1. Linked List

linked_list.cppC++
#include <iostream>
using namespace std;

class LinkedList {
private:
    struct Node {      // nested private struct
        int data;
        Node* next;
        Node(int v) : data(v), next(nullptr) {}
    };
    Node* head;
    int   sz;

public:
    LinkedList() : head(nullptr), sz(0) {}

    ~LinkedList() {           // destructor frees all nodes
        while (head) {
            Node* t = head;
            head = head->next;
            delete t;
        }
    }

    void push_front(int v) {
        Node* n = new Node(v);
        n->next = head;
        head = n; sz++;
    }

    void push_back(int v) {
        Node* n = new Node(v);
        if (!head) { head = n; sz++; return; }
        Node* cur = head;
        while (cur->next) cur = cur->next;
        cur->next = n; sz++;
    }

    void display() const {
        for (Node* c = head; c; c = c->next)
            cout << c->data << " → ";
        cout << "null\n";
    }
    int size() const { return sz; }
};

int main() {
    LinkedList lst;
    lst.push_back(1); lst.push_back(2); lst.push_back(3);
    lst.push_front(0);
    lst.display();
    cout << "Size: " << lst.size() << endl;
    return 0;  // destructor frees all nodes
}
Output
0 → 1 → 2 → 3 → null
Size: 4

2. Stack (array-based)

stack_class.cppC++
#include <iostream>
#include <stdexcept>
using namespace std;

template<typename T, int MAX = 100>
class Stack {
    T   arr[MAX];
    int top;
public:
    Stack() : top(-1) {}

    void push(const T& v) {
        if (top == MAX-1) throw overflow_error("Stack full");
        arr[++top] = v;
    }
    T pop() {
        if (isEmpty()) throw underflow_error("Stack empty");
        return arr[top--];
    }
    const T& peek() const {
        if (isEmpty()) throw underflow_error("Stack empty");
        return arr[top];
    }
    bool isEmpty() const { return top == -1; }
    int  size()    const { return top + 1; }
};

int main() {
    Stack<int> s;
    s.push(10); s.push(20); s.push(30);
    cout << "Top: " << s.peek() << endl;
    while (!s.isEmpty()) cout << s.pop() << " ";
    cout << endl;
    return 0;
}
Output
Top: 30
30 20 10

3. BST Node — struct for competitive programming

bst_node.cppC++
#include <iostream>
using namespace std;

struct BST {
    int  val;
    BST* left  = nullptr;
    BST* right = nullptr;
    BST(int v) : val(v) {}

    static BST* insert(BST* root, int v) {
        if (!root) return new BST(v);
        if (v < root->val) root->left  = insert(root->left,  v);
        else               root->right = insert(root->right, v);
        return root;
    }

    static void inorder(BST* root) {
        if (!root) return;
        inorder(root->left);
        cout << root->val << " ";
        inorder(root->right);
    }
};

int main() {
    BST* root = nullptr;
    for (int x : {5,3,7,1,4,6,8}) root = BST::insert(root, x);
    cout << "Inorder: ";
    BST::inorder(root);
    cout << endl;
    return 0;
}
Output
Inorder: 1 3 4 5 6 7 8

Best Practices & Common Mistakes

✅ Private data, public interface

Never expose data members directly. Use methods that validate before modifying — that's the point of encapsulation.

✅ Use initializer lists

Always initialize members in the constructor's initializer list. Required for const and reference members; preferred everywhere else.

✅ Mark read-only methods const

int getX() const — lets const objects and const references call the method. Without const, passing your object as const ref breaks.

✅ Follow Rule of Three/Five

If your class manages heap memory (raw pointer + new), define the destructor, copy constructor, and copy assignment. Better: use std::vector/unique_ptr instead.

✅ Give base classes virtual destructors

If your class is used polymorphically (Base* p = new Derived()), the base destructor MUST be virtual — otherwise only the base destructor runs on delete.

✅ Use smart pointers

unique_ptr and shared_ptr handle destruction automatically — eliminating the need to manually implement the Rule of Three in most cases.

❌ Uninitialized members

Local variables of built-in types (int, double) in an uninitialized object contain garbage. Initialize ALL members in constructors.

❌ delete without delete[]

For int* arr = new int[n], always use delete[] arr. Using plain delete arr is undefined behavior.

Frequently Asked Questions

The only technical difference is the default access level: class defaults to private; struct defaults to public. Both support all OOP features: constructors, destructors, methods, inheritance, and templates. Convention: use struct for plain data aggregation with no invariants to protect; use class when you need encapsulation (private data + validated public interface).
Use initializer lists for: (1) const members — cannot be assigned after construction. (2) Reference members — same reason. (3) Member objects without a default constructor. (4) All other members — preferred because it initializes directly rather than default-constructing then assigning (two operations vs one). The initializer list executes before the constructor body in declaration order.
The Rule of Three: if your class needs a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs all three. This applies when the class manually manages a resource (raw heap pointer, file handle). Without it, the compiler-generated copy does a shallow copy — both objects share the same pointer, and the first destructor frees the memory, leaving the second with a dangling pointer that causes a double-free crash.
this is a pointer to the current object available inside every non-static member function. It's used to: (1) Disambiguate when a parameter has the same name as a member: this->name = name;. (2) Return the current object for method chaining: return *this;. (3) Pass the current object to another function. In const member functions, this is a pointer to const — you cannot modify members through it.
If you delete a derived object through a base class pointer and the base destructor is not virtual, only the base destructor runs — the derived destructor is skipped, leaking resources and leaving the derived class in a corrupt state. Fix: add virtual ~Base() = default; to any class intended to be used as a base class. Rule: if any method in your class is virtual, the destructor should be virtual too.
Use stack allocation (MyClass obj;) when: the object's lifetime should be tied to the current scope, and the object is of reasonable size. Use heap allocation (new MyClass()) when: the object must outlive the current scope, the object is very large, or you need polymorphism via a base pointer. In modern C++, prefer std::make_unique<MyClass>() over raw new — it prevents leaks even when exceptions are thrown.

Related C++ Topics on CoodeVerse

Inheritance & Polymorphism Advanced OOP Arrays & Strings Pointers & Memory Templates STL Containers Smart Pointers 📚 Full C++ Course

CoodeVerse Editorial Team

Senior engineers and CS educators. All code tested with GCC 13 and Clang 16, updated to C++17.