Part D - Inheritance
Derived Classes and Resources
Define the copy constructor for derived classes with resources
Define the assignment operator for derived classes with resources
Identify the copy constructor and assignment operator defaults for a derived class
"If you use pointers, you have to think about resource management" (Stroustrup, 1997)
Constructor and Destructors
| Copy Constructor
| Assignment Operator
| Summary
| Exercises
A derived class that accesses a resource requires logic to allocate memory for the
resource, copy the resource and deallocate its memory. Without explicit definitions of the
constructor(s), copy constructor, assignment operator and destructor, the compiler
inserts the shallow copy defaults, which only copy the resource's address,
without copying the resource itself.
This chapter describes how to define the constructors, the assignment operator and the
destructor of a derived class that accesses a resource and how to ensure that the
appropriate counterpart in the base class is executed in each case.
Constructors and Destructor
A derived class' constructors require explicit calls to
the corresponding constructors of the base class. That is, when we
define a constructor for a derived class, our definition needs to call the
appropriate constructor of the base class. Without a call to any base class
constructor, the compiler inserts a call to the no-argument constructor of the base-class.
On the other had, a derived class' destructor does not require a call to the
base class' destructor. This is because each class has but one destructor
and the compiler inserts into the derived class' destructor a call to the base
class' destructor. That is, there no variety of destructors from which to choose.
For example, let us upgrade the definition of our Student
class to accommodate an indefinite number of grades. We store the grades
in dynamic memory and their address in a resource instance pointer. The
steps are:
- allocate dynamic memory for the C-style string received by the
derived class' constructor and store that memory's address
in the grade resource instance pointer
- copy the string stored in the address received by the
derived class constructor's parameter to the newly allocated dynamic memory
- deallocate the previously allocated dynamic memory immediately before the derived
part of the object goes out of scope
The header file for our Student class contains a resource
instance pointer:
// Student.h
#include <iostream>
const int N = 30;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
class Student : public Person {
int no;
char* grade;
public:
Student();
Student(const char*, int, const char*);
~Student();
void display(std::ostream&) const;
};
|
The implementation file allocates and deallocate memory for the resource copies:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Person::Person(const char* nm) {
std::strncpy(person, nm, N);
person[N] = '\0';
}
Parson::Person() { }
void Person::display(std::ostream& os) const {
os << person << ' ';
}
Student::Student() {
no = 0;
grade = nullptr;
}
Student::Student(const char* nm, int n, const char* g) : Person(nm) {
// see Current Object chapter for validation logic
no = n;
if (g != nullptr) {
grade = new char[std::strlen(g) + 1];
std::strcpy(grade, g);
}
else
grade = nullptr;
}
Student::~Student() {
delete [] grade;
}
void Student::display(std::ostream& os) const {
Person::display(os);
os << no << ' ' << (grade != nullptr ? grade : "");
}
|
The check in Student::display()
handles the safe empty state where the object is empty and no memory has been
allocated.
The following client program uses this implementation to produce the
result shown on the right:
// Derived Class with a Resource
// drvdResrce.cpp
#include <iostream>
#include "Student.h"
int main() {
Person jane("Jane");
Student harry("Harry", 975, "ABBAD");
harry.display(std::cout);
std::cout << std::endl;
jane.display(std::cout);
std::cout << std::endl;
}
|
Harry 975 ABBAD
Jane
|
Copy Constructor
A client invokes a derived class' copy constructor whenever it copies an
existing object into a new object.
The derived class' copy constructor, before executing the logic
within its own body, calls one of the base class' constructors. In defining
a derived class' copy constructor, we need to call the base class'
copy constructor. Without an explicit call to the base class' copy
constructor, the compiler inserts a call to the base-class' no-argument
constructor.
The definition of the copy constructor for a derived class takes the form
Derived(const Derived& identifier) : Base(argument) {
// ...
}
The sole parameter receives an unmodifiable reference to an
object of the derived class. The argument in the call
to the base class' constructor is the identifier received in
this parameter.
The copying occurs in two distinct stages and four steps altogether:
- copy the base class part of the existing object
- allocate memory for the instance variables of the base class in
the order of their declaration
- execute the base class' copy constructor
- copy the derived class part of the existing object
- allocate memory for the instance variables of the derived class
in the order of their declaration
- execute the derived class' copy constructor
For example, in the following code, the Person
copy constructor is the default copy constructor inserted
by the compiler. The copying steps are:
- shallow copy the Person part of the existing object
- allocate static memory for person in the base class part of the newly created object
- copy into person the string at address
src.person
- copy the Student part of the existing object
- allocate static memory for no
and *grade in the
derived part of the newly created object
- copy src.no into no
- allocate dynamic memory for a copy of src.grade
- copy into grade the string at src.grade
The header file declares the copy constructor:
// Student.h
#include <iostream>
const int N = 30;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
class Student : public Person {
int no;
char* grade;
public:
Student();
Student(const char*, int, const char*);
Student(const Student&);
~Student();
void display(std::ostream&) const;
};
|
The copy constructor for the derived class allocates memory for the
resource copy:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Person::Person(const char* nm) {
std::strncpy(person, nm, N);
person[N] = '\0';
}
Person::~Person() {}
void Person::display(std::ostream& os) const {
os << person << ' ';
}
Student::Student() {
no = 0;
grade = nullptr;
}
Student::Student(const char* nm, int n, const char* g) : Person(nm) {
// see Current Object chapter for validation logic
no = n;
if (g != nullptr) {
grade = new char[std::strlen(g) + 1];
std::strcpy(grade, g);
}
else
grade = nullptr;
}
Student::Student(const Student& src) : Person(src) {
no = src.no;
if (src.grade != nullptr) {
grade = new char[std::strlen(src.grade) + 1];
std::strcpy(grade, src.grade);
}
else
grade = nullptr;
}
Student::~Student() {
delete [] grade;
}
void Student::display(std::ostream& os) const {
Person::display(os);
os << no << ' ' << (grade != nullptr ? grade : "");
}
|
The Person copy
constructor executes before the Student copy constructor executes. Since we
have not defined a copy constructor for the base class,
the compiler inserts one that performs a shallow copy.
The following client program uses this implementation to produce the
result shown on the right:
// Derived Class with a Resource
// drvdResrce.cpp
#include <iostream>
#include "Student.h"
int main() {
Student harry("Harry", 975, "ABBAD"),
harry_ = harry; // calls copy constructor
harry.display(std::cout);
std::cout << std::endl;
harry_.display(std::cout);
std::cout << std::endl;
}
|
Harry 975 ABBAD
Harry 975 ABBAD
|
Assignment Operator
A client invokes the assignment operator of a derived class when it copies
an existing object into an existing object. A derived class' assignment
operator is independent of the base class' assignment operator. When
we define an assignment operator for a derived class, we need to call the base
class' assignment operator to assign the base class part of the source object
to the base class part of the recipient object. Without such a call, the
compiler will not insert any call to the base class.
We call the base class' assignment operator from within the body of the
derived class' assignment operator (unlike the copy constructor).
That is, the base class' assignment operator executes within the scope of
the derived class' assignment operator.
We can call the base-class assignment operator in either of two ways:
- a functional expression
- an assignment expression to an object of base class type
as the left operand
The functional expression takes the form
Base::operator=(identifier);
The assignment expression takes the form
Base& base = *this;
base = identifier;
Base is the name of the base class and
identifier refers to the right operand,
which is the source object for the assignment. While the
address of the derived object is the same as the address of the base
class part of that object, the compiler distinguishes between a
call to the base class' operator and a call to the derived class'
operator by the type of the left operand.
The header file declares a private member function and the assignment operator:
// Student.h
#include <iostream>
const int N = 30;
class Person {
char person[N+1];
public:
Person();
Person(const char*);
~Person();
void display(std::ostream&) const;
};
class Student : public Person {
int no;
char* grade;
void init(int, const char*);
public:
Student();
Student(const char*, int, const char*);
Student(const Student&);
~Student();
Student& operator=(const Student& src);
void display(std::ostream&) const;
};
|
The implementation file defines the assignment operator and a private init()
function, which contains the copying logic shared by the constructors and assignment
operator. Change to the previous version of the constructors are highlighted:
// Student.cpp
#include <cstring>
#include "Student.h"
Person::Person() {
person[0] = '\0';
}
Person::Person(const char* nm) {
std::strncpy(person, nm, N);
person[N] = '\0';
}
Person::~Person() {}
void Person::display(std::ostream& os) const {
os << person << ' ';
}
Student::Student() {
no = 0;
grade = nullptr;
}
void Student::init(int n, const char* g) {
no = n;
if (g != nullptr) {
grade = new char[std::strlen(g) + 1];
std::strcpy(grade, g);
}
else
grade = nullptr;
}
Student::Student(const char* nm, int n, const char* g) : Person(nm) {
// see Current Object chapter for validation logic
grade = nullptr;
init(n, g);
}
Student::Student(const Student& src) : Person(src) {
grade = nullptr;
init(src.no, src.grade);
}
Student::~Student() {
delete [] grade;
}
Student& Student::operator=(const Student& src) {
if (this != &src) {
// Base class assignment
// 1 - functional expression
// Person::operator=(src);
// 2 - assignment expression
Person& person = *this; // only copies address
person = src; // call base class operator
init(src.no, src.grade);
}
return *this;
}
void Student::display(std::ostream& os) const {
Person::display(os);
os << no << ' ' << (grade != nullptr ? grade : "");
}
|
This is one way of coding the construction and assignment member
functions for the derived class - sharing a private
member function. This design avoids a duplicate call to the base
class assignment operator, which would occur if the copy constructor
were to call the assignment operator as in the preceding example.
The following client program uses this implementation to produce the
result shown on the right:
// Derived Class with a Resource Assignment
// drvdAssign.cpp
#include <iostream>
#include "Student.h"
int main() {
Student student("Harry", 975, "ABBAD"), backup;
student.display(std::cout);
std::cout << std::endl;
backup = student;
backup.display(std::cout);
std::cout << std::endl;
}
|
Harry 975 ABBAD
Harry 975 ABBAD
|
Summary
- a derived class with a resource requires explicit definitions of its
copy constructor, assignment operator and destructor
- an explicit definition of a derived
class' copy constructor without a call to the base class' copy
constructor calls the base class' no-argument constructor
-
the derived class' copy constructor executes the base class' copy
constructor first
-
an explicit definition of a derived
class' assignment operator without an explicit call to the base class' assignment operator
does NOT call the base class' assignment operator.
-
the derived class' assignment operator executes the base class' assignment
operator entirely within the scope of the derived class' assignment operator
- the destructor of a derived class automatically calls the destructor of the
base class
Exercises
|