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:
classdeclares the typeRectanglenames 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:
- Access scope begins at the specifier and continues until another specifier appears
- If no subsequent specifier exists, access extends to the closing brace
classdefaults toprivateaccessstructdefaults topublicaccess (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:
- A class serves as a blueprint—it defines structure but allocates no memory
- Multiple objects can instantiate from one class definition
- 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
- Type:
ClassName* const— a constant pointer to the class type - Scope: Only usable within member functions
- Storage: Function parameter stored on the stack (or optimized into a register)
- 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.