[C++] Chapter 13: Class Inheritance

Chapter 13 Class Inheritance

One of the main purposes of object-oriented programming is to provide reusable code. You can achieve this by using library functions, but a better solution is to achieve this by designing classes. The benefits of this approach are:

  • Functionality can be added to existing classes.
  • Data members can be added to a class.
  • The behavior of class methods can be modified.

13.1 A simple base class

Table(const string &);
Table("zhangsan");

The formal parameter of the constructor is written as const string&, but const char* is passed in during instantiation, which leads to a type mismatch, but the sting class has a constructor that accepts const char*, and it will automatically call this constructor first. Initialize the string object, and then initialize it.

13.1.1 Deriving a class

class A : public B(){}

Here A inherits from B, and B is a public base class. Using public derivation, the public members of the base class will become public members of the derived class; the private parts of the base class will also become part of the derived class, but only through the base class public and protected method access.

  • The derived class object stores the data members of the base class;
  • Derived class objects can use the methods of the base class;

What needs to be added to the derived class?

  • Derived classes need their own constructor;
  • Derived classes can add additional data members and member functions as needed;

13.1.2 Constructors: Access Rights Considerations

Constructor of derived class:

RatedPlayer::RatedPlayer(int r,const string & fn,const string & ln,bool ht):TableTennisPlayer(fn,ln,ht){
    rating = r;
}

Written this way is the most standard. The base class object must be created first. If the base class constructor is not called, the program will use the default base class constructor, so the above code is equivalent to the following:

RatedPlayer::RatedPlayer(int r,const string & fn,const string & ln,bool ht)//:TableTennisPlayer()
{
    rating = r;
}//除非要使用默认构造函数,否则应显示调用正确的基类构造函数

insert image description here

The constructor of the derived class always calls a base class constructor, and the initializer list syntax can be used to specify the base class constructor to be used, otherwise the default base class constructor will be used. When the derived class expires, the program will first call the derived class destructor, and then call the base class destructor.

13.1.3 Using derived classes

13.1.4 Special relationship between derived and base classes

There are some special relationships between derived classes and base classes. One of them is that derived class objects can use base class methods, provided the methods are not private. Two other important relationships are: Base class pointers can refer to derived class objects without explicit type conversion.

However, base class pointers or references can only be used to call base class methods. C++ requires reference and pointer types to match the assigned type, but this rule is an exception for inheritance, but this exception is only one-way, and base class objects and addresses cannot be assigned to derived class references and pointers.

The reference compatibility property also enables you to initialize a base class object to a derived class object.

RatedPlayer olaf(1840,"olaf","Loaf",true);//派生类
TableTennisPlayer olaf1(olaf);//基类

To do so, the prototype of the matching constructor is as follows:

TableTennisPlayer(const RatedPlayer &);

Obviously, there is a high probability that such a constructor will not be defined in the class definition, but there is an implicit copy constructor:

TableTennisPlayer(const TableTennisPlayer &);

In other words, it initializes olaf1 to a TableTennisPlayer object nested within olaf.

Similarly, you can also assign derived objects to base class objects:

RatedPlayer olaf(1840,"olaf","Loaf",true);//派生类
TableTennisPlayer olaf1;//基类
olaf1 = olaf;

In this case, the implicitly overloaded assignment operator is invoked:

TableTennisPlayer & operator=(const TableTennisPlayer &) const;

The base class reference points to the derived class object, so the base class part of olaf is copied to olaf1.

13.2 Inheritance: the is-a relationship

There are three types of inheritance in C++: public inheritance, protected inheritance, and private inheritance. Public inheritance is a commonly used method, which establishes an is-a relationship (actually is a kind of).

13.3 Polymorphic public inheritance

Two implementation mechanisms of polymorphic public inheritance:

  • Redefine method of base class in derived class
  • use virtual method

In a derived class method, the standard technique is to use the scope resolution operator to call the base class method.

void BrassPlus :: ViewAcct()const{
    ViewAcct();//这里如果不使用作用域解析运算符,将会调用自己,这样创建的就是一个不会终止的递归函数了
}

Why do we need a virtual destructor?

The code that uses delete to free an object allocated by new illustrates why a base class should contain a virtual destructor, even though it sometimes seems unnecessary.

