1. Using a Derived Object to Initialize or Assign a Base Object
Because there is a conversion from reference to derived to reference to base, these copy-control members can be used to initialize or assign a base object from a derived object:Item_base item; // object of base type
Bulk_item bulk; // object of derived type
// ok: uses Item_base::Item_base(const Item_base&) constructor
Item_base item(bulk); // bulk is "sliced down" to its Item_base portion
// ok: calls Item_base::operator=(const Item_base&)
item = bulk; // bulk is "sliced down" to its Item_base portion
When we call the Item_base copy constructor or assignment operator on an object of type Bulk_item , the following steps happen:
- The Bulk_item object is converted to a reference to Item_base, which means only that an Item_base reference is bound to the Bulk_item object.
- That reference is passed as an argument to the copy constructor or assignment operator.
- Those operators use the Item_base part of Bulk_item to initialize and assign, respectively, the members of the Item_base on which the constructor or assignment was called.
- Once the operator completes, the object is an Item_base . It contains a copy of the Item_base part of the Bulk_item from which it was initialized or assigned, but the Bulk_item parts of the argument are ignored.
2. Defining a Default Constructor
Item_base(const std::string &book = "", double sales_price = 0.0): |
This constructor uses the constructor initializer list to initialize its min_qty and discount members. The constructor initializer also implicitly invokes the Item_base default constructor to initialize its base-class part. The effect of running this constructor is that first the Item_base part is initialized using the Item_base default constructor. That constructor sets isbn to the empty string and price to zero. After the Item_base constructor finishes, the members of the Bulk_item part are initialized, and the (empty) body of the constructor is executed.
The constructor initializer list supplies initial values for a class’ base class and members. It does not specify the order in which those initializations are done. The base class is initialized first and then the members of the derived class are initialized in the order in which they are declared.
A class may initialize only its own immediate base class. An immediate base class is the class named in the derivation list.
3. Defining a Derived Copy Constructor
If a derived class explicitly defines its own copy constructor or assignment operator, that definition completely overrides the
defaults. The copy constructor and assignment operator for inherited classes are responsible for copying or assigning their base-
class components as well as the members in the class itself.class Base { /* ... */ };
class Derived: public Base {
public:
// Base::Base(const Base&) not invoked automatically
Derived(const Derived& d):
Base(d) /* other member initialization */ { /*... */ }
};
The initializer Base(d) converts the derived object, d , to a reference to its base part and invokes the base-class copy constructor. Had the initializer for the base class been omitted,Derived(const Derived& d) /* derived member initizations */
{/* ... */ }
the effect would be to run the Base default constructor to initialize the base part of the object. Assuming that the initialization of the Derived members copied the corresponding elements from d , then the newly constructed object would be oddly configured: Its Base part would hold default values, while its Derived members would be copies of another object.
4. Derived-Class Assignment Operator
If the derived class defines its own assignment operator, then that operator must assign the base part explicitly:// Base::operator=(const Base&) not invoked automatically
Derived &Derived::operator=(const Derived &rhs)
{
if (this != &rhs) {
Base::operator=(rhs); // assigns the base part
// do whatever needed to clean up the old value in the derived part
// assign the members from the derived
}
return *this;
}
5. Derived-Class Destructor
The destructor works differently from the copy constructor and assignment operator: The derived destructor is never responsible for destroying the members of its base objects. The compiler always implicitly invokes the destructor for the base part of a derived object. Objects are destroyed in the opposite order from which they are constructed: The derived destructor is run first, and then the base-class destructors are invoked, walking back up the inheritance hierarchy.
The root class of an inheritance hierarchy should define a virtual destructor even if the destructor has no work to do.
6. Virtuals in Constructors and Destructors
A derived object is constructed by first running a base-class constructor to initialize the base part of the object. While the base-class constructor is executing, the derived part of the object is uninitialized. In effect, the object is not yet a derived object.
When a derived object is destroyed, its derived part is destroyed first, and then its base parts are destroyed in the reverse order of how they were constructed.
In both cases, while a constructor or destructor is running, the object is incomplete. To accommodate this incompleteness, the compiler treats the object as if its type changes during construction or destruction. Inside a base-class constructor or destructor, a derived object is treated as if it were an object of the base type.
If a virtual is called from inside a constructor or destructor, then the version that is run is the one defined for the type of the constructor or destructor itself.
7. Name Collisions and Inheritance
A derived-class member with the same name as a member of the base class hides direct access to the base-class member. We can access a hidden base-class member by using the scope operator. The derived-class member hides the base-class member within the scope of the derived class. The base member is hidden, even if the prototypes of the functions differ :struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int); // hides memfcn in the base
};
Derived d; Base b;
b.memfcn(); // calls Base::memfcn
d.memfcn(10); // calls Derived::memfcn
d.memfcn(); // error: memfcn with no arguments is hidden
d.Base::memfcn(); // ok: calls Base::memfcn
8. Name Lookup and Inheritance
Understanding how function calls are resolved is crucial to understanding inheritance hierarchies in C++. The following four steps are followed:
- Start by determining the static type of the object, reference, or pointer through which the function is called.
- Look for the function in that class. If it is not found, look in the immediate base class and continue up the chain of classes until either the function is found or the last class is searched. If the name is not found in the class or its enclosing base classes, then the call is in error.
- Once the name is found, do normal type-checking to see if this call is legal given the definition that was found.
- Assuming the call is legal, the compiler generates code. If the function is virtual and the call is through a reference or pointer, then the compiler generates code to determine which version to run based on the dynamic type of the object. Otherwise, the compiler generates code to call the function directly.