第8章-cpp函数探幽

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

编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数的地址),并在函数结束时返回。下面更详细地介绍这一过程的典型实现。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。

C++内联函数提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本(参见下图)。

应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。

内联函数

#include <iostream>
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;
}

在函数声明或定义前加上关键字inline即变成内联函数。

通常的做法是省略原型,将整个定义放在本就提供原型的地方。注:只要是在main()前面定义的函数都是不需要写函数原型的!

程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。

在C中,可以编写宏来完成相似的功能:#define SQUARE(X) ((X)*(X)),但这只是替换,宏也不能按值传递参数。这使得C++的内联功能远远胜过C语言的宏定义。如果使用C语言的宏执行了类似函数的功能,应考虑将它们转换为C++内联。

引用变量

int rats;
int& rodents = rats; // 使rodents成为rats的别名
#include <iostream>
int main() {
	using namespace std;
	int rats = 101;
	int& rodents = rats;   // rodents is a reference

	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;
	rodents++;
	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;

	cout << "rats address = " << &rats;
	cout << ", rodents address = " << &rodents << endl;
	return 0;
}
rats = 101, rodents = 101
rats = 102, rodents = 102
rats address = 00A5F8C4, rodents address = 00A5F8C4

关于指针与引用:

int rats = 101;
int& rodents = rats;
int* prats = &rats;
————————————————————
rodents<=>*prats<=>rats
&rodents<=>prats<=>&rats

这样,rodents和*prats都可以同rats互换,&rodents和prats都可以同&rats互换。从这一点来说,引用看上去很像伪装表示的指针。

在声明引用时,必须在声明引用时将其初始化,而不能像指针那样先声明再赋值

int rat;
int& rodent;
rodent = rat; // 不允许

引用更接近指针常量,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它(不易主)。——地址与地址上存的值总相同。

也就是说:

int & rodents = rats;
实际上是下述代码的伪装表示:
int * const pr = &rats;
但由于是指针常量,所以引用可以再赋值,但【一直】效忠于rats变量,不易主!
#include <iostream>
int main() {
	using namespace std;
	int origin = 100;
	int& refers = origin; // refers is a reference

	cout << "origin = " << origin << ", origin_addr = " << &origin << endl;
	cout << "refers = " << refers << ", refers_addr = " << &refers << endl;

	int* const points = &origin;
	cout << "points = " << *points << ", points_addr = " << points << endl;

	int other1 = 300;
	refers = other1; // change the reference

	int other2 = 500; // change again
	refers = other2;
	cout << "——————————————————" << endl;
	cout << "other1 = " << other1 << ", other1_addr = " << &other1 << endl;
	cout << "other2 = " << other2 << ", other2_addr = " << &other2 << endl;
	cout << "refers = " << refers << ", refers_addr = " << &refers << endl;
	cout << "origin = " << origin << ", origin_addr = " << &origin << endl;
	cout << "——————————————————" << endl;
	*points = 700; // change value through pointer
	cout << "refers = " << refers << ", refers_addr = " << &refers << endl;
	cout << "origin = " << origin << ", origin_addr = " << &origin << endl;
	origin = 900; // change value directly by original data
	cout << "——————————————————" << endl;
	cout << "refers = " << refers << ", refers_addr = " << &refers << endl;
	cout << "origin = " << origin << ", origin_addr = " << &origin;
	return 0;
}
origin = 100, origin_addr = 007BFE68
refers = 100, refers_addr = 007BFE68
points = 100, points_addr = 007BFE68
——————————————————
other1 = 300, other1_addr = 007BFE44
other2 = 500, other2_addr = 007BFE38
refers = 500, refers_addr = 007BFE68
origin = 500, origin_addr = 007BFE68
——————————————————
refers = 700, refers_addr = 007BFE68
origin = 700, origin_addr = 007BFE68
——————————————————
refers = 900, refers_addr = 007BFE68
origin = 900, origin_addr = 007BFE68

最初,rodents引用的是rats,但随后程序试图将refers作为other1、other2的引用,最后通过指针和直接改变原值数据,发现refers与origin的值和地址一直都是相同的。对于引用赋值意味着“将变量other1/other2的值赋给origin变量”。简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置

假设程序员试图这样做:

int rats = 101;
int* pt = &rats;
int& rodents = *pt; // 一直效忠rats
int bunnies = 50;
pt = &bunnies;

