Encapsulation, access specifiers, constructors, destructors, copy/move semantics, object initialization, static members, const member functions, friends, unions, and the this pointer.
Encapsulation is the ability to logically group data and functions together. It is a core pillar of object-oriented programming — it keeps related state and behavior in one unit and controls external access to internals.
Abstraction empowers classes to decide which attributes should be visible outside. It hides implementation details and exposes only what the consumer needs to know. Another pillar of OOP.
class BankAccount {
private:
double balance; // hidden — nobody touches this directly
public:
void deposit(double amt); // exposed — the only way in
double getBalance();
};
| Specifier | Who can access |
|---|---|
private | Only the class itself (and friends) |
protected | The class itself and derived (child) classes |
public | Anyone |
class — members are private by defaultstruct — members are public by defaultunion — members are public by defaultclass and struct in C++ is this default access level.
A constructor is a special method invoked automatically when an object is instantiated. It has the same name as the class and no return type. Constructors can be overloaded.
class Student {
public:
Student(); // default constructor
Student(std::string n, int age); // overloaded
};
You can define a constructor (or any member function) outside the class body using the scope resolution operator :::
Student::Student() {
// definition
}
Student* s = new Student();
s->age = 6; // -> operator dereferences the pointer and accesses the member
delete s;
new) is not needed for simple value types. Prefer stack allocation unless you need the object to outlive the current scope or its size is dynamic.A default constructor is one that can be invoked without any arguments.
// Explicitly request the compiler-generated default (C++11)
Student() = default;
// Explicitly delete it to prevent default construction (C++11)
Student() = delete;
An initialization list follows the constructor signature with a colon : and initializes members before the constructor body runs. This is the preferred way to initialize members.
Student(std::string newName, int newAge)
: name(newName), age(newAge) // initialization list
{
// body runs after members are already initialized
}
const members, reference members, and members whose type has no default constructorconst and reference members must be initialized — they cannot be assigned after constructionMyClass(int x, int y) : _x(x), _y(y) {}
// members are initialized directly
MyClass(int x, int y) {
_x = x; // default-init then assign
_y = y; // two operations instead of one
}
Define variables as close to their first use as possible — and initialize them with their needed value immediately, skipping the default-construct-then-assign pattern:
// Not ideal — default constructed, then assigned
Point pt;
pt.x = 5;
pt.y = 5;
// Better — constructed directly with the right values
Point pt2(5, 5);
Where you declare a loop variable affects how many constructor/destructor calls occur:
// Option A: 1 ctor + 1 dtor + n assignments
Object obj;
for (...) { obj.x = 6; }
// Option B: n ctors + n dtors
for (...) { Object x(6); }
A constructor can delegate to another constructor in the same class via the initialization list, avoiding code duplication:
Student() : Student(" ", 10) {} // delegates to the 2-arg constructor
A destructor is automatically invoked when an object is destroyed. It has the same name as the class prefixed with a tilde ~, takes no arguments, and returns nothing.
~Student(); // no return, no arguments
Objects are destroyed when:
delete (heap objects)new), you must define a destructor that calls delete — otherwise every object destruction is a memory leak.The copy constructor is called when an object is initialized from another object of the same type. The compiler generates one automatically — but it only does a shallow copy.
A shallow copy copies the pointer value — both the original and the copy end up pointing to the same heap memory. Destroying either one leaves the other with a dangling pointer.
For classes that own heap memory, define your own copy constructor to allocate new memory and copy the data:
// Must be const ref — or the compiler won't accept it
Student(const Student& source);
// ↑ same name ↑ pass by ref (to avoid infinite recursion)
= on an existing object). You need to overload operator= separately to handle that case.When a function returns an object by value, the copy constructor would normally be called to transfer the data — but for temporary objects this is wasteful. A move constructor lets you "steal" the resource instead of copying it.
Student(Student&& moveSource);
// ↑ rvalue reference (&&) — binds to temporaries
Student myFunc();
Student newStudent(myFunc()); // move constructor invoked, not copy
If your class needs to define any one of these, it almost certainly needs to define all of them — because the need for one implies you're managing a resource that all of them must handle correctly.
| Rule of Three (pre-C++11) | Rule of Five (C++11+) |
|---|---|
| Destructor | Destructor |
| Copy constructor | Copy constructor |
| Copy assignment operator | Copy assignment operator |
| — | Move constructor |
| — | Move assignment operator |
= default and = delete to be explicit, and prefer smart pointers so you don't need to manage memory at all — reducing to the Rule of Zero.To prevent an object from being copied, declare the copy constructor and copy assignment operator as private (pre-C++11) or = delete (C++11, preferred):
class Unique {
Unique(const Unique&) = delete;
Unique& operator=(const Unique&) = delete;
};
A singleton permits exactly one instance of a class:
class Config {
public:
static Config& GetInstance() {
static Config instance; // created once, lives forever
return instance;
}
private:
Config() {}
Config(const Config&) = delete;
};
To prevent stack instantiation, make the destructor private. Only heap instances are then allowed by the compiler. Provide a public static destroy function to allow deletion:
static void Destroy(Student* instance) {
delete instance; // valid — we're inside the class, so private dtor is accessible
}
Static members belong to the class, not to any individual instance. They are shared across all objects of that type.
this pointer).class Student {
public:
static int count;
};
// Must be defined and initialized outside the class
int Student::count = 0;
A static object exists from the time it is constructed until the end of the program. There are two kinds:
| Kind | Where declared | Initialization |
|---|---|---|
| Local static | Inside a function | On the first call to that function |
| Non-local static | File scope, namespace, class | Before main() — order across translation units is undefined |
The fix: wrap non-local statics inside functions (making them local statics). They are guaranteed to be initialized the first time the function is called:
MyClass& getInstance() {
static MyClass instance; // initialized on first call, safe
return instance;
}
explicitA constructor that takes a single argument acts as an implicit conversion constructor — the compiler will use it to automatically convert from that argument type to the class type.
class Student {
public:
Student(int age); // single-argument constructor
};
void CheckStudent(Student s);
CheckStudent(10); // ALLOWED — 10 is implicitly converted to Student(10)
Student s = 30; // ALLOWED — implicit conversion constructor
explicitMark the constructor explicit to require that conversion to be written out:
explicit Student(int age);
CheckStudent(10); // ERROR — implicit conversion blocked
CheckStudent(Student(10)); // OK — explicit construction
explicit can also be applied to conversion operators. It's generally good practice to mark single-argument constructors explicit unless implicit conversion is specifically intended.mutableMember functions can be overloaded on const alone — a trailing const after the parameter list signals that the function will not modify any member variables:
{
public:
void myFunc();
void myFunc() const; // overloaded const version
}
const can lead to code duplication. One workaround: have the non-const version call the const version, though this may require a const_cast. Rarely recommended.constconst member functions are bitwise const — the bits of the object are unchanged — but not necessarily logically const (they might modify data through a pointer member).
Use the mutable keyword to allow a specific member to change inside a const function — useful for caches or lazy-computed values:
{
public:
void myFunc() const;
private:
int var1;
mutable int var2; // can be edited even inside myFunc() const
}
friendThe friend keyword grants an external function or class access to a class's private and protected members.
class Student {
private:
int grade;
friend void CalcArea(); // CalcArea can access Student's privates
friend class Teacher; // the entire Teacher class can access privates
};
friend is listed as one of C++'s "unsafe features" (Chapter 1). Use it sparingly — it breaks encapsulation. Prefer redesigning the interface before reaching for friend.unionA union is a special class type where all non-static data members share the same memory location. Only one member is "active" at any given time — writing to one member invalidates all others.
union Variant {
int i;
float f;
char c;
}; // sizeof(Variant) == sizeof(float) == 4
Variant v;
v.i = 42; // i is active
v.f = 3.14; // f is now active — reading v.i is undefined behavior
std::variant over raw unions — it is type-safe and tracks which member is currently active.Aggregate initialization lets you initialize an object's members with a brace-list, like an array:
Student newStudent = {"Peter", 28}; // name = "Peter", age = 28
A class is an aggregate only if it has all of:
constexpr ConstructorA constexpr constructor tells the compiler to evaluate the creation and instances of the class at compile time wherever possible.
constexpr Student(int age);
constinlineA class defined inside another class. The inner class has access to all members of the enclosing class, but does not have access to the enclosing class's this pointer (unless given a reference to an instance).
class Student {
int x;
public:
class Inner {
void foo() { x = 5; } // can access Student::x
};
};
Defining the inner class's methods outside requires two scope operators:
void Student::Inner::foo() { /* ... */ }
this PointerWithin the body of any non-static member function, an implicit this pointer is passed as a hidden parameter. It points to the object that is calling the member function.
class Student {
std::string name;
public:
Student& setName(std::string n) {
this->name = n; // disambiguates member from parameter
return *this; // enables method chaining
}
};
this pointer — they belong to the class, not to any instance, so there's no "current object" to point to.