当前位置: 首页 > news >正文

C++ 【多态】

目录

继承中要构成多态的条件(两个条件必须同时满足)

多态注意事项

 多态的原理

虚表指针

多态的本质原理是:

析构函数的重写

C++11 override

抽象类

写一个函数打印虚表

多继承

多态常考题


多态概念

完成某个行为,不同的对象去完成时会产生出不同的状态,称为多态。例如抢红包场景,不同的用户抢到金额大小不一样的红包

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

继承中要构成多态的条件(两个条件必须同时满足)

1. 派生类必须对基类的虚函数进行重写

(重写的条件是:被调用的函数必须是虚函数、还有函数名,返回值,参数必须相同才能构成虚函数,不符合重写就是隐藏关系,既不构成多态)

重载:两个函数在同一作用域、参数/函数名相同

重写(覆盖):两个函数分别在基类和派生类的作用域、参数/函数名/返回值必须相同、必须是虚函数

重定义(隐藏):函数名相同、两个函数分别在基类和派生类的作用域、不是重写就是重定义

2. 必须通过基类的指针或者引用调用虚函数

不同人买票不同价格:
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()//虚函数+三同,就是虚函数的重写/覆盖条件
	{
		cout << "半价" << endl;
	}
};

class Soldier :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "免费" << endl;
	}
};

void fun(Person& p)//必须通过基类的指针或者引用调用虚函数
                   //这时候跟p的类型无关,而是看指针or引用指向的对象
{
	p.BuyTicket();
}

 

不构成多态

多态注意事项

1.只有成员函数才能加virtual,全局函数无法加virtual

2.基类加virtual,子类不加virtual,只要子类符合三同和重写,也符合多态(不要这么做,默认加上)

3.返回值可以不同,但是要求必须是父子关系的指针或者引用(只要属于父子类型的指针或引用都可以,不能颠倒关系),称为协变

符合协变
只要是父子类型的指针或引用都可以

不能颠倒,父是父,子是子

以下程序输出结果是什么?

class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
   };

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
    B* p = new B;
    p->test();
    return 0;
}

A: A->0      B: B->1       C: A->1       D: B->0      E: 编译出错         F: 以上都不正确

解答:

首先p->test没有构成多态,A中的test传参A* this调用func,构成多态条件,调用了B类中的func:B->,其中虚函数重写是一个接口继承,重写的是实现;普通函数继承是实现继承。虚函数重写等同于把父类的函数名参数返回值类型和virtual都拿下来,实现的时候并没有使用子类的int val = 0,参数是多少都不影响

以下程序输出结果是什么?

class A
{
public:
    virtual void func(int val) { std::cout << "A->" << val << std::endl; }
    void test() { func(1); }
};

class B : public A
{
public:
    void func(int val) { std::cout << "B->" << val << std::endl; }
};


int main(int argc, char* argv[])
{
    A* p = new B;
    p->test();
    return 0;
}

A: A->1       B: B->1        C: 编译出错         D: 以上都不正确

解答:

class A中test()里面是A* this,内部this调用func,this是父类指针调用虚函数;func又是虚函数重写,构成多态条件。A* p是切片,看到的是父类的一部分,p->test是平传,是指针的传递,传递的过程中不存在切片

 多态的原理

sizeof b的大小是多少

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
    int _b = 1;
    char a = 'a';
};

int main()
{
    Base b;
    cout << sizeof(b);
    return 0;
}

sizeof b大小是12,原因在于内部多出一个指针,称为虚表指针

virtual function table虚表指针

表其实就是数组,能存储多个虚函数地址,其中func1虚函数把地址放入了虚表中

多增加了func2,也进入了虚表

虚表指针

虚函数会存进虚表,对象里面没有虚表,只有虚表指针

class A
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    } 
    virtual void Func2()
    {
        cout << "Func1()" << endl;
    }
private:
    int _a = 1;
    
};

class B :public A
{
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
    int _b;
};

void fun(A& a)
{
    a.Func1();
}

