一、关于虚函数
C++的多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a、编译时多态性:通过重载函数实现
b、运行时多态性:通过虚函数实现。
2、虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
3.虚函数表
编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4(x64下是N*8)的大小。
派生类的虚函数表存放重写的虚函数,当基类的指针指向派生类的对象时,调用虚函数时都会根据vptr(虚表指针)来选择虚函数,而基类的虚函数在派生类里已经被改写或者说已经不存在了,所以也就只能调用派生类的虚函数版本了.
4.虚表指针
虚表指针在类对象中,每个同类对象中都有个一个vptr,指向内存中的vtable,所有同类对象,共享一个vtable,但是每个对象都自带一个vptr指向这个vtable,否则调用虚函数的时候会找不到正确的函数入口,(后面将会讲明)虚表指针是对象的第一个数据成员。
二、什么时候用到虚指针
答:这种通过虚表间接寻址访问的情况只有在使用对象的指针或引用来调用虚函数时候才会出现。(是以避免以后出现子类覆盖父类虚函数时,使用父类引用调用虚函数产生的多态性)当直接使用对象调用自身的虚函数时,没有必要查表访问。这事因为已经明确调用的是自身成员函数,根本没有构成多态性,查询虚表只会画蛇添足,降低程序执行效率,所以将这种情况处理为直接调用方式。而使用指针或引用,因为不知道要调用的虚函数的地址,就只能使用虚表指针间接寻址!
个人总结:
1、没有继承父类的子类(因为没有父类,也可以把自己看成父类)实例化以后,通过引用或指针调用虚函数的时候,才会使用虚表指针间接访问虚函数。访问其他函数,都是静态绑定,在编译期间就已经确定下来了!
三、构造函数初始化列表
类成员初始化总在构造函数执行之前
1)从必要性:
a. 成员是类或结构,且构造函数带参数:成员初始化时无法调用缺省(无参)构造函数
b. 成员是常量或引用:成员无法赋值,只能被初始化
2)从效率上:
如果在类构造函数里赋值:在成员初始化时会调用一次其默认的构造函数,在类构造函数里又会调用一次成员的构造函数再赋值
如果在类构造函数使用初始化列表:仅在初始化列表里调用一次成员的构造函数并赋值
如:
class CExample {
public:
int a;
float b;
//构造函数初始化列表
CExample(): a(0),b(8.8)
{}
//构造函数内部赋值
CExample()
{
a=0;
b=8.8;
}
};
上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。
但有的时候必须用带有初始化列表的构造函数:
1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
2.const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
初始化列表的成员初始化顺序:
C++初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
Example:
class CMyClass {
CMyClass(int x, int y);
int m_x;
int m_y;
};
CMyClass::CMyClass(int x, int y) : m_y(1), m_x(m_y)
{
}
可能以为上面的代码将会首先做m_y=1,然后做m_x=m_y,最后它们有相同的值。
但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的。结果是m_x将有一个不可预测的值。
有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆
应用场景:
在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负 责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。在内部类型如ints或者longs或者其它没有构 造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。