C++进阶--多态

1、多态的概念

多态就是多种形态,去完成某个行为,当不同的对象去完成时会产生不同的状态。

2、多态的定义及实现

多态的构成条件:

1、必须通过基类的指针或者引用调用虚函数。

2、调用的函数必须时虚函数,且派生类必须对虚函数进行重写(三同:函数名,返回值类型,参数类型)

class Person{
public:
    virtual void BuyTicket() const {cout << "买票-全价" << endl;}
};

class Student:public Person{
public:
    virtual void BuyTicket() const { cout << "买票-半价" << endl;}
};
void func(const Person& p) //构成了多态,使用了基类的指针或引用调用虚函数,
//而且也在派生类中对虚函数进行了重写。
{
    p.BuyTicket();
}

int main()
{
    func(Person());
    func(Student());
    return 0;
}
 


void func(const Person* p) //指针
{
    p->BuyTicket();
}
int main()
{
    Person pp;
    func(&pp);
    
    Student st;
    func(&st);

    return 0;
}
   

多态,不同对象传递过去,调用不同函数

多态调用看指向的对象

普通对象,看当前调用者类型。

构成多态的条件之一:虚函数在派生类中要重写

虚函数重写的一些细节:

1、父类必须加virtual,但是子类可以不加virtual(建议都加上)

2、协变:返回值类型可以不同,但是要求返回值必须是父子关系指针和引用

class Person{
public:
    virtual Person* BuyTicket() const {
    cout << "买票-全价" << endl;
    return 0;
    }
};

class Student:public Person{
public:
    virtual Student* BuyTicket() const {
     cout << "买票-半价" << endl;
    return 0;
    }
};
//这样虽然返回值类型不同但是还是可以构成重写的
//因为类型是具有父子关系的指针(只要是具有父子关系的类的指针都可以,没有必须是Person与Student)
class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
    Person p;
    Student s;
    return 0;
}
//这个运行结果是,析构student 析构person 析构person
//person析构了两次,因为在student子类中有一个父类
//所以person会析构两次

析构函数可以是虚函数吗?为什么需要是虚函数?

析构函数加virtual,是不是虚函数重写?

答案:是,因为类析构函数都被处理成destructor这个统一的名字。虽然他们看起来不一样。  为什么要这样处理?因为要让他们构成重写。

那为什么要构成重写呢?

我们看下面的代码:

class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; 
    delete[] ptr;
pretected:
    int* ptr = new int[10];
}
};

int main()
{
    Person* p = new Person;  //new一个父类的对象给p,也可以new一个子类的对象给p,
                              //这种情况是允许的,因为c++支持切片
    delete p;
    
    p = new Student;
    delete p; 

//当析构函数前面不写virtual时,这段代码就会发生内存泄漏,没有调到派生类的析构函数。
//为什么????
//因为,delete的时候,先调析构函数,而析构函数的名称已经不再是上面的,被处理成了统一的
//destructor()+ operator free(就是delete(p))
//此时,是普通调用,普通调用看当前调用者的类型,p的类型是Person,所以会调用两遍Person析构

//但是,我们期望的是指向谁调谁,指向父类调父类,指向子类调子类。所以必须构成多态。



    return 0;
}

设计不想被继承类,如何设计?

方法一:基类构造函数私有。因为派生类的构造必须调用基类的构造。

三个概念的对比: 重载、重写、重定义

3、抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

总结

  • 虚函数可以有实现并允许派生类重写,而纯虚函数没有实现,强制重写。
  • 定义了纯虚函数的类称为抽象类,无法被实例化;而虚函数的类则可以被实例化,只要它们没有纯虚函数。

这两者都用于实现多态性,但在设计目的和使用场景上有所不同。

抽象类有什么作用???
抽象类的主要作用是定义接口而不提供实现,用于强制派生类实现特定的功能。它通过包含纯虚函数(即未实现的虚函数)来实现,确保所有派生类都必须提供这些函数的具体实现。这有助于实现多态性,确保不同的派生类可以用相同的接口来处理不同的实现

