4继承和多态

继承和多态

继承的基本意义:

继承的内存结构:

class A
{
    int a;
    int b;
    int c;
};
class B:public A
{
    int d;
    int e;
    int f;
};


int main() {
    //std::cout << "Hello, World!" << std::endl;
    A a;
    B b;
    std::cout<<sizeof(a)<<"  "<<sizeof(b)<<std::endl;
    return 0;
}
输出:12  24

派生类和基类的成员名字可以重复,因为作用域不同比如a 可以写为A::a

访问限定问题


继承方式     基类的访问限定     派生类的访问限定      (main)外部的访问限定
public 
			  public			public                       Y
			  protected			protected			        N
			  private			不可见的				    N         
protected(基类的成员的访问限定,在派生类里面是不可能超过继承方式的)
			  public			protected			        N
			  protected			protected			        N
			  private			不可见的				    N
private
			  public			private				        N
			  protected			private				        N
			  private			不可见的				    N

总结:

  1. 外部只能访问public的成员,procted和private的成员无法直接访问
  2. 在继承结构中,派生类从基类可以继承过来private的成员但是派生类却不可以直接访问
  3. 如果没有继承结构protected和private一样。存在继承结构,基类的成员想被派生类访问,但不想被外部访问,那么将权限设为protected

默认继承方式

如果是class定义的类那么默认就是以private继承
如果是struck定义的类那么默认就是以public继承

派生类的构造过程

派生类怎么初始化从基类继承而来的成员变量呢?

  1. 派生类可以继承来自基类的所有成员(变量和方法),除构造函数和析构函数

  2. 初始化基类继承而来的部分,只能调用基类的构造函数

派生类对象的构造和析构过程是:

  1. 派生类调用基类的构造函数,初始化派生类对象中属于积累的那一部分
  2. 派生类调用自己的构造函数,初始化自己的那一部分
  3. 派生类调用自己的析构函数
  4. 派生类调用积累的析构函数
class A
{
public:
    A()
    {
        cout<<"A()"<<this<<endl;
    }
    ~A()
    {
        cout<<"~A()"<<this<<endl;
    }
    int a;
    int b;
    int c;
};
class B: public A
{
public:
    B()
    {cout<<"B()"<<this<<endl;}
    ~B()
    {cout<<"~B()"<<this<<endl;}
    int d;
    int e;
    int f;
};

int main() {
    //std::cout << "Hello, World!" << std::endl;
    B b;
    cout<<&(b.a)<<" "<<&(b.d)<<endl;
    return 0;
}
输出;
A()0x2aa4dff8f0
B()0x2aa4dff8f0
0x2aa4dff8f0 0x2aa4dff8fc
~B()0x2aa4dff8f0
~A()0x2aa4dff8f0

基类部分位于低地址(对象的头部),派生类部分位于高地址

重载 隐藏 覆盖

class Base
{
public:
    Base(int data = 10):ma(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
private:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data = 20,int b=0)
        : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
//    void show(int){cout<<"Derive::show(int)"<<endl;}
private:
    int mb;
};
int main()
{
    Derive d;
    d.show();     //派生类中含有与基类同名的变量或方法,就会将基类中同名成员或方法的隐藏掉
    d.show(10);   //Base::show(10);
}

这段代码会报错Too many arguments to function call, expected 0, have 1; did you mean 'Base::show'?
因为编译器在Derive的作用域中看到了有show方法,想要调用基类中的show(int)函数必须加作用域Base::

基类 = 派生类 可行 || 派生类 = 基类 不可行

class Base
{
public:
    Base(int data = 10):ma(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
private:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data = 20,int b=0)
        : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
//    void show(int){cout<<"Derive::show(int)"<<endl;}
private:
    int mb;
};

int main()
{
    Base b(10);
    Derive d(20);
    b = d;    //Base = Derive
    d = b;
}

基类的指针指向派生类对象可行 || 派生类指针指向基类对象不可行

class Base
{
public:
    Base(int data = 10):ma(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
private:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data = 20,int b=0)
        : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
//    void show(int){cout<<"Derive::show(int)"<<endl;}
private:
    int mb;
};

int main()
{
    Base b(10);
    Derive d(20);
    Base* pb = &d;     //基类指针指向派生类对象||可行
//    Derive* pd = &b; //派生类指针指向基类对象不可行
}

基类指针可以指向派生类成员,但是只能访问派生类成员的基类部分(不强转为基类对象时)
派生类指针不可以指向基类成员,因为派生类内存空间大于基类空间。若用派生类的指针指向基类成员可能设计内存的非法访问

