第八章 函数探幽
8.1. C++内联函数
内联函数是CPP为了提高程序运行速度所做出的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于CPP编译器如何将他们组合到程序当中。什么意思呢,就是对程序员除了关键字以外,没有任何编写代码方面的区别,它是让编译器来识别的,不同函数编译器处理的方式不同。
内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置来执行代码(详情见计算机组成原理)。因此,内联函数的运行速度比常规函数稍快,但是代价是会占用更多内存。如果程序在10个不同的地方调用同一个内联函数,那么这个程序将会在内存中生成10个相同的这个函数的代码。
使用方法,二选一:
- 在函数声明前加上关键字inline
- 在函数定义前加上关键字inline
通常的做法是省略原型,将整个定义放在本应该提供原型的地方。这也说明了内联函数最好是那种比较短的程序。
内联与红
inline是CPP新增的特性。C语言使用#define来提供宏——内联代码的原始实现。
8.2. 引用变量
CPP新增了一种复合类型——引用变量。引用是已定义的变量的别名。比如,如果将twin作为element变量的引用,那么可以使用twin和element交替来使用这个变量。引用变量的主要作用是作为函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样的话,除了指针以外,引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。
int rats;
int& rodents = rats;
其中,&不是地址运算符,而是类型标识符的一部分。就想声明中的char*是指向char的指针一样,int&指向的是int的引用。上述声明允许rats和rodents互换——它们指向相同的值和内存单元(地址相同)。使用其中一个变量名称来改变内存当中的值,另一个变量名称也会改变。
它和指针的区别在哪呢?
因为改变它,那么另一个引用也会改变,所以这个引用更加像const,只能在初始化时进行赋值。而指针如果改变它指向的地址,另一个变量名称所指向的东西则不会改变。
8.2.2. 将引用用作函数参数
引用经常被用作函数参数,是的函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法被称为按引用传递。这允许被调用的函数能够访问调用函数中的变量。C语言只能按值传递,这样会导致被调用函数使用调用程序的值的拷贝。当然,C语言还能使用指针传递的方式来实现上述的操作。
8.2.3. 引用的属性和特别之处
如果实参与引用参数不匹配,CPP将生成临时变量。当前,仅当参数为const引用时,CPP才允许这样做。
什么时候创建临时变量你额?
- 实参的类型正确,但不是左值
- 实参的类型不正确,但可以转换为正确的类型
如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量。
8.2.4. 将引用用于结构
引用非常适合用于结构和类,只需在声明结构参数时使用引用运算符&就可以。
struct free_throws{
std::string name;
int made;
int attempts;
float percent;
};
函数原型:
void set_pc(free_throws& ft);
// 如果不希望函数修改传入的结构,可使用const
void display(const free_throws& ft);
8.2.5. 将引用用于类对象
将类对象传递给函数时,CPP通常的做法是使用引用。
例如,可以通过使用引用,让函数将类string、ostream、istream、ofstream和ifstream等类的对象作为参数
8.2.7. 何时使用引用参数
使用引用参数的主要原因有两个。
• 程序员能够修改调用函数中的数据对象。
• 通过传递引用而不是整个数据对象, 可以提高程序的运行速度。
当数据对象较大时(如结构和类对象), 第二个原因最重要。这些也是使用指针参数的原因。这是有道理的, 因为引用参数实际上是基千指针的代码的另一个接口。那么, 什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:对千使用传递的值而不作修改的函数。
• 如果数据对象很小, 如内置数据类型或小型结构, 则按值传递。
• 如果数据对象是数组, 则使用指针, 因为这是唯一的选择, 并将指针声明为指向const 的指针。
• 如果数据对象是较大的结构, 则使用const 指针或const 引用, 以提高程序的效率。这样可以节省复制结构所需的时间和空间。
• 如果数据对象是类对象, 则使用const 引用。类设计的语义常常要求使用引用, 这是C++新增这项特性的主要原因。因此, 传递类对象参数的标准方式是按引用传递。对于修改调用函数中数据的函数:
• 如果数据对象是内置数据类型,则使用指针。如果看到诸如fix.it (&x)这样的代码(其中x 是int),则很明显, 该函数将修改x。
• 如果数据对象是数组, 则只能使用指针。
• 如果数据对象是结构, 则使用引用或指针。
• 如果数据对象是类对象, 则使用引用。
当然, 这只是一些指导原则, 很可能有充分的理由做出其他的选择。例如, 对于基本类型, cin 使用引用, 因此可以使用cin>>n, 而不是cin >> &n。
8.3. 默认参数
默认参数指的是当函数调用中省略了实参时自动使用的一个值。例如, 如果将void wow (int n) 设置成n 有默认值为1’ 则函数调用wow()相当千wow (1)。这极大地提高了使用函数的灵活性。
如何设置默认值呢?必须通过函数原型。由千编译器通过查看原型来了解函数所使用的参数数目, 因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。例如, left( )的原型如下:
char* left(const char* str, int n = 1);
8.4. 函数重载
默认参数让你能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)能让你使用多个同名的函数。“函数重载”指的是可以有多个同名的函数,因此对名称进行了重载。
多态和重载是一回事。
重载的关键是函数的参数列表,也称为函数特征标function signature。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则他们的特征标相同,而变量名是无关紧要的。
CPP如何跟踪每一个重载函数呢
通过名称修饰name decoration或者名称矫正name mangling,它根据函数原型中指定的形参类型对每个函数名进行加密。
long MyFunctionFoo(int, float);
?MyFunctionFoo@@YAXH
对参数数目和类型进行编码。添加的一组符号随函数特征标而不同,而修饰时使用的约定随编译器而异。
8.5.函数模板
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数。其中的泛型可以使用具体的类型来进行替换。通过将类型作为参数传递给模板,可以使编译器生成该类型的函数。由于模板允许以泛型的方式编写程序,因此也被成为通用编程。由于类型是用参数表示的,因此模板特性有时也被成为参数化类型。
template <typename AnyType>
void Swap(AnyType &a, AnyType &b){
AnyType temp;
temp = a;
a = b;
b = temp;
}
第一行指出,要建立一个模板,并将类型命名为AnyType。关键字template和typename是必需的,除非可以使用关键字class代替typename。另外,必需使用尖括号。类型名可以任意选择(这里为AnyType)。许多程序员都用简单的名称,比如T。剩下的代码描述了交换两个AnyType值的算法。模板并不创建任何函数,而是告诉编译器如何定义函数。需要交换int的函数时,编译器将模板模式创建这样的函数,并用int替代AnyType。同样,需要交换double的函数时,编译器将按模板模式创建这样的函数,并用double代替AnyType。
template <class AnyType>
void Swap(AnyType &a, AnyType &b){
AnyType temp;
temp = a;
a = b;
b = temp;
}
8.5.1. 重载的模板
需要多个对不同类型使用同一种算法的函数时,可以使用模板。然而,并非所有的类型都使用相同的算法。可以像重载常规函数定义那样重载模板定义。
8.5.5. 编译器选择使用哪一个版本
对于函数重载、函数模板、函数模板重载,CPP要决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析overloading resolution
- 创建候选函数列表。包含与被调用函数的名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数, 为此有一个隐式转换序列, 其中包括实参类型与相应的形参类型完全匹配的情况。例如, 使用float 参数的函数调用可以将该参数转换为double, 从而与double 形参匹配, 而模板可以为float生成一个实例。
- 确定是否有最佳的可行函数。如果有, 则使用它, 否则该函数调用出错。
还可以通过编写合适的函数调用,引导编译器作出你想要的选择。