概述
**第 8 章 函数探幽**
本章内容包括:
内联函数。
引用变量。
如何按引用传递函数参数。
默认参数。
函数重载。
函数模板。
函数模板具体化。
通过第 7 章,了解到很多有关 C++ 函数的知识,但需要学习的知识还很多。C++ 还提供许多新的函数特性,使之有别于 C 语言。新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数。本章介绍的 C++ 语言基础上新增的特性,以前面各章都多,这是进入加加(++)领域重要一步。
8.1 C++ 内联函数
内联函数是 C++ 为提高程序运行速度所做的一项改进。常规函数和内联函数之间和主要区别不在于编写方式,而在于 C++ 编译器
如何将它们组合到程序中。要了解内联函数与常规函数之间的区别,必须深入到程序内部。
编译过程的最终产品是可执行程序------由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址,计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数也使程序跳到另一个地址(函数的地址),并在函数结束时返回。下面更详细地介绍这一过程的典型实现。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳加到地址被保存的指令处,(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
C++ 内联函数提供了另一种选择。内联函数的编码代码与其他程序代码“内联”起来了,也就是说,编译器将使用相应的函数代码远的函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代码是需要占用更多内存。如果程序在 10 个不同的地方调用同一个内联函数,则该程序将包
含该函数代码的 10 个副本(参见图 8.1)
应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
要使用这项特性。必须采取下述措施之一:
在函数声明前加上关键字 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 ++ ] = '