If the destructor is not virtual, only the destructor corresponding to the pointer type will be called. This means that only the Brass (base class) destructor is called, even if the pointer points to a BrassPlus (derived class) object. If the destructor is virtual, the destructor for the corresponding object type will be called.

Therefore, if the pointer points to a BrassPlus object, the destructor of BrassPlus will be called, and then the destructor of the base class will be called automatically. Therefore, using a virtual destructor ensures that the correct sequence of destructors is called. So it is recommended that base classes should have a virtual destructor, even if that destructor does nothing.

13.4 Static and dynamic binding

Interpreting a function call in source code as executing a specific block of function code is called function name binding. The binding performed during the compilation process is called static binding (static binding), also known as early binding. However, virtual functions make this job more difficult, because which function to use cannot be determined at compile time, because the compiler does not know which type of object the user will choose, so the compiler must generate When selecting the correct virtual method code, this is called dynamic binding (dynamic binding), also known as late binding.

13.4.1 Compatibility of pointer and reference types

Converting a derived class reference or pointer to a base class reference or pointer is known as an upcast, which makes explicit type conversion unnecessary for public inheritance. The upward cast is transitive, that is, if the BressPlusPlus class is derived from the BrassPlus class, the Brass (base class) pointer or reference can refer to Brass objects, BrassPlus objects, and BrassPlusPlus objects.

Converting a base class pointer or reference to a derived class pointer or reference is called downcasting, and downcasting is not allowed without explicit type conversion.

insert image description here

Implicit upcasting allows a base class pointer or reference to either a base class object or a derived class object, thus requiring dynamic binding. C++ uses virtual member functions to meet this need.

13.4.2 Virtual member functions and dynamic binding

The compiler uses static binding for non-virtual methods and dynamic binding for virtual methods.

  • Why are there two types of bindings?
  • Since dynamic binding is so good, why not make it the default?
  • How does dynamic binding work?

Why are there two types of bindings and why is the default static?

There are two reasons: efficiency and conceptual model.

Since static binding is more efficient, it is set as the default choice for C++. One of the guiding principles of C++ is that you don't pay for features you don't use. Use virtual functions only when your programming really requires them. If the method of the base class is redefined in the derived class, it is set as a virtual method, otherwise, it is set as a non-virtual method.

How virtual functions work

Usually, the way the compiler handles virtual functions is to add a hidden member to each object. A pointer to an array of function addresses is stored in the hidden member. This array is called a virtual function table (virtual function table, vtbl).

The address of the virtual function declared for the class object is stored in the virtual function table. For example, a base class object contains a pointer to a table of addresses of all virtual functions in the base class. The derived class object will contain a pointer to a separate address table. If the derived class provides a new definition of the virtual function, the virtual function table will save the address of the new function; if the derived class does not redefine the virtual function, the vtbl will save the address of the original version of the function. If a derived class defines a new virtual function, the address of that function will also be added to the vtbl. Note that no matter whether the class contains 1 or 10 virtual functions, only 1 address member needs to be added to the object, only the size of the table is different.

insert image description here

When calling a virtual function, the program will look at the vtbl address stored in the object, and then turn to the corresponding function address table. If the first virtual function defined in the class declaration is used, the program will use the first function address in the array and execute the function with that address. If you use the third virtual function in the class declaration, the program will use the function whose address is the third element in the array.

In summary, when using virtual functions, there are certain costs in terms of memory and execution speed, including:

  • Each object will grow by the amount of space to store the address;
  • For each class, the compiler creates a virtual function address table (array);
  • For each function call, an additional operation needs to be performed, which is to look up the address in the table.

Although the efficiency of non-virtual functions is slightly higher than that of virtual functions, they do not have the function of dynamic linking.

13.4.3 Notes on virtual functions

  • Using the keyword virtual in the declaration of a base class method makes the method virtual in the base class and all derived classes (including classes derived from derived classes).
  • If a virtual method is invoked using a reference or pointer to an object, the program uses the method defined for the object type instead of the method defined for the reference or pointer type. This is called dynamic binding or late binding. This behavior is important because then base class pointers or references can point to derived class objects.
  • If the defined class is to be used as a base class, those class methods that are to be redefined in derived classes should be declared virtual.

For virtual methods, you need to know some other knowledge, some of which have already been introduced. Let's take a look at these contents.

