Inheritance, part 4

Introduction

- Constructors/destructors require special handling with classes that are inherited or nested

- Constructors/destructors also have a unique order of execution with classes in inheritance hierarchies

- Let’s begin by looking closely at class objects that are included in other classes (nested classes)

 

Nested classes
- Class contains other class objects as members

- We’ll look at handling nested class objects under two different conditions

- Nested class has a constructor(s) that

  1. Does not require arguments (or has default argument values)
  2. Does require arguments (and argument values)

 

 

Case ANested class member has a constructor(s) that does not require arguments

- Constructor of nested class object is called automatically

- Constructor called upon instantiation of enclosing class object (class containing the nested object)

- No special handling is required on the part of the class developer

- Consider an example of an enclosing gallery class containing nested image class objects

 

#include <iostream>

using namespace std;

 

// Class to store a two-dimensional image

class image

{

private:

  char *data;   // color data

  int size;     // size of color data

  int wd;       // width

  int ht;       // height

 

public:

 

  // Constructor - no arguments

  image();

 

  // Destructor

  ~image();

};

 

// Constructor - no arguments

image::image()

{

  cout << "Image constructor called" << endl;

  wd = 0;

  ht = 0;

  size = 0;

  data = NULL;

}

 

// Destructor

image::~image()

{

  if( data )

    delete[] data;

}

 

// Class to store several images

class gallery

{

private:

  image picasso;        // nested object

  image vangogh;        // nested object

 

public:

 

  // Constructor

  gallery()

  {

    cout << "Gallery constructor called" << endl;

  }

};

 

main()

{

  gallery krannert;

}

 

 

Output

Image constructor called

Image constructor called

Gallery constructor called

 

 

- Note the image objects as nested members of gallery class
- Note image constructors called automatically upon gallery instantiation
- No special handling required by class developer

- Note member constructors called before enclosing class constructor

- Same conditions if nested class constructors have arguments with default values

- Below we’ve modified our image constructor to take arguments with default values

- We’ve also added a name variable for the image class

 

#include <iostream>

using namespace std;

 

// Class to store a two-dimensional image

class image

{

private:

  char *data;   // color data

  int size;     // size of color data

  int wd;       // width

  int ht;       // height

  string name;  // name

 

public:

 

  // Constructor - arguments w/ default values

  image(int w=100, int h=100, string n="None");

 

  // Destructor

  ~image();

};

 

// Constructor - arguments w/ default values

image::image(int w, int h, string n )

{

  wd = w;

  ht = h;

  name = n;

  size = wd * ht;

  data = size ? new char[ size ] : NULL;

  cout << "Image constructor named " << name << " called" << endl;

}

 

// Destructor

image::~image()

{

  if( data )

    delete[] data;

}

 

// Class to store several images

class gallery

{

private:

  image picasso;        // nested object

  image vangogh;        // nested object

 

public:

 

  // Constructor

  gallery()

  {

    cout << "Gallery constructor called" << endl;

  }

};

 

main()

{

  gallery krannert;

}

 

 

Output

Image constructor named None called

Image constructor named None called

Gallery constructor called

 

 

- As in the previous example, no special handling is required for nested constructors

- Constructors for each member object are called automatically with default argument values

- Let’s look an example when the nested class has constructors without default argument values

 

 

Case BNested class member has a constructor(s) that do require argument values
- Constructor of nested class object is not called automatically
- The compiler cannot determine what argument values to send to the constructor

- Additional work on the part of the enclosing class developer is required

- Consider our same example and remove the default values on the image constructor

 

#include <iostream>

using namespace std;

 

// Class to store a two-dimensional image

class image

{

private:

  char *data;   // color data

  int size;     // size of color data

  int wd;       // width

  int ht;       // height

  string name;  // name

 

public:

 

  // Constructor - arguments requiring values

  image(int w, int h, string n);

 

  // Destructor

