Classes, Friends and Polymorphism
Table of contents
Private, protected and public
Classes are used to model abstract and concrete entities in ways that combine state with functionality. (The state is held in member variables and the functionality is provided by member functions.) The literature tells us that classes encapsulate both data and the methods (functions) that act upon that data. Objects are instances of a particular class in the same way that (normal) variables are instances of a (built-in) type.
Let us consider a minimalist Person class, which we will later extend to Student and Employee through inheritance. Our first attempt might look like this:
struct Date {
int year{}, month{}, day{};
};
class Person {
Date dob;
string familyname, firstname;
};
Person a_person{};
Person genius{ { 1879, 3, 14 }, "Einstein", "Albert" }; // Error: does not (yet) compile
This Person class (here defined with class as opposed to the struct keyword we met in Chapter 6) contains three members: dob (itself a user-defined type), familyname and firstname (both of which are std::strings). We can define a variable of type Person (here a_person) using default-initialization syntax (the braces are in fact optional) but we cannot do a lot else with this object. Its fields will be zero-initialized for a_person.dob.year, a_person.dob.month, and a_person.dob.day, while a_person.familyname and a_person.firstname are empty strings. This is becuase the access specifier private: (which we also met in Chapter 6) is always implied for classes. This means we cannot either access the fields (member variables) directly using dot-notation, or use uniform initialization syntax, as with genius.
Experiment:
-
Change the above fragment to use
structinstead ofclassin order to enable compilation, and also an emptymain()function. Does the program run? -
Now try to create
geniuswithinmain()using assignment to member variables and uniform initialization. What error messages do you get? Does changing the keywordclasstostructfix this problem in both cases?
The key to solving the inability to create Persons using uniform initialization syntax is solved by writing a constructor. Access to member variables after the object has been created is achieved using getters and setters, which we met previously in Chapter 6. In order to be useful, a constructor must be declared after a public: access specifier. The following program demonstrates this, together with a main() program which produces output:
// 09-person1.cpp : model Person as a class with constructor
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
struct Date {
int year{}, month{}, day{};
};
class Person {
public:
Person(const Date& dob, string_view familyname, string_view firstname)
: dob{ dob }, familyname{ familyname }, firstname{ firstname }
{}
string getName() const { return firstname + ' ' + familyname; }
private:
const Date dob;
string familyname, firstname;
};
int main() {
Person genius{ { 1879, 3, 14 }, "Einstein", "Albert" };
cout << genius.getName() << '\n';
}
Quite a few things to note about this program:
-
A constructor has the format ClassName ( parameter-list ) : member initializers { possibly empty function body } and does not have a return type declared.
-
The constructor’s parameters have names
dob,familynameandfirstname, these being the same names as for the member variables (this is allowed in Modern C++). The conventions for naming (private:) class members vary, historically a trailing underscore is used, but this can become difficult to read. -
The member variables are initialized using uniform initialization syntax; this forbids narrowing conversions, and there shouldn’t be any as the parameter types should have been carefully chosen. (Older code may use parentheses here instead of braces.) The order of construction is the same as the way the member fields are laid out (after the
private:access specifier); the order in the comma-separated initializers is unimportant (although you should try to replicate the order of the member fields, your compiler will warn if they differ). The constructor’s body is empty here (although it must be present), and this is not unusual. -
The
Dateparameter is passed asconst-reference instead of by value, as it is probably too big to fit in a single register to pass by value. The names are passed by value asstd::string_viewalthough in older codeconst std::string&would be common. -
The member function
getName()is declaredconstas it is guaranteed not to change any member variables. It returns a newly createdstd::stringwhich must be returned by value. -
The member variable
dateis declaredconstas it will never need to be changed; of course it needs to be initialized by the constructor, but this is allowed. The member variablesfamilynameandfirstnameneed to be of typestd::string(notstd::string_viewas for the constructor’s parameters) for them to be guaranteed to exist for the lifetime of the class.
Experiment:
-
Add more
Personvariables tomain(), and output their names. -
Rewrite the constructor to initialize the member variables in the body, instead of using the comma-separated list of member initializers.
-
Write getters (all declared
const) calledgetFamilyName(),getFirstName(),getDOB()avoiding creation of unnecessary temporary variables. Modifymain()to use these. -
Write setters called
setFamilyName()andsetFirstName(). Test these frommain()again. -
Modify the original constructor to allow for
firstnamenot being present. Hint: use a defaulted function parameter. What other function needs to be changed? -
Try to create a default-constructed
Person. What do you find?
There is a third type of access specifier called protected:. Its meaning is the same as for private: except when inheritance is in use, when it means that (member functions defined within) derived classes have access to any members in the base class which were declared protected:. It’s rare to find this in real code, although the next program we shall look at demonstrates its syntax and use.
Unlike with the structs we met in Chapter 6, private inheritance is the default for classes. This means that any members which were public in the base class are not visible to users of the derived class. The literature tells us that (public) inheritance describes an is-a relationship, while the much rarer private inheritance describes an is-implemented-by relationship. Typically, this means that a privately derived class must provide (public) member functions which in turn call member functions of the base class. Interestingly, this doesn’t necessarily mean that the size and binary layout of the privately derived class is different from that of the base class, unless it has additional member variables.
(Protected inheritance, as opposed to protected members, is even more unusual, and quite possibly has no useful purpose. It isn’t discussed further here.)
The following program defines three classes, the second and third of which derive from the first. A collection of related classes that utilize inheritance is sometimes called an inheritance hierarchy. Quite a few changes have been made to Person so it is probably worth studying this first, before moving onto the new (derived) Student and Employee classes (quite a few member functions have been written on one line, to save space):
// 09-person2.cpp : model Person, Student and Employee as a class inheritance hierarchy
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
using namespace std;
class Person {
public:
struct Date;
Person(Date dob) : dob{ dob } {}
Person(Date dob, string_view familyname, string_view firstname, bool familynamefirst = false)
: dob{ dob }, familyname{ familyname }, firstname{ firstname },
familynamefirst{ familynamefirst } {}
virtual ~Person() {}
void setFamilyName(string_view familyname) { familyname = familyname; }
void setFirstName(string_view firstname) { firstname = firstname; }
void setFamilyNameFirst(bool familynamefirst) { familynamefirst = familynamefirst; }
string getName() {
if (familyname.empty() || firstname.empty()) {
return familyname + firstname;
}
else if (familynamefirst) {
return familyname + ' ' + firstname;
}
else {
return firstname + ' ' + familyname;
}
}
struct Date {
unsigned short year{};
unsigned char month{}, day{};
};
protected:
const Date dob;
private:
string familyname, firstname;
bool familynamefirst{};
};
class Student : public Person {
public:
enum class Schooling;
Student(const Person& person, const vector<string>& attended_classes = {}, Schooling school_type = Schooling::preschool)
: Person{ person }, school_type{ school_type }, attended_classes{ attended_classes } {}
const Date& getDOB() const { return dob; }
const vector<string>& getAttendedClasses() const { return attended_classes; }
enum class Schooling { preschool, elementary, juniorhigh, highschool, college, homeschool, other };
private:
Schooling school_type;
vector<string> attended_classes;
};
class Employee : public Person {
public:
Employee(const Person& person, int employee_id, int salary = 0)
: Person{ person }, employee_id{ employee_id }, salary{ salary } {}
bool isBirthday(Date today) const { return dob.month == today.month && dob.day == today.day; }
void setSalary(int salary) { salary = salary; }
auto getDetails() const { return pair{ employee_id, salary }; }
private:
const int employee_id;
int salary;
};
int main() {
Person genius{ { 1879, 3, 14 }, "Einstein", "Albert" };
Student genius_student{ genius, { "math", "physics", "philosophy" }, Student::Schooling::other };
Employee genius_employee{ genius, 1001, 15000 };
cout << "Full name: " << genius_student.getName() << '\n';
cout << "School classes: ";
for (const auto& the_class : genius_student.getAttendedClasses()) {
cout << the_class << ' ';
}
cout << '\n';
auto [ id, salary ] = genius_employee.getDetails();
cout << "ID: " << id << ", Salary: $" << salary << '\n';
Person::Date next_bday{ 2020, 3, 14 };
if (genius_employee.isBirthday(next_bday)) {
cout << "Happy Birthday!\n";
}
}
Many things to note about this program:
-
The
Datetype has been moved to be inside thePerson; its fully qualified name is thereforePerson::Date; this has been done to illustrate howstructs andclasses and be nested inside each other. (The typestd::year_month_day, new to C++20, was not available in my compiler when this program was written.) A forward-declarationstruct Date;is necessary to avoid having to defineDatein full before the first constructor. -
A second constructor for
Persontaking only aDatehas been added. Setters can be used later to initialize or modify the other three member variables, which are left defaulted by this constructor (empty for the twostd::strings andfalsefor thebool). -
A
virtualdestructor has been addded toPerson; if you remember one thing about inheritance, it should be that base classes need a virtual destructor. This is so that any heap objects of typeStudentorEmployeeassigned to a pointer of typePerson*(including use of smart pointers), the correct destructor of the derived class can be found and thus called, avoiding memory leaks. -
The
getName()function returns the name(s) provided by either the constructor or the setter(s) as a singlestd::string, ordered according to the member variablefamilynamefirst. (I hope this attempt at cultural inclusion doesn’t offend anyone!) -
The
Datetype has been modified to try to fit it into 32-bits, so it is almost certainly passed more efficiently by value in a single register rather than byconst-reference. -
The member variable
dobis declaredprotected:, the other three areprivate:, as before. -
The
Studenttype is derived fromPersonusing the keywordpublic. If this keyword were omitted, none ofPerson’spublic:members would be visible to users ofStudent, asclasses default to private inheritance. The syntax is exactly as forPixelinheriting fromPointin Chapter 6. -
An
enum classcalledSchoolingis also forward-declared so that it is able to be used as a constructor parameter. -
The three
Studentconstructor parameters are an existingPersonobject used to initialize the base class part, an optionalvector<string>(needed to be passed by value in this case), and an optional value from the enumeration setSchooling. -
The base class portion of
Studentis initialized asPerson{ person }wherepersonis of typeconst Person&. Then the other two fields ofStudentare initialized. The constructor parameter variableattended_classesis passed as aconst vector<string>&so that only one copy is made, which is when the member variable of the same name is initialized. -
A
public:member functiongetDOB()makes theprotected:member of the base classdobavailable to users of the derived class. It is declaredconstand returns aconst-reference. -
The member function
getAttendedClasses()returns aconst-reference toattended_classes, therefore thisstd::vector<string>is made visible to the function which calls this member function, but is not modifiable. -
The
Employeeconstructor takes three parameters, the third of which is optional. The base class portion is initialized in the same way as forStudent. -
The member function
isBirthday()takes aPerson::Dateas a parameter and compares thedayandmonthfields with those ofdob, returningtrueif they are the same, orfalseotherwise. (We’re pretending “today” is March 14, 2020.) -
The member variable
employee_idis not meant to be able to be changed, so is declaredconst. The settersetSalary()is defined so thatsalarycan be updated, while the gettergetDetails()returns an aggregate of both derived class member variables by value.
Experiment:
-
Modify
main()to remove the need for the variablePerson genius. Hint: there will be some necessary code duplication. -
Add some other
Students andEmployees. Experiment with minimalist and partial initializations. -
Experiment with the member functions not previously called from
main(). -
Write a getter/setter pair to retrieve/modify
school_typeforStudent. -
Write a second constructor for
Studentwhich takes (in addition) the parameters needed to define aPerson. Initialize thePersonbase class from these parameters. Should these parameters before or after the ones specific toStudent? Can they be defaulted? -
Write a second constructor for
Employeeto accomplish the same thing. -
Add
getDOB()toEmployee, as forStudent. Now try to add it toPerson, what do you find? Would a singlepublic:getter in the base class be more useful than aprotected:member variable? -
Add member functions
addAttendedClass()andremoveAttendedClass()toStudent. Make them smart enough to handle duplicates/invalid parameters. -
Add the field
job_titletoEmployeeas well as support for this in the relevant getters/setters/constructors.
Copying and comparisons
So far we have created stack objects and accessed their member functions. Often, you will want to make copies of these objects, whether its passing them to, or returning them from, functions, or storing them in a container. Sometimes they are passed by reference instead, and this is preferred for (larger) user-defined types, as passing by value has to cause a (potentially) expensive copy to be made. However the class designer needs to be aware of all of the copy and move operations that might be required of object instances, and must ensure they are implemented correctly.
There are six operations which are involved in this discussion: three constructors, two assignment operators and the destructor. All of these can be explicitly declared = default or = delete. (We have already discovered that defining a constructor which takes parameters causes the default constructor to no longer be generated.) The other two constructors and the two assignment operators each come in two forms: copy and move, as shown in the next code fragment, using Person as the name of the class, and a code example of when the operation would be called. (The boilerplate code shown here can be copied verbatim for other classes, simply changing every occurence Person to the name of the class. The actual variable parameter name, often being rhs, has been omitted; these are the minimalist forms of the member function declarations.)
class Person {
// rest of class definition omitted
public:
// "default constructor"
Person() = delete; // Person p1{}, p2(), p3;
// "copy constructor"
Person(const Person&) = delete; // Person p4{ p1 }, p5(p2);
// "copy assignment operator"
Person& operator= (const Person&) = delete; // Person p6; p6 = p1;
// "move constructor"
Person(Person&&) = delete; // Person p7{ std::move(p2) };
// "move assignment operator"
Person& operator= (Person&&) = delete; // Person p8; p8 = std::move(p3);
// "destructor"
~Person() = delete; // Any Person object going out of scope
};
Experiment:
-
Add the above code to the end of the definition of the
Personclass from09-person1.cpp. Why doesn’t the code compile now? Hint: read the error message carefully. Fix this by= defaulting just one of the operations. -
Try to create
p4top8as above,= defaulting the operations as necessary. Are the (copied/assigned) objects in a valid state? Hint: try to use their member functions. -
Now use
autoinstead ofPerson, for example:auto p1{};. Does the code still compile? Are the objects valid?
As can be seen we are aided by the compiler in the provision of object duplication, as many (probably most) of the classes you will write have valid (= default) special member functions generated as they are needed. (The exact rules of when and which of them are generated automatically are slightly arcane; you may find references to the “rule of five” for Modern C++ online or in literature.) However, the exception proves the rule, and I would suggest declaring the first five of these = delete when writing a new class, enabling them one by one with = default as any compiler errors present themselves, ensuring that objects can be copied and moved correctly. Most member variable types are compatible with default copy/move semantics, the obvious one that isn’t being raw pointers. Writing custom special member functions for derived classes is sometimes tricky, as it involves manually invoking the correct special member function on the base class. (Hopefully you won’t have to do this very often, further explanation is beyond the scope of this Tutorial.) Be aware that if the member variables (of a base or derived class) themselves obey the usual rules of copying (such as int, double, std::string, std::shared_ptr<T>, but not char * for example) then the =default special member functions will always work correctly.
Often we will want to compare objects for equivalence. Some containers, such as std::unordered_map, mandate that operator== is defined, while others such as std::map, require operator<, so we can only store objects in associative containers if the required operators have been defined. The following code defines a rudimentary member operator== for the Person class from 09-person1.cpp, the syntax from Chapter 6 should be familiar:
class Person {
// rest of class definition omitted
public:
bool operator== (const Person& rhs) { return getName() == rhs.getName(); }
};
Alternatively, global operator== can be overloaded for Person, as demonstrated here:
bool operator== (const Person& lhs, const Person& rhs) {
return lhs.getName() == rhs.getName();
}
Defining either one of these variants of operator== is sufficient to make the following code compile:
int main() {
Person person1 { { 2000, 1, 1 }, "John", "Smith" };
auto person2{ person1 };
if (person1 == person2) {
cout << "Same!\n";
}
else {
cout << "Different!\n";
}
}
A couple of things to note:
-
The return type of both variants is
bool(notPerson&). -
The member function version has access to its own member variables and those of
rhs(even though it doesn’t access them directly), while the global (free) function version relies on public getters.
Experiment:
-
Define
person2with a different date of birth. How do they compare now? -
Can you fix this problem by modifying the member
operator==? -
Do the same with global
operator==? -
Write a member
operator==forEmployeefrom09-person2.cpp, that compares theemployee_idmember (only) for equality, and then test this operator.
Friend functions and classes
Friends have access to all members of the class that declares them a friend, including those declared private: or protected:. Sometimes this is desirable, as shown in the following program:
// 09-person3.cpp : define operator== and operator<< for Person class
#include <iostream>
using namespace std;
struct Date {
int year{}, month{}, day{};
};
bool operator== (const Date& lhs, const Date& rhs) {
return lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day;
}
class Person {
public:
Person(const Date& dob, string_view familyname, string_view firstname)
: dob{ dob }, familyname{ familyname }, firstname{ firstname }
{}
string getName() const { return firstname + ' ' + familyname; }
friend bool operator== (const Person&, const Person&);
friend ostream& operator<< (ostream&, const Person&);
private:
const Date dob;
string familyname, firstname;
};
bool operator== (const Person& lhs, const Person& rhs) {
return lhs.familyname == rhs.familyname
&& lhs.firstname == rhs.firstname
&& lhs.dob == rhs.dob;
}
ostream& operator<< (ostream& os, const Person& p) {
os << "Name: " << p.getName() << ", DOB: "
<< p.dob.year << '/' << p.dob.month << '/' << p.dob.day;
return os;
}
int main() {
Person person1{ { 2000, 1, 1 }, "John", "Doe" },
person2{ { 1987, 11, 31 }, "John", "Doe" };
cout << "person1: " << person1 << '\n';
cout << "person2: " << person2 << '\n';
if (person1 == person2) {
cout << "Same person!\n";
}
else {
cout << "Different person!\n";
}
}
Some things to note about this program:
-
Global
operator==is defined forDate. Note that if we used eitherstd::year_month_dayoroperator<=>(both new in C++20, but not covered in this Tutorial), this definition would not be necessary. As thisDateis astructwith all memberspublic:, use of the keywordfriendis not needed. -
Within the definition of
Person, both globaloperator==and globaloperator<<are declaredfriend. This is more boilerplate that you can use in your own classes, changing all occurrences ofPersonto the name of your class. (They are identical to normal function declarations, other than the use of thefriendkeyword.) -
Global
operator==is defined forPerson. Here thestd:::stringmembers are compared explicitly, before theDatemembers are compared, calling the previously definedoperator==forDateautomatically. -
Global
operator<<is also defined forPerson, allowing objects to be put tocout(and any otherstd::ostreams) using<<. This needs to be afriendbecause it accessesdob.
Experiment:
-
Give
person2the same date of birth asperson1. Does the program produce the expected output? -
Now give them different names. What output do you get?
-
Define global
operator<<forDate. Can you remove the need foroperator<<forPerson, to itself be afriendofclass Person? -
Make global
operator==forPersoncomparegetName()s. Can you remove the need for it to be afriend? -
Make
operator==forPersona member function instead of a global function.
Classes can be declared friends as well as functions, although this use is probably less common. The following program defines two classes A and B which are mutual friends, thus allowing member functions of either to access each other’s private: members.
// 09-friends.cpp : two classes as friends of each other
#include <iostream>
using namespace std;
class B;
class A {
public:
friend class B;
void a(B& other);
private:
int m_a{42};
};
class B {
public:
friend class A;
void b(A& other) { cout << "b():" << other.m_a << '\n'; }
private:
double m_b{1.414};
};
void A::a(B& other)
{
cout << "a():" << other.m_b << '\n';
}
int main() {
A obj_a{};
B obj_b{};
obj_a.a(obj_b);
obj_b.b(obj_a);
}
A few things to note about this program:
-
In order for
friend class Bto be written withinclass A, the delarationclass B;must appear beforehand. This forward declaration allows a reference (or pointer, including smart pointer) toBto be taken and used, but members cannot (yet) be accessed. -
The definition of
class A’s member functiona()must be written outside of the function body, after the definition ofclass B. It is important to appreciate that it is still a member function, not a global function, when written after the class definition non-inline (or out-of-line) in this way using the scope resolution operator (::). -
The definition of
class Bdeclaresfriend class Aand its member functionb()can accessother.m_afor this reason. -
The member variables need a prefix (such as
m_) because member functions calleda()andb()are used, and the names would clash.
Experiment:
-
Change the types of the member variables and their values. Does the program compile without further changes?
-
Add a defaulted second parameter to
a()andb()which is used to set the value of other class’s member variable. -
Go back to
Personfrom09-person1.cppand definegetName()outside the class body. Does it still need a declaration inside the class body? Can this definition now appear aftermain()? What do free functions and non-inline member functions have in common?
Polymorphism
The literature tells us that polymorphism “is a concept in type theory wherein a name may denote instances of many different classes as long as they are related by some common superclass” (Booch, “Object-Oriented Analysis and Design with Applications”1). What this means in practice is that derived class objects can be manipulated through a pointer or reference to a base class type, with member function selection being resolved at run-time. This probably doesn’t sound too exciting, but is important in order for C++ to be classified as an object-oriented programming language, as opposed to merely object-based. Member functions whose selection is determined at run-time are called virtual functions, and are defined with the virtual keyword (which we have already met when discussing virtual destructors in base classes).
The following code defines (part of) an abstract base class called Virtual; it makes use of virtual functions in the following forms:
class Virtual {
public:
virtual void f();
virtual void g() = 0;
virtual void h() override;
virtual void k() override final;
};
The meanings implied for these member functions in the context of the virtual keyword are as follows:
-
f()is a function in a base class or derived class which can (optionally) be redefined (in the derived class). -
g()is a pure-virtual function of an abstract base class, which is never defined in this class and must be defined in a class that derives from it, in order for objects of the derived class to able to be created. Objects of an abstract class cannot be instantiated; attempting to do so would trigger a compile-time error. -
h()is a function in a derived class which redefines (overrides) a previous definition; the function signature must exactly match that in the base class (includingconstandnoexceptqualifiers). This function can itself be redefined in any subsequently derived class. -
k()is the same ash()except this function cannot be redefined in a subsequently derived class.
The following program demonstrates all of these uses in a more complex hierarchy deriving from an abstract Shape class:
// 09-shape.cpp : Shape class hierarchy demonstrating polymorphism
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Shape {
public:
struct Point;
Shape(int sides) : sides{ sides } {}
Shape(int sides, Point center) : sides{ sides }, center{ center } {}
virtual void draw(ostream& os) const = 0;
virtual string getSides() const { return to_string(sides); }
void moveBy(int dx, int dy) { center.x += dx; center.y += dy; }
const Point& getCenter() const { return center; }
virtual ~Shape() { cerr << "~Shape()\n"; }
struct Point {
int x{}, y{};
};
private:
int sides;
Point center;
};
ostream& operator<< (ostream& os, const Shape::Point& pt) {
return os << '(' << pt.x << ',' << pt.y << ')';
}
class Triangle final : public Shape {
public:
Triangle(int side) : Shape{ 3 }, side{ side } {}
Triangle(int x, int y, int side) : Shape{ 3, {x, y} }, side{ side } {}
virtual void draw(ostream& os) const override {
os << " /\\\n/__\\\nSide: " << side << "\nAt: " << getCenter() << '\n';
}
private:
int side;
};
class Circle : public Shape {
public:
Circle(int radius) : Shape{ 0 }, radius{ radius } {}
Circle(int x, int y, int radius) : Shape{ 0, {x, y} }, radius{ radius } {}
virtual void draw(ostream& os) const override final {
os << " _\n(_)\nRadius: " << radius << "\nAt: " << getCenter() << '\n';
}
virtual string getSides() const override final { return "infinite"; }
private:
int radius;
};
class Rectangle : public Shape {
public:
Rectangle(int side_x, int side_y) : Shape{ 4 }, side_x{ side_x }, side_y{ side_y } {}
Rectangle(int x, int y, int side_x, int side_y)
: Shape{ 4, {x ,y} }, side_x{ side_x }, side_y{ side_y } {}
virtual void draw(ostream& os) const override {
os << " ____\n|____|\nSize: " << side_x << 'x' << side_y << "\nAt: " << getCenter() << '\n';
}
protected:
int side_x, side_y;
};
class Square final : public Rectangle {
public:
Square(int side) : Rectangle{ side, side } {}
Square(int x, int y, int side) : Rectangle{ x, y, side, side } {}
virtual void draw(ostream& os) const override final {
os << " _\n|_|\nSide: " << side_x << "\nAt: " << getCenter() << '\n';
}
};
int main() {
vector<Shape*> shapes;
shapes.push_back(new Circle{ 10 });
shapes.push_back(new Triangle{ 10, 20, 15 });
shapes.push_back(new Rectangle{ 10, 5 });
shapes.push_back(new Square{ 25, 100, 50 });
shapes[0]->moveBy(20, 50);
for (auto& s : shapes) {
s->draw(cout);
cout << "Sides: " << s->getSides() << '\n';
delete s;
s = nullptr;
}
}
A lot of things to note about this program:
-
The definition of
Shapecontains two constructors, one pure-virtual member function, one virtual function, two non-virtual functions and a virtual destructor. It also definesPointas a localstruct. In addition, twoprivate:member variables are defined. -
Both of the member variables are guaranteed to be initialized whenever a derived class calls either one of the
Shapeconstructors. -
The two non-virtual member functions are not meant to be redefined in derived classes, and provide functionality for both the member variables that
Shapedefines (member functions of a base class cannot access member functions or variables of a derived class). -
To reduce code duplication, an overload of
operator<<which handlesShape::Points is provided (above the derived classes which use it). -
All of the derived classes provide an implementation of
draw(). In addition,Circleprovides its own implementation ofgetSides(). -
The definition of
Triangleis the simplest of those which derive fromShapeand represents an equilateral triangle; public inheritance needs to be specified as for all the other derived classes in this hierarchy. This class definition is qualified with thefinalkeyword, which means that no class can derive fromTriangle(it is therefore the “final” class of that inheritance “branch”). The constructors both call aShapeconstructor. A single member variablesideis defined which is used by the definition ofdraw(). -
The definition of
Circleis very similar to that ofTriangle; this is a common theme with class heirarchies. It redefinesgetSides()in addition to defining and using a member variableradius. -
The definition of
Rectangledefines twoprotected:member variables which are initialized by both constructors and output bydraw(). -
The definition of
Squareis, as forTriangle, qualified withfinal, and inherits fromRectangle(instead of directly fromShape). SinceRectangle’s constructor calls that forShape, neither ofSquare’s constructors need to call it directly. It can access theprotected:member variables ofRectanglewithin its definition ofdraw(). -
In
main()astd::vector<Shape*>(vector of raw pointers toShape) is created, and the populated with the return value fromnew; no intermediate pointer is used. (SinceShapeis an abstract type it is not possible to create astd::vector<Shape>, as these would need to be able to be default-initialized in order for the container to be created.) -
The output from the range-for loop proves that polymorphism is being used, as the loop variable
sis a (reference to a) pointer to the base class type of the hierarchy. -
The member functions
draw()andgetSides()can be called frommain()becuase they were declaredpublic:in all of the base and derived classes. -
The
Shapeobjects are deleted as soon as they have been output; setting a pointer that is finished with tonullptrstraightaway is good practice as it protects against the possibility of trying to access or delete a dangling pointer. Better practice still would be to use astd::vectorof smart pointers.
Experiment:
-
Try to create an object of type
Shape()that would normally use the first constructor (which takes a singleintparameter). What do you find? -
Move all the calls to
getCenter()intomain(). -
Write an overload of
operator<<which handlesconst Shape&. Does this need to be afriendfunction? Decide whether you think this is neat, or just being too clever. -
Remove the
&from the range-for loop inmain(). What is the (single, invisible) difference about the program? -
Write a (
virtual) destructor for all of the classes besidesShape(), observing how the output changes. What do you learn about order of destruction in a class hierarchy? What happens if you omitShapes own destructor? -
Try to derive an empty class from
Square. What compilation error do you get? -
Try removing the
constqualifier from the overload ofgetSides()inCircle. Does the code still compile? What does this tell you about the effect ofconston a member function’s signature? -
Try to derive from
Circle. What happens if you try to overloaddraw()? -
Consider the best way (least code duplication) to add a member function
getArea()(which returns adouble) toShape, and implement this for all classes in the hierarchy.
This course is largely based on the material from Richard Spencer’s repository, where he shares his knowledge and passion for modern C++ programming.
-
Grady Booch, Robert A. Maksimchuk, Michael W. Engle, Bobbi J. Young, Jim Conallen, Kelli A. Houston Object-Oriented Analysis and Design with Applications (3rd ed. Pearson, 2007, ISBN-13: 9780201895513) ↩