C++ 提供许多有别于C语言的函数特性。新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及函数模板。在 C 语言基础上增加的 C++ 新特性,是进入加加(++)领域的重要一步。
内联函数是 C++ 为提高程序运行速度所做的一项改进。内联函数是把函数体直接编译到调用该函数的地方,这样可以节约处理函数调用机制的时间。
要使用这项特性,必须采取下述措施之一:
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。
内联与宏
inline 工具是 C++ 新增的特性。 C 语言使用预处理器语句 #define 来提供宏。例如,下面是一个计算平方的宏:
#define SQUARE(X) X*X
这并不是通过传递参数实现的,而是通过文本替换实现的——X是“参数”的符号标记。
a = SQUARE(5.0); is replaced by a = 5.0 * 5.0;
b = SQUARE(4.5 + 7.5); is replaced by b = 4.5 + 7.5 * 4.5 + 7.5
d = SQUARE(c++); is replaced by d = c++*c++;
上述示例只有第一个能正常工作。
通过使用括号来改进:
#define SQUARE(X) ((X) * (X))
但仍然存在这样的问题,即宏不能按值传递。即使使用新的定义,SQUARE(c++) 仍将 c 递增两次,但是使用内联函数 square(c++)计算c的结果,传递它以计算平方值,然后将c递增一次。
引用变量的主要用途是用作函数的形参。
注意,必须在声明引用变量时进行初始化。
一旦声明变量的引用,引用将和这个变量深度绑定在一起。
按引用传递和按值传递在调用时看起来相同,只能通过函数声明或定义才能指导是按引用传递还是按值传递。
当数据比较大(如结构和类)时,引用参数将很有用。
按值传递的函数,可使用多种类型的实参。例如,下面的调用都是合法的:
函数声明 double cube(doube x);
double z = cube(x+2.0); //将x+2.0这个表达式的值传递给x
z = cube(8.0); // 将一个常数的值传递给x
int k = 10;
z = cube(k); // 先将k从int型转成double型,在传递给x
double yo[3] = {2.2, 3.3, 4.4};
z = cube(yo[2]);// 将 4.4 传递给x
但是传递引用会更加严格,上面的传递方式都不被允许。
临时变量、引用参数 和 const
如果实参和引用参数不匹配,C++将生成临时变量。但是仅当参数为const引用时,C++才允许这样做
首先编译器将在下面两种情况下生成临时变量:
如果声明将引用指定为 const,C++将在必要时生成临时变量,实际上对于形参为 const 引用的 C++ 函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
应尽可能将引用参数声明为const
C++ 11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用 && 声明的:
double && rref = std::sqrt(6.0); // not allowed for double &
double j = 15.0;
double && jref = 2.0 * j + 18.5; // not allowed for double &
std::cout << rref << ‘\n’; // display 6.0
std::cout << jref << ‘\n’; // display 48.5
可以使用右值来实现移动语义,以前的引用(使用&声明的引用)现在成为左值引用。
返回引用,看下面的语句:
dup = accumulate(team, five);
如果 accumulate() 返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给 dup。但在返回引用时,将直接把指向的这个结构复制到dup,效率更高。
注意:返回引用的函数实际上是被引用的变量的别名。
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元的引用。比如应该避免编写下面这样的代码:
const free_throws & clone2(free_throws & ft){
free_throws newguy;
newguy = ft;
return newguy;
}
该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不再存在。
为避免这个问题,最简单的方法是,返回一个作为参数传递给函数的引用。另一种方法是用 new 来分配新的存储空间。
假如要使用引用返回值,同时又不允许执行给该返回值赋值的操作,只需将返回类型声明为const 引用。
const引用在重载运算符的时候就不适用了。
基类引用可以指向派生类对象,而无需进行强制类型转换。
例如,参数类型为 ostream &类型的函数可以接受 ostream 对象(如cout)或者程序员声明的 ofstream 对象作为参数。
使用引用参数的主要原因有两个:
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因,这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么,什么时候应使用引用、什么时候应使用指针呢? 什么时候应该按值传递呢?下面是一些指导原则:
对于使用传递的值而不作修改的函数:
对于修改调用函数中数据的函数:
当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。例如,对于基本类型,cin 使用引用,因此可以使用 cin>>n,而不是 cin>>&n。
默认参数指的是当函数调用中省略了实参时自动使用的一个值。
对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须要为它右边的所有参数提供默认值:
实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。因此,下面的调用是不允许的:
beeps = harpo(3, ,8);
在设计类时就可以发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
函数重载、函数多态两个术语指的是同一回事。
可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。
函数重载的关键是函数的参数列表——也称为函数特征标( function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。
编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
是特征标而不是函数类型使得可以对函数进行重载。
const和非const参数,可以作为函数重载的标志。例如
void dribble(char * bits);
void dribble(const char * cbits);
void dabble(char * bits);
void drivel(const char * bits);
下面列出了各种函数调用对应的原型:
const char p1[20] = "How's the weather?";
char p2[20] = "How's business?";
dribble(p1); // dribble(const char *)
dribble(p2); // dribble(char *);
dabble(p1); // No match!
dabble(p2); // dabble(char *);
drivel(p1); // drivel(const char *);
drivel(p2); // drivel(const char *);
类设计和STL经常使用引用参数,因此知道不同引用类型的重载很有用。请看下面三个原型:
void sink(double & r1); // matches modifiable lvalue
void sank(const double & r2); // matches modifiable or const lvalue, rvalue
void sunk(double && r3); // matches rvalue
左值引用参数 r1 与可修改的左值参数(如 double 变量)匹配;
const 左值引用参数r2与可修改的左值参数、const左值参数和右值参数匹配;
右值引用参数 r3 与右值匹配。
注意到与r1或者r3匹配的参数都与r2匹配。这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本:
void stove(double & r1);
void stove(const double & r2);
void stove(const && r3);
double x = 55.5;
const double y = 32.0;
stove(x); // calls stove(double &)
stove(y); // calls stove(const double &)
stove(x+y); // calls stove(const &&)
但是如果没有定义stove(const &&),stove(x+y)将调用函数stove(const double &)
虽然函数重载很吸引人,但也不要滥用。仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
注意又是可以使用默认参数来实现同样的目的。
C++ 如何跟踪每一个重载函数呢?它给这些函数指定了秘密身份。使用 C++ 开发工具中的编辑器编写和编译程序时,C++编译器将执行一些神奇的操作——名称修饰(name decoration)或名称矫正(name mangling),它根据函数声明中指定的形参类型对每个函数名进行加密。如下述未经修饰的函数声明:
long MyFunctionFoo(int, float);
这种格式对于人类来说很适合;我们知道函数接受两个参数(一个为 int 类型,另一个为 float 类型),并返回一个 long 值。而编译器将名称转换为不太好看的内部表示,来描述该接口,如下所示:
?MyFunctionFoo@@YAXH
对原始名称进行的表面看来无意义的修饰(或矫正,因人而异)将对参数数目和类型进行编码。添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异。
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如 int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
template<typename AnyType>
void Swap(AnyType & a, AnyType & b){
AnyType temp;
temp a;
a = b;
b = temp;
}
第一行指出,要建立一个模板,并将类型命名为 AnyType。关键字 template 和 typename 是必需的,除非可以使用关键字 class 替换 typename。另外,必须使用尖括号。
函数模板不能缩短可执行程序。生成的可执行程序仍将包含独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。
更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。
需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。
并非所有的模板参数都必须是模板参数类型。
编写的模板函数很可能无法处理某些类型,假设有如下模板函数:
template<class T>
void f(T a, T b){
...
}
如果函数体内有
a = b;
但如果T为数组,则就不能处理了。
一种方案是运算符重载,另一种方案是,为特定类型提供具体化的模块定义
假设定义了如下结构:
struct job{
char name[40];
double salary;
int floor;
}
另外,假设希望能够交换两个这种结构的内容。原来的模板使用下面的代码来完成交换:
temp = a;
a = b;
b = temp;
由于,C++允许将一个结构赋给另一个结构,因此即使T是一个job结构,上述代码也适用。然而,假设只想交换 salary 和 floor 成员,而不交换 name 成员,则需要使用不同的代码,但 Swap() 的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其它的代码。
然而,可以提供一个具体化函数定义,称为显式具体化(explicit specialization),其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
// non template function prototype
void Swap(job &, job &);
// template prototype
template <typename T>
void Swap(T &, T &);
// explicit specialization for the job type
template <> void Swap<job>(job &, job &);
具体化优先于函数模板,而非模板函数优先于具体化和常规模板
显示具体化的声明和定义应该以template<>打头,并通过名称来指出类型
其中
是可选的,因为函数的参数类型已经表明这是一个job的具体化
显式实例化用来直接命令编译器创建特定的实例。
语法如下:
template void Swap
该声明的意思是:“使用Swap()模板生成int类型的函数定义。”
与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:
template <> void Swap<int>(int &, int &);
template <> void Swap(int &, int &);
区别在于,这两个声明的意思是“不要使用Swap()模板生成函数定义,而应使用专门为int类型显式地定义的函数定义”。
注意:试图在同一文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
还可以通过在程序中使用函数来创建显式实例化。例如,下面的代码:
template <class T>
T Add(T a, T b){
return a + b;
}
...
int m = 6;
double x = 10.2;
cout << Add<double>(x,m) << endl; // explicit instantiation
这里的模板与函数调用 Add(x,m) 不匹配,因为该模板要求两个函数参数的类型相同。但通过使用Add
,可强制为 double 类型实例化,并将参数 m 强制转换为 double 类型,以便与函数 Add
的第二个参数匹配。
如果对Swap()做类似的处理,效果怎么样呢?
int m = 5;
double x = 14.3;
Swap<double>(m,x);
这将为类型double生成一个显式实例化。但是这些代码不管用,因为第一个形参的类型为double &,它不能指向 int 变量 m。
隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
引入显式实例化后,必须使用新的语法——在声明中使用前缀template和template <>,以区分显式实例化和显式具体化。下面的代码片段总结了这些概念:
template<class T>
void Swap (T &, T &);
template<> void Swap<job>(job &, job &); // explicit specialization for job
int main(){
template void Swap<char>(char &, char &); // explicit instantiation for char
short a, b;
...
Swap(a,b); // implicit template instantiation for short
job n, m;
...
Swap(n,m); // use explicit specialization for job
char g, h;
...
Swap(g, h); // use explicit template instantiation for char
...
}
对于函数重载、函数模板、函数模板重载,C++需要一个良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这个过程成为重载解析(overloading resolution)。
考虑只有一个函数参数的情况,如下面的调用:
may('B'); // actual argument is type char
首先,编译器将寻找候选者,即名称为may()的函数和函数模板。
然后寻找那些可以用一个参数调用的函数。
例如,下面的函数符合要求,因为其名称与被调用的函数相同,且可只给它们传递一个参数:
void may(int);
float may(float, float = 3);
void may(char);
char * may(const char *);
char may(const char &);
template void may(const T &);
template void may(T *);
注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数(#4 和 #7)不可行,因为整数类型不能隐式转换(即没有显式强制类型转换)为指针类型。剩余的一个模板可用来生成具体化,其中 T 被替换为 char 类型。这样剩下 5 个可行的函数,其中的每一个函数,如果它是声明的唯一函数,都可以被使用。
接下来,编译器必须确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述。
例如,函数#1优于函数#2,因为char到int的转换是提升转换,而char到float的转换是标准转换。函数#3、#5和函数#6都优于函数#1和#2,因为它们都是完全匹配的。#3和#5优于#6,因为#6是模板。这种分析引出了两个问题。什么是完全匹配?如果两个函数(如#3和#5)都完全匹配,将如何办呢?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。显然,我们需要对这一点做更深入的探讨。
进行完全匹配时,C++允许某些“无关紧要的转换”。下表列出了这些转换——Type表示任意类型。例如,int 实参与 int & 形参完全匹配。注意,Type 可以是 char &这样的类型,因此这些规则包括从 char & 到 const char & 的转换。Type(argument-list) 意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配的。还有关键字 volatile。
| 从实参 | 到形参|
|--|--|
| Type | Type & |
| Type & | Type |
| Type [] | * Type |
| Type(argument-list) | Type(*) (argument-list) |
|Type | const Type |
| Type | volatile Type |
| Type * | const Type * |
| Type * | volatile Type * |
假设有下面的函数代码:
```cpp
struct blot {int a; char b[10];};
blot int = {25, "spots"};
...
recycle(ink);
```
在这种情况下,下面的原型都是完全匹配的:
void recycle(blot); //#1
void recycle(const blot); //#2
void recycle(blot &); //#3
void recycle(const blot &); //#4
如果有多个完全匹配的类型,编译器将无法完成重载解析的过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如"ambigous"(二义性)这样的词语。
然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非 const 数据的指针和引用优先与非 const 指针和引用参数匹配。也就是说,在 recycle() 实例中,如果只定义了函数#3和#4是完全匹配的,则将选择#3,因为ink没有被声明为const。然而,const 和 非const 之间的区别只适用于指针和引用指向的数据。也就是说,如果只定义了 #1 和 #2,则将出现二义性错误。
一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数将优先于模板函数(包括显式具体化)。
如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。例如,这意味着显式具体化将优先于使用模板隐式生成的具体化:
struct blot { int a; char b[10]; };
template <class Type> void recycle (Type t); // template
template<> void recycle<blot> (blot & t); // specialization for blot
...
blot ink = {25, "spots"};
...
recycle(ink); // use specialization
术语“最具体(most specialized)”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如,请看下面的模板:
template <class Type> void recycle (Type t); // #1
template <class Type> void recycle (Type *t); // #2
```
假设包含这些模板的程序也包含如下代码:
```cpp
struct blot {int a; char b[10];};
blot ink = {25, "spots"};
...
recycle(&ink); // address of a structure
recycle(&ink)调用与#1模板匹配,匹配时将 Type 解释为 blot *。
recycle(&ink)调用也与#2模板匹配,匹配时将Type解释为blot。
因此将两个隐式实例——recycle
和 recycle
发送到可行函数池中。
在这两个模板函数中,recycle
被认为是更具体的,因为在生成过程中它需要进行的转换更少。也就是说,#2模板已经显式指出,函数参数是指向 Type 的指针,因此可以直接用 blot 标识 Type;而#1模板将 Type 作为函数参数,因此 Type 必须被解释为指向 blot 的指针。也就是说,在 #2 模板中,Type 已经被具体化为指针,因此说它更“具体”。
用于找出最具体的模板的规则被称为函数模板的部分排序规则(partial ordering rules)。和显式实例一样,这也是 C++98 新增的特性。
有些情况下,可通过编写合适的函数调用,引导编译器做出希望的选择。
template<class T>
T lesser(T a, T b){ // #1
return a < b ? a : b;
}
int lesser(int a, int b){ // #2
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
}
int main(){
using namespace td;
int m = 20;
int n = -30;
double x = 15.5;
double y = 25.9;
cout << lesser(m,n) << endl; // use #2;
cout << lesser(x,y) << endl; // use #1 with double
cout << lesser<>(m,n) << endl; // use #1 with int
cout << lesser<int>(x,y) << endl;// use #1 with int
}
lesser<>
指出,编译器应使用模板函数,而不是非模板函数;编译器注意到的实参的类型为int,因此使用int替代T对模板进行实例化。
lesser
指出,编译器应使用用int进行实例化的模板函数,因此double实参会被强制转换为int
在 C++ 发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用,他们甚至没有就这个主题发挥想象力。但聪明而专注的程序员挑战模板技术的极限,阐述了各种可能性。根据熟悉模板的程序员提供的反馈,C++98标准做了相应的修改,并添加了标准模板库。从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C++ 11 标准根据这些程序员的反馈做了相应的修改。下面介绍一些相关的问题及其解决方案。
在 C++98 中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。请看下面这个不完整的示例:
template<class T1, class T2>
void ft(T1 x, T2 y){
...
?type? xpy = x + y;
...
}
xpy应为什么类型呢?由于不知道 ft() 将如何使用,因此无法预先知道这一点。正确的类型可能是 T1、T2或者其它类型。例如,T1 可能是 double,而T2可能是int,在这种情况下,两个变量的和将为 double 类型。T1可能是short,而T2可能是int,在这种情况下,两个变量的和为int类型。T1还可能是short,而T2可能是char,在这种情况下,加法运算将导致自动整型提升,因此结果类型为int。另外结构和类可能重载运算符+,这导致问题更加复杂。因此,在C++98中,没有办法声明xpy的类型。
C++ 11 新增的关键字 decltype 提供了解决方案。可这样使用该关键字:
int x;
decltype(x) y; // make y the same type as x
给decltype提供的参数可以是表达式,因此在前面的模板函数 ft() 中,可使用下面的代码:
decltype(x+y) xpy; // make xpy the same type as x + y
xpy = x + y;
另一种方法是,将这两条语句合而为一:
decltype(x+y) xpy = x + y;
因此,可以这样修复前面的模板函数 ft():
template<class T1, class T2>
void ft(T1 x, T2 y){
...
decltype(x + y) xpy = x + y;
...
}
decltype 比这些示例演示得要复杂些。为确定类型,编译器必须遍历一个核对表。假设有如下声明:
decltype(expression) var;
则核对表的简化版如下:
第一步:如果expression是一个没有用括号括起的标识符,则var的类型与该标识符的类型相同,包括 const 等限定符:
double x = 5.5;
double y = 7.9;
double &rx = x;
const double * pd;
decltype(x) w; // w is type double
decltype(rx) u = y; // u is type double &
decltype(pd) v; // v is type const double *
第二步:如果expression是一个函数调用,则var的类型与函数的返回类型相同:
long indeed(int);
decltype (indeed(3)) m; // m is type int
注意:并不会实际调用函数。编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。
第三步:如果 expression 是一个左值,则 var 为指向其类型的引用。这好像意味着前面的 w 应为引用类型,因为x是一个左值。但这种情况在第一步已经处理过了。要进入第三步,expression 不能是未用括号括起的标识符。那么,expression 是什么时候将进入第三步呢?一种显而易见的情况是,expression 是用括号括起的标识符:
double xx = 4.4;
decltype ((xx)) r2 = xx; // r2 is double &
decltype(xx) w = xx; // w is double (Stage 1 match)
顺便说一句,括号并不会改变表达式的值和左值性。例如,下面两条语句等效:
xx = 98.6;
(xx) = 98.6; // () don't affect use of x
第四步:如果前面的条件都不满足,则 var 的类型与 expression 的类型相同:
int j = 3;
int &k = j
int &n = j;
decltype(j+6) i1; // i1 type int
decltype(100L) i2; // i2 type long
decltype(k+n) i3; // i3 type int
请注意,虽然 k 和 n 都是引用,但表达式 k + n 不是引用;它是两个 int 的和,因此类型为 int。
如果需要多次声明,可结合使用 typedef 和 decltype:
template<class T1, class T2>
void ft(T1 x, T2 y){
...
typedef decltype(x + y) xytype;
xytype xpy = x + y;
xytype arr[10];
xytype & rxy = arr[2]; // rxy a reference
}
有一个相关的问题是 decltype 本身无法解决的。请看下面这个不完整的模板函数:
template<class T1, class T2>
?type? gt(T1 x, T2 y){
...
return x + y;
}
同样,无法预先知道将 x 和 y 相加得到的类型。好像可以将返回类型设置为 decltype(x+y),但不幸的是,此时还未声明参数x和y,它们不在作用域内(编译器看不到它们,也无法使用它们)。必须在声明参数后使用 decltype。为此,C++新增了一种声明和定义函数的语法。下面使用内置类型来说明这种语法的工作原理。对于下面的原型:
double h(int x, float y);
使用新增的语法可编写成这样:
auto h(int x, float y) -> double;
这将返回类型已到了参数声明后面。 ->double 被称为后置返回类型(trailing return type)。其中 auto 是一个占位符,标识后置返回类型提供的类型,这是 C++ 11 给auto新增的一种角色。这种语法也可用于函数定义:
auto h(int x, float y) -> double{
/* function body */
}
通过结合使用这种语法和 decltype,便可给 gt() 指定返回类型,如下所示:
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y){
...
return x + y;
}
现在,decltype 在参数声明后面,因此 x 和 y 位于作用域内,可以使用它们。