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

第十三讲:C++多态

目录

1、多态

1.1、概念

1.2、多态的构成条件

1.3、虚函数

1.4、虚函数的重写

1.5、虚函数重写的例外

1.5.1、协变

1.5.2、析构函数的重写

1.6、重载、覆盖、隐藏的对比

1.7、C++11 override 和 final

1.8、抽象类

2、原理

2.1、虚函数表

2.2、多态的原理

2.3、动态多态与静态多态

2.4、单继承与多继承

2.4.1、单继承中的虚函数表

2.4.2、多继承中的虚函数表

2.5、菱形继承与菱形虚拟继承

2.5.1、菱形继承与虚表 

2.5.2、菱形虚拟继承与虚表

3、题目

3.1、第一题

2.2、第二题

3.3、第三题

3.4、第四题

3.5、第五题

3.6、第六题

3.7、第七题


1、多态

1.1、概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。

举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票。

1.2、多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

在继承中要构成多态还有两个条件:

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

2、且派生类必须对基类的虚函数进行重写。

这两个条件只要有一个不满足就没法构成多态。

1.3、虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

例如:

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

注意:这里的虚函数和之前继承中的虚继承中虽然都用了virtual关键字,但两者没有任何关系。

1.4、虚函数的重写

虚函数的重写又称为覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

注意:这里的参数列表完全相同是指形参对应的类型相同,形参的名字可以不相同。

例如:

class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*///虚函数的重写本质上重写的是虚函数的实现,因此子类可以不加virtual,重写的是实现,接口还是用的父类的接口。/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

运行结果为:

再比如我们破坏多态构成的第一个条件,将上面的代码改为下面的代码:

class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*///虚函数的重写本质上重写的是虚函数的实现,因此子类可以不加virtual,重写的是实现,接口还是用的父类的接口。/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

运行结果为:

1.5、虚函数重写的例外

1.5.1、协变

派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,但返回的必须是基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

例如:

class Person 
{
public:virtual Person* BuyTicket() { cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person 
{
public:virtual Student* BuyTicket(){ cout << "买票-半价" << endl; return nullptr;}
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

注意:这里基类虚函数返回基类对象的指针或者引用与派生类虚函数返回派生类对象的指针或者引用不一定要返回的是自己这个类的指针或者引用,也可以返回其他满足继承关系的类,例如:

class A
{
public:void func(){}protected:int _a;
};class B : public A
{
public:void f(){func();++_a;}
protected:int _b;
};class Person 
{
public:virtual A* BuyTicket() { cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person 
{
public:virtual B* BuyTicket(){ cout << "买票-半价" << endl; return nullptr;}
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}
1.5.2、析构函数的重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

例如:

class Person 
{
public:virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person 
{
public:virtual ~Student() { cout << "~Student()" << endl; } // 这里的virtual不加也是可以的
};// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

运行结果为:

注:普通调用是按照类型进行调用的。多态这里的调用是按照指针或者引用的指向来进行调用的。

1.6、重载、覆盖、隐藏的对比

重载:在同一作用域,函数名相同,参数不同(类型、个数、顺序)的函数,构成重载。

重定义(隐藏):两个函数分别在基类和派生类的作用域,且函数名相同,构成重定义。

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

注:基类和派生类的同名函数不构成重写,就构成重定义。

1.7、C++11 override 和 final

C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

final:修饰虚函数,表示该虚函数不能再被重写。例如:

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};

会报错,因为不能再被重写了。 

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。例如:

class Car {
public:void Drive() {}
};class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

再比如:

class Car {
public:virtual void Drive(int n) {}
};class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

虚函数肯定是要完成重写的,不然没有任何价值。

1.8、抽象类

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

例如:

class Car
{
public:virtual void Drive() = 0;
};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};void func(Car* ptr)
{ptr->Drive();
}int main()
{// 会报错//Car c;Car* c;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();func(new Benz);func(new BMW);return 0;
}

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

2、原理

2.1、虚函数表

例如:

// sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function,本质上虚函数表指针指向的的就是虚函数指针数组)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

再比如:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}void Func3(){cout << "Func3()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;Base b;return 0;
}

调试窗口可以看到:

再比如:

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}private:int _b = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}

调试窗口可以看到: 

通过观察和测试,我们发现了以下几点问题:

1、派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是从父类继承下来的成员,另一部分是自己的成员。

2、基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法层面的叫法,覆盖是原理层的叫法。

3、另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

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

6、这里还有一个很容易混淆的问题:虚函数存在哪的呢?虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是虚函数的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

注意:同一个类的对象共用同一张虚表。

2.2、多态的原理

例如:

class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person p;Func(p);Student s;Func(s);return 0;
}

