你好,C++(28)用空间换时间 5.2 内联函数 5.3 重载函数

5.2  内联函数

通过5.1节的学习我们知道,系统为了实现函数调用会做很多额外的幕后工作:保存现场、对参数进行赋值、恢复现场等等。如果函数在程序内被多次调用,且其本身比较短小,可以很快执行完毕,那么,在完成这个函数调用的时候,系统花在完成这些幕后工作上的时间将远大于最核心的函数本身执行的时间,这就像好钢用在了刀背上,将极大地减低程序的性能。为了解决这个问题,C++提供了内联函数的机制,通过将函数代码内联到函数调用的地方,避免函数调用过程中的那些幕后工作,从而提高这种短小的函数被重复多次调用的性能。

5.2.1  用空间换时间的内联函数

内联是C++对函数的一种特殊修饰,实际上,内联函数可以更形象地被称为内嵌函数。当编译器编译程序时,如果发现某段代码调用的是一个内联函数,那么它就不再去调用该函数,而是将该函数的代码直接插入当前函数调用的位置,这样就省去了函数调用过程中的那些繁琐的幕后工作,提高了代码的执行效率,这样就以程序空间的增加换取了执行时间的减少。就像家里的电灯经常坏掉,每次都要花时间去找修理工来修理,修完了还得把人家送回去。修理灯泡可能只需要5分钟,而找修理工却可能需要50分钟,大把的时间浪费在了非关键的事情上。为了提高效率,于是干脆自己在家里养一个修理工。这样只要电灯坏了,就可以立刻进行修理,省去了每次去找修理工所浪费的时间,提高了效率。

内联函数是对函数的一种修饰,我们只需要在一个普通函数的定义前加上inline关键字,就可以把它修饰为一个内联函数。其语法格式如下:

inline返回值类型标识符 函数名(形式参数表)
{
    函数体语句;
}

例如,前面的Add()函数会被频繁调用且比较短小,那么我们就可以在其定义前加上inline关键字使其成为一个内联函数:

// 内联的Add()函数
inline int Add(int a, int b)
{
    return a + b;
}

这里需要注意的是,如果一个函数的声明和定义是相互分离的,我们必须保证在函数的定义之前具有inline关键字(为了提高代码的可读性,声明前也最好加上),这样才能使其成为一个内联函数。

当一个函数成为内联函数后,其调用形式跟普通函数的调用形式没有任何区别,例如:

int main()
{

    // 调用内联函数Add()
    int res = Add(2,3);

    return 0;
}

编译器在编译这段代码时,因为被调用的Add()函数是一个内联函数,所以它将不再是一次函数调用,编译器会直接把Add()函数的代码插入到函数调用的位置,省略了中间的函数调用过程,在一定程度上提高了性能。

5.2.2  内联函数的使用规则

凡事都是有利有弊。就像在家里养个修理工,可以节省时间,可也会带来一个麻烦:家里不得不多花钱养一个修理工。而内联函数也有着同样的烦恼,因为内联函数需要在每个调用该函数的地方插入该函数的代码,这将导致整个程序体积的增大。只有在使用的时候遵循下面这些内联函数的使用规则,才能够尽量做到扬长避短,充分利用内联函数的优势来提高程序的性能。

1. 内联函数要短小精悍

内联函数的实质是将函数代码复制到函数调用的位置, 它的使用会增加程序的体积,所以内联函数应该尽量做到短小精悍,一般不要超过5行。如果将一个比较复杂的函数内联,往往会导致程序的体积迅速膨胀,最后往往得不偿失。

2. 内联函数执行的时间要短

内联函数不仅要短小,同时其执行时间也要短,这样才能体现内联函数的优势。如果函数执行的时间远大于函数调用过程中那些幕后工作所花费的时间,那么通过内联函数所节省下来的那一点点时间也就无足轻重了。

3. 内联函数应该是被重复多次调用的

内联函数每次调用节省下来的时间其实非常微小,只有当这些时间累积到一定程度后,它才具有实际的意义。如果一个内联函数只被调用过一次,那么它节省下来的那一点点时间是没有任何实际意义的,反倒是因为内联函数而增加了程序的体积。

4.  inline关键字仅仅是一种建议

inline关键字仅仅是一种对编译器的建议,表明程序员对这个函数的处理意见。但是在某些特定的情况下,编译器将不理会inline关键字,而强制让函数成为普通函数。这时编译器会给出相应的警告消息。反过来,如果某个函数并没有加上inline修饰,但如果编译器在进行代码优化的时候,认为将这个函数内联会提高代码性能,也会自作主张地将其内联。所以,inline关键字只是表示我们建议编译器将函数内联处理,至于到底是否进行内联处理,最终还是要看编译器的脸色。

