C++, class inheritance and virtual functions

1 inheritance

1.1 Inheritance method

  1. public, the access rights of base class members in derived classes remain unchanged.
  2. Protected, public and protected members in the base class become protected members in the derived class.
  3. Private, public and protected members in the base class become private members in the derived class.

Regardless of the type of inheritance, public and protected members in the base class are accessible in the definition body of the derived class.

1.2 Conversion and assignment from derived class to base class

The memory header of the derived class object contains the complete base class memory structure, followed by the memory of newly defined members of the derived class. Therefore, a derived class object can, to a certain extent, be converted into a pointer or reference to a base class object, or initialized and assigned to a base class object.

Within the same program, objects of a derived class type may or may not be convertible or assignable to a base class. The base class object can only access the public members of the base class, and for different inheritance methods, the access rights of derived class objects to these members will change. Conversion or assignment is safe only if the derived class object has the same accessibility to the public members inherited from the base class as the base class object.

  1. Regardless of the inheritance method, the public members of the base class in the derived class definition are accessible, so conversion or assignment within the member function of the derived class is allowed;

  2. For protected and private inheritance, the derived class object cannot access the public members of the base class, and conversion or assignment outside the derived class definition is not allowed.

class A {
    
    };
class B : private A {
    
    
   void tmp(B b) 
   {
    
    
      A x = b;    // ok,使用派生类对象进行基类对象的初始化
      x = b;      // ok,使用派生类对象对基类对象赋值
      A &y = b;   // ok,派生类通过引用转换为基类
      A *z = &b;  // ok,派生类通过指针转换为基类
   }
};
class C : protected B {
    
    
   void tmp(C c)
   {
    
    
      A x1 = c;   // error,C类不能访问A类的public成员,除非B类至少是以protected继承A类
      A &y1 = c;  // error,同上
      B x2 = c;   // ok,这时B类是C类的直接基类,C类可以访问B类的public成员
      B &y2 = c;  // ok,同上
   }
};
int main() {
    
    
   A a1 = B();    // error,这时B类不能访问A类的public成员
   A a2 = C();    // error,这时C类不能访问A类的public成员
   B b1 = C();    // error,这时C类不能访问B类的public成员
}

The conversion mentioned here has a different meaning from initialization or assignment (personal understanding):

  1. Conversion is relative to pointers or references, that is, there is no instance of the base class object in the memory, but the base class pointer or reference can access the data members inherited from the base class in the object instance of the derived class according to the access permissions of the base class or Function members. Note that if the derived class redefines the data members or function members of the base class and rewrites or overloads the functions, then after conversion, the base class pointer or reference can only access members with the base class domain qualifier. Instead of the redefined member, because the memory of the redefined member is not within the accessible range of the base class.

    However, there is something special about function members. If a function in the base class is declared as a virtual function, then the base class pointer or reference calls the rewritten function. This is the key to inheritance and implementation of runtime polymorphism. At this time, a virtual function table will be maintained in the memory. The pointer or reference of the base class contains a pointer to the virtual function table. When the base class pointer or reference calls the virtual function, the actual call is the virtual function corresponding to the derived class object, thereby achieving runtime polymorphism. See the "Virtual Functions" section for details.

  2. Initialization or assignment means copying the data members of the derived class object instance to the corresponding members of the base class object instance being constructed or existing according to the access method of the base class. This is generally achieved by calling the copy constructor and assignment function of the base class. From then on, the base class object has no connection with the input derived class object, so runtime polymorphism cannot be achieved.

Note that recalling the difference between pointers and references, there is also a difference between using pointers and using references for derived class conversion:

  1. Citation A &a=b; a=c;

    Since the reference can only be initialized and cannot be changed once bound, when A &a=b; is executed, the runtime type of a is fixed to the type of B, and the virtual table pointer of a is updated to the virtual table pointer of b. . When a=c; is executed, the runtime type of a will not be changed to the type of C. Instead, the base class members of c will be copied to the base class members of b by calling the assignment function of the base class. The vtable pointer of a will not change.

  2. Directions A *a=&b; a=&c;

    Since pointers are assignable, when A *a=&b; is executed, the runtime type of a changes to the type of B, and the virtual table pointer of a will also be updated to the virtual table pointer of b. But after executing a=&c;, the runtime type of a changes to the type of C, and its virtual table pointer will also be updated to the virtual table pointer of c. In this process, there are only assignments of pointer variables, but no calls to assignment functions.

2 virtual functions

Because the memory structure of the derived class object contains the complete memory structure of the base class object, the derived class object can be directly assigned to the base class object, or the derived class pointer can be assigned to the base class pointer, or the derived class object can be derived through the base class type. citation.

class A {
    
    };
class B : public A {
    
    };
B b;
A a = b;
A *a = &b;
A &a = b;

The scope of accessible members of variable a is defined by base class A. If a function of the base class is redefined in a derived class, the base class variable cannot therefore call the implementation method of the derived class. In order to achieve dynamic binding of member functions of variable a, C++ provides a virtual function method.