将rodents初始化为*pt使得rodents指向rats。接下来将pt改为指向bunnies,并不能改变这样的事实,即rodents引用的是rats。

下图直观演示了按值传递与按引用传递的区别:

我们用三种方式,交互两个int值:

#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 variables
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;
	// cin.get();
	return 0;
}

void swapr(int& a, int& b) {
	int temp;
	temp = a;
	a = b;
	b = temp;
}
void swapp(int* p, int* q) {
	int temp;
	temp = *p; // use *p, *q for values of variables
	*p = *q;
	*q = temp;
}
void swapv(int a, int b) {
	int temp;
	temp = a;
	a = b;
	b = temp;
}
wallet1 = $300 wallet2 = $350            <<原始数据
Using references to swap contents:       
wallet1 = $350 wallet2 = $300            <<值被交换了
Using pointers to swap contents again:
wallet1 = $300 wallet2 = $350            <<值又被交换了
Trying to use passing by value:
wallet1 = $300 wallet2 = $350            <<交换失败
#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;
}
27 = cube of 3
27 = cube of 27

即:变量x传入refcube函数的第一步就是赋值ra = x,然后才是值的运算操作,所以引用ra早已经效忠于x变量了。

refcube( )函数修改了main( )中的x值,而cube( )没有,这提醒我们为何通常按值传递。变量a位于cube( )中,它被初始化为x的值,但修改a并不会影响x。但由于refcube( )使用了引用参数,因此修改ra实际上就是修改x。如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。例如,在这个例子中,应在函数原型和函数头中使用const:

double refcube(const double &ra);

如果这样做,当编译器发现代码修改了ra的值时,将生成错误消息。

顺便说一句,如果要编写类似于上述示例的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。当数据比较大(如结构和类)时,引用参数将很有用。

将引用用于结构

#include <iostream>
using namespace std;
struct sysop {
	char name[26];
	char quote[64];
	int used;
};
const sysop& use(sysop& sysopref);
int main() {
	// NOTE: some implementations require using the keyword static
	// in the two structure declarations to enable initialization
	sysop looper = {
		"Rick \"Fortran\" Looper",
		"I'm a goto kind of guy.",
		0
	};

	use(looper);
	cout << "Looper: " << looper.used << " use(s)\n";
	sysop copycat;
	copycat = use(looper);
	cout << "Looper: " << looper.used << " use(s)\n";
	cout << "Copycat: " << copycat.used << " use(s)\n";
	cout << "use(looper): " << use(looper).used << " use(s)\n";
	return 0;
}

const sysop& use(sysop& sysopref) {
	cout << sysopref.name << " says:\n";
	cout << sysopref.quote << endl;
	sysopref.used++;
	return sysopref;
}
Rick "Fortran" Looper says:
I'm a goto kind of guy.
Looper: 1 use(s)
Rick "Fortran" Looper says:
I'm a goto kind of guy.
Looper: 2 use(s)
Copycat: 2 use(s)
Rick "Fortran" Looper says:
I'm a goto kind of guy.
use(looper): 3 use(s)

引用是可以赋值给变量的:

#include <iostream>
using namespace std;
int main() {
	int x = 6;
	int& y = x;
	int& z = y;
	y = 9;
	cout << x << "," << y << "," << z << endl;
	z = 10;
	cout << x << "," << y << "," << z << endl;
	return 0;
}
############输出###############
9,9,9
10,10,10

y绑定x、z绑定y,这样就相当于y、z都绑定x了。y、z成了x的别名。


#include <iostream>
#include <string>
using namespace std;
struct free_throws {
	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() {
	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 };
	free_throws dup;
	set_pc(one);
	display(one); // display:1th
	accumulate(team, one);
	display(team); // display:2th
	display(accumulate(team, two)); // display:3th
	accumulate(accumulate(team, three), four);
	display(team); // display:4th
	dup = accumulate(team, five); // use return value in assignment
	cout << "Displaying team:\n";
	display(team); // display:5th
	cout << "Displaying dup after assignment:\n";
	display(dup); // display:6th
	set_pc(four);
	accumulate(dup, five) = four; // 如果函数accumulate()按值返回,这条语句将不能通过编译
	cout << "Displaying dup after ill-advised assignment:\n";
	display(dup); // display:7th
	return 0;
}
void display(const free_throws& ft) {
	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;
}

Name: Ifelsa Branch
  Made: 13      Attempts: 14    Percent: 92.8571
