通俗来说,多态就是指就是多种形态,具体点就是对于完成某个行为,当不同的对象去完成时会产生出不同的状态。比如买票这个行为,当普通人买票时是全价买票,学生买票时是半价买票,军人买票时是优先买票。
虚函数的概念
被关键字 virtual 修饰的类成员函数我们称其为虚函数:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
虚函数的重写/覆盖
在继承关系中,如果子类中有一个跟父类完全相同的虚函数 – 函数名、函数参数、函数返回值都相同 (三同),则称子类的虚函数重写或者覆盖了父类的虚函数。
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; } //虚函数重写
};
虚函数重写的两个特例
虚函数重写存在两个特例:
第一,子类虚函数中的 virtual 可以省略,但是父类的 virtual 一定不能省略 – 虚函数的继承是接口继承,也就是说,子类中继承得到的虚函数和父类虚函数的函数接口是完全相同的,而子类如果对虚函数进行重写,重写的也只是虚函数的实现,并没有改变虚函数的接口,所以即使我们不加 virtual 子类虚函数的类型也和父类一样,是虚函数类型。但为了程序的可读性,我们建议子类虚函数也加上 virtual。
第二,子类虚函数和父类虚函数的返回值可以不同,但必须是子类类型或父类类型的指针或者引用,我们将这种特性称为 协作。需要注意的是,这里的子类和父类不一定非要是当前子类父类,使用其他子类或父类类型作为函数返回值也可以;
注:如果子类函数和父类函数不满足这四个条件中的任意一个 – 虚函数、返回值相同、参数类型相同、函数名相同,也不属于两个特例的话,那么它们一般构成隐藏,因为隐藏只要求函数名相同。
总结:构成重写的条件:虚函数 + 三同 + 两种特殊情况;同时,需要特别注意的时,虽然在实际开发中,我们较少会遇到重写中的两种特殊情况,特别是协作,在工作中可以说几乎不会遇到 (比菱形继承的应用场景还少),但是校招中很喜欢用这两个特例来考察构成重写的条件。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person,Person 对象买票全价,Student对象买票半价。
构成多态的三个条件如下:
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 Func(Person& p)
{
p.BuyTicket();
}
如上,在 Func 函数中,我们使用父类的引用来调用虚函数 BuyTicket(),然后分别使用 父类对象 和 子类对象作为 Func 函数的参数,实现了 不同的对象进行同一种行为产生不同的状态,从而构成了多态。
普通调用和多态调用
多态调用:
像什么 BuyTicket 函数的调用我们就称为多态调用,多态调用必须满足构成多态的几种条件 – 继承、虚函数重写、由父类的指针/引用来调用虚函数,多态调用与调用此函数的对象指向/引用的类型有关; 比如在上面的 Func 函数中,pe 是 person 的对象,它作为实参传递给 p,此时 P 是 pe 的别名,p 调用 BuyTicket 函数,满足多态条件,由于 p 引用的对象 pe 的类型是 Person,所以实际上调用的是 Person 的虚函数 BuyTicket,得到结果 “买票-全价”; st 是 Student 的对象,它作为实参传递给 p (此过程发生切片,无类型转换),此时 P 是 st 的别名,p 调用 BuyTicket 函数,满足多态条件,由于 p 引用的对象 st 的类型是 Student,所以实际上调用的是 Student 的虚函数,得到结果 “买票-半价”; so 是 Soldier 的对象,它作为实参传递给 p (此过程发生切片,无类型转换),此时 P 是 so 的别名,p 调用 BuyTicket 函数,满足多态条件,由于 p 引用的对象 so 的类型是 Soldier,所以实际上调用的是 Soldier 的虚函数,得到结果 “买票-优先”;
普通调用:
多态调用之外的为普通调用,即只要不满足多态调用条件中任意一个的就是普通调用,普通调用与调用此函数的对象的类型有关;
如上,当我们将 Func 中的引用去掉以后,由于此时调用 BuyTicket 不满足多态的第三个条件 – 由父类的指针/引用调用,所以即使我们调用的是虚函数,子类中也重写了虚函数,这里仍然是普通调用,普通调用与对象的类型有关,与对象指向的类型无关,而这里不管且=切不切片,p 的类型都是 Person,所以全部调用对象类型 Person 的 BuyTicket 函数。
在上一节继承中我们提到,为了满足子类先析构,父类后析构的特性,子类析构函数调用完成后会自动调用父类析构函数完成父类成员的清理工作,不需要我们在子类析构中显式调用父类析构;对于一般场景来说,这种做法是没问题的。
class A {
public:
~A() {
cout << "~A:" << this << endl;
}
protected:
int* _a = new int[10];
};
class B : public A {
public:
~B() {
cout << "~B:" << this << endl;
}
protected:
int* _b = new int[20];
};
但是在一些特殊场景下,上述代码可能会发生内存泄漏:
可以看到,在切片场景下和动态开辟空间的场景下,由于 delete b 的步骤如下:使用指针调用析构函数析构、调用 operator delete 释放堆上 B 对象;这里虽然将调用 A 析构 将 A 的资源释放了,但是 b 是由 B 切片得来的,所以 b 中 B 对象的那部分资源并没有被释放,从而发生内存泄露。
为了解决上面这种情况,我们一般将父类的析构函数定义为虚函数,然后在子类中对虚函数进行重写:析构函数没有返回值和参数,并且所有的析构函数的函数名在编译阶段都会变成 destructor,再加上子类虚函数的 virtual 关键字可以省略,所以只要父类析构函数定义为虚函数,子类虚函数就一定可以构成重写。
在子类析构和父类析构构成重写的前提下,如果再遇到上面 切片和动态开辟 相结合的情况,就会满足多态的条件 – 子类虚函数重写 + 父类的指针/引用调用虚函数,为多态调用,与调用函数的对象 (父类的对象) 指向的类型 (子类类型) 有关,所以会去调用子类的析构;同时,由于子类析构调用完毕会自动调用父类析构,所以使得子类和父类的资源都得以清理。
注:可能设计 C++ 的大佬也是考虑到这样的场景才设计出子类虚函数可以省略 virtual 的语法。
总结:我们在继承关系中,可以无脑的将父类析构定义为虚函数;虽然虚函数会建立虚函数表,使得时空效率有一丢丢的浪费,但是它避免了可能存在的内存泄漏的风险,是完全值得的。
从上面可以看出,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; }
};
override: 检查派生类虚函数是否重写了父类某个虚函数,如果没有重写则直接编译报错:
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drvie() override { cout << "Benz-舒适" << endl; } //故意写错函数名
};
重载:
隐藏/重定义:
重写/隐藏:
在虚函数的后面写上 =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;
}
};
override 和 纯虚函数的区别:override 是检查重写,没重写直接编译报错;纯虚函数是强制我们重写,因为不重写就不能实例化出对象,但是不会不实例化对象也可以不重写。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口,重写的是实现。所以如果不实现多态,不要把函数定义成虚函数。
注:函数接口指的是函数控制块 {} 上面的部分,即函数声明,函数实现指的是函数控制块 {} 中的部分。这其实也解释了为什么子类函数不加 virtual 修饰也是虚函数。
class Person {
public:
virtual void Print(int a = 1) {
cout << "Car::Drive" << this << endl;
}
};
//函数接口
virtual void print(int a = 1)
//函数实现
cout << "Car::Drive" << this << endl;
以下程序输出结果是什么()
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: 以上都不正确
相信这道题可以难倒九成以上的同学,先说答案:B;解析如下:
1、main 函数中,p 指向一个 B 对象,然后 p 调用 test 函数,由于 B 继承 A,所以 B 中也会有 test 函数;但是由于 B 中没有对 test 函数进行隐藏或重写,所以 B 中的 test 和 A 中的 test 一模一样,包括隐藏的 this 指针;显示写出来如下:
//为了方便理解,我们将隐藏的 this 指针也写出来
class A {
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test(A* this) { this->func(); } //this的类型是A*
};
class B : public A {
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
//继承过来的test函数和A中的test一模一样
virtual void test(A* this) { this->func(); } //this的类型是A*
};
2、在此处,由于 p 是的类型是 B*,而 test 函数的 this 指针类型是 A*,所以发生切片; 3、然后,test 函数在其内部调用 func 函数,由于子类对 func 函数进行了重写,再加上 this 指针的类型是 A*,满足多态条件,所以这里是一个多态调用,多态调用看调用对象指向的类型,this 指向 p 中属于父类的那一部分,所以 this 指向的类型是 B,调用 B 的 func; 4、很多同学想到这一步之后可能直接就选 D 选项了,但是 D 是错误的,因为在多态调用中,虚函数的继承是接口继承,重写只是重写实现,所以 B 中 func 函数的接口应该使用从 A 中继承下来的接口,即 val 的缺省值为 1;至此,答案选 B。
总结:其实在绝大多数情况下,大家想到第三步就已经能够得到正确答案了,因为这里主要考察的知识点是多态的条件;而最后一步的接口继承说实话太坑了,一般都不会这样考。
我们以一道笔试题来引出虚函数表:在下面的程序中,sizeof(Base) 是多少?
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
virtual void Func2() {
cout << "Func2()" << endl;
}
void Func3() {
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
上面这种考法还是比较简单的,因为只要稍微想一想就知道答案绝不可能是 4 这么简单,怕的是它像下面这样出题:
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
virtual void Func2() {
cout << "Func2()" << endl;
}
void Func3() {
cout << "Func3()" << endl;
}
private:
int _b = 1;
char _c = 'a';
};
如果没学多态或者学的不扎实的同学看到这道题会心想,这道题不就是考察内存对齐嘛 – b 对齐 4 的倍数,占 0~3 号位置,c 对齐 1 的倍数,占 4 号位置,合计 5 个字节,再整体对齐一下,答案是 8;但答案真的是这样吗?我们看运行结果:
通过运行结果调试窗口我们可以看到,Base 类的大小是 12 字节,比我们预期的 8 字节多了四字节,这 其实是因为 Base 类中还存在一个默认的虚函数表指针 vfPtr,该指针指向一个虚函数表,也称为虚表,该虚函数表中存放的是虚函数的地址,因为普通函数 Func3 的地址并没有被放进虚表中。
注意事项:
1、在 VS 下,虚表指针放在对象的最前面,且虚基表的最后一个元素是空,但这并不代表其他平台下也是这样的,就比如 gcc 下虚表最后一个元素并不为 nullptr; 2、注意区分虚表和虚基表 – 虚表是多态中的概念,该表本质上是一个函数指针数组,里面存放的是虚函数的地址;而虚基表是菱形虚拟继承中的概念,该表本质上是一个整形数组,里面存放的是当前类与虚基类的偏移量。
现在我们对上面的代码进行改造,去除 Base 类中的 c 成员,然后增加一个派生类 Derive 去继承 Base,并在派生类中重写虚函数 Func1:
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
virtual void Func2() {
cout << "Func2()" << endl;
}
void Func3() {
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
观察调试窗口,我们可以发现:
1、子类对象 d 由两部分构成,一部分是父类成员,一部分是自己的成员;同时我们发现,子类对象中也有虚表指针,并且该虚表指针是存在于子类对象中父类那一部分中的,说明子类对象的虚基表是从父类继承/拷贝过来的。 2、我们发现,虽然子类并没有对 func2 进行重写,但是虚表中仍然有 func2 函数的地址,这是因为子类会继承父类的 func2;同时,还可以发现子类虚表指针指向的虚表和父类虚表指针指向的虚表的内容是不同的,并且不同的那部分内容刚好是子类中进行虚函数重写的那部分内容;所以子类虚表是通过拷贝父类虚表,然后将子类进行重写得到的新的虚函数地址覆盖掉虚表中该需函数原来的地址得到的。 3、这也是为什么重写也叫作覆盖的原因 – 重写指的是重写虚函数,覆盖指的是将虚表中已重写的虚函数的原地址覆盖掉;重写是语法层的叫法,覆盖是原理层的叫法 4、同时,父类中的 Func3 函数也被继承下来了,但由于 Fun3 不是虚函数,所以并不会进虚表。
总结 :子类虚表是如何生成的
拓展学习:写一个函数来找出虚表的存储位置
int main() {
int a;
cout << "栈 " << &a << endl;
int* p = new int;
cout << "堆 " << p << endl;
const char* str = "hello world";
cout << "代码段/常量区 " << (void*)str << endl;
static int b;
cout << "静态区/数据段 " << &b << endl;
Base be;
cout << "虚表 " << (void*)*((int*)&be) << endl;
Derive de;
cout << "虚表 " << (void*)*((int*)&de) << endl;
}
如上,我们可以将位于各个区域的类型的变量的地址和虚表的地址进行比对,看虚表地址和哪个类型变量的地址最接近,那么虚表就存储在哪个区域 – 因为虚表地址存储在虚表指针 vfptr 中,所以我们设法得到 vfptr 即可,而 vfptr 是一个指针变量,32 位平台下指针 4 字节,所以我们将类对象的地址强转为 int*,然后对其解引用就能得到 vfptr 了。
需要注意的是,当我们将类对象地址强转为 int* 后,此时再对其解引用得到的就是一个整形,而整形不方便和地址进行对比,所以我们可以将其转换为 指针类型,在使用 cout 输出即可。
可以看到,虚表的地址和代码段中变量的地址最接近,所以虚表存储在代码段中;同时,由于虚表存储在代码段中,所以同一类型的虚表是共享的。
注意:有的老铁可以认为虚表如果在代码段是不是就不能对其进行覆盖了,其实不是的,子类的虚表是先拷贝父类虚表,然后进行覆盖,覆盖完毕后才存储到代码段中的。
讲了怎么多,那么多态的原理到底是什么呢?我们还是以下面的代码为例:
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;
}
void Func3() {
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
如上,结合运行结果和反汇编分析:
1、可以看到,当调用 Func3 时,由于 func3 是普通函数,所以即使调用 func3 函数的是父类的指针也仍然不满足多态调用,此时调用只与对象有关,与对象指向的类型无关,由于 ptr 是 Base 的指针,所以全部调用 Base 的是父类的 func3;从反汇编我们也可以看到,调用 Func3 时是直接 call 父类 Func3 函数的地址。 2、然而当调用 Func1 时,由于 func1 是虚函数,且子类对其进行了重写,再加上是父类的指针去调用,所以这里满足多态调用;此时调用与对象指向的类型有关,当 ptr 指向父类对象 b 时,调用父类的 Func1 函数,指向子类对象 d 时,调用子类的 Func1 函数; 3、从反汇编我们能更清楚的看出多态的调用逻辑 – 当进行多态调用时,编译器会先将 ptr 移动到 eax 中,然后取出 eax 中的前四个字节内容移动到 edx 中 (这个步骤相当于取出 ptr 指向对象的虚表指针 vfptr), 然后再取出 edx 中的前四个字节内容移动到 eax 中 (因为 Func1 函数是最先定义的虚函数,所以其地址位于虚表中的前四个字节,所以这个步骤相当于取出 ptr 指向对象的虚表中 Func1 函数的地址),最后 call eax 中存放的地址,也就是 call Func1。
总结:
当进行多态调用时,父类指针 ptr 如果指向的是父类对象,就去父类对象的虚表里面取被调函数的地址,此时取出来的地址是父类的虚函数地址; 如果指向的是子类对象中父类的那一部分,则去子类对象中属于父类对象那部分中找到虚表,然后从虚表里面取出被调函数的地址,由于子类对象对虚表进行了覆盖,所以取出来的地址是子类重写后虚函数的地址; 这样,就实现了父类的指针/引用指向父类对象就调父类虚函数,指向子类对象就调用子类虚函数,从而实现了多态。所以,多态的运行原理是: 依靠虚表实现指向谁就调用谁。
拓展思考:为什么父类对象不能实现多态,而必须是父类的指针/引用?
因为子类对象赋值给父类对象时不会将子类对象的虚表拷贝给父类,也就无法运行时动态绑定;而且就算拷贝,父类也区分不了此时的父类对象的虚表是父类本身的虚表还是从子类拷贝过来的虚表,所以只能实现静态绑定,而无法实现多态。
需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,就是将所有的虚函数都放进虚表中,然后在类对象中增加一个 vfptr 来指向这个虚表而已,模型比较简单。
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;
};
观察监视窗口,我们发现,子类重写了父类的 func1,继承了父类的 func2,但是这里发生了一个奇怪的事情 – 子类自己的虚函数 func3 和 func4 并不在监视窗口中;这里其实是编译器结果优化处理后在监视窗口故意隐藏了这两个函数,也可以认为是他的一个小 bug,我们可以通过内存窗口来查看子类的虚表:
但是,每次都要通过调用内存窗口来查看虚表很不方便,所以我们可以自己写一个虚表的打印函数,如下:
//将返回值为void,参数为void的函数指针重命名为 VFPTR
typedef void(*VFPTR)();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用,调用可以看出虚表中存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" [%d]:%p,->", i, vTable[i]);
vTable[i]();
}
cout << endl;
}
此时,我们就可以很清楚的观察到了:在单继承中,子类首先会拷贝父类的虚表,然后进行重写,最后将自己特有的虚函数的地址填入虚表中。
注意事项:
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;
};
在上述代码中,父类 Base1 中有两个虚函数,要建立虚表;Base2 中也有两个虚函数,也要建立虚表;然后子类同时继承了 Base1 和 Base2,通过监视窗口,我们可以看到,子类拥有两张虚表,一张是从 Base1 拷贝过来的,一张是从 Base2 拷贝过来的;
同时,因为子类有两种虚表,所以我们也需要对子类对象调用两次 PrintVTable 函数,一次打印子类对像中父类 Base1 的虚表,另一次打印 Base2 的虚表;通过观察 PrintVTable 的打印结果我们可以发现,子类中特有的虚函数存放在第一个继承的父类的虚表的后面。
所以,在多继承中,父类一共有多少张虚表,那么子类就会拷贝多少张虚表,然后进行重写,最后将自己特有的虚函数的地址添加到最先继承的父类的虚表后面。
需要特别注意的是,子类在对一个虚函数进行重写时,如果同时对应了不同父类的虚函数,则在进行虚表覆盖时不同父类的虚表中的同一个被重写函数覆盖的不是同一个地址。
在上一节继承中我们就提到,实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗;实际上研究菱形继承和菱形虚拟继承在实际工作中是几乎没有意义的;
但是在继承中,我们说要掌握菱形继承和菱形虚拟继承,这是因为校招时要考察;但是几乎没人会去考察菱形继承和菱形虚拟继承的虚表模型,所以我们这里不再对其进行深入探索,如果好奇心比较强的童鞋,可以看看程皓大佬写的这两篇文章:
#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) :C(s1, s3), B(s1, s2), 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 解析:A B C D 是菱形虚拟继承关系,所以 D 对象中只存在一份 A,所以 A 只能调用一次构造函数,并且 A 对象应该由 D 对象来调用其构造;同时,变量初始化的顺序与变量在初始化列表出现的顺序无关,而与变量的声明顺序有关,对应到继承关系中,就是先被继承的先完成初始化。 变形:下面程序输出结果是什么? ()
#include<iostream>
using namespace std;
class A {
public:
A(char* s) { cout << s << endl; }
~A() {}
};
class B : public A
{
public:
B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C : 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) :C(s1, s3), B(s1, s2), A(s1)
{
cout << s4 << endl;
}
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
解析:A B C D 为菱形继承,所以 D 对象中的属于 B 和 属于 C 的部分各有一个 A,即 A 应该调用两次构造;同时,变量初始化 的顺序与变量在初始化列表出现的顺序无关,而与变量的声明顺序有关,对应到继承关系中,就是先被继承的先完成初始化。
所以,输出顺序是:ABACD 8. 多继承中指针偏移问题?下面说法正确的是( )
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
变形:下面说法正确的是( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
解析:子类对象模型中先被继承的父类,其父类模型会被放在子类对象模型的上方;栈的使用规则是先使用高地址,再使用低地址,而对象模型内部是先使用低地址,再使用高地址,即先继承的父类其对象模型在子类模型的低地址处。
所以,大小关系为:p1 > p2 == p3 9. 以下程序输出结果是什么()
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: 以上都不正确
解析:在上面抽象类那里讲了。
下面函数输出结果是( )
class A {
public:
virtual void f() {
cout << "A::f()" << endl;
}
};
class B : public A {
private:
virtual void f() {
cout << "B::f()" << endl;
}
};
int main() {
A* pa = (A*)new B;
pa->f();
}
A.B::f() B.A::f(),因为子类的f()函数是私有的 C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象 D.编译错误,私有的成员函数不能在类外调用 解析:虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化。
参考答案:
结语:继承和多态是 C++ 中非常重要的两个知识点,在面试中出现十分频繁,希望大家能真正把它理解,而不是浅尝辄止,亦或是在面试前背八股突击。