我是靠谱客的博主 飞快麦片,最近开发中收集的这篇文章主要介绍C++学习之路(七),C++primer plus 第八章 函数探幽8.1 C++ 内联函数8.2 引用变量8.3 默认参数函数重载函数模板8.6 总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

								**第 8 章  函数探幽**
	本章内容包括:
				内联函数。
				引用变量。
				如何按引用传递函数参数。
				默认参数。
				函数重载。
				函数模板。
				函数模板具体化。

       通过第 7 章,了解到很多有关 C++ 函数的知识,但需要学习的知识还很多。C++ 还提供许多新的函数特性,使之有别于 C 语言。新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数。本章介绍的 C++ 语言基础上新增的特性,以前面各章都多,这是进入加加(++)领域重要一步。

8.1 C++ 内联函数

       内联函数是 C++ 为提高程序运行速度所做的一项改进。常规函数和内联函数之间和主要区别不在于编写方式,而在于 C++ 编译器
如何将它们组合到程序中。要了解内联函数与常规函数之间的区别,必须深入到程序内部。
  编译过程的最终产品是可执行程序------由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址,计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数也使程序跳到另一个地址(函数的地址),并在函数结束时返回。下面更详细地介绍这一过程的典型实现。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳加到地址被保存的指令处,(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
  C++ 内联函数提供了另一种选择。内联函数的编码代码与其他程序代码“内联”起来了,也就是说,编译器将使用相应的函数代码远的函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代码是需要占用更多内存。如果程序在 10 个不同的地方调用同一个内联函数,则该程序将包
含该函数代码的 10 个副本(参见图 8.1)
Alt
   应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
  要使用这项特性。必须采取下述措施之一:
  在函数声明前加上关键字 inline;
  在函数定义前加上关键字 inline.
  通常的做法是省略原型,将整个定义(即函数头和所有的函数代码)放在本应提供原型的地方。
  程序员请求将自己(内联函数不能递归),因此不将其作为内联函数dmj有些编译器没有启用或实现这种特性。
  程序清单 8.1 通过内联函数 square()(计算参数的平方)演示了内联技术。注意到整个函数定义都放在一行中,但并不一定非得这样做。然而,如果函数定义占用多行(假定没有使用冗长的标识符),则将其作为内联函数就不太合适。在这里插入图片描述

程序清单 8.1 inline.cpp

//inline.cpp -- using an inline function
#include <iostream>

// an inline function definition
inline double square (double x) { return x * x; }

int main()
{
	using namespace std;
	double a, b;
	double c = 13.0;
	
	a = square(5.0);
	b = square(4.5 + 7.5);	// can pass expressions
	cout << "a = " << a << ", b = " << b << "n";
	cout << "c = " << c;
	cout << ", c squared = " << square(c++) << "n";
	cout << "Now c = " << c << "n";

	return 0;
}

程序运行结果如下:
在这里插入图片描述
  输出表明,内联函数和常规函数一样,也是按值来传递参数的。如果参数为表达式,如 4.5 + 7.5,则函数将传递表达式的值(这里为 12)。这使得 C++ 的内联功能远远胜过 C 语言的宏定义(#define 宏定义),请参见旁注“内联与宏”。
  尽管程序没有提供独立的原型,但 C++ 原型特性仍在起作用。这是因为在函数首次使用前出现的整个函数定义充当了原型。这意味着可以给 square()传递 int 或 long 值,将值传递给函数前。程序彼动将这个值强制转换为 double类型。
把代码的 inline去掉再运行:在这里插入图片描述
  说实话,写在这里并没有真正的理解到 inline的作用。用不用 inline效果是一样的????????,先多打几个问号

    							内联与宏 

       inline 工具是 C++新增的特性。 C语言使用预处理器语句 #define 来提供宏-------内联代码的原始实现。例如,下面是一个计算平方的宏:
  #define SQUARE(X) XX
  这并不是通过传递参数实现的,而是通过文本替换来实现的------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递增两次,但是程序清单 8.1 中的内联函数SQUARE()计算c的结果,传递它,以计算平方值,然后将C递增一次。这里的目的不是演示如何编码 C 宏,而是要指出。如果使用 C语言的宏执行了类似函数的功能,应考虑将它们转换为 C++ 内联函数。
在这里插入图片描述
在这里插入图片描述
  后来晚上我睡前想了一下,本来不加 inline 它只能在调用的时候使用,而加上 inline 关键字后,此函数变成了内联函数,应该是酱紫吧,运行结果肯定一样的,只是运行此函数的方式发生了改变。

8.2 引用变量

       C++ 新增了一种复合类型 ──引用变量。引用是已定义的变量的别名(另一个名称)。例如,如果将 twain 作为 clement 变量的引用,则可以交替使用 twian 和 clement 来表示该变量。那么,这种别名有何作用呢?是否能帮助那些不知道如何选择变量名的人呢?有可能,但引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种灰常方便的途径,同时对于设计类来说,引用也是必不可少的。然而,介绍如何将引用用于函数之前,先介绍一下定义和使用引用的基本知识。请记住,下述讨论旨在说明引用是如何工作的,而不是其典型用法。

8.2.1 创建用引变量

       前面讲过, C 和 C++ 使用 & 符号来指示变量的地址。C++ 给 & 符号赋予了另一个含义,将其用来声明引用。例如,要将rodents 作为rats 变量的别名,可以这样做:
  int rats;
  int &todents = rats; // make rodents an alias for rats ( 使 rodents【 rodents 】 作为 rats【老鼠; 耗子 】 的一个别名 ) 其中,& 不是地址运算符, 而是类型标识符的一部分。就像声明中的 char* 指的是指向char 的指针一样,int & 指的是指向 int 的引用。上述引用声明允许将 rats 和 rodents 与换 ---- 它们指向相同的值和内存 单元,程序清单 8.2 表明了这一点。
  &(引用)是来传值的 == > 出现在变量声明语句中位于变量左边时,表示声明的引用,
例如:int &a; //声明一个 int 型的引用 a。
&(取地址运算符) == > 在给变量赋初始值时出现在等号右边或执行语句中,表示取对象的地址。
说得更加简单易懂点:
①引用在等号_左边,而取地址在等号右边,例如:
int a = 3; int &b = a; // 引用
int *p = &a // 取地址
②和类型在一起的是引用,和变在一起的是取地址,例如,同样如上,还有下例: int function( int &i) { } //引用

程序清单 8.2 firstref.cpp

// firstref.cpp -- defining and using a reference
#include <iostream>
int main()
{
	using namespace std;
	int rats = 101;
	int & radents - rats;	// rodents is a reference
	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;
	rodents ++;
	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;

	// some implementations require type casting the following
	// addresses to type usingned
	cout << "rats address = " << &rats;
	cout << ", rodents address = " << &rodents << endl;

	return 0;
}

       请注意,下述语句中的 & 运算符不是地址运算符,而是将 rodents 的类型声明为 Int &,即指向 int 变量的引用:   
  int & rodents = rats;
  但下述语句中的 & 运算符是地址运算符,其中 &rodents 表示 rodents 引用的变量的地址:
  cout << “, rodents address = ” << &rodents << endl;
下面是程序运行结果:在这里插入图片描述
  从图中可知,rats 和 rodents 的值和地址都完全相同(具体的地址和显示格式随系统而异)。将 rodents 加 1 将影响这两个变量。更准确地说,rodents ++ 操作将一个有两个名称的变量加 1。(同样,虽然该 示例演示了引用是如何工作的,但并没有说明引用的典型用途,即作为函数参数,具体地说是结构和对象参数,稍后将介绍这些用法)。
  对于 C 语言用户而言,首次接触到引用可能 也会有些困惑,因为这些用户很自然地会想到指针,但它们之间还是有区别的。例如,可创建指向 rats 的引用和指针:
  int rats = 101;
  int & rodents = rats; // rodents is a reference
  int * prats = &rats; //prats is a pointer
  这样,表达式 rodents 和 *prats都可以同 rats 互换,而表达式 &rodents 和 prats 都可以同 &rats 互换。从这一点来说,引用看上去很像伪装表示的指针(其中,**解除引用运算符被隐式理解)。实际上,引用还是不同于指针的。除了表示法不同外,还有其他的差别。例如,差别之一是,必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值:
  int rat;
  int & rodent;
  rodent = rat; // No,you can’t do this.
  注意:必须在声明引用变量时进行初始化。
  引用更接近 const 指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。也就是说:
  int & rodents = rats;
  实际上是下述代码的伪装表示:
  int * const pr = &rats;
  其中,引用 rodents 扮演的角色与表达式 *pr相同。
  程序清单 8.3 演示了试图将 rats 变量的引用改为 bunnies 变量引用时,将发生的情况。

程序清单 8.3 sceref.cpp

// scref.cpp -- defining and using a reference
#include <iostream>

int main()
{
	using namespace std;
	int rats = 101;
	int & rodents = rats;		// rodents is a reference
	
	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;

	cout << "rats address = " << &rats;
	cout << ", rodents address = " << &rodents << endl;

	int bunnies = 50;
	rodents = bunnies;		// can we change the reference?
	cout << "bunnies = " << bunnies;
	cout << ", rats = " << rats;
	cout << ", rodents = " << rodents << endl;

	cout << "bunnies address = " << &bunnies;
	cout << ", rats address = " << &rats;
	cout << ", rodents address = " << &rodents << endl;


	return 0;
}

下面是程序运行结果:在这里插入图片描述
  最初,rodents 引用的是 rats,但随后程序试图将 rodents 作为 bunnies的引用:
  rodents = bunnies;
  咋一看,这种意图暂时是成功的,因为 rodents 的值从 101 变成了 50.但仔细研究将发现,rats 也变成了50,同时 rats 和 rodents的地址是相同的,而该地址与 bunnies 的地址不同。由于 rodents 是 rats 的别名,因此下述赋值语句与下面的语句等效:
  rats = bunnies;
  也就是说,这意味着 ”将 bunnies 变量的赋值给 rats 变量“。简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置。
  假设程序员试图这样操作:
  int rats = 101;
  int * pt = &rats;
  int & rodents = *pr;
  int bunnies = 50;
  pr = &bunnies;
  将 rodents 初始化为 *pt使用 rodents 指向 rats。接下来将 pt 改为指向 bunnies,并不能改变这样的事实,即 rodents 引用的是 rats。
在这里插入图片描述
在这里插入图片描述于黄冈办公室

8.2.2 将引用用作函数参数

        引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被
调用的函数能够访问函数中的变量。C++ 新增的这项特性是对 C语言的超越,C 语言只能按值传递。按值传递导致被调用函数使用调用
程序的值的拷贝(参见图 8.2)。当然,C 语言也允许避开按值传递的限制,采用按指针传递的方式。
在这里插入图片描述
        现在我们通过一个常见的计算机问题 ---- 交换两个变量的值,对使用引用和使用指针做一下比较。交换函数必须能够修改调用程序中的变量的值。这意味着按值传递变量将不管用,因为函数将交换原始变量副本的内容,而不是变量本身的内容。但传递引用时,函数将可以使用原始数据。另一种方法是,传递指针来访问原始数据。程序清单 8.4 演示了这三种方法。其中包括一种不可行的方法,以便能够对这些方法进行比较。

程序清单 8.4 swaps.cpp

// swaps.cpp -- swapping with references and with pointers
#include <iostream>
void swapr(int & a, int & b);	// a, b are aliases for ints
void swapp(int * p, int * q);	// p, q are addresses of ints
void swapv(int a, int b);		// a, b are new veriables

int main()
{
	using namespace std;
	int wallet1 = 300;
	int wallet2 = 350;
	cout << "wallet1 = $" << wallet1;
	cout << " wallet2 = $" << wallet2 << endl;

	cout << "Using references to swap contents: n";
	swapr(wallet1, wallet2);	// pass variables
	cout << "wallet1 = $" << wallet1;
	cout << " wallet2 = $" << wallet2 << endl;

	cout << "Using pointers to swap contents again:n";
	swapp(&wallet1, &wallet2);	// pass addresses of variables
	cout << "wallet1 = $" << wallet1;
	cout << " wallet2 = $" << wallet2 << endl;

	cout << "Trying to use passing by value:n";
	swapv(wallet1, wallet2); 	// pass values of variables
	cout << "wallet1 = $" << wallet1;
	cout << " wallet2 = $" << wallet2 << endl;

	return 0;
}

void swapr(int &a, int & b)		// use references
{
	int temp;

	temp = a;				// use a, b for values of variables
	a = b;
	b = temp;
}

void swapp(int * p, int * q)		// use pointers
{
	int temp;
	
	temp = *p;			// us *p, *q for values of variables
	*p = *q;
	*q = temp;
}

void swapv(int a, int b)		// try using value
{
	int temp;

	temp = a;				// use a, b for value of variables
	a = b;
	b = temp;
}

程序运行如下:
在这里插入图片描述
  引用和指针方法都成功的交换了两个钱夹(wallet)中的内容。而按值传递的方法没能完成这项任务。
  程序说明
  首先来看程序清单 8.4 中每个函数是如何被调用的:
swapr(wallet1, wallet2); // pass variables (传递变量)
swapp(&wallet1, &wallet2); // pass addresses of variables (传递变量的地址)
swapv(wallet1, wallet2); // pass values of variables (传递变量的值)
  按引用传递(swapr(wallet1, wallet2)) 和按值传递(swapv(wallet1, wallet2)) 看起来相同。只能通过原型或函数定义才能知道 swapr() 是按引用传递的。然则,地址运算符 & 使得按地址传递(swapp(&wallet1, &wallet2))一目了然(类型声明 int * p表明,p 是一个 int 指针,因此与 p 对应的参数应为地址,如 &wallet1)。
  接下来,比较函数swapr()(按引用传递) 和 swapv()(按值传递)的代码,唯一的外在区别是声明函数参数的方式不同:
void swapr(int & a , int & b)
void swapv(int a, int b)
当然还在内在区别,在 swapr()中,变量 a 和 b 是 wallet1 和 wallet2 的别名,所以交换 a 和 b 的值相当于交换 wallet1 和wallet2 的值;会是在 swapv()中,变量 a 和 b 是复制了 wallet1 和 wallet2 的值的新变量,因此交换 a 和 b 的值并不会影响 wallet1 和 wallet2 的值。
最后,比较函数 swapr()(传递引用)和 swapp()(传递指针)。第一个区别是声明函数参数的方式不同:
void swapr(int & a, int & b)
void swapp(int * p, int * b)
另一个区别是指针版本需要函数使用 p 和 q 的整个过程中使用解除引用运算符 *。
前面说过,应在定义引用 变量时对其进行初始化。函数调用使用实参初始化形参。因此函数的引用参数被初始化为函数调用传递的参数。也就是说,下面的函数调用将形参 a 和 b 分别初始化 wallet1 和wallet2:
swapr(wallet1, wallet2);在这里插入图片描述
在这里插入图片描述人是不能懒惰,一懒起来什么都不想写,继续…、

8.2.3 引用的属性和特别之处

        使用引用参数时,需要了解其一些特点,首先,看程序清单 8.5,它使用两个函数来计算参数的立方,其中一个函数接受 double 类型的参数,另一个接受 double 引用,我们有意将计算立方的代码编写得比较奇怪。
程序清单 8.5 cubes.cpp

// cubes.cpp -- regular and reference arguments
#include <iostream>
double cube (double a);
double refcube(double &ra);

int main()
{
	using namespace std;
	double x = 3.0;
	cout << cube( x );
	cout << " = cube of " << x << endl;
	cout << refcube( x );
	cout << " = cube of " << x << endl;
	
	return 0;
}

double cube(double a)
{
	a *= a * a;
	return a;
}

double refcube(double &ra)
{
	ra *= ra * ra;
	return ra;
}

程序运行结果为:在这里插入图片描述
        refcube() 函数修改了 main() 中的 x 值,而 cube()没有,这提醒我们为何通常按值传递。变量 a 位于 cube()中,它被初始化为 x 的值,但修改 a 并不会影响 x。但由于 refceub()使用了引用参数,因此修改 ra ,实际上也就是修改 x。如果程序员的意图是使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用,例如,在例子中,应该在函数原型和函数头中使用 const:
double refcube(const double &ra);
如果这样做,当编译器发现代码修改了 ra 的值时,将生成错误信息。
在这里插入图片描述
        顺便说一句,如何要编写类似于上述示例的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。当数据比较大(如结构和类)时,引用参数将很有用,稍后便会明白这一点。
  按值传递的函数,如何程序清单 8.5 中的函数 cube(),可使用多种类型的实参。
  题外语,实参和形参:
  实参:可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值:
  比如函数 int fun(int a,int c){some operation;}
  a和c都是形参。
  当我调用函数fun时,例如:
  int n,i=1,j=2;
  n=fun(i,j);
  调用fun(i,j)形式中的i,j就是实参。

  回来继续之。例如,下面的调用都是合法的:
   double z = cube(x + 2.0); // evaluate x + 2.0, pass value < 计算 x + 2.0,传递这个值 >
  z = cube(8.0); // pass the value 8.0 < 传递这个值 8.0>
  int k = 10;
  z = cube(k); // convert value of k to double, pass value < 将k值转换为double类型,传递这个值 >
  double yo[ 3 ] = { 2.2, 3.3, 4.4 };
  z = cube (yo[ 2 ]); // pass the value 4.4 < 传递这个值 4.4>
  如果将与上面类似的参数传递给接受引用参数的函数,将会发现,传递引用的限制更严格。毕竟,如果 ra 是一个变量的别名,则实参应是该变量。下面的代码不合理,因为表达式 x + 3.0 并不是变量:
  double z = refcube(x + 3.0 ); // should not compile < 不能编译 >
  例如,不能将值赋给该表达式:
  x + 3.0 = 5.0; // nonsensical < 荒谬的;无意义的 >
  如果试图使用像 refcube(x + 3.0)这样的函数启用,将发生什么情况呢?在现代的 C++中,这是错误的,大多数编译器都将会指出这一点;而有些较老的编译器将发出这样的警告:
  Warning: Temporary used for parameter ‘ra’ in call to refcube(double &)
  之所以做出这种比较温和的反应是由于早期的 C++ 确实允许将表达式传递给引用变量。有些情况下,仍然是这样做的。这样做的结果如下:由于 x + 3.0 不是 double 类型的变量,因此程序将创建一个临时的无名变量,将将其初始化为表达式 x + 3.0 的值,然后,ra 将成为该临时变量的引用。下面详细讨论这种临时变量,看看什么时候创建它们,什么时候不能创建。吃饭去
在这里插入图片描述完了,这饭还真不能吃,嘴里的泡又变大了,杯具了.
  临时变量、引用参数和 const
  如果实参与引用参数不匹配,C++ 将生成临时变量。当前,仅当参数为 const 引用时,C++ 才允许这样做,但以前不是这样。下面来看看何种情况下,C++ 将生成临时变量,以及为何对 const 引用的限制是合理的
  首先,什么时候将创建临时变量?如果引用参数是 const ,则编译器将在下面两种情况下生成临时变量:
  实参的类型正确,但不是左值;
  实参的类型不正确,但可以转换为正确的类型。
  左值是什么呢?,左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在 C 语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字 const 之前的情况。现在,常规变量和 const 变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而 const 变量属于不可修改的左值。
  回到前面的示例。假设重新定义了 refcube(),使其接受一个常量引用参数:

double refcube(donst 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.0, 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 varibale
double c6 = refcube(7.0);					// ra is temporary varibale
double c7 = refcube(sie + 10.0);			// ra is temporary varibale

       参数 side、lens[ 2 ]、rd 和 *pd 都是有名称的、double 类型的数据对象,因此可以为其创建引用,而不需要临时变量(还记得吗,数组元素的行为与同类型的变量类似)。然而,edge 虽然是变量,类型却不正确,double 引用不能指向 long。另一方面,参数 7.0我列个大艹,网页抽风,其它网页都没问题,这个网页突然崩溃,弄得我之后到程序清单 8.6 都没有保存,又是重新写一遍!!! 和 side + 10.0 的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让 ra 指向它。这些临时变量是只在函数调用期间存在,此后编译器便可以随意将其删除。
  那么为什么对于常量引用,这种行为是可行的,其他情况下却不行的呢?对于程序清单 8.4 中的函数 swapr():

void swapr(int & a, int & b) 	// us references
{
	int temp;

	temp = a;	// use a, b for values of variables
	a = b;
	b = temp;
}

       如果在早期 C++较宽松的规则下,执行下面的操作将发生什么情况呢?

long a = 3, b = 5;
swapr(a, b);

       这里的类型不匹配,因此编码器将创建两个临时 int 变量,将它们初始化为 3 和 5,然后交换临时变量的内容,而 a 和 b 保持不变。
  简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方是,禁止创建临时变量,现在的 C++ 标准正是这样做的(然而,在默认情况下,有些编译器仍将发出警告,而不是错误消息,因此如果看到了有关临时变量的警告,请不要忽略)。
  现在来看 refcube()函数。该函数的目的只是使用传递的值,而不是修改它们,因此临时变量不会造成任何不利的影响,反而会使函数在可处理的参数种类方面更通用。因此,如果声明交换引用指定为 const,C++将在必要时生成临时变量,实际上,对于形参为 const 引用的 C++ 函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
  注意:如果函数调用的参数不是左值或与相应的 const 引用参数的类型不匹配,则 C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
        应尽可能使用const
       将引用参数声明为常量数据的引用的理由有三个:

  • 使用 const 可以避免无意中修改数据的编程错误;
  • 使用const 使函数能够处理 const 和非 const实参,否则将只能接受非 const 数据;
  • 使用 const 引用使函数能够正确生成并使用临时变量。

因此,应尽可能将引用形参声明为 const。
  C++新增了另一种引用 ---- 右值引用(rvalue reference)。这种引用可指向右值。是使用 && 声明的:

double && rref = std :: sqrt(36.00);// not allowed fo double &
double j = 15.0;
double && jref = 2.0* j + 18.5;	 	// not allowed fo double &
std :: cout << rref << 'n';		// display 6.0
std :: cout << jref << 'n';		// display 48.5

       新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。第 18 章将讨论如何使用右值来实现移动语义(move semantics)。以前的引用(使用 & 声明的引用)现在称为左值引用。

8.2.4 将引用用于结构

       引用非常适合于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用这些类型的,而不是基本的内置类型。
  使用结构引用参数的方式是使用基本变量引用相同,只需在声明结构参数时使用引用运算符 & 即可。例如,假设有如下结构定义:`

struct free_throws
{
	std :: string name;
	int made;
	int attempts;
	float percent;
};

       则可以这样编写函数原型,在函数中将指向结构的引用作为参数:

void set_pc(free_throws & ft);// use a reference to a structure

       如果不希望函数修改传入的结构,可使用 const:

void display(const free)throws & ft); //don't allow changes to structure

       程序清单 8.6 中的程序正是这样做的。它还通过让函数返回指向结构的引用添加了一个有趣的特点,这与返回结构有所不同,对此。有一些需要注意的地方,稍后将进行介绍。
程序清单 8.6 strtref.cpp

// strc_ref.cpp -- using structure references
#include <iostream>
#include <string>
struct free_throws
{
	std:: string name;
	int made;
	int attempts;
	float percent;
};

void display(const free_throws & ft);
void set_pc(free_throws & ft);
free_throws & accumulate(free_throws &target, const free_throws & source);

int main()
{
	// partial initializations - remaining menbers set to 0
	free_throws one = { "Ifelsa Branch", 13, 14 };
	free_throws two = { "Andor Knott", 10, 16 };
	free_throws three = { "Minnie Max", 7, 9 };
	free_throws four = { "Whily Looper", 5, 9 };
	free_throws five = { "Long Long", 6, 14 };
	free_throws team = { "Throwgoods", 0, 0 };
	
	// no initialization
	free_throws dup;

	set_pc(one);
	display(one);
	accumulate(team, one);
	display(team);

	// use return value as argument
	dup = accumulate(team, five);
	std :: cout << "Displaying team:n";
	display(team);
	std:: cout << "Displaying duo after assignment:n";
	display(dup);
	set_pc(four);

	//ill-advised assignment
	accumulate(dup, five) = four;	// 这是几个意思、??
	std :: cout << "Displaying duo after ill-advised assignment:n";
	display(dup);

	return 0;
}

void display(const free_throws & ft)
{
	using std :: cout;
	cout << "Name: " << ft.name << 'n';
	cout << " Made: " << ft.made << 't';
	cout << "Attempts: " << ft.attempts << 't';
	cout << "Percent: " << ft.percent << 'n';
}

void set_pc(free_throws & ft)
{
	if (ft.attempts != 0)
		ft.percent = 100.0f *float(ft.made) / float(ft.attempts);
	else 
		ft.percent = 0;
}

free_throws & accumulate(free_throws & target, const free_throws & source)
{
	target.attempts += source.attempts;
	target.made += source.made;
	set_pc(target);

	return target;
}

运行代码如下却发现报错
在这里插入图片描述
还好还有一个 VS2010,代入代码再次运行:
在这里插入图片描述
VC6++虽然报错,应该还有办法解决的,查了下报错信息,意思是:(非聚合对象),不能使用初始化列表。只有聚合对象才可以这样使用。是不是只有结构成员的类型要一样才能编译通过,而此次结构中有 int 类型,又有 string类型,还有 float 类型,才不能通过?于是又找了下,发现一个解决之道,将结构这样改下:

struct free_throws
{
	std:: string name;
	int made;
	int attempts;
	float percent;
};

改成:

struct free_throws
{
	char name[ 30 ];
	int made;
	int attempts;
	float percent;
};

再用VC6++编译:
在这里插入图片描述
在这里插入图片描述
刚看了下天气,这太离谱了在这里插入图片描述
今天最高22度,明天却最高只有8度,一下子低了14度,明天看下是个什么情况,这天气真是离谱
在这里插入图片描述
360浏览器而面崩溃了两次,还好就只写了两句话,换GOOGLE浏览器再来。来黄州已经一周了,原以为在这里晚上会有大把的时间学习 C++,谁知道一落下就一直不想写了,好了,不多说,继续~
       1,程序说明
  该程序首先初始化了我个结构对象。前面讲过,如果指定的初始值比成员少,余下的成员(这里只有 percent)将被设置为零。第一个函数调用如下:
  set_pc(one);
  由于函数 set_pc() 的形参 ft 为引用,因此指向 one,函数 set_pc() 的代码设置成员 one.percent。就这里而言,按值传递不可行,因此这将导致设置的是 one 的临时拷贝的成员 percent。根据前一章介绍的知识,另一种方法是使用指针参数并传递地址,但要复杂些:
  set_pc( &one); // using pointers instead - &one instead of one
  在这里插入图片描述今天是大年初二,看看上面的时间,发现已经有50天没有学习了,希望不要就引落下了,继续,先温习一下之前学习的内容.
  在这里插入图片描述温习了上一个例子,没看完,先到这,明天再来看
  在这里插入图片描述
  1.程序说明
  该程序首先初始化了多个结构对象。本书前面说过,如果指定的初始值比成员少,余下的成员(这里只有 percent)将被设置为零。第一个函数调用如下:
  set_pc(one );
  由于函数 set_pc( one )的形参 ft 为引用,因此 ft 指向 one, 函数 set_pc 的代码设置成员 one.percent。就这里而言,按值传递不可行,因此这将导致设置的是 one 的临时拷贝的成员 percent。根据前一章介绍的知识,另一种方法是使用指针参数并传递地址,但是要复杂些:

set_pcp(&one);		// using pointers instead - &one instead of one (instead:代替; 顶替)
...
void set_pcp(free_throws *pt)
{
	if (pt -> attempts != 0)
		pt -> attempts = 100.9f *float (pt -> made)/ float (pt -. attempts);
	else
		pt -> percent = 0;
}

下一个函数调用如下:
  display(one);
  由于 display()显示结构的内容,而不修改它,因此这个函数使用了一个 const 引用参数。就这个函数而言,也可按值传递结构,但与复制原始拷贝相比,使用引用可节省时间和内存。
  再下一个函数调用如下:
  accumulate(team, one);
  函数 accumulate() 接收两个结构参数,并将第二个结构的成员 attempts 和 made 的数据添加到第一个结构的相应成员中。只修改了第一个结构,因此第一个参数为引用,而第二个参数是const 引用“
  free_throws & accumulate(free_throws & target, const free_throws & scource);
  返回值呢?当前讨论的函数调用没有使用它;就目前而言,原来可以将返回值声明为 void,但请看下述函数调用:
  display(accumulate(team, one));
  上述代码是什么意思?首先,将结构对象 team作为第一个参数传递给 accumulate()。这意味着在函数 accumulate()中,target指向的是 team。函数 accumulate() 修改 team,再返回指向它的引用。注意到返回语句如下:
  return target;
  光看这条语句并不能知道返回的是引用,但函数头和原型指出了这一点:
  free_throws & accumulate(free_throws & target, const free_throws & source)
  如果返回类型被声明为 free_throws 而不是 free_throws &, 上述返回语句将返回 target (也就是team)的拷贝。但返回类型为引用,这意味着返回的是最初传递给 accumulate() 的team 对象。
  接下来,将accumulate()的返回值作为参数传递给 display(),这意味着将 team传递给了display()。display()的参数为引用,这意味着函数display()中的ft指向的是team,因此将显示team的内容。所以,下述代码:
  display(accumulate(team, two);
  与下面的代码等效:
  accumulate(team, tow);
  display(team);
  上述逻辑也适用于如下语句:
  accumulate(accumulate(team, three), four);
  因此,该语句与下面的语句等效:
  accumulate(team, three);
  accumulate(team, four);
  接下来,程序使用了一条赋值语句:
  dup = accumulate(team, fivr);
  正如预期的,这条语句将 team 中的值复制到 dup中。
  最后,程序以独特的方式使用了 accumulate():
  accumulate(dup, five) = four:
  这条语句将值赋给函数调用,这是可行的,因为函数的返回值是一个引用。如果函数 accumulate()按值返回,这条语句将不能通过编译,由于返回的是指向 dup 的引用,因此上述代码与下面的代码等效:
  accumulate(dup, five); // add five’s date to dup
  dup = four; // overwrite the contents of dup with the contents of four
  其中第二条请多消除了第一条语句所做的工作,因此在原始赋值语句中使用 accumulate()的方式并不好。
  2.为何要返回引用
  下面更深入的讨论返回引用与传统返回机制的不同之处。传统返回机制死心塌地按值传递函数参数类似;计算关键字 return 后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。请看下面的代码:
  double m = sqrt( 16.0 );
  cout << sqrt( 25.0 );
  在第一条语句中,值 4.0 被复制到一个临时位置,然后被复制给 m。在第二条语句中,值 5.0 被复制到一个时间位置,然后被传递给 cout (这里理论上的描述,实际上,编译器可能 合并某些步骤)。
  现在来看下面的语句:
  dup = accumulate( team, five );
  如果 accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给 dup。但在返回值为引用时,将直接把 team复制到 dup ,其效率更高。
  注意:返回引用的函数实际上是被引用的变量的别名。
  3.返回引用时需要注意的问题
  返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。应该避免编写下面的这样代码:
  const free_throws & clone2( free_throws & ft)
  {
   free_throws newguy; // first step to big error (第一步就是一个大错误)
   newguy = ft; // copy info (复制信息)
   return newguy; // return reference to copy (将引用返回副本)
  }
  该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不复存在。第 9 章将讨论各种变量的持续性。同样,也应避免返回指向临时变量的指针。
  为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。程序清单中的 accumulate()正是这样做的。
  另一种方法是用 new 来分配新的存储空间。前面见过这样的函数,它使用 new 为字符串分配内存空间,并返回反射内存空间的指针。下面是使用引用来完成类似工作的方法:

const free_throws & clone( free_throww & 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 的调用,这使得以后很容易忘记使用 delete来释放内存。第 16 章讨论的 auto_ptr 模板以及 C++11新增的unique_ptr 可帮助程序员自动释放完成释放工作。
  4.为何将 const 用于引用返回类型
  程序清单 8.6 包含如下语句:
  accumulate( dup, five ) = four;
  其效果如下:首先将 five 的数据添加到 dup 中,再使用 four 的内容覆盖 dup 的内容。这条语句为何能够通过编译?在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向 dup 的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。
  另一方面,常规(非引用)返回类型是右值?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。
  假设林使用引用返回值,但又不允许执行像给 accumulate()赋值这样的操作,只需将返回类型声明为 const 引用:
  const free_throws & accumulate( free_throws & target, const free_throws & source);
  现在返回类型为 const, 是不可修改的左值,因此下面的赋值语句不合法:
  accumulate( dup, five ) = four: // not allowed for const reference return
  该程序中的其他函数调用又如何呢?返回类型为 const 引用后,下面的语句仍合法:
  display(accumulate(team, two));
  这是因为 display()的开参也是 const free_throws & 类型。但下面的语句不合法,因此 accumulate()的第一个形参不是 const:
  accumulate( accumulate( team, three), four);
  这影响大么?就这里而言不大,因为仍可以这样做:
  accumulate(team, three):
  accumulate(team, four);
  另外,仍可以在赋值语句右边使用 accumulate()。
  通过省略 const,可以编写更简短代码,但其含义也更模糊。
  通常,应该避免在设计中添加模糊的特性,因为模糊特性增加了犯错的机会。将返回类型声明为 const 引用,可避免犯糊涂。然而,有时候省略 const 确实有道理,第 11 章将讨论的重载运算符 << 就是一个这样的例子。
  在这里插入图片描述
  在这里插入图片描述

8.2.5 将引用用于类对象

将类对象传递给函数时,C++ 通常的做法是使用引用。例如,可以通过使用引用,让函数将类 string、ostream、istream、ofstream 和 ifstream 等类的对象作为参数。
  下面来看一个例子,它使用了 string 类,并演示了一些不同的设计方案,其中的一些是糟糕的。这个例子的基本思想是,创建一个函数,它将指定的字符串加入到另一个字符口中的前面和后面。程序清单 8.7 提供了三个这样的函数,然而其中的一个存在非常大的缺陷,可能导致程序崩溃甚至不能==(书中错别字)==通过编译
  
  程序清单8.7 strquote.cppp

// strquote.cpp -- different designs
#include <iostream>
#include <string>
using namespace std;
string version1(const string & s1, const string & s2);
const string & version2(string & s1, const string & s2);	// has side effect (有副作用)
const string & version3(string & s1, const string & s2);	// bad design (坏设计)

int main()
{
	string input;
	string copy;
	string result;

	cout << "Enter a string: ";
	getline(cin, input);
	copy = input;
	cout << "Your string as entered: " << input << endl;
	result = version1(input, "***");
	cout << "Your string enhanced: " << result << endl;
	cout << "Your original string: " << input << endl;

	result = version2(input, "###");
	cout << "Your string enhanced: " << result << endl;
	cout << "Your string string: " << input << endl;

	cout << "Reseting original string.n";
	input = copy;
	result = version3(input, "@@@");
	cout << "Your string enhanced: " << result << endl;
	cout << "Your original string: " << input << endl;

	return 0;
}

string version1(const string & s1, const string & s2)
{
	string temp;

	temp = s2 + s1 + s2;
	return temp;
}

const string & version2(string & s1, const string & s2)		// has side effect
{
	s1 = s2 + s1 + s2;
	//safe to return reference passed to function (安全地返回传递给函数的引用)
	return s1;
}

const string & version3(string & s1, const string & s2)		// bad design
{
	string temp;

	temp = s2 + s1 + s2;
	// unsafe to return reference to local variable (返回对局部变量的引用是不安全的)
	return temp;
}

程序运行结果如下:在这里插入图片描述
  此时,该程序已经崩溃。
  程序说明
  在程序清单 8.7 的三个函数中,version1最简单:

string version1(const string & s1, const string & s2)
{
	string temp;

	temp = s2 + s1 + s2;
	return temp;
}

它接受两个 string 参数,并使用 string 类的相加功能来创建一个满足要求的新字符串。这两个函数参数都是 const 引用。如果使用 string 对象作为参数,最终结果不变:
  string version4( string s1, string s2) // would work the same
  在这种情况下,s1 和 s2 将为 string 对象,使用引用的效率更高,因为函数不需要创建新的 string 对象,并将原来对象中的数据复制到亲的对象中。限定符 const 指出,该函数将使用原来的 string 对象,但不会修改它。
  temp 是一个新的 string 对象,只在函数 version1()中有效,该函数执行完毕后,它将不复存在。因此,返回指向 temp 的引用不可行,因此该函数的返回类型为 string, 这意味着 temp 的内容将被复制到一个临时存储单元中,然后在 main()中,该存储单元的内容被复制到一个名为 result 的 string 中:
  result = version1(input, “***”);

将 C-风格字符串用作 string 对象引 参数
  对于函数 version1(),你可能注意到了很有趣的一点:该函数的两个形参(s1 和 s2)的类型都是 const string &,但实参(input 和 ”*“)的类型分别是 string 和 const char。由于 input 的类型为 string,因此让 s1 指向它没有任何总是。然而,程序是怎么能够接受将 char 指针赋给 string 引用呢?
  这里有两点需要说明。首先,string 类定义了一种 char * 到 string 的转换功能,这使得可以使用 C-风格字符串来初始化 string 对象。其次是本章前面讨论过的类型为 const 引用的参数的一个属性。假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该 临时变量的引用。例如,在本章前面,将 int 实参传递给 const double & 形参时,就是以这种方式进行处理的。同样,也可以将实参 char * 或 const char
传递给形参 const string &。
  这种属性的结果是,如果形参类型为 const string &,在调用函数时,使用的实参可以是 string 对象或 C-风格字符串,如用引号括起的字符串字面量、以空字符结尾的 char 数组或指向 char 的指针变量。因此,下面代码是可行的:
  result = version1(input, “***”);
  函数 version2()不创建临时 string 对象,而是直接修改原来的 string 对象;

 const string & version2(string & s1, const string & s2)		// has side effect
  {
			s1 = s2 + s1 + s2;
			// safe to return reference passed to function
			return s1;
		}

       该函数可以修改 s1,因为不同于 s2,s1 没有被声明为 const。
       由于 s1 是指向 main()中一个对象(input)的引用,因此将 s1 作==(又一处错误)==为引用返回是安全的。由于 s1 是指向 input 的引用,因此,下面一行代码:
       result = version2(input, “###”);
       等价于下面的代码:
       version2(input, “###”); // input altered directly by version2() (altered:改变,更改,改动;修改; directly:直接地;立即;立刻)
       result = input; // reference to s1 is reference to input
       然而,由于 s1 是指向 input 的引用,调用该函数将带来修改 input 的副作用:
       Your original string: It’s not my fault.
       Your string enhanced: ###It’s not my fault.###
       Your original string: ###It’s not my fault.###
       因此,如果要保留原来的字符串不变,这将是一种错误设计。
       程序清单 8.7 中的第三个函数版本指出了什么不能做:

const string & version3(string & s1, const string & s2) 		// bad design
		{
			string temp;
	
			temp = s2 + s1 + s2;
			// unsafe to return reference to local variable
			return temp;
		}

       它存在一个致命的缺陷:返回一个指向 version3()中声明的变量的引用。这个函数能够通过编译(但编译器会发出警告),但当程序试图执行该函数时将崩溃。具体来说,总是是由下面的赋值语句引发的:
       result = version3(input, “@@@”);
       程序试图引用已经释放的内存。
在这里插入图片描述
在这里插入图片描述

8.2.6 对象、继承和引用

       ostream 和 ofstream 类凸现引用的一个有趣属性。正如第 6 章介绍的,ofstream 对象可以使用 ostream 类的方法,这使得文件输入/的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承,这将在第 13 章详细讨论。简单地说, ostream 是基类(因为 ofstream 是建立在它的基础之上的),而 ofstream 是派生类 (因为它是从 ostream 派生而来的)。派生类继承了基类的方法,这意味着 ofstream 对象可以使用基类的特性,如格式化方法 precision()和 setf()。
       继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际是,可以定义一个接受基类引用作为参数的函数,高用该函数时,可以将基类对象作为参数,也可以将拳打脚踢生类对象作为参数。例如,参数类型为 ostream & 的函数可以接受 ostream 对象(如cout)或你声明的 ofstream 对象作为参数。
       程序清单 8.8 通过调用同一个函数(只有函数调用参数不同)将数据写入文件和显示到屏幕上来说明了这一点。该程序要求用户输入望远镜和一些目镜的焦距,然后计算并显示每个目镜的放大倍数。放大倍数等于物镜的焦距除以目镜的焦距,因此计算起来很简单,该程序还使用了一些格式化方法,这些方法用于 cout 和 ofstream 对象(在这个例子中为 fout)时作用相同。
       程序清单 8.8 filerunc.cpp

// filefunc.cpp -- function with ostream & parameter
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

void file_it(ostream & os, double fo, const double fe[ ], int n);
const int LIMIT = 5;

int main()
{
	ofstream fout;
	const char * fn = "ep-data.txt";		// 文件名
	fout.open(fn);
	if(!fout.is_open())
	{
		cout << "Cant't open " << fn << ". Bye.n";
		exit(EXIT_FAILURE);
	}
	double objective;
	cout << "Enter the focal length of your "		// < focal length: 焦距>
			"telescope objective in mm: ";		// <telescope: 望远镜>
	cin >> objective;
	double eps[ LIMIT ];
	cout << "Enter the focal lengths, in mm, of " << LIMIT
		<< " eyepieces:n";			// < eyepieces: (望远镜或显微镜的) 目镜 >
	for (int i = 0; i < LIMIT; i++)
	{
		cout << "Eyepiece #" << i + 1 << ": ";
		cin >> eps[ i ];
	}
	file_it(fout, objective, eps, LIMIT);
	file_it(cout, objective, eps, LIMIT);
	cout << "Donen";

	return 0;
}

void file_it(ostream & os, double fo, const double fe[ ], int n)
{
	/*/::是C++的“域操作符”,用来指明作用域的,这个相信LZ应该知道的哈。
	也就是说,ios_base::fmtflags就说明了fmtflags是ios_base这个class里面的一个成员。
问题是fmtflags后面还跟个空格,还写个initial,查看参考链接可知,fmtflags原来是个typedef,
也就是一个类型定义,关于这一行是这样的:typedef implementation-defined-bitmask-type fmtflags;
也就是说,它定义了一个类型叫fmtflags,它实际上就是implementation-defined-bitmask-type的一个马甲。
这个implementation-defined-bitmask-type是啥呢?我也不知。名字上看,有“bitmask”,
也就是一个“位掩码”,作标志位运算的吧。*/

	ios_base::fmtflags initial;		// 就是定义了一个fmtflags类型的变量,变量名叫做initial
	initial = os.setf(ios_base::fixed);		// save initial formatting state
	os.precision(0);		// 设置精确度为0,并返回上一次的设置。
	os << "Focal length of objective: " << fo << " mmn";
	os.setf(ios::showpoint);		// 显示浮点数小数点后面的零。
	os.precision(1);
	os.width(12);
	os << "f.l. eyepiece";
	os.width(15);
	os << "magnification" << endl;		//< magnification: 放大; 放大率; 放大倍数; >
	for (int i = 0; i < n; i ++)
	{
		os.width(12);
		os << fe[ i ];
		os.width(15);
		os << int ( fo / fe[ i ] + 0.5 ) << endl;
	}
	os.setf(initial);		// restore initial formatting state

}

       下面是该程序运行情况:在这里插入图片描述
       下述代码行将目镜数据写入到文件 ep-data.txt 中:

file_it(fout, objective, eps, LIMIT);

       而下述代码行将同样的信息以同样的格式显示到屏幕上:

file_it(cout, objective, eps, LIMIT);

       程序说明
       对于该程序,最重要的一点是,参数 os(其类型为 ostream&)可以指向 ostream 对象(如 cout),也可以指向 ofstream 对象(如 fout)。该程序还演示了如何使用 ostream 类中格式化方法。下面复习(介绍)其中的一些,更详细的讨论请参阅第 17 章。
       方法 setf()让你能够设置各种格式状态。例如,方法调用 setf(ios_base::fixed)将对象置于使用定点表示法模式;setf(ios_base::showpoint)将对象置于显示小数点的模式,即使 小数部分为零。方法 precision()指定显示多少位小数(假定对象处于定点模式下)。所有这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们。方法 width()设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零,这意味着刚好能容纳下要显示的内容。
       函数 file_it() 使用了两个有趣的方法调用:

ios_base::fmtflags initial;
initial = os.setf(ios_base::fixed);
...
os.setf(initial);

       方法 setf()返回调用它之前有效的所有格式化设置。ios_base::fmtflags 是存储这种信息所需的数据类型名称。因此,将返回值赋给 initial 将存储调用 file_it()之前的格式化设置,然后便可以使用变量 initial 作为参数来调用 setf(),将所有的格式化设置恢复到原来的值。因此,该函数将对象回到传递给 file_it()之前的状态。
       了解更多有关类的知识将有助于更好地理解这些方法的工作原理,以及为何在代码中使用 ios_base。然而,你不用等到第 17 章才使用些方法。
       需要说明的最后一点是,每个对象都存储了自己的格式化设置。因此,当程序将 cout 传递给 file_it()时, cout 的设置将被修改,然后被恢复;当程序将 fout 传递给 file_it()时,fout 的设置将被修改,然后被恢复 。

8.2.7 何时使用引用参数

  使用引用参数的主要原因有两个。
  ● 程员能够修改调用函数中的数据对象
  ● 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
  当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因,这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么,什么时候应该使用引用、什么时候应该使用指针呢?什么时候应按值传递呢?下面是一些指导原则:
  对于使用传递的值而不作修改的函数。
  ● 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
  ● 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向 const 的指针。
  ● 如何数据对象是较大的结构,则使用 const 指针或 const 引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
  ● 如果数据对象是类对象,则使用 const 引用。类设计的主义常常要求使用相用,这是 C++ 新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
  对于修改调用函数中数据的函数:
  ● 如果数据对象是内置数据类型,则使用指针。如果看到诸如 fixit(&x) 这样的代码(其中 x 是 int)则很明显,该函数将修改 x。
  ● 如果数据对象是数组,则只能使用指针。
  ● 如果数据对象是结构,则使用引用或指针。
  ● 如果数据对象是类对象,则使用引用。
  当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。例如,对于基本类型,cin 使用引用,因此可以使用 cin >> n,而不是 cin >> &n。
  在这里插入图片描述
  在这里插入图片描述

8.3 默认参数

  下面介绍 C++ 的另一项新内容 – 默认参数。默认参数指的是当函数调用中省略了实参时自动使用的一值。例如,如果将 void wow(int n) 设置成 n 有默认值为 1,则函数调用 wow()相当于 wow(1)。这极大地提高了使用函数的灵活性。假设有一个名为 left()的函数。它将字符串和 n 作为参数,并返回该 字符串的前 n 个字符。更准确地说,该 函数返回一个指针,该指针指向由原始字符串中被选中的部分组成的字符串。例如,函数调用 left(”theory“, 3) 将创建新字符串”the“,并返回一个指向该字符串的指针。现在假设第二个参数的默认值被设置为 1,则函数调用 left(“theory”, 3)仍像前面讲述的那样工作,3 将覆盖默认值。但函数调用 left(“theory”)不会出错.它认为第二个参数的值为 1,工返回指向字符串 “t” 的指针。如果程序经常需要抽取一个字符组成的字符串,而偶尔需要抽取较长的字符串,则这种默认值很有帮助。
  如何设置默认值?必须通过函数原型。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知智育。方法是将值赋给原型中的参数。例如,left()的原型如下

char * left(const char * str, int n = 1);

  你希望该函数返回一个新的字符串,因此将其类型设置为 char *(指向 char 的指针);你希望原始字符串保持不变,因此对第一个参数使用了 const 限定答;你希望 n 的默认值为 1,因此将这个值赋给 n。默认参数值是初始化值,因此上面的原型将 n 初始化为 1,如果省略参数 n,则它的值将为 1;否则,传递的值将覆盖 1。
  对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值:

int harpo(int n, int m = 4, int j = 5);			// valid < 有效的 >(这里第一个参数未设置默认值,为何有效呢??不解)
int chico(int n, int m = 6, int j);				// invalid < 无效的 >
int groucho(int k = 1, int m = 2, int n = 3);	// valid < 有效的 >

  例如,harpo()原型允许调用该函数时提供 1 个、2 个或 3 个参数:

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 (没有使用默认参数)

  实参按左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。因此,下面的调用是不允许的:
bees = harpo(3, , 8); // invalid, doesn't set m to 4
  默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在设计类时你将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及重载的数量。
  程序清单 8.9 使用了默认参数,请注意,只有原型指定了默认值。函数定义与没有默认参数时完全相同。
  程序清单 8.9 left.cpp

// left.cpp -- string function with a default argument (字符串函数带一个默认参数)
#include <iostream>
const int ArSize = 80;
char * left(const char * str, int n = 1);
using namespace std;

int main()
{
	char sample[ ArSize ];
	cout << "Enter a string: n";
	cin.get(sample, ArSize);
	char *ps = left(sample, 4);
	cout << ps << endl;
	delete [] ps;		// free old string
	ps = left(sample);
	cout << ps << endl;
	delete [] ps;		// free new string

	return 0;
}

// This function returns a pointer to a new string
// consisting of the first n characters in the str string. (由 str 字符串中的前 n 个字符组成。)
char * left(const char * str, int n)
{
	if (n < 0)
		n = 0;
	char * p = new char[ n + 1];
	int i ;
	for (i = 0; i < n && str[ i ]; i ++)
		p[ i ] = str[ i ];		// copy characters
	while ( i <= n )
		p[ i ++ ] = '';		// set rest of string to ''
	
	return p;
}

  下面是该程序运行结果:在这里插入图片描述
  程序说明
  该程序使用 new 创建一个新的字符串,以存储被选择的字符。一种可能出现的尴尬情况是,不合作的用户要求的字符数目可能为负。在这种情况下,函数将字符计数设置为 0,并返回一个空字符串。另一种可能出现的尴尬情况是,不负责任的用户要求的字符数目可能多于字符串包含的字符数,为预防这种情况,函数使用了一个组合测试:

i < n && str[ i ]

  i < n 测试让循环复制了 n 个字符后终止。测试的第二部分 ---- 表达式 str[ i ], 是要复制的字符的编码遇到空值字符(其编码为 0)后,循环将结束。这样,while 循环将使字符串以空值字符结束,将将余下的空间(如果有的话)设置为空值字符。
  另一种设置新字符串长度的方法是,将 n 设置为传递的值 和字符串长度中较小的一个:

int len = strlen(str);
n = (n < len )? n : len;		// the lesser of n and len
char * p = new char [ n + 1 ];

  这将确保 new 分配的空间不会多于存储字符串所需的空间。如果用户执行像left(“Hi!”, 32767)这样的调用,则这种方法很有用。第一种方法将把 “Hi!” 复制到由 32767 个字符组成的数组中,并将除前 3 个字符外的所有字符都设置为空值字符;第二种方法将 “Hi!”复制到由 4个字符组成的数组中。但由于添加了另外一个函数调用 (str()),因此程序将更长,运行速度降低,同时还必须包含头文件 cstring (或 string.h)。C 程序员倾向于选择运行速度更快、更简洁的代码,因此需要程序员在正确使用函数方面承担更多责任。然而,C++ 的传统是更强调可靠性,毕竟,速度较慢但能正常运行的程序,要比运行速度虽快但无法正常运行的程序好。如果调用 strlen()所需的时间很长,则可以让 left()直接确定 n 和字符串长度哪个小。例如,当 m 的值等于 n 或到达 字符串结尾时,下面的循环将终止:

int m = 0;
while (m <= n && str[ m ] != '')
	m++;
char * p = new char[ m + 1];
// use m instead of n in rest of code

  别忘了,在 str[ m ] 不是空值字符时,表达式 str[ m ] !=’’ 的结果为 true,否则为 false。由于在 && 表达式中,非零值被转换为 true,而零被转换为 false,因此也可以这样编写这个 while测试:

while ( m < = n && str[ m ])

在这里插入图片描述休息。。。。
在这里插入图片描述

函数重载

  函数多态是 C++ 在 C 语言基本上新增的功能。默认参数让你能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让你能够使用多个同名的函数。术事 ”多态“ 指的是有多种形态,因此函数多态允许函数可以有多种形式。类似地,术语 “函数重载” 指的是有多个同名的函数,因此对名称进行了重载。这两个术语指的是同一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数 ---- 它们完成相同的工作,但使用不同的参数列表。
  重载函数就像是有多种含义的动词。例如,piggy 小姐可以棒球场为家乡球队助威(root),也可以在地里种植(root)茵类作物。根据上下文可以知道在每一种情况下,root 的含义是什么。同样,C++ 使用上下文来确定要使用的重载函数版本。
  函数重载的关键是函数的参数列表 ---- 也称为函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。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()函数时,编译器将根据所采取的用法使用有相应特征标的原型:

print("Pancakes", 15);							// use #1
print("Syrup");									// use #5
print(1999.9, 10);								// use #2
print(1999, 12);								// use #4
print(1999L, 15);								// use #3

  例如,print(“Pancakes”, 15) 使用一个字符串一个整数作为参数,这与 #1 原型匹配。
  使用被重载的函数时,需要在函数调用中使用正确的参数类型。例如,对于下面的语言:

unsigned int year = 3210;
print(year, 6);									// ambigous call < 模糊调用;ambigous 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 变量。请看下面的原型:

void dribble(char * bits);					// overloaded
void dribble(const char * cbits);			// overloaded
void dabble(char * bits);					// not overloaded
void drivel(const char * bits);				// not overloaded

  下面列出了各种函数调用对应的原型:

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 *);

  dribble()函数有两个原型,一个用于 const 指针,另一个用于常规,编译器将根据实参是否为 const 来决定使用哪个原型。dribble()函数只与带非 const 参数的调用匹配,而 drivel()函数可以与带 const 或非 const 参数的调用匹配。drivel()和 dabble() 之所以在行为上有这种差别,主要是由于将非 const 值赋给 const 变量是合法的,但反之则是不合法的。
  请记住,是特征标,而不是函数类型使得可以对函数进行重载。例如,下面的两个声明是互斥的:

long gronk(int n, float m);				// same signatures, < 相同的特征标 >
double gronk(int n, float m);			// hence not alowed < 因此不允许 >

  因此,C++ 不允许以这种方法重载 gronk()。返回类型可以不同,但特征标也必须不同:

long gronk(int n, float m);			// different signatures; < 不同的特征标 >
double gronk(float n, float m);		//hence allowed < 因此允许 >

  在本章稍后讨论过模板后,将进一步讨论函数匹配的问题。

在这里插入图片描述
在这里插入图片描述现在湖北人还在家里躲避新冠疫情
                    重载引用参数
  类设计和 STL 经常使用引用参数,因此知道不同引用类型的重载很有用。请看下面三个原型:

void sink(double & r1);			// matches modifialbe 1lvaue
void sank(const double & r2);	// matches modifialbe or const 1value, rvalue
void sunk(double && r3);		// matches rvalue

  左值引用参数 r1 与可修改的左值参数(如 double 变量)匹配;const 左值引用参数 r2 与可修改的左值参数、const 左值参数和右值参数(如两个 double 值的和)匹配;最后,左值引用参数 r3 与左值匹配。注意到与 r1 或 r3 匹配的参数都与 r2匹配。这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本:

void staff(double & rs);			// mathes modifiable 1value
void staff(const double & rcs);		// matches rvalue, const 1value
void stove(double & r1);			// matches modifiable 1value
void stove(const double & r2);		// matches const 1value
void stove(double && r3);			// matches const rvalue

题外语:这里书中函数返回类型有一个写成了 voit 我翻了翻第五版,这一段省略了,我又翻了英文版,原来英文的也写成了 voit了,中文版的把错误的也照抄过来了,我去改过来先,放两个英文和中文的对照图片在这里插入图片描述在这里插入图片描述
  这让你能够根据参数是左值、const 还是右值来定制函数的行为:

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(double &&)

  如果没有定义函数 stove(double &&),stove(x + y)将调用函数 stove(const double &)。

8.4.1 重载示例

  本章前面创建了一个 left()函数,它返回一个指针,指向字符串的前 n 个字符。下面添加另一个 left()函数,它返回整数的前 n 位。例如,可以使用该函数来查看被存储为整数的、美国邮政编码的前 3 位 —— 如果要根据城区分拣邮件,则这种操作很有用。
  该函数的整数版本编写起来比字符串版本更困难些,因为并不是整数的每一位被存储在相应的数组元素中。一种方法是,先计算数字包含多少位。将数字除以 10 便可以去掉一位,因此可以使用除法来计算数位。更准确地说,可以用下面的循环完成这种工作:

unsigned digits = 1;
while (n /= 100
digits ++;

上述循环计算每次删除 n 中的一位时,需要多少次才能删除所有的位。前面讲过, n / = 10 是 n = n / 10 的缩写。例如,如果 n 为 8,则该测试条件将 8 / 10 的值(0,由于这是整数除法)赋给 n。这半结束循环,digits 的值仍然为 1.但如果 n 为 238,第一轮循环测试将 n 设置为 238 / 10,既 23。这个值不为零,因此循环将 digits 增加到 2。下一轮循环将设置为 23 / 10,既 2.这个值还是不为零,因此 digits 将增加到 3。下一轮循环将 n 设置为 2 / 10,既 0,从而循环结束,而 digits 被设置为正确的值 —— 3。
  现在假设知道数字共有 5 位,并要返回前 3 位,则将这个数除以10 后再除以 10,便可以得到所需的值。每除以 10 次就删除数字的最后一位。要知道需要删除多少位,只需将总位数减去要获得的位数即可。例如,要获得 9 位数的前 4 位,需要删除后面的 5 位。可以这样编写代码:

ct = digits = ct;
while (ct --)
	num / = 10;
return num;

程序清单 8.10 将上述代码放到一个新的 left()函数中。该函数还包含一些用于处理特殊情况的代码,如用户要求显示 0 位或要求显示的位数多于总位数。由于新 left()的特征标不同于旧的 left(),因此可以在同一个程序中使用这两个函数。
  
  程序清单 8.10 leftover.cpp
  ——————————————————————————————————————————————————————
在这里插入图片描述
在这里插入图片描述

// leftover.cpp -- overloading the left() function
#include <iostream>
unsigned long left(unsigned long num, unsigned ct);
char * left(const char * str, int n = 1);

int main()
{
	using namespace std;
	char * trip = "Hawaii!!";		// test value
	unsigned long n = 12345678;		// test value
	int i;
	char * temp;

	for (i = 1; i < 10; i ++)
	{
		cout << left(n, i) << endl;
		temp = left(trip, i);
		cout << temp << endl;
		delete [] temp;		// point to temporary storage
	}

	return 0;
}

// This function returns the first ct digits of the number num.
unsigned long left(unsigned long num, unsigned ct)
{
	unsigned digits = 1;
	unsigned long n = num;

	if (ct == 0 || num == 0)
		return 0;		// return 0 if no digits

	// 先将n的值除以10,然后判断其是否为0,为0则终止循环。
	// 当n不等于0时执行while循环中的内容,并且n自减1,一直到n等于0时跳出while循环(n还是会自减1)
	while (n /= 10)		
		digits ++;
	if (digits > ct)
	{
	ct = digits - ct;
	while (ct --)
		num /= 10;
	return num;			// return left ct digits
	}
	else				// if ct >= number of digits
		return num;		// return the whole number
}

// This function returns a pointer to a new string
// consisting of the first n characters in the str string.
char * left(const char * str, int n)
{
	if (n < 0)
		n = 0;
	char * p = new char[ n + 1 ];
	int i;
	for (i = 0; i < n && str[ i ]; i ++)
		p[ i ] = str [ i ];	// copy characters
	while (i <= n)
		p[ i ++ ] = '';	// set rest of string to ''
	return p;
}

——————————————————————————————————————————————————————
  下面是该程序的输出在这里插入图片描述

8.4.2 何时使用函数重载

  虽然函数重载很吸引人,但也不要滥用。仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。另外,你可能还想知道,是否可以通过使用默认参数来实现同样的目的。例如,可以用两个重载函数来代替面向字符串的 left()函数:

char * left (const char * str, unsigned n);		// two arguments
char * left (const char * str);					// one arguments

  使用一个琏默认参数的函数要简单些。只需要编写一个函数(而不是两个函数),程序也只需为一个函数(而不是两个)请求内存;需要修改函数时,只需要修改一个。然而,如果需要使用不同类型的参数,默认参数便不管用了,在这种情况下,应该使用函数重载。
  
   什么是名称修饰
   C++如何跟踪每一个重载函数呢?它给这些函数指定了秘密身份。使用 C++开发工具中的编辑器编写和编译程序时,C++编译器将执行一些神奇的操作 —— 名称修饰(name decoration)或名称矫正(name mangling),它根据原型中指定的形参类型对每个函数名进行加密。请看下述未经修饰的函数原型:

long MyFunctionFoo(int, float);

  这种格式对于人类来产很适合;我们知道函数接受两个参数(一个为 int 类型,另一个为 float 类型),并返回一个 long 值。而编译器将名称转换为不太好看的内部表示,来描述该接口,如下所示:

?MyFunctionFoo@@YAXH

对原始名称进行的表面看来无意义的修饰(或矫正,因人而异)将对参数数目和类型进行编码。添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异。

函数模板

  现在的 C++ 编译器实现了 C++ 新增的一项特性 —— 函数模板。函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如 int 或 double )替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized type)。下面介绍为何需要这种特性以及其工作原理。
  在前面的程序清单 8.4 中,定义了一个交换两个 int 值的函数。假设要交换两个 double 值,则一种方法是复制原来的代码,并用 double 替换所有的 int。如果需要交换两个 char 值,可以再次使用同样的技术。进行这种修改浪费富贵的时间,且容易出错。如果进行手工修改,则可能漏掉一个 int。如果进行全局查找和替换(如用 double 替换 int )时,可能将:

int x;
short interval;
转换为:
double x;				// intended change of type 变更类型<intend;打算;计划;想要;意指>
short doubleerval;		// unintended change of variable name 变量名的意外更改,<unintended;非计划的; 无意的; 无心的>

  C++ 的函数模板功能能自动完成这一过程,可以节省时间,而且更可靠。函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板:

template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}

  第一行指出,要建立一个模板,并将类型命名为 AnyType。关键字 template 和 typename 是必需的,除非可以使用关键字 class 代替 typename。另外,必须使用尖括号。类型名可以任意选择(这里为 AnyType),只要遵守 C++ 命名规则即可;许多程序员都使用简单的名称,如 T。余下的代码描述交换两个 AnyType 值的算法。模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换 int 的函数时,编译器将按模板模式创建这样的函数,并用 int 代替 AnyType。同样,需要交换 double 的函数时,编译器将按模板模式创建这样的函数,并用 double 代替 AnyType。
  在标准 C++98 添加了关键字 typename 之前,C++使用关键字 class 来创建模板。也就是说,可以这样编写模板定义:

template <class AnyType>
void Swap(AnyType &a, AnyType &b)
{
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}

  typename 关键字使得参数 AnyType 表示类型这一点更为明显;然而,有大量代码库是使用关键字 class 开发的。在这种上下文中,这两个关键字是等价的。本书使用了这两种形式,旨在让你在其他地方遇到它们时不会感到陌生。
  提示:如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字 typename 而不是 class。
  要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用 Swap()函数即可,编译器将检查所使用的参数类型,并生成相应的函数。程序清单 8.11 演示为何可以这样做。该程序的布局和使用常规函数时相同,在文件的开始位置提供模板函数的原型,并在 main()后面提供模板函数的定义。这个示例采用了更觉的做法,即将 T 而不是 AnyType 用作类型参数。
在这里插入图片描述休息~
在这里插入图片描述
程序清单 8.11 funtemp.cpp
————————————————————————————————————————————

// funtemp.cpp -- using a function template
#include <iostream>
// function template prototype
template <typename T>		// or <class T>
void Swap(T &a, T &b);

int main()
{
	using namespace std;
	int i = 10;
	int j = 20;
	cout << "i, j = " << i << ", " << j << ".n";
	cout << "Using compiler-generated int swapper:n";  // compiler-generated:编译器生成
	Swap(i, j);		// generates void Swap(int &, int &)
	cout << "Now i, j = " << i << ", " << j << ".n";

	double x = 24.5;
	double y = 81.7;
	cout << "x, y = " << x << ", " << y << ".n";
	cout << "Using conpiler-generated double swapper:n";
	Swap(x, y);		// generates void Swap(double &, double &)
	cout << "Now x, y = " << x << ", " << y << ".n";
	// cin.get();
	
	return 0;
}

// function template definition
template <typename T>	// or class T
void Swap(T &a, T &b)
{
	T temp;		// temp avariable of type T
	temp = a;
	a = b;
	b = temp;
}

————————————————————————————————————————————
  程序清单 8.11 中的第一个 Swap()函数接受两个 int 参数,因此编译器生成该函数的 int 版本。也就是说 int 替换所有的 T,生成下面这样的定义:

void Swap(int &a, int &b)
{
	int temp;
	temp = a;
	a = b;
	b = temp;
}	

  程序员看不到这样的代码,但编译器确实生成并在程序中使用了它们。第二个 Swap()接受两个 double 参数,因此编译器将生成 double 版本。也就是说,用 double 替换 T,生成下述代码:

void Swap(double &a, double &b)
{
	double temp;
	temp = a;
	a = b;
	b = temp;
}

  下面是程序清单 8.11 中程序的输出,从中可知,这种处理方式是可靠的:在这里插入图片描述
  注意,函数模板不能缩短可执行程序。对于程序清单 8.11,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样,最终的代码不包含任何模板,而只包含了程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。
  更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。头文件将在第 9 章讨论。
  在这里插入图片描述

8.5.1 重载的模板

  需要多个对不同类型使用同一种算法的函数时,可使用模板,如程序清单 8.11 所示。然而,并非所有的类型都使用相同的算法。为满足这种需要,可以像重载常规函数定义那样生重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。例如,程序汇款单 8.12 新增一个交换模板,用于交换两个数组中的元素。原来的模板的特征标为(T &, T &),而新模板的特征标为(T [ ], T [ ], int)。注意,在后一个模板中,最后一个参数是类型为具体类型(int)。而不是泛型。并非所有的模板参数都必须是模板参数类型。
  编译器见到 twotemps.cpp 中第一个 Swap()函数调用时,发现它有两个 int 参数,因此将它与原来的模板匹配。但第二次调用将两个 int 数组和一个 int 值用作参数,这与新模板匹配。
  程序清单 8.12 twotemps.cpp
  ——————————————————————————————————————————————————

// twotemps.cpp -- using overloaded template functions
#include <iostream>
template <typename T>		//	original template < 原始模板 >
void Swap(T &a, T &b);

template <typename T>		// new template < 新模板 >
void Swap(T *a, T *b, int n);		// 写代码时把这一句给抄掉了
void Show(int a[]);
const int Lim = 8;

int main()
{
	using namespace std;
	int i = 10, j = 20;
	cout << "i, j = " << i << ", " << j << ".n";
	cout << "Using compiler-generated int swapper:n";
	Swap(i, j);					// matches original template < 匹配原始模板 >
	cout << "Now i, j = " << i << ", " << j << ".n";

	int d1[ Lim ] = { 0, 7, 0, 4, 1, 7, 7, 6 };
	int d2[ Lim ] = { 0, 7, 2, 0, 1, 9, 6, 9 };
	cout << "Original arrays:n";
	Show(d1);
	Show(d2);
	Swap(d1, d2, Lim);			// matches new template < 匹配新模板 >
	cout << "Swapped arrays:n";
	Show(d1);
	Show(d2);
	// cin.get();

	return 0;
}

template <typename T>
void Swap(T &a, T &b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

template <typename T>
void Swap(T a[], T b[],int n)
{
	T temp;
	for (int i = 0; i < n; i++)
	{
		temp = a[ i ];
		a[ i ] = b[ i ];
		b[ i ] = temp;
	}
}

void Show(int a[])
{
	using namespace std;
	cout << a[ 0 ] << a[ 1 ] << "/";	//	输出 a[ 0 ] 和a[ 1 ],后紧接着打印一个斜杠,即07/
	cout << a[ 2 ] << a[ 3 ] << "/";	//	输出 a[ 2 ] 和a[ 3 ],后紧接着打印一个斜杠,即04/
	for (int i = 4; i < Lim; i++)		//  然后循环打印出数组后面所有元素
		cout << a[ i ];
	cout << endl;
}

——————————————————————————————————————————————————
  下面是程序清单 8.12 中程序的输出:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

8.5.2 模板的局限性

  假设有如下模板函数:

template <class T>		// or template <typename T>
void f(T a, T b>
{...}

  通常,代码假定执行哪些操作。例如,下面的代码假定定义了赋值,但如果 T 为数组,这种假设不成立:
a = b;
  同样,下面的语句假设定义了 < ,但如果 T 为结构,该假设不成立:
if (a > b)
  另外,为数组名定义了运算符 >,但由于数组名为地址,因此它比较的是数组的地址,而这可能不是你希望的。下面的语句假定为类型 T 定义了乘法运算符,但如果 T 为数组、指针或结构,这种假设便不成立:
T c = a * b;
  总之,编写的模板函数很可能无法处理某些类型。另一方面,有时候通用化是有意义的,但 C++ 语法不允许这样做。例如,将两个包含位置坐标的结构相加是有意义的,虽然没有为结构定义运算符 +。一种解决方案是,C++允许你重载运算符 +,以便能够将其用于特定的结构或类(运算符重载将在第 11 章讨论)。这样使用运算符 + 的模板便可处理重载了运算符 + 的结构。另一种解决方案是,为特定类型提供具体化的模板定义,下面就来介绍这种解决方案。

8.5.3 显式具体化

  假设定义了如下结构:

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), 其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
  具体化机制随着 C++ 的演变而不断变化。下面介绍 C++ 标准定义的形式。
  1. 第三代具体化(ISO/ANSI C++标准)
  试验其他具体化方法后,C++标准选择了下面的方法。
  ● 对于给定的函数后,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
  ● 显式具体化的原型和定义应以 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 &, job &);

  正如前面指出的,如果有多外原型,则编译器有选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。例如,在下面的代码中,第一次调用 Swap()时使用通过版本,而第二次调用使用基本 job 类型的显式具体化版本。

...
template <class T>		// template
void Swap(T &, T &);

// explicit specialization for the job type
template <> void Swap (job> (job &, job &);
int main()
{
	double u , v;
	...
	Swap(u, v);		// use template
	job a, b;
	...
	Swap(a, b);		// use void Swap<job>(job &, job &)
}

  Swap中的是可选的,因为函数的参数类型表明,这是 job 的一个具体化,因此,该原型也可以这样编写:

template <> void Swap(job &, job &); 	// simpler form < 简单形式 >

  下面来看一看显式具体化的工作方式。
  2. 显式具体化示例
  程序清单 8.13 演示了显式化的工作方式。

程序清单 8.13 twoswap. cpp
 在这里插入图片描述
 —————————————————————————————————————————————————————

// towswap.cpp -- specialization overrides a template
#include <iostream>
template <typename T>
void Swap(T &a, T &b);

struct job
{
	char name [ 40 ];
	double salary;
	int floor;
};

// explicit specialization
template <> void Swap<job>(job &j1, job &j2);
void Show(job &j);

int main()
{
	using namespace std;
	cout.precision(2);	// 返回当前的浮点数的精度值
	cout.setf(ios::fixed, ios::floatfield);
	int i = 10, j = 20;
	cout << "i, j = " << i << ", " << j << ".n";
	cout << "Using compiler-generated int swapper:n";
	Swap(i, j);		// generates void Swap(int &, int &)
	cout << "Now i, j = " << i << ", " << j << ".n";

	job sue = {"Susan Yaffee", 73000.60, 7};
	job sidney = {"Sidney Taffee", 78060.72, 9};
	cout << "Before job swapping:n";
	Show(sue);
	Show(sidney);
	Swap(sue, sidney);		// uses void Swap(job &, job &)
	cout << "After job swapping:n";
	Show(sue);
	Show(sidney);
	// cin.get();
	
	return 0;
}

template <typename T>
void Swap(T &a, T &b)		// general version
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

// swaps just the salary and floor fields of a job structure

template <> void Swap<job>(job &j1, job &j2)	// specialization 专门化; 特化
{
	double t1;
	int t2;
	t1 = j1.salary;
	j1.salary = j2.salary;
	j2.salary = t1;
	t2 = j1.floor;
	j1.floor = j2.floor;
	j2.floor = t2;
}

void Show(job &j)
{
	using namespace std;
	cout << j.name << ": $" << j.salary << " on floor " << endl;
}

—————————————————————————————————————————————————————
  代码运行情况如下:
在这里插入图片描述

8.5.4 实例化和具体化

  为进一步了解模板,必须理解术语实例化和具体化。==记住,在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。==编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。例如,在程序清单 8.13 中,函数调用 Swap(i, j)导致编译器生成 Swap() 的一个实例,该实例使用 int 类型。模板并非函数定义,但使用 int 模板实例是函数定义。这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以需要进行定义,是由于程序调用 Swap()函数时提供了 int 参数。
  最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在 C++ 还 允许显式实例化(explicit instantiation)。这意味着可以直接命令编译器创建特定的实例,如 Swap()。其语法是,声明所需的种类 —— 用 <> 符号指示类型,并在声明前加上关键字 template:

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

  实现了这种特性的编译器看到上述声明后,将使用 Swap()模板生成一个使用 int 类型的实例。也就是说,该声明的意思是 ”使用 Swap()模板生成 int 类型的函数定义。“
  与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:`

template < > void Swap<int> (int &, int &);		// explicit specialization
template < > void Swap (int &int &);		// explicit specialization

 &#8195区别在于,这些声明的意思是 “ 不要使用 Swap() 模板来生成函数定义,而应使用专门为 int 类型显式地定义的函数定义 ”。这些原型必须有自己的函数定义。显式具体化声明在关键字 template 后包含 <>,而显式实例化没有。

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

  还可以通过在程序使用函数赤创建显式实例化。例如,请看下面的代码:

template <class T>
T Add(T a, T b)		// pass by value
{
	return a + b;
}
...
int m = 6; 
double x = 10.2;
cout << Add<double>(x, m) << endl;		// explicit instantiation

  这里的模板与函数调用 Add(x, m) 不匹配,因为该模板要求两个函数参数的类型相同。但通过使用
Add(x, m),可强制为 double 类型实例化,并将参数 m 强制转换为 double 类型,以便与函数
Add(double, double)的第二个参数匹配。
  如果对 Swap()做类似的处理,结果将如何呢?

int m = 5;
double x = 14.3;
Swap<double>(m , x);		// almost works

  这将类型 double 生成一个显式实例化。不幸的是, 这些代码不管用,因为第一个形参的类型为 doulbe&,不能指向 int 变量 m。
  隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
  引入显式实例化后,必须使用新的语法 —— 在声明中使用前缀 template 和 template<>,以区分显式实例化和显式具体化。通常,功能越多,语法规则也越多。下面的代码片段总结了这些概念:

template <calss T>
void Swap(T &, T &);		// template prototype < template 原型 >

template <> 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 instantiantion 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 < 使用显式模板实例化 >
}

在这里插入图片描述
  编译器看到 char 的显式实例化后,将使用模板定义来生成 Swap()的 char 版本。对于其他 Swap()调用,编译器根据函数调用中实际使用的参数,生成相应的版本。例如,当编译器看到函数调用 Swap(a,b)后,将生成 Swap() 的 short 版本,因为两个参数的类型都是 short。当编译器看到 Swap(n,m)后,将使用为 job类型提供的独立定义(显式具体化)。当编译器看到 Swap(g,h)后,将使用处理显式实例化时生成的模板具体化。

8.5.5 编译器选择使用哪个函数版本

  对于函数重载、函数模板和函数模板重载,C++ 需要(且有)一个定义良好的策略,来决定函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,因此我们先大致了解一下这个过程是如何进行的。
  ● 第 1 步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
  ● 第 2 步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 float 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为 float 生成一个实例。
  ● 第 3 步:确定是否有最佳的可行函数。如何有,则使用它,否则该函数调用出错。
  考虑只有一个函数参数的情况,如下面的调用:

may('B');		// actual argument is type char

首先,编译器将寻找候选者,即名称为 may()的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如,下面的函数符合要求,因为其名称与被调用的函数相同,且可只给它们传递一个参数:

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

  注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数(#4 和 #&)不可行,因为正数类型不能被隐式地转换)即没有显式强制类型转换)为指针类型。剩余的一个模板可用来生成具体化,其中 T 被替换为 char 类型。这样剩下 5 个可行的函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。
  接下来,编译器必须确定哪个可行函数是最佳的。它查看为使函数参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述。
  1,完全匹配,但常规函数优先于模板。
  2,提升转换(例如,char 和 shorts 自动转换为 int,float 自动转换为 double)。
  3, 标准转换(例如,int 转换为 char, long 转换为 double)。
  4,用户定义的转换,如类声明中定义的转换。
  例如,函数 #1 优于函数 #2,因为 char 到 int 的转换是提升转换(参见第 3 章),而 char 到 float 的转换是标准转换(参见第 3 章)。函数 #3、函数 #5 和 函数 #6都优于函数 #1 和函数 #2,因为它们都是完全匹配的,#3 和 #5 优于 #6, 因为函数是模板。有两个函数完全 匹配是一种错误,但这一规则有两个例外。显然,我们都需要对这一点做更深入的探讨。

1. 完全匹配和最佳匹配
  进行完全匹配时,C++ 允许某些“无关紧要的转换”。表 8.1列出了这些转换 ── Type表示任意类型。例如,int 实参与int &形参完全匹配。注意,Type 可以是 char & 这样的类型,因此这些规则包括从 char & 到 const char & 的转换。Type(argument-list)意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配的(第 7 章了函数指针以及为何可以将函数名作为参数传递给接受函数指针的函数)。第 9 章将介绍关键字 volatile。在这里插入图片描述

  假设有下面的函数代码:

struct blot {int a; char b[ 10 ];};
blot ink = {25, "spots"};
...
recycle(ink);

  在这种情况下,下面的原型都是完全匹配的:

void recycle(blot);				// #1 blot - to - blot
void recycle(const blot);		// #2 blot - to - (const blot)
void recycle(blot &);			// #3 blot - to - (bolt &)
void recycle(const blot &);		// #4 blot - to - (const blot &)

  正如你预期的,如果有多个匹配的原型,则编译器半无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如“ambiguous(二义性)”这样的词语。
  然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非 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

  假设包含这些模板的程序也包含如下代码:

struct blot {int a; char b[ 10 ];};
blot ink = { 25, "spots"};
...
recycle(&ink); 		// address of a structure

  recycle(&ink)调用与 #1 模板匹配,匹配时将 Type 解释为 blot *。recyclt(&ink) 函数调用也与 #2 模板匹配,这次 Type 被解释为 ink。因此将两个隐式实例 ── recyclt<blot *>(bolt *) 和 recycle(blot *) 发送到可行函数池中。
  在这两个模板函数中,recycle<blot *>(bolt *) 被认为是更具体的,因为在生成过程中,它需要进行的转换更少。也就是说,#2 模板已经显式指出,函数参数是指向 Type 的指针,因此可以直接用 blot 标识 Type;而 #1 模板将 Type 作为函数参数,因此 Type 必须被解释为指向 blot 的指针。也就是说,在 #2 模板中, Type 已经被具体化为指针,因此说它”更具体化“。
  用于找出最具体的模板的规则被称为函数模拟的部分排列规则(partial ordering rules)。和显式实例一样,这也是 C++98 新增的特性。

  2.部分排序规则示例

  我们先看一个完整的程序,它使用部分规则来确定要使用哪个模板定义。程序清单 8.14 有两个用来显示数组内容的模板定义。第一个定义(模板 A)假设作为参数传递的数组中包含了要显示的数据;第二个定义(模板 B)假设数组元素为指针,指向要显示的数据。

  程序清单 8.14 temptempover.cpp
──────────────────────────────────────────────────────────────────────────

// tempover.cpp -- template voerloading
#include <iostream>

template <typename T>		// template A
void ShowArray(T arr[], int n);

template <typename T>		// template B
void ShowArray(T * arr[], int n);

struct debts
{
	char name[ 50 ];
	double amount;
};

int main()
{
	using namespace std;
	int things[ 6 ]= {13, 31, 103, 301, 310,130};
	struct debts mr_E[ 3 ] =
	{
		{"Ima Wolfe", 2400.0},
		{"Ura Foxe", 1300.0},
		{"Iby Stout", 1800.0},
	};
	double * pd[ 3 ];

	// set point to the amount members of the structures in mr_E
	for (int i = 0; i < 3; i ++)
		pd[ i ] = &mr_E[ i ].amount;

	cout << "Listing Mr. E's counts of things:n";
	// things is an array of int
	ShowArray(things, 6);	// uses template A
	cout << "Listing Mr. E's debts:n";
	// pd is an array of pointers to double
	ShowArray<double>(pd, 3);		// uses template B (more specialized) 这里在 vc6++编译器里会出错
	return 0;

}

template <typename T>
void ShowArray(T arr[], int n)
{
	using namespace std;
	cout << "template An";
	for (int i = 0; i < n; i ++)
		cout << arr[ i ] << ' ';
	cout << endl;
}

template <typename T>
void ShowArray(T * arr[], int n)
{
	using namespace std;
	cout << "template Bn";
	for (int i = 0; i < n; i ++)
		cout << *arr[ i ] << ' ';
	cout << endl;
}

  注意:上面代码中 ShowArray(pd, 3);在 VC6++编译器里会出错,错误信息为在这里插入图片描述
  大致意思是:(none of 2 overload have a best conversion)两个重载都没有最佳转换;
  (ambiguous call to overloaded function)对重载函数的不明确调用。
  目前有两种解决方法,一种是用高版本的编译器,我正好有个vs2010,同样的代码在2010中运行没有问题,在这里插入图片描述
另一种方法是,修改在编译器VC6++原代码,`

ShowArray(pd, 3);
// 修改为
ShowArray<double>(pd, 3);

  由于 ShowArray() 是一个模板类,重载的时候明确指定类型参数即可。如下运行结果在这里插入图片描述
──────────────────────────────────────────────────────────────────────────

  请看下面的函数调用:`

ShowArray(things, 6);

标识符 things 是一个 int 数组的名称,因此与下面的模板匹配:

template <typename T>			// template A
void ShowArray(T arr[], int n);

  其中 T 被替换为 int 类型。
  接下来,再请看下面的函数调用:

ShowArray(pd, 3);

  其中 pd 是一个 double *数组的名称。这与模板 A 匹配:

template <typename T>			// template A
void ShowArray(T arr[], int n);

  其中,T 被替换为类型 double *。在这种情况下,模板函数半显示 pd 数组的内容,即 3 个地址。该函数调用也与模板 B 匹配:

template <typename T>			// template B
void ShowArray(T * arr[], int n);

  在这里,T 被替换为类型 double,而函数将显示被解除引用的元素 *arr[ i ],即数组内容指向的 double值。在这两个模板中,模板 B 更具体,因为它做了特定的假设 —— 数组内容是指针,因此被使用。
  如果将模板 B 从程序中删除,则编译器将使用模板 A 来显示 pd 的内容,因此显示的将是地址,而不是值,我们来试试看。在这里插入图片描述
  
  注意看,将模板 B 注释掉,下面的代码要改回 ShowArray(pd, 3);才能运行该程序,否则类型不匹配。
  简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误的。

  3.自己选择

在某些情况下,可通过编写合适的函数调用,引导编译器做出自己希望的选择。请看程序清单 8.15,该程序将模板函数定义放在文件开头,从而无需提供模板原型。与常规函数一样,通过在使用函数前提供模板函数定义,字让它也充当原型。

程序清单 8.15 choices.cpp
 ─────────────────────────────────────────────────────────────────────────

#include<iostream>
 
template<class T>
T lesser(T a, T b)
{
	return a < b ? a : b;
 
}
 
int lesser(int a, int b)
{
	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;
	cout << lesser(x, y) << endl;
	cout << lesser<>(m, n) << endl;
	cout << lesser<int>(x, y)<<endl;
 
	system("pause");
	return 0;
}

————————————————————————————————————————————————
  最后的函数调用将 double 转换为 int,有些编译器会针对这一点发出警告。
  这里有一个大大的问题就是低版本的编译器和高版本的编译器所运行程序的结果会不同,这里 VC6运行结果:
  在这里插入图片描述

  这下面是 VS2010 运行结果:
  在这里插入图片描述
  相同的代码,复制 -> 粘贴,竟然运行结果不一样,发现 VS2010 环境下 lesser<>(m, n) 会调用 #1,而 VC6++ 这行代码却去调用 #2,这个不知道是何解。如果要 VS2010 和 VC6++ 运行结果一致,即第三次调用 #1,需要把代码:

cout << lesser<>(m, n) << endl;

改成

cout << lesser<double>(m, n) << endl;

  此时两个编译器运行结果一致。
在这里插入图片描述

程序清单 8.15 提供了一个模板和一个标准模板,其中模板返回两个值中较小的一个,而标准函数返回两个值中绝对值较小的那个。如果函数定义是在使用函数前提供的,它将充当函数原型,因此这个示例无需提供原型,请看下面的语句:

cout << lesser(m, n) << endl;		// use #2

  这个函数调用与模板函数和非模板函数都匹配,因此选择非模板函数,返回 20.
  接下,下述语句中的函数调用与模板匹配 (T 为 double):

dout << lesser(x, y) << endl;		// use #1 with double

  现在来看下面的语句:

cout << lesser<>(m, n) << endl;		// use #1 with int

  lesser<>(m, n)中的 <> 指出,编译器应选择模板函数,而不是非模板函数;编译器注意到实参的类型为 int,因此使用 int 替代 T 对模板进行实例化。
  最后,来看下面的语句:

cout << lesser<int>(x, y) << endl;	// use #1 with int

  这条语句要求进行显式实例化(使用 int 替代 T),将使用显式实例化得到的函数。x 和 y 的值将被强制转换为int,该函数返回一个 int 值,这就是程序显示 15 而不是15.5 的原因反在。
在这里插入图片描述
  4.多个参数的选择
  将有多个参数的函数调用与多个参数的原型进行匹配时,情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行函数都合适的函数,则选择该函数。一个函数要比其他函数都合适,其所有参数的匹配程序都必须不比其他函数差,同时至少有一个参数的匹配程序比其他函数都高。
  本书并不是要解释复杂示例的匹配过程,这些规则只是为了让任何一组函数原型和模板都存在确定的结果。

8.5.6 模板函数的发展

  在 C++ 发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用,它们甚至没有就这个主题发挥想像力。但聪明而专注的程序员挑战模板技术的极限,阐述了各种可能性。根据熟悉模板的程序员提供的反馈,C++ 98 标准做了相应的修改,并添加了标准模板库。从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C++11 标准根据这些程序员的反馈做了相应的修改。下面介绍一些相关的问题及其解决方案。

  1. 是什么类型

  在 C++98 中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。请看下面这个不完整的示例:

template<class T1, class T2>
void ft(T1 x, T2 y)
{
	...
	?type? xpy = x + y;  // ?type? 不知道类型的写法
	...

}

  xpy应为什么类型呢?由于不知道 ft()将如何使用,因此无法预先知道这一点。正确的类型可能是 T1、T2 或其他类型。例如,T1可能是 double,而 T2 可能是 int,在这种情况下,两个变量的和将为 double 类型。 T1 可能是 short,而 T2 可能是 int,在这种情况下,两个变量的和为 int 类型。T1 还可能是 short,而 T2 可能是 char,在这种情况下,加法运算将导致自动整形提升,因此结果类型为 int。另外,结构和类可能重载运算符 +,这导致问题更加复杂。因此 C++98中,没有办法声明 xpy的类型。

  2. 关键字 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():

template<class T1class T2>
void ft(T1 x, T2 y)
{
	...
	decltype(x + y) xpy = x + y;
	...
}

  decltype 比这些示例演示的要复杂些。为确定类型,编译器必须遍历一个核对表。假设有如下声明:

delctype(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 *

  第二步:如果 expressiion 是一个函数调用,则 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 XX < ()不影响使用 XX

  第四步:如果前面的条件都不满足,则 var 的类型与 expression 的类型相同:

int j = 3;
int &k = j;
int &n = j;
decltype (j + 6) il;		// il type int
decltype(100L) i2;			// i2 type long
decltype(k + N) i3; 		// i3 type int;

  请注意,虽然 k 和 n 都是引用,但表达式 k + n 不是引用;它是两个 int 的和,因此类型是 int。
  如果需要多次声明,可结合使用 typedef 和decltype:

templte<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
	...	
}

在这里插入图片描述
  3. 另一种函数声明语法(C++ 后置返回类型)
  有一个相关的问题是 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 bldy */};

  通过结合使用这种语法和 decltpye,便可给 gt()指定返回类型,如下所示:

template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{
	...
	return x + y;
}

  现在, decltype 在参数声明后面,因此 x 与 y位于作用域内,可以使用它们。

8.6 总结

  C++ 扩展了 C 语言的函数功能。通过将 inline 关键字用于函数定义,并在首次调用该函数前提供基函数定义,可以使得 C++ 编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式。
  引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如 ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。
  C++原型让你能够定义参数的默认值。如果函数用省略了相应的参数,则程序将使用默认值;如果函数调用提供了函数值,则程序将使用这个值(而不是默认值)。只能在参数列中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。
  函数的特征标是其参数列表。程序员可以定义两个同名的函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
  函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。

  题外语,这一章终于学习完了,从开始到现在经历了半年之久,其中经历了全世界最大的疫情 ---- 新冠病毒。其实期间自己也是断断续续的学习,还真怕自己会断掉,每次有空做别的或是玩的时候都会想到要继续,好多次都没有做到,不管如何,坚持,坚持,再坚持吧。
在这里插入图片描述

最后

以上就是飞快麦片为你收集整理的C++学习之路(七),C++primer plus 第八章 函数探幽8.1 C++ 内联函数8.2 引用变量8.3 默认参数函数重载函数模板8.6 总结的全部内容,希望文章能够帮你解决C++学习之路(七),C++primer plus 第八章 函数探幽8.1 C++ 内联函数8.2 引用变量8.3 默认参数函数重载函数模板8.6 总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(51)

评论列表共有 0 条评论

立即
投稿
返回
顶部