继承和多态是C++面向对象程序设计的关键。继承机制使得派生类能够获得基类的成员数据和方法,只需要在派生类中增加基类没有的成员。多态是建立在继承的基础上的,它使用了C++编译器最核心的一个技术,即动态绑定技术。这些都是面试必考题型。其核心思想是父类对象调用子类对象的方法。下面通过举例简单地说明继承的概念,如图7.1所示
生物是所有类的基类,所有生物都有寿命,所以可以把年龄作为生物类的属生。如果继续给生物分类,大家会想到有动特类和植物类等。当建立动物类和植物类的时候,无须再定义基类已经有的数据成员,而只需要描述动物类和植物类所特有的特性即可。比如动物类有奔跑、睡觉等行为,往动特类添加相尖的方法。
动物类和植物类的特性是由在生物类原有特性的基础上增加而来的,那么动物类和植物类就是生物类的派生类(也称作子类)。同样,老虎类和狮子类也是动特类的派生类,它们拥有动物类的一切特性。这种子类获得父类特性的概念就是继承。
面试题1 C++类继承的三种关系
【解析】
C++中继承主要有三种关系:public, protected和private。
(1)public继承
public继承是一种接口继承,子类可以代替父类完成父类接口所声明的行为。此时子类可以自动转换成为父类的接口,完成接口转换。从语法角度上来说,public继承会保留父类中成员(包括函数和变量等)的可见性不变,也就是说,如果父类中的某个函数是public的,那么在被子类继承后仍然是public的。
(2)protected继承
protected继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换成为父类的接口。从语法角度上来说,protected继承会将父类中的public可见性的成员修改成为protected可见性,相当于在子类中引入了protected成员,这样在子类中同样还是可以调用父类的protected和public成员,子类的子类也就可以调用被protected继承的父类的protected和public成员。
(3)private继承
private继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换成为父类的接口。从语法解度上来说,private继承会将父类中的public和protected可见性的成员修改成为private可见性。这样一来,虽然子类中同样还是可以调用父类的protected和public成员,但是子类的子类就不可以再调用被private继承的父类的成员了。
下面的程序代码说明了protected继承和private继承的区别。
#include <iostream>
using namespace std;
class Base
{
protected:
void printProtected() {cout << "print Protected" << endl;}
public:
void printPublic() {cout << "print Public" << endl;}
};
class Derived1: protected Base //protected继承
{
};
class Derived2: private Base //private继承
{
};
class A: public Derived1
{
public:
void print()
{
printProtected();
printPublic();
}
};
class B: public Derived2
{
public:
void print()
{
//printProtected(); //编译错误,不能访问
//printPublic(); //编译错误,不能访问
cout << "print Derived2" << endl;
}
};
int main()
{
class A a;
class B b;
a.print();
b.print();
cout << "Hello World!" << endl;
return 0;
}
Derived1类通过protected继承Base类,因此它的派生类A可以访问Base基类的protected和public成员函数。
Derived2类通过private继承Base类,因此它的派生类B不可以访问Base基类的任何成员函数。
面试题2 C++继承关系
请考虑下面的标记为A~J的语句在编译时可能出现的情况。如果能够成功编译,请记为“RIGHT”,否则记为“ERROR”。
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int var = -1) {
m_nPub = var;
m_nPtd = var;
m_nPrt = var;
}
public:
int m_nPub;
protected:
int m_nPtd;
private:
int m_nPrt;//私有变量
};
//
class Child1:public Parent{
public:
int getPub() {
return m_nPub;
}
int getPtd() {
return m_nPtd;
}
int getPrt() {
return m_nPrt; //A,这边错误,m_nPrt是基类Parent的私有变量,不能被派生类访问
}
};
//
class Child2 :protected Parent {
public:
int getPub() {
return m_nPub;
}
int getPtd() {
return m_nPtd;
}
int getPrt() {
return m_nPrt; //B,这边错误,m_nPrt是基类Parent的私有变量,不能被派生类访问
}
};
//私有继承,通过函数,子类还是可以访问基类的公有成员和保护成员,但在类外就不可以访问了
class Child3 :private Parent {
public:
int getPub() {
return m_nPub;
}
int getPtd() {
return m_nPtd;
}
int getPrt() {
return m_nPrt; //c,这边错误,m_nPrt是基类Parent的私有变量,不能被派生类访问
}
};
//
int main() {
Child1 cd1;
Child2 cd2;
Child3 cd3;
int nVar = 0;
//public inherited
cd1.m_nPub = nVar; //D 正确
cd1.getPtd = nVar; //E是错误的,m_nPtd是基类Parent的protected成员变量,公有继承变成child1的protected成员,只能child1内部访问
//不能被child1的对象访问
nVar = cd1.getPtd(); //F 正确
//protected inherited
cd2.m_nPub = nVar; //G 错误 m_nPub在Child2是Protected成员了
nVar = cd2.getPtd(); //H 正确
//private inherited
cd3.m_nPub = nVar; //I 错误 m_nPub变成Private成员了
nVar = cd3.getPtd(); //J 正确,因为调用了类成员函数访问其私有变量
return 0;
}
【解析】
A、B、C错误。m_mPtr是基类的Parent的私有变量,不能初派生类访问。
D正确。Child1是Public继承,可以访问并修改基类的Parent的Public成员变量。
E错误。m_nPtd是基类的Parent的Protected成员变量,通过公有继承后变成了派生类的Child1的Protected成员,因此只能在Child1类内部访问,不能使用Child1对象访问。
F正确。可以通过Child1类的成员函灵敏访问其Protected变量。
G错误。Child2是Protected继承,其基类Parent的Public和Protected成员变成了它的Protected成员,因此m_nPub只能在Child2类内部访问,不能使用Child2对象访问。
H正确。可以通过Child2类的成员函数访问其Protected变量。
I错误。Child3是Private继承,其基类Parent的Public和Protected成员变成了它的Private成员,因此m_mPub只能在Child2类内部访问,不能使用Child2对象访问。
J正确。可以通过Child3类的成员函数访问其Private变量。
【答案】
A、B、C、E、G、I为“ERROR”
D、F、H、J为“RIGHT”
面试题3 看代码找错——C++继承
#include <iostream>
using namespace std;
class base {
private: //改为pubic:
int i;
public:
base(int x) {
i = x;
}
};
//
class derived :public base {
private:
int i;
public:
//这边错误,错误理由如下:
derived(int x, int y) {//改为===》derived(int x, int y) :base(y) {
i = x;
}
void printTotal() {
int total = i + base::i; //无法调用私有成员变量i
cout << "total = " << total << endl;
}
};
int main() {
derived d(1, 2);
d.printTotal();
return 0;
}
【答案】
(1)在 derived 类进行构造时,首先调用其基类(base类)的构造方法,由于没有指明何种构造方法,
因此默认调用base类不带参数的构造方法。然而,基类 base 中已经定义了一个带参数的构造函数,所以编译器就不会给他定义默认的构造函数了,
因此会出出“找不到构造方法”的编译错误,解决方法:可以在derived的构造函数中显示调用base的构造函数。
derived(int x, int y) 改为derived(int x, int y) :base(y)
(2)在derived类的printTotal()中,使用base::i的方式调用base类的私有成员i,这样会得到“不能访问私有成员”的编译错误,解决办法:把成员i的访问权限设为public.
修改后,运行结果如下:
面试题4 私有继承有什么作用
先下下面的代码:
#include <iostream>
using namespace std;
class Person {
public:
void eat() {
cout << " Person eat===========" << endl;
}
};
//私有继承
class Student :private Person {
public:
void study() {
cout << " Student Study========" << endl;
}
};
//
int main() {
Person p;
Student s;
p.eat();
s.study();
//s.eat(); //从私有基类继承而来的成员都成为派生类的私有成员--即使它们在基类中是保护或者私有成员变量。
//p = s; //如果两个类之间的继承关系为私有private,编译器一般不会将派生类对象转成基类对象。
return 0;
}
【解析】
此程序的两个编译错误分别说明了私有继承的规则。
第一个规则正如大家现在所看到的,和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象(如Student)转换成基类对象(如Person)。
第二个规则是,从私有基类继承而来的成员都成为了派生类的私有成员————即使它们在基类中是保护或公有成员。
可以看出,私有继承时派生类与基类不是“is a”的关系,而是意味着“Is-Implement-In-terms-Of”(以......实现)。如果使类D私有继承于类B,这样做是因为你想利用类B中已经存在的某些代码,而不是因为类B的对象和类D的对象之间有什么概念上的关系。因此,私有继承在软件“设计”过程中毫无意义,只是在软件“实现”时才有用。
面试题5 私有继承和组合有什么相同点和不同点
【解析】
使用组合表示 “有一个(Has-A)”的关系。如果在组合中需要使用一个对象的某些方法,则完全可与利用私有继承代替。
私有继承下派生类会获得基类的一份备份,同时得到了访问基类的公共以及保护接口的权力和重写基类虚函数的能力。它意味着 “以......实现”,
它是组合的一种语法上的变形(聚合或者“有一个”)。
例如“汽车有一个(Has-A)引擎”关系可以用单一组合表示,也可以用私有继承表示。例如下面的程序:
#include <iostream>
using namespace std;
class Engine {
public:
Engine(int num) :numCylinders(num) { //Engine构造函数
}
void start() {
cout << " Engline start," << numCylinders << " Cylinders" << endl;
}
private:
int numCylinders;
};
//私有继承
class Car_pri :private Engine {
public:
Car_pri() :Engine(8) { //调用基类的构造函数
}
void start() {
Engine::start(); //调用基类的start()函数
}
};
class Car_comp {
private:
Engine engine; //组合Engline类对象
public:
Car_comp():engine(8){} //给engline 成员初始化
void start() {
engine.start(); //调用engline的start()
}
};
int main() {
Car_pri car_pri;
Car_comp car_comp;
car_pri.start();
car_comp.start();
return 0;
}
由此看出,“有一个”关系既可以用私有继承表示,也可以用单一组合表示。类Car_pri和类Car_comp有很多相似点:
(1)它们都只有一个Engine被确切地包含于Car中。
(2)它们在外部都不能进行指针转换,如将Car_pri*转换为Engine*。
(3)它们都有一个start()方法,并且都在包含的Engine对象中调用start()方法。
也有下面一些区别:
(1)如果想让每一个Car都包含若干Engine,那么只能用单一组合的形式。
(2)私有继承形式可能引入不必要的多重继承。
(3)私有继承形式允许Car的成员将Car*转换成Engine*。
(4)私有继承形式允许访问基类的保护(protected)成员。
(5)私有继承形式允许访问Car重写Engine的虚函数。
应该在组合和私有继承之间如何选择呢?这里有一个原则:尽可能使用组合,万不得已才用私有继承。请看下面的例子程序。
#include <iostream>
using namespace std;
//抽象
struct Base {
public:
virtual void Func1() = 0; //纯虚函数
virtual void Func2() = 0; //纯虚函数
void print() {
Func1(); //调用派生类的Fun1()
Func2(); //调用派生类的Fun2()
}
};
struct T :private Base {
public:
virtual void Func1() { //覆盖基类的Fun1
cout << "Func1" << endl; //覆盖基类的Fun2
}
virtual void Func2() {
cout << "Func2" << endl;
}
//调用基类的print()
void UseFunc() {
Base::print();
}
};
int main()
{
T t;
t.UseFunc();
return 0;
}
程序输出如下。
Func1
Func2
上面的代码中Base类含有纯虚函数Fun1()以及Fun2(),因此它为抽象类,它通过函数调用了T中的重写版本。
这种情况就不能使用组合了,因为组合的对象关系不能使用一个抽象类,抽象类不能被实例化。
【答案】
相同点:都可以表示“有一个”关系。
不同点:私有继承中派生类能访问基类的protected成员,并且可以重写基类的虚函数,甚至当基类是抽象类的情况。组合不具有这些功能。
注意:选择它们的原则为尽可能使用组合,万不得已才用私有继承。
面试题6 什么是多态
【解析】
多态、封装和继承是面向对象思想的“三大特征”。可以说,不懂得什么是多态就不能说懂得面向对象。
多态性的定义:同一操作作用于不同的对象,可以得到不同的解释,产生不同的执行结果。有两种类型的多态性:
(1)编译时多态性。编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作;
(2)运行时的多态性。运行时的多态性就是指直到系统运行时,才能根据实际情况决定实现何种操作。C++中,运行时的多态性通过虚函数实现。例如下面的程序代码:
#include <iostream>
using namespace std;
class Person {
public:
virtual void print() {
cout << " I'm a Person" << endl;
}
};
class Chinese :public Person {
public:
virtual void print() {
cout << " I'm from china" << endl;
}
};
class American :public Person {
public:
virtual void print() {
cout << " I'm from USA" << endl;
}
};
void printPerson(Person &person) {
person.print(); //运行时决定调用哪个类中的print()函数
}
int main()
{
Person p;
Chinese c;
American a;
printPerson(p);
printPerson(c);
printPerson(a);
return 0;
}
运行结果:
可以看到,在运行时通过基类Person的对象,可以来调用派生类Chinese和American中的实现方法。Chinese和American的方法都是通过覆盖基类中的虚函数方法来实现的。
面试题7 虚函数是怎么实现的
【解析】
简单地说,虚函数通过虚函数表来实现的。那么,什么是虚函数表?
事实上,如果一个类中含有虚函数,那么这个系统会为这个类分配一个指针成员指向一张虚函数表,表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组。为了说明虚函数表,请看下面的程序用例。
#include <iostream>
using namespace std;
class Parent {
public:
virtual void foo1() {}
virtual void foo2() {}
};
class Child1:public Parent {
public:
void foo1() {
cout << "Child1======foo1()" << endl;
}
void foo3();
};
class Child2:public Parent {
public:
void foo1() {
cout << "Child1======foo1()" << endl;
}
void foo2() {
cout << "Child1======foo2()" << endl;
}
void foo3();
};
class Child3{
public:
void foo1() {
cout << "Child3======foo1()" << endl;
}
void foo2() {
cout << "Child3======foo2()" << endl;
}
void foo3();
};
int main()
{
cout << " sizeof(Parent) ====" << sizeof(Parent) << endl;
cout << " sizeof(Child1) ====" << sizeof(Child1) << endl;
cout << " sizeof(Child2) ====" << sizeof(Child2) << endl;
cout << " sizeof(Child3) ====" << sizeof(Child3) << endl;
return 0;
}
下面列出了各个类的虚函数表(vtbl)的内容。
Parent类的vtbl:Parent::foo1()的地址、Parent::foo2()。
Child1类的vtbl:Child1::foo1()的地址、Parent::foo2()。
Child2类的vtbl:Child2::foo1()的地址、Child2::foo2()。
可以看出,虚函数既有继承性,又有多态性。每个派生类的vtbl继承了它各个基类的vtbl,如果基类vtbl中包含某一项,则其派生类的vtbl中也将包含同样的一项,但是两荐值可能不同。如果派生类覆盖(override)了该项对应的虚函数,则派生类vtbl的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。
在类对象的内存布局中,首先是该类的vtbl指针,然后才是对象数据。在通过对象指针调用一个虚函数时,编译器生成的代码将先获取对象类的vtbl指针,然后调用vtbl中对应的项。对于通过对象指针调用的情况,在编译期间无法确定指针指向的是基类对象还是派生类对象,或者是哪个派生类的对象。但是在运行期间执行到调用语句时,这一点已经确定,编译后的调用代码能够根据具体对象获取正确的vtbl,调用正确的虚函数,从而实现多态性。
分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了,怎么在二者之间做一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到真正的绑定。这个数据结构就是vtbl。可以看到,实现用户所需的抽象和多态需要进行后绑定,而编译器又是通过抽象和多态实现后绑定的。
面试题8 构造函数调用虚函数
#include <stdio.h>
class A {
public:
A() {
doSth(); //构造函数调用虚函数
}
virtual void doSth() {
printf("I am A");
}
};
class B :public A {
public:
virtual void doSth() {
printf("I am B");
}
};
int main() {
B b;
return 0;
}
【解析】
在构造函数中,虚拟机制不会发生作用。因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间调用的虚函数向下匹配到派生类,派生类的函数理所当然会涉及本地数据成员,但是那些数据成员还没有被初始化,而调用涉及一个对象还没有被初始化的部分自然是危险的,所以C++会提示此路不通。因此,虚函数不会向下匹配到派生类,而是直接执行基类的函数。
【答案】
在构造函数中,虚据机制不会发生作用,执行结果为:
面试题9 看代码写结果——虚函数的作用
#include <iostream>
using namespace std;
class A {
public:
virtual void print(void)
{
cout << " A::print()" << endl;
}
};
class B :public A {
public:
virtual void print(void)
{
cout << " B::print()" << endl;
}
};
class C :public A {
public:
void print(void)
{
cout << " C::print()" << endl;
}
};
void print(A a) {
a.print();
}
int main() {
A a;
B b;
C c;
A *pa, *pb, *pc;
pa = &a;
pb = &b;
pc = &c;
a.print();
b.print();
c.print();
//上面都是自己类中的成员函数,在构造函数中,虚拟机制不会发生作用
pa->print();
pb->print();
pc->print();
print(a);
print(b);
print(c);
return 0;
}
【解析】
代码第41~43行,分别使用类A、类B和类C的各个对象来调用其print()成员函数,因此执行的是各个类的print()成员函数。
代码第45~47行,使用3个类A的指针来调用print()成员函数,而这3个指针分别指向类A、类B和类C的3个对象。由于print()函数是虚函数,因些这里有多态,执行的是各个类的print()成员函数。
代码第49~51行,全局的print()函数的参数使用传值的方式(注意与传引用的区别,如果是引用,则又是多态),在对象a、b、c分别传入时,在函数栈中会分别生成类A的临时对象,因此执行的都是类A的print()成员函数。
运行结果如下:
面试题10 看代码写结果——虚函数
#include <iostream>
#include <string>
using namespace std;
void println(const string& msg) {
cout << msg << '\n';
}
class Base {
public:
Base() {
println(" Base::Base()");
virt();
}
void f() {
println(" Base::f()");
virt();
}
virtual void virt() {
println(" Base::virt()");
}
};
class Derived :public Base {
public:
Derived() {
println(" Derived::Derived()");
virt();
}
virtual void virt() {
println(" Derived::virt()");
}
};
int main() {
Derived d; //构造 Derived 对象d,首先调用 Base 的构造函数,然后调用 Derived 的构造函数,
//在 Base 类的构造函数,又调用了虚函数 virt()
Base *pB = &d;
pB->f();
return 0;
}
【解析】
代码第39行,构造Derived对象d。首先调用Base的构造函数,然后调用Derived的构造函数。在Base类的构造函灵敏中,又调用了虚函数virt(),此时虚拟机制还没有开始作用(因为是在构造函数中),所以执行的是Base类的virt()函数。同样,在Derived类的构造函数中,执行的是Derived类的virt()函数。
代码第43行,通过Base类的指针pB访问Base类的公有成员函数f()。f()函数又调用了虚函数virt(),这里出现多态。由于指针pB是指向Derived类对象的,因此实际执行的是Derived类的virt()成员。
执行结果: