玄之又玄,众妙之门。
我们的程序都会用到函数,函数的调用过程看似简单易懂,实则“玄之又玄”(先营造一下氛围<( ̄︶ ̄)>)。下面本文会对各种类型的函数进行一些探究,并和大家一起学习函数方面的知识。
在正式进入函数的探究之前,先介绍一些书在讲函数时可能会碰到的两个概念。
概念1:函数名称mangling
即编译器会为我们的函数名加上一些修饰,使它在程序中独一无二。具体的名称修饰算法不同的编译器可能会有不同的实现,感兴趣的同学可以自己在网上搜。要想看到修饰后的结果,可以声明一个函数而不定义,在main中调用它。这时,在连接的时候将会报错,可以在这个错误中看到结果(起码VC中是可以的)。当然可以使用extern“C”声明来抑制nonmember functions的“mangling”操作。
概念2:NRV优化
name return value优化。即在函数返回值为对象时,不再直接返回得到的结果对象,而是通过引用的方式将结果值直接赋到外部的变量中。从而减少对象构造操作的调用。对于很多现在的主流编译器来说,在默认情况下都会对直接返回对象的函数实施NRV优化。但并不是所有的都这样,例如早期的 cfront需要提供一个显式的拷贝构造函数来激活这项功能。
1.非成员函数
对于这种情况相信大家都很熟悉了。一般过程就是执行到函数调用语句之后会进入函数体(参数从右往左入栈,然后call相应的函数实现地址),执行相应的操作之后返回,然后接着执行函数调用后面的语句。想知道地更详细可以查看程序的反汇编内容,在这里可以清楚地看到函数的执行逻辑。在VC6.0中,可以在调试的时候通过“查看”——>”调试窗口”——>”Disassembly”来进行查看。如下图示:
我们下面主要探索的是关于成员函数的“故事”。是的,我虽然不知道它们的梦想是什么,但从它们的歌声中我隐约地感觉到它们有“故事”。
2.普通成员函数
C++的设计准则之一:nonstatic member function至少必须和一般的nonmember function有相同的效率。
其实在编译器的内部会将member function的函数实例转换为对等的nonmember function函数实例。
假如我们有如下的类定义:
class Person { private: int age; string name; public: /*一些成员函数的声明*/ };
以成员函数 int Person::getAge(){return age;}为例。
首先,编译器将改写函数原型,在其中插入一个额外的参数,用于提供一个存取的通道,使得类对象可以来调用该函数。
扩展后:int Person::getAge(Person *const this)//this指针指向特定的对象
若为const成员函数:int Person::getAge()const
扩展后:int Person::getAge(const Person *const this)//this指针指向特定的常量对象。
然后,将每一个非静态成员变量的存取操作改为经由this指针来存取。
{return this->age;}
这也非静态成员函数中this指针的由来。
最后,将成员函数重新写成一个外部的非成员函数。
extern getAge(register Person *const this)
说明:本文中getAge(蓝底红字格式)用以表示经过“mangling”操作后相应标识符的名称。extern是函数默认的修饰,register只是对编译器的优化建议,不一定会采用。
若有Person类的对象person和Person类对象指针ptr,则其函数的调用将会发生如下变化:
person.getAge() ==> getAge(&person)
ptr->getAge() ==> getAge(ptr)
3.静态成员函数(static)
假设类中有静态成员函数static void study(),则对其的如下调用操作:
person.study();//person是Person类的实例对象
ptr->study();//ptr是Person类对象的指针
Person::study();
((Person *) 0)->study();
都会转换为: study(); //study为相应成员函数被改写为非成员函数,然后经过函数名的“mangling”操作后得到的名称。
与普通成员函数相比,static成员函数没有增加this参数,自然也不会有this相关的操作。它的主要特性如下:
1. 不能直接存取class中的非静态成员变量。因为没有this指针的绑定,无法存取到特定对象的成员变量。
2. 不能被声明为const、volatile、或virtual。
3. 不需要经由类对象来调用。
由上面的分析我们也可以知道,指向静态成员函数的指针类型其实和一般函数的指针类型并没有什么区别(整个重写为外部函数的过程中只是名称的改变而已)。
即我们取指针: Person::study;
得到的类型是:void (*)(); 而不是:void (Person::*) () // Person::*表示Person类的成员函数指针 。
关于这一点可以用如下代码进行验证
typedef void(*Fun)();
//main---------------------
Person p;
Fun pf = p.study;
这段代码可以顺利通过,说明pf的类型与p.study的类型是一样的。当然,也可以故意定义一个错误的类型进行赋值,然后在编译时的错误信息中查看相关函数的类型。
类似的也可以用如下的代码来验证非静态成员函数的类型:
typedef int(Person::*Fun)();
//main----------------------------
Person p;
Fun pf =p.getAge;
4.虚成员函数(virtual)
虚成员函数的调用是通过虚拟机制动态决议的,其关键就在于虚函数表上对应函数实现地址的配置。
假如我们的类中有一个虚函数为:virtual void vFunction();
则对其的调用:
Person p;
Person *pp= &p;
pp->vFunction(); ==> (*pp->vptr[N])(pp)
说明:第一步,pp->vptr是取到对应的虚函数表指针;第二步,pp->vptr[N]是指向虚函数表的第N项;第三步,*pp->vptr[N]是取到虚函数表第N项的值,即对应虚函数的实现地址;最后参数中传递的pp是this指针的实参。注意各个操作符的优先级顺序。
当然,如果我们显示调用某个类中的虚函数是会压制虚函数机制的。例如:
假设LittleSon类继承自Person,也有 void vFunction();但是在其非静态成员函数中的如下调用会直接指定调用Person中的vFunction函数而不会触发虚拟机制。
例如某个成员函数中的如下语句:
Person:: vFunction ();
上述语句不能在成员函数外调用,因为虚函数需要this参数,非静态成员函数中可以提供相应的实参,而直接在其它地方调用则取不到所需的this指针(当然也可以通过对象或对象指针来调用)。
虚拟函数只介绍这些肯定让人感觉意犹未尽,这里只是大致有个印象,我会在第三式中详细介绍相关的细节操作。
5.内联函数(inline)
先说明三点:
1. 内联(inline)只是对编译器的一个建议,编译器不一定会采用。
2. 编译器隐式地将在类内定义的成员函数当做内联函数(还是老规矩,最后是否真的实现了内联得看编译器)。
3. 内联函数应该在头文件中定义,以便编译器能在调用点内联展开该函数的代码。不在源文件中定义也可以方便以后的维护。
一般比较适合定义成内联函数的是语句量少,调用频繁的函数。
处理一个inline函数可以分为两个阶段:
1. 分析函数的定义,以确定其是否适合以内联的方式展开。如果一个函数因其复杂度或构建问题,被判断不可成为inline ,它会被转换为一个static函数,并在“被编译模块”内产生对应的函数定义。
2. 获得编译器认可的inline函数将在调用点处扩展相应的操作。
内联扩展时会产生两个需要处理的问题,一是参数的求值;二是临时对象的管理。
形式参数求值:每一个形式参数都会被对应的实际参数取代。如果是“可能带副作用的参数”,通常会引入临时性对象;如果实际参数是一个常量表达式,可以在替换之前完成求值操作,后面展开内联的时候就可以直接用常量替换表达式;如果既不是常量表达式,也不是带副作用的表达式,那么直接替换。
假设我们有如下的inline函数:
inline int min(int i, int j)
{
return i<j ? i : j;
}
调用代码如下:
int minval;
int val1 = 10;
int val2 = 20;
minval =min(10,20);//1
minval =min(val1,val2);//2
minval =min(val1++,val2++);//3
则其实际展开情况如下:
语句1处:实际参数为常量表达式,直接用计算结果值替换,即转换为minval = 10;
语句2处:不是常量表达式,但也不会产生副作用,直接替换为相应的表达式,即为minval = val1 < val2 ? val1 :val2;
语句3处:是一个会产生副作用(val1++或val2++将会被计算多次)的表达式,需要引入临时对象。会被扩展为如下形式:
static int t1;
static int t2;
minval =(t1 = val1++),(t2 = val2++),
t1 < t2 ? t1 : t2;
局部变量的处理:函数中有局部变量是很常见的情况,将min函数做如下变动
inline int min(int i, int j)
{
int minval = i<j ? i : j;
return minval;
}
以下面的形式进行调用:
{
int minval;
minval = min(val1, val2);
}
为了维护其局部变量,可能扩展为如下的形式(理论上该局部变量是可以被优化的):
{
int minval;
//inline局部变量mangling的结果:minval
int minval;
minval = (minval=val1 < val2 ? val1 : val2), minval;
}
一般而言,inline函数中的每一个局部变量都必须被放在函数调用的一个封闭区段中,拥有独一无二的名称。
inline函数中的局部变量,在加上有副作用的参数,可能会导致大量临时对象的产生。
inline函数也是C语言宏操作的一个安全代替品(特别是宏中的参数有副作用的时候),然而一个inline函数被调用太多次的话,会产生大量的扩展代码,使程序体积暴涨。
inline中再有inline,可能会使表面是看起来普通的inline却因其连锁复杂度而无法展开。对于既要安全又要效率的程序,inline函数提供了一个强有力的工具。然而,与非inline函数比起来,它们需要更加小心地处理。