Almost everything interesting in C++ — from std::string to the game engines and databases written in it — is built out of classes. If you understand how a class groups data with the functions that operate on it, the rest of object-oriented programming falls into place quickly.
This guide walks through C++ classes from the ground up: defining a class, controlling access to its members, writing constructors and destructors, and finishing with a real-world example that manages memory the way professional C++ code does. Every code example in this article compiles clean with g++ -std=c++17 -Wall -Wextra and runs exactly as shown.
Table of Contents
- What Is a Class in C++?
- Defining a Class: Syntax and a First Example
- Access Specifiers: public, private and protected
- Member Functions: Inside and Outside the Class
- Constructors in C++
- Destructors and RAII
- Static Data Members and Static Member Functions
- struct vs class in C++: The Only Real Difference
- Friend Functions and Operator Overloading
- A Real-World Example: Building a Simple Dynamic Array
- Key Takeaways
- Frequently Asked Questions
- Conclusion
What Is a Class in C++?
A class in C++ is a user-defined type that groups data (data members) together with the functions that operate on that data (member functions). A class acts as a blueprint: it describes what an object looks like and what it can do, and you create as many objects from it as you need.
The distinction between a class and an object trips up many beginners, so it is worth stating plainly:
- A class is the definition — the blueprint. It occupies no memory by itself.
- An object is a concrete instance of that class, created at runtime, with its own copy of the data members.
If BankAccount is a class, then your account and my account are two separate objects of that class. Both have a balance, but each holds its own value.
Grouping data and behavior into one type is called encapsulation, and it is the core idea behind object-oriented programming in C++. Instead of loose variables and free functions that anyone can misuse, a class exposes a small, controlled interface and hides everything else. If you are new to the language, our C++ programming tutorials cover the fundamentals this guide builds on.
Defining a Class: Syntax and a First Example
A class definition starts with the class keyword, followed by the class name, a body in curly braces, and a terminating semicolon — forgetting that final semicolon is one of the most common beginner compile errors.
#include <iostream>
#include <string>
class BankAccount {
public:
void deposit(double amount) {
balance += amount;
}
double getBalance() const {
return balance;
}
private:
std::string owner;
double balance = 0.0;
};
int main() {
BankAccount account; // create an object of the class
account.deposit(250.0);
std::cout << "Balance: " << account.getBalance() << '\n';
return 0;
}
Output:
Balance: 250
Three things to notice in this small example:
- Creating an object looks just like declaring a variable:
BankAccount account;. C++ objects are values by default — nonewkeyword required, unlike Java or C#. - Members are accessed with the dot operator:
account.deposit(250.0)calls thedepositmember function on that specific object. balance = 0.0is a default member initializer (available since C++11). Every newBankAccountstarts at zero instead of holding garbage — uninitialized members were a classic source of bugs in older C++ tutorials.
Variables, functions, and the dot operator are all covered in detail in The Basics of C++ Programming.
Access Specifiers: public, private and protected
Access specifiers decide who is allowed to touch each member of a class. C++ has exactly three:
| Access specifier | Who can access the member | Typical use |
|---|---|---|
public | Any code that can see the object | The interface: member functions others call |
private | Only member functions (and friends) of the class itself | Data members and internal helpers |
protected | The class itself plus classes derived from it | Members shared with subclasses in inheritance |
One rule surprises developers coming from other languages: in a C++ class, members are private by default. Anything you declare before the first access specifier is private.
Here is what access control buys you in practice:
#include <iostream>
class Thermostat {
public:
void setTarget(double celsius) {
if (celsius >= 5.0 && celsius <= 30.0) { // validation lives with the data
target = celsius;
}
}
double getTarget() const { return target; }
private:
double target = 20.0;
};
int main() {
Thermostat t;
t.setTarget(24.5); // OK: public member function
std::cout << t.getTarget() << '\n';
// t.target = 500.0; // Error: 'target' is private
return 0;
}
Output:
24.5
Uncomment the last line and the compiler stops you cold:
error: 'double Thermostat::target' is private within this context
Because target is private, the only way to change it is through setTarget(), which enforces a sane range. No code anywhere in the program can set the thermostat to 500 °C. That is encapsulation doing real work — the class defends its own invariants.
Member Functions: Inside and Outside the Class
A member function (often called a method) can be defined in two places. Short functions are usually defined directly inside the class body, as in the examples above; functions defined inside the class are implicitly inline. Longer functions are typically declared in the class and defined outside it using the scope resolution operator ::.
#include <iostream>
class Rectangle {
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const; // declared here...
private:
double width;
double height;
};
// ...defined here, using the scope resolution operator ::
double Rectangle::area() const {
return width * height;
}
int main() {
Rectangle r(4.0, 2.5);
std::cout << "Area: " << r.area() << '\n';
return 0;
}
Output:
Area: 10
This split matters in real projects: the class definition goes in a header file (rectangle.h), and the member function definitions go in a source file (rectangle.cpp). Client code includes the header and never needs to see the implementation.
Note the const after area(). A const member function promises not to modify the object, and the compiler enforces that promise. Mark every member function that only reads state as const — it documents intent, catches accidental writes, and lets the function be called on const objects and references. getBalance() and getTarget() earlier follow the same rule.
Constructors in C++
A constructor is a special member function that runs automatically when an object is created. It has the same name as the class and no return type, and its job is to put the object into a valid initial state. A class can have several constructors with different parameter lists — this is ordinary function overloading.
The four kinds you will meet constantly:
- Default constructor — takes no arguments.
- Parameterized constructor — takes arguments to initialize members.
- Copy constructor — builds a new object as a copy of an existing one.
- Move constructor — transfers resources from a temporary object instead of copying them (C++11 and later; demonstrated in the dynamic array example below).
#include <iostream>
#include <string>
class BankAccount {
public:
// Default constructor
BankAccount() : owner("Unassigned"), balance(0.0) {
std::cout << "Default constructor called\n";
}
// Parameterized constructor with a member initializer list
BankAccount(const std::string& name, double openingBalance)
: owner(name), balance(openingBalance) {
std::cout << "Parameterized constructor called for " << owner << '\n';
}
// Copy constructor
BankAccount(const BankAccount& other)
: owner(other.owner + " (copy)"), balance(other.balance) {
std::cout << "Copy constructor called\n";
}
void print() const {
std::cout << owner << ": " << balance << '\n';
}
private:
std::string owner;
double balance;
};
int main() {
BankAccount a; // default constructor
BankAccount b("Sara", 1500.0); // parameterized constructor
BankAccount c = b; // copy constructor
a.print();
b.print();
c.print();
return 0;
}
Output:
Default constructor called
Parameterized constructor called for Sara
Copy constructor called
Unassigned: 0
Sara: 1500
Sara (copy): 1500
The : owner(name), balance(openingBalance) syntax is a member initializer list, and it is the preferred way to initialize members. Members are initialized directly instead of being default-constructed and then assigned, and for reference members and const members it is the only way.
Two more facts worth knowing:
- If you write no constructors at all, the compiler generates a default constructor for you. The moment you write any constructor of your own, that freebie disappears — a very common source of “no matching function for call” errors.
- Mark single-argument constructors
explicitunless you specifically want implicit conversions. Without it,BankAccount acc = 500.0;could silently compile if aBankAccount(double)constructor existed.
Destructors and RAII
A destructor is the constructor’s mirror image: a member function named ~ClassName() that runs automatically when an object’s lifetime ends — when it goes out of scope, or when a dynamically allocated object is deleted. A class has exactly one destructor, and it takes no arguments.
Destructors power the single most important idiom in C++: RAII — Resource Acquisition Is Initialization. The constructor acquires a resource (a file, memory, a lock, a network socket); the destructor releases it. Because destructors run deterministically, the resource is cleaned up on every path out of a scope, including early returns and exceptions.
#include <cstdio>
#include <iostream>
class LogFile {
public:
explicit LogFile(const char* path) : file(std::fopen(path, "w")) {
if (file == nullptr) {
std::cout << "Could not open log file\n";
}
}
// Destructor: releases the resource automatically
~LogFile() {
if (file != nullptr) {
std::fclose(file);
std::cout << "Log file closed\n";
}
}
void write(const char* message) {
if (file != nullptr) {
std::fprintf(file, "%s\n", message);
}
}
private:
std::FILE* file;
};
int main() {
{
LogFile log("session.log");
log.write("Application started");
} // log goes out of scope here -- the destructor closes the file
std::cout << "Back in main\n";
return 0;
}
Output:
Log file closed
Back in main
Nobody called a close() function — the destructor did it the instant log left its scope. This is why idiomatic C++ has no finally blocks and rarely needs manual cleanup: std::vector, std::string, std::unique_ptr, and std::lock_guard are all RAII classes doing exactly what LogFile does here.
Static Data Members and Static Member Functions
A static data member belongs to the class itself rather than to any object — there is exactly one copy shared by every instance. A static member function can be called without an object and can only touch static members.
The classic use case is counting live instances:
#include <iostream>
class Sensor {
public:
Sensor() { ++count; }
~Sensor() { --count; }
static int activeCount() { // static member function
return count;
}
private:
static inline int count = 0; // static data member (C++17 inline variable)
};
int main() {
Sensor a, b;
std::cout << "Active sensors: " << Sensor::activeCount() << '\n';
{
Sensor c;
std::cout << "Active sensors: " << Sensor::activeCount() << '\n';
}
std::cout << "Active sensors: " << Sensor::activeCount() << '\n';
return 0;
}
Output:
Active sensors: 2
Active sensors: 3
Active sensors: 2
Notice the call syntax: Sensor::activeCount() uses the class name, not an object. Also note static inline int count = 0; — since C++17, inline lets you initialize the static member right in the class definition. Before C++17 you had to define it separately in a .cpp file (int Sensor::count = 0;), which is the pattern you will still see in older codebases.
struct vs class in C++: The Only Real Difference
In C++, struct and class both define classes, and the difference between them is smaller than most tutorials suggest.
The only language difference: members and base classes of a struct are public by default, while members and base classes of a class are private by default. Everything else — constructors, destructors, member functions, inheritance, templates — works identically in both.
| Feature | struct | class |
|---|---|---|
| Default member access | public | private |
| Default inheritance | public | private |
| Can have constructors and member functions | Yes | Yes |
| Can use access specifiers explicitly | Yes | Yes |
| Convention | Passive data bundles | Types with invariants and hidden state |
#include <iostream>
struct Point { // struct: members are public by default
double x;
double y;
};
class Circle { // class: members are private by default
public:
Circle(Point c, double r) : center(c), radius(r) {}
double area() const { return 3.14159265 * radius * radius; }
private:
Point center;
double radius;
};
int main() {
Point p{3.0, 4.0}; // aggregate initialization works: x and y are public
Circle c(p, 2.5);
std::cout << "Area: " << c.area() << '\n';
return 0;
}
Output:
Area: 19.635
The convention followed by the C++ Core Guidelines is simple: use struct when the members can vary independently (plain data, no rules to enforce), and use class when the type has an invariant to protect — like Circle, where a negative radius would be nonsense.
Friend Functions and Operator Overloading
Sometimes a function that is not a member genuinely needs access to a class’s private data. C++ handles this with the friend keyword: the class grants access to a specific external function (or an entire other class).
The most common real-world friend is the stream output operator, which pairs naturally with operator overloading — defining what operators like +, ==, or << mean for your own types:
#include <iostream>
class Money {
public:
Money(long dollars, int cents) : totalCents(dollars * 100 + cents) {}
// Operator overloading: add two Money objects with +
Money operator+(const Money& other) const {
return Money(0, static_cast<int>(totalCents + other.totalCents));
}
// Friend function: operator<< needs access to private data
friend std::ostream& operator<<(std::ostream& out, const Money& m);
private:
long totalCents;
};
std::ostream& operator<<(std::ostream& out, const Money& m) {
out << '$' << m.totalCents / 100 << '.'
<< (m.totalCents % 100 < 10 ? "0" : "") << m.totalCents % 100;
return out;
}
int main() {
Money price(19, 99);
Money shipping(4, 5);
std::cout << "Total: " << price + shipping << '\n';
return 0;
}
Output:
Total: $24.04
operator<< cannot be a member of Money (its left operand is the stream, not the Money object), so declaring it a friend is the standard idiom. Use friendship sparingly — every friend is a small hole in your encapsulation — but do not fear it where the design calls for it.
A Real-World Example: Building a Simple Dynamic Array
Everything above comes together when a class owns a resource. Here is a simplified version of what std::vector does internally: a class that manages a heap-allocated array, with a destructor, copy constructor, move constructor, and an overloaded [] operator.
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <utility>
class IntArray {
public:
// Parameterized constructor acquires the resource
explicit IntArray(std::size_t n) : size(n), data(new int[n]{}) {}
// Destructor releases it (RAII)
~IntArray() { delete[] data; }
// Copy constructor: deep copy
IntArray(const IntArray& other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
std::cout << "Deep copy of " << size << " elements\n";
}
// Move constructor: steal the buffer, no copying (C++11 and later)
IntArray(IntArray&& other) noexcept
: size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
std::cout << "Moved buffer, zero elements copied\n";
}
// Copy and move assignment via copy-and-swap
IntArray& operator=(IntArray other) noexcept {
std::swap(size, other.size);
std::swap(data, other.data);
return *this;
}
int& operator[](std::size_t i) { return data[i]; }
std::size_t length() const { return size; }
private:
std::size_t size;
int* data;
};
int main() {
IntArray a(1000);
a[0] = 42;
IntArray b = a; // copy constructor
IntArray c = std::move(a); // move constructor
std::cout << "b[0] = " << b[0] << ", c[0] = " << c[0]
<< ", c holds " << c.length() << " elements\n";
return 0;
}
Output:
Deep copy of 1000 elements
Moved buffer, zero elements copied
b[0] = 42, c[0] = 42, c holds 1000 elements
The copy constructor allocates a fresh buffer and copies all 1000 elements. The move constructor just takes the pointer from the temporary and leaves it empty — constant time regardless of size. This is why moves make modern C++ fast: returning large objects from functions or putting them into containers no longer means copying them.
Without the destructor, this class would leak memory; without the deep-copy constructor, two objects would delete the same buffer and crash. (This example was additionally verified with AddressSanitizer — no leaks, no double frees.)
Key Takeaways
- A class is a user-defined type combining data members and member functions; an object is a runtime instance of a class.
- Class members are private by default; struct members are public by default — that is the only language difference between the two keywords.
- Keep data members
privateand expose a smallpublicinterface so the class can protect its own invariants. - Constructors initialize objects (prefer member initializer lists); destructors clean up and enable RAII, the idiom behind
std::vector,std::unique_ptr, and safe resource handling generally. - Mark read-only member functions
constand single-argument constructorsexplicit. - Static members belong to the class, not to any object; friends and operator overloading let your types integrate naturally with the rest of C++.
- If a class manages a raw resource, it needs a destructor, copy operations, and move operations — or better, it should hold the resource in a standard RAII type and need none of them.
Frequently Asked Questions
Conclusion
The old way to learn C++ classes was to memorize syntax; the way that actually sticks is to see what each feature protects you from. Access specifiers stop other code from corrupting your state, constructors stop objects from existing half-initialized, and destructors stop resources from leaking. Once you read a class definition as a set of guarantees rather than a block of syntax, unfamiliar codebases — and the standard library itself — become far easier to navigate.
From here, the natural next step is what classes make possible: inheritance, virtual functions, and polymorphism, where one interface serves many implementations. You can explore those and related topics in our object-oriented programming tutorials — and if any example here behaves differently on your compiler, that is worth investigating: it usually means a different language standard is selected.