Name: Throwgoods
  Made: 13      Attempts: 14    Percent: 92.8571
Name: Throwgoods
  Made: 23      Attempts: 30    Percent: 76.6667
Name: Throwgoods
  Made: 35      Attempts: 48    Percent: 72.9167
Displaying team:
Name: Throwgoods
  Made: 41      Attempts: 62    Percent: 66.129
Displaying dup after assignment:
Name: Throwgoods
  Made: 41      Attempts: 62    Percent: 66.129
Displaying dup after ill-advised assignment:
Name: Whily Looper
  Made: 5       Attempts: 9     Percent: 55.5556

为何要返回引用

计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。请看下面的代码:

double m = sqrt(16.0);
cout << sqrt(25.0);

在第一条语句中,值4.0被复制到一个临时位置然后被复制给m。在第二条语句中,值5.0被复制到一个临时位置,然后被传递给cout。

现在来看下面的语句:

dup = accumalate(team, five);

如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,将直接把team复制到dup,其效率更高。

注:返回引用的函数实际上是被引用的变量的别名

int x = 6;
int& y = x;
y=9 <=> x=9 ← 实际上y是x的别名

返回引用时需要注意的问题:

返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。您应避免编写下面这样的代码:

const free_throws & clone2(free_throwns & ft) {
    free_throwns newguy; // first step to big error
    newguy = ft; // copy info
    return newguy; // return reference to copy
}

该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不再存在。同样,也应避免返回指向临时变量的指针。

为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。上面程序清单中的accumulate()正是这样做的。

下面的一个简单例子来看不注意上面要避免的内容的后果:

#include <iostream>
using namespace std;
int& test(int&);
int& testok(int&);
int main() {
	int x = 6;
	int& y = test(test(x));// test()函数运行完后返回的临时引用消失!因为临时引用无变量承载!
	/*  替换为下面现行打印正确
		int x1 = test(x);// x1承载临时引用!
		y = test(x1);
	*/
	cout << y << endl;

	int& z = testok(testok(x));// 返回的引用由变量x承载。
	cout << z << endl;
}
int& test(int& i) {
	int tmp = 2 * i;
	return tmp;
}
int& testok(int& i) {
	i = 2 * i;
	return i;
}
-1717986920
24

在VS2019下替换上一行为下面注释的那两行,没有出错。但嵌套使用出错。在多数IDE及系统下用注释的两行运行会报错比如CLion、CodeLite、EclipseC++、CodeBlocks、VSCode,如下是它们基本类似的报错:

VSCode:
hIn function ‘int& test(int&)’:
17:6: warning: reference to local variable ‘tmp’ returned [-Wreturn-local-addr]
  int tmp = 2 * i;
      ^~~
Segmentation fault

为什么在VS2019下打印出的值为-1717986920呢?因为当定义一个int型变量时,有些系统会自动给该int型变量赋值于-858993460,如:

#include <stdio.h>
void main() {
    int a; //没有给int型变量赋予初道值
    printf("%d\n",a);
}
######有些系统报错,有些输出######
-858993460

test(x)返回一个引用,但函数运行完毕此引用不再存在,直接使用,则相当于使用一个未赋初值的变量,而 -1717986920 = 2 × -858993460 也正说明了这一点。事实上,运行如下程序的结果和原程序一致,也更证实了这一点:

int t;
int& m = test(t);
cout << m; // -1717986920

赋值给x1可以,这是函数正常的行为,返回的值有了着落,临时的引用已经无效不存在了。不经过赋值给x1的操作就相当于返回的值没有被保存在某个变量里面,那就直接不存在了!所以test(test(x))里层的test(x)没被变量接收,即它就是一个不存在的,和如上的代码性质一模一样了。对于testok(),传入进的变量已经和参数绑定,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。


另一种方法是用new来分配新的存储空间。前面见过这样的函数,它使用new为字符串分配内存空间,并返回指向该内存空间的指针。下面是使用引用来完成类似工作的方法:

const free_throws & clone(free_thrown & ft) {
    free_throws * pt;
    *pt = ft; // 赋值操作
    return *pt;
}

第一条语句创建一个无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构。上述代码似乎会返回该结构,但函数声明表明,该函数实际上将返回这个结构的引用。这样,便可以这样使用该函数:

free_throwns & jolly = clone(three);

