First knowledge of C++03: reference, inheritance and derivation

------------Restore content start------------

Reference, inheritance and derivation

Reference Introduction

First of all, we should know that the transfer of parameters is essentially a process of assignment, which is to copy the memory. The so-called memory copy refers to copying data from one memory to another. For aggregate types (complex types, similar structures and classes), the memory consumption may be very large.

A reference can be regarded as an alias of the data, and the data can be found through this alias and the original name (pointing to the same memory)

be careful:

  • The reference must be initialized at the same time as the definition, and it must be consistent in the future. No other data can be referenced, which is a bit similar to a constant (const variable)
  • When defining a reference, you need to add &, but you can't add &. Adding & when using means taking an address
int a = 99;
int &r = a;
cout << a << ", " << r << endl;

In general c + +, reference is used as a function parameter instead of the function of pointer, which can also change the data content, which is very practical;

At the same time, in c + +, reference can be used as the return value of function, but!!! You cannot return references to local data (such as local variables, local objects, local arrays, etc.), because the local data will be destroyed after the function call is completed, and the data may not exist when it is used next time

int &plus10(int &r) {
    int m = r + 10;
    return m;  //Returns a reference to local data
}
//For some compilers, errors will be reported
int &num3 = plus10(num1);
int &num4 = plus10(num3);
//However, some compilers can run, such as gcc, but the values of num3 and num4 are the same, because the function runs on the stack, and the management right of all local data will be abandoned after running. The subsequent function call will overwrite the local data of the previous function, and the two points will be changed to the last value;

Nature of reference:

In fact, the reference simply encapsulates the pointer, and its bottom layer is still realized through the pointer. The memory occupied by the reference is the same as that occupied by the pointer. In 32-bit environment, it is 4 bytes and in 64 bit environment, it is 8 bytes. The reason why the reference address cannot be obtained is because the compiler has carried out internal conversion:

int a = 99;
int &r = a;
r = 18;
cout<<&r<<endl;
//It will be converted to the following form during compilation:
int a = 99;
int *r = &a;
*r = 18;
cout<<r<<endl;

&When r takes the address, the compiler will implicitly convert the code, so that the code outputs the content of R (the address of a), not the address of R, which is why the address of the reference variable cannot be obtained. In other words, it is not that the variable r does not occupy memory, but that the compiler does not let it get its address.

