编译过程的最终产品是可执行程序 —— 由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机在内中,因此每条指令都有特定的内存地址。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
C++ 内联函数提供了另一个选择。内联函数的编译代码与其他程序代码“内联”了起来。编译器将使用相应的函数代码替换函数调用,对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多的内存。调用几次内联函数,就包含该函数代码的几个副本。
应有选择的使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间乱短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
要使用这项特性,必须采取下述措施之一:
- 在函数声明前加上关键字inline;
- 在函数定义前加上关键字inline;
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
程序员请求将函数作为内联函数时,编译器并不一定满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。
内联函数与常规函数一样,也是按值传递参数的,所以参数可以是表达式,这使得 C++ 的内联功能远远胜过 C 语言的宏定义。
C++ 新增了一种复合类型 —— 引用变量。引用是已定义的变量的别名(另一个名称)。引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
创建引用变量C++ 给&符号赋予了另一个含义,将其用来声明引用。
int rats; int & rodents = rats; // makes rodents an alias for rats
其中,&不是地址运算符,而是类型标识符的一部分。就像声明中的char *指的是指向char的指针一样,int &指的是指向int的引用。上述引用声明允许将rats和rodents互换 —— 它们指向相同的值和内存单元。
引用看上去像伪装表示的指针(其中,*解除引用运算符被隐式理解)。实际上,引用还是不同于指针的。除了表示法不同外,还有其他的有差别。例如,差别之一是,必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值。
注意:必须在声明引用变量时进行初始化。
引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。
可以通过初始化来设置引用,但不能通过赋值来设置。
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。C++ 新增的这项特性是对 C 语言的超越,C 语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝。C 语言也允许避开按值传递的限制,采用按指针传递的方式。
按引用传递的按值传递看起来相同,只能通过原型或函数定义才能知道是按引用传递。地址运算符&使用按地址传递一目了然。按引用传递的函数与按值传递的函数,唯一的外在区别是声明函数参数的方式不同。
传递引用的函数和传递指针的函数,第一个区别是声明函数参数的方式不同,另一个区别是需要在函数使用指针过程中使用解除引用运算符*。
如果实参与引用参数不匹配,C++ 将生成临时变量。当前,仅当参数为const时,C++ 才允许这样做。
如果引用参数是const,则编译器将在下面两种情况下生成临时变量:
- 实参的类型不正确,但不是左值;
- 实参的类型不正确,但可以转换为正确的类型。
左值参数是可被引用的数据对象,例如,变量、数组原素、结构成员、引用和解除引用的指针都是左值。非左值包括字符常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。
在C 语言中,左值最初指的是可出现在赋值语句左边的实体,但引入关键字const后,常规变量和const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
double refcube(const double & ra){return ra * ra * ra;}
double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = {2.0, 5.0, 10., 12.0};
double c1 = refcube(side); // ra is side
double c2 = refcube(lens[2]); // ra is lens[2]
double c3 = refcube(rd); // ra is rd is side
double c4 = refcube(*pd); // ra is *pd is side
double c5 = refcube(edge); // ra is temporary variable
double c6 = refcube(7.0); // ra is temporary variable
double c7 = refcube(side + 10.0); // ra is temporary variable
参数side、lens[2]、rd和*pd都是有名称的、double类型的数据对象,因此可以为其创建引用。edge虽然是变量,类型却不正确,7.0和side + 10.0的类型正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它,函数调用结束后将被删除。
实际上,对于形参为const引用的 C++ 函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则 C++ 将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
将引用参数声明为常量数据的引用的理由有三个:
- 使用const可以改免无意中修改数据的编程错误;
- 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
- 使用const引用使函数能够正确生成并使用临时变量。
因此,应尽可能将引用形参声明为const。
C++11 新增了另一种引用 —— 右值引用。这种引用可指向右值,是使用&&声明的:
double && rref = std::sqrt(36.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
新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。以前的引用(使用&声明的引用)现在称为左值引用。
将引用用于结构引用非常适合用于结构和类。引入引用主要是为了用于这些类型,而不是基本的内置类型。
使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时使用引用运算符&即可。
如果不希望函数修改传入的结构,可使用const。
传统返回机制与按值传递函数参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用函数将使用这个值。
如果函数返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,调用函数再使用这个值。这样不如返回值为引用时的效率高。
注意:返回引用的函数实际上是被引用函数的别名。
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。
为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。
另一种方法是用new来分配新的存储空间。
const free_throws & clone(free_throws & ft)
{
free_throws * pt;
*pt = ft; // copy info
return *pt; // return reference to copy
}
第一条语句创建一个无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构。上述代码似乎返回该结构,但函数声明个表明,该函数实际上将返回这个结构的引用。这样,便可以这样使用该函数:
free_throws & jolly = clone(three);
这使得jolly成为新结构的引用。这种方法存在一个问题:在不再需要new分配的内存时,应使用delete来释放它们。调用clone()隐藏了对new的调用,这使得以后很容易忘记用deldte来释放内存。
将 const 用于引用返回类型在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。如果函数返回引用,它确实标识的是一个这样的内存块,所以这样的函数是左值。如果返回类型是const引用,就是不可修改的左值。
常规(非引用)返回类型是右值 —— 不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。其他右值包括字面值(如 10.0)和表达式(x + y)。显然 ,获取字面值(如 10.0)的地址没有意义,但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。
将类对象传递给函数时,C++ 通常的做法是使用引用。
可以将C- 风格字符串用作string对象引用参数。首先,string类定义了一种将char*到string的转换功能,这使得可以使用C- 风格字符中来初始化string对象。其次是本章前面讨论过的类型为const引用的形参的一个属性。假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参为业初始化它,然后传递一个指向该临时变量的引用。这种属性的结果是,如果形参类型为const string &,在调用函数时,使用的实参可以是string对象或 C- 风格字符串,如用引号括起的字符串字面量、以空字符结尾的char数组可指向char的指针变量。
使得能够将特性从一个类传递给另一个类的语言特性被称为继承。派生类继承了基类的方法。继承的另一个特征是,基类引用可以以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
ostream 类中的格式化方法方法setf()能够设置各种格式化状态。例如,方法调用setf(ios base:fixed)将对象置于使用定点表示法的模式;setf(ios_base:showpoint)将对象置于显示小数点的模式,即使小数部分为零。方法precision()指定显示多少位小数(假定对象处于定点模式下)。所有这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们。方法wdth()设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零,这意味着刚好能容纳下要显示的内容。
两个有趣的方法调用:
ios_base::fmtflags initial; initial = os.setf(ios base:fixed); // save initial formatting state ... os.setf(initial); // restore initial formatting state
方法sctf()返回调用它之前有效的所有格式化设置。ios base::fmtflags是存储这种信息所需的数据类型名称。因此,将返回值赋给initial将存储调用之前的格式化设置,然后便可以使用变量initial作为参数来调用setf(),将所有的格式化设置恢复到原来的值。
何时使用引用参数使用引用参数的主要原因有两个。
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。对于使用什么参数的一些指导原则:
对于使用传递的值而不作修改的函数:
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
- 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需要的时间和空间。
- 如果数据对象类对象,则使用const引用。类设计的语义常常要求使用引用,这是 C++ 新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
当然,这只是一些指导原则,很可能有充分的理由做出其他的类型。例如,对于基本类型,cin使用引用,因此可以使用cin >> n,而不是cin >> &n。
默认参数默认参数指的是当函数调用中省略了实参时自动使用的一个值。设置默认值必须通过函数原型。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告诉程序。方法是将值赋给原型中的参数。
对于带参数列表的函数,必须从右向左添加默认值。实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。
int harpo(int n, int m = 4, int j = 5); // valid int chico(int n, int m = t, int j) // invalid int groucho(int k = 1, int n = 2, int m = 3) // valid beeps = harpo(2); // same as harpo(2, 4, 5) beeps = harpo(1, 8) // same as harpo(1, 8, 5) beeps = harpo(8, 7, 6) // no default arguments used beeps = harpo(3, , 8) // invalid, doesn't set m to 4
默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在设计类时,通过使用默认参数,可以减少要定义的析构函数、方法及方法重载的数量。
函数重载函数多态是 C++ 在 C 语言的基础上新增的功能。默认参数能够使用不同数目的参数调用一个函数,而函数多态(函数重载)让您能够使用多个同名的函数。术语“多态”指的是有多种形式,因此函数多态允许函数可以有多种形式。术语“函数重载”指的是可以有多个同名的函数,因此对名称进行了重载。这两个术语指的是同一回事,但通常使用函数重载。可以通过函数重载来设计一系列函数 —— 它们完成相同的工作,但使用不同的参数列表。
重载函数就像是有多种含义的动词。C++ 使用上下文来确定要使用的重载函数版本。
函数重载的关键是函数的参数列表 —— 也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++ 允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则特征标也不同。例如可以定义一组原型如下的print()函数:
void print(const char * str, int width); // #1
void print(double d, int width); // #2
void print(long l, int width); // #3
void print(int i, int width); // #4
void print(const char * str); // #5
print("Pancakes", 15); // use #1
print("Syrup"); // use #5
print(1999.0, 10); // use #2
print(1999, 12); // use #4
print(1999L, 15); // use #3
使用print()函数时,编译器将根据所采取的用法使用有相应特征标的原型。
使用被重载的函数时,需要在函数调用中使用正确的参数类型。例如下面的语句:
unsigned int year = 3210; print(year, 6); // ambiguous call
print()调用与哪个原型匹配呢?它不与任何原型匹配!没有匹配的原型并不会自动停止使用其中的某个函数,因为 C++ 将尝试使用标准类型转换强制进行匹配。如果 #2 原型是print()唯一的原型,则函数调用print(year, 6)将把year转换为double类型。但在上面的代码中,有 3 个将数字作为第一个参数的原型,因此有 3 种转换year的方式。在这种情况下,C++ 将拒绝这种函数调用,并将其视为错误。
一些看起来彼此不同的特征标是不能共存的。例如:
double cube(double x); double cube(double & x); cout << cube(x);
参数x与double x原型和double & x原型都匹配,因此编译器无法确定究竟应使用哪个原型。为避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
匹配函数时,并不区分const和非const变量。将非const值赋给const变量是合法的,但反之则是非法的。
请记住,是特征标,而不是函数类型使得可以对函数进行重载。例如,下面的两个声明是互斥的:
long gronk(int n, float m); // same signatures double gronk(int n, float m); // hence not allowed
因此,C++ 不允许以这种方式重载函数。返回类型可以不同,但特征标也必须不同:
long gronk(int n, float m); // different signatures double gronk(float n, float m); // hence allowed重载引用参数
左值引用参数与可修改的左值参数匹配;const左值引用参数与可修改的左值参数、const左值参数和右值参数匹配;右值引用参数与右值匹配。如果重载使用这三种参数的函数,结果是将调用最匹配的版本。
何时使用函数重载仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
什么是名称修饰C++ 如何跟踪每一个重载函数呢?它给这些函数指定了秘密身份。使用 C++ 开发工具中的编辑器编写
和编译程序时,C++ 编译器将执行一些神奇的操作 —— 名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。请看下述未经修饰的函数原型:
long MyFunctionFoo (int, float);
这种格式对于人类来说很适合,我们知道函数接受两个参数(一个为int类型,另二个为floatt类型),并返回一个long值。而编译器将名称转换为不太好看的内部表示,来描述该接口,如下所示:
?MyFunctionFoo@@YAXH
对原始名称进行的表面看来无意义的修饰(或矫正,因人而异)将对参数数目和类型进行编码。添加
的一组符号随函数特征标而异,而修饰时便用的约定随编译器而异。
函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型。
函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板:
template// or template void Swap(AnyType &a, AnyType &b) { AntType = temp; a = b; b = temp; }
第一行指出,要建立一个模板,并将类型命名为AnyType。关键字template和typename是必需的,除非可以使用关键字class代替typename。另外,必须使用尖括号。类型名可以任意选择(这里是AnyType),只要遵守 C++ 命名规则即可;许多程序都使用简单的名称,如T。余下的代码描述了交换两个AnyType值的算法。模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int代替AnyType。
在标准 C++98 添加关键字typename之前,C++ 使用关键字class来创建模板。
提示:如果需要多个将同一算法用于不同类型的函数,请使用函数模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename而不使用class。
要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用Swap()函数即可。编译器将检查所使用的参数类型,并生成相应的函数。
注意:函数模板不能缩短可执行的程序。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使用生成多个函数定义更简单、更可靠。
需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规函数一样,被重载的模板的函数特征标必须不同。注意:并非所有的模板参数都必须是模板参数类型,也可以是具体类型。
模板的局限性编写的模板可能无法处理具体的类型。另一方面,有时候通用化是有意义的,但 C++ 语法不允许这样做。例如,将两个包含位置坐标的结构相加是有意义的,虽然没有为结构定义运算符+。一种解决方案是,C++ 允许重载运算符+,以便能够将其用于特定的结构或类。这样使用运算符+的模板便可以处理重载了运算符+的结构。另一种解决方案是,为特定类型提供具体化的模板定义。
显式具体化假设定义了如下结构:
struct job
{
char name[40];
double salary;
int floor;
}
另外,假设希望能够交换两个这样结构的内容。因为 C++ 允许将一个结构赋给别一个结构,因此即使T是一个job结构,也是可以实现的。然而,假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,但Swap()参数将保持不变,因此无法使用模板重载来提供其他的代码。
然而,可以提供一个具体化函数定义 —— 称为显示具体化,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
试验其他具体方法后,C++98 标准选择了下面的方法:
- 对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及它们的重载版本。
- 显示具体化的原型和定义应以template <>打头,并通过名称来指出类型。
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
下面是用于交换job结构的非模板函数、模板函数和具体化的原型:
// 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 &);
Swap
在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。这种实例化方式被称为隐式实例化,因为编译器之所以知道需要进行定义,是由于程序调用时提供了参数。
最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现代 C++ 还允许显式实例化。这意味着可以直接命令编译器创建特定的实例。其语法是,声明所需的种类 —— 用<>符号指示类型,并在声明前加上关键字template:
template void Swap(int, int);
实现了这种特性的编译器看到上述声明后,将使用Swap()模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap()模板生成int类型的函数定义”。
与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:
template <> void Swap(int &, int &); template <> void Swap(int &, int &);
区别在于,这些声明的意思是“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义”。这些原型必须有自己的定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。
警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
隐式实例化、显式实例化和显式具体化统称为具体化。它们的相同之外在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
引入显式实例化后,必须使用新的语法 —— 在声明中使用前缀template和template <>,以区分显式实例化和显式具体化。通常,功能越多,语法规则也越多。
对于函数重载、函数模板和函数模板重载,C++ 需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析。详细解释这个策略需要更大的篇幅,因此大致了解一下这个过程是如何进行的。
- 第 1 步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 第 2 步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
- 第 3 步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
通常,从最佳到最差的顺序如下所述:
1.完全匹配,但常规函数优先于模板函数。
2.提升转换(例如char自动转换成int,float自动转换为doulbe)。
3.标准转换(例如int转换为char,long转换为double)。
4.用户定义的转换,如类声明中定义的转换。
通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。
完全匹配和最佳匹配进行完全匹配时,C++ 允许某些“无关紧要的转换”。下表列出了这些转换 —— Type表示任意类型。例如,int实参与int &形参完全匹配。注意,Type可以是char &这样的类型,因此这些规则包括从char &到const char &的转换。Type(argument-list)意味着用作实参的函数名与用作形参的函数的指针只要返回类型和参数列表相同,就是匹配的。
表:完全匹配允许的无关紧要转换
| 从实参 | 到形参 |
|---|---|
| Type | Type & |
| Type & | Type |
| Type [] | * Type |
| Type(argument-list) | Type (*)(argument=list) |
| Type | const Type |
| Type | volatile Type |
| Type * | const Type |
| Type * | volatile Type * |
如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如“ambiguous(二义性)”这样的语句。
然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非const数据的指针和引用优先与非const指针和引用参数匹配。
一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数将优先于模板函数(包括显式具体化)。
如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。例如,这意味着显式具体化将优于使用模板隐式化的具体化。
术语“具体化”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。
用于找出最具体的模板的规则被称为函数模板的部分排序规则。和显式实例一样,这也是 C++98 新增的特性。
简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误。
自己选择在有些情况下,可通过编写合适的函数调用,引导编译器做出您希望的选择。下面的程序将模板函数定义放在文件开头,从而无需提供模板原型。与常规函数一样,通过在使用函数前提供模板函数定义,它让它也充当原型。
// choices.cpp -- choosing a template #includetemplate // or template 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; return a < b ? a : b; } int main() { using namespace std; 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 (x, y) << endl; // USE #1 with int return 0; } // 该程序输出如下: 20 15.5 -30 15
最后的函数调用将double转换为int,有些编译器会针对这一点发出警告。
多个参数的函数将有多个参数的函数调用与有多个参数的原型进行匹配时,情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行函数都合适的函数,则选择该函数。一个函数要比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。
这些规则只是为了让任何一组函数原型和模板都存在确定的结果。
在 C++ 发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用,它们甚至没有就这个主题发挥想象力。但聪明而专注的程序员挑战模板技术的极限,阐述了各种可能性。根据熟悉模板的程序员提供的反馈,C++98 标准做了相应的修改,并添加了标准模板库。从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C++11 标准根据这些程序员的反馈做了相应的修改。下面介绍一些相关的问题及其解决方案。
是什么类型在C++98 中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。请看下面这个不完整的示例:
templatevoid 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的类型。
关键字 decltype(C++11)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():
templatevoid 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的类型与函数的返回类型相同:
int 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 xx
第四步:如果前面的条件都不满足,则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另一种函数声明语法(C++11 后置返回类型)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?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++ 给auto新增的一种角色。这种语法也可用于函数定义:
auto h(int x, float y) -> double
{}
通过结合使用这种语法和decltype,便可给gt()指定返回类型,如下所示:
templateauto gt (T1 x, T2 y) -> decltype (x + y) { return x + y; }
现在,decltype在参数声明后面,因此x和y位于作用域内,可以使用它们。
第 8 章总结C++ 扩展了 C 语言的函数功能。通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得 C++ 编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式。
引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。
C++ 原型能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。
函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。



