Class Relationships¶
Class relationships describe how classes interact and depend on each other. They represent real-world connections between objects and define the structure of your program. They are how you model the real world and, more importantly, how you create code that is reusable, maintainable, and flexible.
“uses-a”
Independent objects
“has-a”
Weak ownership
“part-of”
Strong ownership
“is-a”
Code reuse
Association¶
Definition: Association is a loose relationship where objects exist independently. One object uses or interacts with another, but neither owns the other. If one object is destroyed, the other can continue existing.
Key Characteristics:
✓ Objects exist independently
✓ Neither object owns the other
✓ Relationship is typically “uses-a”
✓ Both objects can survive independently
Real-World Example
A Teacher and a Student have an association. If the teacher leaves, students still exist. If a student graduates, the teacher remains.
UML Representation¶
In UML diagrams, association is shown with a simple line connecting two classes. Multiplicity can be indicated at each end:
Notation |
Meaning |
|---|---|
|
exactly n |
|
zero or one |
|
zero or more |
|
one or more |
Teacher ─────────── Student
1..* 1..*
Teacher ───────────> Student
1..* 1..*
teaches
Teacher ───────────> Student
1..* 1..*
Aggregation¶
Definition: Aggregation is a type of association and represents a “has-a” relationship where the container has a weak ownership of the contained objects. The contained objects can exist independently of the container. When the container is destroyed, the contained objects continue to exist.
◇ Represents a “has-a” relationship
◇ Weak ownership (hollow diamond in UML)
◇ Contained objects can exist independently
◇ Container destruction doesn’t destroy contained objects
A Fleet “contains” Vehicles. If the fleet is dissolved, the vehicles still exist and can be transferred to another fleet or operate independently.
Code Example¶
class Vehicle {
private:
std::string id_;
public:
Vehicle(const std::string& id) : id_{id} {}
// Vehicle can exist independently
};
class Fleet {
private:
std::vector<Vehicle*> vehicles_; // Non-owning pointers
public:
void add_vehicle(Vehicle* v) {
vehicles_.push_back(v);
}
// Vehicles are NOT destroyed when Fleet is destroyed
};
Important
Notice the use of raw pointers (Vehicle*) - the Fleet doesn’t own the vehicles, it just references them.
Composition¶
Definition: Composition is a strong “has-a” relationship with exclusive ownership. The contained object is an integral part of the container and cannot exist independently. When the container is destroyed, all its parts are destroyed as well.
◆ Strong ownership (filled diamond in UML)
◆ Contained objects cannot exist independently
◆ Container destruction destroys all parts
◆ Represents “part-of” relationship
A Vehicle has Sensors. The sensors are integral parts of the vehicle. If the vehicle is destroyed (scrapped), its sensors are destroyed with it.
Code Example¶
class Sensor {
private:
std::string type_;
double reading_{0.0};
public:
Sensor(const std::string& type) : type_{type} {}
};
class Vehicle {
private:
std::vector<Sensor> sensors_; // Owns sensors by value
public:
Vehicle() {
sensors_.emplace_back("lidar");
sensors_.emplace_back("radar");
}
// Sensors are automatically destroyed with Vehicle
};
Tip
Notice the sensors are stored by value - the Vehicle owns them completely. When the Vehicle is destroyed, the sensors go with it.
Comparison: Aggregation vs Composition¶
std::vector<Vehicle*> vehicles_;
// Non-owning pointers
Hollow diamond in UML
✓ Parts survive independently
std::vector<Sensor> sensors_;
// Owned by value
Filled diamond in UML
✗ Parts destroyed with container
Inheritance¶
Definition: Inheritance represents an “is-a” relationship where a derived class inherits attributes and behaviors from a base class. The derived class specializes or extends the base class, providing specific implementations while maintaining the common interface.
▲ Represents an “is-a” relationship
▲ Derived class inherits from base class
▲ Enables code reuse and polymorphism
▲ Supports specialization and extension
RoboTaxi “is-a” Vehicle. Taxi “is-a” Vehicle. Both inherit common vehicle behavior but add their own specific features.
Types of Inheritance¶
A class inherits from exactly one base class.
class Animal {
protected:
std::string name_;
int age_;
public:
Animal(const std::string& name, int age)
: name_{name}, age_{age} {}
};
class Bird : public Animal {
private:
double wingspan_;
public:
Bird(const std::string& name, int age, double wingspan)
: Animal(name, age), wingspan_{wingspan} {}
};
class Elephant : public Animal {
private:
double trunk_length_;
public:
Elephant(const std::string& name, int age, double trunk_length)
: Animal(name, age), trunk_length_{trunk_length} {}
};
Note
BirdandElephantinheritAnimal’spublicandprotectedmembersUML diagrams typically don’t show inherited members
Multiple inheritance occurs when a class inherits from more than one base class. The derived class gains access to all public and protected members from all parent classes.
class Animal {
protected:
std::string species_;
public:
Animal(const std::string& species) : species_{species} {}
};
class Human {
protected:
std::string language_;
public:
Human(const std::string& language) : language_{language} {}
};
class MythicalCreature : public Animal, public Human {
public:
MythicalCreature(const std::string& species,
const std::string& language)
: Animal(species), Human(language) {}
};
Important
We focus exclusively on single inheritance in this course. For assignments and projects, you are welcome to use any inheritance approach.
Generalization and Specialization¶
Bottom-up approach which should be used every time classes have specific differences and common similarities.
Process:
Identify common attributes and methods across multiple classes
Extract commonalities into a base class
Keep differences in specialized subclasses
Top-down approach which creates new classes from an existing class.
Process:
Start with a general base class
Create derived classes for specific variants
Add specialized attributes and methods to derived classes
UML Representation¶
The protected specifier is denoted with a # symbol in UML diagrams.
Inheritance Access Types¶
The access specifier used during inheritance determines how base class members are accessible in the derived class:
Base Class Member |
public inheritance |
protected inheritance |
private inheritance |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
not accessible (hidden) |
not accessible (hidden) |
not accessible (hidden) |
Note
privatemembers are inherited but hidden from derived classesDefault inheritance in C++ is
privateAlways explicitly specify
publicinheritance for is-a relationships
Best Practice Example:
class Base {
public:
void public_method();
protected:
void protected_method();
private:
void private_method();
};
// Use public inheritance for is-a relationships
class Derived : public Base {
// public_method() is public
// protected_method() is protected
// private_method() is not accessible
};
Constructors in Inheritance¶
Critical Rule
The constructors of a class must address the attributes specific to that class.
Problem: How do we initialize base class attributes when constructing a derived object?
Wrong Approach #1: Initialize base class members in derived class initializer list
class Base {
protected:
int base_member_;
public:
Base(int base_value = 50) : base_member_{base_value} {}
};
class Derived : public Base {
private:
double derived_member_;
public:
// ERROR: Cannot initialize inherited members
Derived(double derived_value, int base_value)
: derived_member_{derived_value}, base_member_{base_value} {}
};
Wrong Approach #2: Assign base class members in constructor body
class Derived : public Base {
private:
double derived_member_;
public:
Derived(double derived_value, int base_value)
: derived_member_{derived_value} {
base_member_ = base_value; // Works, but not ideal
}
};
Warning
This approach works but is performed in two steps and will not work if the attribute is a const or a reference.
Correct Approach: Explicitly call the base class constructor
class Base {
protected:
int base_member_;
public:
Base(int base_value = 50) : base_member_{base_value} {}
};
class Derived : public Base {
private:
double derived_member_;
public:
Derived(double derived_value, int base_value)
: Base(base_value), // Call base constructor FIRST
derived_member_{derived_value} {
// Empty body
}
};
int main() {
Derived derived(20.5, 10);
// Execution order:
// 1. Base(10) is called -> base_member_ = 10
// 2. Derived(20.5, 10) is called -> derived_member_ = 20.5
// 3. Control returns to main()
}
✅ Best Practice
Add parameters for base class attributes in the derived class constructor
Explicitly call the base class constructor in the member initializer list
Each constructor worries only about its own attributes
Key Takeaways¶
“uses-a”
Independent objects that interact
“has-a”
Weak ownership, parts can survive independently
“has-a”
Strong ownership, parts destroyed with whole
“is-a”
Derived class extends base class
Additional Guidelines:
Always use
publicinheritance for is-a relationshipsCall base class constructors explicitly in derived class initializer lists
Focus on single inheritance for clarity and maintainability