Other differences between pointers and References:

  • The reference must be initialized at the time of definition, and must be consistent in the future. It can no longer point to other data; The pointer does not have this restriction. The pointer does not have to be assigned when defining, and can point to any data in the future

  • You can have const pointer, but without const reference, r can't change the direction. Adding const is superfluous.

  • Pointers can have multiple levels, but references can only have one level (it's not correct to think about reference folding). For example, int **p is legal, while int & &r is illegal (c++11 adds a right value reference, which is legal). The following is OK

    int a = 10;
    int &r = a;
    int &rr = r;
    //Are all addresses pointing to a
    
  • The meaning of self increment (+ +) and self decrement (- -) operation of pointer and reference is different. Use + + for the pointer to point to the next data, and use + + for the reference to add 1 to the data it refers to

References generally cannot bind temporary data:

Pointers and references can only point to memory, not registers or hard disks, because registers and hard disks cannot be addressed.

Defined variables, created objects, string constants, function parameters, function body itself, memory allocated by new or malloc(), etc. these contents can be used to obtain the address.

What data cannot be used &, it will be in the register:

  • int, double, bool, char and other basic types of data are often no more than 8 bytes, which can be stored in one or two registers, so these types of temporary data are usually placed in registers; Objects and structural variables are user-defined data with unpredictable size, so these types of temporary data are usually put into memory.
int *p2 = &(n + 100);//No, n+100 will be in the register, and the constant expression will also be in the register
S s1 = {23, 45};
S s2 = {90, 75};
S *p1 = &(s1 + s2);//In Visual C + +, s1+s2 is in memory, but!!!! gcc can't, because gcc can't refer to any temporary variables!!!

bool isOdd(int &n){
    if(n%2 == 0){
        return false;
    }else{
        return true;
    }
}
isOdd(a);  //correct
isOdd(a + 9);  //Error, sometimes it's easy to pass temporary data to it

I'm going to have a look

const reference binding temporary data:

Constant reference: the compiler will create a new, nameless temporary variable for the temporary data, put the temporary data into the temporary variable, and then bind the reference to the temporary variable.

Change to constant reference, because creating temporary variables for common reference has no meaning. The created temporary variables only modify the data in the temporary variables, which will not affect the original data and has little significance. For constant reference, we can only read the value of data through const reference, but can't modify its value, so we don't need to consider the problem of synchronous update, and we won't produce two different data.

bool isOdd(const int &n){  //Change to constant reference
    if(n/2 == 0){
        return false;
    }else{
        return true;
    }
}
isOdd(7 + 8);  //correct
isOdd(a + 9);  //correct

const reference and type conversion:

Pointer type conversion is wrong (unexpected error hhh), because the amount of memory occupied by different types of data is different, and the processing method is also different (you can see< How are integers stored in memory><How are decimals stored in memory >(for reference); Because the essence of a reference is also a pointer, the type conversion of a reference is also wrong.

int n = 100;
int *p1 = &n;  //correct
float *p2 = &n;  //error
int &r1 = n;  //correct
float &r2 = n;  //error

But!!! Type conversion can occur by adding constant references

Principle: when the type of reference and data are inconsistent, if their types are similar and follow the rule of "automatic conversion of data type", the compiler will create a temporary variable and assign the data to the temporary variable (automatic type conversion will occur at this time), and then bind the reference to the temporary variable, This is the same as the scheme used in "bind const reference to temporary data".

int n = 100;
int &r1 = n;  //correct
const float &r2 = n;  //correct

To sum up: if the function does not require to change the referenced value, try to use const as the function parameter; First, avoid temporary data, second, avoid different types, and third, const and non const arguments are acceptable;

double volume(const double &len, const double &width, const double &hei){
    return len*width*2 + len*hei*2 + width*hei*2;
}
double v4 = volume(a+12.5, b+23.4, 16.78);
double v5 = volume(a+b, a+c, b+c);

Inheritance and derivation

Inheritance introduction

The inherited class is called parent class or base class, and the inherited class is called child class or derived class. "Child class" and "parent class" are usually called together, and "base class" and "derived class" are usually called together. It means the same thing.

When to use inheritance: there must be a great correlation between classes. There are many common member functions and member variables.

Inherited members can be accessed through subclass objects, just like their own.

Inheritance format:

Class derived class name: [inheritance method] base class name{
Newly added member of derived class
};

Three inheritance methods:

public inheritance method

  • Public member in base class - > public attribute in derived class
  • Protected member in base class - > protected attribute in derived class
  • private member in base class - > cannot be used in derived class, invisible

protected inheritance method

  • public member in base class - > protected attribute in derived class
  • Protected member in base class - > protected attribute in derived class
  • private member in base class - > cannot be used in derived class, invisible

private inheritance method

  • Public \ protected - > in the base class becomes a private attribute in the derived class, which can only be used in the class in the derived class
  • private member in base class - > cannot be used in derived class, invisible

The protected attribute can only be accessed in the derived class (class code); Nothing else;

Not difficult to find:

1) Public, protected and private in the inheritance method are used to indicate the highest access permission of the base class member in the derived class, which can not be exceeded. That is, even if the base class is a public member attribute and the derived class adopts protected inheritance, the public member attribute can only become protected;

2) The private member in the base class cannot be used in the derived class, but it can be used through the set and get functions of public (the only way to access the private member of the base class in the derived class is through the non private member function of the base class)

3) If you want the members of the base class to be inherited by the derived class and used without obstacles, these members can only be declared as public or protected

4) If you want the members of the base class to be neither exposed (not accessible through objects) nor used in derived classes, you can only declare them protected

be careful 👀 What we are talking about here is that the private members of the base class cannot be used in derived classes, and we do not say that the private members of the base class cannot be inherited. In fact, the private member of the base class can be inherited, and (member variable) will occupy the memory of the derived class object. It is just invisible in the derived class, so it cannot be used.

