C++进阶之多态

多态
- 多态的概念
- 多态的定义及实现
- 1.多态的构成条件
- 2.虚函数
- 3.虚函数的重写
- 4.虚函数重写的两个例外
- 5.C++11 override 和 final
- 6.重载、覆盖(重写)、隐藏(重定义)的对比
- 抽象类
- 1.概念
- 2.接口继承和实现继承
- 多态的原理
- 1.虚函数表
- 2.多态的原理
- 3.动态绑定与静态绑定
- 单继承和多继承关系的虚函数表
- 1.单继承中的虚函数表
- 2.多继承中的虚函数表
- 继承和多态常见的面试问题
- 1.概念查考
- 2.问答题
多态的概念
多态的概念:通俗来说,去完成某个行为,当不同的对象去完成时会产生出不同的状态 。
在C++中,多态(Polymorphism)是面向对象编程的一个重要概念,它允许你使用统一的接口来处理不同的数据类型,从而增加代码的灵活性和可扩展性。多态分为编译时多态性(静态多态性)和运行时多态性(动态多态性)两种类型。
-
编译时多态性(静态多态性):编译时多态性是通过函数重载(
Function Overloading)和运算符重载(Operator Overloading)实现的。这种多态性在编译阶段就能够确定要调用的函数或操作符,根据函数或操作符的参数类型来选择执行不同的代码。例如,函数重载允许你定义多个同名函数,但参数列表不同,编译器根据传递的参数类型来选择调用合适的函数。
void print(int num) {cout << "Printing an integer: " << num << endl; }void print(double num) {cout << "Printing a double: " << num << endl; }运算符重载则允许你定义自定义类型的操作符行为。
-
运行时多态性(动态多态性):运行时多态性是通过继承和虚函数(
Virtual Function)实现的。这种多态性允许你在运行时根据对象的实际类型来决定调用哪个函数,从而实现动态的函数分发。在运行时多态性中,基类可以定义虚函数,并且派生类可以重写这些虚函数。通过使用基类指针或引用指向派生类对象,可以在运行时调用派生类的虚函数。
class Shape { public:virtual void draw() {cout << "Drawing a shape." << endl;} };class Circle : public Shape { public:void draw() override {cout << "Drawing a circle." << endl;} };class Square : public Shape { public:void draw() override {cout << "Drawing a square." << endl;} };使用时:
Shape* shapePtr;Circle circle; Square square;shapePtr = &circle; shapePtr->draw(); // 调用 Circle 的 draw()shapePtr = □ shapePtr->draw(); // 调用 Square 的 draw()这样,根据指针指向的实际对象类型,实现了在运行时根据对象类型调用不同的函数。
总而言之,C++的多态性允许你通过统一的接口处理不同类型的对象,无论是在编译时还是运行时。这大大提高了代码的可维护性和可扩展性。
多态的定义及实现
1.多态的构成条件
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
3.虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
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 ps;Student st;Func(ps);Func(st);return 0;
}
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

多态的本质原理,符合多态的两个条件,那么调用时,会到指向对象的虚表中找到对应的虚函数地址,进行调用

多态调用运行时去指向对象的虚表中找到函数地址进行调用

普通调用在编译链接时就已经确认了函数地址,运行时直接调用
4.虚函数重写的两个例外
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A{};
class B : public A {};class Person {
public:virtual A* f() {return new A;}
};class Student : public Person {
public:virtual B* f() {return new B;}
};
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:virtual ~Person() {cout << "~Person()" << endl;}
};class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
5.C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
- final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒适" << endl;}
};
错误(活动) E1850 无法重写“final”函数 "Car::Drive" (已声明 所在行数:4)
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void test() override { cout << "Benz-舒适" << endl; }
};
错误 C3668 “Benz::test”: 包含重写说明符“override”的方法没有重写任何基类方法
错误(活动) E1455 使用“override”声明的成员函数不能重写基类成员
6.重载、覆盖(重写)、隐藏(重定义)的对比