4、多态的原理

1、父类的指针或引用指向虚函数

第一个问题,为什么必须是父类的指针或引用?????

因为,只有父类的指针才既可以指向父类的对象又可以指向子类的对象。在虚表中,子类指针只能指向子类,找到子类的虚函数,不能实现多种形态。

第二个问题,为什么不能是子类的对象去调用虚函数????

派生类的虚表和基类的虚表不一样,一部分是父类,一部分是派生类自己。派生类有自己的虚表,是因为它完成了虚函数的重写。

指针和引用的切片和对象的切片的差异是什么??

Person ps;

Student st;

ps = st;   //切片1

Person* ptr = &st; //切片2

Person& ref = st;//切片3

如果是指针,那么可以指向父类对象,在父类对象中看到的就是父类的虚表。指向子类,看到的是子类当中父类的那一部分

派生类的虚表就是先把父类的虚表拷贝下来,重写的虚函数的地址会覆盖这个位置

指向父类看到的是父类的虚函数,指向子类看到的是子类的虚函数,但是这个指针和引用看到的都是一个父类对象,如果是父类对象那就是一个父类对象,是子类对象中的父类对象就是子类对象

子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中是父类虚函数还是子类虚函数就不确定了,就乱套了。

这块不容易理解,以后再作补充。

派生类的虚表里可能会有父类的虚函数,派生类的虚表是怎么生成的??就是把父类的蓄虚表进行复制,再将重写的虚函数进行覆盖即可。

2、虚函数的重写

虚函数的重写:是重写它的实现。(属于接口继承)

为什么要完成虚函数的重写????

只有完成了虚函数的重写,派生类的虚表里面才能是派生类的虚函数,这样才能做到指向父类调父类指向子类调子类

总结:1、派生类对象中也有一个虚表指针,对象由两部分组成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

2、基类对象与派生类对象的虚表是不一样的,func1重写了,所以派生类对象中存的是重写的是派生类的func1函数,所以,虚函数的重写也叫覆盖,覆盖就是指虚函数表中虚函数的覆盖。重写是语法层面,覆盖是原理层面。

3、父类中有一个func3函数,但他不是虚函数,虽然会被继承到子类中但是它不会被放到虚表中。

4、虚表实际上就是存放虚函数指针的指针数组,一般情况下这个数组最后一个放一个nullptr

5、总结一下派生类的虚表构成,a、先将基类的虚表拷贝一份到派生类的虚表中。b、如果派生类重写了基类的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。c、派生类自己新增加的虚函数按其在派生类中的申明次序怎会加到派生类虚表的后面。

6、一个类的不同成对象共享该类的虚表。但是,不是一个类只有一个虚表,比如多继承中的派生类可能就会有两个虚表。

7、虚函数存在哪?虚表存在哪?

注意虚表存的是虚函数的指针不是虚函数,虚函数和普通函数一样都存在代码段,只是它的指针又存到了虚表。另外对象中存的不是虚表,而是虚表指针。

虚表存在哪???
堆:不可能,堆是留给我们动态申请用的,而且还要释放

栈:同类型的对象共用一个虚表,如果是存在栈帧里面,那需要存在哪个栈帧里面,函数调用的时候栈帧才能去开栈的空间,并且函数结束,栈帧销毁,虚表也会跟着销毁,那么之后再进行虚表的重建吗?所以不可能再栈里

我们可以作进一步的验证

通过验证,可以看到虚表因该是存在常量区的

静态绑定与动态绑定:

静态的多态:函数重载,在编译时的

动态的多态:继承,虚函数重写,实现的多态,允许时的(是运行时在虚表中去找地址)

5、单继承和多继承关系中的虚函数表

单继承我们上面已经详细说过了,下面主要讲一下多继承。

class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;

};

class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};

class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};