Change the method of access permission and use the using keyword

using can only change the access permissions of public and protected members in the base class, not private members, because private members in the base class are invisible in derived classes and cannot be written;

//Base class People
class People {
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show() {
    cout << m_name << "What is your age" << m_age << endl;
}
//Derived class Student
class Student : public People {
public:
    void learning();
public:
    using People::m_name;  //Change protected to public
    using People::m_age;  //Change protected to public
    float m_score;
private:
    using People::show;  //Change public to private
};

Inherited name

For functions, regardless of the parameters of the function, as long as the name is the same, it will cause masking and no overloading. If you want to call, use the domain name and domain resolver;

For member variables, as long as the name is the same, the derived class will mask the base class, but the member variables of the base class exist when they are the same. At this time, the get and set methods inherited from the base class operate on the variables with the same name of the base class, not on the variables with the same name of the derived class.

Scope nesting and object memory model of class inheritance

Assuming Base is the Base class and Derived is the Derived class, the nesting relationship of their scopes will be:

The compiler will find out of the scope of the current class:

When accessing the member variable n through obj (Class C object), the name n can be found in the scope of class C. Although class A and class B have the name n, the compiler will not look in their scope, so they are invisible, that is, N in the derived class masks n in the base class.

When accessing the member function func() through obj, the name func is not found in the scope of class C, and B is not found. Continue to search in the scope of class A, and the name func is found. The search is over, The compiler decides to call the func() function in the scope of class A (this process is called name search, which is through name search. This process will not occur unless it is found directly through the domain name and domain resolver);

Object memory model:

When there is no inheritance, it is relatively simple. Variables exist in the heap or stack area, and functions exist in code segments;

When inheritance exists:

All variables continuously exist in the heap area or stack area (the member variables are arranged in order according to the derived level, and the new member variables are always last, and the private and masked ones will also be in memory). The function has a code area (all objects are shared, but whether they can be used depends on its permission. If they can not be used, private can not be used)

example:

obj_a is the base class object, obj_b is a derived class object. Assume obj_ If the starting address of a is 0X1000, its memory distribution is shown in the following figure:

Assume obj_ The starting address of B is 0X1100, m in class a_ If B is private, its memory distribution is shown in the following figure:

Assume obj_ The starting address of C is 0X1300. If it is masked, its memory distribution is shown in the following figure:

Summary: in the object model of derived classes, member variables of all base classes will be included. The advantage of this design scheme is high access efficiency. It can directly access the base class variables in the derived class objects without several layers of indirect calculation.

Constructors / destructors of base and derived classes

When designing a derived class, the initialization of inherited member variables should also be completed by the constructor of the derived class, but most base classes have member variables with private attribute, which cannot be accessed in the derived class, let alone initialized by the constructor of the derived class. The idea to solve this problem is to call the constructor of the base class in the constructor of the derived class.

#include<iostream>
using namespace std;
//Base class People
class People{
protected:
    char *m_name;
    int m_age;
public:
    People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}

//Derived class Student
class Student: public People{
private:
    float m_score;
public:
    Student(char *name, int age, float score);
    void display();
};
//People(name, age) is to call the constructor of the base class
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
    cout<<m_name<<"What is your age"<<m_age<<",The result is"<<m_score<<". "<<endl;
}
int main(){
    Student stu("Xiao Ming", 16, 90.5);
    stu.display();
    return 0;
}

People(name, age) is to call the constructor of the base class and pass name and age as arguments to it, m_score(score) is the parameter initialization table of the derived class. Secondly, M_ There is no problem with putting score (score) in front. It will follow the principle of calling the base class constructor first and then initializing other member variables in the parameter initialization table.

Constructor call order:

