1、你一定想知道虚函数是怎么做出来的,对不对?
如果能够了解 C++编译器 对于 虚函数 的实现方式,我们就能够知道为什么虚函数可以做到动态绑定。为了达到动态绑定的目的,C++编译器通过某个表格,在执行期“间接”调用实际上欲绑定的函数。这样的表格称为 虚函数表(vtable)。每一个内含虚函数的类,编译器都会为它做出一个虚函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器也会为类的对象加上一项成员变量,是一个指向该虚函数表的指针(vptr)。举个例子:
class class1 { public: int data1; int data2; memfunc(); virtual vfunc1(); virtual vfunc2(); virtual vfunc3(); };
class1对象在内存中占据的空间为:一个虚表指针、2个整数。C++类的成员函数(非虚函数),你可以想象成就是C语言中的函数,他只是被编译器改过名称,并增加一个参数(this指针),因而可以处理调用者(C++对象)中的成员变量。所以,在class1对象的内存中并没有看到与成员函数有关的任何东西。
每一个由此类派生出来的对象,都有这么一个vptr。当我们通过这个对象调用虚函数时,事实上就是通过vptr找到虚函数表,再找出虚函数的真正地址。虚函数表的内容是依据类中虚函数的声明次序,一一填入函数指针。派生类会继承基类的虚函数表(以及所有其它可以继承的成员),当我们在派生类中改写虚函数时,虚函数表就受了影响:表中元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。动态绑定机制,在执行期,根据虚函数表,做出了正确的选择。
2、静态成员(变量与函数)
我想你已经很清楚了,如果你依据一个类产生出3个对象,每个对象将各有一份成员变量。假设你有一个类,专门用来处理存款账户,它至少要有存户的姓名、地址、存款额、利率等成员变量:
class SavingAccount { private: char m_name[40];//存户姓名 char m_addr[60];//存户地址 double m_total;//存款额 double m_rate;//利率 };
这家银行采用浮动利率,每个账户的利息都是根据当天的挂牌利率来计算。这时候m_rate就不适合成为每个账户对象中的一笔数据,否则每天一开市,光把所有账户内容叫出来,修改m_rate的值,就花掉不少时间。m_rate应该独立在各对象之外,成为类的数据。怎么做,在m_rate前加上static修饰词即可。
static成员变量不属于对象的一部分,而是类的一部分,所以程序可以在 还没有诞生任何对象的时候 就处理此种成员变量。但首先你必须初始化它。不要把static成员变量的初始化安排在类的构造函数中,因为构造函数可能一再被调用,而变量的初值却只应该设定一次。也不要把初始化操作安排在头文件中,因为它会被载入许多地方,因此也就可能被执行许多次。你应该在应用程序文件中,类以外的任何位置设定其初值。例如在main中,或全局函数中,或任何函数之外:double SavingAccount::m_rate = 0.0075;//设定static成员变量初值(定义,类中只是声明),这么做可曾考虑到m_rate是个private数据,没关系,设定static成员变量初值时,不受任何存取权限的束缚。请注意:static成员变量的类型也出现在初值设定语句中,因为这是一个设定操作,不是一个赋值操作。事实上,static在这时候才被定义出来,在类中只是声明了一下。如果你没有做这个初始化操作,会产生链接错误。
下面是几种存取static成员变量的方式,注意:此时,还没有诞生任何对象实例:
1、SavingAccount::m_rate=0.0075; 2、SavingAccount myAccount; myAccount.m_rate=0.0075;//m_rate需为public
你要搞清楚一个概念,static成员变量不是因为对象的实现才得以实现,它本来就存在,你可以想象他是一个全局变量,因此,第一种处理方式在意义上比较不会给人错误的印象。只要access level允许,任何函数(包括全局函数或成员函数,static 或 non-static)都可以存取static成员变量。但如果你希望在产生任何object对象之前就存取其class的private static成员变量,则必须设计一个static成员函数。
由于static成员函数不需要借助任何对象,就可以被调用执行,所以编译器不会为它暗加一个this指针。也因为如此,static成员函数无法处理类中的non-static成员变量。还记得吗?前面说过,成员函数之所以能够以一份单一函数代码处理各个对象的数据而不紊乱,完全靠的是this指针的指示。
static成员函数没有this参数的这种性质,正是我们的MFC应用程序在准备callback函数时所需要的。
3、C++程序的生与死:兼谈构造函数与析构函数
C++的new运算符 和 C的malloc函数都是用于配置内存,但前者比之后者的优点是,new不但配置对象所需的内存空间,同时会引发构造函数的执行。
所谓构造函数,就是对象诞生后第一个执行(并且是自动执行)的函数,它的函数名称必定要与类名称相同。相对于构造函数,自然就有个析构函数,也就是在对象行将毁灭但未毁灭之前一刻,最后(并且是自动执行)的函数。它的函数名称必定要与类名称相同,再在最前面加一个~符号。
一个有着层次结构的类群组,当派生类的对象诞生之前,构造函数的执行是由最基类 至 最尾端派生类;当对象要毁灭之前,析构函数的执行则是反其道而行。
结论:
1)对于全局对象,程序一开始,其构造函数就先被执行(比程序进入点更早);程序即将结束前其析构函数被执行。MFC程序就有这样一个全局对象,通常以application object称呼之。
2)对于局部对象,当对象诞生时,其构造函数被执行;当程序流程将离开该对象的存活范围时,其析构函数被执行。
3)对于静态(static)对象,当其对象诞生时其构造函数被执行;当程序将结束时,其析构函数被执行,但比全局对象的析构函数早一步执行。
4)对于以new方式产生出来的局部对象,当对象诞生时其构造函数被执行,析构函数则在对象被delete时执行。
4、四种不同的对象生存方式(stack-堆栈、heap-堆、global-全局对象、static-静态对象)
既然谈到了static对象,就顺便说一下所有可能的对象生存方式 及其 构造函数调用机制。在C++中,有四种方法可以产生一个对象,
第一种方法:是在堆栈(stack)中产生它:void MyFoo(){CFoo foo;//在堆栈中产生foo对象}
第二种方法:是在堆中产生它:void Myfoo(){CFoo* pFoo=new CFoo();//在堆中产生对象}
第三种方法:产生一个全局对象(同时也必然是个静态对象)CFoo foo;//在任何函数范围之外做次操作
第四种方法:产生一个静态局部对象void MyFoo(){static CFoo foo;}//在函数范围之内的一个静态对象
不论那一种做法,C++都会产生一个针对CFoo构造函数的调用操作。前2种情况,C++在配置内存之后,立刻产生一个隐藏的构造函数调用。第三种情况,由于对象实现于任何“函数活动范围”之外,显然没有地方来安置这样一个构造函数调用操作,这就需要startup代码帮忙,startup代码是什么?是更早于程序进入点执行起来的代码,由C++编译器提供,被链接到你的程序中。startup代码可能做些像函数库初始化、进程信息设立、I/Ostream产生等操作,以及对static对象的初始化操作。