这使得jolly成为新结构的引用。这种方法存在一个问题:在不再需要new分配的内存时,应使用delete来释放它们。调用clone()隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。

为何将const用于引用返回类型

accumlate(dup, five) = four;

其效果如下:首先将five的数据添加到dup中,再使用four的内容覆盖dup的内容。这条语句为何能够通过编译呢?在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向dup的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。

另一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。其他右值包括字面值(如10.0)和表达式(如x + y)。显然,获取字面值(如10.0)的地址没有意义,但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。

假设您要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,只需将返回类型声明为const引用

const free_throwns & accumlate(free_throwns & target, const free_throwns & source);

现在返回类型为const,是不可修改的左值,所以上面的赋值语句不合法。如果省略const,可以编写更简短的代码,但其含义也更糊糊。通常,就避免在设计中添加模糊的特性,因为模糊特性增加了犯错的机会。当然,有时省略const确实有道理,在第11章讨论的重载运算符<<就是一个这样的例子。(常量引用和常量指针是一样的,即引用的值/指向的值不能修改,引用/指向可修改)

将引用用于类对象

将类对象传递给函数时,C++通常的做法是使用引用。例如,可以通过使用引用,让函数将类string、ostream、istream、ofstream和ifstream等类的对象作为参数。

#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 original string: " << input << endl;

	cout << "Resetting 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;
	return s1; // safe to return reference passed to function
}
const string& version3(string& s1, const string& s2) { // bad design
	string temp;
	temp = s2 + s1 + s2;
	return temp; // unsafe to return reference to local variable
}
Enter a string: |It's not my fault.
Your string as entered: It's not my fault.
Your string enhanced: ***It's not my fault.***
Your original string: It's not my fault.
Your string enhanced: ###It's not my fault.###
Your original string: ###It's not my fault.###
Resetting original string.
此时,该程序已经崩溃

这次输入:abcdefg,则局部变量temp = "@@@abcdefg@@@",有15个字符,函数version3()调用完后,续续调试跟踪:

(1)

(2)    

(3)    (4)

通过最终抛出的异常throw std::bad_alloc();就可以知道是内存方面出错。

我们通过打印version3()函数就可更明确知道原因:(往往很多时候,程序直接给出的错误提示难理解。)

违规访问读取的地址(内存空间的误读)!而赋值给result就是一种访问,从而出错。

写一个简版直击问题的程序:

#include <iostream>
#include <string>
using namespace std;
string& version3(string& s1, string& s2);
int main() {
	string input = "abc";
	string add = "@@@";
	string result = version3(input, add);
	//cout << version3(input, add);
	return 0;
}
string& version3(string& s1, string& s2) {
	string temp;
	temp = s2 + s1 + s2;
                  // 打印临时字符串地址,打印是正常值,如:012FF600
	cout << &temp;// 返回值也是这个地址,但地址所存内容已被释放,所以返回的是个无意义且有问题的地址
	return temp;
}
异常: std::bad_alloc,位于内存位置 0x00BBF00C 处。
——————————————如果解注释,并注释掉其上一行———————————————
引发的异常: 0xC0000005: 读取位置 0xCCCCCCCC 时发生访问冲突。

返回值引用类型时,会返回该变量的地址。当返回值不是引用型时,编译器会专门给返回值分配出一块临时内存。如果version3()返回类型不是引用类型,则临时的内存(当然有对应的地址)存储着返回值,可以赋值给变量,运行正常。而如果返回的是引用,则return temp;后函数结束,并没有分配给它临时内存,temp变量被回收不存在了,所以赋值给result一个野地址,给野地址赋值当然就有问题了。由于temp是临时变量,所以绑定到引用无效,导致段错误(Segmentation fault)。对于version2(),它返回的是传入的参数,调用函数的那个参数不是函数作用域的临时变量,所以不会被回收消失,所以version2()正常运行。负面影响就是参数关联到了返回值。version1()就不存在这个问题。总之,绝不能将一个指向局部变量的引用类型值作为函数的返回值!如果version3()函数返回string类型,则是可以的。因为如果返回的是地址,而地址指向的内存已释放;如果返回的不是地址,则不存在这个问题。

需要明白一件事情:临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是已经可以被分配给其他变量了,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了由此也可以推知,返回string类型是值的复制操作,可行;但返回string&类型是地址的复制操作,而地址里面的内容可能已被清空,所以就有bad_alloc的问题了。