int main()
{
    A a;
    fun(a);
   
    B b;
    fun(b);
    return 0;
}

a类和b类各自都有一个虚表指针,内部指向的虚函数地址指针一个完成了重写,地址改变;一个没有完成重写,地址不变

观察以上情况,我们可以发现:

1.单继承中,派生类只有一个虚表指针,父类有虚表指针,子类就不会生成虚表指针

2.Func1完成了重写,所以d的虚表中存的是重写的B::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

构成多态后,fun函数如何实现指向谁调用谁?

实际fun函数去虚函数表中call对应函数的地址

对于A来说,看到的都是父类对象,去虚表中寻找的就是父类的虚函数;

对于B来说,看到的是虚函数表是子类重写的虚函数

多态的本质原理是:

如果符合多态的两个条件,那么调用时会到指向对象的虚表中找到对应的虚函数地址,进行调用。是在运行时去对象的虚表中确定地址。

如果不符合多态条件,例如基类没有virtual,就没有进入虚表中,直接在编译(直接call确定地址)链接(符号表)时确定地址

 如果符合多态但是子类没有完成重写,子类虚表中函数地址和父类一样,虽然也会走运行时确定地址,但是调用同一个函数

析构函数的重写

注意事项:

1.有时候建议在继承中,析构函数定义成虚函数,这时候子类和基类构成虚函数重写

2.如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写。

3.虽然函数名不相同, 但编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

为什么要重写析构函数

普通情况下,析构函数没问题。Student先析构,子类析构函数不需要显示调用父类的析构函数,会自动调用父类析构函数来完成析构,达成子类对象先析构,父类对象后析构

当父类对象new一个子类对象后,如果不把析构函数写成虚函数便会出问题

指向父类调用父类析构函数,指向子类调用子类析构函数,正好符合多态场景

class Person {
public:
    virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};


int main()
{
    Person* p1 = new Person;
    delete p1;

    Person* p2 = new Student;
    delete p2;
    return 0;
}

不加virtual,调用有问题。

不符合多态,按照类型调用对应的析构函数;符合多态,按照指向的对象去虚表中找

Person* p2 = new Student实际上调用了p2->destructor和operator delete(p2),call了p2的析构

new的是子类对象,调用的却是父类析构

多态行为让子类能够正确调用析构函数

C++11 override

override: 检查子类虚函数是否完成了重写,如果没有重写编译报错

加在子类的虚函数中

class A
{
   virtual void fun()
   {}
};

class B:public A
{
   virtual void fun() override
   {}
}

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

VS下无论是否完成重写,子类虚表指针跟父类虚表指针都不是同一个

class Person {
public:
    virtual void fun(){ cout << "~Person()" << endl; }
};

int main()
{
    Person p1;
    Person p2;
    return 0;
}
同一个类型对象,共用一个虚表
class Person {
public:
    virtual void fun(){ cout << "~Person()" << endl; }
};
class Student : public Person {
public:
    //virtual void fun(){ cout << "~Student()" << endl; }
};
int main()
{
    Person p1;
    Person p2;

    Student s1;
    Student s2;
    return 0;
}

vs下没有完成重写,子类继承父类虚表,虽然虚表指针不同,里面内容相同

 

写一个函数打印虚表

虚函数表是一个函数指针数组,想打印虚表只需要拿到里面内容即可

虚表的地址在对象的头4/8字节中

class Person {
public:
    virtual void fun1() { cout << "Person: fun1" << endl; }
    virtual void fun2() { cout << "Person: fun2" << endl; }
};
class Student : public Person {
public:
    virtual void fun1() { cout << "Student: fun1" << endl; }
    virtual void fun3(){ cout << "Student: fun3" << endl; }
};

typedef void(*VTP)();//把返回值为void,参数为null的函数指针重命名为VTP

void PrintVT(VTP t[])
{
    for (size_t i = 0; i < 3; ++i)
    {
        printf("VT:%d,t[%p],", i, t[i]);
        t[i]();
    }
}

int main()
{
    Person p1;
    Person p2;

    Student s1;
    Student s2;

    PrintVT((VTP*)*(int*)&s1);//强转成int*取头四个字节,解应用是为了拿到虚表指针内部的数据
    return 0;
}
fun1重写,fun2没重写,fun3子类中的虚函数没重写

虚函数都要进入虚表,父类的虚函数进父类虚表,子类虚函数包含两个部分:父类/自己的虚函数

多继承

单继承情况下,子类继承使用的还是父类的虚表,只是完成了重写,整个对象中只有一张虚表

C多继承中sizeof大小是20,A和B都是8。

C中存在两个虚表

class A {
public:
    virtual void fun1() { cout << "A: fun1" << endl; }
    virtual void fun2() { cout << "A: fun2" << endl; }
private:
    int _a=1;
};

class B {
public:
    virtual void fun1() { cout << "B: fun1" << endl; }
    virtual void fun2(){ cout << "B: fun2" << endl; }
private:
    int _b=2;
};

class C :public A, public B
{
    virtual void fun1() { cout << "C: fun1" << endl; }
    virtual void fun3() { cout << "C: fun3" << endl; }
private:
    int _c=3;
};


int main()
{
  
    C c;
    cout << sizeof c;
    return 0;
}

 多继承中C重写了fun1,把两张虚表中的fun1都重写,没有重写fun2,两张表放的分别是A,B的fun2

那么fun3放在哪里?监视窗口看不见子类新增的虚函数

打印第一个虚表,可以看见子类新增的虚函数进入了A的虚表中

打印第二个虚表,没有新增

多继承中子类新增的虚函数进入第一个继承的虚表中

    C c;
    PrintVT((VTP*)*(int*)&c);
    cout << endl;
    PrintVT((VTP*)(*(int*)((char*)&c+sizeof A)));

为什么fun1的地址不同?

 重写的都是C类的函数fun1,地址却不同,原因在于切片指针偏移,最终调用的还是同一函数

多态常考题

1. inline函数可以是虚函数吗?答:不可以但是编译通过,因为编译器会忽略inline属性(inline对编译器只是建议),这个函数就不再是inline,多态调用时liline失效,因为虚函数要放到虚表中去,而内联函数没有地址(调用地方直接展开)。

2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,得使用类域指定方式调用,属于编译时决议。多态是运行时决议。使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。

5. 对象访问普通函数快还是虚函数更快?答:虚函数构成多态,普通函数快,因为需要去虚表中找徐函数地址;否则一样快

6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

7. 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

8. 什么是多态? 参考最开始:多态概念

9.什么是重载、重写(覆盖)、重定义(隐藏)?参考标题:继承中要构成多态的条件(两个条件必须同时满足)

10.多态的实现原理?参考标题:多态的本质原理是:

11.拷贝构造和赋值运算符重载可以是虚函数吗?答:拷贝构造不可以,拷贝构造也是构造函数,有初始化列表。赋值运算符重载可以,但是子类中没有完成重写,因为参数不同。可以修改参数为父类的const& 等同于父给子

相关文章:

  • 一台服务器最大能支持多少条TCP连接
  • Linux中目录的概述以及 查看 切换 创建和删除目录
  • 剑指 Offer 03. 数组中重复的数字
  • 5_会话管理实现登录功能
  • 【STL】STL入门(9)
  • 超市积分管理系统(Java+Web+MySQL)
  • 超级简单的机器学习入门
  • 基于SSM跨境电商网站的设计与实现/海外购物平台的设计
  • Flutter——常用布局
  • RBF神经网络python实践学习(BP算法)
  • _Linux 动态库
  • spring5(一):概述
  • C++基础知识梳理<2>(引用、内联函数、auto关键字) [入门级】
  • Halcon图像分割总结
  • 学习笔记-WinRM
  • Java内存模型与volatile
  • LIO-SAM中的mapOptmization
  • Pandas数据处理可视化
  • NA of optical fiber(光纤的数值孔径)
  • 花了整整一天,总结了C语言所有常用的文件操作