When class A - > b - > C, the execution order is class a constructor -- > class B constructor -- > class C constructor. A is the indirect base class of C and B is the direct base class of C; In the derived class constructor, only the constructor of the direct base class can be called, and the constructor of the indirect base class cannot be called, because C uses the constructor of class B, and in B, the constructor of class a will be called first, which is equivalent to that C indirectly (or implicitly) calls the constructor of A. if C explicitly calls the constructor of a, the constructor of a will be called twice, and the initialization work will be done twice accordingly, This is not only redundant, but also a waste of CPU time and memory.

Calling rules of base class constructor:

When creating an object through a derived class, you must call the constructor of the base class, which is the syntax. In other words, it is better to specify the base class constructor when defining the derived class constructor; If not specified, call the default constructor of the base class (constructor without parameters); If there is no default constructor.

#include <iostream>
using namespace std;
//Base class People
class People{
public:
    People();  //Base class default constructor
    People(char *name, int age);
protected:
    char *m_name;
    int m_age;
};
People::People(): m_name("xxx"), m_age(0){ }
People::People(char *name, int age): m_name(name), m_age(age){}

//Derived class Student
class Student: public People{
public:
    Student();
    Student(char*, int, float);
public:
    void display();
private:
    float m_score;
};
Student::Student(): m_score(0.0){ }  //Derived class default constructor
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
    cout<<m_name<<"What is your age"<<m_age<<",The result is"<<m_score<<". "<<endl;
}
int main(){
    Student stu1;
    stu1.display();
    Student stu2("Xiao Ming", 16, 90.5);
    stu2.display();
    return 0;
}

When creating the object stu1, execute the constructor Student::Student() of the derived class, which does not indicate which constructor of the base class to call. It is obvious from the running results that the system calls the constructor without parameters by default, that is, People::People().

When creating object stu2, execute the constructor Student::Student(char *name, int age, float score) of the derived class, which indicates the constructor of the base class.

For destructors

Destructors cannot be inherited. Different from the constructor, there is no need to explicitly call the destructor of the base class in the destructor of the derived class, because each class has only one destructor, and the compiler knows how to choose without our intervention.

The execution order of destructors and constructors are also just the opposite:

  • When creating a derived class object, the execution order of the constructor is the same as the inheritance order, that is, the base class constructor is executed first, and then the derived class constructor is executed.
  • When destroying a derived class object, the execution order of the destructor is opposite to the inheritance order, that is, the derived class destructor is executed first, and then the base class destructor is executed.

Multi inheritance and multi inheritance object memory model

c + + has not only single inheritance, but also multiple inheritance

class D: public A, private B, protected C{
//New members of class D
}

D is a derived class with multiple inheritance forms. It inherits class A in a public way, class B in a private way and class C in a protected way. D obtains members in a, B and C according to different inheritance methods and determines their access rights in derived classes.

Construction under multiple inheritance:

The calling order of the base class constructor is independent of the order in which they appear in the derived class constructor, but is the same as the order in which the base class appears when declaring the derived class (similar to the variables in the class). For example, in the above example, A will be constructed first, then B, then C, and finally D;

Naming conflict:

When there is a member with the same name (member variable or member function) in two or more base classes, if the member is accessed directly, there will be a naming conflict. The compiler does not know which base class member to use. At this time, you need to add the class name and domain resolver:: before the member name to explicitly indicate which class member to use and eliminate ambiguity.

Memory model:

Direct example:

#include <cstdio>
using namespace std;
//Base class A
class A{
public:
    A(int a, int b);
protected:
    int m_a;
    int m_b;
};
A::A(int a, int b): m_a(a), m_b(b){ }
//Base class B
class B{
public:
    B(int b, int c);
protected:
    int m_b;
    int m_c;
};
B::B(int b, int c): m_b(b), m_c(c){ }
//Derived class C
class C: public A, public B{
public:
    C(int a, int b, int c, int d);
public:
    void display();
private:
    int m_a;
    int m_c;
    int m_d;
};
C::C(int a, int b, int c, int d): A(a, b), B(b, c), m_a(a), m_c(c), m_d(d){ }
void C::display(){
    printf("A::m_a=%d, A::m_b=%d\n", A::m_a, A::m_b);
    printf("B::m_b=%d, B::m_c=%d\n", B::m_b, B::m_c);
    printf("C::m_a=%d, C::m_c=%d, C::m_d=%d\n", C::m_a, C::m_c, m_d);
}
int main(){
    C obj_c(10, 20, 30, 40);
    obj_c.display();
    return 0;
}