C语言里规定:16位程序中,返回值保存在ax寄存器中;32位程序中,返回值保持在eax寄存器中;如果是64位返回值,edx寄存器保存高32bit,eax寄存器保存低32bit。

由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了。当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系。

如果我们需要返回值,一般使用赋值语句就可以了:A a = func();

综上,函数是可以将临时变量的值作为返回值的。

将一个指向局部变量的指针作为函数的返回值是有问题的!由于指针指向局部变量,因此在函数返回时,临时变量被销毁,指针指向一块无意义的地址空间,所以一般不会有返回值。如果得到正常的值,只能是幸运的,因为退出函数的时候,系统只是修改了栈顶的指针,并没有清内存;所以,是有可能正常访问到局部变量的内存的。但因为栈是系统自动管理的,所以该内存可能会可以被分配给其他函数,这样,该内存的内容就会被覆盖,不再是原来的值了。

顺便提一下string的最大长度为多少?如下代码可打印出它的最大容量:

#include <iostream>
#include <string>
#include <limits>
using namespace std;
int main() {
	cout << numeric_limits<string::size_type>::max() << endl;
}
输出为:
4294967295

string的最大长度为0xffffffff=4294967295,即maximum unsigned int value

程序怎么能够接受将char指针赋给string引用呢?

这里有两点需要说明。首先,string类定义了一种char *到string的转换功能,这使得可以使用C-风格字符串来初始化string对象。其次是本章前面讨论过的类型为const引用的形参的一个属性。假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。例如,在本章前面,将int实参传递给const double &形参时,就是以这种方式进行处理的。同样,也可以将实参char *或const char *传递给形参const string &。

这种属性的结果是,如果形参类型为const string &,在调用函数时,使用的实参可以是string对象或C-风格字符串,如用引号括起的字符串字面量、以空字符结尾的char数组或指向char的指针变量。因此,下面的代码是可行的:

函数原型:string version1(const string& s1, const string& s2);
result = version1(input, "abcdef");

函数version2()不创建临时变量,而是直接修改原来的string对象。不同于s2,s1没有被const修饰,所以可以修改s1,如下:

const string& version2(string& s1, const string& s2) {
	s1 = s2 + s1 + s2;
	return s1;
}

然而,由于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.###

因此,如果要保留原来的字符串不变,这将是一种错误设计。

对于version3()函数,它存在一个致命的缺陷:

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

返回一个指向version3()中声明的变量的引用。这个函数能够通过编译(但编译器会发出警告),但当程序试图执行该函数时将崩溃。具体地说,问题是由下面的赋值语句引发的:

result = version3(input, "@@@");

程序试图引用已经释放的内存。


ostream和ofstream类凸现了引用的一个有趣属性。正如第6章介绍的,ofstream对象可以使用ostream类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承,这将在第13章详细讨论。简单地说,ostream是基类(因为ofstream是建立在它的基础之上的),而ofstream是派生类(因为它是从ostream派生而来的)。派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性,如格式化方法precision()和setf()。

继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。例如,参数类型为ostream &的函数可以接受ostream对象(如cout)或你声明的ofstream对象作为参数。

该程序要求用户输入望远镜物镜和一些目镜的焦距,然后计算并显示每个目镜的放大倍数。放大倍数等于物镜的焦距除以目镜的焦距。

#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 << "Can't open " << fn << ". Bye.\n";
		exit(EXIT_FAILURE);
	}
	double objective; // 望远镜物镜
	cout << "Enter the focal length of your "
		"telescope objective in mm: ";
	cin >> objective;
	double eps[LIMIT]; // 目标焦距
	cout << "Enter the focal lengths, in mm, of " << LIMIT
		<< " eyepieces:\n";
	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 << "Done\n";
	return 0;
}

void file_it(ostream& os, double fo, const double fe[], int n) {
	ios_base::fmtflags initial;
	initial = os.setf(ios_base::fixed, ios_base::floatfield); // save initial formatting state
	std::streamsize sz = os.precision(0);
	os << "Focal length of objective: " << fo << " mm\n";
	os.precision(1);
	os.width(12);
	os << "f.l. eyepiece";
	os.width(15);
	os << "magnification" << endl;
	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, ios_base::floatfield); // restore initial formatting state
	os.precision(sz);
}

C++ I/O

