【C++进阶】二、多态详解(总)
目录
一、多态的概念
二、多态的定义及实现
2.1 多态的构成条件
2.2 虚函数
2.3 虚函数的重写
2.4 虚函数重写的两个例外
2.4.1 协变
2.4.2 析构函数的重写
2.5 C++11 override 和 final
2.5.1 final
2.5.2 override
2.6 重载、覆盖(重写)、隐藏(重定义)的对比
三、抽象类
3.1 抽象类概念
3.2 接口继承和实现继承
四、多态的原理
4.1虚函数表
4.2 多态原理
4.3 动态绑定与静态绑定
五、单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
5.2 多继承中的虚函数表
5.3. 菱形继承、菱形虚拟继承
六、继承和多态常见的面试问题
6.1 概念查考
6.2 问答题
一、多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
比如,在现实生活中买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票
二、多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为,在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
注:虚函数的 virtual 和虚继承中的 virtual 是同一个关键字,但是它们之间没有任何关系,虚函数的 virtual 是为了实现多态,而虚继承的 virtual 是为了解决菱形继承的数据冗余和二义性
2.3 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数,重写也叫覆盖
测试代码,比如买票
//基类 -- 普通人
class Person {
public://基类的虚函数virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
//派生类 -- 学生
class Student : public Person {
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};
//派生类 -- 军人
class Soldier : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket(){ cout << "Soldier-优先-买票" << endl;}
};
上面的派生类已经对基类的虚函数进行重写,下面再通过基类的引用调用虚函数即可构成多态
void Func(Person& p)//基类的引用
{p.BuyTicket();//基类的引用调用虚函数
}int main()
{Person p;Student s;Soldier sd;Func(p);Func(s);Func(sd);return 0;
}
运行结果,多态已经构成,BuyTicket 函数进行多态调用
下面测试使用基类的指针, 基类的指针调用虚函数也可以形成多态
void Func(Person* p)//基类的指针
{p->BuyTicket();//基类的指针调用虚函数
}int main()
{Person p;Student s;Soldier sd;Func(&p);Func(&s);Func(&sd);return 0;
}
运行结果,多态可以构成
注意:在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,派生类的虚函数也建议加上 virtual 关键字
不满足多态的任何一个条件,构造不了多态
(1)基类是引用或指针调用函数,但是基类调用的不是虚函数,无法形成多态(基类一定要有 virtual)
(2)基类不是引用或指针调用虚函数,也无法形成多态
重写(覆盖)和隐藏(重定义)的区别:
(1)隐藏的定义是:基类和派生类的成员函数名字相同,就构成隐藏
(2)虚函数重写的条件是:虚函数(virtual) + 三同
三个相同:
- 基类和派生类的成员函数名字相同
- 参数列(参数类型、数量)表完全相同
- 函数返回值相同
可以看出,虚函数的重写比隐藏的条件更苛刻
2.4 虚函数重写的两个例外
2.4.1 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,仍可构造虚函数的重写,这种行为称为协变(协变简单了解即可,平时基本不使用)
测试代码
class Person {
public:virtual Person* fun() {cout << "Person" << endl; return new Person;}
};
class Student : public Person {
public:virtual Student* fun() {cout << "Student" << endl;return new Student;}
};int main()
{Person p;Student s;//基类的引用调用虚函数Person& rp = p;rp.fun();Person& rs = s;rs.fun();cout << endl;//基类的指针调用虚函数Person* ptr1 = &p;ptr1->fun();Person* ptr2 = &s;ptr2->fun();return 0;
}
运行结果
注:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,返回基类和返回派生类的对象也可以是其它的基类或派生类,也可构成虚函数的重写
测试代码
//基类
class A
{};
//派生类
class B : public A
{};
//基类
class Person
{
public://返回基类A的指针virtual A* fun(){cout << "A* Person::f()" << endl;return new A;}
};
//派生类
class Student : public Person
{
public://返回派生类B的指针virtual B* fun(){cout << "B* Student::f()" << endl;return new B;}
};
2.4.2 析构函数的重写
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
例如,下面代码中基类 Person 和派生类 Student 的析构函数构成重写
//基类
class Person
{
public://基类和派生类析构函数构成重写virtual ~Person() { cout << "~Person()" << endl; }
};
//派生类
class Student : public Person
{
public:virtual ~Student() { cout << "~Student()" << endl; }
};
为什么要把析构函数进行重写??
因为在某种场景下析构函数的重写是必要的
比如,如果析构函数不构成重写,在这种场景下会造成内存泄漏
//基类
class Person
{
public://没有加virtual,基类和派生类析构函数不构成重写~Person() { cout << "~Person()" << endl; }
};
//派生类
class Student : public Person
{
public:~Student() { cout << "~Student()" << endl; }
};int main()
{//分别new一个父类对象和子类对象,并均用父类指针指向它们Person* p1 = new Person;Person* p2 = new Student;//使用delete调用析构函数并释放对象空间delete p1;delete p2;return 0;
}
运行结果,很明显派生类的部分没有进行析构,造成了内存泄漏,因为此时delete p1和delete p2都是调用的父类的析构函数
而我们所期望的是p1调用父类的析构函数,p2调用子类的析构函数,即我们期望析构函数的行为是的是一种多态行为
只有派生类Student的析构函数重写了 Person 的析构函数,delete 对象调用析构函数,才能构成多态,才能保证 p1和 p2指向的对象正确的调用析构函数
此时只有基类和派生类的析构函数构成了重写,才能实现多态,才能使得 delete 按照我们的预期进行析构函数的调用。因此,为了避免出现这种情况,非常建议将基类类的析构函数定义为虚函数,在什么情况下都没有错
注:在继承的时候基类和派生类的析构函数构成隐藏原因也在这里,编译器编译后把析构函数的名称统一处理成destructor(),目的就是为了形成多态
2.5 C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
2.5.1 final
final:修饰虚函数,表示该虚函数不能再被重写
测试代码
//基类
class Person {
public://被final修饰,该虚函数不能再被重写virtual void BuyTicket() final{ cout << "Person-买票-全价" << endl; }
};
//派生类
class Student : public Person {
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};
进行编译直接报错
final 关键字也可用于实现一个不能被继承的类
如何实现一个不能被继承的类??
- 把构造函数进行私有,这是 C++98 的做法
- 类定义时 加 final 关键字,这是 C++11的做法
测试代码
//基类使用 final 修饰
class A final
{};
//派生类无法进行继承基类
class B : public A
{};
编译也是直接报错
2.5.2 override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
测试代码
//基类
class Person {
public://基类的虚函数virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
//派生类
class Student : public Person {
public://派生类完成了虚函数的重写,编译通过virtual void BuyTicket()override{ cout << "Student-买票-半价" << endl; }
};
//派生类
class Soldier : public Person
{
public://派生类没有完成虚函数的重写,编译报错virtual void BuyTicket(int n)override{ cout << "Soldier-优先-买票" << endl;}
};
编译结果
2.6 重载、覆盖(重写)、隐藏(重定义)的对比
三、抽象类
3.1 抽象类概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
测试代码
class Car
{
public://纯虚函数virtual void Drive() = 0;
};int main()
{Car c;return 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;}
};int main()
{Benz B;BMW bmw;return 0;
}
编译通过,可以实例化出对象
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
四、多态的原理
4.1虚函数表
下面 Base 类实例化出对象的大小是多少?
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{Base b;cout << sizeof(b) << endl; return 0;
}
运行结果是 8
结果令人诧异,居然是8,为什么是 8 呢??
调试模式下监视窗口查看 b
通过监视窗口我们发现除了 _b 成员外还有了一个 _vfptr 在 b 对象中 (注意有些平台可能会放到对象的最后面,这个跟平台有关)
_vfptr 是一个指针,对象中的这个指针我们叫做虚函数表指针(v代表 virtual,f代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
32 位平台下,指针为 4字节,所以上面实例化出的对象 b 的大小是8,虚表指针(4字节)+ 成员 _b(4字节)
这个表放了些什么呢? 我们继续往下分析
测试代码
基类有三个成员函数,Func1 和 Func2 是虚函数,Func3 是普通成员函数,派生类仅对基类的 Func1 函数进行了重写
//基类
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://重写虚函数Func1virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
调试模式下在监视窗口查看 b 和 d 对象
通过观察发现:派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员
对象模型如下
基类b对象和派生类d对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以d的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖
重写是语法的叫法,覆盖是原理层的叫法
另外 Func2 派生类继承下来后是虚函数,所以放进了虚表,Func3 也继承下来了,但是不是虚函数,所以不会放进虚表,所以 Func3 不会在虚表里面
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
内存窗口查看
总结一下派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
虚表是什么阶段初始化的?虚函数存在哪里?
虚表实际上是在构造函数初始化列表阶段进行初始化的
注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的是指向虚表的指针,不是虚表
虚表存在哪里?
可以通过以下段代码进行判断
//基类
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://重写虚函数Func1virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{int a = 0;cout << "栈:" << &a << endl;int* p1 = new int;cout << "堆:" << p1 << endl;const char* str = "hello world";cout << "代码段/常量区:" << (void*)str << endl;static int b = 0;cout << "静态区/数据段:" << &b << endl;Base b1;cout << "虚表:" << (void*)*((int*)&b1) << endl;Derive de;cout << "虚表:" << (void*)*((int*)&de) << endl;return 0;
}
运行结果,可以发现代码当中打印了对象b1 和 对象de 的虚表指针,也就是虚表的地址,可以发现虚表地址与代码段的地址非常接近,由此我们可以得出虚表实际上是存在代码段的
4.2 多态原理
谈论那么久的虚表,那多态的原理到底是什么??
下面进行测试,测试代码,依旧是买票的例子
//基类
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:int _a = 1;
};
//派生类
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:int _b = 2;
};void Func(Person& p)//基类引用
{p.BuyTicket();//基类引用调用虚函数
}
int main()
{Person A;Func(A);Student B;Func(B);return 0;
}
也是与上面一样,进行调试查看
观察发现,对象A中包含一个虚表指针和 _a,对象 B 中也包含一个虚表指针 和 两个成员变量 _a 和 _b,这两个对象当中的虚表指针分别指向自己的虚表
模型如下
进行分析:
- 基类引用 p 指向 A 对象时,p.BuyTicket() 在 A 的虚表中找到虚函数是 Person::BuyTicket
- 基类引用 p 指向 B 对象时,p.BuyTicket() 在 B 的虚表中找到虚函数是 Student::BuyTicket
传递的是 A 对象,就直接去基类虚表中寻找该虚函数的地址
传递的是 B 对象,先会完成切片操作,再去派生类的虚表中寻找 重写的虚函数,进而完成调用,实现多态
基类的指针或者引用,调用谁,就去谁的虚表中找到对应的位置的虚函数,就实现了对应的多态的功能
这样就实现出了不同对象去完成同一行为时,展现出不同的形态
注意:同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的
总结:
- 构成多态,指向谁就调用谁的虚函数,跟对象有关
- 不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关
4.3 动态绑定与静态绑定
静态绑定:静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定:动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
测试代码
//基类
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
//派生类
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};int main()
{Student A;Person B = A;//不够成多态B.BuyTicket();Person* C = &A;//够成多态C->BuyTicket();Person& D = A;//够成多态D.BuyTicket();return 0;
}
在调试模式下查看反汇编
不构成多态的对象 A 函数调用的反汇编如下:
下面看构成多态的对象 C 和 D 函数调用的反汇编如下:
我们进行对比一下函数调用的 call
484: Person B = A;//不够成多态
004027A6 call Person::BuyTicket (04011BDh)488: C->BuyTicket();
004027BD call eax 490: D.BuyTicket();
004027D8 call eax
对于对象B,首先 BuyTicket 虽然是虚函数,但是 B 对象不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接 call 地址
对于对象 C、D,call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的
这样就很好的体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的
五、单继承和多继承关系的虚函数表
5.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;
};int main()
{Base b;Derive d;return 0;
}
在调试下的监视窗口查看
很奇怪,在监视窗口居然没有看见 func3 和 func4 , 原因是可能是VS编译器的监视窗口故意隐藏了这两个函数,也可以认为这是一个小bug,此时如果我们想要看到派生类对象完整的虚表有两个方法
直接去内存窗口查看虚表,看看有没有 func3 和 func4,内存窗口确实有 func3 和 func4
但是这种方式我们很难看出,另一个方法就是把虚表打印到输出屏上
我们可以使用以下代码,打印上述基类和派生类对象的虚表内容,在打印过程中可以顺便用虚函数地址调用对应的虚函数,从而打印出虚函数的函数名,这样可以进一步确定虚表当中存储的是哪一个函数的地址
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]);vTable[i](); // 使用虚函数地址调用虚函数}cout << endl;
}// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。int main()
{Base b;Derive d;VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}
运行结果,可以打印虚表,即可以查看完整的虚表了
5.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]);vTable[i](); // 使用虚函数地址调用虚函数}cout << endl;
}int main()
{Base1 b1;Base2 b2;PrintVTable((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容PrintVTable((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);//打印派生类对象d的第一个虚表地址及其内容VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);//打印派生类对象d的第2个虚表地址及其内容return 0;
}
两个基类 Base1 和 Base2 的虚表模型如下:
派生类的虚表模更复杂了,派生类的虚表模型如下:
内存查看
模型如下
在多继承关系当中,派生类的虚表生成过程如下:
- 分别继承各个基类的虚表内容到派生类的各个虚表当中
- 对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1
- 在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如 func3
运行结果如下
5.3. 菱形继承、菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用
六、继承和多态常见的面试问题
6.1 概念查考
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
3. 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象
B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
8. 下面程序输出结果是什么? ()
#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
9. 多继承中指针偏移问题?下面说法正确的是( )
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
10. 以下程序输出结果是什么()
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: 以上都不正确
参考答案:
1. A 2. D 3. C 4. A 5. B 6. D 7. D 8. A 9. C 10. B
6.2 问答题
1. 什么是多态?
答:参考本节课件内容
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:参考本节内容
3. 多态的实现原理?
答:构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数
4. inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的
10. C++菱形继承的问题?虚继承的原理?
答:参考上节继承内容。注意这里不要把虚函数表和虚基表搞混了
11. 什么是抽象类?抽象类的作用?
答:参考本节内容。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系
注:以上问题答案仅供参考,自己组织语言即可
----------------我是分割线---------------
文章到这里就结束了,下一篇即将更新
相关文章:
【C++进阶】二、多态详解(总)
目录 一、多态的概念 二、多态的定义及实现 2.1 多态的构成条件 2.2 虚函数 2.3 虚函数的重写 2.4 虚函数重写的两个例外 2.4.1 协变 2.4.2 析构函数的重写 2.5 C11 override 和 final 2.5.1 final 2.5.2 override 2.6 重载、覆盖(重写)、隐藏(重定义)的对比 三、…...
node-sass@4.14.1 包含风险, 如何升级依赖至 dart-sass
文章目录需求我上网都查到了哪些信息在 github 看到了 node-sass 依赖的最新版本的列表:关于方案2的失败不同版本的 nodejs 和 node-sass依赖的**适配关系**从何得知替代方案——dart-sass如何安装 dart sass?需求 在做一个基于Node、React的前端项目&a…...
DataWhale 大数据处理技术组队学习task2
三、Hadoop分布式文件系统 1. 产生背景 数据量越来越大,一台独立的计算机已经无法存储所有的数据---->将大规模的数据存储到成百上千的计算机中------为了解决数据管理以及维护极其繁琐与低效------>分布式文件系统 分布式文件系统是管理网络中跨多台计算机…...
一文读懂select、poll、epoll的用法
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,…...
《C陷阱与缺陷》----词法“陷阱”
导言: 由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷: ①.词法“陷阱” ②.语法“陷阱” ③.语义“陷阱” ④.连接问题 ⑤.库函数问…...
千锋教育+计算机四级网络-计算机网络学习-04
UDP概述 UDP协议 面向无连接的用户数据报协议,在传输数据前不需要先建立连接;目地主机的运输层收到UDP报文后,不需要给出任何确认 UDP特点 相比TCP速度稍快些简单的请求/应答应用程序可以使用UDP对于海量数据传输不应该使用UDP广播和多播应用…...
蓝桥杯算法训练合集十四 1.P08052.P07053.同余方程4.P08015.ascii应用
目录 1.P0805 2.P0705 3.同余方程 4.P0801 5.ascii应用 1.P0805 问题描述 当两个比较大的整数相乘时,可能会出现数据溢出的情形。为避免溢出,可以采用字符串的方法来实现两个大数之间的乘法。具体来说,首先以字符串的形式输入两个整数&…...
判断字符串中的字符的类型isdecimal();isalpha();isdigit();isalnum()
【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 判断字符串中的字符的类型 isdecimal();isalpha();isdigit();isalnum() [太阳]选择题 对于代码中isdecimal()和isalnum()输出的结果是? s "ABc123&…...
VSCode远程调试Linux代码,python解释器配置
安装插件并配置 安装后找到插件图标,点击 点击SSH上的 号 在弹出框中输入命令:ssh usernameip -p port username: 远程服务器的用户名 ip: 远程ip port:端口号,没有可以不用 输入完毕后点击enter 选择ssh配置文件保存…...
03:入门篇 - CTK Plugin Framework 基本原理
作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 CTK Plugin Framework 技术是面向 C++ 的动态模型系统。该系统允许插件之间的松散耦合,并且提供了设计良好的方式来进行功能和数据的交互。此外,它没有预先对插件施加限制,这样就可以很容易地将插件的相关…...
面试攻略,Java 基础面试 100 问(九)
数组有没有 length()方法?String 有没有 length()方法? 数组没有 length()方法,有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java混淆。 在 Java 中&…...
JavaScript 代码不嵌套主义
文章目录前言一、何为嵌套代码二、避免嵌套1.提炼抽取2.反转排列总结前言 看过不少过度嵌套的代码, 我真正意识到问题的严重性是刚入职那会, 我在一个老项目里看到了40个连续的else if, 套了6层的if, for和forEach, 因为我们并没有做什么限制代码嵌套的提前约定. 呃, 那之后认…...
使用默认参数的4大要点
概述 默认参数是C中新增的特性。在C中,可以为函数的参数指定默认值。调用函数时,如果没有指定实参,则自动使用默认参数。默认参数的基本语法这里就不作介绍了,下面重点介绍使用默认参数的一些知识要点。 基本规则 1、当函数中某个…...
Linux文件系统中的硬链接及常见面试题
如果能对inode的概念有所了解,对理解本文会有所帮助。如果对inode的概念不太清楚也没有关系,我们会捎带介绍一下。在文件系统的实现层面,我们可以认为包含两个组件:一个是包含数据块的池子,池子中的数据块是等大小的&a…...
opencv-StereoBM算法
原理解释目前立体匹配算法是计算机视觉中的一个难点和热点,算法很多,但是一般的步骤是:A、匹配代价计算匹配代价计算是整个立体匹配算法的基础,实际是对不同视差下进行灰度相似性测量。常见的方法有灰度差的平方SD(squ…...
图像分类竞赛进阶技能:OpenAI-CLIP使用范例
OpenAI-CLIP 官方介绍 尽管深度学习已经彻底改变了计算机视觉,但目前的方法存在几个主要问题:典型的视觉数据集是劳动密集型的,创建成本高,同时只教授一组狭窄的视觉概念;标准视觉模型擅长于一项任务且仅擅长于一项任务,并且需要大…...
Metasploit框架基础(一)
文章目录前言一、基础认知二、批量POC/EXP的构想三、poc检测框架的简单实现四、xray五、Meatsploit框架参考前言 Metasploit 一款渗透测试框架漏洞利用的集合与构建和定制满足你的需求的基础漏洞利用和验证的工具 这几个说法都是百度或者官方文档中出现的手法,说…...
pytorch零基础实现语义分割项目(二)——标签转换与数据加载
数据转换与加载项目列表前言标签转换RGB标签到类别标签映射RGB标签转换成类别标签数据数据加载随机裁剪数据加载项目列表 语义分割项目(一)——数据概况及预处理 语义分割项目(二)——标签转换与数据加载 语义分割项目&#x…...
python(8.5)--列表习题
目录 一、求输出结果题 二、计算列表元素个数 三、查找是否存在某元素 四、删除某元素 五、如何在列表中插入元素 六、如何从列表中删除重复的元素 七、 如何将列表中的元素按照从小到大的顺序排序 八、从列表中删除重复的元素 九、大到小的顺序排序 一、求输出结…...
rt-thread pwm 多通道
一通道pwm参考 https://blog.csdn.net/yangshengwei230612/article/details/128738351?spm1001.2014.3001.5501 以下主要是多通道与一通道的区别 芯片 stm32f407rgt6 1、配置PWM设备驱动相关宏定义 添加PWM宏定义 #define BSP_USING_PWM8 #define BSP_USING_PWM8_CH1 #d…...
C语言练习 | 初学者经典练习汇总
目录 1、下面代码输出多少,为什么? 2、你要好好学习么? 3、一直写代码, 4、两个数求最大值 5、输入1-5输出工作日,输入6-7输出休息日,其他输入错误 6、写一个输入密码的代码 7、怎么样当输入数字时候…...
华为OD机试 - 自动曝光(Python) | 机试题算法思路 【2023】
最近更新的博客 华为OD机试 - 卡片组成的最大数字(Python) | 机试题算法思路 华为OD机试 - 网上商城优惠活动(一)(Python) | 机试题算法思路 华为OD机试 - 统计匹配的二元组个数(Python) | 机试题算法思路 华为OD机试 - 找到它(Python) | 机试题算法思路 华为OD机试…...
「6」线性代数(期末复习)
🚀🚀🚀大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点🚀🚀🚀 目录 第五章 相似矩阵及二次型 &2)方阵的特征值与特征向量 &3ÿ…...
1.1 硬件与micropython固件烧录及自编译固件
1.ESP32硬件和固件 淘宝搜ESP32模块,20-50元都有,自带usb口,即插即用. 固件下载地址:MicroPython - Python for microcontrollers 2.烧录方法 为简化入门难度,建议此处先使用带GUI的开发工具THonny,记得不是给你理发的tony老师. 烧录的入口是: 后期通过脚本一次型生成和烧…...
【MySQL进阶】视图 存储过程 触发器
😊😊作者简介😊😊 : 大家好,我是南瓜籽,一个在校大二学生,我将会持续分享Java相关知识。 🎉🎉个人主页🎉🎉 : 南瓜籽的主页…...
[Linux篇] Linux常见命令和权限
文章目录使用XShell登录Linux1.Linux常用基本命令:1.1 ls(列出当前的目录下都有哪些文件和目录)1.2 cd (change directory 切换目录)1.3 pwd(查看当前目录的绝对路径)1.4 touch(创建文件)1.5 ca…...
29岁从事功能测试被辞,面试2个月都找不到工作吗?
最近一个28岁老同学联系我,因为被公司辞退,找我倾诉,于是写下此文。 他是14年二本毕业,在我的印象里人特别懒,不爱学习,专业不好,毕业前因为都没找到合适工作,直接去创业了…...
【C#个人错题笔记1】
观前提醒 记录一些我不会或者少见的内容,不一定适合所有人 字符串拼接 int a3,b8; Console.WriteLine(ab);//11 Console.WriteLine("ab");//ab Console.WriteLine(a""b);//38 Console.WriteLine("ab"ab);//ab38 Console.WriteLine…...
基于lambda的mongodb查询插件
需求背景需要一个像mybatis plus 一样的基于lambda, 且面向对象的查询mongo数据的插件。在网上找了很久,没有发现有类似功能的插件。于是自己手写了一个,借助mongoTemplate屏蔽了底层查询语句的实现细节。在此基础上,实现了查询的统一封装。技…...
基于微信小程序的微信社团小程序
文末联系获取源码 开发语言:Java 框架:ssm JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7/8.0 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包:Maven3.3.9 浏览器…...
网站美工培训课程/网络优化公司哪家好
作者 | Raini 出品 | 图特摩斯(北京)科技有限公司 AbutionGraph是什么? AbutionGraph是一种能对历史和实时数据提供亚秒级别查询的多维图数据存储与分析平台。 AbutionGraph支持低延时的数据摄取,灵活的数据探索分析,高性能的…...
wordpress qq邮箱 smtp/网站数据分析
据官方消息,华为鸿蒙手机操作系统将于6月2日正式发布。同时,还有很多产品会安装鸿蒙操作系统。比如华为最新的智能手表,华为MatePadPro,等等。事实上,对于业界和消费者来说,最关心的还是初始型号商名单。按…...
建设网站销售/推广网站公司
我用来绕过“完整路径和文件名”长度限制以移动,复制或删除某些内容的一个技巧是,通过使用指向文件夹的映射驱动器号 “中途”(或更多)“ 插入 ”来缩短它顺路。所以你有c:\ some \ long \ path ... \ and \ foo \ bar \ folders \ oldfiles …...
wp博客怎么改wordpress/洛阳搜索引擎优化
Kettle8.2转换组件之计算器一、相关说明二、设计转换三、转换配置四、运行转换五、结果分析一、相关说明 需求说明: 从Excel读取数据,将其中一些字段进行计算成目标数值,并把结果数据保存在Excel中。计算器组件说明: 计算器是一个…...
sae wordpress 3.9/建网站的详细步骤
注意:本文是在乌班图和Windows10环境下配置,Centos与乌班图略有不同,就是Centos的MySQL配置文件路径为/etc/my.cnf,其他操作一致1. 主从同步的定义主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时&#x…...
wordpress慢 数据库/seo网站优化助理
本文简介decorator模块是 Michele Simionato 为简化python的decorator的使用难度而开发的,使用它,您可以更加容易的使用decorator机制写出可读性、可维护性更好的代码。本文大部分翻译自下面这篇文档: www.phyast.pitt.edu/~micheles/python/documentati…...