In procedural programming, programs are organized around sequential routines and standalone functions operating on detached variables. While this works well for small scripts, software systems modeling real-world entities—such as banking ledgers, student records, or vehicle telematics—quickly become difficult to manage when state and behavior are kept apart.
Object-Oriented Programming (OOP) solves this architectural problem by bundling data and the functions that manipulate that data into cohesive, self-contained units. In C++, the two foundational pillars of this approach are classes and objects.
What You’ll Learn
- The conceptual distinction between a class and an object
- Why classes are required to manage complexity at scale
- Basic C++ class syntax and object instantiation
- The difference between data members (state) and member functions (behavior)
- How access specifiers (
public,private, andprotected) protect internal state - How to define member functions inside versus outside the class using the scope resolution operator (
::) - How multiple objects maintain independent memory and state
- The role and mechanics of the implicit
thispointer - Practical real-world modeling using
Student,Car, andBankAccountexamples
What Is a Class?
A class is a user-defined data type that serves as a blueprint or template for creating objects.
Think of an architectural blueprint for a house:
- The blueprint specifies the layout, room dimensions, wiring conduits, and plumbing connections.
- The blueprint itself is not a physical building—you cannot live inside a blueprint, and it occupies no physical land.
- However, using that single blueprint, construction crews can build dozens of distinct physical houses.
In C++, a class definition tells the compiler:
- What data attributes every instance of this type will contain (data members).
- What operations can be executed on those attributes (member functions).
A class definition does not allocate memory for individual object values. It merely declares a custom type within your program’s type system.
+-------------------------------------------------------+
| Class: Student |
+-------------------------------------------------------+
| Attributes (Data Members): |
| - string name |
| - int id |
| - double gpa |
+-------------------------------------------------------+
| Behaviors (Member Functions): |
| + printDetails() |
| + isHonorStudent() |
+-------------------------------------------------------+
What Is an Object?
An object is a concrete instance of a class that exists in computer memory.
When you declare a variable whose type is a class, the compiler reserves memory on the runtime stack (or heap) to hold that specific object’s data members.
Each object possesses three defining characteristics:
- Identity: A unique location in memory (its memory address).
- State: The current values assigned to its data members.
- Behavior: The member functions defined by its class that can read or mutate its state.
Returning to the blueprint analogy: if Car is the class blueprint, your silver 2024 sedan parked in the driveway is an object. Your neighbor’s red 2022 SUV is another object instantiated from the same automotive rules, with its own independent mileage, color, and fuel level.
Why Classes Are Needed
To appreciate why C++ introduced classes, consider tracking student records using purely procedural variables:
// Procedural approach: variables are detached from logic
string student1Name = "Shafin";
int student1Id = 101;
double student1Gpa = 3.85;
string student2Name = "Ayman";
int student2Id = 102;
double student2Gpa = 3.60;
This procedural approach breaks down quickly:
- No cohesion: An entity’s data is fragmented across unrelated variables.
- No data protection: Any function anywhere in the file can accidentally set
student1Gpa = -50.0. - Poor scalability: Adding ten fields to a student record requires declaring and passing dozens of individual variables across function parameters.
Classes bind data and logic together into a single structured type:
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int id;
double gpa;
void display() {
cout << "ID: " << id << " | Name: " << name << " | GPA: " << gpa << endl;
}
};
int main() {
Student s1;
s1.name = "Shafin";
s1.id = 101;
s1.gpa = 3.85;
s1.display();
return 0;
}
Output:
ID: 101 | Name: Shafin | GPA: 3.85
Class Syntax and Access Specifiers
In C++, a class definition begins with the class keyword followed by the class identifier and an opening brace {. The definition ends with a closing brace and a mandatory semicolon };.
Access Specifiers
Access specifiers determine where data members and member functions can be accessed from in your program:
| Specifier | Accessibility |
|---|---|
private | Accessible only by member functions of the same class (and friend entities). |
public | Accessible from anywhere the object is visible. |
protected | Accessible within the class and by derived classes (used in inheritance). |
[!IMPORTANT] In C++, members of a
classare private by default. In contrast, members of astructare public by default. Aside from this default access difference,classandstructare syntactically identical in C++.
Let’s observe access control in action:
#include <iostream>
#include <string>
using namespace std;
class BankAccount {
private:
double balance; // Hidden from external access
public:
string accountNumber;
void setBalance(double initialAmount) {
if (initialAmount >= 0.0) {
balance = initialAmount;
} else {
balance = 0.0;
}
}
void deposit(double amount) {
if (amount > 0.0) {
balance += amount;
cout << "Deposited: $" << amount << endl;
}
}
void printBalance() {
cout << "Account " << accountNumber << " balance: $" << balance << endl;
}
};
int main() {
BankAccount account;
account.accountNumber = "BA-90210";
account.setBalance(500.0);
// Direct access to public members is permitted:
cout << "Managing account: " << account.accountNumber << endl;
// Direct access to private members is rejected by the compiler:
// account.balance = 1000000.0; // COMPILE ERROR: 'balance' is private!
account.deposit(250.0);
account.printBalance();
return 0;
}
Output:
Managing account: BA-90210
Deposited: $250
Account BA-90210 balance: $750
By marking balance as private, the class protects its internal state from unauthorized modification or invalid numbers (such as negative balances). External code must interact with the object via controlled public member functions.
Member Functions: Inside vs Outside the Class
C++ allows you to define member functions in two distinct ways:
1. Defined Inside the Class Definition
Functions defined inside the class body are treated by the compiler as inline candidates automatically:
#include <iostream>
#include <string>
using namespace std;
class Book {
public:
string title;
int pages;
// Defined inside the class
void showSummary() {
cout << "'" << title << "' has " << pages << " pages." << endl;
}
};
2. Declared Inside, Defined Outside
For larger codebases, declaring member functions inside the class and providing their definitions outside the class separates the class interface from its implementation.
You connect the external function body to its class using the scope resolution operator (::):
ReturnType ClassName::FunctionName(Parameters) {
// Body
}
Here is a complete program demonstrating external definition:
#include <iostream>
#include <string>
using namespace std;
class Book {
public:
string title;
string author;
int pages;
// Declaration inside the class
void showSummary();
bool isThickBook();
};
// Definition outside the class using the scope resolution operator (::)
void Book::showSummary() {
cout << "'" << title << "' by " << author << " (" << pages << " pages)" << endl;
}
bool Book::isThickBook() {
return pages > 400;
}
int main() {
Book cppBook;
cppBook.title = "Programming: Principles and Practice Using C++";
cppBook.author = "Bjarne Stroustrup";
cppBook.pages = 1312;
cppBook.showSummary();
if (cppBook.isThickBook()) {
cout << "This is a comprehensive reference book." << endl;
} else {
cout << "This is a concise introductory book." << endl;
}
return 0;
}
Output:
'Programming: Principles and Practice Using C++' by Bjarne Stroustrup (1312 pages)
This is a comprehensive reference book.
Multiple Objects and Independent State
Every time you instantiate an object, C++ allocates a distinct region of memory for that instance. Modifying the data members of one object has zero impact on another object instantiated from the same class.
#include <iostream>
#include <string>
using namespace std;
class Car {
public:
string brand;
int speed;
void accelerate(int increase) {
speed += increase;
}
void displayStatus() {
cout << brand << " is traveling at " << speed << " km/h." << endl;
}
};
int main() {
Car sedan;
sedan.brand = "Toyota Camry";
sedan.speed = 60;
Car sportsCar;
sportsCar.brand = "Porsche 911";
sportsCar.speed = 110;
// Accelerating sportsCar does not alter sedan's speed
sportsCar.accelerate(40);
sedan.displayStatus();
sportsCar.displayStatus();
return 0;
}
Output:
Toyota Camry is traveling at 60 km/h.
Porsche 911 is traveling at 150 km/h.
Memory Representation
Stack Frame (main)
+------------------------------------+
| sedan (Object 1) |
| brand: "Toyota Camry" |
| speed: 60 |
+------------------------------------+
| sportsCar (Object 2) |
| brand: "Porsche 911" |
| speed: 150 |
+------------------------------------+
Notice that both objects share the same compiled machine code for accelerate() and displayStatus(). However, when sportsCar.accelerate(40) is executed, C++ passes the address of sportsCar behind the scenes so the function knows which object’s speed variable to mutate. This brings us to the this pointer.
The this Pointer
Inside any non-static member function, C++ provides a built-in pointer named this.
The this pointer holds the memory address of the specific object upon which the member function was invoked. Its type inside a member function of class Car is Car* const (a constant pointer to a Car).
Primary Uses of this
- Resolving Parameter Name Collisions: When a function parameter has the same name as a data member,
this->memberNameexplicitly refers to the object’s member. - Method Chaining: Returning
*this(by reference) allows calling multiple member functions in a single statement.
#include <iostream>
#include <string>
using namespace std;
class StudentProfile {
private:
string name;
int id;
public:
// Parameter names match the private member names
void setInfo(string name, int id) {
// name = name; // Ambiguous self-assignment!
this->name = name; // Resolves unambiguously to the member variable
this->id = id;
}
void printMemoryAddress() {
cout << "Object for " << this->name
<< " lives at memory address: " << this << endl;
}
};
int main() {
StudentProfile alice;
alice.setInfo("Alice", 101);
StudentProfile bob;
bob.setInfo("Bob", 102);
alice.printMemoryAddress();
bob.printMemoryAddress();
return 0;
}
Output:
Object for Alice lives at memory address: 0x7ffee1428a20
Object for Bob lives at memory address: 0x7ffee1428a40
Notice that this points to two different addresses during the two invocations. It always identifies the current instance executing the code.
Common Beginner Mistakes
1. Forgetting the Trailing Semicolon After the Class Body
A classic syntax error in C++ is omitting the semicolon after the closing brace of a class:
class Vector2D {
double x;
double y;
} // ERROR: Missing semicolon here!
int main() {
return 0;
}
The compiler interprets subsequent tokens as attempting to declare a variable of type Vector2D, resulting in cryptic compiler diagnostics. Always close classes with };.
2. Assuming Default Access Is Public
In languages like Java or C#, class members may have package-private or default access. In C++, members of a class are strictly private unless declared under a public: label:
class Counter {
int count; // PRIVATE by default!
};
int main() {
Counter c;
// c.count = 10; // COMPILE ERROR: 'count' is private
return 0;
}
3. Attempting to Initialize Data Members Directly Without Defaults
Prior to C++11, in-class member initialization was not allowed. In modern C++, you should provide default values directly:
class Account {
public:
int id = 0; // Clean modern C++ default
double balance{0.0}; // Modern brace-initialization default
};
Best Practices
- Keep data members private: Expose state only through documented, validated member functions. Direct public member variables make it impossible to enforce invariants.
- Use clear naming conventions: Many teams distinguish private data members with a trailing underscore (
name_) or prefix (m_name) to immediately distinguish them from local function parameters. - Prefer modern member initialization: Supply default member values in the class definition so objects never hold uninitialized garbage memory.
- Separate interface from implementation: In production systems, declare classes in header files (
.hppor.h) and define substantial member functions in source files (.cpp).
Summary
- A class is a custom blueprint defining data attributes and allowable behaviors.
- An object is an individual instance of a class occupying memory on the stack or heap.
- Class members default to
privateaccess in C++;structmembers default topublic. - Member functions can be defined inline inside the class or externally using the
ClassName::FunctionNamesyntax. - Each object maintains its own independent set of data members.
- The implicit
thispointer points to the specific object instance executing a non-static member function.
What’s Next in the Series?
Notice in our Student and BankAccount examples that after creating an object, we had to manually call helper functions like s1.setInfo(...) or assign members one by one. If a programmer forgot to call those functions, the object remained uninitialized.
C++ provides a robust, automatic mechanism to guarantee that objects are initialized the moment they are created, and cleaned up when they go out of scope: constructors and destructors.
Continue Learning C++ OOP
- Next Article: C++ Constructors and Destructors Explained
- Topic Hub: Explore the complete roadmap on the C++ Object-Oriented Programming Topic Hub
- Related Fundamental: Pointers & Memory Addresses in C++