OOP Relationships
"Has-a" comes in two flavors — the difference is entirely about who owns the lifetime.
Beginner
| Pillar | What it actually means |
|---|---|
| Abstraction | you define what an object should be — a Person class, not one class per individual person — and instantiate specific objects from that definition |
| Encapsulation | bundle an object's data with the methods that operate on it, and keep the data private unless another class genuinely needs it |
| Inheritance | reuse an existing class's attributes and behavior by deriving a new class from it — see Inheritance & Polymorphism |
| Polymorphism | the same interface behaves differently depending on the actual object — see Inheritance & Polymorphism |
| Relationship | Lifetime | Example | |
|---|---|---|---|
| Composition | "has-a" (strong) | the contained object is destroyed with the container | a Car holds an Engine by value, or allocates and deletes it itself |
| Aggregation | "has-a" (weak) / "uses-a" | the contained object outlives the container — the container just borrows it | a Department holds a pointer to a Teacher created (and destroyed) elsewhere |
| Association | general term covering both | — | any relationship where one class refers to another |
class Engine { public: Engine(int id) { std::cout << "Engine ctor\n"; } ~Engine() { std::cout << "Engine dtor\n"; } };
class Car { // COMPOSITION — Car owns its Engine outright
Engine engine; // held by value: constructed with Car, destroyed with Car
public:
Car(int id) : engine(id) {}
};
{
Car c(1); // prints "Engine ctor"
} // c goes out of scope — prints "Engine dtor" automatically
class Teacher { public: Teacher(std::string n) : name(n) {} std::string name; };
class Department { // AGGREGATION — Department borrows a Teacher it doesn't own
Teacher *teacher; // just a pointer to something created elsewhere
public:
Department(Teacher *t) : teacher(t) {}
~Department() { /* deliberately does NOT delete teacher */ }
};
Teacher *t = new Teacher("Dr. Lee"); // created outside Department's control
{
Department d(t); // d borrows t
} // d is destroyed — t is untouched, still valid
delete t; // whoever created t is responsible for destroying it
Bus/Engine example). What determines aggregation vs. composition is
strictly who calls delete — or, equivalently today, which object holds the
owning smart pointer.
friend declaration grants a specific function (or another class) access
to a class's private members, bypassing encapsulation on purpose for that one relationship.
It's a deliberate escape hatch, not a workaround to reach for by default — per Scott
Meyers, most binary operators don't actually need it (see
Operator Overloading), since they can be implemented
purely in terms of the class's already-public interface.
class Matrix {
double data[9];
friend class MatrixInverter; // MatrixInverter can now touch Matrix's private data directly
};
static data member is shared by every instance of the class —
there's exactly one copy, not one per object, which is why it can't be initialized in a
constructor and needs a definition outside the class. A static member function
has no this pointer at all: it can only touch other static members, and it's
callable through the class name without any instance existing.
class Widget {
static int totalCount; // declaration only
public:
Widget() { totalCount++; }
static int count() { return totalCount; } // no 'this' — can't touch non-static members
};
int Widget::totalCount = 0; // definition — exactly one copy, lives outside any single Widget
Widget a, b, c;
Widget::count(); // 3 — called via the class name, the recommended style
a.count(); // also legal, but calling it via an instance is discouraged
static members aren't counted in
sizeof(Widget), since they don't live inside any individual instance — only
non-static data members and (if present) the vptr contribute to an object's size. See
C++ Best Practices for the full struct-padding/sizeof
breakdown.
int a = 0;
{
a = 1;
int a; // a NEW variable named a — shadows the outer one starting here
a = 2; // sets the inner a, not the outer one
std::cout << a; // 2
}
std::cout << a; // 1 — the outer a was set to 1 before the inner a was declared, and never touched again