int main()
{
    //Base1 对象是8,一个虚表指针,一个整型int
    //Base2 对象同上
    //对象的前四个字节是指向该类的虚函数表的指针
    Derive d;
    cout << sizeof(d) << endl;
    return 0;
}

首先看一个问题:sizeof(d)是多大?答案是20.

派生类会继承基类的虚表,对象里面存的是虚表指针,Base1是一个指针一个int是8 Base2与1相同也是8,Derive有一个int是4,所以是20

因此,多继承里面是两个虚表

这个func3放在了第一张虚表中

6、继承和多态常见的面试问题

1、

再对上图作一些补充:

构成多态,是指向谁就调谁

p->test(); p的类型是B* 将B*传给A  即A指向B,所以调用派生类的

再看一段代码:

   class A
   {
   public:
       virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
      
   };
   
   class B : public A
   {
   public:
       void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
        virtual void test() //这时,this指针时B*
    {
             func(); //B*指向虚函数,但是不满足基类指针,所以不构成多态。
           // 所以,就是普通调用。  这里调用的是自己的,和父类的函数没有什么关系
            //结果为B->0
    }
   };
   
   int main(int argc ,char* argv[])
   {
       B*p = new B;
       p->test();
       return 0;
   }

2、动态绑定实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。

3、内联函数不能是虚函数,为什么???
a、编译时和运行时的区别:内联函数是在编译时处理的,而虚函数的调用需要在运行时根据对象的实际类型决定。

b、虚函数表的存在:虚函数依赖于虚函数表来决定调用哪个函数。这意味着函数调用需要通过虚函数表来完成,而不是直接的函数调用。内联函数的实现要求函数体在编译时可见,以便插入到调用点。虚函数表的动态性使得编译时无法确定函数调用的具体目标,从而不适合内联。

3、虚函数不能是static型函数,为什么???

虚函数的设计理念是依赖于对象实例来实现多态,而 static 函数则是类级别的,与具体的对象实例无关。static函数是属于类的,不属于任何一个对象。而且不能访问非静态成员,因为它没有this指针。因此,虚函数不能是 static 型的,因为这两者的用途和特性是不兼容的。

补充一下:非静态成员与静态成员的区别:非静态成员是指类的普通成员,它们与类的每个对象实例相关联。与静态成员不同,非静态成员的数据和函数是绑定到对象实例上的,意味着每个对象都有自己的非静态成员的副本静态成员:内存分配-----静态成员(包括静态数据成员和静态成员函数)在程序的整个生命周期内只有一份拷贝。它们在程序启动时分配内存,并在程序结束时释放。而非静态成员是在对象创建时才创建的。

4、虚表不是在运行期间动态生成的,为什么??
虚表通常是在编译期间生成的,但它的内容是在程序运行时动态决定的

一般情况下存在代码段(常量区)

虚表(vtable)是用来支持多态的机制,它包含了指向虚函数的指针。在程序运行时,根据对象的实际类型,虚表的内容会有所不同,以便正确调用对应的虚函数。

5、构造函数可以是虚函数吗??
不能,因为对象中的虚函数表指针 是在构造函数初始化列表阶段才初始化的。因为要通过这个指针去找虚函数,如果构造函数是虚函数的话,在没有虚函数表指针的情况下 怎么来找这个函数?

6、虚函数表与虚基表的区别

  • 虚函数表(Vtable):用于实现运行时的多态性,使得可以动态调用正确的虚函数实现。每个类有自己的虚函数表,并且每个对象有一个指向该表的指针。
  • 虚基表(Vbase Table):用于虚继承中,确保虚基类只存在一个实例,并提供指针以正确访问虚基类的子对象。虚基表帮助管理多重继承中的虚基类实例。

7、什么是多态???
静态多态:函数重载

动态多态:继承中虚函数重写+父类指针调用(由于指针的切片,指向父类调父类,指向子类调子类)  更方便和更灵活多种形态的调用。

8、多态的实现原理??
静态:函数名修饰规则

动态:虚函数表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值