There are many different kinds of relationships two objects may have in real-life, and we use specific “relation type” words to describe these relationships. For example:
a square “is-a” shape(Inheritance).
A car “has-a” steering wheel(Aggregation).
A computer programmer “uses-a” keyboard(Association).
A flower “depends-on” a bee for pollination(Dependencies).
A student is a “member-of” a class(Container).
your brain exists as “part-of” you(Composition).
All of these relation types have useful analogies in C++:
1 Inheritance(Is a)
Inheritance allows us to model an is-a relationship between two objects. The object being inherited from is called the parent class, base class, or superclass. The object doing the inheriting is called the child class, derived class, or subclass.
When a derived class inherits from a base class, the derived class acquires all of the members of the base class.
Derived classes can add new functions, change the way functions that exist in the base class work in the derived class, change an inherited member’s access level, or hide functionality.
#include <iostream>
#include <string>
class Fruit
{
private:
std::string m_name;
std::string m_color;
public:
Fruit(const std::string& name, const std::string& color)
: m_name{ name }, m_color{ color }
{
}
const std::string& getName() const { return m_name; }
const std::string& getColor() const { return m_color; }
};
class Apple: public Fruit
{
// The previous constructor we used for Apple had a fixed name ("apple").
// We need a new constructor for GrannySmith to use to set the name of the fruit
protected: // protected so only derived classes can access
Apple(const std::string& name, const std::string& color)
: Fruit{ name, color }
{
}
public:
Apple(const std::string& color="red")
: Fruit{ "apple", color }
{
}
};
class Banana : public Fruit
{
public:
Banana()
: Fruit{ "banana", "yellow" }
{
}
};
class GrannySmith : public Apple
{
public:
GrannySmith()
: Apple{ "granny smith apple", "green" }
{
}
};
int main()
{
Apple a{ "red" };
Banana b;
GrannySmith c;
std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n";
return 0;
}
2 Composition(Part-of)
They are typically created as structs or classes with normal data members(primitive or composition types, such as struct or class). Because these data members exist directly as part of the struct/class, their lifetimes are bound to that of the class instance itself.
In a composition, we typically add our parts to the composition using normal member variables (or pointers where the allocation and deallocation process is handled by the composition class).
#include <iostream>
#include <string>
class Point2D
{
private:
int m_x;
int m_y;
public:
// A default constructor
Point2D()
: m_x(0), m_y(0)
{
}
// A specific constructor
Point2D(int x, int y)
: m_x(x), m_y(y)
{
}
// An overloaded output operator
friend std::ostream& operator<<(std::ostream& out, const Point2D &point)
{
out << "(" << point.m_x << ", " << point.m_y << ")";
return out;
}
// Access functions
void setPoint(int x, int y)
{
m_x = x;
m_y = y;
}
};
class Creature
{
private:
std::string m_name;
Point2D m_location;
public:
Creature(const std::string &name, const Point2D &location)
: m_name(name), m_location(location)
{
}
friend std::ostream& operator<<(std::ostream& out, const Creature &creature)
{
out << creature.m_name << " is at " << creature.m_location;
return out;
}
void moveTo(int x, int y)
{
m_location.setPoint(x, y);
}
};
int main()
{
std::cout << "Enter a name for your creature: ";
std::string name;
std::cin >> name;
Creature creature(name, Point2D(4, 7));
while (1)
{
// print the creature's name and location
std::cout << creature << '\n';
std::cout << "Enter new X location for creature (-1 to quit): ";
int x=0;
std::cin >> x;
if (x == -1)
break;
std::cout << "Enter new Y location for creature (-1 to quit): ";
int y=0;
std::cin >> y;
if (y == -1)
break;
creature.moveTo(x, y);
}
return 0;
}
3 Aggregation Has-a
In an aggregation, we also add parts as member variables. However, these member variables are typically either references or pointers that are used to point at objects that have been created outside the scope of the class.
#include <iostream>
#include <string>
#include <vector>
class Teacher
{
private:
std::string m_name;
public:
Teacher(std::string name)
: m_name(name)
{
}
std::string getName() { return m_name; }
};
class Department
{
private:
std::vector<Teacher*> m_teacher;
public:
Department()
{
}
void add(Teacher *teacher)
{
m_teacher.push_back(teacher);
}
friend std::ostream& operator <<(std::ostream &out, const Department &dept)
{
out << "Department: ";
for (const auto &element : dept.m_teacher)
out << element->getName() << ' ';
out << '\n';
return out;
}
};
int main()
{
// Create a teacher outside the scope of the Department
Teacher *t1 = new Teacher("Bob"); // create a teacher
Teacher *t2 = new Teacher("Frank");
Teacher *t3 = new Teacher("Beth");
{
// Create a department and add some Teachers to it
Department dept; // create an empty Department
dept.add(t1);
dept.add(t2);
dept.add(t3);
std::cout << dept;
} // dept goes out of scope here and is destroyed
std::cout << t1->getName() << " still exists!\n";
std::cout << t2->getName() << " still exists!\n";
std::cout << t3->getName() << " still exists!\n";
delete t1;
delete t2;
delete t3;
return 0;
}
Summarizing composition and aggregation
Compositions:
- Typically use normal member variables
- Can use pointer members if the class handles object allocation/deallocation itself
- Responsible for creation/destruction of parts
Aggregations:
- Typically use pointer or reference members that point to or reference objects that live outside the scope of the aggregate class
- Not responsible for creating/destroying parts
4 Association(Uses-a)
Because associations are a broad type of relationship, they can be implemented in many different ways. However, most often, associations are implemented using pointers, where the object points at the associated object.
#include <iostream>
#include <string>
#include <vector>
// Since Doctor and Patient have a circular dependency, we're going to forward declare Patient
class Patient;
class Doctor
{
private:
std::string m_name{};
std::vector<Patient *> m_patient{};
public:
Doctor(std::string name) :
m_name(name)
{
}
void addPatient(Patient *pat);
// We'll implement this function below Patient since we need Patient to be defined at that point
friend std::ostream& operator<<(std::ostream &out, const Doctor &doc);
std::string getName() const { return m_name; }
};
class Patient
{
private:
std::string m_name{};
std::vector<Doctor *> m_doctor{}; // so that we can use it here
// We're going to make addDoctor private because we don't want the public to use it.
// They should use Doctor::addPatient() instead, which is publicly exposed
void addDoctor(Doctor *doc)
{
m_doctor.push_back(doc);
}
public:
Patient(std::string name)
: m_name(name)
{
}
// We'll implement this function below Doctor since we need Doctor to be defined at that point
friend std::ostream& operator<<(std::ostream &out, const Patient &pat);
std::string getName() const { return m_name; }
// We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor()
friend void Doctor::addPatient(Patient *pat);
};
void Doctor::addPatient(Patient *pat)
{
// Our doctor will add this patient
m_patient.push_back(pat);
// and the patient will also add this doctor
pat->addDoctor(this);
}
std::ostream& operator<<(std::ostream &out, const Doctor &doc)
{
unsigned int length = doc.m_patient.size();
if (length == 0)
{
out << doc.m_name << " has no patients right now";
return out;
}
out << doc.m_name << " is seeing patients: ";
for (unsigned int count = 0; count < length; ++count)
out << doc.m_patient[count]->getName() << ' ';
return out;
}
std::ostream& operator<<(std::ostream &out, const Patient &pat)
{
unsigned int length = pat.m_doctor.size();
if (length == 0)
{
out << pat.getName() << " has no doctors right now";
return out;
}
out << pat.m_name << " is seeing doctors: ";
for (unsigned int count = 0; count < length; ++count)
out << pat.m_doctor[count]->getName() << ' ';
return out;
}
int main()
{
// Create a Patient outside the scope of the Doctor
Patient *p1 = new Patient("Dave");
Patient *p2 = new Patient("Frank");
Patient *p3 = new Patient("Betsy");
Doctor *d1 = new Doctor("James");
Doctor *d2 = new Doctor("Scott");
d1->addPatient(p1);
d2->addPatient(p1);
d2->addPatient(p3);
std::cout << *d1 << '\n';
std::cout << *d2 << '\n';
std::cout << *p1 << '\n';
std::cout << *p2 << '\n';
std::cout << *p3 << '\n';
delete p1;
delete p2;
delete p3;
delete d1;
delete d2;
return 0;
}
5 Dependency(Depends on)
A dependency occurs when one object invokes another object’s functionality in order to accomplish some specific task. This is a weaker relationship than an association, but still, any change to object being depended upon may break functionality in the (dependent) caller. A dependency is always a unidirectional relationship.
In a dependency, one class uses another class to perform a task. The dependent class typically is not a member of the class using it, but rather is temporarily created, used, and then destroyed, or passed into a member function from an external source.
A good example of a dependency that you’ve already seen many times is std::cout (of type std::ostream). Our classes that use std::cout use it in order to accomplish the task of printing something to the console, but not otherwise.
#include <iostream>
class Point
{
private:
double m_x, m_y, m_z;
public:
Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
{
}
friend std::ostream& operator<< (std::ostream &out, const Point &point);
};
std::ostream& operator<< (std::ostream &out, const Point &point)
{
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
return out;
}
int main()
{
Point point1(2.0, 3.0, 4.0);
std::cout << point1;
return 0;
}
6 Container(Member of)
a container class is a class designed to hold and organize multiple instances of another type (either another class, or a fundamental type).
#include <iostream>
#include <cassert>
class IntArray
{
private:
int m_length{};
int *m_data{};
public:
IntArray() = default;
IntArray(int length):
m_length{ length }
{
assert(length >= 0);
if (length > 0)
m_data = new int[length]{};
}
~IntArray()
{
delete[] m_data;
// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
}
void erase()
{
delete[] m_data;
// We need to make sure we set m_data to nullptr here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
int& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
// reallocate resizes the array. Any existing elements will be destroyed. This function operates quickly.
void reallocate(int newLength)
{
// First we delete any existing elements
erase();
// If our array is going to be empty now, return here
if (newLength <= 0)
return;
// Then we have to allocate new elements
m_data = new int[newLength];
m_length = newLength;
}
// resize resizes the array. Any existing elements will be kept. This function operates slowly.
void resize(int newLength)
{
// if the array is already the right length, we're done
if (newLength == m_length)
return;
// If we are resizing to an empty array, do that and return
if (newLength <= 0)
{
erase();
return;
}
// Now we can assume newLength is at least 1 element. This algorithm
// works as follows: First we are going to allocate a new array. Then we
// are going to copy elements from the existing array to the new array.
// Once that is done, we can destroy the old array, and make m_data
// point to the new array.
// First we have to allocate a new array
int *data{ new int[newLength] };
// Then we have to figure out how many elements to copy from the existing
// array to the new array. We want to copy as many elements as there are
// in the smaller of the two arrays.
if (m_length > 0)
{
int elementsToCopy{ (newLength > m_length) ? m_length : newLength };
// Now copy the elements one by one
for (int index{ 0 }; index < elementsToCopy ; ++index)
data[index] = m_data[index];
}
// Now we can delete the old array because we don't need it any more
delete[] m_data;
// And use the new array instead! Note that this simply makes m_data point
// to the same address as the new array we dynamically allocated. Because
// data was dynamically allocated, it won't be destroyed when it goes out of scope.
m_data = data;
m_length = newLength;
}
void insertBefore(int value, int index)
{
// Sanity check our index value
assert(index >= 0 && index <= m_length);
// First create a new array one element larger than the old array
int *data{ new int[m_length+1] };
// Copy all of the elements up to the index
for (int before{ 0 }; before < index; ++before)
data [before] = m_data[before];
// Insert our new element into the new array
data[index] = value;
// Copy all of the values after the inserted element
for (int after{ index }; after < m_length; ++after)
data[after+1] = m_data[after];
// Finally, delete the old array, and use the new array instead
delete[] m_data;
m_data = data;
++m_length;
}
void remove(int index)
{
// Sanity check our index value
assert(index >= 0 && index < m_length);
// If we're removing the last element in the array, we can just erase the array and return early
if (m_length == 1)
{
erase();
return;
}
// First create a new array one element smaller than the old array
int *data{ new int[m_length-1] };
// Copy all of the elements up to the index
for (int before{ 0 }; before < index; ++before)
data[before] = m_data[before];
// Copy all of the values after the removed element
for (int after{ index+1 }; after < m_length; ++after)
data[after-1] = m_data[after];
// Finally, delete the old array, and use the new array instead
delete[] m_data;
m_data = data;
--m_length;
}
// A couple of additional functions just for convenience
void insertAtBeginning(int value) { insertBefore(value, 0); }
void insertAtEnd(int value) { insertBefore(value, m_length); }
int getLength() const { return m_length; }
};
int main()
{
// Declare an array with 10 elements
IntArray array(10);
// Fill the array with numbers 1 through 10
for (int i{ 0 }; i<10; ++i)
array[i] = i+1;
// Resize the array to 8 elements
array.resize(8);
// Insert the number 20 before element with index 5
array.insertBefore(20, 5);
// Remove the element with index 3
array.remove(3);
// Add 30 and 40 to the end and beginning
array.insertAtEnd(30);
array.insertAtBeginning(40);
// Print out all the numbers
for (int i{ 0 }; i<array.getLength(); ++i)
std::cout << array[i] << ' ';
std::cout << '\n';
return 0;
}
ref:https://www.learncpp.com/cpp-tutorial/10-1-object-relationships/
-End-