本系列博客基于董山海的<C和C++程序员面试秘笈>,旨在记录,欢迎交流,可联系 zywang@shu.edu.cn !
第七章:C++继承和多态
文章目录
1、面试题1
继承和多态:
继承:使派生类能够获得基类的成员数据和方法,只需要在派生类中增加基类没有的成员;
多态:建立在继承的基础之上,父类调用子类对象的方法。
2、面试题2
C++类继承的三种关系:
- public继承
public继承是一种接口继承,子类可以代替父类去完成父类接口所声明的行为。此时子类可以自动转换成为父类的接口,完成接口转换。从语法角度来说,public继承会保留父类中成员(包括函数和变量等)的可见性不变,也就是说,如果父类中的某个函数是 public 中,那么在被子类继承后仍然为 public 中。 - protected继承
protected继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换为父类的接口。从语法的角度上,protected继承会将父类中的 public 可见性的成员修改为 protected 可见性,相当于在子类中引入 protected 成员,这样在子类中同样还是可以调用父类的 protected 和 public 成员,子类的子类也可以再调用被 protected 继承的父类的 protected 和 public 成员。 - private继承
private 继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换为父类的接口。从语法的角度上,private 继承会将父类中的 public 和 protected 可见性的成员修改为 private 可见性。这样一来虽然子类中同样还是可以调用父类的 protected 和 public 成员,但是子类的子类就不可以再调用被 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();
}
};
//
int main() {
class A a;
class B b;
a.print();
b.print();
system("pause");
return 0;
}
Derived1 类通过 protected 继承 Base 类,因此它的派生类 A 可以访问 Base 基类的 protected 和 public 成员函数;
Derived2 类通过 private 继承 Base 类,因此它的派生类 B 不可以访问 Base 基类的任何成员函数。
3、面试题3
#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; //正确
cd1.getPtd = nVar; //E是错误的,m_nPtd是基类Parent的protected成员变量,公有继承变成child1的protected成员,只能child1内部访问
//不能被child1的对象访问
nVar = cd1.getPtd();
//protected inherited
cd2.m_nPub = nVar; //错误
nVar = cd2.getPtd();
//private inherited
cd3.m_nPub = nVar; //错误
nVar = cd3.getPtd(); //正确,因为调用了类成员函数访问其私有变量
system("pause");
return 0;
}
4、面试题4
C++类继承的三种关系:
#include <iostream>
using namespace std;
class base {
private:
int i;
public:
base(int x) {
i = x;
}
//
};
//
class derived :public base {
private:
int i;
public:
//这边错误,错误理由如下:
//derived(int x, int y) {
// i = x;
//}
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();
system("pause");
return 0;
}
在 derived 类进行构造时,首先调用其基类(base类)的构造方法,由于没有指明何种构造方法,因此默认调用 base 类不带参数的构造方法。然而,基类 base 中已经定义了一个带参数的构造函数,所以编译器就不会给他定义默认的构造函数。我们可以:那么在derived 的构造函数中显示调用 base 的构造函数。
5、面试题5
C++类的私有继承:
#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,编译器一般不会将派生类对象转成基类对象。
system("pause");
return 0;
}
6、面试题6
私有继承和组合的相同点和不同点,该如何选择?:
使用组合表示 “有一个(Has-A)”的关系。如果在组合中需要使用一个对象的某些方法,则完全可与利用私有继承代替。
私有继承下派生类会获得基类的一份备份,同时得到了访问基类的公共以及保护接口的权力和重写基类虚函数的能力。它意味着 “以------实现”,它是组合的一种语法上的变形。
7、面试题7
私有继承和组合:
#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();
system("pause");
return 0;
}
8、面试题8
组合和私有继承的选择:尽可能使用组合:
#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();
system("pause");
return 0;
}
9、面试题9
多态:同一操作作用于不同的对象,可以得到不同的解释,产生不同的执行结果。有两种类型的多态性:
- 编译时多态性。编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作;
- 运行时的多态性。运行时的多态性就是指直到系统运行时,才能根据实际情况决定实现何种操作。
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);
system("pause");
return 0;
}
10、面试题10
C++虚函数实现的细节:
虚函数通过虚函数表来实现的。
什么是虚函数表?
事实上, 如果一个类中含有虚函数,那么这个系统会为这个类分配一个指针成员指向一张虚函数表,表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组。
11、面试题11
构造函数调用虚函数:
#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+会提示此路不通。因此,虚函数不会向下匹配到派生类,而是直接执行基类的函数。
12、面试题12
虚函数的作用::
#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;
A *pa, *pb, *pc;
B b;
C c;
//
pa = &a;
pb = &b;
pc = &c;
a.print();
b.print();
c.print();
pa->print();
pb->print();
pc->print();
//上面都是自己类中的成员函数
//在构造函数中,虚拟机制不会发生作用
print(a);
print(b);
print(c);
system("pause");
return 0;
}
13、面试题13
#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();
system("pause");
return 0;
}
14、面试题14
对C++类虚拟机制的理解:
#include <iostream>
#include <complex>
using namespace std;
//
class Base {
public:
Base() {
cout << "Base-ctor" << endl;
}
//
~Base() {
cout << "Base-dtor" << endl;
}
//
virtual void f(int) {
cout << "Base::f(int)" << endl;
}
//
virtual void f(double) {
cout << "Base::f(double)" << endl;
}
//
virtual void g(int i = 10) {
cout << "Base::g()" << i << endl;
}
};
//
class Derived :public Base {
public:
Derived() {
cout << "Derived-ctor" << endl;
}
//
~Derived() {
cout << "Derived-dtor" << endl;
}
//
void f(complex<double> c){
cout << "Derived::f(complex)" << endl;
}
//
virtual void g(int i = 20) {
cout << "Derived::g()" << i << endl;
}
};
Base b;
Derived d;
Base* pb = new Derived;
c
o
u
t
<
<
s
i
z
e
o
f
(
B
a
s
e
)
<
<
e
n
d
l
;
cout << sizeof(Base) <<endl;
cout<<sizeof(Base)<<endl;
这边为4,因为 Base 类没有任何数据成员,并且含有虚函数,所以系统会为它分配一个指针指向虚函数表。指针的大小为4个字节。
c
o
u
t
<
<
s
i
z
e
o
f
(
D
e
r
i
v
e
d
)
<
<
e
n
d
l
;
cout<<sizeof(Derived)<<endl;
cout<<sizeof(Derived)<<endl;
这边也为4。Derived 类没有任何数据成员,它是 Base 类的派生类,因此它继承了 Base 的虚函数表。系统也为它分配一个指针指向这张虚函数表。
p
b
−
>
f
(
1.0
)
pb->f(1.0)
pb−>f(1.0);
Base 类中定义了两个 f() 的重载函数,Derived 只有一个 f(), 其参数类型为 complex,因此 Derived 并没有 Base 的 f() 覆盖。由于参数 1.0 默认是 double 类型,因此调用的是 Base::f(double)
p
b
−
>
g
(
)
pb->g()
pb−>g();
Base 类和 Derived 都定义了含有相同参数列表的 g(),因此这里发生了多态了。pb指针指向的是 Derived 类的对象,因此调用的为 Derived 的 g()。
15、面试题15
C++多重继承:C++允许一个派生类指定多个基类,多重继承的优点:对像可以调用多个基类中的接口,缺点容易出现继承向上的二义性。:
#include <iostream>
using namespace std;
//
class cat {
public:
void show() {
cout << "cat" << endl;
}
};
//
class fish {
public:
void show() {
cout << "fish" << endl;
}
};
//
class catfish :public cat, public fish {
//
};
//
int main() {
catfish obj;
//obj.show();
//
obj.cat::show();
system("pause");
return 0;
}
因为 catfish 类多重继承 cat 类和 fish 类,因此继承了 cat 和 fish 的 show() 方法。因此直接调用 obj.show() 无法区分应该执行哪个基类的 show() 函数,所以改成 obj.cat::show();
16、面试题16
多重继承中二义性的消除:
类 A 派生 B 和 C,类 D 从 B、C派生,如何将一个类 A 的指针指向一个类 D 的实例?
class A {};
class B :public A {};
class C :public A {};
class D :public B, public C {};
//
int main() {
D d;
A *pd = &d; //编译错误
return 0;
}
//由于B、C继承自 A,所以B、C都拥有A的一份拷贝,类D多重继承自B、C,因此拥有A的两份拷贝,会出现"模糊的转换"。
//解决办法如下:
class A {};
class B :virtual public A {}; //B虚拟继承自A
class C :virtual public A {}; //C虚拟继承自A
class D:public B, public C{};
int main() {
D d;
A *pd = &d; //成功
return 0;
}
//将B、C都改成虚拟继承自A,消除继承的二义性
17、面试题17
多重继承和虚拟继承:
#include <iostream>
using namespace std;
//
class Parent {
public:
Parent() :num(0) {
cout << "Parent" << endl;
}
//
Parent(int n) :num(n) {
cout << "Parent(int)" << endl;
}
//
private:
int num;
};
//
class Child1 :public Parent {
public:
Child1() {
cout << "Child1()" << endl;
}
//
Child1(int num) :Parent(num) {
cout << "Child1(int)" << endl;
}
};
//
class Child2 :public Parent {
public:
Child2() {
cout << "Child2()" << endl;
}
//
Child2(int num) :Parent(num) {
cout << "Child2(int)" << endl;
}
};
//
class Derived :public Child1, public Child2 {
public:
Derived() :Child1(0), Child2(1) {
//
}
//
Derived(int num) :Child2(num), Child1(num + 1) {
//
}
};
//
int main() {
Derived d(4);
system("pause");
return 0;
}
首先讨论不存在 virtual 继承的情况:
多重继承类对象的构造函数与其列表中基类的排列顺序一致,而不是与构造函数的初始化 列表顺序一致。
- 构造 Child1.先调用 Parent 的构造函数,再调用 Child1 的构造函数;
- 构造 Child2.先调用 Parent 的构造函数,再调用 Child2 的构造函数;
- 调用 Derived 类的构造函数;
下面讨论存在 virtual 继承的情况:
当 Child1 和 Child2 为虚拟继承时,当系统碰到多重继承的时候就会自动先加入一个虚拟基类(Parent)的拷贝,即首先调用虚拟基类(Parent)默认的构造函数,然后再调用派生类(Child1 和 Child2)的构造函数和自己的(Derived)的构造函数。由于只生成一份拷贝,因此后面再也不会再调用基类(Parent)的构造函数。
18、面试题18
抽象基类与纯虚函数:
纯虚函数在基类中没有定义,必须在子类中加以实现,很像 Java 中的接口函数。如果一个基类中含有一个或多个纯虚函数,那它属于抽象基类,不能被实例化。
为什么要引入抽象基类和纯虚函数?
- 方便使用多态特性;
- 很多情况下,基类本身生成对象是不合理的。例如,动物作为一个基类可以派生出老虎、狮子等子类,但是动物本身生成对象明显不合理。抽象基类不能被实例化,因此它定义的纯虚函数相当于接口,能把派生类的共同行为提取出来。
#include <iostream>
#include <memory.h>
#include <assert.h>
using namespace std;
//
class Animal {
public:
virtual void sleep() = 0; //纯虚函数,必须在派生类中被定义
virtual void eat() = 0; //纯虚函数,必须在派生类中被定义
};
//
class Tiger :public Animal {
public:
void sleep() {
cout << "Tiger sleep" << endl;
}
//
void eat() {
cout << "Tiger eat" << endl;
}
};
//
class Lion :public Animal {
public:
void sleep() {
cout << "Lion sleep" << endl;
}
//
void eat() {
cout << "Lion eat" << endl;
}
};
//
int main() {
Animal *p; //Animal指针,不能用 Animal animal 定义对象
Tiger tiger;
Lion lion;
//
p = &tiger;
p->sleep();
p->eat();
p = &lion;
p->sleep();
p->eat();
system("pause");
return 0;
}
这边注意的是:如果子类中,有一个基类的纯虚函数没有被定义,那么这个子类也是抽象类。
19、面试题19
虚函数与纯虚函数的不同:
- 类中虚函数,这个函数是实现的,即使为空,也就是实现好的,目的:让这个函数在它的子类中可以被覆盖。而纯虚函数只是一个接口,只是个函数的声明而已,需要留到子类中去实现。
- 虚函数在子类中可以不重载,但是纯虚函数必须子类中去实现。
- 虚函数的类用于“实作继承”,也就是继承接口的同时也继承父类的实现,当然也可以完成自己的实现。而纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类完成。
- 虚基类:带纯虚函数的类。不能直接生成对象,而只有被继承,并且重写其虚函数后,才能使用。这样的类就做抽象类。
20、面试题20
对面向编程的理解:
编写一个与图形有关的应用程序,需要处理大量图形(shape)信息。图形有矩形(Rectangle)、正方形(Square)、圆形(Circle)等种类,应用需要计算出这些图形的面积,并打印出来。
- 编写需要的类。面向对象的思想;
- Square 是否 继承自 Rectangle;
#include <iostream>
using namespace std;
#define PI 3.14159 //圆周率
//形状类
class Shape {
public:
Shape(){}
~Shape() {}
virtual void Draw() = 0; //纯虚函数
virtual double Area() = 0; //纯虚函数
};
//长方形类
class Rectangle :public Shape {
public:
Rectangle() :a(0), b(0) {}
Rectangle(int x, int y) :a(x), b(y) {}
virtual void Draw() {
cout << "Rectangle,area:" << Area() << endl;
}
//
virtual double Area() {
return a*b;
}
private:
int a;
int b;
};
//圆形类
class Circle :public Shape {
public:
Circle(double x) :r(x) {}
virtual void Draw(){
cout << "Circle,area:" << Area() << endl;
}
//
virtual double Area() {
return PI*r*r;
}
//
private:
double r;
};
//正方形类
class Square :public Rectangle {
public:
Square(int length):a(length){}
virtual void Draw() {
cout << "Square,area:" << Area() << endl;
}
//
virtual double Area() {
return a*a;
}
private:
int a;
};
//
int main() {
Rectangle rect(10, 20);
Square square(10);
Circle circle(8);
//
Shape *p; //抽象类指针
p = ▭
cout << p->Area() << endl;
p->Draw();
p = □
cout << p->Area() << endl;
p->Draw();
p = &circle;
cout << p->Area() << endl;
p->Draw();
system("pause");
return 0;
}
21、面试题21
什么是COM:简单的说,COM 即组件对象模型,它定义了一种二进制标准,使得任何编程语言存取它所编写的模块。
COM(Component Object Model)即组件对象模型,新技术都以COM为基础。各种文档也充斥着诸如COM对象、接门、服务器之类的术语。简单地说,COM是一种跨应用和语言共享二进制代码的方法。与C++不同,它提倡源代码重用。源码级重用虽然好,但只能用于C++。它还带来了名字冲突的可能性,更不用说不断拷贝重用代码而导致工程膨胀和臃肿。
Windoes 使用DLLS(动态链接库)在二进制级共享代码。这也是Windows程序运行的关键–重用 kernel32.dll,user32.dll等。但DLLs 是针对C接口而写的,它们只能被C或者理解C调用规范的语言使用的。编程语言负责实现共享代码,而不是由动态链接库本身,这样的话,动态链接库的使用受到限制。
COM通过定义二进制标准解决了这些问题。这是因为 COM 明确指出二进制模块(动态链接库和可执行文件)必须被编译成与指定的结构匹配。这个标准也确切地规定了在内存中如何组织 COM 对象。COM定义的二进制标准还必须独立于任何编程语言(如C++中的命名修饰)。一旦满足这些条件,就可以轻松地从任何编程语言中存取这些模块。由编译器所负责产生的二进制代码与标准兼容。
在内存中,COM对象与编写模块所用的语言无关,因为结果二进制代码为所有语言可用。
22、面试题22
COM 组件的特点:
COM组件是遵循COM规范编写、以 Win32 动态链援库(DLL)或 可执行文件 (EXE) 形式发布的可执行二进制代码,能够满足对组件架构的所有需求。遵循COM的规范标准,组件与应用、组件与组件之间可以互操作,极其方便地建充可伸缩的应用系统。COM 是一种技术标准,其商业品牌则称之为:Active X。
组件在应用开发方面具有以下特点:
- 组件是与开发工具语言无关的。开发人员可以根据特定情况选择特定语言工具实现组件的开发。编译之后的组件以二进制的形式发布,可跨Windows平台使用,而且源程序代码不会外泄,有效地保证了组件开发者的版权。
- 通过接口有效保证了组件的复用性。一个组件具有若千个接口,每个接口代表组件的某个属性或方法。其他组件或应用程序可以设置或调用这些属性和方法来进行特定的逻辑处理。组件和应用程序的连接是通过其接口实现的。负责集成的开发人员无须了解组件功能是如何实现的,只需简单地创建组件对象并与其接口建立连接。在保证接口一致性的前提之下,可以调换组件、更新版本,也可以把组件安插在不同的应用系统之中。
- 组件运行效率高,便于使用和管理。因为组件是二进制代码,所以运行效率比ASP脚本高很多。核心的商务逻辑计算任务必须由组件来担当,ASP脚本只起组装的角色。而且组件在网络上的位置可被透明分配,组件和使用它的程序能在同一进程中、不同进程中或不同机器上运行。组件之间是相互独立的。组件对象通过-一个内部引用计数器来管理它自己的生存期,这个计数器存放任何时候连接到该对象的客户数。当引用计数变为0时,对象可以把自已从内存中释放掉。这使程序员不必考虑与提供可共享资源有关的问题。
23、面试题23
简述COM、ActiveX和DCOM:
COM(Component Object Mode):组件对象模型,是组件之间相互接口的规范。其作用是使各种软件构件和应用软件能够用一种统一的标准方式进行交互。COM 不是一种面向对象的语言,而是一种与源代码无关的二进制标准。
ActiveX:Microsoft 提出的一套基于 COM 的构件技术标准,实际上是对象嵌入与链接的新版本。
DCOM(Distribute COM):基于分布式环境下的 COM。
24、面试题24
DLL HELL:
DLL HELL 主要是指 DLL (动态链接库)版本冲突的情况。一般情况,DLL的新版本会覆盖旧的版本,使用旧版本的 DLL 的应用程序就不能继续正常工作了。