1、观察下图的红色箭头我们看到,func函数的形参p是p对象的引用时,p.BuyTicket在p对象的虚表中找到虚函数是Person::BuyTicket。

2、观察下图的绿色箭头我们看到,func函数的形参p是s对象的引用时,p.BuyTicket在s对象的虚表中找到虚函数是Student::BuyTicket。

3、这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

4、反过来思考我们要达到多态,有两个条件,第一个条件是虚函数覆盖,第二个条件是对象的指针或引用调用虚函数。

注意:子类的对象赋值给父类的对象是不会拷贝虚表指针的,正因如此必须要是对象的指针或者引用来调用虚函数。

5、满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中去找的。不满足多态的函数调用时编译链接时确认好的。

注意:与普通函数不同,要想获取类中成员函数的地址,需要指定类域并使用&函数名的方式获取,不能使用指定类域并使用函数名的方式获取函数地址。

2.3、动态多态与静态多态

1、在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载。

2、在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态 

2.4、单继承与多继承

2.4.1、单继承中的虚函数表

例如:

class Base 
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};class Derive :public Base 
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};// 打印虚表
typedef void (*VFUNC)();
void PrintVFT(VFUNC a[])
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];f();}printf("\n");
}int main()
{// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数,指针的指针数组,这个数组最后面放了一个nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFUNC*,因为虚表就是一个存VFUNC类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVFT进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。Base b;PrintVFT((VFUNC*)(*((int*)&b)));Derive d;PrintVFT((VFUNC*)(*((int*)&d)));return 0;
}

运行结果为:

注:虚函数的地址一定会被放进虚函数表。 

2.4.2、多继承中的虚函数表

例如:

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;
};// 打印虚表
typedef void (*VFUNC)();
void PrintVFT(VFUNC a[])
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];f();}printf("\n");
}int main()
{Derive d;PrintVFT((VFUNC*)(*(int*)&d));PrintVFT((VFUNC*)(*(int*)((char*)&d + sizeof(Base1))));// 上面的这个也可以像下面这样写Base2* ptr = &d;PrintVFT((VFUNC*)(*(int*)ptr));return 0;
}

运行结果为:

从上面可以看出,多继承派生类的未重写的虚函数会放在第一个继承的基类的虚函数表中。

有几个父类有虚函数,子类就有几个虚函数表。

2.5、菱形继承与菱形虚拟继承

实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定的性能损耗。

2.5.1、菱形继承与虚表 