1. Constructor

Constructors cannot be virtual functions. When a derived class object is created, the constructor of the derived class is called instead of the constructor of the base class, and then the constructor of the derived class uses a constructor of the base class, which is different from the inheritance mechanism. Therefore, derived classes do not inherit base class constructors, so declaring class constructors as virtual doesn't make sense.

2. Destructor

Destructors should be virtual unless the class is not used as a base class. By the way, it's not an error to define a virtual destructor for a class, even if the class isn't used as a base class; it's just a matter of efficiency.

3. Tomomoto

Friends cannot be virtual functions, because friends are not class members, and only members can be virtual functions. If a design problem arises because of this, it can be solved by making friend functions use virtual member functions.

4. No redefinition

If the derived class does not redefine the function, the base class version of the function will be used. If the derived class is in the derivation chain, the latest virtual function version will be used, except that the base class version is hidden (described later).

5. Redefining will hide the method

class Dwelling{
    public:
    virtual void showperks(int a) const;
}
class Hovel : public Dwelling{
    public:
    virtual void showperks() const;
}

The new definition defines showperks( ) as a function that takes no arguments. The redefinition doesn't generate two overloaded versions of the function, but instead hides the base class version that takes one int parameter. In conclusion, redefining an inherited method is not overloading. If a function is redefined in a derived class, instead of overriding the base class declaration with the same function signature, the base class method of the same name is hidden, regardless of the parameter signature.

This leads to two rules of thumb: first, if you redefine an inherited method, it should be exactly the same as the original prototype, but if the return type is a reference or pointer to the base class, you can modify it to be a reference or pointer to the derived class ( This exception is new). This feature is known as covariance of return type (covariance of return type), because the return type is allowed to vary with the class type: note that this exception only applies to return values, not parameters.

class Dwelling{
    public:
    virtual Dwelling & build(int n);
}
class Hovel : public Dwelling{
    public:
    virtual Hovel & build (int n);
}

Second, if the base class declaration is overloaded, all base class versions should be redefined in derived classes. If only one version is redefined, the other two will be hidden and derived class objects will not be able to use them. Note that the new definition can just call the base class version if no modification is required.

13.5 Access control: protected

The keyword protected is similar to private, and only public class members can be used outside the class to access class members in the protected part. The distinction between private and protected is only manifested in classes derived from a base class. Members of a derived class can directly access protected members of the base class, but not private members of the base class. Thus, to the outside world, protected members behave like private members; but to derived classes, protected members behave like public members.

13.6 Abstract base class (ABC)

C++ provides unimplemented functions through the use of pure virtual functions. =0 at the end of a pure virtual function declaration. When a class declaration contains pure virtual functions, objects of that class cannot be created. To be a true ABC, it must contain at least one pure virtual function. C++ even allows the definition of pure virtual functions.

In short, use =0 in the prototype to indicate that the class is an abstract base class, and the function may not be defined in the class.

13.6.1 Applying the ABC concept

13.6.2 ABC concept

Think of ABC as an interface that must be implemented. ABC requires concrete derived classes to override their pure virtual functions - forcing derived classes to follow the interface rules set by ABC.

13.7 Inheritance and Dynamic Memory Allocation

13.7.1 Case 1: The derived class does not use new

Dynamic memory allocation is used in the base class, but the declaration includes the special methods needed when the constructor uses new: a destructor, a copy constructor, and an overloaded assignment operator.

But new is not used in its derived classes. It also does not cover some other less commonly used design features that require special handling:

Do I need to define an explicit destructor, copy constructor and assignment operator for the lackDMA class? unnecessary.

First, see if a destructor is needed. If no destructor is defined, the compiler will define a default constructor that does nothing. In fact, the default constructor of a derived class always has to do something: call the base class destructor after executing its own code. Since we assume that lackDMA members do not need to perform any special operations, a default destructor is appropriate.

Next, let's look at the copy constructor. As introduced in Chapter 12, the default copy constructor performs member copying, which is inappropriate for dynamic memory allocation but is appropriate for the new lacksDMA member. So only consider the inherited baseDMA object. Be aware that member copying will use the corresponding copy method according to the data type, so copying a long into a long is done by using a normal assignment; but when copying a class member or an inherited class component, you use the copy construction of the class function completed. So, the default copy constructor of the lacksDMA class uses the explicit baseDMA copy constructor to copy the baseDMA part of the lacksDMA object. Therefore, the default copy constructor is appropriate for the new lacksDMA member as well as for the inherited baseDMA object.

