C++ Programming Tutorial Part II: Object-Oriented Programming
C++ Programming Tutorial Part II: Object-Oriented Programming
C. David Sherrill
Georgia Institute of Technology
Chapter 9: Introduction to Objects
• Declaring and Defining a Class
• Data encapsulation; public and private class members; getter/setter
methods
• Pointers to classes
• Initializing data
• Constructors and default constuctors
• The “this” pointer
• Initialization lists
• Destructors
• Shallow and deep copies, copy constructors, and copy by assignment
• Intro to move constructors
• Classes that don’t allow copying, singleton classes
• Classes only creatable on the heap
• sizeof() a class
• Friend Functions and Friend Classes
• Const member functions
Introduction to Objects
An object is a user-defined datatype like an
integer or a string. Unlike those simple
datatypes, though, an object can have much
richer functionality. It typically collects some
data (“member data”) and some functionality
(“methods”). For example, we might create a
class to handle a matrix, or a tensor, or a
student’s record in a class, etc.
Declaring and Defining a Class
Before we can use variables of a given class, we first have
to specify the class. Analogous to functions, we can
declare the class by specifying any member data it
contains and providing a list of its available functions
(along with what arguments those functions take and
their return types). This is often done in a header file
(whose name is typically the name of the class, with a
“.h” suffix). We can then define the functions in the
class in another file if we like (often with the name of
the class with a “.cc” suffix). Alternatively, some or all
of the function definitions can also go in the header file
(often this is done for short functions or inline
functions).
Example: The Student Class
• Suppose we want to keep track of students
taking a course. Each student will have a
name, a midterm grade, a final grade, and a
course project grade. These grades will be
used to compute a final course grade.
//Listing of student1.cc:
#include <iostream>
#include <string>
using namespace std;
class Student
{
private:
string Name;
double MidtermGrade;
double FinalGrade;
double ProjectGrade;
public:
void SetName(string theName)
{
Name = theName;
}
void SetMidtermGrade(double grade)
{
MidtermGrade = grade;
}
void SetFinalGrade(double grade)
{
FinalGrade = grade;
}
void SetProjectGrade(double grade)
{
ProjectGrade = grade;
}
double ComputeCourseGrade(void)
{
double courseGrade = (MidtermGrade + FinalGrade + ProjectGrade) / 3.0;
return(courseGrade);
}
string GetName(void)
{
return(Name);
}
}; // done defining and declaring Student class
Dissecting the Class
• Because this is a simple class, we forgo writing a
declaration in a header file, and we just declare and define
the class all at once in a .cc source file (here, student1.cc).
• The class is declared using the syntax “class classname { …
};” Inside the declaration, we place member data and
function declarations
• For this example, the member data for this class is found
within the “private” section, and the function declarations
are found within the “public” section. However, member
data and functions can generally be either public or private,
as desired. See below for what these keywords mean.
Public and Private
• Member data and/or functions declared
“public” are accessible by code that resides
outside the class (e.g., in main())
• Member data and/or functions declared
“private” may only be used by code contained
within the class (e.g., class member functions)
• If “private” stops you from accessing
data/functions outside the class, why would
you ever want to use it?
Data Encapsulation
One of the key tenents of object-oriented
programming is that of “data encapsulation.”
This means that (at least some) member data is
hidden within a class and is not accessible from
outside that class (at least not directly accessible).
This is considered a good thing because in a large
program, another programmer coming in and
directly manipulating data in your class might
have unexpected side-effects. By requiring other
programmers to go through an interface you (the
class programmer) provide, you lessen the
chance of such side-effects.
Getter/Setter Methods
• Of course, programmers who use your class will want
to be able to interact with it and get or set data within
it. Hence, the class programmer provides “getter” and
“setter” methods for data that a user of the class might
need to interact with.
• In our example, we provide a setter method for every
piece of member data (conveniently, all the setter
functions begin with “Set” so it’s clear what they do).
However, in this case we only provide getter functions
for the student’s name [GetName()] and the overall
course grade [ComputeCourseGrade()]. We could
certainly provide getter functions for the other data,
too, if we wanted.
Using the Class
• Ok, here’s some code in main() that allows us
to use the class
int main()
{
// Construct a student object and set some properties
Student Student1;
Student1.SetName("John Smith");
Student1.SetMidtermGrade(80.0);
cout << "Student " << Student1.GetName() << " has course grade ";
cout << Student1.ComputeCourseGrade() << "\n";
}
Program output:
Student John Smith has course grade 26.6667
Accessing Member Data and Functions
• Notice that once we create a new object of type Student
using the syntax “Student student1”, we can access its
associated functions (methods) using syntax like
“student1.SetName(“John Smith”);” The dot operator
comes between the name of the object and the name of
the data/method we want to access.
• We could try setting the name directly using something like
“student1.Name = “John Smith”;” That would normally
work, but not in this case because we declared Name to be
Private, meaning that direct access without going through
Public functions is impossible. See the next page for a
modified version of the program that would allow direct
access to the Name variable in the class.
Direct Access to Public Member Data
• Here is a list of modifications to our program that would allow direct
accessing of the Name field (see listing student1a.cc). However, keep in
mind this kind of direct access is discouraged because it breaks data
encapsulation!
class Student
{
private:
double MidtermGrade;
double FinalGrade;
double ProjectGrade;
public:
string Name;
…
}; // done defining and declaring Student class
int main()
{
Student Student1;
Student1.Name = "John Smith"; // direct access now
Student1.SetMidtermGrade(80.0);
cout << "Student " << Student1.Name << " has course grade "; // direct now
cout << Student1.ComputeCourseGrade() << "\n";
}
Access to Classes Using Pointers
• Of course, sometimes we might want to create our
new object dynamically using the “new” operator
(perhaps we want an array of type Student, for
example)
• When we have a pointer to a class, we access data
and/or methods using the “->” operator instead of the
“.” operator. For example (see listing student1b.cc):
int main()
{
// Construct a student object and set some properties
Student* Student1 = new Student(); // note parentheses
Student1->SetName("John Smith");
Student1->SetMidtermGrade(80.0);
cout << "Student " << Student1->GetName() << " has course grade ";
cout << Student1->ComputeCourseGrade() << "\n";
}
Uninitialized Data
• Note that we only bothered to set the Midterm exam
grade. Perhaps it’s early in the semester, or perhaps
the student never completed the other two
assignments. The overall course grade reflects zeroes
for these two assignments and thus yields a overall
course grade of 26.6 … unless it doesn’t!
• Note that we never initialized the grades for the other
assignments to zero. So, on some machines these
other grades might have random, nonzero values,
leading to trouble computing grades unless all of the
grades are set using the setter methods!
Initializing Data
• You might think this would be easy to fix… we
could just go into the “private” section
defining the member data and change it like
so: private:
string Name;
double MidtermGrade = 0.0;
double FinalGrade = 0.0;
double ProjectGrade = 0.0;
class Student
{
public:
Student()
{
// code can go here
}
Constructors
• To separate the definition from the declaration,
use this syntax:
class Student
{
public:
Student(); // constructor declaration
};
// constructor definition
Student::Student()
{
// constructor code goes here
}
• The scope resolution operator (::) in the definition shows that the
function Student() [constructors are functions with the same name
as their class] belongs to class Student. In fact we could use this
syntax for any of the other methods of class Student to define them
separately from their declaration, e.g., Student::SetName().
Next Iteration of the Student Class
On the following page is our next version of the
student class (student2.cc) that uses a
constructor to zero out the grades when a
new Student object is constructed. We’ve also
decided that the calling program probably
only needs to print out the grades, so we’ve
moved the course grade computation and the
printing together into a new function,
PrintCourseGrade()
class Student
{
private:
string Name;
double MidtermGrade;
double FinalGrade;
double ProjectGrade;
public:
Student()
{
MidtermGrade = FinalGrade = ProjectGrade = 0.0; // initialize vars to 0
}
void PrintCourseGrade(void)
{
cout << "Student " << Name << " has course grade ";
cout << (MidtermGrade + FinalGrade + ProjectGrade) / 3.0 << "\n";
}
}; // done defining and declaring Student class
int main()
{
Student Student1;
Student1.SetName("John Smith");
Student1.SetMidtermGrade(80.0);
Student1.PrintCourseGrade();
}
More Elaborate Constructors
Our constructor is pretty basic: it just sets the
grades to zero when the object is created. But
the constructor function, like other functions, can
be overloaded. This gives us the option to create
an object with certain information specified at
the time of creation. For example, we could
make a constructor that takes a string argument
to automatically set the student’s name, like this:
Student student1(“John Smith”);
The next page shows how we could implement
this.
Constructor with Arguments Example
// student3.cc
class Student
{
// private data as before
…
public:
Student() // default constructor
{
MidtermGrade = FinalGrade = ProjectGrade = 0.0;
}
Student(string theName) // constructor to set Name
{
MidtermGrade = FinalGrade = ProjectGrade = 0.0;
Name = theName;
}
…
}; // done defining and declaring Student class
int main()
{
Student Student1("John Smith");
Student1.SetMidtermGrade(80.0);
Student1.PrintCourseGrade();
}
Classes Without a Default Constructor
• In our original listing (student1.cc), we didn’t have a
constructor. That’s fine, if we have absolutely no
constructors in our code, the compiler will make a basic
default constructor one for us (although it doesn’t initialize
variables, so it’s pretty useless in that regard).
• However, if we do specify any constructors, then the
compiler will not make a default constructor. That means
that if we make a constructor that takes arguments, and we
don’t make a default constructor, then objects can only be
created with the constructor that takes arguments. Syntax
like this won’t work:
Student Student1;
Indeed, in our example, it does no good to track students
who don’t at least have a name associated with their
record; we’ll eliminate our default constructor in our next
version of the Student class.
Variable Names in Setter Methods
• We haven’t mentioned it until now, but notice that the
arguments to all our setter methods have different
names than the class member data we’re trying to set.
Why bother to make the variable names different?
After all, normally we can name function arguments
anything we want! But using a different name is
important…otherwise, we would get nonsense-looking
code like this:
void SetName(string Name)
{
Name = Name;
}
Output:
In copy constructor!
Auditor grades: 80,0,0
Auditor has course grade 26.6667
In copy constructor!
Auditor grades: 100,0,0
Auditor has course grade 33.3333
Importance of const ref in Copy
Constructors
Student(const Student& Source)
{
// copy constructor code goes here
}
int main() {
X* pX = new X();
X::DestroyInstance(pX); // do this instead of delete pX
}
The this Pointer and static Functions
• We saw before in this chapter that the “this”
pointer is implicitly passed to all member
functions of a class and is useful to clarify
whether an operation is on the current class
object or on one passed as a parameter
• “this” is not implicitly passed to member
functions declared as static, since static functions
are shared among all members of a class
• Thus, in static functions, we do not have access to
the “this” pointer
sizeof() a Class
• Just like we can use sizeof() to get the number
of bytes taken up by a plain old data type, we
can also use it on a class
• When used on a class, sizeof() returns the
number of bytes for the basic member data,
not including the size of any dynamically
allocated memory pointed to by the class (and
also not including any space for member
functions)
Friend Functions and Classes
• We’ve seen that member functions and member
data listed as “private” are not accessible outside
the class
• We can make special exceptions using the
“friend” keyword: a friend function can access
private class data, and so can a friend class
• We declare friend functions or classes like this:
Class X
{
private:
friend void FriendlyFunction(…); // this function is a friend
friend class FriendlyClass; // this entire class is a friend
…
};
Friend Function Example
// student7.cc
class Student
{
private:
string Name;
bool Auditor;
double *Grades;
public:
….
// friend declaration gives function outside class access to private bits
friend void PrintStudentGrades(const Student& Input);
}; // done defining and declaring Student class
void PrintStudentGrades(const Student& Input)
{
if (Input.Auditor) cout << "Auditor ";
else cout << "Student ";
cout << Input.Name << " grades: ";
cout << Input.Grades[0] << "," << Input.Grades[1] << "," << Input.Grades[2];
cout << ". Course grade: ";
cout << (Input.Grades[0] + Input.Grades[1] + Input.Grades[2]) / 3.0 << endl;
}
int main()
{
Student Student1("John Smith"); // no 2nd argument given, assumes default
Student1.SetGrade(0, 80.0);
Student Student2("Jane Doe", true);
Student2.SetGrade(0, 100.0);
PrintStudentGrades(Student1);
PrintStudentGrades(Student2);
}
// friend8.cc
Friend Class Example
class Student
{
private:
string Name;
bool Auditor;
double *Grades;
public:
…
// friend declaration gives function outside class access to private bits
friend class PrintHelper;
}; // done defining and declaring Student class
class PrintHelper
{
public:
void PrintStudentGrades(const Student& Input)
{
if (Input.Auditor) cout << "Auditor ";
else cout << "Student ";
cout << Input.Name << " grades: ";
cout << Input.Grades[0] << "," << Input.Grades[1];
cout << "," << Input.Grades[2];
cout << ". Course grade: ";
cout << (Input.Grades[0] + Input.Grades[1] + Input.Grades[2]) / 3.0;
cout << endl;
}
};
int main()
{
Student Student1("John Smith");
Student1.SetGrade(0, 80.0);
PrintHelper X;
X.PrintStudentGrades(Student1);
}
const Member Functions
• In Part 1, we saw various uses of the const keyword: we can
have constants (e.g., const int), constant pointers to
variables (e.g., int* const p), constant pointers to contants
(e.g., const int* const p), or references to constants (int&
const p or equivalently const int& p, which simply means
even though we have a ref we promise not to modify p)
• When dealing with classes, we can also have constant
functions. These are functions that are guaranteed to not
modify the object. They are specified like this:
int MyFunction([args]) const { … }
The const needs to be added to the function declaration
and the definition
• Several of our previous and future examples could have
been modified to specify that the member function was
constant
Chapter 10: Inheritance
• Defining inheritance
• Base classes and derived classes
• Protected members
• Public, Protected, and Private inheritance
• Derived classes hiding base class methods
• Invoking base class methods in derived classes
• Slicing of derived classes
• Multiple inheritance
Inheritance
• In this chapter, we discuss inheritance, which
is a powerful feature of object-oriented
programming
• Inheritance allows us to specify more generic
behavior for some objects, and more specific
behavior for other objects (for example,
squares have all the properties of rectangles,
but they also have additional special
properties as well)
Polygon Class Example
// polygon.cc
#include <iostream>
#include <string>
using namespace std;
class Polygon
{
public:
int NumSides;
void Draw(void) {
cout << "Drawing polygon with " << NumSides << " sides." << endl;
}
Polygon(int sides) {
NumSides = sides;
}
};
int main()
{
Polygon Triangle(3);
Polygon Square(4);
Triangle.Draw();
Square.Draw();
}
Polygon Derived Classes
• You can imagine that it might make sense to create
Triangle, Square, etc., classes that behave as typical
polygons but also know about the special properties of
triangles, squares, etc.
• We can do this through inheritance. We will create
Triangle and Square classes that inherit from the
Polygon class.
• In this example, Polygon is the base class, and Triangle
and Square are the derived classes
• Alternatively, we can call Polygon the superclass, and
we can call Triangle and Square the subclasses
Inheritance Syntax
• We declare and define the Triangle and Square classes
just like we do for any other class; the only difference
inheritance makes is that we add some extra bits to the
class declaration line, like this:
class Triangle: public Polygon
{
…
};
This specifies that class Triangle inherits from class
Polygon (the “public” is an “access specifier” which
could also be “protected” or “private”, but usually it’s
public … we’ll explain this below)
// polygon2.cc
class Polygon
{
public:
int NumSides;
void Draw(void) {
cout << "Drawing polygon with " << NumSides << " sides." << endl;
}
Polygon(int sides) {
NumSides = sides;
}
};
class Square: public Polygon
{
public:
Square() { // problem here … see next page
NumSides = 4;
}
};
class Triangle: public Polygon
{
public:
Triangle() { // problem here … see next page
NumSides = 3;
}
};
int main()
{
Triangle MyTriangle;
Square MySquare;
Polygon MyPentagon(5);
MyTriangle.Draw();
MySquare.Draw();
MyPentagon.Draw();
}
Base Class Initialization
• The listing on the previous page won’t work;
we get an error message like the following:
polygon2.cc: In constructor Square::Square():
polygon2.cc:21: error: no matching function for call to Polygon::Polygon()
polygon2.cc:13: note: candidates are: Polygon::Polygon(int)
polygon2.cc:7: note: Polygon::Polygon(const Polygon&)
polygon2.cc: In constructor Triangle::Triangle():
polygon2.cc:29: error: no matching function for call to Polygon::Polygon()
polygon2.cc:13: note: candidates are: Polygon::Polygon(int)
polygon2.cc:7: note: Polygon::Polygon(const Polygon&)
class Base {
virtual ReturnType MyFunction (params);
};
class Derived {
ReturnType MyFunction (params);
}
// polygon8.cc
class Polygon
{
public:
int NumSides;
virtual void Draw(void) {
cout << "Drawing polygon with " << NumSides << " sides." << endl;
}
Polygon(int sides) {
NumSides = sides;
}
};
class Triangle: public Polygon
{
public:
Triangle(): Polygon(3) {
}
void Draw(void) { // now this will get used
cout << "Using special triangle drawing routine" << endl;
}
};
void DrawPolygon(Polygon& poly) {
poly.Draw();
}
int main()
{
Polygon MyPentagon(5);
Triangle MyTriangle; Program output:
DrawPolygon(MyPentagon); Drawing polygon with 5 sides.
// Now thanks to virtual functions, the correct (more specialized) Using special triangle drawing routine
// version of the Draw() routine will be used, i.e., Triangle::Draw()
DrawPolygon(MyTriangle);
}
The Need for Virtual Destructors
• A more dangerous situation when we need to call
the right function for a derived class is when we
need to call a destructor
• If we are treating an object generically and call
the generic (parent class) destructor, we might
not free up all the memory we’re supposed to ---
a derived class should always be calling its own
destructor
• The next example shows an example of a derived
class (Triangle) destructor not being called
// polygon9.cc
class Polygon Program output:
{ About to create a Triangle on the heap
public:
int NumSides; Creating a Polygon
Polygon(int sides) { Creating a Triangle
cout << "Creating a Polygon" << endl; About to destroy the Triangle on the heap Missing “Destroyed
NumSides = sides;
} Destroyed Polygon Triangle”!
~Polygon() { About to create a Triangle on the stack
cout << "Destroyed Polygon" << endl; Creating a Polygon
}
}; Creating a Triangle
Triangle on stack about to go out of scope
class Triangle: public Polygon Destroyed Triangle
{ Destroyed Polygon
public:
Triangle(): Polygon(3) {
cout << "Creating a Triangle" << endl;
}
~Triangle() { •Recall that when constructing a
cout << "Destroyed Triangle" << endl; derived class, the base class
}
}; constructor is supposed to be called
void DeletePolygon(Polygon* poly) { first, then the derived class
delete poly;
} constructor
int main()
•Destructors should be called in the
{ reverse order of constructors
cout << "About to create a Triangle on the heap" << endl;
Triangle* pTriangle = new Triangle;
cout << "About to destroy the Triangle on the heap" << endl;
DeletePolygon(pTriangle);
cout << "About to create a Triangle on the stack" << endl;
Triangle MyTriangle;
cout << "Triangle on stack about to go out of scope" << endl;
return 0;
}
Virtual Destructors
• We know how to solve this problem now… just
make the destructor of the base class virtual,
like this:
virtual ~Polygon() { … }
• See the next listing for a corrected version of
our example
// polygon9a.cc
class Polygon Program output:
{ About to create a Triangle on the heap
public: Creating a Polygon
int NumSides; Creating a Triangle
Polygon(int sides) { About to destroy the Triangle on the heap
cout << "Creating a Polygon" << endl; Destroyed Triangle Got it now!
NumSides = sides; Destroyed Polygon
}
virtual ~Polygon() { About to create a Triangle on the stack
cout << "Destroyed Polygon" << endl; Creating a Polygon
} Creating a Triangle
}; Triangle on stack about to go out of scope
Destroyed Triangle
class Triangle: public Polygon Destroyed Polygon
{
public:
Triangle(): Polygon(3) {
cout << "Creating a Triangle" << endl;
}
~Triangle() {
cout << "Destroyed Triangle" << endl;
}
};
void DeletePolygon(Polygon* poly) {
delete poly;
}
int main()
{
cout << "About to create a Triangle on the heap" << endl;
Triangle* pTriangle = new Triangle;
cout << "About to destroy the Triangle on the heap" << endl;
DeletePolygon(pTriangle);
cout << "About to create a Triangle on the stack" << endl;
Triangle MyTriangle;
cout << "Triangle on stack about to go out of scope" << endl;
return 0;
}
Always Supply Virtual Destructors
• To avoid potential memory leaks, etc., you
should always supply your base classes virtual
destructors
The Virtual Function Table
• Classes use a “virtual function table” to keep track of
what function they’re supposed to be using in different
contexts
• For each class, this is just a list of function pointers,
pointing to the functions that the class is supposed to
be using (base class functions or derived class
functions)
• This only applies to functions that were declared
virtual in the base class
• This adds a little extra memory overhead (not much) to
classes that have virtual functions and to their derived
classes
Run Time Type Identification (RTTI)
• Occasionally the programmer also wants to know
whether an object of a generic class is really of
that class, or whether it actually belongs to a
derived class
• This can be done using dynamic_cast to see if a
pointer of type Base* is really of type Derived*
• More on this later
• A dynamic cast is sometimes considered a poor
programming practice and a sign that the class
hierarchy wasn’t designed optimally
Pure Virtual Functions and Abstract
Base Classes
• Sometimes we might want to ensure that every derived class has its
own special version of some function (i.e., we will never want to call
a generic version of the function from the base class)
• We can ensure this happens by declaring the base class function to
be “pure virtual”, like this:
class Base
{
public:
virtual void SomeFunction() = 0; // pure virtual
};
• A pure virtual function can’t be used … it’s just a signal that any
derived class must override this function
• A pure virtual function means that the base class can never be
instantiated (because it would have no working definition of this
pure virtual function) --- this makes the base class an Abstract Base
Class (one that can’t be instantiated)
The “Diamond Problem”
• Suppose we have an
Class MobileDevice
inheritance hierarchy like int Memory;
the one on the right
• This can cause a problem in
C++ because the final class
(Phablet) inherits two Class Phone Class Tablet
instances of MobileDevice
(one from Tablet, one from
Phone) --- the compiler
doesn’t know which
instance to access if we Class Phablet
want to get at an inherited
variable like Memory!
// mobile.cc
class MobileDevice
{
public:
int Memory; Program output:
MobileDevice() { About to create a Phablet on the stack
cout << "MobileDevice constructor" << endl;
} MobileDevice constructor
};
Phone constructor
class Phone: public MobileDevice MobileDevice constructor
{
public: Tablet constructor
Phone() {
cout << "Phone constructor" << endl; Phablet constructor
}
};
class Tablet: public MobileDevice
{
•Notice MobileDevice constructor is called
public: twice (once for each parent of Phablet)
Tablet() {
cout << "Tablet constructor" << endl; •That means if we try to do something like
}
}; set MyPhablet.Memory = 32 it will fail
class Phablet: public Phone, public Tablet
because it’s not sure which Memory we
{ mean, the one from Phone or the one
public:
Phablet() { from Tablet!
cout << "Phablet constructor" << endl;
}
};
int main()
{
cout << "About to create a Phablet on the stack" << endl;
Phablet MyPhablet;
// MyPhablet.Memory = 32; // this won't work --- ambiguous reference
}
Solving the Diamond Problem
• We really want just one instance of MobileDevice
associated with Phablet, not two
• We can accomplish this using “virtual inheritance”
• Unfortunately virtual inheritance has nothing to do
with virtual functions
• Virtual inheritance just means we want a common
instance of a base class if we have a diamond problem,
not multiple instances of the base class
• To use virtual inheritance, in the “middle-level” classes
of the diamond, specify their inheritance like this:
class Derived1: public virtual Base { … };
class Derived2: public virtual Base { … };
// mobile2.cc
class MobileDevice Program output:
{ About to create a Phablet on the stack
public: Just one now!
int Memory; MobileDevice constructor
MobileDevice() { Phone constructor
cout << "MobileDevice constructor" << endl;
} Tablet constructor
}; Phablet constructor
class Phone: public virtual MobileDevice
{
public:
Phone() {
cout << "Phone constructor" << endl;
}
};
class Tablet: public virtual MobileDevice
{
public:
Tablet() {
cout << "Tablet constructor" << endl;
}
};
class Phablet: public Phone, public Tablet
{
public:
Phablet() {
cout << "Phablet constructor" << endl;
}
};
int main()
{
cout << "About to create a Phablet on the stack" << endl;
Phablet MyPhablet;
MyPhablet.Memory = 32; // now this works!
}
Virtual Copy Constructors
• One might imagine it would be useful to define virtual
copy constructors, so that derived classes could
implement the copy constructor appropriate to them
(with deep copies, etc., specialized to the derived class)
• Unfortunately constructors are considered a special
type of function that work only for a specified type;
hence, virtual copy constructors are not allowed in C++
• If virtual copy constructors are needed, one can use a
work-around of defining a regular virtual function that
does much the same work as a copy constructor
Chapter 12: Operator Overloading
In Chapter 4, we considered operators; now we
consider how they can be overloaded to work
on classes
• How to overload operators
Increment and Decrement Operators
• Suppose we have a Time class that keeps track
of the hour (0 to 23) and the minute (0 to 59)
• It would be really convenient to be able to
update the time by one minute using a simple
++ operator, like this:
class PrintClass
{
public:
void operator () (string Input) const
{
cout << Input << endl;
}
};
int main()
{
PrintClass myPrintObject;
myPrintObject("Hello, World!");
}
C++11: Move Constructors
• We’ve already discussed the need to have deep copies
when we copy an object that contains dynamically
allocated memory
• However, this copying can degrade performance if it’s
called for objects that only exist temporarily
• For example, an overloaded + or – operator typically
constructs a new object of the current class and returns it --
- but the object goes out of scope when the +/- function is
done, so we wind up invoking a copy constructor (and
deep-copying dynamically allocated memory)
• It would be better if we could “move” the temporary object
about to go out of scope into the object that is now being
created from it, to avoid an otherwise unnecessary deep
copy
Move Constructor Example
Suppose we have a record of class grades for student
John Smith, and we want to see how far above or
below the Class Average John scored on each
assignment; we can make another record for the Class
Averages and then just subtract the two objects using
an overloaded subtraction operator.
int main()
{
ClassGrades Student1("John Smith");
Student1[0] = 99; Student1[1] = 85; Student1[2] = 86;
Student1.PrintGrades();
ClassGrades Avg("Class Average");
Avg[0] = 70; Avg[1] = 72; Avg[2] = 68;
Avg.PrintGrades();
// Compute how far above/below Class Avg were each of Student1's scores
ClassGrades StudentvsAvg("Label-will-be-overwritten");
StudentvsAvg = Student1 - Avg;
StudentvsAvg.PrintGrades();
}
Move Constructor Example
• We can define the subtraction operator this way:
ClassGrades operator- (const ClassGrades& ToSubtract) {
cout << "Subtracting record " << ToSubtract.Name;
cout << " from record " << Name << endl;
string NewName;
NewName = Name + " - " + ToSubtract.Name;
ClassGrades NewClassGrades(NewName);
for (int i=0; i<3; i++) {
NewClassGrades[i] = Grades[i] - ToSubtract.Grades[i];
}
return NewClassGrades;
}
• When we return NewClassGrades it goes out of scope, so in principle the
compiler should call a copy constructor to copy it into a new object in the
calling code (here, Student1 – Avg)
• Some compilers seem to be smart enough to optimize away this extra
copy automatically; if this doesn’t happen, we can use a Move Constructor
Move Constructor Example
ClassGrades(ClassGrades&& src) { // move constructor (C++11 only)
cout << "In Move Constructor moving from " << src.Name << endl;
Name = src.Name;
Grades = src.Grades; // take ownership of arrays
src.Grades = NULL;
}
• The && in the argument distinguishes this constructor as a move
constructor
• We “move” the pointer to the Grades array from being owned by src to
being owned by the current object (“this”)
• We then invalidate the src.Grades pointer in the source object, so that if it
goes out of scope, the memory won’t go away with it (this means we need
to change the destructor to free Grades[] only if the pointer isn’t NULL)
~ClassGrades() {
cout << "In destructor for record " << Name << endl;
if (Grades != NULL) delete [] Grades;
}
Move Assignment Operators
• In our example code we use a copy assignment
operator to copy the result of subtraction into an
existing object:
ClassGrades StudentvsAvg("Label-will-be-overwritten");
StudentvsAvg = Student1 - Avg;
Program output:
•Compiler seems to have In constructor for record John Smith
optimized out the copy Student John Smith has grades: 99 85 86
constructor when we return In constructor for record Class Average
Student Class Average has grades: 70 72 68
from the (-) operator; In constructor for record Label-will-be-overwritten
excellent! Subtracting record Class Average from record John Smith
•But we are still having to do a In constructor for record John Smith - Class Average
In Copy Assignment Operator
deep copy in the copy Copying record John Smith - Class Average into record Label-will-be-overwritten
assignment operator; better to In destructor for record John Smith - Class Average
“move” the tempoarary rvalue Student John Smith - Class Average has grades: 29 13 18
(Student1-Avg) into In destructor for record John Smith - Class Average
In destructor for record Class Average
StudentvsAvg ! In destructor for record John Smith
• If we add the move constructor and move
assignment operators given above (example
listing grades3a.cc), we now get this output
In constructor for record John Smith
Student John Smith has grades: 99 85 86
In constructor for record Class Average
Student Class Average has grades: 70 72 68
In constructor for record Label-will-be-overwritten
Subtracting record Class Average from record John Smith
In constructor for record John Smith - Class Average
In Move Assignment Operator
Moving record John Smith - Class Average into record Label-will-be-overwritten
In destructor for record John Smith - Class Average
Student John Smith - Class Average has grades: 29 13 18
In destructor for record John Smith - Class Average
In destructor for record Class Average
In destructor for record John Smith
• The move assignment operator has replaced the copy assignment operator (and
saved us one deep copy)
• To get the C++ features of grades3a.cc working, we compiled it like this (using g++):
g++ -std=c++11 -o grades3a grades3a.cc
Chapter 13: Casts
A cast is an operation that converts one type to
another, sometimes in a way that is not
strictly safe; use with caution
• C-style casts
• static_cast
• dynamic_cast
• reinterpret_cast
• const_cast
The Reason for Casts
• In well written C++, casts should hardly ever be needed.
However, in C, they were needed somewhat frequently, and
hence they may also be needed in a C++ program if it calls
legacy C code
• For example, C originally did not support Boolean data
types; integers were usually used as stand-ins (0 for false
and 1 for true)
• If we want to call an old C routine from C++, we might need
to convert our data from Boolean types to integers
• Rather than making the programmer do the conversion
manually, we can do it automatically using a cast
• A C-style cast can still be used in C++; the syntax to convert
variable y from some type old_type to a variable x of type
new_type is
new_type x = (new_type) y;
C-Style Casting Example
// c-cast.cc
#include <iostream>
using namespace std;
int main()
{
int i;
bool b_t = true, b_f = false;
i = (int) b_t;
cout << "true casts to int as " << i << endl;
i = (int) b_f;
cout << "false casts to int as " << i << endl;
}
Program output:
true casts to int as 1
false casts to int as 0
Implicit Casts
• It is also possible to forget the cast syntax, and just let the compiler
do the conversion for you. For example,
old_type j = some_value;
new_type i = j;
• This avoids the need to add the cast specifier [e.g., new_type i =
(old_type) j]
• However, since a cast is being performed, it is considered better
form to specify the cast directly as a clue to other programmers
that a conversion is happening (especially because casts should
always be used with caution)
• Implicit casts or C-style casts can effectively convert things that
should be convertible, like between integers and Booleans, double
precision and integers (with roundoff of course), etc.
• They may not work as desired (or at all) for other examples,
especially for user-defined data types (classes)
Implicit Cast Example (not
recommended)
// implicit-cast.cc
#include <iostream>
using namespace std;
i = b_t;
cout << "true casts to int as " << i << endl;
i = b_f;
cout << "false casts to int as " << i << endl;
}
Program output:
true casts to int as 1
false casts to int as 0
Problems with C-Style Casts
• As we’ve just seen, it’s usually considered better to use casting
syntax rather than let the compiler handle it implicitly, as a way of
warning other programmers that a conversion is happening
• On the other hand, C-style casts can be very dangerous because
they can be used to write code that doesn’t really make sense
• For example, the following code won’t work in C++:
char* name = “David”;
int* buf = name; // can’t convert char* to int*
• However, we could force it to execute with a C-style cast:
int* buf = (int *)name;
• But we couldn’t (or at least shouldn’t!) use buf as an integer pointer
to do any integer operations, because the contents aren’t really
meaningful as integers
• The reason C allows such nonsense casts is that in old C, libraries
that dealt with arrays of arbitrary types had to have the pointers
cast to some common pointer type (often char* or, later, void*)
C++ Ways to do Casts
• Given the dangerous nature of C-style casts, C++
introduces several new casting operations; which one
to use depends on the situation
• Unfortunately, the C++ casts aren’t particularly safe
operations, either, and the syntax makes them a bit
more cumbersome to use than the old C-style casts
• The C++ casts are of four types: static_cast,
dynamic_cast, reinterpret_cast, and const_cast
• The syntax for all these has a common form:
new_type x = cast_type <new_type>(y)
• This casts variable y of some old datatype to variable x
of type new_type
static_cast
• static_cast will do explicit type conversion
between basic data types where it makes sense
(e.g., converting a double like 4.184 to an integer
4) and it can convert pointers between related
data types
• The validity of the cast is checked at compile type
(that’s the “static” in static_cast)
• When converting pointers, the cast is valid if the
compiler detects the data types are “related”
(e.g., they are from the same class inheritance
hierarchy)
Base and Derived Class Pointers
• Suppose we have a Base Class (say, Polygon) and a
Derived Class (say, Square)
• If we make a new Square, we can have a pointer to it of
type Square*
• We could just as easily treat this as a pointer of type
Polygon*, since of course a Square is a Polygon
• Hence, we could create a Polygon* pointer to a new
Square like this, with no problems:
Polygon* pPolygon = new Square();
• This does an implicit conversion of Square* to
Polygon*; converting a pointer to a type upwards
(towards parents) in an inheritance hierarchy is called
“upcasting” and is perfectly valid
Base and Derived Class Pointers
• However, going the other way (converting a pointer of a
parent type to a derived type) is called “downcasting”
(because it goes down in the inheritance hierarchy) is not
ok:
Square* pSquare = pPolygon; // error
after all, we don’t know the Polygon is really a square! The
line above won’t compile.
• But what if pPolygon is really a pointer to a square, e.g., if
we got it from an upcast:
Polygon* pPolygon = new Square();
• Then we should be able to convert the Polygon* pointer
back to a Square* because the object pointed to really is a
square! Can do this with static_cast:
Square* pSquare = static_cast<Square*>(pPolygon);
Dangers of static_cast
• So, static_cast gives us a way to explicitly convert basic
data types or to convert pointers for related data types,
like a Polygon* to a Square* when the Polygon* is
really pointing to a Square
• The problem is that static_cast will also allow us to
convert any Polygon* to Square*, even when Polygon*
might have been upcast from some other shape (like
Triangle*) or when Polygon* is simply pointing to a
generic Polygon
• Of course this is bad because if we had a Polygon*
pointing to a Triangle, and then cast it to a Square*, we
would get undefined, nonsensical results if we then
tried to access the Triangle like it was a Square
dynamic_cast
• The problems in using static_cast raised in the
previous slide can be remedied using
dynamic_cast, which means that the cast is
attempted at runtime, when the system can
know whether or not the cast succeeded
• Continuing the previous example, if we tried to
dynamically cast a Polygon* that points to a
Triangle down to a Square*, the cast would fail
(as it should)
• If a cast fails, the pointer resulting from the cast is
set to NULL; we can check for this before
executing code that depends on the cast working
dynamic_cast Example
Polygon* pPolygon = new Triangle();
// try a dynamic downcast
Square* pSquare =
dynamic_cast<Square*>(pPolygon);
int main()
{
MyClass X;
PrintData(X);
}
Casting Summary
• Casting is ok in some situations, like when we need to truncate a
double to an integer
• Casting is necessary in some cases when we want to interact with a
legacy library (that can only deal with bytes of type unsigned char*,
for example)
• Casting a pointer from a derived type to a parent type is called
upcasting and is ok
• Casting a pointer from a parent type to a derived type is called
downcasting and is only ok if the parent type pointer is really
already of the derived type (e.g., we got the parent pointer from an
upcast); dynamic_cast will check this for us and is safe to use as
long as we catch failures by checking if the resulting pointer is NULL
or not
• Usually it is better to use virtual functions than dynamic casts
• Using a const_cast is a bit of a hack but can be acceptable if you
don’t have control over all the code you’re using and you need a
workaround to solve a const mismatch
Chapter 14: Macros, Templates, and
Smart Pointers
• The preprocessor and #define macros
• #ifdef, #ifndef, #endif, and defining symbols (like
–D DEBUG) at compile time
• Using assert() in debugging
• Header guards
• Template functions
• Template classes
• Static member data in template classes
• shared_ptr
• unique_ptr
The C++ Preprocessor
• The preprocessor performs some basic text
substitutions on a source file before handing it
to the compiler
• It inserts the contents of header files included
with #include
• It makes substitutions specified by #define
• #include and #define are called “preprocessor
directives”
#define Macros
• #define allows one to specify shortcuts to
more complicated expressions or to name
constants. Examples:
#define PI 3.1415926
#define SQUARE(y) ((y)*(y))
• These types of uses made sense in the days of
C, but C++ offers better ways to do these
things now
#define constants
• In the days of C, it made much more sense to #define a
constant like #define PI 3.14159 and then use the symbol
PI, instead of literally hard-coding the number 3.14159 over
and over in the code
• After all, it might be important to use the same number of
digits consistently to avoid numerical issues, and we may
decide later we need to add more digits; we could
accomplish that by changing just one definition in the code
• However, with the C++ const keyword, it’s better now to
just define this as a const (which is type-safe):
const double PI 3.14159;
#define macros
• For certain simple operations, it can be convenient to create a
#define macro and use that instead of using a function call
• For example,
#define SQUARE(y) ((y)*(y))
can be used to replace something like SQUARE(42) with ((42)*(42))
• The advantage of the macro is that it will work equally well with
float, double, and int types, and it doesn’t require the overhead of
an explicit function call
• The disadvantage is that it doesn’t protect us if the user tries to
pass it a string, etc.
• If you do use #define to make a macro, you do need all those extra
parentheses: otherwise, unexpected results could occur if we pass
an expression instead of a simple argument
#ifdef, #ifndef, and #endif
• The preprocessor can “turn on” or “turn off”
(really, include or not include) a section of code
based on directives
• To include a section of code if a symbol X has
been defined, do this:
#ifdef X
[code to be included]
#endif
• To include a section of code if a symbol X has not
been defined, do this:
#ifndef X
[code to be included]
#endif
Defining Symbols
• So, #ifdef X will include code up to the
subsequent #endif directive, if symbol X is
defined. How to we define this symbol?
• (1) We can simply define it using a preprocessor
directive like this:
#define X some-value
(like in our previous SQUARE(x) macro example)
• (2) We don’t even need to give the symbol a
value; it’s enough to just say it’s defined, like this:
#define X
• (3) We can also define the symbol at compile
time; C++ compilers allow symbols to be defined
using –D SYMBOL[=VALUE]
Example: Preprocessor Directives for
Optional Debugging Code
• Frequently, we want to run a lot of extra checks when
we are debugging a code, but not necessarily when we
are running it normally (depending on what tests are
done, they might slow down the code)
• We can do this with preprocessor directives! Just wrap
the debug code in #ifdef DEBUG … #endif
• To turn on debugging, add #define DEBUG to the top of
the file (before any instances of #ifdef DEBUG), or even
better, just recompile with a compiler flag –D DEBUG
(or –DDEBUG … the space is optional)
• To turn off debugging, remove any #define DEBUG
directives and recompile without the –D DEBUG flag
-D DEBUG Example
// define_debug.cc
// for g++ compiled with:
// g++ -DDEBUG -o define_debug define_debug.cc
// -DDEBUG is equivalent to the source #define DEBUG
// If we recompile without -DDEBUG then the debug test
// below will not run
#include <iostream>
using namespace std;
int main() {
#ifdef DEBUG
cout << "I'm doing a debug test now." << endl;
#endif
cout << "Hello, world" << endl;
}
Debugging with assert()
• Alternatively, we can also add some debugging checks with
the assert() function (sometimes implemented as
preprocessor macros)
• assert(x) checks whether the expression x evaluates as true
or not, e.g., assert(5>3); if the assertion fails, then the
program prints an error message
• To use, #include <assert.h>
• Not recommended over #define DEBUG sections, which are
more flexible (can do more than check the truth of some
expression) and can be turned on or off (although some
build systems might be smart enough turn off assert() for
release builds)
• Another drawback: assert() can cause a core dump, and the
programmer and/or user might not want the core dump
files cluttering the directory
assert() example
// assert.cc
#include <iostream>
#include <assert.h>
using namespace std;
int main() {
int x=5;
assert (x>7);
cout << "End of program" << endl;
}
Program output:
assert: assert.cc:9: int main(): Assertion `x>7' failed.
Abort (core dumped)
Preprocessor Header Guards
• Another legitimate use of the preprocessor is to create
“header guards”
• Some C++ projects are complicated enough, with some
header files including other header files, that it can be
hard to track whether or not you already have included
all the header files you need
• To avoid unnecessary multiple inclusion of header files
(or even an infinite regression of header files including
each other!), we can use a “header guard”
• The header guard will make use of preprocessor
directives #ifndef and #endif
Header Guard Syntax
File myheader.h:
#ifndef MYHEADER_H
#define MYHEADER_H
[regular contents of header go here]
#endif // end of myheader.h
int main()
{
int p = 2;
int pSquared = Square(p);
cout << p << " squared = " << pSquared << endl;
double q = 0.5;
double qSquared = Square(q);
cout << q << " squared = " << qSquared << endl; Program output:
} 2 squared = 4
0.5 squared = 0.25
Template Classes
• Just like we can use templates to create generic functions,
we can also use templates to create generic classes that
employ different types for member data and/or in their
member functions
• The syntax for specifying a template class is similar to that
for a template function:
template <typename T1[, typename T2, …]>
class MyClass
{ … class definition here, using T1, etc., to substitute for
specific type names where desired…};
• To create a template class, you have to tell it what type(s)
to use in creating it; specify these in <>:
MyClass <int> IntMyClass;
// template_class.cc
#include <iostream>
using namespace std;
template <typename T>
class MyClass
{
private:
T Val;
public:
void SetVal(const T& x) { Val = x; }
const T& GetVal() const { return(Val); }
// function above is const function: does not change obj
// returns const ref, meaning we can't change the value
// even though GetVal() is giving us a ref
};
int main()
{
MyClass <int> I; // specify what type to use with <>
I.SetVal(42);
std::cout << "I holds value " << I.GetVal() << endl;
MyClass <double> D;
D.SetVal(3.1415926);
std::cout << "D holds value " << D.GetVal() << endl; Program output:
} I holds value 42
D holds value 3.14159
Templates with Default Types
• We can specify default datatypes for
templates like this:
template <typename T1=int,
typename T2=int>
class MyClass { … };
• A class could be instantiated like this:
MyClass <> X(1,2); // use default type int
MyClass <double, double> Y(1.2, 3.4);
Static Member Data in
Template Classes
• Static member data in template classes have
to be defined outside the template class, even
if the data type of the static data is fixed and
doesn’t depend on the template
• We accomplish this as follows:
template<typename T[,…]>
StaticMemberType
ClassName<T[,…]>::StaticMemberName;
// template_class_static.cc
#include <iostream>
using namespace std;
// oversimplified template class example where the class doesn't
// even really depend on typename, but the point is to show
// how template classes work with static member data
template <typename T>
class MyClass
{
public:
static int S;
};
// static data has to be declared outside the class
// here's how we do it, even though in this case the
// static data is always an int and doesn't depend on typename
template <typename T> int MyClass<T>::S;
int main()
{
MyClass<int> IntClass;
MyClass<int> IntClass2;
IntClass.S = 1; Program output:
IntClass static value = 1
MyClass<double> DoubleClass; IntClass2 static value = 1
DoubleClass.S = 99;
DoubleClass static value = 3
cout << "IntClass static value = " << IntClass.S << endl;
cout << "IntClass2 static value = " << IntClass2.S << endl;
cout << "DoubleClass static value = " << DoubleClass.S << endl;
}
Smart Pointers
A “smart pointer” is a pointer that automates
destruction of the object it points to; consider
using smart pointers if you want to
dynamically allocate user-defined objects
(classes) with “new”
When to Use Smart Pointers
• Smart pointers are a very useful replacement for regular
pointers when you create a new object using “new”: they
work like regular pointers, but when no pointer points to
the object anymore, the object is automatically cleaned up
with “delete”
• This is extremely helpful because it removes the need for
the programmer to track when to delete the object (or who
“owns” the object and is responsible for cleaning it up)
• They are not necessarily helpful if the programmer needs
an object just within the scope of one function: there may
be no need to create the object using new, or else it may be
very simple to just call delete at the end of the function
std::shared_ptr
• A “shared pointer” is a smart pointer that tracks
all references to the object; when the reference
count gets to 0, the object is no longer needed
and is automatically deleted. This type of pointer
is called a “reference counted” pointer
• Originally standardized in the Boost library
(www.boost.org), a popular place for helpful C++
libraries; eventually moved into the standard
library as of C++11 (may need to compile with
C++11 flags like this: g++ -std=c++11)
• Need to #include <memory> to use it
std::shared_ptr
• Create a shared_ptr smart pointer pointing to an object
of type X using this syntax:
int main()
{
unique_ptr<int> p(new int(5)); // make a smart ptr to an int holding val 5
//unique_ptr<int> q = p; // compile error can't make copy of unique_ptr
unique_ptr<int> q = std::move(p); // Transfer ownership of p to q
}
Chapter 15: Exception Handling
• Introduction to exception handling
• try and catch
• The std::exception class
• Custom exceptions derived from
std::exception
Introduction to Exceptions
• An “exception” is just a (hopefully) unusual
situation that might cause problems for a
program: an attempt to allocate more memory
than we have, trying to read too many lines from
a file, trying to divide by zero, etc.
• C++ provides a mechanism to try to catch these
types of errors in a generic sort of way
• If we “handle” these sorts of exceptions, we
make our code “exception safe”
Bad Allocation Example
• Suppose we want to create an array of 10
integers, but we have a typo and we accidentally
request allocation of -10 integers:
int *p = new int[-10];
• This makes no sense, and the allocation will fail,
as demonstrated in the following example
program
• A more common example of failed memory
allocation would be if we ask for too much
memory
// bad_alloc.cc
#include <iostream>
using namespace std;
int main() {
int* p = new int[-10];
p[0] = 42;
delete[] p;
}
Program output:
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Abort (core dumped)
Handling the Exception
• The previous program crashes messily and we get
a core dump (creating a potentially large file with
a name like core.17820; the core file might be
useful to the programmer for debugging
purposes, but the user is just going to see it as
clutter
• We can avoid the core dump if we “handle” the
exception using the try/catch keywords:
try { [some code to execute] }
catch (catch-type) { [do this upon exception]}
// bad_alloc2.cc
#include <iostream>
using namespace std;
int main() {
try {
int* p = new int[-10];
p[0] = 42;
delete[] p;
}
catch (...) {
cout << "Some exception occurred" << endl;
}
}
Program output:
Some exception occurred
Discussion of try/catch Example
• We put the code that might cause an exception into a try{}
block
• If an exception occurs within this block, then immediately
upon the exception, the program skips to the code
specified by the catch{} block
• The core dump is avoided because the exception is caught
• Immediately after the catch keyword, the type of exception
to catch is specified in parentheses; ellipses (…) say to
catch all possible exceptions
• This try/catch handling of the exception saves us from the
core dump, but the error message is actually less
informative than what we got before! Need less generic
handling of the exception.
More Specific Catch Statements
• We can specify multiple catch{} statements after a try{},
with code appropriate to handle each of the types of
exception that might have occurred in the try{} block. For
example,
try { }
catch (exception_type_1) { … }
catch (exception_type_2) { … }
catch (…) { … } // all remaining exception types
int main() {
try {
int* p = new int[-10];
p[0] = 42;
delete[] p;
}
catch (std::bad_alloc& ex) {
cout << "Bad allocation occurred: " << ex.what() << endl;
}
catch (std::exception& ex) {
cout << "Some standard exception occurred: " << ex.what() << endl;
}
catch (...) {
cout << "Some exception occurred" << endl;
}
}
Program output:
Bad allocation occurred: std::bad_alloc
Catch Statements in the Example
• In the previous listing, we first try to catch
exceptions of the type std::bad_alloc
(corresponding to a failed memory allocation,
which is the case here)
• If that hadn’t worked, we then would have
tried exceptions of the type std::exception
(which would be virtually all of them)
• If that somehow still didn’t work, we would
catch all remaining exceptions with (…)
Common Types of Exceptions
The std::exception class is a base class for multiple types of
exceptions, including:
• bad_alloc: An attempt to create new memory using the
new keyword has failed
• bad_cast: An attempt to do a dynamic cast has failed
because objects are not of the appropriate type for a
dynamic cast
• ios_base::failure: Problems in functions from the iostream
library
Because bad_alloc is of type std::exception, in our previous
listing, either of the first two catch() statements would have
been a match; but the first match found is executed and the
others are ignored
Creating an Exception with throw
• The “throw” keyword does the opposite of
catch: instead of handling an exception, it
creates an exception
• You can “throw” an exception of any type, so
long as there is a “catch” that knows how to
catch that type (usually derived from
std::exception, but technically could also be a
simple string or an integer)
Creating a Custom Exception Class
• You might want to handle your own types of
exceptions that aren’t defined already in C++
• To do this, create a custom exception class; you will
probably want to derive from std::exception, because
then all existing handlers for that type will also work
for your exception
• Alternatively, you could also inherit from
std::logic_error (error related to the program logic) or
std::runtime_error (error detected during runtime),
which both inherit from std::exception
• With this exception class available, you can then throw
an object of this type
Custom Exception Class Example
// custom_exception.cc
#include <exception>
using namespace std;
public:
MyException(const char *msg):Message(msg) {}
virtual const char* what() const throw() { return Message.c_str(); }
~MyException() throw() {}
};
Custom Exception Class Explanation
• The std::exception class defines a virtual function what() that
should return an error message of what kind of exception occurred
• This function is normally defined in the class like this:
virtual const char* what() const throw() {…}
• The “const” after what() just says the function doesn’t change the
class
• The “throw()” right before the definition just means that this
function itself is not supposed to throw an exception (otherwise we
would be throwing an exception while handling another exception,
not a good situation); anything in parentheses would indicate some
type that the function could throw
• We also need to promise the exception class destructor will not
throw, either, and that’s done with this:
~MyException() throw() { } // don’t really need to define further
Custom Exception Class in Action
(using the custom exception class MyClass() defined previously)
double SafeSqrt(double x) {
if (x < 0.0)
throw MyException("MyException: Tried to take sqrt of negative number");
return sqrt(x);
}
int main() {
double x = -4.0, y;
try {
y = SafeSqrt(x);
cout << "The square root of " << x << " is " << y << endl;
}
catch (std::exception& ex) { // any std::exception incl. ours!
cout << ex.what() << endl;
}
}
Program output:
MyException: Tried to take sqrt of negative number
Uncaught Exceptions
• What if we throw an exception that we don’t catch?
• Then we’re back in the same situation we were with
our first exception example, bad_alloc.cc : we get an
error message, and possibly a core dump, but the
program terminates and we are unable to proceed
• If we handle (catch) the exception, we at least have the
option of doing something and then letting the
program proceed
• The next listing shows what happens if we throw an
exception using MyException but don’t catch it
// from listing custom_exception2.cc
// MyException still defined as previously
double SafeSqrt(double x) {
if (x < 0.0)
throw MyException("MyException: Tried to take sqrt of negative number");
return sqrt(x);
}
Program output:
terminate called after throwing an instance of 'MyException'
what(): MyException: Tried to take sqrt of negative number
Abort (core dumped)
Which catch will Catch?
• Suppose we have an exception thrown inside multiple
try{}/catch{} statements
• The throw statement will send the thrown exception
“up the chain” (from the current, innermost try{}
statement, outwards to enveloping try{} statements)
until an appropriate catch{} is encountered
• The thrown exception will also percolate up from inner
function calls to outer function calls (eventually all the
way to main()) in search of an appropriate catch, so
long as it remains within some try{} block
// from custom_exception3.cc
double SafeSqrt(double x) {
if (x < 0.0)
throw MyException("MyException: Tried to take sqrt of negative number");
return sqrt(x);
}
void ComputeRoots() {
try {
cout << "Square root of 4 is " << SafeSqrt(4.0) << endl; Exception caught in this
cout << "Square root of -4 is " << SafeSqrt(-4.0) << endl;
“inner” function, not in
}
catch (MyException& ex) { main()
cout << "Caught a sqrt exception in ComputeRoots()" << endl;
}
}
int main() {
try {
ComputeRoots();
}
catch (std::exception& ex) { // any std::exception incl. ours!
cout << "Caught an exception in main()" << endl;
cout << ex.what() << endl;
} Program output:
} Square root of 4 is 2
Caught a sqrt exception in ComputeRoots()
// from custom_exception4.cc
double SafeSqrt(double x) {
if (x < 0.0)
throw MyException("MyException: Tried to take sqrt of negative number");
return sqrt(x);
}