第一章: 多态的概念
1.1 概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
第二章:多态的定义及实现
2.1多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2 虚函数
class Person {
public:
virtual void BuyTicket() { cout << "Person买票-全价" << endl; }
};
2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "Person买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "Student买票-半价" << endl; }
//注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写
//(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
//void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p) { p.BuyTicket(); }
int main() {
Person ps;
Student st;
//函数参数是父类的引用,所以既可以传父类对象,也可以传子类对象
Func(ps);//Person买票-全价
Func(st);//Student买票-半价
return 0;
}
虚函数重写的两个例外
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
//第一种
class Person {
public:
virtual Person* BuyTicket() { cout << "Person买票-全价" << endl; }
};
class Student : public Person {
public:
virtual Student* BuyTicket() { cout << "Student买票-半价" << endl; }
};
//第二种
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; }
};
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
为什么要将析构函数改为虚函数
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main() {
//下方delete调用的都是父类的析构函数
//delete是先调用析构函数,在调用operator delete释放。
//由于多态的原因,父子类的析构函数名都被统一处理为destructor。
//p是Person类型指针,所以指向父类的析构函数。
//但父类指针有可能指向父类,也有可能指向子类,期望是多态调用
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
//子类重写父类虚函数可以不加virtual就是为这里准备的
};
int main() {
//析构函数需要改为虚函数是为了解决new/delete场景下析构对象不正确
//Person* p1 = new Person;
//Person* p2 = new Student;
//delete p1;
//delete p2;
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
2.4 C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
1. final:修饰虚函数,表示该虚函数不能再被重写
class Car {
public:
//void Drive() final {} //非虚函数不能用final修饰
virtual void Drive() final {}
};
class Benz :public Car {
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }//报错
};
2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
2.5 重载、覆盖(重写)、隐藏(重定义)的对比
第三章:抽象类
3.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 func(Car* ptr) { ptr->Drive(); }
int main() {
//Car c;//报错。无法实例化抽象类
//Car* c;//指针可以定义
Benz b;//重写纯虚函数才能定义
func(new Benz);//Benz - 舒适
func(new BMW);//BMW - 操控
return 0;
}
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
第四章:多态的原理
4.1虚函数表
这里常考一道笔试题:sizeof(Base)是多少?
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
private:
int _b = 1;
};
int main() {
Base b;
cout << sizeof(Base) << endl;//8
return 0;
}
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面 (注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?
针对上面的代码做出以下改造
- 增加一个派生类Derive去继承Base
- Derive中重写Func1
- 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;
}
1.派生类对象d中包含一个虚表指针。d对象的内存结构由两部分组成:一部分是从父类继承下来的成员(其中包含继承自父类的虚表指针),另一部分是派生类自己的成员。
2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5.总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6.虚函数存在哪的?
答:对象中存虚表指针,虚表指针指向虚表;虚表中存虚函数地址。虚函数和普通函数一样,都是存在代码段的。
虚表存在哪的?
class Base {
public:
virtual void Func1() { cout << "Base::Func1()" << endl; }
virtual void Func2() { cout << "Base::Func2()" << endl; }
private:
int a;
};
void func() { cout << "func()" << endl; }
int main() {
Base b1;
static int a = 0;
int b = 0;
int* p1 = new int;
const char* p2 = "hello world";
printf("静态区:%p\n", &a);
printf("栈:%p\n", &b);
printf("堆:%p\n", &p1);
printf("代码段:%p\n", &p2);
printf("虚表:%p\n", (int*)&b1);//vs中虚表指针通常存在前4个字节
printf("虚函数:%p\n", &(Base::Func1));//成员函数名要加&才能取到地址
printf("普通函数:%p\n", func);
//从打印的地址来看,虚表的地址和代码段靠的最近
//虚函数地址和普通函数地址靠的最近
//所以虚表存在代码段
return 0;
}
4.2多态的原理
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void func() {}
private:
int a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
int b = 1;
};
void Func(Person& p) { p.BuyTicket(); }
int main() {
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
为什么只有当子类赋值给父类的指针/引用才能构成多态?
1.父类指针/引用指向子类对象时
a.指针或引用实际指向的是子类对象内存中的父类部分(因为子类对象的内存布局是父类成员在前,子类新增成员在后)。
b.虚表指针(vptr)仍然是子类的虚表指针(因为对象本身是子类对象,虚表指针在构造时就被初始化为子类的虚表)。
c.调用虚函数时,通过这个子类的虚表指针找到子类重写的虚函数(动态绑定)。
2.对象赋值(切片)时
a.子类对象赋值给父类对象,会发生对象切片(Object Slicing),即仅复制父类部分的成员(子类新增成员被丢弃)。
b.虚表指针也会被覆盖为父类的虚表指针(因为赋值后,新对象是一个独立的父类对象,不再和子类有关联)。
c.因此,通过这个父类对象调用虚函数时,只能调用父类的实现(多态失效)。
为什么是父类虚表指针覆盖子类?
当发生 Base b = d; (d 是子类对象)时:
1.赋值操作的静态类型是 Base
C++ 编译器只认为b是一个Base对象,因此只会拷贝d中属于Base的部分(包括Base的成员变量和虚表指针)。
2.虚表指针的初始化规则
a.对象的虚表指针是在构造函数中初始化的。
b.Base b = d; 实际上调用的是Base的拷贝构造函数(或拷贝赋值运算符),而Base的拷贝构造函数会将虚表指针设为Base的虚表(因为b是一个新构造的Base对象,与Derived无关)。
c.子类的虚表指针不会被复制到b中,因为Base的拷贝操作根本不“知道”子类的存在。
4.3 动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
第五章:单继承和多继承关系的虚函数表
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; }
void func5() { cout << "Derive::func5" << endl; }
private:
int b;
};
class X :public Derive {
public:
virtual void func3() { cout << "X::func3" << endl; }
};
int main() {
Base b;
Derive d;
//通过监视窗口中发现d对象中看不见func3和func4
//因为父类并不总是Base,X的父类是Derive
//Derive并不总是子类,它还是X的父类
X x;//x中也看不到func3
//验证func3是否可以多态调用
Derive* p = &d;
p->func3();
p = &x;
p->func3();//经验证,可以实现多态调用
//说明vs2019只展示被实际重写或与基类相关的虚函数。
return 0;
}
打印虚表中函数地址
typedef void(*VFPTR) ();//重命名该类型的函数指针void(*)() 为 VFPTR
void PrintVFT(VFPTR a[]) { //a是一个数组,每个元素是函数指针
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << "虚表地址>" << a << endl;
for (int i = 0; a[i] != nullptr; ++i) {
//a[i]是虚函数地址,光有地址还不够,需要用地址去调用虚函数才能确定
printf("第%d个虚函数地址:0x%p -> ", i + 1, a[i]);//a[i]是虚函数表中第i个槽位(slot)存储的函数地址
VFPTR f = a[i];//从虚函数表(vtable)中获取第i个虚函数的地址,并将其赋值给函数指针f。
f();//这里通过函数指针f直接调用该虚函数。
//由于f指向的是虚函数表中的具体实现,所以实际调用的是对象(Base或Derive)对应的虚函数。
}
cout << endl;
}
int main() {
Base b;
Derive d;
X x;
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个
// 存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVFT进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,
//导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
VFPTR* ab = (VFPTR*)(*(int*)&b);
PrintVFT(ab);
VFPTR* ad = (VFPTR*)(*(int*)&d);
PrintVFT(ad);
VFPTR* ax = (VFPTR*)(*(int*)&x);
PrintVFT(ax);
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 PrintVFT(VFPTR a[]) {
cout << "虚表地址>" << a << endl;
for (int i = 0; a[i] != nullptr; ++i) {
printf("第%d个虚函数地址:0x%p -> ", i + 1, a[i]);
VFPTR f = a[i];
f();
}
cout << endl;
}
int main() {
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVFT(vTableb1);
//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
//转换为char*是为了按字节计算偏移量
//+sizeof(Base1)指向Derive对象中Base2子对象的起始位置。
//PrintVFT(vTableb2);
Base2* ptr = &d;//直接将子类赋值给父类指针,发生切片。
//ptr指向d对象中Base2的部分
PrintVFT((VFPTR*)(*(int*)ptr));
Base1* p1 = &d;
p1->func1();
Base2* p2 = &d;
p2->func1();
return 0;
}
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
为什么会有两个虚表?为了多态调用。如果只有一个虚表,无法切片
虽然p1和p2调用的都是d对象的func1,但是两个func1的地址不同
虽然Derive重写了两个func1。但p1和p2调用的又是同一个函数。
Base1和Base2各自有自己的虚表指针(vptr),分别指向不同的虚表。
Derive重写了func1(),这两个虚表中的func1条目都会指向Derive::func1(),但调用方式不同(需要调整this指针)。
p1指向Base1部分:直接调用,this指针就是Derive对象的Base1部分地址(不需要调整)。
p2指向Base2部分:p2当前指向的是Base2部分,但Derive::func1()需要this指针指向整个Derive对象的起始地址,
所以编译器会插入一段thunk代码来调整this指针,再跳转到真正的Derive::func1()。
结论:p1和p2调用的是同一个函数Derive::func1(),但它们的调用路径不同:
p1->func1() 直接调用。
p2->func1() 经过 thunk 调整 this 指针后再调用。
因此,它们的函数地址不同,但最终执行的是相同的代码。
为什么这么设计?
C++ 的对象内存布局是连续的,成员变量的偏移量是相对于对象起始地址计算的。
p2必须回到Derive的起始位置,才能正确访问所有成员
p2指向的是Base2子对象,而不是整个Derive对象的起始位置
5.3. 菱形继承、菱形虚拟继承
实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表就不看了。
第六章:继承和多态常见的面试问题
6.1 概念查考
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
答案:A
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
答案:D
3. 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
答案:C
A.继承是白盒复用(子类知道父类细节)。
B.组合是黑盒复用(对象之间通过接口交互,不关心内部实现)。
D.继承会破坏封装性(子类可能依赖父类实现细节)。
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
答案:A
A.正确。包含纯虚函数的类是抽象类,不能直接实例化。
B.错误。"虚基类"是多继承中解决菱形继承问题的概念,与纯虚函数无关。
C.错误。只有子类实现所有纯虚函数后,才能实例化,否则子类仍是抽象类。
D.错误。纯虚函数可以有实现(但通常不提供)
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
答案:B
A.错误。派生类虚函数必须与基类虚函数的签名一致(协变返回类型除外)。
B.正确。内联函数在编译时展开,虚函数通过虚表动态绑定,二者机制冲突(但语法上可以声明为虚函数,编译器会忽略inline)。
C.错误。派生类可以不重写基类的虚函数(直接继承)。
D.错误。虚函数必须是非静态成员函数(static函数属于类,不参与动态绑定)。
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
答案:D
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类使用的不是同一张虚表
答案:D
A.错误。B类对象前4个字节也是虚表地址(B有自己的虚表)。
B.错误。"虚基表"是多继承中解决菱形继承问题的概念,与题目无关。
C.错误。A和B的虚表地址不同(B重写了虚函数,虚表中对应项指向B的实现)。
D.正确。A和B的虚表结构相同(虚函数个数相同),但B的虚表中重写的函数地址不同。
8. 以下程序输出结果是什么()
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
class A {
public:
A(const char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A {
public:
B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A {
public:
C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C {
public:
D(const char* s1, const char* s2, const char* s3, const 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
//在 C++ 中,构造顺序遵循:
//虚基类(按声明顺序,深度优先)。
//直接基类(按声明顺序)。
//成员对象(按声明顺序)。
//派生类自身的构造函数。
//虚基类的构造由最派生类(D)直接初始化,忽略中间类(B 和 C)对虚基类的初始化。
//由于 A 是虚基类,B 和 C 对 A 的初始化会被忽略,A 由 D 直接初始化。
//因此,构造函数的调用顺序和输出为:
//1.A("class A") → class A。
//2.B("class A", "class B") → class B(忽略 A 的初始化)。
//3.C("class A", "class C") → class C(忽略 A 的初始化)。
//4.D 的构造函数体 → class D。
//虚基类 A 的构造优先于所有非虚基类(B 和 C)。
//非虚基类的顺序由 D 的继承声明顺序决定(B → C)。
//因此构造顺序是 A → B → C → D,对应选项 A。
9. 多继承中指针偏移问题?下面说法正确的是( )
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 Base1, public Base2 { public: int _d; };
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
//答案:C
//先继承Base1,所以在前面。后继承Base2,最后是_d。
//Base1是Derive的第一个基类,所以Base1的子对象位于Derive对象的起始位置。
//Base2是Derive的第二个基类,它的子对象位于Base1子对象之后。
//p3直接指向Derive对象的起始地址,所以p3的值与p1相同。
10. 程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E:编译出错 F:以上都不正确
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();
//p->func();//B->0
//不构成多态,不是父类指针调用。所以调用B类的func()
return 0;
}
//答案:B
//B公有继承A,因此B获得了A的所有成员(包括 test() 方法)。
//p是B*类型,test()继承后并未重写,因此直接调用A::test()。
//因为当B继承A时,如果B没有重写A的虚函数(如 test()),
//那么B的虚表(vtable)中test的条目仍然指向 A::test()。
//关于test()中隐藏的 this 指针:
//静态类型(编译期类型):A*(因为 test() 是 A 的成员函数)。
//动态类型(运行时类型):B*(因为 p 指向 B 对象)。
//所以test函数中的func函数是B在调用。
//而func()是虚函数,且this的静态类型是A*,动态类型是B*,完全满足多态条件。
//所以实际是在调用B类的func。
//但默认参数值根据静态类型(A*)决定,使用A中定义的默认值1,所以最后结果是B->1。
//或可以理解为子类继承父类虚函数是继承该接口,重写是对实现重写。所以缺省参数用父类的。
//多态的发生是因为A::test()内部的 this->func()满足“基类指针调用虚函数”的条件(this的静态类型是 A*,动态类型是B* )。
//虽然 p 是 B* ,但真正触发多态的是成员函数内部的 this 指针。
6.2 问答题
1.inline函数可以是虚函数吗?
答:可以。多态调用时无内联属性,普通调用有内联属性。
2. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段初始化的。如果构造函数是虚函数,调用它需要查虚表,但此时虚表还未构造,导致矛盾
4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。new对象时。
5. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通调用,是一样快的。如果是多态调用,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
6. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
作业
1.关于不能设置成虚函数的说法正确的是( )
A.友元函数可以作为虚函数,因为友元函数出现在类中
B.成员函数都可以设置为虚函数
C.静态成员函数不能设置成虚函数,因为静态成员函数不能被重写
D.析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数
答案:D
A.友元函数不属于成员函数,不能成为虚函数
B.静态成员函数就不能设置为虚函数
C.静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
D.尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态
2. 关于虚函数说法正确的是( )
A.被virtual修饰的函数称为虚函数
B.虚函数的作用是用来实现多态
C.虚函数在类中声明和类外定义时候,都必须加虚拟关键字
D.静态虚成员函数没有this指针
答案:B
A.被virtual修饰的成员函数称为虚函数
B.正确
C.virtual关键字只在声明时加上,在类外实现时不能加
D.static和virtual是不能同时使用的
3. 要实现多态类型的调用,必须( )
A.基类和派生类原型相同的函数至少有一个是虚函数即可
B.假设重写成功,通过指针或者引用调用虚函数就可以实现多态
C.在编译期间,通过传递不同类的对象,编译器选择调用不同类的虚函数
D.只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
答案:D
A.必须是父类的函数设置为虚函数
B.必须通过父类的指针或者引用才可以,子类的不行
C.不是在编译期,而应该在运行期间,编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
D.正确,实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了
4. 关于多态,说法不正确的是( )
A.C++语言的多态性分为编译时的多态性和运行时的多态性
B.编译时的多态性可通过函数重载实现
C.运行时的多态性可通过模板和虚函数实现
D.实现运行时多态性的机制称为动态绑定
答案:C
A.多态分为编译时多态和运行时多态,也叫早期绑定和晚期绑定
B.编译时多态是早期绑定,主要通过重载实现
C.模板属于编译时多态,故错误
D.运行时多态是动态绑定,也叫晚期绑定
5. 以下哪项说法时正确的( )
class A {
public:
void f1() { cout << "A::f1()" << endl; }
virtual void f2() { cout << "A::f2()" << endl; }
virtual void f3() { cout << "A::f3()" << endl; }
};
class B : public A {
public:
virtual void f1() { cout << "B::f1()" << endl; }
virtual void f2() { cout << "B::f2()" << endl; }
void f3() { cout << "B::f3()" << endl; }
};
A.基类和子类的f1函数构成重写
B.基类和子类的f3函数没有构成重写,因为子类f3前没有增加virtual关键字
C.基类引用引用子类对象后,通过基类对象调用f2时,调用的是子类的f2
D.f2和f3都是重写,f1是重定义
答案:D
A.错误,构成重写是子类重写父类的virtual函数,
B.f3构成重写,重写时子类可以不要求加virtual关键字
C. 选择题一定要扣字眼,题目前半句说的是基类引用 引用了子类对象,但是后半句调用虚函数时,说的是基类的对象调用f2,通过对象调用时编译期间就直接确定调用那个函数了,不会通过虚表以多态方式调用
D.正确
6. 关于重载和多态正确的是 ( )
A.如果父类和子类都有相同的方法,参数个数不同, 将子类对象赋给父类对象后, 采用父类对象调用该同名方法时,实际调用的是子类的方法
B.选项全部都不正确
C.重载和多态在C++面向对象编程中经常用到的方法,都只在实现子类的方法时才会使用
//D
class A {
public:
void test(float a) { cout << a; }
};
class B :public A {
public:
void test(int b) { cout << b; }
};
int main() {
A* a = new A;
B* b = new B;
a = b;
a->test(1.1);
//结果是1
}
答案:B
A.使用父类对象调用的方法永远是父类的方法
B.正确
C.重载不涉及子类
D.输入结果为1.1。由于 A::test 不是虚函数,调用完全由指针的静态类型(A*)决定。
7. 关于重载、重写和重定义的区别说法正确的是( )【不定项选择】
A.重写和重定义都发生在继承体系中
B.重载既可以在一个类中,也可以在继承体系中
C.它们都要求原型相同
D.重写就是重定义
E.重定义就是重写
F.重写比重定义条件更严格
G.以上说法全错误
答案:A F
A.重写即覆盖,针对多态, 重定义即隐藏, 两者都发生在继承体系中
B.重载只能在一个范围内,不能在不同的类里
C.只有重写要求原型相同
D.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏
E.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏
F.重写要求函数完全相同,重定义只需函数名相同即可
G.很明显有说法正确的答案
8. 假设A为抽象类,下列声明( )是正确的
A.A fun(int);
B.A*p;
C.int fun(A);
D.A obj;
答案:B
A.抽象类不能实例化对象,所以以对象返回是错误
B.抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态
C.参数为对象,所以错误
D.直接实例化对象,这是不允许的
9. 关于抽象类和纯虚函数的描述中,错误的是 ( )
A.纯虚函数的声明以“=0;”结束
B.有纯虚函数的类叫抽象类,它不能用来定义对象
C.抽象类的派生类如果不实现纯虚函数,它也是抽象类
D.纯虚函数不能有函数体
答案:D
A.纯虚函数的声明以“=0;”结束,这是语法要求
B.有纯虚函数的类叫抽象类,它不能用来定义对象,一般用于接口的定义
C.子类不实现父类所有的纯虚函数,则子类还属于抽象类,仍然不能实例化对象
D.纯虚函数可以有函数体,只是意义不大
10. 以下程序输出结果是( )
class A {
public:
A() :m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << ' '; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A {
public:
B() { test(); }
virtual void func() {
++m_iVal;
std::cout << m_iVal << ' ';
}
};
int main() {
A* p = new B;
p->test();
return 0;
}
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F.以上都不对
答案:C
分析:new B时先调用父类A的构造函数,执行test()函数,在调用func()函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0,构造完父类后执行子类构造函数,又调用test函数,然后又执行func(),由于父类已经构造完毕,虚表已经生成,且调用test函数的this指针是B*,只要后续不显式修改this,this的指向在对象的整个生命周期内始终不变,指向最初构造的完整对象。func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1, 最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2, 所以答案为C 0 1 2
11. 如果类B继承类A,A::x()被声明为虚函数,B::x()重写了A::x()方法,下述语句中哪个x()方法会被调用:( )
B b;
b.x();
A.A::x()
B.B::x()
C.A::x() B::x()
D.B::x() A::x()
答案:B
虽然子类重写了父类的虚函数,但只要是用对象去调用,则只能调用相对类型的方法,故B正确
12. 下面函数输出结果是( )
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();
return 0;
}
A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用
答案:A
A.正确
B.虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化
C.不强制也可以直接赋值,因为赋值兼容规则作出了保证
D.编译正确
如果直接通过 B* 调用 f(),会因为 f() 是 private 而编译错误。
但通过 A* 调用时,编译器只检查 A::f() 的访问权限(public),因此合法。
运行时实际调用的是 B::f(),但访问权限检查已经通过,因此不会报错。
13. 关于虚表说法正确的是( )
A.一个类只能有一张虚表
B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C.虚表是在运行期间动态生成的
D.一个类的不同对象共享该类的虚表
答案:D
A.多继承的时候,就会可能有多张虚表
B.父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象
C.虚表是在编译期间生成的
D.正确
14. 假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:( )
A.D类对象模型中包含了3个虚表指针
B.D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后
C.D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后
D.以上全部错误
答案:B
A.D类有几个父类,如果父类有虚函数,则就会有几张虚表,自身子类不会产生多余的虚表,所以只有2张虚表
B.正确
C.子类自己的虚函数只会放到第一个父类的虚表后面,其他父类的虚表不需要存储,因为存储了也不能调用
D.错误
15. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A.A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表
答案:B
A.父类对象和子类对象的前4字节都是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址,只是各自指向各自的虚表
C.不相同,各自有各自的虚表
D."内容完全一样" 不准确,因为 B 可能新增了成员变量或函数。