虽然内联函数可以在一定程度上提高应用程序的性能,但是它并不是解决性能问题的灵丹妙药,也并不是用得越多越好。很多时候,如果发现应用程序存在性能上的问题,更多地应该从应用程序的结构和设计上寻找问题的解决办法。内联函数,只是饭前的小甜点而已,偶尔尝一点还不错,但是并不能当饭吃。

5.3  重载函数

“嘿,编译器,我需要一个Add()函数,帮我搬一个Add()函数箱子过来。”

“老板,我这有好几个Add()函数箱子呢,有的可以做int类型数据的加法,有的可以做float类型数据的加法,有的甚至还可以做字符串的加法,你到底要哪个Add()函数箱子呢?”

“这么多?我就想要一个可以计算两个int类型数据之和的Add()函数箱子。”

“我明白了,老板你要的是贴有‘int Add(int a, int b)’标签的函数箱子。”

“干吗这么多函数箱子都叫Add啊,不怕搞混淆了吗?”

5.3.1  重载函数的声明

所谓重载(overload)函数,就是让具有相似功能的函数拥有相同的函数名。反过来讲,也就是同一个函数名可以用于功能相似但又各有差异的多个函数。这里大家可能会问,一个函数名表示一个意义不是很好吗,为什么要用一个函数名表示多个意义呢?这样不会造成混乱吗?

回到我们刚才的那个例子,如果我们想要在程序中实现“加和”这个意义,很自然地,我们可以定义一个Add()函数:

int Add(int a, int b)
{
      return a + b;
}

这个Add()函数可以计算两个整数的和,可是我们在程序中还可能需要计算其他类型数据的和,比如两个浮点数的和,这个Add()函数就无法计算了。为此,我们不得不实现另外一个AddFloat()函数来完成两个浮点数“加和”的计算:

float AddFloat(float a, float b)
{
    return a + b;
}

AddFloat()函数可以暂时地解决问题,可是我们还可能会遇到更多种类型数据的“加和”,这样在我们的程序中,就可能出现AddDouble()、AddString()、AddHuman()等一系列表示“加和”意义的函数。同样是表示“加和”这个动作,却有着不同的函数名,这使得我们在调用这些函数的时候,还不得不人为地根据具体情况进行选择。这不仅非常麻烦,而且如果选择错误,还会导致结果出错。函数重载机制就是用来解决这个问题的。它可以让拥有相似功能的函数拥有相同的函数名,而在实际调用的时候,编译器会根据实际参数的个数和类型的不同,在多个同名的重载函数中进行匹配,最终确定调用哪个函数。这样省去了我们选择函数的麻烦,同时也让代码的形式更加统一一致。

准确地讲,两个函数,因为实现的功能相似,所以取相同的函数名,但是参数的个数或类型不同,就构成了函数重载,而这两个函数就被称为重载函数。

下面来看看如何使用函数重载来解决上文中Add()函数所遇到的问题:

// 定义第一个Add()函数,使其可以计算两个int型数据的和
int Add( int a, int b )
{
    cout<<"int Add( int a, int b )被调用!"<<endl;
    return a + b;
}

// 重载Add()函数,对其进行重新定义,
// 使其可以计算两个double型数据的和
double Add( double a, double b )
{
    cout<<" double Add( double a, double b )被调用!"<<endl;
    return a + b;
}

int main()
{
    // 因为参数是整型数,其类型、个数
    // 与int Add( int a, int b )匹配
    // 所以int Add( int a, int b )被调用
    int nSum = Add(2,3);
    cout<<" 2 + 3 = "<<nSum<<endl;

    // 作为参数的小数会被表示成double类型,
    // 其类型、个数与double Add( double a, double b )匹配
    // 所以double Add( double a, double b )被调用
    double fSum = Add(2.5,10.3);
    cout<<" 2.5 + 10.3 = "<<fSum<<endl;

    return 0;
}

经过以上这样的函数重载,编译器会根据实际调用函数时不同的参数类型和个数调用与之匹配的重载函数,这样虽然我们在代码形式上调用的都是Add()函数,可实际最终调用的却是重载函数的不同版本,从而得到正确的结果:

int Add( int a, int b )被调用!

2 + 3 = 5

double Add( double a, double b )被调用!

2.5 + 10.3 = 12.8

