8.1 C++内联函数
内联函数是C++为提高程序运行速度所作的一项改进。常规函数与内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。
编译过程的最终产品是可执行程序--由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(循环或分支语句),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数地址),并在函数结束时返回。
执行到函数调用指令使,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中),然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定开销。
内联函数的编译代码与其他程序代码"内联"起来了。编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占据更多内存。
如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。如果代码执行时间很长,则内联函数节省的时间并不大。
使用内联特性,必须采取以下措施之一:
在函数声明前加上关键字inline;
在函数定义前加上关键字inline;
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
程序员请求将函数作为内联函数时,编译器并不一定满足要求。他可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数。
#defien SQUARE(X) X*X //宏定义函数
8.2 引用变量
C++新增了一种复合类型--引用变量。引用是一定义的变量的别名。
引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
除指针之外,引用也为函数剢大型结构提高了一种个非常方便的途径。
8.2.1 创建引用变量
int rats;
int& ratrf = rats;
如同声明的int*一样,int&指的是指向int的引用。
int rat;
int& ratrf = rat;
int* ratpt = &rat;
引用还是不同于指针的,除了表示法不同,还有差别之一在于声明引用时候必须将其初始化。
int* const pt = &rat;
引用更接近const指针,一旦与某个变量关联起来,就将一直效忠于它。
8.2.2 将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。
按引用传递允许被调用函数访问调用函数中的变量。
swapv(wallet1, wallet2); // pass by value
swapr(&wallet1, &wallet2); // pass by reference
swapp(*wallet1, *wallet2); // pass by pointer
调用函数时区分是按值还是按引用的方法是查看函数原型。
传引用和传指针区别在于:
声明函数参数的方式不同;
传指针版本需要使用解除引用运算符符*;
函数调用时使用实参初始化形参,因此函数的引用参数被初始化为函数调用传递的实参。
8.2.3 引用的属性和特别之处
函数的引用形参会修改函数调用时传递的实参本身,这也提醒我们为何通常按值传递。
如果程序圆的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。
void func(const int& val1);
如果使用基本类型(整型,浮点型),应采用按值传递,而不要用按引用传递方式。当数据比较大(结构,类)时,引用参数将很有用。
double cube(double a);
double cuberef(double& ra);
double x = 2.0;
double z = cube(x + 2.0); // OK, 计算x + 2.0的值,然后pass值
double z = cuberef(x + 2.0); // shoule not compile
因此ra是一个变量的别名,则实参应该是变量,而不是一个临时值。因为x + 2.0并不是变量。
如果实参与引用参数(形参)不匹配,C++将生成临时对象。当前,仅当参数为const引用时,C++才允许这样左。
什么时候创建临时变量呢? 如果引用参数时const,则编译器将在下面两种情况下生成临时对象:
实参类型正确,但不是左值;
实参类型不正确,但可以转换为正确的类型;
左值是什么? 左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。
现在,常规变量和const变量都可视为左值,因此可通过地址访问它们,但const变量属于不可修改的左值。
double recube(cosnt double& ra);
double side = 3.0;
long edge = 5L;
double c4 = refcube(edge); // ra is temperary variable
double c5 = refcube(7.0); // ra is temperary variable
double c6 = refcube(side + 10.0); // ra is temperary variable
edge虽然是变量,但类型不对。参数7.0与side + 7.0的类型都正确,但没有名称。在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它们。这些临时变量只在函数调用期间存在,此后编译器便可随意将其删除。
简而言之,如果接受引用参数的函数意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决办法就是,禁止创建临时变量。
如果声明将引用指定为const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
如果函数调用的参数不是左值或与相应的cosnt引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
应尽可能使用const的三个理由:
使用const可以避免无意中修改数据的编程错误;
使用const使函数能处理const和非const实参,否则只能接受非const数据;
使用const引用时函数能够正确生成并使用临时对象;
C++11 新增了右值引用,引用可指向右值,是使用&&声明的:
double j == 15.0;
double&& jref = 2 * j + 18.5; // 右值引用
8.2.4 将引用用于结构
如果指定的初始值比成员少,余下的成员将被设置为零。
free_throws& accumulate(free_throws& target, const free_throws& source);
free_throws& accumulate(free_throws& target, const free_throws& source)
{
...
return target;
}
accumulate(dup, five) = four; // 可行,因为accumulate返回引用。
传统返回机制与按值传递参数类似:
计算关键字return后面的表达式,并将结果返回给调用函数.。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
diuble m = sqrt(16.0);
在上一条语句中,值4.0被复制到一个临时位置,然后被复制给m。
返回引用的函数实际上是被引用的变量的别名。
返回引用时最重要的一点是,应避免返回函数终止时不在存在的内存单元引用,即临时对象。同时也应避免返回临时变量的指针。
为避免返回临时对象的引用最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
另一个方法是用new来分配新的存储空间。
在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须表示一个可修改的内存块。
常规返回(非引用)类型式右值--不能通过地址访问的值。这是因为此中返回值位于临时内存单元中,运行到下一条语句时,它们可能不存在了。
8.2.5 将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。
// 这是个有问题的函数,因为不能返回指向一个临时对象的引用
const string& version3(const string& s1, const string& s2)
{
string temp;
temp = s1 + s2;
return temp;
}
假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。
8.2.6 对象、继承和引用
使特性从一个类传递到另一个类的语言特性叫做继承。派生类继承了基类的方法。
继承的另一个特性使,基类引用可以指向派生类对象,而无需强制类型转换。此特征的一个实际结果为,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
8.2.7 何时使用引用参数
使用参数的主要原因有两个;
程序员能够修改调用函数中的数据对象;
通过传递引用而不是整个数据对象,可以提高程序运行速度;
当数据对象较大时,第二个原因更重要。这些也是使用指针参数的原因。
对于使用传递的值而不作修改的函数:
如果数据对象很小,如内置数据类型与小型结构,则按值传递;
如果数据对象时数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针;
如果数据对象时较大的结构时,则使用const指针或const 引用,以提高程序效率,节省复制结构所需的时间和空间;
如果数据对象是类对象,则使用const引用。C++中传递类对象参数的标准方式是按引用传递;
对于修改调用函数中数据的函数:
如果数据对象是内置数据类型,则使用指针;
如果数据对象是数组,则只能使用指针;
如果对象是结构,则使用引用或指针;
如果对象是类对象,则使用引用。
8.3 默认参数
默认参数指的是当函数调用中省略了实参时自动使用的一个值。
void wow(int n =1);
如何设置默认值呢?必须通过函数原型。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法时将值赋给原型中的参数。
对于带参数列表的函数,必须从右往左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。
int group1(int k, int m = 2, int n); // wrong, 因为默认参数的右边有一个非默认参数
int group2(int k, int m = 2, int n = 2); // OK
实参按从左到右的顺序依次赋给相应的形参,而不能跳过任何参数。
int group2(2, , 3); // wrong, 实参赋值给形参时,不能跳过任何参数。
在设计类时,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
只有函数原型指定了默认值。函数定义与没有默认参数时完全相同。
8.4 函数重载
默认参数可以让您使用数目的参数调用同一个函数,而函数”多态“(函数重载)让您能够使用多个同名的函数。术语多态指的是有多种形式,因此,函数多态允许函数可以有多种形式。类似地,术语”函数重载“指的是可以有多个同名的函数,因此对名称进行了重载。函数多态与函数重载是指同一回事。
C++使用上下文来确定要使用的重载函数版本。
函数重载的关键是函数的参数化列表--也称为函数的特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名无关紧要。
void funcPrint(double d, int width);
void funcPrint(int d, int width);
void funcPrint(long d, int width);
unsigned int year = 2020;
funcPrint(year, 20); // 错误,因为存在3中转换方式
没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型强制转换进行匹配。若转换后,有多个原型符合,则C++将拒绝这种函数的调用,并将其视为错误。
double cube(double x);
double cube(double& x); // 不是overload
为避免doube 与double&原型都匹配的混乱,编译器在检查函数特征标时,将类型引用和引用本身视为同一个特征标。
double cube(double x);
double cube(const double x); // 是overload
匹配函数时,要区分const与非const变量。
请记住,是特征标,而不是函数类型使得可以对函数进行重载。
long gronk(int n, float m);
double gronk(int n, float m); // not overload,因为函数返回类型与函数重载无关
8.4.2 何时使用函数重载
仅当函数基本执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
C++如何跟踪每一个重载函数?它给这些函数指定了秘密身份。C++编译器将执行一些神奇操作--名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。
对原始名称进行的表面无意义的修饰将对参数数目和类型进行编码。添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异。
8.5 函数模板
函数模板时通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
由于模板允许以泛型的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。
template<typename AnyType>
void Swap(AnyType& a, AnyType& b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
关键字template和typename是必须的,除非可用使用关键字class代替typename。另外,必须使用尖括号。类型名可以任意选择,但一般使用简单的名称T。
模板并不创建任何函数,而只是告诉编译器如何定义函数。
如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意输入较长的单词,则声明类型参数时,应使用关键字typename而不使用class。
注意,函数模板不能缩短可执行程序。最终的代码不包含模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单,可靠。
8.5.1 重载的模板
需要对多个不同类型使用同一种算法的函数时,可使用模板。
并非所有的模板参数都必须使模板参数类型,其中也可以包含基本类型和用户定义类型。
8.5.2 模板的局限性
编写的模板函数很可能无法处理某些类型。一种方法是C++允许重载运算符,一边能够将其用于特定的结构或类。另一种是为特定类型提供具体化模板。
8.5.3 显示具体化
一个具体化函数定义--称为显示具体化(explicit specialization)。
第三代具体化:
对于给定函数名,可以有非模板函数,模板函数和显示具体化模板函数以及它们的重载版本;
显示具体化的原型和定义应以template<>打头,并通过名称来指出类型;
具体化优先于常规模板,而非模板函数优先于具体化和常规模板;
struct job
{
char name[40];
double salary;
int floorl;
};
// no template function prototype
void Swap(job& a, job& b);
// template prototype
template<typename T>
void Swap(T&, T&);
// explicit specialization template
template<>
void Swap<job>(job&, job&);
template<typename T>
void Swap(T& a, T& b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template<>
void Swap<job>(job& j1, job& j2)
{
double t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
在代码中的包含函数模板本身并不会生成函数定义,它只是用于生成函数定义的一个方案。
编译器使用模板为特定类型生成函数定义时,得到的是模板实例化(instantiation)。
模板并非函数定义,但使用具体类型的模板实例是函数定义。这种实例化方式被称为隐士实例化(implicit instantiation)。
现在C++还允许显示实例化(explicit instantiation)。这意味是直接命令编译器创建特定的实例,如Swap<int>。其语法是,声明所需的种类--用<>符号指示类型,并在声明前加上关键字template。
template void Swap<int>(int, int);
与显示实例化不同的是,显示具体化使用下面两个等价的声明之一:
template <> void Swap<int>(int&, int&);
template <> void Swap(int&, int&);
区别在于,这些声明的意思是”不要使用Swap()模板来生成函数定义“,而应使用专门为int类型显示地定义地函数定义。
显示具体化声明在关键字template后包含<>,而显示实例化没有。
隐式实例化,显式实例化和显式具体化统称为具体化(specialization)。它们的相同之处,它们表示的都是具体类型的函数定义,而不是通用描述。
引入显示实例化后,必须使用新的语法--在声明中使用前缀template和template<>,以区分显示实例化和显示具体化。
...
template <class T>
void Swap(T&, T&); // template prototype
tempalte<>
void Swap<job>(job&, job&); // explicit specialization for job
int main(void)
{
template void Swap<char>(char&, char&); // explicit instantiation for char;
short a, b;
...
Swap(a, b); // implicit template instantiation for short;
char g, h;
...
Swap(g, h); // use explicit template instantiation for short;
job n, m;
...
Swap(n, m); // use explicit specialization for job;
}
8.5.5 编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其有多个参数时。这个过程称为重载解析(overloading resolution)。步骤如下:
第1步:创建候选函数列表。其中包含与被调用函数的名称相同的函数与模板函数。
第2步:使用候选列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配情况。
第3步:确定是否有最佳的可行函数。
void may(int); // 1
float may(float,, float = 3); // 2
void may(char); // 3
char* may(const char*); // 4
char may(const char&); // 5
template<class T>
void may(const T&); // 6
template<class T>
void may(T*); // 7
may('B');
注意,只考虑特征标,不考虑返回类型。调用may('B')函数后,4和7选项被排除,因为整型不能被隐式地转换为指针类型。这样剩下的5个可行函数。编译器必须确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要的进行的类型。通常,从最佳到最差的顺序是:
1.完全匹配,但常规函数优于模板;
2.提升转换(如,char和short自动转换为int, float转换为double);
3.标准转换(如, int转换为char,long转换为double)
4.用户定义的转换,如类声明定义的转换;
通常有两个函数完全匹配是一种错误,但这规则也有两个例外。
1. 完全匹配和最佳匹配
进行完全匹配时,C++运行某些”无关紧要的转换“。Type(argument-lish)意味着用作实参的函数与用作形参的函数指针只要返回和参数列表相同,就是匹配的。
从实参 | 到形参 |
Type | Type& |
Type& | Type |
Type[] | Type* |
Type(argument-lish) | Type(*)(argument-lish) |
Type | const Type |
Type | volatile Type |
Type* | const Type |
Type* | volatile Type* |
如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,比如二义性(ambiguous)。
一个完全匹配优于另一个的另一种情况使,其中一个非模板函数,而另一个不是。在这种情况下,非模板函数优于模板函数(包括显式具体化)。如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。
术语(most specialized)并不一定意味着显式具体化,而是指编译器推断使用那种类型时执行的转化最少。
template<class Type>
void recycle(Type t); // 1
template<class Type>
void recycle(Type* t); // 2
struct blot {int a ; char b[10];};
blot ink = {25, "wwqeq"};
recycle(&ink); // 最后调用 2, 而不是1. 因为指针更具体化引用。
用于找出最具体的模板的规则称为模板函数的部分排序规则(partial ordering rules)。
简而言之,重载解析将寻找最匹配函数。
如果只存在一个这样的函数,则选择它;
如果存在多个这样的函数,但其中只有一个非模板函数,则选择该函数;
如果存在多个适合的函数,且它们都是模板函数,但其中有一个函数比其他更具体,则选择该函数。
如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的。
当然,如果不存在匹配的函数,也是错误的。
如果函数定义在调用函数之前提供的,它将充当函数原型。
<>指出,编译器应选择模板函数,而不是非模板函数。
8.5.6 模板函数的发展
问题如下:
是什么类型?
template<class T1, class T2>
void ft(T1 x, T2 y)
{
...
?type? xpy = x + y;
...
}
关键字decltype(C++ 11)
int x;
decltype(x) y; // make y is the same type as x
decltype(x + y) xpy = x + y;
decltype为确定类型,编译器必须遍历一个核对表,核对表步骤简化如下:
decltype(expression) var;
第一步,如果expression是一个没有括号括起的标识符,则var的类型与该标识符的类型相同,包括const等限定符。
第二步,如果expression是一个函数调用,则var的类型与函数的返回类型相同。(并不会实际调用函数,编译器通过查看函数原型来获悉返回类型,而无需实际调用函数)
第三步,如果expression是一个左值,则var为指向其类型的引用。expression不能是未用括号括起的标识符。expression何时进入第三步呢? expression是用括号括起的标识符。
第四步,如果前面的条件都不满足,则var的类型与expression类型相同。
另一种函数声明语法(C++11 后置返回类型)
template<class T1, class T2>
?type? ft(T1 x, T2 y)
{
...
return x + y;
...
}
此时还未参数x与y,它们不在作用域内(编译器看不到它们,也无法使用它们。
template<class T1, class T2>
auto ft(T1 x, T2 y) -> decltype(x + y)
{
...
return x + y;
...
}
这将返回类型移动了参数声明后面的类型,auto是一个占位符,表示后置返回类型提供的类型。
8.6 总结
通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以将使得C++编译器将该函数视为内联函数。
引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。如果一个类型是从另一个类派生出来的,则基类引用可以指向派生类对象。
C++原型让您能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表从右到左默认参数。因此,如果为某个参数了提供了默认值,则必须为该参数右边所有的参数提供默认值。
函数的特征标是其参数列表。只要其特征标不同。这被称函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。