Break access with pointer:

Think that the pointer points to the memory address, and the object pointer points to the memory address of the object. Through the memory model, we can know that private is also in continuous memory, so!!! As long as you use pointer offset, you can forcibly access private member variables; For example:

In the figure, it is assumed that the starting address of obj object is 0X1000, m_a(public),m_b(public),m_c (private) is 0, 4 and 8 bytes away from the beginning of the object. We call this distance Offset

You know:

int b = p -> m_b;

It will be converted to: int b = * (int *) ((int) P + sizeof (int));

In fact, it is: int b = * (int *) ((int) P + 4);

yes:

So: int c = * (int *) ((int) P + sizeof (int) * 2)// It's that simple

Virtual inheritance

1. What are virtual inheritance and virtual base classes

Multi inheritance is prone to naming conflict: Classic diamond inheritance

The first kind of problem: retain multiple members of the indirect base class with the same name in a derived class. Although different data can be stored in different member variables, this is redundant in most cases: retaining multiple member variables not only takes up more storage space, but also prone to naming conflicts. If class A has a member variable a, it will be ambiguous to directly access a in class D. the compiler does not know whether it comes from a -- > B -- > D or a -- > C -- > D.

In order to eliminate ambiguity, we can use M_ The front of a indicates which class it comes from (handled by domain)

However, there are still two indirect base classes in memory, which consume memory. Therefore, in order to solve the problems of naming conflict and redundant data in multiple inheritance, virtual inheritance is proposed, so that only one member of the indirect base class is retained in the derived class.

//Indirect base class A
class A{ //virtual base class
protected:
    int m_a;
};
//Direct base class B
class B: virtual public A{  //Virtual inheritance
protected:
    int m_b;
};
//Direct base class C
class C: virtual public A{  //Virtual inheritance
protected:
    int m_c;
};
//Derived class D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //correct
    void setb(int b){ m_b = b; }  //correct
    void setc(int c){ m_c = c; }  //correct
    void setd(int d){ m_d = d; }  //correct
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

The purpose of virtual inheritance is to make a class declare that it is willing to share its base class. Among them, the shared base class is called virtual base class.

We can see a problem: the virtual derivation operation must be completed before the real requirements of virtual derivation appear, that is, before the requirements of D appear, B and C should be set as virtual inheritance;

That is, virtual derivation only affects the classes further derived from the derived classes that specify the virtual base class (classes that inherit BC, such as E inherits B and F inherits C, will be affected), and it will not affect the derived class itself (BC);

In actual development, the base class in the middle level declares its inheritance as virtual inheritance, which generally will not bring any problems. The class hierarchy using virtual inheritance is designed by one person or a project team at one time, so there is no need to modify the middle function because it does not take into account the following requirements; The c + + library iostream adopts virtual inheritance.

2. Visibility of virtual base class members

Taking diamond inheritance as an example, suppose A defines A member variable named x. when we directly access x in D, there are three possibilities:

  • If there is no definition of X in B and C, x will be resolved to the member of A, and there is no ambiguity at this time.
  • If one of the classes in B or C defines x, there will be no ambiguity. The X of the derived class has higher priority than the X of the virtual base class.
  • If x is defined in both B and C, direct access to x will cause ambiguity.

The second problem is that BC contains the same variables with equal priority. At this time, domain resolution can only be used to remove ambiguity.

It is not recommended to use multiple inheritance in programs!!! Multi inheritance should be used only when it is relatively simple and difficult to have ambiguity or when it is really necessary. Multi inheritance should not be used for problems that can be solved by single inheritance. Boy, this is not something you can control 🤣🤣🤣

3. Constructor and memory model of virtual inheritance:

Different from the construction process during inheritance, the constructor of the final derived class must call the constructor of the virtual base class.