<iostream>自动定义了一些标准对象:

  • cout:ostream类的一个对象,可以将数据显示在标准输出设备上.
  • cerr: ostream类的另一个对象,它无缓冲地向标准错误输出设备输出数据.
  • clog:类似cerr,但是它使用缓冲输出.
  • cin:istream类的一个对象,它用于从标准输入设备读取数据.

<fstream>允许编程人员利用ifstreamofstream类进行文件输入和输出.

一些C++ I/O流(精度,判断等)的行为可以通过操作不同的标志来修改。

C++标准程序库中类 ios_base 的用法

类层次结构如下:

ios_base - C++ Reference

对于该程序,最重要的一点是,参数os(其类型为ostream &)可以指向ostream对象(如cout),也可以指向ofstream对象(如fout)。该程序还演示了如何使用ostream类中的格式化方法。下面复习(介绍)其中的一些,更详细的讨论请参阅第17章。

方法setf()让您能够设置各种格式化状态。例如,方法调用setf(ios_base::fixed)将对象置于使用定点表示法的模式;setf(ios_base::showpoint)将对象置于显示小数点的模式,即使小数部分为零。方法precision( )指定显示多少位小数(假定对象处于定点模式下)。所有这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们。方法width( )设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零,这意味着刚好能容纳下要显示的内容。

方法setf( )返回调用它之前有效的所有格式化设置。ios_base::fmtflags是存储这种信息所需的数据类型名称。因此,将返回值赋给initial将存储调用file_it( )之前的格式化设置,然后便可以使用变量initial作为参数来调用setf( ),将所有的格式化设置恢复到原来的值。因此,该函数将对象回到传递给file_it( )之前的状态。

需要说明的最后一点是,每个对象都存储了自己的格式化设置。因此,当程序将cout传递给file_it( )时,cout的设置将被修改,然后被恢复;当程序将fout传递给file_it()时,fout的设置将被修改,然后被恢复。

通过void file_it(ostream& os, double fo, const double fe[], int n);第一个参数ostream可以接收它的子对象,所以这个函数参数可以是cout输出到屏幕,如果是ofstream则输出到文件。这里也体现了继承这种优势所在。

屏幕输出如下:

Enter the focal length of your telescope objective in mm: |1800
Enter the focal lengths, in mm, of 5 eyepieces:
Eyepiece #1: |39
Eyepiece #2: |19
Eyepiece #3: |24
Eyepiece #4: |8.8
Eyepiece #5: |7.4
Focal length of objective: 1800 mm
f.l. eyepiece  magnification
        39.0             46
        19.0             95
        24.0             75
         8.8            205
         7.4            243
Done

文件输出:


使用引用参数的主要原因有两个。

  • 程序员能够修改调用函数中的数据对象。
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:

对于使用传递的值而不作修改的函数。

  • 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
  • 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
  • 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数:

  • 如果数据对象是内置数据类型,则使用指针。如果看到诸如 fixit(&x) 这样的代码(其中x是int),则很明显,该函数将修改x。
  • 如果数据对象是数组,则只能使用指针。
  • 如果数据对象是结构,则使用引用或指针。
  • 如果数据对象是类对象,则使用引用。

默认参数

例如,函数调用left(“theory”, 3)将创建新字符串“the”,并返回一个指向该字符串的指针。现在假设第二个参数的默认值被设置为1,则函数调用left(“theory”, 3)仍像前面讲述的那样工作,3将覆盖默认值。但函数调用left(“theory”)不会出错,它认为第二个参数的值为1,并返回指向字符串“t”的指针。如果程序经常需要抽取一个字符组成的字符串,而偶尔需要抽取较长的字符串,则这种默认值将很有帮助。

如何设置默认值呢?必须通过函数原型。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。例如,left( )的原型如下:

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

对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值

int harpo(int n, int m = 4, int j = 5); // 有效
int chico(int n, int m = 6, int j); // 无效
int groucho(int k = 1, int m = 2, int n = 3); // 有效

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

beeps = harpo(2); // <=> harpo(2,4,5);
beeps = harpo(1, 8); // <=> harpo(1,8,5);
beeps = harpo(8, 7, 6) // 没有使用默认值

默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在设计类时您将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。

#include <iostream>
const int ArSize = 80;
char* left(const char* str, int n = 1);
int main() {
	using namespace std;
	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;
}

