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:

  1. copy the base class part of the existing object
    1. allocate memory for the instance variables of the base class in the order of their declaration
    2. execute the base class' copy constructor
  2. copy the derived class part of the existing object
    1. allocate memory for the instance variables of the derived class in the order of their declaration
    2. 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:

  1. 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
  2. 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




Previous Reading  Previous: Functions of a Derived Class Next: Overview of Polymorphism   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License