虚函数、静态绑定、动态绑定

什么是静态绑定:

class Base
{
public:
    Base(int data = 10):ma(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data = 20,int b=0)
        : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
    void show(int){cout<<"Derive::show(int)"<<endl;}
private:
    int mb;
};

int main()
{
    Derive d(50);
    Base* p = &d;
    p->show();
    p->show(10);
    cout<<sizeof(int)<<"  "<<sizeof(void*)<<endl;
    cout<<sizeof(Base)<<"  "<<sizeof(Derive)<<endl;
    cout<<typeid(p).name()<<" || "<<typeid(*p).name()<<endl;
}
运行结果:
Base::show()
Base::show(int)
4  8
4  8
class Base * __ptr64 || class Base

比如这个show方法的调用就是提前在编译器编译时就确定的,这就是静态的

当将基类中的函数变为虚函数时

class Base
{
public:
    Base(int data = 10):ma(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data = 20,int b=0)
        : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
    void show(int){cout<<"Derive::show(int)"<<endl;}
private:
    int mb;
};

int main()
{
    Derive d(50);
    Base* p = &d;
    p->show();
    p->show(10);
    cout<<sizeof(int)<<"  "<<sizeof(void*)<<endl;
    cout<<sizeof(Base)<<"  "<<sizeof(Derive)<<endl;
    cout<<typeid(p).name()<<" || "<<typeid(*p).name()<<endl;
}
运行结果:
Derive::show
Derive::show(int)
4  8
16  24
class Base * __ptr64 || class Derive

当类中基类中定义了虚函数结果就完全不一样了,这其中到底发生了什么事情?

一个类中添加了虚函数对这个类有什么影响?

  1. 如果类中定义了虚函数,那么在编译阶段,编译器会给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTL指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的 .rodata区

  2. 一个类里面定义了虚函数,那么这个类定义的对象其运行时,内存中开始部分多储存了一个名为vfptr的指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表

  3. 一个类里面虚函数的个数不影响对象内存的大小,影响的是虚函数表的大小

  4. 如果派生类中的方法和基类继承来的某个方法 返回值,函数名,参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理为虚函数

RTTI: run_time_type_information 运行时的类型信息

图片中的偏移量指的是 vfptr指针 在对象中距离对象内存的偏移量,但是由于,vfptr常位于对象内存的开始位置,所以这个偏移量一般为0(在没有虚继承的情况下)

class Base
{
public:
    Base(int data = 10):ma(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data = 20,int b=0)
        : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
private:
    int mb;
};

int main()
{
    Derive d(50);
    Base* p = &d;
   	//p是Base类型的指针,编译器就在Base::的作用域中找show
   	//若发现show是普通函数,就进行静态绑定 call Base::show()
   	//若发现show是virtual函数,就进行动态绑定,
   	/*
   	mov eax,dword ptr[p]   将指针pd所指对象的前四个字节放入eax
   	mov ecx,dword ptr[eax] 
   	call ecx (虚函数的地址)
   	*/
    p->show();    //(*p).show()  *p时Derive类型,所以调用
    p->show(10);
    cout<<sizeof(int)<<"  "<<sizeof(void*)<<endl;
    cout<<sizeof(Base)<<"  "<<sizeof(Derive)<<endl;
    cout<<typeid(p).name()<<"  "<<typeid(*p).name()<<endl;
    /*
    如果Base中没有虚函数,*p识别就是编译时类型 *p 就是 Base类型
    如果Base中含有虚函数,*p识别的就是运行时类型 *p就是 Derive类型
    */
}
运行得到:
Derive::show
Base::show(int)
4  8
16  24
class Base * __ptr64  class Derive

覆盖:基类和派生类的方法同名同参同返回,而且基类方法是虚函数,那么派生类的方法就会自动处理成虚函数,他们之间就是覆盖关系

虚析构函数

那些函数不能实现成虚函数

虚函数依赖:

  1. 虚函数能产生地址,存储在vftabel中
  2. 对象必须存在 (vfptr -> vftable -> 虚函数地址),若对象不存在则vfptr就不存在

构造函数 :

  1. virtual + 构造函数 : 不可以,对象在构造函数之后再回产生,没有对象就没有vfptr
  2. 在类的构造函数当中,调用虚函数,也是静态绑定(构造函数中调用其它函数(虚),不会发生动态绑定的)

static静态成员方法(virtual static NO!!!) 。 static方法调用的本质是通过类调用的而不是通过对象,所以没有 vfptr 指针,也就不可以实现为虚函数