char* left(const char* str, int n) {
	int len = strlen(str);
	n = (n < len) ? n : len; // the lesser of n and len
	if (n < 0)
		n = 0;
	char* p = new char[n + 1];
	int i;
	for (i = 0; i < n && str[i]; i++) // 检查了str!='\0'的情况
		p[i] = str[i];  // copy characters
	p[i] = '\0';  // set rest of string to '\0'
	return p;
}
Enter a string:
|forthcoming
fort
f

可以看到,left(sample)不带参数也是可以的,就是默认带有函数原型里的默认参数n==1。

可以定义一组函数名都为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.0, 10);                        // use #2
print(1999, 12);                          // use #4
print(1999L, 15);                         // use #3

C++如何跟踪每一个重载函数呢?它给这些函数指定了秘密身份。使用C++开发工具中的编辑器编写和编译程序时,C++编译器将执行一些神奇的操作——名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。请看下述未经修饰的函数原型:

long MyFunctionFoo(int, float);

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

?MyFunctionFoo@@YAXH

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

函数模板:

现在的C++编译器实现了C++新增的一项特性——函数模板。函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。

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

在标准C++98添加关键字typename之前,C++使用关键字class来创建模板。也就是说,上面的“typename”可以用“class”关键字代替。

#include <iostream>
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";
	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 compiler-generated double swapper:\n";
	Swap(x, y);  // generates void Swap(double &, double &)
	cout << "Now x, y = " << x << ", " << y << ".\n";
	return 0;
}

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

并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。

#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] << "/";
	cout << a[2] << a[3] << "/";
	for (int i = 4; i < Lim; i++)
		cout << a[i];
	cout << endl;
}
i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
Original arrays:
07/04/1776
07/20/1969
Swapped arrays:
07/20/1969
07/04/1776

假设有如下模板函数:

template <class T>
void f(T a, T b) {
    ...
}

如果T为数组,a = b;将不成立。如果T为数组(名即为其指针)、指针或结构,T c = a*b;不成立。

总之,编写的模板函数很可能无法处理某些类型。另一方面,有时候通用化是有意义的,但C++语法不允许这样做。例如,将两个包含位置坐标的结构相加是有意义的,虽然没有为结构定义运算符+。一种解决方案是,C++允许你重载运算符+,以便能够将其用于特定的结构或类(运算符重载将在第11章讨论)。这样使用运算符+的模板便可处理重载了运算符+的结构。另一种解决方案是,为特定类型提供具体化的模板定义,下面就来介绍这种解决方案。

显示具体化

假设定义如下结构:

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

假设希望能够交换两个这种结构的内容:

temp = a;
a = b;
b = temp;

由于C++允许将一个结构赋给另一个结构,因此即使T是一个job结构,上述代码也适用。然而,假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,但Swap( )的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其他的代码。这种通用模板,遇到如T为job结构时,命名为t,但t.salary非法。只能完成简单的加减乘除赋值运算。

然而,可以提供一个具体化函数定义——称为显式具体化(explicit specialization),其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

具体化机制随着C++的演变而不断变化。下面介绍C++标准定义的形式。

第三代具体化(ISO/ANSI C++标准)

试验其他具体化方法后,C++98标准选择了下面的方法。

➀对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。

➁显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。

➂具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

下面是用于交换job结构的非模板函数、模板函数和具体化的原型:

void Swap(job &, job &); // 无模板的函数原型

template <typename T>
void Swap(T &, T &); // 有模板的函数原型

template <> void Swap<job>(job &, job &); // 为job类型显示具体化的模板函数原型

如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。

...
template <class T>
void Swap<T &, T &);
template <> void Swap<job>(job &, job &);
// 也可以简化成:template <> void Swap(job &, job &);
int main() {
    double u, v;
    ...
    Swap(u, v); // 使用模板
    job a, b;
    ...
    Swap(a, b); // 使用void Swap<job>(job &, job &)
}
#include <iostream>
template <typename T>
void Swap(T& a, T& b);

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

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);
	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 " << j.floor << endl;
}
i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
Before job swapping:
Susan Yaffee: $73000.60 on floor 7
Sidney Taffee: $78060.72 on floor 9
After job swapping:
Susan Yaffee: $78060.72 on floor 9
Sidney Taffee: $73000.60 on floor 7

为进一步了解模板,必须理解术语实例化和具体化。记住,在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。例如,上面程序清单中,函数调用Swap(i, j)导致编译器生成Swap()的一个实例,该实例使用int类型。模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用Swap()函数时提供了int参数。