#include <iostream>
using namespace std;
//Virtual base class A
class A{
public:
    A(int a);
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
//Directly derived class B
class B: virtual public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//Directly derived class C
class C: virtual public A{
public:
    C(int a, int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
    cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}
//Indirectly derived class D
class D: public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }
void D::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}

In the constructor of the final derived class D, in addition to calling the constructors of B and C, the constructor of A is also called. Because now A has only one memory in the final derived class D, if it is constructed through B\C, the compiler will be confused and do not know which to initialize.

The execution order of constructors in virtual inheritance is different from that in normal inheritance: in the constructor call list of the final derived class, regardless of the order in which each constructor appears, the compiler always calls the constructor of the virtual base class first, and then calls other constructors in the order in which they appear; For ordinary inheritance, it is called in the order in which constructors appear.

For normal inheritance, the base class sub object is always in front of the derived class object (that is, the base class member variable is always in front of the derived class member variable), and no matter how deep the inheritance level is, its offset from the top of the derived class object is fixed (fixed position). Take the following example:

class A{
protected:
    int m_a1;
    int m_a2;
};
class B: public A{
protected:
    int b1;
    int b2;
};
class C: public B{
protected:
    int c1;
    int c2;
};
class D: public C{
protected:
    int d1;
    int d2;
};
int main(){
    A obj_a;
    B obj_b;
    C obj_c;
    D obj_d;
    return 0;
}

The memory location of class A is always ahead

1) Modify the above code so that A is the virtual base class of B:

class B: virtual public A

A will move to the back

2) Suppose A is the virtual base class of B and B is the virtual base class of C

It can be seen from the above two figures that the derived class objects in virtual inheritance are divided into two parts:

  • The offset of a part without shadow is fixed and will not change with the increase of inheritance level, which is called fixed part;
  • The shaded part is the sub object of the virtual base class, and the offset will change with the increase of inheritance level, which is called the shared part

There is a problem: how to calculate the offset of the shared part?

For virtual inheritance, the derived class is divided into fixed part and shared part, and the shared part is put last. Almost all compilers have reached a consensus on this point. The main difference is how to calculate the offset of the shared part. A hundred flowers bloom and there is no unified standard.

Here are some examples of VS solutions:

VC introduces the virtual base class table. If a derived class has one or more virtual base classes, the compiler will insert a pointer in the derived class object to point to the virtual base class table. The virtual base class table is actually an array. The elements in the array store the offset bytes of each virtual base class.

Assuming that A is the virtual base class of B and B is the virtual base class of C, the memory model of each object is shown in the following figure:

The virtual inheritance table stores the offsets of all virtual base classes (including direct inheritance and indirect inheritance) relative to the current object. In this way, when accessing the member variables of the virtual base class through the derived class pointer, no matter how deep the inheritance level is, only one indirect conversion is required.

This scheme can also avoid making the derived class object carry too many pointers when there are multiple virtual base classes. You only need to carry one pointer. For example, suppose the inheritance relationship of classes A, B, C and D is:

The memory model is:

Assign derived classes to base classes

When data type conversion occurs, when the data of int type is assigned to the variable of float type, the compiler will first convert the data of int type into float type and then assign the value; Similarly, data type conversion can also occur for a class, which is also a data type;

However, this transformation is meaningful only between the base class and the derived class, and can only assign the derived class to the base class, including assigning the derived class object to the base class object and the derived class object Pointer Assigning a value to a base class pointer and a derived class reference to a base class reference is called Upcasting in C + +. The downward transformation is called the base class.

It is very safe during the transformation.

The essence of assignment is to write the existing data into the allocated memory. The memory of the object only contains member variables, so the assignment between objects is the assignment of member variables, and there is no assignment problem for member functions.

This conversion relationship is irreversible. You can only assign values to the base class object with the derived class object, but not to the derived class object with the base class object. (because the base class does not contain member variables of derived classes, the member variables of derived classes cannot be assigned. Similarly, different derived class objects of the same base class cannot be assigned).