虚析构函数

class Base
{
public:
    Base(){cout<<"Base()"<<endl;}
    ~Base(){cout<<"~Base"<<endl;}
    Base(int data):ma(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int ma;
};
class Derive : public Base
{
public:
    Derive(){cout<<"Derive()"<<endl;}
    ~Derive(){cout<<"~Derive"<<endl;}
    Derive(int data,int b)
            : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
//    void show(int){cout<<"Derive::show(int)"<<endl;}
private:
    int mb;
};

int main()
{
    Base* b1 = new Derive();
    b1->show();
    delete b1;     
    //编译器先看b1的类型是Base* 所以就在Base的作用域中找析构函数,发现析构函数是一个普通函数,就进行静态绑定
}
输出:
Base()
Derive()
Derive::show
~Base        //这里出现了问题,派生类的析构函数没有被调用!!!

编译器看到 b1 是Base* 类型,就在Base::的作用域下寻找delete要调用的析构函数,发现析构函数是一个普通函数,就进行静态绑定call Base::~Base() ,这也就导致了Derive的析构函数没有被调用。
但是,如果将基类中的析构函数写为虚函数,就会进行动态绑定,优先调用指针实际指向对象的析构函数,之后编译器会自动调用共基类的析构函数(调试发现)

什么时候把基类的析构函数必须写为虚函数?

基类的指针(引用)指向堆上new出来的对象的时候,delete 基类指针,调用析构函数的时候,必须发生动态绑定,否则导致积累的析构函数无法被调用共

再谈动态绑定

对象调用虚函数

class Base
{
public:
    Base(){cout<<"Base()"<<endl;}
    virtual ~Base(){cout<<"~Base"<<endl;}
    Base(int data):ma(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int ma;
};
class Derive : public Base
{
public:
    Derive(){cout<<"Derive()"<<endl;}
    ~Derive(){cout<<"~Derive"<<endl;}
    Derive(int data,int b)
            : Base(data),mb(b)
    {
    }
    void show(){cout<<"Derive::show"<<endl;}
//    void show(int){cout<<"Derive::show(int)"<<endl;}
protected:
    int mb;
};

int main()
{
    Base b;
    Derive d;
    b.show();  //callq  0x7ff7521c1145            ; Base::show(void) __ptr64
    d.show();  //callq  0x7ff7521c1145            ; Base::show(void) __ptr64
    cout<<"==========="<<endl;
}
Base()
Base()
Derive()
Base::show()
Derive::show
===========
~Derive
~Base
~Base

通过反汇编可以看到。对象调用虚函数是通过静态绑定的方式调用。
那么为什么要动态绑定而不是静态绑定?
因为:假设是动态绑定,调用的是各自的虚函数表中记录的虚函数,结果是一样的。但是这一过程较为麻烦所以编译器就直接编译为了静态绑定

int main()
{
    Base b;
    Derive d;
    Base* pb1 = &b;
    pb1->show();                   //动态绑定
    /*
    0x7ff694695464 <+68>:  leaq   0x28(%rsp), %rax
    0x7ff694695469 <+73>:  movq   %rax, 0x78(%rsp)
    0x7ff69469546e <+78>:  movq   0x78(%rsp), %rax
    0x7ff694695473 <+83>:  movq   (%rax), %rax
    0x7ff694695476 <+86>:  movq   0x78(%rsp), %rcx
    0x7ff69469547b <+91>:  callq  *0x10(%rax)
    0x7ff69469547e <+94>:  leaq   0x58(%rsp), %rax
     * */
    Base* pb2 = &d;
    pb2->show();                    //动态绑定
    /*
    0x7ff694695483 <+99>:  movq   %rax, 0x80(%rsp)
    0x7ff69469548b <+107>: movq   0x80(%rsp), %rax
    0x7ff694695493 <+115>: movq   (%rax), %rax
    0x7ff694695496 <+118>: movq   0x80(%rsp), %rcx
    0x7ff69469549e <+126>: callq  *0x10(%rax)
     * */
    cout<<"==========="<<endl;
}
int main() 
{   
	Base b;
	Derive d;
	Derive* p3 = (Derive*)&b;      //将基类指针强转为派生类指针
	p3->show();                    //调用了基类中的虚函数
}
输出:
Base()
Base()
Derive()
Base::show()     //调用基类中的show函数
~Derive
~Base
~Base

因为,这里用的指针调用虚函数 ,所以要调用动态绑定。就会进入对象b内,取对象b内的vfptr,由于b对象的本质是Base类对象,Base类中又存在虚函数show,所以会调用基类中的virtual show()

动态绑定:虚函数通过指针或者引用调用,才发生动态绑定 || 如果不是通过指针或引用来调用虚函数,那就是静态绑定

理解多态

如何解释多态:

静态(编译期)的多态:函数重载。模板(函数模板,类模板)
动态(运行期)的多态:在继承结构中基类的指针或者引用指向派生类的对象,通过指针(引用)调用同名覆盖方法(虚函数)。基类指针指向那个派生类对象就会调用那个派生类对象的覆盖方法(本质就是通过动态绑定实现)

#include <iostream>
using namespace std;
class Animal
{
public:
    Animal(string name) :_name(name) {}
    virtual void bark() = 0;   //纯虚函数
protected:
    string _name;
};
// 以下是动物实体类
class Cat : public Animal
{
public:
    Cat(string name) :Animal(name) {}
    void bark() { cout << _name << " bark: miao miao!" << endl; }
};
class Dog : public Animal
{
public:
    Dog(string name) :Animal(name) {}
    void bark() { cout << _name << " bark: wang wang!" << endl; }
};
class Pig : public Animal
{
public:
    Pig(string name) :Animal(name) {}
    void bark() { cout << _name << " bark: heng heng!" << endl; }
};
/*
下面的一组bark API接口无法做到我们软件涉及要求的“开-闭“原则
软件设计由六大原则   “开-闭“原则  对修改关闭,对扩展开放
*/
void bark(Animal *p)
{
    p->bark(); // Animal::bark虚函数,动态绑定了 
    /*
    p->cat Cat vftable &Cat::bark
    p->dog Dog vftable &Dog::bark
    p->pig Pig vftable &Pig::bark
    */
}
int main()
{
    Cat cat("猫咪");
    Dog dog("二哈");
    Pig pig("佩奇");
    bark(&cat);
    bark(&dog);
    bark(&pig);
    return 0;
}

当在一个类中定义了一个纯虚函数,那么这个类就成为了抽象类。
抽象类不能再实例化Animal anm这是不被允许的·,但是可以定义指针和引用变量

面经

class Base
{
public:
    virtual void show(int i=10){cout<<"Base::show  i:"<<i<<endl;}
};
class Derive : public Base
{
public:
    virtual void show(int i=20){cout<<"Derive::show  i:"<<i<<endl;}
};
int main()
{
    Base* pb = new Derive;
    pb->show();
    //输出:Derive::show  i:10
    //动态绑定调用派生类的 show() 的show方法
    //但是,对于默认参数却调用的是基类show方法的默认参数
    return 0;
}
输出:
Derive::show  i:10

要理解这个问题,需要用到函数调用堆栈的过程。在调用函数时编译器会先将函数的参数压入栈中,再去执行call指令。由于这是动态绑定,所以编译器只能看到Base作用域,而不能去加载.rodata段的vfpttr,由于在调用函数时没有传入参数,那么压栈用的参数是 Base类中show的默认参数

class Base
{
public:
    virtual void show(){cout<<"Base::show i:"<<endl;}
};
class Derive : public Base
{
private:
    virtual void show(){cout<<"Derive::show i:"<<endl;}
};
int main()
{
    Base* p = new Derive();
    p->show();
    //最终可以调用到Derive::show(),是在运行时期才确定的
    //方法的访问权限是在编译阶段就确定的,在编译阶段,编译器只能看到是Base::p调用
    //p是Base类型的指针,编译器会在Base类中找show方法,并查看权限
    //看到权限是public,在类外不可以访问,通过编译
    delete p;
    return 0;
}
输出:
cout<<"Derive::show i:"<<endl;
class Base
{
public:
    Base()
    {
        cout<<"Base"<<endl;
        clear();
    }
    void clear()
    {
        memset(this,0,sizeof(*this));
    }
    virtual void show(){cout<<"Base::show :"<<endl;}
};
class Derive : public Base
{
public:
    Derive(){cout<<"Derive"<<endl;}
    virtual void show(){cout<<"Derive::show :"<<endl;}
};
int main()
{
   /* Base* pb1 = new Base;  //调用构造器,在栈帧开辟完成时,就会创建vfptr指针
    pb1->show();   //动态绑定,访问Base类中的vfptr,访问了空指针
    delete pb1;*/

    Base* pb2 = new Derive;
    pb2->show();  //动态绑定,访问Derive类中的vfptr正常
    delete pb2;
    return 0;
}
posted @ 2022-10-18 20:20  satellite2002  阅读(31)  评论(0编辑  收藏  举报