Re-learning C++ notes (11) class inheritance


One of the main purposes of object-oriented is to provide reusable code. Class inheritance can derive a new class from an existing class, and the derived class inherits the characteristics of the original class (base class). Just as inheriting a property is easier than starting from scratch, classes derived through integration are usually much easier than designing new classes. Some work can be done by inheriting classes.

  • Functions can be added on the basis of existing classes. For example, for the array class, you can add mathematical operations.
  • You can add data to the class. For example, for the string class, you can derive a class and add data that specifies the display color of the string.
  • You can modify the behavior of class methods.

Therefore, if the purchased class library only provides the header files and compiled code of the class method, you can still use the classes in the library to derive new classes.

1. A simple base class

The following is the base class for marking whether someone has a table.

my_class.h code

#ifndef MY_CLASS_H
#define MY_CLASS_H
#include<string>
using namespace std;

class TableTennisPlayer
{
    
    
private:
    string firstName;
    string lastName;
    bool hasTable;
public:
    TableTennisPlayer(const string& fn = "none",
                      const string& ln = "none", bool ht = false);
    void ShowName()const;
    bool HasTable()const{
    
    return hasTable;};
    void ResetTable(bool v){
    
    hasTable = v;};
};
#endif // MY_CLASS_H

my_class.cpp code

#include"my_class.h"
#include<iostream>
TableTennisPlayer::TableTennisPlayer(const string& fn,
                                     const string&ln,bool ht):firstName(fn),lastName(ln),
    hasTable(ht){
    
    }
void TableTennisPlayer::ShowName()const
{
    
    
    std::cout<<lastName<<", "<<firstName;
}

Main function code:

#include<iostream>
#include"my_class.h"
int main()
{
    
    
    TableTennisPlayer player1("Chuck", "Blizzard", true);
    player1.ShowName();
    if(player1.HasTable())
        cout<<": has a table.\n";
    else
        cout<<": hasn't a table.\n";
}

Output:

Blizzard, Chuck: has a table.

1.1 Deriving a class

Let us give an example of public derivation. Public derived classes cannot directly access the private members of the base class, but must be accessed through the base class method. When creating a derived class object, the program first creates a base class object.

We create a public derived class:

class RatedPlayer:public TableTennisPlayer
{
    
    
private:
    unsigned int rating;
public:
    RatedPlayer(unsigned int r = 0, const string& fn = "none",
                const string& ln = "none", bool ht = false);
    RatedPlayer(unsigned int r, const TableTennisPlayer& tp);
    unsigned int Rating()const{
    
    return rating;}
    void ResetRating(unsigned int r){
    
    rating = r;};
};

Implement the first derived class constructor:

//先基类对象TableTennisPlayer,后RatedPlayer的对象创建
RatedPlayer::RatedPlayer(unsigned int r, const string &fn, const string &ln, bool ht)
    :TableTennisPlayer(fn, ln, ht)
{
    
    
    rating = r;
}

The above is to call the base class constructor of TableTennisPlayer(fn, ln, ht) first. If TableTennisplayer is omitted, the default constructor of the base class will be called by default (because the constructor of the base class must be called first!).

Continue to implement the second constructor:

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer &tp)
    :TableTennisPlayer(tp)
{
    
    
    rating = r;
}

The second constructor of RatedPlayer also calls TableTennisPlayer(tp) first, and then enters the function body to perform rating = r; operation.

The main points of the derived class constructor are summarized as follows:

  • First create the base class object;
  • The derived class constructor should pass the base class information to the base class constructor through the member initialization list , otherwise use the default base class constructor;
  • The derived class constructor should initialize the newly added data members of the derived class.

The order of releasing objects is opposite to the order of creating objects, that is, the destructor of the derived class is executed first, and then the constructor of the base class is automatically called.

1.2 Use derived classes

Put the above-mentioned derived class and definition implementation into the header file and cpp file of the base class respectively. Then modify main to:

#include<iostream>
#include"my_class.h"
int main()
{
    
    
    RatedPlayer rPlayer1(1140,"Mallory", "Duck", true);
    rPlayer1.ShowName();
    if(rPlayer1.HasTable())
        cout<<": has a table.\n";
    else
        cout<<": hasn't a table.\n";
}

Output:

Duck, Mallory: has a table.

We can see that the derived class can also access the methods in the base class.

1.3 The special relationship between derived classes and base classes

  1. Derived objects can use the methods of the base class, provided that the methods are not private.
RatedPlayer rPlayer1(1140,"Mallory", "Duck", true);
rPlayer1.ShowName();//使用基类的方法
  1. The base pointer can point to the derived class object without display type conversion
RatedPlayer rPlayer1(1140,"Mallory", "Duck", true);//派生类对象
TableTennisPlayer * pt = &rplayer;//基类对象
pt->ShowName();//指针方法调用
  1. Base class references can refer to derived class objects without display type conversion.