  ~image();

};

 

// Constructor - arguments w/ default values

image::image(int w, int h, string n )

{

  wd = w;

  ht = h;

  name = n;

  size = wd * ht;

  data = size ? new char[ size ] : NULL;

  cout << "Image constructor named " << name << " called" << endl;

}

 

// Destructor

image::~image()

{

  if( data )

    delete[] data;

}

 

// Class to store several images

class gallery

{

private:

  image picasso;        // nested object

  image vangogh;        // nested object

 

public:

 

  // Constructor

  gallery()

  {

    cout << "Gallery constructor called" << endl;

  }

};

 

main()

{

  gallery krannert;

}

 

 

Output

Gallery2.C: In constructor `gallery::gallery()':

Gallery2.C:55: error: no matching function for call to `image::image()'

Gallery2.C:9: error: candidates are: image::image(const image&)

Gallery2.C:28: error:                 image::image(int, int,

   std::basic_string<char, std::char_traits<char>, std::allocator<char> >)

Gallery2.C:55: error: no matching function for call to `image::image()'

Gallery2.C:9: error: candidates are: image::image(const image&)

Gallery2.C:28: error:                 image::image(int, int,

   std::basic_string<char, std::char_traits<char>, std::allocator<char> >)

 

 

- Note that this example will not compile, note the compiler output error

- The compiler reports that it cannot find a constructor without arguments (or default values)

- The only constructor available is one that requires argument values

- As a result, it reports the error and halts further compilation

 

- To correct this problem, constructor argument values must to be provided for the image class

- To accomplish this, we use a special mechanism on what is called the initialization list

- Using the same example, we make a change to the enclosing class constructor

 

#include <iostream>

using namespace std;

 

// Class to store a two-dimensional image

class image

{

private:

  char *data;   // color data

  int size;     // size of color data

  int wd;       // width

  int ht;       // height

  string name;  // name

 

public:

 

  // Constructor - arguments requiring values

  image(int w, int h, string n);

 

  // Destructor

  ~image();

};

 

// Constructor - arguments w/ default values

image::image(int w, int h, string n )

{

  wd = w;

  ht = h;

  name = n;

  size = wd * ht;

  data = size ? new char[ size ] : NULL;

  cout << "Image constructor named " << name << " called" << endl;

}

 

// Destructor

image::~image()

{

  if( data )

    delete[] data;

}

 

// Class to store several images

class gallery

{

private:

  image picasso;        // nested object

  image vangogh;        // nested object

 

public:

 

  // Constructor with initialization list

  gallery()

    : picasso(100,100,"picasso"), vangogh(200,200,"vangogh")

  {

    cout << "Gallery constructor called" << endl;

  }

};

 

main()

{

  gallery krannert;

}

 

 

Output

Image constructor named picasso called

Image constructor named vangogh called

Gallery constructor called

 

 

- Note the syntax immediately before the gallery constructor brace

 

gallery()

    : picasso(100,100,"picasso"), vangogh(200,200,"vangogh")

 

- The line after the name and before the brace is referred to as the initialization list

- After a leading colon, class members can be initialized in this manner

- Note that the actual name of the member variable is used for the constructor call (picasso, vangogh)

- Note the order in which the member object constructors are called (picasso, then vangogh)

- This is the order in which the objects are listed in the declaration, not the order in the initialization list

- Order for destructors is opposite (enclosing class destructor, then member destructors)

- This method of initialization can be used for any member of a class (not just class objects)

- Instead of initializing in the body of the method, it can be done using the initialization list

- Below, we initialize image members directly on the initialization list (rather than the constructor body)

- This method is considered more efficient instead of initializing members in the body of the constructor

- Note that as with class objects, the names of the variables are used in the initialization list

 

#include <iostream>

using namespace std;

 

// Class to store a two-dimensional image

class image

{

private:

  char *data;   // color data

  int size;     // size of color data

  int wd;       // width

  int ht;       // height

  string name;  // name

 

public:

 

  // Constructor

  image(int w, int h, string n);

 

  // Destructor

  ~image();

};

 

// Constructor - initializing on initialization list

image::image(int w, int h, string n )

  :wd(w), ht(h), name(n)

{

  size = wd * ht;

  data = size ? new char[ size ] : NULL;

  cout << "Image constructor named " << name << " called" << endl;

}

 

// Destructor

image::~image()

{

  if( data )

    delete[] data;

}

 

main()

{

  image picasso(100,100,"picasso");

  image vangogh(200,200,"vangogh");

}

 

 

Output

Image constructor named picasso called

Image constructor named vangogh called

 

 

- Note in our previous gallery example, that argument values are “hard-coded” (actual values passed in list)

- Instead, variables can be passed from the enclosing constructor to the member constructors

- To accommodate this, we’ll re-write our previous example with variables instead of values

- Note that we pass variables from the enclosing constructor to the member constructors

- This allows the application developer to initialize the members of the gallery class

 

 

#include <iostream>

using namespace std;

 

// Class to store a two-dimensional image

class image

{

private:

  char *data;   // color data

  int size;     // size of color data

  int wd;       // width

  int ht;       // height

  string name;  // name

 

public:

 

  // Constructor - arguments requiring values

  image(int w, int h, string n);

 

  // Destructor

  ~image();

};

 

// Constructor

image::image(int w, int h, string n )

  :wd(w),ht(h),name(n)

{

  size = wd * ht;

  data = size ? new char[ size ] : NULL;

  cout << "Image constructor named " << name << " called" << endl;

}

 

// Destructor

image::~image()

{

  if( data )

    delete[] data;

}

 

// Class to store several images

class gallery

{

private:

  image picasso;        // nested object

  image vangogh;        // nested object

 

public:

 

  // Constructor with initialization list

  gallery(int w1,int h1,string n1,int w2,int h2,string n2)

    : picasso(w1,h1,n1), vangogh(w2,h2,n2)

  {

    cout << "Gallery constructor called" << endl;

  }

};

 

main()

{

  gallery krannert(100,100,"picasso",200,200,"vangogh");

}

 

 

 

 

Inherited classes

- Unique conditions also exist for classes derived from other classes (inherited)

- We’ll look at handling inherited class objects under two different conditions

- Base class has a constructor(s) that

  1. Does not require arguments
  2. Does require arguments



Case A Base class has a constructor(s) that does not require arguments

- Constructor of base class object is called automatically

- Constructor called upon instantiation of derived class object

- No special handling is required on the part of the class developer

- Consider an example of a square class derived from a rectangle class

 

#include <iostream>

using namespace std;

 

// Rectangle class

class rectangle

{

protected:

  float wd, ht;

  string name;

 

public:

 

  // Constructor - no arguments

  rectangle();

 

  // Access

  void setWidth(float w) { wd = w; }

  void setHeight(float h) { ht = h; }

};

 

// Constructor - no arguments

rectangle::rectangle()

  :wd(0.0),ht(0.0),name(“r1”)

{

  cout << "Rectangle constructor called, " << name << endl;

}

 

// Derived square class (type of rectangle)

class square : public rectangle

{

public:

 

  // Constructor

  square( float sz )

  {

    cout << "Square constructor called, " << name << endl;

 

    // equal length sides

    wd = ht = sz;

  }

};

 

main()

{

  square s(2.0);

}

 

 

Output

Rectangle constructor called, r1

Square constructor called, r1

 

 

- Note the rectangle class constructor does not require arguments

- (we use our new method to initialize members of the rectangle class)

- Note the base rectangle constructor is called before the derived square constructor

- With derived objects, constructors from the top of the hierarchy down are called in order

- Constructors from parent classes can initialize their members in a constructor

- Derived class constructors, therefore, don’t need to initialize parent class members

- Note, for example, that the name variable (in the base) was set in the derived object above

- The square class constructor did not need to initialize this parent member

- Same conditions are true if we add arguments with default values to a base constructor

- Below we’ve added arguments with default values to our base rectangle constructor

 

#include <iostream>

using namespace std;

 

// Rectangle class

class rectangle

{

protected:

  float wd, ht;

  string name;

 

public:

 

  // Constructor - arguments with default values

  rectangle(float w=4,float h=2,string n="r1");

 

  // Access

  void setWidth(float w) { wd = w; }

  void setHeight(float h) { ht = h; }

};

 

// Constructor – arguments with default values

rectangle::rectangle(float w,int h,string n)

  :wd(w),ht(h),name(n)

{

  cout << "Rectangle constructor called, " << name << endl;

}

 

// Derived square class (type of rectangle)

class square : public rectangle

{

public:

 

  // Constructor

  square( float sz )

  {

    cout << "Square constructor called, " << name << endl;

 

    // equal length sides

    wd = ht = sz;

  }

};

 

main()

{

  square s(2.0);

}

 

 

Output

Rectangle constructor called, r1

Square constructor called, r1

 

 

- As in the previous example, no special handling is required in derived class constructors

- Base constructors are called automatically with default argument values

- Let’s look an example when the base class has constructors without default argument values

 

 

Case BBase class has a constructor(s) that does require argument values
- Constructor of base class is not called automatically
- The compiler cannot determine what argument values to send to the constructor

- Additional work on the part of the derived class developer is required

- Consider our same example and remove the default values on the rectangle constructor

 

#include <iostream>

using namespace std;

 

// Rectangle class

class rectangle

{

protected:

  float wd, ht;

  string name;

 

public:

 

  // Constructor - arguments without default values

  rectangle(float w,float h,string n);

 

  // Access

  void setWidth(float w) { wd = w; }

  void setHeight(float h) { ht = h; }

};

 

// Constructor – arguments without default values

rectangle::rectangle(float w,float h,string n)

  :wd(w),ht(h),name(n)

{

  cout << "Rectangle constructor called, " << name << endl;

}

 

// Derived square class (type of rectangle)

class square : public rectangle

{

public:

 

  // Constructor

  square( float sz )

  {

    cout << "Square constructor called, " << name << endl;

 

    // equal length sides

    wd = ht = sz;

  }

};

 

main()

{

  square s(2.0);

}

 

 

Output

Rectangle2.C: In constructor `square::square(float)':

