Chapter8 函数探幽
8.1 内联函数
内联函数是C++为了提高程序运行速度所做的一项改进。常规函数和内联函数的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。
执行常规函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完教主后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着使用函数时需要一定的开销。而内联函数的编译代码与其他程序代码“内联“起来了。这也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多的内存。
要使用内联函数,必须采取下述措施之一:
(1)在函数声明前加上关键字inline;
(2)在函数顶以前加上关键字inline。
通常的做法是省略原型,将整个定义放在本应该提供原型的地方。(内联函数不能递归,与常规函数一样,也是按值来传递参数的)
宏是通过文本替换来实现的,并非传递参数。
8.2 引用变量
引用是已定义的变量的别名。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。
8.2.1 创建引用变量
C和C++使用&符号来指示变量的地址,而C++给&符号赋予了另一个含义,将其用来声明引用。例如,要将rodents作为rats 变量的别名,可以这样做:
1 | int rats; |
其中,&不是地址运算符,而是类型标识符的一部分。就像生命中的char * 指的是指向char的指针一样,int &指的是指向int的引用。上述声明允许将rats和rodents互换——它们指向相同的值和内存单元。
必须在声明引用的时候进行初始化,引用更接近const 指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于他。也就是说:
1 | int & rodents = rats; |
实际上是下述代码的伪装表示:
1 | int * const pr = &rats; |
其中引用rodents扮演的角色与表达式*pr相同。
8.2.2 将引用作为函数参数
引用经常被用作函数参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。
如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。
引用参数应尽可能使用const引用:
- 使用const可以避免无意中修改数据的编程错误;
- 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
- 使用const引用使函数能够正确生成并使用临时变量。
返回引用的函数返回的实际上是被引用的变量的别名。返回引用时应避免返回函数终止时不再存在的内存单元引用。
基类引用可以指向派生类对象,而无需进行强制类型转换。
8.2.3 何时使用引用参数
使用引用参数的主要原因有两个:
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
引用、指针、值传递使用场景:
- 对于使用传递的值而不做修改的函数
- 如果数据对象很小,则按值传递
- 如果数据对象是数组,则使用指针
- 如果数据对象是较大的结构,则使用const指针或const引用给,以提高程序的效率
- 如果数据对象是类对象,则使用const引用
- 对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针
- 如果数据对象是数组,则只能使用指针
- 如果数据对象是结构,则使用引用或指针
- 如果数据对象是类对象,则使用引用。
8.3 默认参数
默认参数是指当函数调用中省略了实参时自动使用的一个值。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数(只有原型需指定默认值,函数定义与没有默认参数时完全相同)。
对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。
8.4 函数重载
函数多态(函数重载)使得能能够使用多个同名的函数。术语”多态”指的是有多种形式,因此函数多态可以有多种形式。类似地,术语“函数重载”指的是可以有多个同名的函数,因此对名称进行了重载。这两个术语指的是同一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。
C++使用上下文来确定要使用的重载函数版本。
函数重载的关键是函数的参数列表——也称为函数特征标识(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。*编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标*
将非const值赋给const变量时合法的,但反之则是非法的。
仅当函数基本上执行相同的任务,但是用不同形式的数据时,才应采用函数重载。
使用C++开发工具中的编译器编写和编译程序时,C++编译器将执行名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。
8.5 函数模板
函数模板时通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。
函数模板允许以任意类型的方式来定义函数,例如,如下示建立一个交换模板:
1 | template <typename AnyType> //建立一个模板,并把类型命名为AnyType,关键字template和typename是必须的,可用class代替typename |
模板不创建任何函数,而只是告诉编译器如何定义函数。
可以提供一个具体化函数定义——称为显式具体化(explicit specialization),其中包含所需的代码。当编译器找到与函数调用相匹配的具体化定义时,将使用该定义,而不再寻找模板。如:
1 | struct job |
- 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本
- 显式具体化函数的原型和定义应以template<>打头,并通过名称来指出类型
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板
在代码中包含函数模块本身并不会生成函数定义,它指示一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的时模板实例(instantiation)。模板并非函数定义,但某一类型的模板实例时函数定义,这种实例化方式被称为隐式实例化。
最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化(explicit instantiation),这意味着可以直接命令编译器创建特定的实例。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:
1 | template void Swap<int> (int, int); //explicit instantiation |
试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
隐式实例化、显式实例化和显式具体化统称为具体化(specialization),它们的相通之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
示例:
1 | ... |
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这个过程称为重载解析(overloading resolution),大致过程如下:
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
如果有多个匹配的原型,则百年一起将无法完成重载解析过程;如果没有最佳的可行函数,编译器将生成一条错误信息。
重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个时非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体[3],则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然如果不存在匹配的函数,也是错误的。
1 | decltype(x) y; // make y the same type as x |
对于无法预先得知模板返回值,可通过使用关键字decltype和后置返回类型(trailing return type)来创建模板:
1 | template <class T1, class T2> |
[1]进行完全匹配时,C++允许某些“无关紧要”的转换
[2]如果两个完全匹配的函数都是模板函数,则较具体[3]的模板函数优先
[3]指编译器推断使用哪种类型时执行的转换最少