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::string
s). 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 class
es. 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
struct
instead ofclass
in order to enable compilation, and also an emptymain()
function. Does the program run? -
Now try to create
genius
withinmain()
using assignment to member variables and uniform initialization. What error messages do you get? Does changing the keywordclass
tostruct
fix this problem in both cases?
The key to solving the inability to create Person
s 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 <chrono>
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
using namespace std::chrono;
class Person {
public:
Person(const year_month_day& dob, string_view familyname, string_view firstname)
: dob{ dob }, familyname{ familyname }, firstname{ firstname }
{}
string getName() const { return firstname + ' ' + familyname; }
const year_month_day& getDob() const { return dob; }
private:
const year_month_day dob;
string familyname, firstname;
};
int main() {
Person genius{ { 1879y, March, 14d }, "Einstein", "Albert" };
cout << genius.getName() << " was born " << genius.getDob() << '\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
,familyname
andfirstname
, 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
std::chrono::year_month_day
parameter (itself initialized by uniform initialization) 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_view
although in older codeconst std::string&
would be common. -
The member function
getName()
is declaredconst
as it is guaranteed not to change any member variables. It returns a newly createdstd::string
which must be returned by value. -
The member variable
dob
is declaredconst
as it will never need to be changed; of course it needs to be initialized by the constructor, and this case is allowed. The member variablesfamilyname
andfirstname
need to be of typestd::string
(notstd::string_view
as for the constructor's parameters) for them to be guaranteed to exist for the lifetime of the class. -
The member function
getDob()
is also declaredconst
and returns aconst
-reference. It is possible to put this return value directly to astd::ostream
as the Standard Library overloadsoperator<<
forstd::chrono::year_month_day
.
Experiment:
-
Add more
Person
variables 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.
-
Modify this program to use
std::println()
instead ofcout
. -
Write getters (all declared
const
) calledgetFamilyName()
andgetFirstName()
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
firstname
not 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 struct
s we met in Chapter 6, private inheritance is the default for class
es. 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 class
es, 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 <chrono>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
using namespace std;
using namespace std::chrono;
class Person {
public:
Person(year_month_day dob) : dob{ dob } {}
Person(year_month_day 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;
}
}
protected:
const year_month_day 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 year_month_day& 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 isBirthdayToday(year_month_day 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{ { 1879y, March, 14d }, "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';
year_month_day next_bday{ 2023y, March, 14d };
if (genius_employee.isBirthdayToday(next_bday)) {
cout << "Happy Birthday!\n";
}
}
Many things to note about this program:
-
A second constructor for
Person
taking only astd::chrono::year_month_day
has 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::string
s andfalse
for thebool
). -
A
virtual
destructor has been added 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 typeStudent
orEmployee
assigned 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 member variable
dob
is declaredprotected:
, the other three areprivate:
, as before. -
The
Student
type is derived fromPerson
using the keywordpublic
. If this keyword were omitted, none ofPerson
'spublic:
members would be visible to users ofStudent
, asclass
es default to private inheritance. The syntax is exactly as forPixel
inheriting fromPoint
in Chapter 6. -
An
enum class
calledSchooling
is also forward-declared so that it is able to be used as a constructor parameter. -
The three
Student
constructor parameters are an existingPerson
object 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
Student
is initialized asPerson{ person }
whereperson
is of typeconst Person&
. Then the other two fields ofStudent
are initialized. The constructor parameter variableattended_classes
is 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:
data member of the base classdob
available to users of the derived class, in this caseStudent
. It is declaredconst
and 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
Employee
constructor takes three parameters, the third of which is optional. The base class portion is initialized in the same way as forStudent
. -
The member function
isBirthdayToday()
takes astd::chrono::year_month_day
as a parameter and compares the return values of theday()
andmonth()
members with those ofdob
, returningtrue
if they are the same, orfalse
otherwise. (We're pretending "today" is March 14, 2024, so this function always returnstrue
.) -
The member variable
employee_id
is not meant to be able to be changed, so is declaredconst
. The settersetSalary()
is defined so thatsalary
can 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
Student
s andEmployee
s. 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_type
forStudent
. -
Write a second constructor for
Student
which takes (in addition) the parameters needed to define aPerson
. Initialize thePerson
base class from these parameters. Should these parameters before or after the ones specific toStudent
? Can they be defaulted? -
Write a second constructor for
Employee
to 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_title
toEmployee
as well as support for this in the relevant getters/setters/constructors.
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 class
es, 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
Person
class from09-person1.cpp
. Why doesn't the code compile now? Hint: read the error message carefully. Fix this by= default
ing just one of the operations. -
Try to create
p4
top8
as above,= default
ing the operations as necessary. Are the (copied/assigned) objects in a valid state? Hint: try to use their member functions. -
Now use
auto
instead 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 operator
s 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
person2
with 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==
forEmployee
from09-person2.cpp
, that compares theemployee_id
member (only) for equality, and then test this operator.
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<=> for Person class
#include <iostream>
using namespace std;
struct Date {
int year{}, month{}, day{};
auto operator<=>(const Date&) const = default;
};
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; }
const auto& getDob() const { return dob; }
auto operator<=>(const Person&) const = default;
friend ostream& operator<< (ostream&, const Person&);
private:
string familyname, firstname;
const Date 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 }, "Doe", "John" },
person2{ { 1987, 11, 31 }, "Doe", "John" };
cout << "person1: " << person1 << '\n';
cout << "person2: " << person2 << '\n';
if (person1 == person2) {
cout << "Same person!\n";
}
else {
cout << "Different person!\n";
}
cout << "person1 is ";
if (person1.getDob() > person2.getDob()) {
cout << "younger than ";
}
else if (person1.getDob() < person2.getDob()) {
cout << "older than ";
}
else {
cout << "the same age as ";
}
cout << "person2" << '\n';
}
Some things to note about this program:
-
Member
operator<=>
(the "spaceship operator") is defaulted for this roll-your-ownDate
; this is all that is needed for the equality and ordering comparisons to be defined for this class, with ordering performed member-wise starting with the first data member. -
Within the definition of
Person
, globaloperator<<
is declared as afriend
function. This is more boilerplate that you can use in your own classes, changing parameterconst Person&
to the name of your class. (They are identical to normal function declarations, other than the use of thefriend
keyword.) -
Member
operator<=>
is defaulted forPerson
; with this code thestd:::string
members will be compared (familyname
beforefirstname
), before theDate
members are compared. -
Global
operator<<
is also defined forPerson
, allowing objects to be put tocout
(and any otherstd::ostream
s) using<<
. This needs to be afriend
because it accessesdob
.
Experiment:
-
Give
person2
the 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 afriend
ofclass Person
? -
Compare a few
Person
instances with similar or same family names and first names, storing them in astd::set<Person>
. Write code to output them telephone-book style. Are they ordered in the way you would expect?
Classes can be declared friend
s as well as functions, although this use is probably less common. The following program defines two class
es 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 B
to be written withinclass A
, the delarationclass B;
must appear beforehand. This forward declaration allows a reference (or pointer, including smart pointer) toB
to 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 B
declaresfriend class A
and its member functionb()
can accessother.m_a
for 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
Person
from09-person1.cpp
and 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?
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 (includingconst
andnoexcept
qualifiers). 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
Shape
contains two constructors, one pure-virtual member function, one virtual function, two non-virtual functions and a virtual destructor. It also definesPoint
as 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
Shape
constructors. -
The two non-virtual member functions are not meant to be redefined in derived classes, and provide functionality for both the member variables that
Shape
defines (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::Point
s is provided (above the derived classes which use it). -
All of the derived classes provide an implementation of
draw()
. In addition,Circle
provides its own implementation ofgetSides()
. -
The definition of
Triangle
is the simplest of those which derive fromShape
and 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 thefinal
keyword, which means that no class can derive fromTriangle
(it is therefore the "final" class of that inheritance "branch"). The constructors both call aShape
constructor. A single member variableside
is defined which is used by the definition ofdraw()
. -
The definition of
Circle
is 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
Rectangle
defines twoprotected:
member variables which are initialized by both constructors and output bydraw()
. -
The definition of
Square
is, 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 ofRectangle
within 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. (SinceShape
is 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
s
is 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
Shape
objects are deleted as soon as they have been output; setting a pointer that is finished with tonullptr
straightaway 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::vector
of smart pointers.
Experiment:
-
Try to create an object of type
Shape()
that would normally use the first constructor (which takes a singleint
parameter). What do you find? -
Move all the calls to
getCenter()
intomain()
. -
Write an overload of
operator<<
which handlesconst Shape&
. Does this need to be afriend
function? 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 omitShape
s own destructor? -
Try to derive an empty class from
Square
. What compilation error do you get? -
Try removing the
const
qualifier from the overload ofgetSides()
inCircle
. Does the code still compile? What does this tell you about the effect ofconst
on 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.
All text and program code ©2019-2024 Richard Spencer, all rights reserved.
Footnotes
-
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) ↩