Understanding C++ Classes and Objects: Core Concepts Explained

Procedural vs Object-Oriented Programming

C follows a procedural paradigm, focusing on functions that solve problems step by step. Consider a laundry scenario: you might manually execute a sequence of washing, rinsing, and drying steps. Each step requires separate logic and coordination.

C++ introduces a different approach through classes and objects, treating data and operations as cohesive units. Rather than managing discrete functions, you interact with self-contained objects that handle their own behavior.

Evolution from C Structs to C++ Classes

In C, structs can only hold data members. C++ extends this capability by allowing structs to contain functions as well.

typedef int Item;

struct Inventory
{
    void Initialize()
    {
        // initialization logic
    }

    void AddItem(Item entry)
    {
        // insertion logic
    }

    void RemoveItem()
    {
        // deletion logic
    }

    Item* buffer;
    int count;
    int capacity;
};

Unlike C, you can instantiate this struct without the struct keyword:

Inventory stock;  // Direct instantiation, no "struct" prefix needed

While C++ structs work as enhanced structs, the more common approach uses the class keyword for defining types.

Class Definition Syntax

class Rectangle
{
// body contains member functions and member variables
};  // semicolon required

Key rules:

  • class declares the type
  • Rectangle names the class
  • The closing brace must be followed by a semicolon

Two Definition Patterns

Pattern 1: All definitions inside the class declaration

class Rectangle
{
public:
    void SetDimensions(int w, int h)
    {
        width = w;
        height = h;
    }
private:
    int width;
    int height;
};

Note: Short functions defined within the class may be treated as inline functions by the compiler.

Pattern 2: Declaration and definition separated

// Rectangle.h
class Rectangle
{
public:
    void SetDimensions(int w, int h);
private:
    int width;
    int height;
};

// Rectangle.cpp
#include "Rectangle.h"
void Rectangle::SetDimensions(int w, int h)
{
    width = w;
    height = h;
}

Best practice: Keep small utility functions inline within the class, separate complex implementations for better readability.

Naming Conventions

CamelCase Guidelines

Combine words without underscores, capitalizing the first letter of each new word.

Element Convention Example
Class names All words capitalized BankAccount
Function names First word lowercase, others capitalized calculateTotal()
Member variables Prefix first word with underscore _balance, _accountId

Compare problematic and proper naming:

// Problematic - unclear parameter vs member variable
class Date
{
public:
    void SetDate(int year, int month, int day)
    {
        year = year;  // ambiguous assignment
    }
private:
    int year;
    int month;
    int day;
};

