Chapter8 函数探幽

Chapter8 函数探幽

8.1 内联函数

内联函数是C++为了提高程序运行速度所做的一项改进。常规函数和内联函数的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中

执行常规函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完教主后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着使用函数时需要一定的开销。而内联函数的编译代码与其他程序代码“内联“起来了。这也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多的内存
内联函数和常规函数

要使用内联函数,必须采取下述措施之一:
(1)在函数声明前加上关键字inline;
(2)在函数顶以前加上关键字inline。
通常的做法是省略原型,将整个定义放在本应该提供原型的地方。(内联函数不能递归,与常规函数一样,也是按值来传递参数的)

宏是通过文本替换来实现的,并非传递参数

8.2 引用变量

引用是已定义的变量的别名。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。

8.2.1 创建引用变量

C和C++使用&符号来指示变量的地址,而C++给&符号赋予了另一个含义,将其用来声明引用。例如,要将rodents作为rats 变量的别名,可以这样做:

1
2
int rats;
int & rodents = rats; //makes rodents an alias for 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
2
3
4
5
6
7
8
template <typename AnyType>	//建立一个模板,并把类型命名为AnyType,关键字template和typename是必须的,可用class代替typename
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}

模板不创建任何函数,而只是告诉编译器如何定义函数

可以提供一个具体化函数定义——称为显式具体化(explicit specialization),其中包含所需的代码。当编译器找到与函数调用相匹配的具体化定义时,将使用该定义,而不再寻找模板。如:

1
2
3
4
5
6
7
struct job
{
char name[40];
double salary;
int floor
};
template <> void Swap<job>(job &, job &) //显式具体化
  • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本
  • 显式具体化函数的原型和定义应以template<>打头,并通过名称来指出类型
  • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板

在代码中包含函数模块本身并不会生成函数定义,它指示一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的时模板实例(instantiation)。模板并非函数定义,但某一类型的模板实例时函数定义,这种实例化方式被称为隐式实例化

最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化(explicit instantiation),这意味着可以直接命令编译器创建特定的实例。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:

1
template void Swap<int> (int, int); //explicit instantiation

试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。

隐式实例化、显式实例化和显式具体化统称为具体化(specialization),它们的相通之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
template <class T>
void Swap(T &, T &); // 模板原型
template<> void Swap<job>(job &, job&); //显式具体化, job声明同上
int main(void)
{
template void Swap<char> (char &, char &); //显式实例化
short a,b;
...
Swap(a,b); //short隐式实例化
job n,m;
...
Swap(n,m); //job结构显式具体化
char g, h;
...
Swap(g, h); //char型显式实例化
...
}

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这个过程称为重载解析(overloading resolution),大致过程如下:

  1. 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
  2. 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
  3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
    1. 通常从最佳到最差的顺序如下所述:
      1. 完全匹配[1],但常规函数优先于模板[2]
        image-20201018192526290
      2. 提升转换(如,char和short自动转换为int,float自动转换为double)
      3. 标准转换(如int转换为char,long转换为double)
      4. 用户定义的转换,如类声明中定义的转换。

如果有多个匹配的原型,则百年一起将无法完成重载解析过程;如果没有最佳的可行函数,编译器将生成一条错误信息。

重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个时非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体[3],则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然如果不存在匹配的函数,也是错误的。

1
decltype(x) y; // make y the same type as x

对于无法预先得知模板返回值,可通过使用关键字decltype和后置返回类型(trailing return type)来创建模板:

1
2
3
4
5
6
template <class T1, class T2>
auto func(T1 x, T2 y) ->decltype(x + y)
{
...
return x + y;
}

[1]进行完全匹配时,C++允许某些“无关紧要”的转换

[2]如果两个完全匹配的函数都是模板函数,则较具体[3]的模板函数优先

[3]指编译器推断使用哪种类型时执行的转换最少