C++ Classes and Objects
A class in C++ is a blueprint for creating your own data type: it bundles related data (called member variables) together with the functions that operate on that data (called member functions, or methods). An object is a concrete, independent instance created from that blueprint — each object gets its own copy of the data members, while all objects of the same class share the same member function code. Classes and objects are the foundation of object-oriented programming in C++, letting you model real things — a bank account, a student, a rectangle — as self-contained units that combine state and behavior in one place.
Overview: How Classes and Objects Work
A class definition by itself uses no memory at runtime. It is purely a compile-time description that tells the compiler: "objects of this type will have these data members, and these functions can operate on them." Memory is only allocated once you declare an actual object. When you write Rectangle rect;, the compiler reserves enough storage (on the stack, in this case) to hold exactly the data members of Rectangle — nothing more.
Every member of a class has an access specifier: public, private, or protected. Members are private by default in a class (this is the only real difference between class and struct in C++, since a struct defaults to public). Private members can only be accessed by code inside the class itself; public members can be accessed from anywhere the object is visible. This mechanism is called encapsulation: it lets a class hide its internal representation and expose only a controlled interface, so the internal details can change later without breaking code that uses the class.
Member functions are special because they can refer to a class’s data members directly, without any qualification. Under the hood, every non-static member function secretly receives a hidden pointer to the object it was called on, named this. Only one copy of a member function’s compiled code exists no matter how many objects you create; the this pointer is what lets that single copy of code know which object’s data to read and modify each time it runs.
A constructor is a special member function with the same name as the class and no return type. It runs automatically every time an object is created, and its job is usually to initialize the data members to sensible values. A class can have several constructors with different parameter lists (constructor overloading), and if you write no constructor at all, the compiler silently generates a default one that does nothing to the data members (leaving built-in types like int or double uninitialized). A destructor (named ~ClassName) runs automatically when an object is destroyed — when it goes out of scope, or is deleted if it was allocated with new — and is typically used to release any resources the object was holding.
Syntax
The general shape of a class definition and how you create an object from it:
class ClassName {
private:
// data members (variables) - hidden from outside code
dataType memberVariable;
public:
// constructor - same name as the class, no return type
ClassName(parameters) {
// initialize members here
}
// member function (method)
returnType methodName(parameters) {
// can use memberVariable directly
}
}; // the semicolon after the closing brace is required
int main() {
ClassName objectName; // create an object (calls the constructor)
objectName.methodName(arguments); // call a public method with the dot operator
return 0;
}
| Syntax element | Purpose |
|---|---|
class ClassName { ... }; |
Defines a new type named ClassName. Note the required semicolon after the closing brace. |
private: / public: / protected: |
Access specifiers. Everything listed after one applies until the next specifier or the end of the class. |
| Data members | Variables that hold an object’s state. Every object gets its own independent copy. |
| Constructor | Runs automatically when an object is created; usually initializes data members. |
| Member function | A function defined inside the class that can access the class’s data members directly. |
ClassName objectName; |
Declares an object (an instance of the class), which triggers a constructor call. |
objectName.member |
The dot operator, used to access an object’s public members from outside the class. |
Examples
Example 1: A simple Rectangle class
This example shows the most basic form of a class: public data members and public methods that use them.
#include <iostream>
using namespace std;
class Rectangle {
public:
double width;
double height;
double area() {
return width * height;
}
double perimeter() {
return 2 * (width + height);
}
};
int main() {
Rectangle rect;
rect.width = 5.0;
rect.height = 3.0;
cout << "Width: " << rect.width << endl;
cout << "Height: " << rect.height << endl;
cout << "Area: " << rect.area() << endl;
cout << "Perimeter: " << rect.perimeter() << endl;
return 0;
}
Output:
Width: 5
Height: 3
Area: 15
Perimeter: 16
Here, rect is an object of type Rectangle. Setting rect.width and rect.height stores values directly in that object’s own memory. Calling rect.area() runs the single shared copy of the area function, using rect‘s own width and height because of the hidden this pointer that points at rect.
Example 2: Encapsulation with a BankAccount class
This is a more realistic example: private data members that can only be changed through validated public methods, plus a constructor that guarantees every object starts in a valid state.
#include <iostream>
#include <string>
using namespace std;
class BankAccount {
private:
string owner;
double balance;
public:
BankAccount(string ownerName, double initialBalance) {
owner = ownerName;
balance = initialBalance;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}
void printStatement() {
cout << "Account holder: " << owner << endl;
cout << "Balance: $" << balance << endl;
}
};
int main() {
BankAccount account("Alice Smith", 100.0);
account.deposit(50.0);
bool success = account.withdraw(30.0);
cout << "Withdraw of $30 succeeded: " << (success ? "yes" : "no") << endl;
bool failed = account.withdraw(1000.0);
cout << "Withdraw of $1000 succeeded: " << (failed ? "yes" : "no") << endl;
account.printStatement();
return 0;
}
Output:
Withdraw of $30 succeeded: yes
Withdraw of $1000 succeeded: no
Account holder: Alice Smith
Balance: $120
Because owner and balance are private, no outside code can set balance directly to an invalid value — every change has to go through deposit or withdraw, which validate the amount first. The balance starts at 100, rises to 150 after the deposit, drops to 120 after the successful $30 withdrawal, and the $1000 withdrawal is rejected because it exceeds the current balance.
Example 3: Constructor overloading and the this pointer
A class can define more than one constructor. Here, Student has a default constructor and a parameterized one, and setGpa shows how this resolves a naming conflict between a parameter and a member variable.
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string name;
double gpa;
public:
Student() : name("Unknown"), gpa(0.0) {}
Student(string name, double gpa) : name(name), gpa(gpa) {}
void setGpa(double gpa) {
this->gpa = gpa;
}
void display() const {
cout << name << " - GPA: " << gpa << endl;
}
};
int main() {
Student s1;
Student s2("Maria Lopez", 3.8);
s1.setGpa(3.2);
s1.display();
s2.display();
return 0;
}
Output:
Unknown - GPA: 3.2
Maria Lopez - GPA: 3.8
s1 is built with the default constructor, which uses a member initializer list (the : name("Unknown"), gpa(0.0) part) to set its starting values. s2 is built with the parameterized constructor. Inside setGpa, the parameter is also named gpa, so this->gpa is required to refer to the object’s member instead of the parameter.
How It Works Step by Step (Under the Hood)
Using Example 1 as a trace: Rectangle rect; compiles down to roughly these steps.
- At compile time, the compiler records that a
Rectangleconsists of twodoublevalues laid out consecutively in memory (typically 16 bytes total on common platforms), and thatarea()andperimeter()exist as functions that expect aRectangle*. - At the line
Rectangle rect;, the program reserves 16 bytes of stack space forrectand runs the (compiler-generated, since none was written) default constructor. rect.width = 5.0;writes directly into the first 8 bytes of that block;rect.height = 3.0;writes into the second 8 bytes.rect.area()is translated internally into something equivalent to callingarea(&rect): the address ofrectis passed as the hiddenthispointer, so insidearea,widthreally meansthis->width.- Only one copy of the machine code for
area()andperimeter()exists in the whole program, no matter how manyRectangleobjects you create; only the data (the two doubles) is duplicated per object. - When
mainreturns,rectgoes out of scope and its stack memory is reclaimed (its destructor, here compiler-generated and empty, runs first).
Access specifiers like private add no runtime cost at all — there is no extra memory, no hidden flag, no runtime check. The rule is enforced purely by the compiler while it reads your source code: if it sees code outside the class trying to name a private member, it refuses to generate the program and reports a compile error.
Common Mistakes
Mistake 1: Forgetting the semicolon after the class definition
Unlike a function definition, a class definition must end with a semicolon. Leaving it off produces a confusing compiler error because the compiler treats what follows as part of the same statement.
class Point {
public:
int x;
int y;
}
int main() {
Point p;
p.x = 1;
p.y = 2;
return 0;
}
This fails to compile with an error near int main(), because the compiler is still expecting a semicolon to close the Point declaration. The fix is simply to add the semicolon after the closing brace:
#include <iostream>
using namespace std;
class Point {
public:
int x;
int y;
};
int main() {
Point p;
p.x = 1;
p.y = 2;
cout << "x = " << p.x << ", y = " << p.y << endl;
return 0;
}
Output:
x = 1, y = 2
Mistake 2: Trying to access private members from outside the class
Beginners often forget that class members are private by default, and try to set them directly from outside code.
class Point {
private:
int x;
int y;
};
int main() {
Point p;
p.x = 5;
return 0;
}
This fails to compile with an error such as "x is a private member of Point". The fix is to provide a public method that is allowed to change the private data, keeping the class in control of its own state:
#include <iostream>
using namespace std;
class Point {
private:
int x;
int y;
public:
void setCoordinates(int newX, int newY) {
x = newX;
y = newY;
}
void display() const {
cout << "x = " << x << ", y = " << y << endl;
}
};
int main() {
Point p;
p.setCoordinates(5, 10);
p.display();
return 0;
}
Output:
x = 5, y = 10
Best Practices
- Keep data members
privateand expose behavior throughpublicmethods; this is encapsulation, and it lets you change the internal representation later without breaking other code. - Prefer initializing data members in a constructor’s member initializer list (
: name(n), gpa(g)) rather than assigning them in the constructor body; this avoids default-constructing a member and then immediately overwriting it. - Mark member functions that don’t modify the object’s state as
const(as withdisplay() constabove); this documents intent and lets the function be called onconstobjects and references. - Give every constructor a way to leave the object fully initialized; never rely on data members happening to contain zero.
- Keep each class focused on one responsibility; if a class is doing too many unrelated things, it’s usually a sign it should be split into smaller classes.
- Reserve public data members for simple, passive data-holding types where no invariants need protecting; anything with rules to enforce should hide its data behind methods.
Practice Exercises
- Write a
Circleclass with a privatedouble radius, a constructor that takes the radius, and public methodsarea()andcircumference()that use3.14159for pi. Create one object with radius4and print both results. - Write a
Carclass with private members for brand, model, and year, a constructor to set all three, and adisplay()method. Create three differentCarobjects and calldisplay()on each. - Extend the
BankAccountclass from Example 2 with a methodbool transferTo(BankAccount& other, double amount)that withdrawsamountfrom the current account and, only if that withdrawal succeeds, deposits it intoother. Test it by transferring money between two accounts and printing both statements afterward.
Summary
- A class is a blueprint for a custom type; an object is an instance of that blueprint with its own copy of the data members.
- Access specifiers (
public,private,protected) control which code can see a member; class members areprivateby default. - Member functions can access data members directly because every call implicitly passes a hidden
thispointer to the calling object. - Constructors run automatically on object creation to set up initial state; destructors run automatically on destruction to clean up.
- A class definition itself takes no memory; memory is allocated per object, while member function code is shared by all objects of that class.
- Forgetting the semicolon after a class body and accessing private members from outside the class are two of the most common beginner compile errors.
