Classes, Objects, Constructors, and Destructors: How C++ Expresses Invariants
A C struct aggregates data fields.
A C++ class pushes further by binding data and operations together, establishing invariants via constructors, releasing resources via destructors, and hiding internal state via access control.
You do not learn classes merely to shove functions inside structs; you learn them to ensure an object remains unassailably correct from creation to destruction.
Class Definition
class Counter {
public:
void increment() {
++value_;
}
int value() const {
return value_;
}
private:
int value_ = 0;
};
The public section forms the external interface.
The private section constitutes the internal implementation.
External code absolutely cannot modify value_ directly.
This mechanism empowers the class to violently protect its own invariants.
Objects Are Class Instances
Counter counter;
counter.increment();
std::cout << counter.value() << '\n';
counter is an object.
Every object maintains its own independent member variables.
Member functions access the current object via an implicit this pointer.
void increment() {
++this->value_;
}
You rarely need to write this explicitly, but acknowledging its existence is crucial for understanding how member functions operate under the hood.
Constructors Establish Initial State
A constructor executes precisely when the object is created. It has no return type, and its name exactly matches the class name.
class Buffer {
public:
explicit Buffer(std::size_t size)
: size_(size), data_(new char[size]) {}
private:
std::size_t size_;
char* data_;
};
The member initializer list initializes members before the function body executes. Resource members should almost exclusively be established within this initialization list.
explicit Prevents Accidental Implicit Conversions
class Port {
public:
explicit Port(int value) : value_(value) {}
private:
int value_;
};
Without explicit, specific single-argument constructors can be hijacked by the compiler for implicit conversions.
This renders function invocations dangerously opaque.
In engineering code, single-argument constructors must default to being explicit.
Destructors Release Resources
A destructor executes precisely when the object is destroyed.
Its name is ~ClassName.
class Buffer {
public:
explicit Buffer(std::size_t size)
: size_(size), data_(new char[size]) {}
~Buffer() {
delete[] data_;
}
private:
std::size_t size_;
char* data_;
};
This snippet demonstrates the raw mechanics of destruction.
Modern C++ fiercely advocates using std::vector<char> or std::unique_ptr<char[]> to manage memory, drastically reducing the need for hand-written delete statements.
The Foundational Shape of RAII
class File {
public:
explicit File(const char* path) {
file_ = std::fopen(path, "rb");
if (file_ == nullptr) {
throw std::runtime_error("open failed");
}
}
~File() {
if (file_ != nullptr) {
std::fclose(file_);
}
}
private:
std::FILE* file_ = nullptr;
};
If object construction succeeds, the resource is guaranteed valid. When the object destructs, the resource is guaranteed released. This pattern makes error paths exponentially more reliable.
const in Member Functions
int value() const {
return value_;
}
The const qualifier trailing a member function declares that the function will not mutate the observable state of the current object.
This permits a const Counter& to invoke it legally.
void print(const Counter& counter) {
std::cout << counter.value() << '\n';
}
Read-only operations must always be marked const.
This forces interface boundaries to remain crisp and clear.
Encapsulation Is Not Just Hiding Fields
The sole purpose of encapsulation is to protect invariants.
class Percent {
public:
explicit Percent(int value) {
if (value < 0 || value > 100) {
throw std::out_of_range("percent");
}
value_ = value;
}
int value() const {
return value_;
}
private:
int value_ = 0;
};
If the field were public, external code could rewrite it to -100 at any moment.
Private fields coupled with constructor validation mathematically guarantee the object remains perpetually valid.
struct vs. class
In C++, struct and class are nearly identical.
Their primary difference lies in default access control:
structdefaults topublic.classdefaults toprivate.
Engineering convention:
- Use
structfor simple, dumb data aggregation. - Use
classfor objects harboring invariants and behavioral logic.
struct Point {
int x;
int y;
};
Point is merely data.
Percent enforces a legal mathematical range; therefore, it requires a class.
Copy Construction and Assignment
Counter a;
Counter b = a;
If a class lacks special resources, the compiler-generated default copy operations are usually adequate. If a class owns raw resources, a default shallow copy will trigger a catastrophe.
class Buffer {
char* data_;
};
Copying this object merely duplicates the pointer value. Two independent objects will subsequently attempt to free the exact same memory block (double-free). Subsequent articles on RAII will dissect the Rule of Three/Five/Zero to address this.
Object Destruction Order
When local objects exit their scope, they are destructed in the exact reverse order of their creation.
{
File a("a.txt");
File b("b.txt");
}
b is destructed first, followed by a.
Member variables are constructed in the order they are declared in the class definition, and destructed in reverse.
Declaration order can and should express explicit resource dependencies.
Static Members
Static members belong strictly to the class itself, not to any individual instantiated object.
class IdGenerator {
public:
static int next();
};
Static members are suited for stateless utility functions or isolated shared state. If they encapsulate mutable shared state, rigorous concurrency auditing is mandatory. Never treat static members as merely a syntactically different way to write global variables.
Use friend Sparingly
The friend keyword permits external functions or classes to bypass access control and manipulate private members.
It fundamentally expands the encapsulation boundary.
class Value {
friend bool equal(const Value&, const Value&);
};
Friends are appropriate for closely coupled operations like custom operators or highly specific test injections.
Abusing them renders the private keyword entirely meaningless.
Engineering Risks
Common risks associated with classes and objects:
- Constructors failing to establish comprehensive invariants.
- Destructors throwing exceptions (which violently terminates the program).
- Classes owning raw resources defaulting to shallow copies.
- Mismatched member initialization order versus declaration order.
- Trivial getter/setter pairs utterly defeating the purpose of encapsulation.
- Mutable static members inciting catastrophic data races.
- Missing
constqualifiers rendering member functions unusable in read-only contexts. - Single-argument constructors lacking the
explicitkeyword, spawning invisible conversions.
These architectural fractures will become dramatically more pronounced as we dive deeper into the C++ object model.
Summary
The supreme value of a class is articulating and defending an object invariant. Constructors ensure the object is unequivocally legal from the millisecond it exists. Destructors ensure resources are flawlessly released the millisecond the object leaves scope. Access control fiercely guards the internal state against tampering. C++ engineering begins right here: by transmuting fragile human discipline into ironclad type and lifecycle rules.