// Proper - clear distinction
class Date
{
public:
    void SetDate(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

Access Specifiers

Three Access Levels

C++ provides three access specifiers to control visibility of class members:

Specifier External Access Typical Use
public Allowed Interface methods
protected Blocked Inherited class access
private Blocked Internal data

Rules:

  1. Access scope begins at the specifier and continues until another specifier appears
  2. If no subsequent specifier exists, access extends to the closing brace
  3. class defaults to private access
  4. struct defaults to public access (maintains C compatibility)
class BankAccount
{
public:
    void Deposit(double amount);   // accessible anywhere
    void Withdraw(double amount);  // accessible anywhere
    
protected:
    double GetBalance() const;     // accessible to derived classes only
    
private:
    double _balance;               // accessible within this class only
    int _accountId;                // accessible within this class only
};

Understanding Encapsulation

OOP fundamentals include encapsulation, inheritance, and polymorphism. At this stage, encapsulation is the primary focus.

Encapsulation definition: Binding data with the functions that manipulate that data, hiding internal implementation while exposing controlled interfaces.

Practical Example

Consider a stack implementation:

C approach (unprotected):

typedef struct {
    int* data;
    int top;
    int capacity;
} Stack;

// Some implementations expose StackTop()
// Others directly access data[top]

The problem: developers might bypass the interface and access internal array directly. If stack top initialization varies (0 vs -1), users accessing data[top] directly will encounter undefined behavior.

C++ approach (encapsulated):

class Stack
{
private:
    int* _data;
    int _top;
    int _capacity;
    
public:
    int StackTop();  // Only way to retrieve top element
    void Push(int value);
    void Pop();
};

By making members private, external code cannot bypass the defined interface. The stack enforces correct usage through its public methods.

Class Scope

A class definition creates a distinct namespace. When defining member functions outside the class declaration, use the scope resolution operator :: to indicate which class owns the function.

class Timer
{
public:
    void SetTimeout(int seconds);  // declaration
private:
    int _duration;
};

// Outside class definition - must qualify with class name
void Timer::SetTimeout(int seconds)
{
    _duration = seconds;
}

Object Instantiation

Creating actual objects from a class definition is called instantiation.

Key concepts:

  1. A class serves as a blueprint—it defines structure but allocates no memory
  2. Multiple objects can instantiate from one class definition
  3. Each object occupies physical memory for its member variables
class Person
{
public:
    void SetAge(int years)
    {
        _age = years;
    }
private:
    int _age;
};

int main()
{
    Person user1;   // allocates memory for _age
    Person user2;   // separate memory for _age
    
    user1.SetAge(25);  // operates on user1's _age
    user2.SetAge(30);  // operates on user2's _age
    
    return 0;
}

Calculating Object Size

Object size follows structure memory alignment rules. The combined size of all member variables, adjusted for alignment, determines the object's footprint.

Member functions do not contribute to object size. C++ uses three possible storage models:

Model 1: Each object contains both variables and function copies

  • Wasteful: identical function code duplicated across instances

Model 2: Objects store variables plus a pointer to a function table

  • Size includes the pointer overhead

Model 3 (actual implementation): Objects store only member variables

  • Member functions reside in shared code segment

Verification

class Empty { };

int main()
{
    std::cout << sizeof(Empty) << std::endl;  // outputs 1
    return 0;
}

Empty classes receive 1 byte for object identity, even though no data is stored.

class Person
{
public:
    void Introduce();
private:
    char _gender;
    int _age;
    double _height;
};

int main()
{
    std::cout << sizeof(Person) << std::endl;  // includes alignment padding
    return 0;
}

The this Pointer

Motivation

Consider this class with two instances:

class Clock
{
public:
    void SetTime(int hour, int minute, int second)
    {
        _hour = hour;
        _minute = minute;
        _second = second;
    }
    
    void Display()
    {
        std::cout << _hour << ":" << _minute << ":" << _second << std::endl;
    }
    
private:
    int _hour;
    int _minute;
    int _second;
};

int main()
{
    Clock morning, evening;
    morning.SetTime(6, 30, 0);
    evening.SetTime(18, 45, 30);
    
    morning.Display();   // which object's data?
    evening.Display();
    
    return 0;
}

When morning.Display() executes, how does the function know it should print morning's data, not evening's?

The Solution

C++ compilers implicitly add a hidden pointer parameter to every non-static member function. This pointer references the specific object invoking the function.

The compiler transforms:

void Clock::Display()
{
    std::cout << _hour << ":" << _minute << ":" << _second << std::endl;
}

Into effectively:

void Clock::Display(Clock* const this)
{
    std::cout << this->_hour << ":" << this->_minute << ":" << this->_second << std::endl;
}

this Pointer Characteristics

  1. Type: ClassName* const — a constant pointer to the class type
  2. Scope: Only usable within member functions
  3. Storage: Function parameter stored on the stack (or optimized into a register)
  4. Transmission: Compiler automatically passes the object address when invoking the function

Usage Restrictions

Explicit declaration is prohibited:

// INVALID - compiler error
void Clock::Display(Clock* const this)
{
    // this cannot be self-declared
}

Implicit usage is automatic:

void Clock::SetTime(int hour, int minute, int second)
{
    _hour = hour;  // internally becomes this->_hour = hour
}

Critical Scenario: nullptr this

class Logger
{
public:
    void LogMessage()
    {
        std::cout << "Message logged" << std::endl;
    }
    
    void ProcessData()
    {
        // No dereferencing of this occurs
    }
    
private:
    int _status;
};

int main()
{
    Logger* ptr = nullptr;
    ptr->LogMessage();     // works - no member variable access
    ptr->ProcessData();    // works - this is nullptr but not dereferenced
    
    return 0;
}

Both calls succeed because no member variables are accessed. The this pointer is nullptr, but no dereferencing occurs.

void Logger::ProcessData()
{
    _status = 10;  // becomes this->_status = 10
}

int main()
{
    Logger* ptr = nullptr;
    ptr->ProcessData();  // crash - dereferencing nullptr
    return 0;
}

This crashes because assigning to _status dereferences the nullptr this pointer.

Tags: C++ Object-Oriented Programming Classes Objects encapsulation

Posted on Fri, 18 Sep 2026 16:52:19 +0000 by teynon