在这段程序中,根据计算的数据类型的不同,我们对Add()函数进行了重载,分别实现了其int类型版本和double类型版本。在输出结果中,我们可以清楚地看到第一次对Add()函数的调用,实际上执行的是int类型版本“int Add( int a, int b )”,而第二次执行的是double类型版本“double Add( double a, double b)”。为什么同样是对Add()函数的调用,两次执行的却是不同的函数呢?这是因为主函数在第一次调用Add()函数时,给出的实际参数“2”和“3”的类型为int类型,而在Add()函数的两个重载版本中,只有第一个int类型版本与之最为匹配,不仅参数类型相同,同时参数个数也相同,所以执行第一个int类型版本的Add()函数;而在第二次用“2.5”和“10.3”作为实际参数调用Add()函数时,因为小数常数在程序中会被表示成double类型,所以编译器会找到第二个double类型版本的Add()函数跟它匹配,所以最终会执行第二个Add()函数。

函数重载可以让我们以统一的代码形式调用那些功能相近的多个函数,这样可以简洁代码同时省去选择函数的麻烦。那么,什么时候需要使用函数重载呢?只要发现程序中有多个函数的功能相似,只是处理的数据不同,就可以使用函数重载。换句话说,只要多个函数表达的动作相同而动作的对象不同,就可以使用相同的函数名来表示相同的动作,而用不同的参数来表示不同的动作对象。需要注意的是,我们不要将不同功能的函数定义为重载函数,虽然这样做在语法上是可行的,但是这样做违背了“函数名应准确表达函数功能”的原则,很有可能导致对函数的误用。

5.3.2  重载函数的解析

我们知道,编译器是通过参数的类型和个数的不同来区分重载函数的不同版本的。所以,相同函数名的多个函数,只有相互之间的参数类型或者个数不同,才可以构成合法的重载函数的多个版本。例如:

// 参数类型不同构成合法的函数重载
int max(int a, int b);
float max(float a, float b);
double max(double a, double b);

以上三个函数分别接受两个int、float和double类型的参数,具有不同的参数类型,因此可以构成正确的函数重载。但要特别注意的是,如果两个函数仅仅是返回值类型不同,并不能构成函数重载,例如:

// 仅仅是函数返回值不同,不能构成合法的函数重载
int max(int a, int b);
float max(int a, int b);

这是因为函数调用时,函数的返回值有时并不是必需的,如果只是单纯调用函数而没有返回值,这时编译器就无法判断到底该调用哪一个重载函数了。

当定义了正确的重载函数后,在调用这些重载函数时,编译器就该忙活了,它需要根据我们在调用时给出的实际参数找到与之匹配的正确的重载函数版本。

首先,编译器会进行严格的参数匹配,包括参数的类型和个数。如果编译器发现某个重载函数的参数类型和个数都跟函数调用时的实际参数相匹配,则优先调用这个重载函数。例如:

// 调用int max(int a,int b)
int nMax = max(1, 2);

这里的实际参数“1”和“2”都是int类型,参数个数是两个,这就跟“int max(int a, int b)”这个重载函数严格匹配,所以这里调用的就是这个int版本的max()函数。

如果无法找到严格匹配的重载函数,那么编译器会尝试将参数类型转换为更高精度的类型去进行匹配,比如将char类型转换为int类型,将float类型转换为double类型等。如果转换类型后的参数能够找到与之匹配的重载函数,则调用此重载函数。例如:

// 调用int max(int a,int b)
char cMax = max('a','A');

这里的实际参数‘a’和‘A’是char类型,并没有与之匹配的重载函数,编译器会尝试将char类型的参数转换为更高精度的int类型,这时就会找到“int max(int a,int b)”与之匹配,所以这里实际调用的是int类型版本的max()函数。这里值得注意的是,参数可以从低精度的数据类型转化为高精度的数据类型进行匹配,但并不意味着高精度的数据类型也可以转化为低精度的数据类型进行匹配。这是因为在由高到低的转化中会有多种选择,反正都有精度损失,一个double类型的参数既可以转化为int类型也可以转化为short类型,这样编译器就不知道到底该调用哪一个版本的重载函数,自然也就无法完成重载了。

除了理解重载函数的匹配原则之外,在使用重载函数时,还要特别注意带参数默认值的函数可能给函数重载带来的麻烦。例如,有这样两个重载函数:

// 比较两个整型数的大小
int max(int a, int b);
// 比较三个整型数的大小,第三个数的默认值是0
int max(int a, int b , int c = 0);

当我们以

int nMax = max( 1, 2 );

的形式尝试调用这个重载函数时,从函数调用表达式中我们无法得知它到底是只有两个参数,还是拥有三个参数只不过第三个参数使用了默认参数,这就使得这个调用跟两个重载函数都能严格匹配,这时编译器就不知道到底该调用哪个重载函数版本了。所以在重载函数中应该尽量避免使用默认参数,让编译器能够准确无误地找到匹配的重载函数。

posted @ 2015-01-12 14:54  你好,C++  阅读(1605)  评论(0编辑  收藏  举报