Only member functions in a class are eligible to become virtual functions, because virtual functions are only applicable to class objects with inheritance relationships. The virtual keyword is added before the declaration of a virtual function in the base class, and the definition outside the class cannot be added with virtual. When a virtual function exists, a base class object maintains a virtual table pointer at the head of its memory space when it is instantiated or used as a pointer or reference.

The virtual table of a program is usually a large block of continuous memory. The compiler will fill in the addresses of virtual functions of classes in the program into the virtual table in order. For example, assuming that B inherits from A, and two virtual functions f1() and f2() are defined in A; and C inherits from B, and a virtual function f3() is defined in B, then the virtual table may be:

|vptrA=A::f1|A::f2| xxx |vptrB=B::f1|B::f2|B::f3| xxx |vptrC=C::f1|C::f2|C::f3|

When an object instance or pointer or reference of a class is defined, the vtable pointer will be automatically initialized to the position of the first function pointer stored in the vtable of the class. For exampleA a; then a.vptr=A::f1; B b; thenb.vptr=B::f1; C c; Thenc.vptr=C::f1; Of course vptr is not visible to programming. Different compilers may have different implementation methods.

When a derived class object is converted to a base class type through a pointer or reference, the vptr of the base class pointer or reference is updated to the vptr of the derived class. Since virtual functions are placed in order, when the base class calls a virtual function such as f2(), the function actually called is vptr[1], and at this time the vptr has been updated to the vptr of the derived class, so the derived class can be called class version. For example, A &a = b; at this time a.vptr=vptrB=B::f1. b->vptr[2]=vptrC[2] =C::f3 is called b->f3(). b->vptr=vptrC=C: :f1 at this time B *b = &c;. In the same way, a.vptr[1]=vptrB[1]=B:: f2 is called a.f2().

Note that different compilers may not necessarily implement virtual functions in the same way, but they are generally similar. In vs, right-click on the properties of the xxx.cpp file -> c/c++ -> Command Line and add /d1 reportAllClassLayout, the detailed memory structure of all classes will be output during compilation, or /d1 reportSingleClassLayoutX Print the memory structure of a specific class X. In g++, the detailed memory structure of a class can be obtained through the g++ -fdump-class-hierarchy xxx.cpp command. The following is an example of a memory structure output by g++:

   Vtable for A
   A::_ZTV1A: 4u entries
   0     (int (*)(...))0
   8     (int (*)(...))(& _ZTI1A)
   16    (int (*)(...))A::f1
   24    (int (*)(...))A::f2

   Class A
      size=16 align=8
      base size=12 base align=8
   A (0x0x7f9333cd3840) 0
      vptr=((& A::_ZTV1A) + 16u)

   Vtable for B
   B::_ZTV1B: 5u entries
   0     (int (*)(...))0
   8     (int (*)(...))(& _ZTI1B)
   16    (int (*)(...))B::f1
   24    (int (*)(...))B::f2
   32    (int (*)(...))B::f3

   Class B
      size=16 align=8
      base size=16 base align=8
   B (0x0x7f9333c979c0) 0
      vptr=((& B::_ZTV1B) + 16u)
   A (0x0x7f9333cd38a0) 0
         primary-for B (0x0x7f9333c979c0)

When a derived class redefines a virtual function, it does not need to add the virtual keyword. It defaults to a virtual function. The parameter list and return type must be consistent with the base class. But there is an exception. When the base class virtual function returns a base class pointer or reference, the derived class can return a derived class pointer or reference.

The visibility of base class virtual functions in derived classes is the same as that of ordinary functions. That is, if the base class virtual function is not overridden in the derived class, but it contains overloads of other functions with the same name, the derived class object cannot directly access the virtual function, but The base class version can be called when casting to the base class type via a pointer or reference. However, when the derived class is inherited by other classes, the virtual table of the derived class still contains the virtual functions of the base class. Therefore, base class variables can also accept conversion of objects of derived classes through pointers or references and calls to virtual functions.

The constructor cannot be a virtual function. For defining an object instance, when the constructor is called, the object does not exist yet and the virtual table pointer has not been initialized. It is meaningless to discuss virtual functions at this time. For pointers or references, because the constructor is not a member function in the general sense, you cannot call the constructor through pointers and references. They must be bound to an already constructed object to make sense. At this time, whether the constructor is virtual Functions are also meaningless.

Static functions cannot also be virtual functions. Because static functions exist independently of object instances, they cannot be dynamically bound.

The destructor can be a virtual function. Because the destructor is called after the object is instantiated, and can be called explicitly like a normal member function. In fact, the destructor should preferably be a virtual function. Especially when there is dynamic memory allocation in a derived class, the base class destructor must be defined as a virtual function. Because if the destructor is not a virtual function, then when the base class pointer is deleted, it cannot access the destructor of the derived class, which causes a memory leak problem. However, because virtual functions need to maintain virtual tables, which will cause problems such as increased memory and reduced efficiency, the compiler defaults the destructor to a non-virtual function.

Guess you like

Origin blog.csdn.net/qq_33552519/article/details/124092454
Recommended