#include <iostream>
using namespace std;
//Base class
class A{
public:
    A(int a);
public:
    void display();
public:
    int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
    cout<<"Class A: m_a="<<m_a<<endl;
}
//Derived class
class B: public A{
public:
    B(int a, int b);
public:
    void display();
public:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
int main(){
    A a(10);
    B b(66, 99);
    //Before assignment
    a.display();
    b.display();
    cout<<"--------------"<<endl;
    //After assignment
    a = b;                  //At this time, a.m_a becomes 66;
    a.display();
    b.display();
    return 0;
}

In addition to assigning derived class objects to base class objects, you can also assign derived class pointers to base class pointers:

The following inheritance relationships:

#include <iostream>
using namespace std;
//Base class A
class A{
public:
    A(int a);
public:
    void display();
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
    cout<<"Class A: m_a="<<m_a<<endl;
}
//Intermediate derived class B
class B: public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//Base class C
class C{
public:
    C(int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int c): m_c(c){ }
void C::display(){
    cout<<"Class C: m_c="<<m_c<<endl;
}
//Final derived class D
class D: public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
void D::display(){
    cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main(){
    A *pa = new A(1);
    B *pb = new B(2, 20);
    C *pc = new C(3);
    D *pd = new D(4, 40, 400, 4000);
    pa = pd;		//Assign the pointer of pd to pa; The address of pa is also the address of pd
    pa -> display();  //But the function still uses the function of pa, which is related to the pointer type
    pb = pd;
    pb -> display();
    pc = pd;
    pc -> display();
    cout<<"-----------------------"<<endl;
    cout<<"pa="<<pa<<endl;
    cout<<"pb="<<pb<<endl;
    cout<<"pc="<<pc<<endl;
    cout<<"pd="<<pd<<endl;
    return 0;
}

It can be seen that different from the assignment between object variables, the assignment between object pointers does not copy the members of the object, nor modify the data of the object itself, but only change the pointer.

The first question: Why did the address of PA change and point to pd? Why did the function called still belong to pa?

Interpretation of output value: pa was originally a pointer to base class A, but now it points to the object of derived class D, which changes the implicit pointer this and points to the object of class D. therefore, it is not difficult to understand that the member variable of class D object is finally used inside display().

Function call explanation: Although the compiler accesses member variables through the pointer, it does not access member functions through the pointer: the compiler accesses member functions through the pointer type. For pa, its type is A. no matter which object it points to, it uses class A member functions. The specific reasons have been explained in detail in the previous notes: the principle of C + + function compilation and the implementation of member functions;

The second question: why is the address of pc not the same as that of pd, but pa, pb and pd are the same;

We usually think that assignment is to give the value of one variable to another variable. Although this idea is correct, it should be noted that the compiler may process the existing value before assignment. For example, if you assign a value of type double to a variable of type int, the compiler will directly erase the decimal part, resulting in unequal values of variables on both sides of the assignment operator

The same is true when assigning a pointer of a derived class to a pointer of a base class. The compiler may also handle it before assigning:

The pointer to the object must point to the starting position of the object. For class A and class B, the starting address of their sub objects is the same as that of class D objects, so there is no need to make any adjustment when assigning pd to pa and pb, and the existing value can be passed directly; The class C sub object has a certain offset from the beginning of the class D object. This offset should be added when assigning pd to pc, so that pc can point to the starting position of the class C sub object, that is, execute pc = pd; Statement, the compiler adjusts the value of pd, which leads to the different values of pc and pd

Memory model:

Assign derived class reference to base class reference

The reference just encapsulates the pointer, which is essentially no different. Therefore, I guess the effect of assigning a derived class reference to a base class reference should be the same as that of a pointer.

//Change the contents of the above main function
int main(){
    D d(4, 40, 400, 4000);
   
    A &ra = d;
    B &rb = d;
    C &rc = d;
   
    ra.display();
    rb.display();
    rc.display();
    return 0;
}

Sure enough, the operation results:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400

The specific analysis is the same as the pointer;

You can take a look at this blog to enhance your understanding of upward transformation
------------End of recovery------------

Tags: C++

Posted by justinh on Tue, 10 May 2022 08:16:15 +0300