The same is true for assignments. The class's default assignment operator will automatically use the base class's assignment operator to assign to base class components. Therefore, the default assignment operator is also suitable.

These properties of derived class objects also apply to class members that are themselves objects. For example, Chapter 10 introduced that when implementing the Stock class, you can use string objects instead of char arrays to store company names. The standard string class, like the String class created earlier in this book, also uses dynamic memory allocation. Now the reader knows why this doesn't cause problems. Stock's default copy constructor will use string's copy constructor to copy the object's company member; Stock's default assignment operator will use string's assignment operator to assign the object's company member; and Stock's destructor (default or other destructor) will automatically call the string's destructor.

13.7.2 The second case: the derived class uses new

In this case, an explicit destructor, copy constructor, and assignment operator must be defined for the derived class.

The derived class destructor automatically calls the destructor of the base class, so its own responsibility is to clean up the work performed by the derived class constructor. Therefore, the hasDMA destructor must free the memory managed by the pointer style, and relies on the baseDMA destructor to free the memory managed by the pointer label.

Next look at the copy constructor. BaseDMA's copy constructor follows the usual pattern for char arrays, i.e. use strlen( ) to know the space required to store a C-style string, allocate enough memory (number of characters plus 1 byte required to store a null character ) and use the function strcpy( ) to copy the original string to the destination:

The hasDMA copy constructor can only access hasDMA's data, so it must call the baseDMA copy constructor to handle the shared baseDMA data.

One thing to note is that the member initializer list passes a hasDMA reference to the baseDMA constructor. There is no baseDMA constructor with parameter type referenced by hasDMA, nor is there any need for such a constructor. Because the copy constructor baseDMA has a baseDMA reference parameter, and a base class reference can point to a derived type. Therefore, the baseDMA copy constructor will use the baseDMA portion of the hasDMA parameter to construct the baseDMA portion of the new object.

Next, let's look at the assignment operator.

Since hasDMA also uses dynamic memory allocation, it also needs an explicit assignment operator. As a method of hasDMA, it can only directly access the data of hasDMA. However, the explicit assignment operator of the derived class must be responsible for the assignment of all inherited baseDMA base class objects, which can be done by explicitly calling the base class assignment operator

baseDMA::operator=(hs);

But by using function notation instead of operator notation, you can use scope resolution operators. In fact, the meaning of the statement is as follows:

*this = hs;

Of course the compiler will ignore the comment, so when using the following code, the compiler will use hasDMA::operator=( ), thus forming a recursive call. Use function notation so that the assignment operator is invoked correctly.

In short, when both the base class and the derived class use dynamic memory allocation, the destructor, copy constructor, and assignment operator of the derived class must use the corresponding base class method to process the base class elements. This requirement is met in three different ways. For destructors, this is done automatically; for constructors, this is done by calling the base class's copy constructor in the initialization member list; if you don't, the base class's default constructor will be called automatically. For assignment operators, this is done by explicitly calling the base class's assignment operator using the scope resolution operator.

13.7.3 Inheritance example using dynamic memory allocation and friends

Because friends are not member functions, you cannot use the scope resolution operator to figure out which function to use. The solution to this problem is to use a cast so that the correct function is selected when matching the prototype.

13.8 Class Design Review

The copy constructor is used in the following cases:

  • Initialize the new object as a homogeneous object;
  • pass an object to a function by value;
  • Functions return objects by value;
  • The compiler generates temporary objects.

Using explicit disallows implicit conversions, but still allows explicit conversions.

C++11 supports the keyword explicit for conversion functions. Like constructors, explicit allows explicit conversions using casts, but not implicit conversions.

When inheritance uses virtual functions, functions that are defined to accept base class reference parameters can accept derived classes.

13.8.1 Considerations for public inheritance

1. is-a relationship

In some cases, the best approach may be to create an abstract class with pure virtual functions and derive other classes from it.

Remember that one of the ways to express an is-a relationship is that a base class pointer can point to a derived class object, and a base class reference can refer to a derived class object, without explicit type conversion.

