在很多次面试中,我发现面试官都喜欢问同一个问题:谈谈你对多态的认识。
遇到这个问题,通常我第一句话就是:多态分为静态多态和动态多态,然后静态多态就是balabala动态多态是巴拉巴拉。关于静态多态这里不再赘述。这里先描述一下博主在CVTE面试时遇到的多态问题。面试官要求:你先写一个多态的例子,然后我就飞快的写下了下面的语句:
class Animal
{
public:
virtual void eat()
{
cout << "Animal eat" << endl;
}
};
class Dog : public Animal
{
private:
void eat()
{
cout << "dog eat" << endl;
}
};
int main()
{
Animal *A = new Dog;
A->eat();
return 0;
}
然后面试官接着问,这个类Dog中的函数eat为什么要把属性设置为public,设置为private行不行呢,如果设置为private,基类指针是否还可以访问这里的eat函数?当时遇到这个问题有点懵,因为接口函数都是用来被类的对象调用的,一般就直接设置为public了,怎么会想到设为私有呢。后来自己回来想清楚了,面试官这样问其实就是考察你对虚函数原理是否了解。
先公布一下答案:答案是可以的。下面说说原因。
类的内存结构
如果一个类中有虚函数,类的起始位置将存放虚函数表的地址(4个字节),虚函数表是按顺序存放该类的虚函数。虚函数表地址之后再存放该类的数据成员。在无继承的情况下,不管虚函数有多少个,系统为其分配的内存空间只有4个字节。这样,我们就知道用sizeof关键字计算类的大小。
虚函数表
多态与成员函数的访问权限是没有关系的, 即是两回事。
现在来解释一下为什么可以突破对象访问私有函数的限制:编译器编译是静态的,运行时多态是动态的,也就是说编译器在编译的时候不太关注动态特性。A的指向Animal类型对象的指针,编译器编译时将A当成指向Animal类型对象处理,Animal中恰好有个成员函数是eat(),并且Animal中的eat()函数属性是public,所以A->eat()语句是合法的,编译时可以通过编译器的检查,但程序在执行时通过虚函数表动态匹配调用的虚函数,所以我们看到的执行结果是“dog eat”
基类定义了虚函数, 并且是public的,那么子类只要override 虚函数 无论放在什么样的访问权限下(private,protect,public), 都以基类的访问权限为准, 即是public的。
关于虚函数的特点:
(1)要有子类公有继承父类,虚函数才有意义
(2)子类重写的父类的虚函数也是虚函数(类Dog中的eat函数是虚函数),只不过省略了关键字virtual
(3)虚函数必须是所在类的成员函数,而不能是友元函数或静态函数。因为虚函数调用要靠特定的对象来决定激活哪一个函数。
(4)构造函数不能是虚函数,原因是构造函数在创建时首先被调用,也就是说在编译时就要把构造函数的地址绑定好。而析构函数可以是虚函数,因为析构函数最后调用,可以在运行时动态匹配。
虚析构
把析构函数设为虚函数主要是为了防止内存泄漏
我们首先要知道的事实(单继承时):
构造函数的调用顺序:从当前类往上找父类,一直找到最上层的父类,我们可以理解为始祖,它是最先构造的,然后沿着继承路径依次往下构造,一直到当前类。
析构函数的调用顺序:从当前类开始析构,析构完再沿着继承路径往上找父类 ,析构父类 ,再找到最上层的父类 析构。
分析以下代码:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A的构造函数" << endl;
}
~A()
{
cout << "A的析构函数" << endl;
}
};
class B:public A
{
public:
B()
{
p = new char[20];
cout << "B的构造函数" << endl;
}
~B()
{
if (p != NULL)
delete[] p;
cout << "B的析构函数" << endl;
}
private:
char *p;
};
void func(A *pa)
{
delete pa;
}
int main()
{
A *pa = new B; //构造函数被调用两次,A先B后
func(pa); //析构函数只被调用一次,A调用
return 0;
}
执行结果:
在main函数中用new建立一个子类无名对象和定义一个父类对象指针,并将匿名对象的地址赋给父类对象指针,当我们用delete运算符回收匿名对象时,系统只执行父类的析构函数,而不执行子类的析构函数。
解决办法:把基类的析构函数定义为虚函数
关于这类问题,博主心血来潮,还想赘述一番,于是又举了个栗子:
(1)不是虚析构
#include <iostream>
using namespace std;
class A
{
public:
~A() //(1)基类A的析构未加virtual,即不是虚析构
{
cout << "A的析构" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "B的析构" << endl;
}
};
int main()
{
A *pa1 = new A;
delete pa1; //A的析构
A *pa2 = new B;
delete pa2; //A的析构
B *pb = new B;
delete pb; //B的析构 A的析构
return 0;
}
(2)是虚析构
#include <iostream>
using namespace std;
class A
{
public:
virtual ~A() //(2)基类A的析构加virtual,即是虚析构
{
cout << "A的析构" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "B的析构" << endl;
}
};
int main()
{
A *pa1 = new A;
delete pa1; //A的析构
A *pa2 = new B;
delete pa2; //B的析构 A的析构
B *pb = new B;
delete pb; //B的析构 A的析构
return 0;
}
还有一个可能会被误导的地方:虚函数的默认参数是动态还是静态
看下面的栗子:
#include <iostream>
using namespace std;
class A
{
public:
virtual void fun(int i = 10)
{
cout << "A::fun() is calling, i = " << i << endl;
}
};
class B:public A
{
public:
virtual void fun(int i = 99)
{
cout << "B::fun() is calling, i = " << i << endl;
}
};
int main()
{
A *pa = new B;
pa->fun();
return 0;
}
相信很多朋友觉得结果为“B::fun() is calling, i = 99”更符合预期。其实这是C++的虚函数规则,虚函数是动态绑定的,而虚函数的默认参数是静态绑定的,因此在编译时已经把i的值确定为10,而不管在后面执行时动态调用哪个虚函数。
多重继承
一个类有多个直接基类的继承关系称为多继承
class D : public B, public A, public C
{
};
多继承时的构造函数调用顺序:按照它们被继承时声明的顺序(从左到右)(B A C)
钻石型继承二义性:
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。
虚继承 和 虚基类
虚继承主要用来解决多继承时可能发生对同一父类继承多次而产生的二义性问题。为最远的子类提供唯一的父类成员,而不重复产生多次复制。多个子类继承自同一个父类时,将继承方式指定为虚继承(继承时加关键字virtual)即可,此时的父类称为虚基类。
class A{};
class B : virtual public A{};
class C : virtual public A{};
class D : public C, public B
{
};
这里的A就是虚基类。