RatedPlayer rPlayer1(1140,"Mallory", "Duck", true);//派生类对象
TableTennisPlayer& rt = rplayer;//基类对象
rt.ShowName();//引用方法调用
  • But the pointers and references of the base class can only call methods of the base class, not any members of the derived class.
  • Normally, C++ requires reference and pointer types to match the assigned type, but this rule is an exception for inherited types.
  • But this kind of exception is only one-way, you cannot assign base class objects and addresses to derived class references and pointers.

Reference compatibility properties can also make the base class object initialized as a derived class object:

RatedPlayer olaf1(1840,"Olaf", "Loaf", true);//派生类
TableTennisPlayer olaf2(olaf1);

To initialize olaf2, the prototype of the matching constructor is as follows:

TableTennisPlayer(const RatedPlayer &);//这种实现是没有的!!!

There is no such complete matching constructor! But there is an implicit copy constructor:

TableTennisPlayer(const TableTennisPlayer &);

It is also possible to assign derived objects to base objects:

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

In this case, the program will use the implicitly overloaded copy operator:

TableTennisPlayer& operator= (const TableTennisPlayer &) const;

2. Inheritance: is-a relationship

Public inheritance establishes an is-a relationship, that is, a derived class object is also a base class object, and any operation that can be performed on the base class object can also be performed on the derived class object.

3. Polymorphic public inheritance

If there is the same method in the derived class and the base class, but the behavior is different, that is, the behavior of the same method varies with the context. There are two important mechanisms to implement polymorphic public inheritance:

  • Redefine the methods of the base class in the derived class.
  • Use virtual methods (virtual functions).

Polymorphic attention

Note the following when using polymorphic public inheritance:

First: The program will use the object type to determine which version to use:

Brass dom("Dominic Banker",11224, 4183.45);//基类
BrassPlus dot("Dorothy Banker", 12118,2592.00);//派生类
dom.ViewAcct();//调用基类方法
dot.ViewAcct();//调用派生类方法

Second: If the method is called by reference or pointer instead of object.

  • If the keyword virtual is not used, the program will select the method based on the reference type or the pointer type;
  • If virtual is used, the program will choose based on the type of object pointed to by the reference or pointer.

If ViewAcct() is not virtual, using pointers or reference types is the following:

Brass dom("Dominic Banker",11224, 4183.45);//基类
BrassPlus dot("Dorothy Banker", 12118,2592.00);//派生类
Brass &b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct();//使用基类方法
b2_ref.ViewAcct();//使用基类方法

If ViewAcct() is virtual, the pointer or reference calling method is as follows:

Brass dom("Dominic Banker",11224, 4183.45);//基类
BrassPlus dot("Dorothy Banker", 12118,2592.00);//派生类
Brass &b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct();//使用基类方法
b2_ref.ViewAcct();//使用派生类方法!!!

Third: The virtual destructor is used in the base class. If the destructor is not virtual, only the destructor corresponding to the pointer type will be called. Then the Destructor of Brass is called, even if the pointer points to a BrassPlus object. If the destructor is virtual, the destructor of the corresponding object type will be called. So the pointer points to the BrassPlus object, the Destructor of BrassPlus will be called, and then the destructor of the base class will be called automatically. Therefore, the use of virtual functions can ensure that the correct sequence of destructors is called.

4. Static and dynamic binding

When a program calls a function, the compiler decides which code block to execute. Interpreting function calls in the source code as executing specific function code blocks is called function binging.

  • Static binging: also known as early binding, which is performed during the compilation process.
  • Dynamic binging: Also known as late bingding, virtual functions make it impossible to determine which type of object the user will choose during the compilation process. Therefore, the compiler must generate code that can select the correct virtual method when the program is running.

4.1 Compatibility of pointer and reference types

In C++, dynamic binding is related to calling methods through pointers and references. To some extent, this is controlled by inheritance. Generally, C++ does not allow one type of address to be assigned to another type of pointer, and the same goes for references:

double x = 2.5int * pi = &x;//不允许
long & r1 = x;//不允许

However, we can see that a pointer or reference to a base class can refer to a derived class object without having to perform an explicit type conversion. E.g:

BrassPlus dilly("Annie Dill", 493222, 2000);
Brass * pb = &dilly;
Brass& rb = dilly;

This form is called upcasting, which eliminates the need for explicit type conversions for public inheritance. And this kind of operation is passable. For example, if BrassPlus continues to derive the BrassPlusPlus class, then the pointer or reference to Brass can also refer to the BrassPlusPlus class object.

4.2 Virtual member functions and dynamic binding

Let us first review the process of calling a method by reference or pointer.

BrassPlus ophelia;
Brass *bp;
bp = &ophelia;
bp->ViewAcct();