Rectangle2.C:37: error: no matching function for call to `rectangle::rectangle(

   )'

Rectangle2.C:8: error: candidates are: rectangle::rectangle(const rectangle&)

Rectangle2.C:25: error:                 rectangle::rectangle(int, int,

   std::basic_string<char, std::char_traits<char>, std::allocator<char> >)

 

 

- Note that this example will not compile, note the compiler output error

- The compiler reports that it cannot find a constructor without arguments (or default values)

- The only constructor available is one that requires argument values

- As a result, it reports the error and halts further compilation

 

- To correct this problem, constructor argument values must to be provided for the rectangle class

- To accomplish this, we use the initialization list to explicitly call the base constructor

- Using the same example, we make a change to the derived class constructor

 

#include <iostream>

using namespace std;

 

// Rectangle class

class rectangle

{

protected:

  float wd, ht;

  string name;

 

public:

 

  // Constructor - arguments without default values

  rectangle(float w,float h,string n);

 

  // Access

  void setWidth(float w) { wd = w; }

  void setHeight(float h) { ht = h; }

};

 

// Constructor - arguments without default values

rectangle::rectangle(float w,float h,string n)

  :wd(w),ht(h),name(n)

{

  cout << "Rectangle constructor called, " << name << endl;

}

 

// Derived square class (type of rectangle)

class square : public rectangle

{

public:

 

  // Constructor – initialization list

  square( float sz )

    : rectangle( sz, sz, "r1" )

  {

    cout << "Square constructor called, " << name << endl;

 

    // equal length sides

    wd = ht = sz;

  }

};

 

main()

{

  square s(2.0);

}

 

 

Output

Rectangle constructor called, r1

Square constructor called, r1

 

 

- Note the syntax immediately before the square constructor brace

 

square( float sz )

    : rectangle( sz, sz, "r1" )

 

- The initialization list is used to pass argument values to the base rectangle constructor

- Note that the actual name of the class is used for the constructor call (rectangle) in the call

- This is different from nested objects where the name of the nested object variable is used

- Note again that base constructors are called before derived constructors

- Order for destructors is opposite (derived class destructor, then base destructors up the hierarchy)

- If derived class has nested objects and base classes requiring constructor argument values, order is

  1. Base class constructor
  2. Nested object constructor
  3. Constructor of derived class

- Order for destructors in this condition is opposite

  1. Destructor of derived class
  2. Nested object destructors
  3. Base class destructor

 
 

Virtual Destructors

- When using abstract classes and dynamic binding, it’s important to destroy objects properly

- Destructors in class hierarchies should be virtual to ensure dynamic binding upon destruction

- This way, if objects are used with base class pointers, the correct destructor will be called

- Let’s add destructors to our example above and use base class pointers in a test application

 

#include <iostream>

using namespace std;

 

// Rectangle class

class rectangle

{

protected:

  float wd, ht;

  string name;

 

public:

 

  // Constructor - arguments without default values

  rectangle(float w,float h,string n);

 

  // Destructor

  ~rectangle() { cout << "Rectangle destructor" << endl; }

 

  // Access

  void setWidth(float w) { wd = w; }

  void setHeight(float h) { ht = h; }

};

 

// Constructor - arguments without default values

rectangle::rectangle(float w,float h,string n)

  :wd(w),ht(h),name(n)

{

}

 

// Derived square class (type of rectangle)

class square : public rectangle

{

public:

 

  // Constructor

  square( float sz )

    : rectangle( sz, sz, "r1" )

  {

    // equal length sides

    wd = ht = sz;

  }

 

  // Destructor

  ~square() { cout << "Square destructor" << endl; }

};

 

main()

{

  rectangle *s = new square( 2.0 );

 

  delete s;

}

 

 

Output

Rectangle destructor

 

 

- Note that we have not made our base class destructor virtual

- As a result, dynamic binding does not work properly when our object is destroyed

- The only destructor called in the example is the one from the base rectangle class

- We need both the destructor for the base class and the destructor for the derived class called

- If we had dynamic memory allocated in the square class, it would never get freed!

- To correct our problem, we simply make our base class method virtual

- This ensures dynamic binding, and as a result, both destructors will be called

 

#include <iostream>

using namespace std;

 

// Rectangle class

class rectangle

{

protected:

  float wd, ht;

  string name;

 

public:

 

  // Constructor - arguments without default values

  rectangle(float w,float h,string n);

 

  // Virtual Destructor

  virtual ~rectangle() { cout << "Rectangle destructor" << endl; }

 

  // Access

  void setWidth(float w) { wd = w; }

  void setHeight(float h) { ht = h; }

};

 

// Constructor - arguments without default values

rectangle::rectangle(float w,float h,string n)

  :wd(w),ht(h),name(n)

{

}

 

// Derived square class (type of rectangle)

class square : public rectangle

{

public:

 

  // Constructor

  square( float sz )

    : rectangle( sz, sz, "r1" )

  {

    // equal length sides

    wd = ht = sz;

  }

 

  // Destructor

  ~square() { cout << "Square destructor" << endl; }

};

 

main()

{

  rectangle *s = new square( 2.0 );

 

  delete s;

}

 

 

Output

Square destructor

Rectangle destructor