最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化(explicit instantiation)。这意味着可以直接命令编译器创建特定的实例,如Swap<int>( )。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:

template void Swap<int>(int, int); // 显示实例化

实现了这种特性的编译器看到上述声明后,将使用Swap( )模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap()模板生成int类型的函数定义。”

与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:

template <> void Swap<int>(int &, int &);
template <> void Swap(int &, int &);

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

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

还可通过在程序中使用函数来创建显式实例化。

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; // 显示实例化

这里的模板与函数调用Add(x, m)不匹配,因为该模板要求两个函数参数的类型相同。但通过使用Add<double>(x, m),可强制为double类型实例化,并将参数m强制转换为double类型,以便与函数Add<double>(double, double)的第二个参数匹配。

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,因此我们先大致了解一下这个过程是如何进行的。

➊ 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。

➋ 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。

➌ 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

考虑只有一个函数参数的情况,如下调用:

may('B'); // 实际参数是个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和#7)不可行,因为整数类型不能被隐式地转换(即没有显式强制类型转换)为指针类型。剩余的一个模板可用来生成具体化,其中T被替换为char类型。这样剩下5个可行的函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。

接下来,编译器必须确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述。

1.完全匹配,但常规函数优先于模板。

2.提升转换(例如,char和shorts自动转换为int,float自动转换为double)。

3.标准转换(例如,int转换为char,long转换为double)。

4.用户定义的转换,如类声明中定义的转换。

例如,函数#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)意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配的(第7章介绍了函数指针以及为何可以将函数名作为参数传递给接受函数指针的函数)。

从实参到形参
TypeType &
Type &Type
Type[]* Type
Type (argument-list)Type(*)(argument-list)
Typeconst Type
Typevolatile Type
Type *const Type
Type *volatile Type *

假设有下面的函数代码:

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-(blot &)
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); // #1
template <> void recycle<blot>(blot & t);   // #2
...
blot ink = {25, "spots"};
...
recycle(ink); // 使用#2

术语“最具体(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); // 使用#2

recycle(&ink)调用与#1模板匹配,匹配时将Type解释为blot *。recycle(&ink)函数调用也与#2模板匹配,这次Type被解释为ink。因此将两个隐式实例——recycle<blot *>(blot *)和recycle<blot>(blot *)发送到可行函数池中。

在这两个模板函数中,recycle<blot *>(blot *)被认为是更具体的,因为在生成过程中,它需要进行的转换更少。也就是说,#2模板已经显式指出,函数参数是指向Type的指针,因此可以直接用blot标识Type;而#1模板将Type作为函数参数,因此Type必须被解释为指向blot的指针。也就是说,在#2模板中,Type已经被具体化为指针,因此说它“更具体”。

用于找出最具体的模板的规则被称为函数模板的部分排序规则(partial ordering rules)。和显式实例一样,这也是C++98新增的特性。

#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 pointers 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(pd, 3);      // uses template B (更具体)
	return 0;
}

template <typename T>
void ShowArray(T arr[], int n) {
	using namespace std;
	cout << "template A\n";
	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 B\n";
	for (int i = 0; i < n; i++)
		cout << *arr[i] << ' ';
	cout << endl;
}
Listing Mr. E's counts of things:
template A
13 31 103 301 310 130
Listing Mr. E's debts:
template B
2400 1300 1800

自己选择

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

#include <iostream>
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;
    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<int>(x, y)  << endl; // use #1 with int
    return 0;
}
20
15.5
-30
15

程序提供了一个模板和一个标准函数,其中模板返回两个值中较小的一个,而标准函数返回两个值中绝对值较小的那个。如果函数定义是在使用函数前提供的,它将充当函数原型,因此这个示例无需提供原型。lesser<>(m, n)中的<>指出,编译器应选择模板函数,而不是非模板函数;编译器注意到实参的类型为int,因此使用int替代T对模板进行实例化。

根据熟悉模板的程序员提供的反馈,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的类型。

关键字decltype(C++11

int x;
decltype(x) y; // 使y和x的类型相同
decltype(x + y) xpy; // 使xpy和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;
    ...
}

如果需要多次声明,可结合使用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是一个引用
    ...
}

后置返回类型(C++11)

有一个相关的问题是decltype本身无法解决的。请看下面这个不完整的模板函数:

template<class T1, class T2>
?type? gt(T1 x, T2 y) {
    ...
    return 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值