需要注意的是,子类虚函数即使不加virtual依旧构成重写,重写的协变返回值可以不相同,但必须是父子关系的指针或引用
抽象类
1.概念
在虚函数的后面写上 =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 Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}
2.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
在面向对象编程中,接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)是两种不同的继承方式,用于在类之间共享功能和行为。让我们更详细地了解这两种继承方式:
- 接口继承(Interface Inheritance): 接口继承是一种继承方式,其中一个类(称为子类或派生类)从另一个类(称为父类或基类)继承方法声明而不继承实际实现。这种继承方式用于定义一组方法的标准接口,而不关心具体的实现。在C++中,接口继承通常通过定义纯虚函数(Pure Virtual Function)实现。
示例:
class Shape {
public:virtual void draw() = 0; // 纯虚函数,定义接口
};class Circle : public Shape {
public:void draw() override {// 实现具体的绘制圆的代码}
};class Square : public Shape {
public:void draw() override {// 实现具体的绘制正方形的代码}
};
在上面的例子中,Shape 类定义了一个纯虚函数 draw,它作为接口继承被继承类 Circle 和 Square 实现。
- 实现继承(Implementation Inheritance): 实现继承是一种继承方式,其中一个类从另一个类继承方法的声明和实际实现。这种继承方式用于共享已经存在的代码和实现。在C++中,通过普通的继承机制实现实现继承。
示例:
class Vehicle {
public:void startEngine() {// 启动引擎的代码}void stopEngine() {// 关闭引擎的代码}
};class Car : public Vehicle {
public:void drive() {// 具体的驾驶代码}
};
在上面的例子中,Car 类从 Vehicle 类进行实现继承,它继承了 startEngine 和 stopEngine 的实现。
需要注意的是,C++中的单继承限制了一个类只能从一个父类继承,这样有助于避免多继承可能带来的复杂性和歧义。
总结:
- 接口继承用于定义方法的标准接口,而不关心实际实现。通过纯虚函数来实现接口继承。
- 实现继承用于共享已有代码和实现,子类继承父类的方法和实现。
- 在实际编程中,应根据需要选择合适的继承方式,避免继承关系过于复杂,从而保持代码的可读性和维护性。
多态的原理
1.虚函数表
sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,那么派生类中这个表放了些什么呢?我们接着往下分析

针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
}
通过观察和测试,我们发现了以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成**,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员**。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的

2.多态的原理
上面分析了这么多,那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket

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 Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
- 观察下图的红色箭头我们看到,
p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。- 观察下图的蓝色箭头我们看到,
p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。- 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
- 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
- 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

void Func(Person& p)
{// 将参数 p 存储到栈上// rcx 寄存器包含了传递的参数 p 的引用mov qword ptr [rsp+8],rcx push rbp // 保存调用函数之前的 rbp 寄存器状态push rdi // 保存调用函数之前的 rdi 寄存器状态sub rsp,0E8h // 在栈上为局部变量分配空间,0xE8 字节的空间lea rbp,[rsp+20h] // 计算 rbp 的值,指向当前栈帧的基址lea rcx,[__B666B148_test@cpp (07FF629704067h)] call __CheckForDebuggerJustMyCode (07FF6296F1410h) // 调用某个函数(可能是与调试器相关的),用于检查是否只调试自己的代码p.BuyTicket(); // 调用对象 p 的 BuyTicket() 方法mov rax,qword ptr [p] // 将对象 p 的地址存储到 rax 寄存器mov rax,qword ptr [rax] // 将 p 对象的 vtable(虚函数表)的地址加载到 raxmov rcx,qword ptr [p] // 将对象 p 的地址存储到 rcx 寄存器call qword ptr [rax] // 通过虚函数表调用 p.BuyTicket() 方法// 清理栈上的局部变量分配add rsp, 0E8hpop rdi // 恢复调用函数之前的 rdi 寄存器状态pop rbp // 恢复调用函数之前的 rbp 寄存器状态
}
3.动态绑定与静态绑定
动态绑定(Dynamic Binding)和静态绑定(Static Binding)是与多态性相关的两个重要概念,用于描述在编译时和运行时如何决定调用哪个函数或方法。
- 静态绑定(Static Binding): 静态绑定是在编译时(编译阶段)确定调用哪个函数或方法的过程。在静态绑定中,编译器会根据调用表达式中的信息,确定要调用的函数,这通常发生在编译器生成目标代码时。静态绑定适用于非虚函数,普通的函数重载,以及运算符重载等。
示例:
class Base {
public:void print() {cout << "Base class" << endl;}
};class Derived : public Base {
public:void print() {cout << "Derived class" << endl;}
};int main() {Derived d;Base& b = d;b.print(); // 静态绑定,编译时确定调用 Base::print()
}
在上述例子中,尽管 b 引用的是 Derived 类的对象,但因为 print 函数不是虚函数,所以在编译时就已经确定了调用 Base::print()。
- 动态绑定(Dynamic Binding): 动态绑定是在运行时(运行阶段)根据对象的实际类型来决定调用哪个函数或方法的过程。这通常涉及虚函数的使用,其中基类中声明的函数被标记为
virtual,并且派生类中进行了重写。运行时会根据对象的实际类型(而不仅仅是引用或指针的类型)调用适当的函数。
示例:
class Shape {
public:virtual void draw() {cout << "Drawing a shape." << endl;}
};class Circle : public Shape {
public:void draw() override {cout << "Drawing a circle." << endl;}
};int main() {Circle c;Shape& s = c;s.draw(); // 动态绑定,运行时根据实际对象类型调用 Circle::draw()
}
在上述例子中,由于 draw 函数被声明为虚函数,调用 s.draw() 会根据实际对象类型 Circle 调用 Circle::draw()。
总结:
- 静态绑定 是在编译时确定函数调用的方式,主要适用于非虚函数和函数重载。
- 动态绑定 是在运行时根据对象的实际类型确定函数调用的方式,主要适用于虚函数和多态性的实现。
- 动态绑定使得多态性成为可能,使代码更加灵活和可扩展。
单继承和多继承关系的虚函数表
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;
};
观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数

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

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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

继承和多态常见的面试问题
1.概念查考
下面哪种面向对象的方法可以让你变得富有 A
A: 继承 B: 封装 C: 多态 D: 抽象
B 是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
动态绑定(Dynamic Binding)是面向对象编程中的一种机制,也被称为运行时多态性。它使得方法的调用可以在运行时根据对象的实际类型来确定,而不是在编译时就确定。这允许你在统一的接口下使用不同类型的对象,并根据对象的实际类型来决定调用哪个方法。动态绑定实现了方法的调用与具体的对象关联,使得多态性成为可能。
面向对象设计中的继承和组合,下面说法错误的是? C
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
这个说法是错误的。在面向对象设计中,通常推荐使用组合而不是继承。这被称为"组合优于继承"的原则,指的是在设计中更倾向于通过组合(对象之间的关联)来构建新的类,而不是通过继承来获得现有类的功能。这是因为继承可能引入不必要的紧耦合,破坏封装性,并导致继承链的脆弱性。正确的做法是根据具体的情况选择继承或组合。继承在某些情况下是合适的,例如当子类是父类的一种特例,并且子类不需要对父类的行为进行修改时。而在其他情况下,组合可以更好地实现代码的灵活性、可维护性和松耦合。
以下关于纯虚函数的说法,正确的是 A
A:声明纯虚函数的类不能实例化对象
B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数
关于虚函数的描述正确的是 B
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数
关于虚表说法正确的是 D
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
虚表是一个存储着虚函数指针的表格,每个类(包括基类和派生类)都可能有一个对应的虚表。下面是对其他选项的讨论:A:一个类只能有一张虚表。这个说法是不正确的。每个类(包括基类和派生类)都有可能有一个虚表,以支持多态性。B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表。这个说法是不正确的。每个类都有自己的虚表,即使子类没有重写虚函数,它也会有自己的虚表。C:虚表是在编译期间就生成的,而不是在运行时动态生成的。每个类的虚表在编译时就被创建,它存储了指向类的虚函数的指针。
假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则 D
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
下面程序输出结果是什么? A
#include<iostream>
using namespace std;
class A {
public:A(char* s) { cout << s << endl; }~A() {}
};
class B :virtual public A
{
public:B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1){cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
多继承中指针偏移问题?下面说法正确的是 C
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
在多继承中,每个基类都有自己的成员变量和虚表指针(如果有虚函数)。派生类的对象包含每个基类的成员变量,按照它们在派生类的声明中出现的顺序排列。虚表指针在派生类对象的开头。在给定的代码中,
Derive类从两个基类Base1和Base2继承,按照继承的顺序,它的内存布局是_b1,_b2,_d。而指针p1和p3都指向对象d的开头,所以它们的值相等。指针p2指向对象d中_b2成员的位置,所以它的值与p1和p3不同。所以,正确的描述是C:p1 == p3 != p2。
以下程序输出结果是什么 B
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: 以上都不正确
首先这里开辟的是派生类的对象,主要和
new有关,我们调用test函数时,因为此函数并没有被重写,所以我们正常调用的基类的test函数,传入的是A的this指针,但在其中调用的其实是B中重写的func函数,虚函数重写是接口继承,所以这里传入的val值实际为基类func函数缺省值1,所以这题选B
2.问答题
什么是多态?答:参考上面的多态概念和定义小节
什么是重载、重写(覆盖)、重定义(隐藏)?答:参考上面重载、覆盖(重写)、隐藏(重定义)的对比小节
多态的实现原理?答:参考上面多态的实现原理小节
inline函数可以是虚函数吗?答:可以,不过编译器忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
拷贝构造和operator==可以是虚函数吗?答:拷贝构造不可以,拷贝构造也是构造函数。operator==可以但无实际价值。
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考虚函数重写的两个例外小节
对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
C++菱形继承的问题?虚继承的原理?答:参考继承博客。注意这里不要把虚函数表和虚基表搞混了
什么是抽象类?抽象类的作用?答:参考抽象类小节。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
相关文章:
C++进阶之多态
多态 多态的概念多态的定义及实现1.多态的构成条件2.虚函数3.虚函数的重写4.虚函数重写的两个例外5.C11 override 和 final6.重载、覆盖(重写)、隐藏(重定义)的对比 抽象类1.概念2.接口继承和实现继承 多态的原理1.虚函数表2.多态的原理3.动态绑定与静态绑定 单继承和多继承关系…...
QtCreator中三种不同编译版本 debug、release、profile 的区别
debug调试模式,编译后的可执行文件很大,带了很多调试符号信息等,方便开发阶段调试的时候进入具体的堆栈查看值。会打开所有的断言,运行阶段性能差速度慢,可能会有卡顿感觉。 release发布模式,编译后的可执…...
golang中map赋值
众所周知,golang中map是一个指针,既然是一个指针,那么参数传递、赋值应该都是指针传递,而下面的例子也印证了我的想法 package mainimport "fmt"func test_map2(m map[string]string) {fmt.Printf("inner: %v, %p…...
myspl使用指南
mysql数据库 使用命令行工具连接数据库 mysql -h -u 用户名 -p -u表示后面是用户名-p表示后面是密码-h表示后面是主机名,登录当前设备可省略。 如我们要登录本机用户名为root,密码为123456的账户: mysql -u root -p按回车,然后…...
【深度学习_TensorFlow】过拟合
写在前面 过拟合与欠拟合 欠拟合: 是指在模型学习能力较弱,而数据复杂度较高的情况下,模型无法学习到数据集中的“一般规律”,因而导致泛化能力弱。此时,算法在训练集上表现一般,但在测试集上表现较差&…...
uniapp授权小程序隐私弹窗效果demo(整理)
<template> <view class"dealBox"><view class"txtBox padding10"><!-- 查看协议 -->在您使用施工现场五星计划小程序之前,请仔细阅读<text class"goToPrivacy" click"handleOpenPrivacyContract&qu…...
c++学习之string实现
字符串 - C引用 (cplusplus.com)这里给出标准官方的string实现,可以看到设计还是较为复杂的,有成员函数,迭代器,修饰符,容量,元素访问,字符串操作等,将字符尽可能的需求都设计出来&a…...
kubevirt虚机创建svc通过NodePort的方式暴露端口
背景 存在kubevit存在的三个虚机: ubuntu-4tlg7 7d22h Running True ubuntu-7kgrk 7d22h Running True ubuntu-94kg2 7d22h Running True 网络没有做透传,pod也不是underlay网络想要通过NodePort方式暴露虚机22端口进行远程登录。 …...
Elasticsearch终端命令行用法大全
API作用使用场景curl localhost:9200/_cluster/health?pretty查看ES健康状态curl localhost:9200/_cluster/settings?pretty查看ES集群的设置其中persistent为永久设置,重启仍然有效;trainsient为临时设置,重启失效curl localhost:9200/_ca…...
nacos版本升级注意事项
背景:nacos版本升级,1.0.1升级到2.1.2,nacos主要用作配置中心 1 从官网下载新版本nacos压缩包 2 由于1.x到2.x版本数据结构发生变化,无法沿用旧的数据库,所以新建一个数据库实例,来保存具体的nacos配置信息…...
JavaScript作用域与作用域链
JavaScript作用域与作用域链 JavaScript的作用域和作用域链是理解这门语言的关键概念之一。作用域指的是变量和函数在程序中可被访问的范围。作用域链是由函数的嵌套关系决定的变量对象的链式结构。 静态作用域与动态作用域 JavaScript使用静态作用域,也称为词法…...
MQTT异常掉线原因
一、业务场景 我们在使用MQTT协议的时候,有些伙伴可能会遇到MQTT客户端频繁掉线、上线问题 二、原因分析及异常处理 1.原因:使用相同的clientID 方案:全局使用的clientID保证唯一性,可以采用UUID等方式 2.原因: 当前用户没有Top…...
重新理解百度智能云:写在大模型开放后的24小时
在这些回答背后共同折射出的一个现实是——大模型不再是一个单选题,而更是一个综合题。在这个新的时代帆船上,产品、服务、安全、开放等全部都需要成为必需品,甚至是从企业的落地层面来看,这些更是刚需品。 作者| 皮爷 出品|产…...
Stable Diffusion 提示词技巧
文章目录 背景介绍如何写好提示词提示词的语法正向提示词负向提示词 随着AI技术的不断发展,越来越多的新算法涌现出来,例如Stable Diffusion、Midjourney、Dall-E等。相较于传统算法如GAN和VAE,这些新算法在生成高分辨率、高质量的图片方面表…...
VS2019编译curl库
下载: curl-7.61.0.tar.gz 编译: 解压到一个文件下,然后右键以管理员权限运行buildconf.bat 编译x64的库使用的是x64 Native Tools Command Prompt for VS 2019 本机工具命令提示,如果想编译x86的库,可以选择x86 Nat…...
yolov5自定义模型训练三
经过11个小时cpu训练完如下 在runs/train/expx里存放训练的结果, 测试是否可以检测ok 网上找的这张识别效果不是很好,通过加大训练次数和数据集的话精度可以提升。 训练后的权重也可以用视频源来识别, python detect.py --source 0 # webca…...
服务器中了mkp勒索病毒该怎么办?勒索病毒解密,数据恢复
mkp勒索病毒算的上是一种比较常见的勒索病毒类型了。它的感染数量上也常年排在前几名的位置。所以接下来就由云天数据恢复中心的技术工程师来对mkp勒索病毒做一个分析,以及中招以后应该怎么办。 一,中了mkp勒索病毒的表现 桌面以及多个文件夹当中都有一封…...
Docker环境搭建Prometheus实验环境
环境: OS:Centos7 Docker: 20.10.9 - Community Centos部署Docker 【Kubernetes】Centos中安装Docker和Minikube_云服务器安装docker和minikube_DivingKitten的博客-CSDN博客 一、拉取Prometheus镜像 ## 拉取镜像 docker pull prom/prometheus ## 启动p…...
Python Qt学习(七)Listview
源代码: # -*- coding: utf-8 -*-# Form implementation generated from reading ui file qt_listview.ui # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not…...
哈希表HashMap(基于vector和list)
C数据结构与算法实现(目录) 1 什么是HashMap? 我们这里要实现的HashMap接口不会超过标准库的版本(是一个子集)。 HashMap是一种键值对容器(关联容器),又叫字典。 和其他容易一样…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