If ViewAcct() is not declared as virtual, the member function ViewAcct() of Brass is called. This is because the pointer type is known at compile time, so the compiler can associate ViewAcct() with Brass::ViewAcct() at compile time. In short, the compiler uses static binding for non-virtual functions.

If ViewAcct is declared as virtual. Then the member function is called according to the pointed object, and the compiler uses dynamic binding for the virtual function.

(1) Why are there two types of binding

If dynamic binding allows you to redefine class methods, but static binding is poor in this regard, why not abandon static binding and use static binding as the default binding method? There are two reasons for efficiency and conceptual model

First of all, static binding is more efficient, because if you want the program to make decisions at the runtime, you must take some methods to track the type of object pointed to by the base pointer or reference, which adds additional processing overhead. And if the base class is not redefined, there is no need for dynamic binding. So C++ chooses static binding by default.

Next look at the conceptual model. Not all class methods need to be reimplemented, so not all methods need to set virtual functions.

(2) The working principle of virtual function

C++ specifies the behavior of virtual functions, but leaves the implementation method to the compiler author. You don't need to know the implementation method to use virtual functions, but understanding how virtual functions work can help you better understand the concepts.

The efficiency of the non-virtual function is slightly higher than that of the virtual function, but it does not have the dynamic binding function.

For details, please refer to Chapter 13.4.2 of C++Primer

4.3 Notes about virtual functions

  • The constructor cannot be a virtual function.
    When a derived class is created, the constructor of the derived class will be called instead of the constructor of the base class, and then the constructor of the derived class will use a constructor of the base class.

  • The destructor should be a virtual function to
    ensure the correct order of destruction. By the way, even if the class is not used as a base class, it is not an error to define a virtual destructor for the class, it is just a matter of efficiency.

  • Friends cannot be virtual functions. Because friends are not class members, and only members can be virtual functions.

  • 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 .

  • Redefining the method of inheritance is not overloading, but hiding the function version of the base class.

For example, a function in a base class is a virtual function with parameters, while a function in a derived class is a virtual function without parameters.

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

Please pay attention to the following hidden situations:

Hovel trump;
trump.showperks();//有效
trump.showperks(5);//无效,隐藏了,不可以这样

So once the base class declaration is overloaded, you should redefine all base class versions in the derived class! Otherwise, the virtual function method will hide all base class methods.

Of course, there are exceptions, that is, the return type is a base class reference or pointer, which will not be hidden ( but the parameters should be consistent ). This feature is called covariance of return type, because it allows the return type to vary with the type of the class.

class Dwelling
{
    
    
	public:
		virtual Dwelling& showperks(int a)const;
}
class Hovel : public Dwelling
{
    
    
	public:
		virtual Hovel& showperks(int a)const;//参数一致!!!
}

5. Access control: protected

Protected and private are the same in the class, and they can only be accessed through public member functions.

The difference between protected and private is mainly reflected in derived classes. The members of the derived class can directly access the protected members of the base class, but cannot directly access the private members of the base class.

Using protected data members can simplify code writing, but there are design flaws. For example, protected members are protected for base classes, just like private members, but in derived classes, they can be easily modified.

Therefore, it is best to use private access control for the class data members, do not use protected access control; at the same time, the derived class can access the base class data through the base class method.

However, for member functions, it is useful to protect access control, which allows derived classes to access internal functions that are not available to the public.

6. Abstract base class

C++ provides unimplemented functions by using pure virtual functions. The end of the pure virtual function declaration is =0.

virtual double Area() const = 0;//纯虚函数

When a pure virtual function is included in the class declaration, an object of that class cannot be created. That is to say, a class containing pure virtual functions can only be used as a base class, that is, an abstract base class (ABC).

If the functions of the class methods derived from the abstract base class are all the same, we can also define pure virtual functions in the abstract base class:

void Move(int nx, int ny) = 0;//原型
void BaseEllipse::move(int nx, ny){
    
    x = nx; y = ny;}//定义

7. Inheritance and dynamic memory allocation

How does inheritance relate to dynamic memory allocation (using new and delete)? For example, if the base class uses dynamic memory allocation and redefines assignment and copy constructors, how will this affect the derived class? This problem depends on the properties of the derived class. Let's take a look at two situations.

7.1 The first case: the derived class does not use new

This situation is similar to the string class, and the string can be treated as a general variable. For derived classes, no explicit destructors, copy constructors, and overloaded assignment operators are required.

7.2 The second case: use new for derived classes

The derived class uses new:

class hasDMA:public baseDMA
{
    
    
	private:
		char * style;
	public:
		...
}

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


The
last one: (ten) classes and dynamic memory allocation


Article reference: "C++ Primer Plus Sixth Edition"

Guess you like

Origin blog.csdn.net/QLeelq/article/details/111059260