← Back to all posts

13 C++ Notes

Classes and Objects

C++ Notes


What Is a Class

C++
class Cylinder {
public:
    // data members
    double radius{};
    double height{};

public:
    // default constructor (compiler generates it)
    Cylinder() = default;

    // parameterized constructor
    Cylinder(double r, double h) {
        radius = r;
        height = h;
    }

    // member function
    double volume() {
        return PI * radius * radius * height;
    }
};

Constructor Member Initializer List

C++
class Example {
    const int x;
    int& ref;

public:
    Example(int v, int& r)
        : x(v), ref(r) // initializer list: runs BEFORE the body
    {} // body can be empty here
};

Creating Objects

SyntaxWhat It Does
Cylinder cylinder;Stack object. Requires a default constructor. Primitive data members contain garbage values unless your constructor initializes them.
Cylinder cylinder{};Stack object. Requires a default constructor. Primitive data members are zero-initialized if you don't define your own default constructor. If you do define one, behavior depends on how you initialize inside it.
Cylinder cylinder(2, 3);Stack object. Calls the parameterized constructor with arguments 2 and 3.
Cylinder* c = new Cylinder(2, 3);Heap object. Requires a parameterized constructor. Must be freed manually with delete.
Cylinder cylinder();Disaster. This looks like object creation but is actually parsed as a function declaration. Never use this form.

Accessing Members of Heap Objects

C++
delete c;
c = nullptr; // prevent dangling pointer

Destructors

C++
~Cylinder() {
    delete p; // release heap memory owned by this object
}

When Destructors Are Called

The compiler calls the destructor automatically when it's about to destroy the object. The important cases: when a local stack object goes out of scope (its block ends), when a heap object is released with delete, when an object is passed by value to a function (the local copy inside that function is destroyed when the function ends), and when a local object is returned from a function (the original copy may be destroyed after the return if RVO or NRVO optimization isn't applied).

Order of Construction and Destruction

Stack objects are destroyed in reverse order of creation, LIFO, last in, first out.

C++
// creation order
obj1 constructor called
obj2 constructor called
obj3 constructor called

// destruction order (reversed)
obj3 destructor called
obj2 destructor called
obj1 destructor called

If a class contains a raw pointer and you don't define a copy constructor, the compiler makes a shallow copy. Both objects end up pointing to the same heap memory. When one is destroyed, its destructor deletes that memory, the other object now holds a dangling pointer, and its destructor then tries to delete already-freed memory, a double-delete crash. This is exactly why the Rule of Three and Rule of Five exist.

The this Pointer

Method Chaining

By returning this (or *this) from a setter, you can call multiple setters in a single line. Two forms exist.

Pointer Return (via this)Reference Return (via *this)
Return typeCylinder*Cylinder&
Returnsthis*this
Call syntaxArrow: c->setHeight(10)->setRadius(5);Dot: c.setHeight(10).setRadius(5);
NotesRequires a pointer object to use cleanlyMore common in C++. Works on both stack and heap objects
C++
// pointer return style
Cylinder* setHeight(double h) {
    height = h;
    return this;
}

// reference return style (preferred)
Cylinder& setHeight(double h) {
    height = h;
    return *this;
}

Struct vs Class

Size of a Class Object

sizeof(object) is greater than or equal to the sum of its member sizes. The difference comes from padding for alignment, and from the fact that pointers are counted by their own size, not by what they point to.


Code Example: main.cpp

C++
#include <iostream>
#include <string>
#define PI 3.14159

// ============================================================
// CLASS WITH CONSTRUCTOR, DESTRUCTOR, METHOD CHAINING
// ============================================================
class Cylinder {
public:
    double radius{};
    double height{};
    double* volumePtr; // pointer member (heap memory)

    // default constructor
    Cylinder() {
        volumePtr = new double(0);
        std::cout << "Default constructor, this = " << this << "\n";
    }

    // parameterized constructor
    Cylinder(double r, double h) {
        radius = r;
        height = h;
        volumePtr = new double(volume());
        std::cout << "Parameterized constructor, this = " << this << "\n";
    }

    // destructor: releases heap memory owned by this object
    ~Cylinder() {
        delete volumePtr;
        std::cout << "Destructor called, this = " << this << "\n";
    }

    // const member function: does not modify the object
    double volume() const {
        return PI * radius * radius * height;
    }

    // method chaining via pointer return
    Cylinder* setHeight(double h) { height = h; return this; }
    Cylinder* setRadius(double r) { radius = r; return this; }

    // method chaining via reference return (more common in C++)
    Cylinder& setHeightRef(double h) { height = h; return *this; }
    Cylinder& setRadiusRef(double r) { radius = r; return *this; }
};

// ============================================================
// MEMBER INITIALIZER LIST (for const and reference members)
// ============================================================
class Example {
    const int x;
    int& ref;
public:
    Example(int v, int& r)
        : x(v), ref(r) // must be initialized here, not in body
    {}
    void show() { std::cout << "x = " << x << ", ref = " << ref << "\n"; }
};

// ============================================================
// STRUCT: public by default, used for simple data grouping
// ============================================================
struct Point {
    int x;
    int y;
    void print() { std::cout << "(" << x << ", " << y << ")\n"; }
};

// ============================================================
// MAIN
// ============================================================
int main() {
    std::cout << "\n--- STACK OBJECTS ---\n";
    Cylinder c1;       // default constructor, garbage in radius/height
    Cylinder c2{};     // default constructor, radius/height zero-initialized
    Cylinder c3(2, 3); // parameterized constructor
    std::cout << "Volume of c3 = " << c3.volume() << "\n"; // PI*4*3 = ~37.7

    std::cout << "\n--- HEAP OBJECT ---\n";
    Cylinder* cHeap = new Cylinder(4, 5);
    std::cout << "Heap volume = " << cHeap->volume() << "\n";
    (*cHeap).radius = 10;  // dereference then access
    cHeap->height  = 2;   // arrow operator (preferred)
    std::cout << "Updated heap volume = " << cHeap->volume() << "\n";
    delete cHeap;
    cHeap = nullptr;

    std::cout << "\n--- METHOD CHAINING ---\n";
    c3.setHeight(10)->setRadius(5);         // pointer style
    std::cout << "After pointer chaining: " << c3.volume() << "\n";
    c3.setHeightRef(7).setRadiusRef(3);    // reference style (preferred)
    std::cout << "After reference chaining: " << c3.volume() << "\n";

    std::cout << "\n--- MEMBER INITIALIZER LIST ---\n";
    int val = 100;
    Example ex(42, val);
    ex.show();        // prints: x = 42, ref = 100
    val = 200;         // change original variable
    ex.show();        // prints: x = 42, ref = 200 (ref sees the change)

    std::cout << "\n--- STRUCT ---\n";
    Point p{1, 2};
    p.print();        // prints: (1, 2)

    std::cout << "\n--- OBJECT SIZE ---\n";
    std::cout << "sizeof(c1):    " << sizeof(c1) << "\n"; // pointer + 2 doubles + padding
    std::cout << "sizeof(Point): " << sizeof(p)  << "\n"; // 2 ints = 8 (no padding needed)

    // stack objects c1, c2, c3 destroyed here in reverse order (LIFO)
    return 0;
}

Closing

Constructors, destructors, the hidden this pointer, and why a class object's size is rarely just the sum of its parts. Next up, inheritance, where one class starts building on another.

← Previous 12 - Functions, Part 2: Lambdas, Templates, and Concepts Next → 14 - Inheritance