菱形继承本质上就是多继承,和一般的多继承的区别就是数据冗余和二义性,除此之外两者没什么区别。

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}
public:int _a;
};class B : public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}
public:int _b;
};class C : public A
{
public:virtual void func1(){cout << "C::func1()" << endl;}
public:int _c;
};class D : public B, public C
{
public:virtual void func1(){cout << "D::func1()" << endl;}virtual void func2(){cout << "D::func2()" << endl;}
public:int _d = 1;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

D对象总共有两张虚表,分别继承自B和C。与多继承一样,func2会被放到第一张虚表中。

2.5.2、菱形虚拟继承与虚表

例如:

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}
public:int _a;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}public:int _b;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1()" << endl;}public:int _c;
};class D : public B, public C
{
public:virtual void func1(){cout << "D::func1()" << endl;}virtual void func2(){cout << "D::func2()" << endl;}
public:int _d = 1;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

总共有两张虚表, D单独有一个虚表,B和C共享一个虚表。之所以D会单独创建一个虚表的原因是D中有一个func2,D的func2原本是要放到B的虚表中的,但是B自己没有独立的虚表。

再比如:

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}
public:int _a;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}virtual void func3(){cout << "B::func3()" << endl;}
public:int _b;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1()" << endl;}virtual void func5(){cout << "C::func5()" << endl;}
public:int _c;
};class D : public B, public C
{
public:virtual void func1(){cout << "D::func1()" << endl;}virtual void func2(){cout << "D::func2()" << endl;}
public:int _d = 1;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

通过内存窗口进行查看:

总共有三张虚表,分别是A中的虚表和B以及C中的虚表 。

3、题目

3.1、第一题

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

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()
{B* p = new B;p->test();return 0;
}

重写的本质是重写的实现,接口用的还是父类的接口,因此运行结果为:B->1

2.2、第二题

inline函数可以是虚函数吗?

答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。这样被inline修饰的虚函数,当是普通调用时,具有内联属性,inline起作用;当是多态调用时,就不具有内敛属性,inline不起作用。

3.3、第三题

静态成员函数可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,可以使用 类型::成员函数 的调用方式,无法构成多态。

3.4、第四题

构造函数可以是虚函数吗?

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

3.5、第五题

析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,当父类指针 = new 子类对象,delete 父类指针时,把基类的析构函数定义成虚函数。

3.6、第六题

对象访问普通函数快还是虚函数更快?

答:首先如果是普通调用的话(当不满足多态条件时调用虚函数),两者是一样快的。如果是多态调用,则调用普通函数快,调用虚函数慢,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

3.7、第七题

虚函数表是在什么阶段生成的,存在哪的?

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

http://www.dtcms.com/a/304073.html

相关文章:

  • 多个参数组合生成sql的方法
  • BERT 的 NSP慢慢转换为SOP
  • arm v3 smmu 图示
  • Android四大组件之BroadcastReceiver解析
  • [2025CVPR]HUGNet2架构:事件相机光流估计
  • 智能落地扇方案:青稞RISC-V电机 MCU一览
  • Java 10 新特性解析
  • 【嵌入式电机控制#18】有刷直流串级控制
  • Redis的持久化策略-AOF和RDB(详细图解)
  • 同创永益 IStorM Chaos 混沌工程平台智能化:智能实验工作台,多智能体协作,筑牢系统稳定性防线
  • [css]旋转流光效果
  • RabbitMQ 消息持久化的三大支柱 (With Spring Boot)
  • 焊接机器人智能化气体节约方案
  • arm smmu v3 队列实现机制
  • 【Linux知识】Linux Shell 脚本中的 `set -ex` 命令深度解析
  • Spark SQL 数组函数合集:array_agg、array_contains、array_sort…详解
  • 水仙花数(python)
  • CMake 完全实战指南:从入门到精通
  • Linux重定向的理解
  • VR全景制作流程分享-众趣VR全景制作平台
  • 【微信小程序】12、生物认证能力
  • 【MySQL基础篇】:MySQL表的约束常用类型以及实战示例
  • 算法篇----前缀和
  • 【前端开发】一. html css js 初印象
  • 验证pyspark提交参数指定环境变量生效
  • 什么情况下会出现数据库和缓存不一致的问题?
  • VS Code编辑器
  • jvm冷门知识十讲
  • Three.js实现银河螺旋星云粒子特效——原理、实现
  • 译 | 介绍PyTabKit:一个试图超越 Scikit-Learn的新机器学习库