2. What cannot be inherited

Constructors cannot be inherited, that is, when creating an object of a derived class, the constructor of the derived class must be called. However, derived class constructors typically use member initializer list syntax to call base class constructors to create the base class portion of the derived object. If a derived class constructor does not explicitly call a base class constructor using member initializer list syntax, the base class' default constructor is used.

Destructors are also not inherited. However, when releasing an object, the program will first call the destructor of the derived class, and then call the destructor of the base class. If the base class has a default destructor, the compiler will generate a default destructor for the derived class. Usually, for a base class, its destructor should be made virtual.

3. Assignment operator

If the object is of a derived class, the compiler will use the base class assignment operator to handle the assignment of the base class portion of the derived object.

If the class constructor uses new to initialize the pointer, you need to provide an explicit assignment operator. Because for the base class part of the derived object, C++ will use the assignment operator of the base class, so there is no need to redefine the assignment operator for the derived class.

If a derived class uses new, an explicit assignment operator must be provided. Assignment operators must be provided for every member of the class, not just new members. In summary, it is possible to assign derived objects to base class objects, but only for members of the base class. A derived class reference cannot automatically refer to a base class object.

In summary, the answer to the question "Is it possible to assign a base class object to a derived object?" is "maybe". If the derived class contains a constructor that defines the conversion of a base class object to a derived class object, then the base class object can be assigned to the derived object. This can also be done if the derived class defines an assignment operator for assigning an object of the base class to the derived object. If neither of the above conditions is true, you cannot do so, except with an explicit cast.

4. Friend function

Since friend functions are not class members, they cannot be inherited. However, you may want friend functions of derived classes to be able to use friend functions of base classes. To do this, you can convert derived class references or pointers to base class references or pointers through mandatory type conversion, and then use the converted pointers or references to call base class friend functions. You can also use the dynamic_cast<> operator discussed in Chapter 15 to perform casts.

5. Instructions for using base class methods

Objects of a publicly derived class can use methods of the base class in a number of ways.

  • The derived class object automatically uses the inherited base class method, if the derived class does not redefine the method.
  • The constructor of the derived class automatically calls the constructor of the base class.
  • Constructors of derived classes automatically call the default constructor of the base class if no other constructor is specified in the member initialization list.
  • A derived class constructor explicitly calls the base class constructor specified in the member initializer list.
  • Derived class methods can use scope resolution operators to call public and protected base class methods.
  • The metafunction of the derived class can convert the derived class reference or pointer to the base class reference or pointer through forced type conversion, and then use the reference or pointer to call the friend function of the base class.

13.9 Summary

Inheritance enables programming code to be modified as needed by using existing classes (base classes) to define new classes (derived classes). Public inheritance establishes an is-a relationship, which means that derived class objects should also be some kind of base class object. As part of the is-a model, derived classes inherit the data members and most of the methods of the base class, but not the constructors, destructors, and assignment operators of the base class. The derived class can directly access the public members and protected members of the base class, and can access the private members of the base class through the public methods and protected methods of the base class. You can add new data members and methods in the derived class, and you can use the derived class as a base class for further development. Each derived class must have its own constructor. When a program creates an object of a derived class, it will first call the constructor of the base class, and then call the constructor of the derived class; when the program deletes an object, it will first call the destructor of the derived class, and then call the destructor of the base class.

If you are using a class as a base class, you can declare members as protected instead of private so that derived classes will have direct access to those members. However, using private members generally reduces the possibility of programming problems. If you want the derived class to be able to redefine the method of the base class, you can use the keyword virtual to declare it as virtual. In this way, objects accessed through pointers or references can be processed according to the type of the object instead of the type of the reference or pointer. Specifically, base class destructors should generally be virtual.

Consider defining an ABC: only the interface is defined, not the implementation. For example, you can define an abstract class Shape and then use it to derive concrete shape classes such as Circle and Square. ABC must contain at least one pure virtual method, which can be declared by adding =0 before the semicolon in the declaration.

It is not necessary to define a pure virtual method. For a class that contains a pure virtual member, it cannot be used to create an object. Pure virtual methods are used to define the common interface of derived classes.

Guess you like

Origin blog.csdn.net/weixin_43717839/article/details/130608144
Recommended