54.函数传递参数的三种方式
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//值传递
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
//指针传递
void swap2(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//引用传递
void swap3(int &a, int &b)//int &a=a,int &b=b;
{
int tmp = a;
a = b;
b = tmp;
}
//打印
void myprint(int &a,int &b)
{
cout << "a=" << a << " b=" << b << endl;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
myprint(a, b);//10,20
swap2(&a, &b);
myprint(a, b);//20,10
a = 10;
b = 20;
swap3(a, b);
myprint(a, b);//20,10
system("pause");
return EXIT_SUCCESS;
}
1.传引用参数
1.1使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类 型的对象。
举个例子,我们准备编写一个函数比较两个string对象的长度。因为string对象 可能会非常长,所以应该尽址避免直接拷贝它们,这时使用引用形参是比较明智的选择。又因为比较长度无须改变string对象的内容,所以把形参定义成对常量的引用(参见 2.4.1节,第54页):
//比较两个string对象的长度
bool isShorter(const string &sl, const string &s2)
{
return s1.size() < s2.size();
}
如6.2.3节(第191页)将要介绍的,当函数无须修改引用形参的值时最好使用常量引用。
1.2使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。
该如何定义函数使得它能够既返回位置也返回出现次数呢?一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:
//返回s中c笫一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size_type find_char(const string &s, char c, string::size_type &occurs)
{
auto ret = s.size();//笫一次出现的位置(如果有的话)
occurs = 0;//设咒表示出现次数的形参的值
for (decltype(ret) i = 0; i != s.size(); ++i)
{
if(s[i] == c)
{
if (ret == s.size())
ret = i;//记录c第一次出现的位置
++occurs;//将出现的次数加1
}
}
return ret;//出现次数通过occurs隐式地返回
}
当我们调用find_char函数时,必须传入三个实参:作为查找范围的一个string对象、要找的字符以及一个用于保存字符出现次数的size_type(参见3.2.2节,第79页)对象。假设s是 个string对象,ctr是一个size_type对象, 则我们通过如下形式调用find_char函数:
auto index = find_char(s, 'o', ctr);
调用完成后,如果string对象中确实存在O,那么ctr的值就是o出现的次数,index 指向o第一次出现的位置:否则如果string对象中没有o,index等于s.size()而 Crr等于0。
2.const 形参和实参
当形参是const时,必须要注意2.4.3节(第57页)关于顶层const的讨论。 如前所述,顶层 const 作用于对象本身:
const int ci = 42;//不能改变ci,const是顶层的
int i= ci;//正确:当拷贝ci时,忽略了它的顶层const
int * const p = &i;//const是顶层的,不能给p赋值
*p = 0;//正确:通过p改变对象的内容是允许的,现在i变成了0
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层 const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const 时,传给它常量对象或者非常量对象都是可以的:
void fcn(const int i) {/* fcn能够读取i,但是不能向i写值*/}
调用 fcn 函数时,既可以传入 const int 也可以传入 int。忽略掉形参的顶层 const 可能产生意想不到的结果:
void fcn(const int i) {/* fcn能够读取i,但是不能向i写值*/}
void fcn(int i) {/* ... */}//错误:重复定义了 fcn(int)
调用 fen 函数时,既可以传入 const int 也可以传入 int。忽略掉形参的顶层 const 可能产生意想不到的结果:
在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层 const 被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
2.1指针或引用形参与 const
形参的初始化方式和变量的初始化方式是一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
int i= 42;
const int *cp = &i;//正确:但是cp不能改变 i(参见2.4.2节,第56页)
const int &r = i;//正确:但是r不能改变i(参见2.4.1节,第55页)
const int &r2 = 42;//正确:但是r不能改变i(参见2.4.1节,第55页)
int *p = cp;//错误:p的类型和cp的类型不匹配(参见2.4.2节,第56页)
int &r3 = r;//错误:r3的类型和r的类型不匹配(参见2.4.1节,第55页)
int &r4 = 42;//错误:不能用字面值初始化一个非常量引用(参见2.3.1节,第45页)
将同样的初始化规则应用到参数传递上可得如下形式:
void reset(int &i)
{
i = 0;
}
int i= O;
const int ci = i;
string::size_type ctr = 0;
reset(&i);//调用形参类型是int*的reset函数
reset(&ci);//错误:不能用指向const int对象的指针初始化int *
reset(i);//调用形参类型是int &的reset函数
reset(ci);//错误:不能把昔通引用绑定到const对象ci上
reset(42);//错误:不能把普通应用绑定到字面值上
reset(ctr);//错误:类型不匹配,ctr是无符号类型
//正确:find_char的第一个形参是对常量的引用
find_char("Hello World!", 'o', ctr);
要想调用引用版本的reset(参见 6.2.2 节,第 189 页),只能使用int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。类似的,要想调用指针版本的 reset(参见 6.2.1节,第 188 页)只能使用 int *。
另一方面,我们能传递一个字符串字面值作为find_char(参见6.2.2节,第189页)的第一个实参,这是因为该函数的引用形参是常量引用,而C++允许我们用字面值初始化常量引用。
2.2尽量使用常量引用
把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
这种错误绝不像看起来那么简单,它可能造成出人意料的后果。以6.2.2节(第189页)的find_char函数为例,那个函数(正确地)将它的string类型的形参定义成常量引用。假如我们把它定义成普通的string &:
//不良设计:第一个形参的类型应该是const string &
string::size_type find_char(string &s, char c, string::size_type &occurs);
则只能将find_char函数作用于string对象。类似下面这样的调用
find_char("Hello World", 'o', ctr);
将在编译时发生错误。
还有一个更难察觉的问题,假如其他函数(正确地)将它们的形参定义成常量引用,那么第二个版本的find_char无法在此类函数中正常使用。举个例子,我们希望在一个判断string对象是否是句子的函数中使用find_char:
bool is_sentence(const string &s)
{
//如果在s的末尾有且只有一个句号,则s是一个句子
string::size_type ctr= O;
return find char(s,'.', ctr) == s.size() - 1 && ctr == 1;
}
如果find_char 的第一个形参类型是string &,那么上面这条调用 find_char的语句 将在编译时发生错误。原因在于s是常量引用,但find_char被(不正确地)定义成只能接受普通引用。
解决该问题的一种思路是修改is_sentence的形参类型,但是这么做只不过转移了错误而已,结果是is_sentence函数的调用者只能接受非常量string对象了。
正确的修改思路是改正find_char函数的形参。如果实在不能修改find_char,就在is_sentence内部定义 一个string类型的变量,令其为s的副本,然后把这个string对象传递给find_char。
3.数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组(参见3.5.1节,第102页)以及使用数组时(通常)会将其转换成指针(参见3.5.3节,第105贞)。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递个数组时,实际上传递的是指向数组首元素的指针。尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
//尽管形式不同,但这三个print函数是等价的
//每个函数都有一个const int *类型的形参
void print(const int*);
void print(const int[]);//可以看出来函数的意图是作用于一个数组
void print(const int[10]);//这里的维度表示我们期望数组含有多少元素,实际不一定
尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是const int* 类型的。当编译器处理对print函数的调用时,只检查传入的参数是否是const int* 类型:
int i= 0, j[2] = {0, 1};
print(&i);//正确:&i的类型是int*
print(j);
如果我们传给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
警告
和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
3.1使用标记指定数组长度
管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串(参见3.5.4节,第109页)。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止:
void print(const char *cp)
{
if(cp)//若cp不是一个空指针
while(*cp)//只要指针所指的字符不是空字符
cout << *cp++;//输出当前字符并将指针向前移动一个位置
}
这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像int这样所有取值都是合法值的数据就不太有效了。
3.2使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发,关于其细节将在第Ⅱ部分详细介绍。使用该方法,我们可以按照如下形式输出元素内容:
void print(const int *beg, const int *end)
{
//输出beg到end之间(不含end)的所有元素
while(beg ! = end)
cout << *beg++ << endl;//输出当前元素并将指针向前移动一个位置
}
while循环使用解引用运算符和后置递减运算符(参见4.5节,第131页)输出当前元素并在数组内将beg向前移动一个元素,当beg和end相等时结束循环。
为了调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置:
int j[2] = {0, 1};
//j转换成指向它首元素的指针
//第二个实参是指向j的尾后元素的指针
print(begin(j),end(j));// begin和end函数,参见笫3.5.3节(106页)
只要调用者能正确地计算指针所指的位置,那么上述代码就是安全的。在这里,我们使用标准库begin和end函数(参见3.5.3节,第106页)提供所需的指针。
3.3显式传递一个表示数组大小的形参
第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。使用该方法,可以将print函数重写成如下形式:
//const int ia[]等价于const int *ia
//size表示数组的大小,将它显式地传给函数用于控制对ia元素的访问
void print(const int ia[], size_t size)
{
for(size_t i = 0; i != size; ++i)
{
cout << ia[i] << endl;
}
}
这个版本的程序通过形参size的值确定要输出多少个元素,调用print函数时必须传入这个表示数组大小的值:
int j [] = { 0, 1 } ;//大小为2的整型数组
print(j, end(j) - begin(j));
只要传递给函数的size值不超过数组实际的大小,函数就是安全的。
3.4数组形参和const
我们的三个print函数都把数组形参定义成了指向const的指针,6.2.3节(第191 页)关于引用的讨论同样适用于指针。当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针(参见2.4.2节,第56页)。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
3.5数组引用形参
C++语言允许将变量定义成数组的引用(参见3.5.1节,第101页),基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)(10))
{
for (auto elem : arr)
cout <<elem<< endl;
}
我们的三个print函数都把数组形参定义成了指向const的指针,6.2.3节(第191 页)关于引用的讨论同样适用于指针。当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针(参见2.4.2节,第56页)。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
&arr两端的括号必不可少(参见1.5.1节,第101页):
f(int &arr [10]) //错误:将arr声明成了引用的数组
f(int (&arr) [10]) //正确:arr是具有10个整数的整型数组的引用
因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print函数的可用性,我们只能将函数作用于大小为10的数组:
int i= 0, j[2] = {o, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i);//错误:实参不是含有10个整数的数组
print(j);//错误:实参不是含有10个整数的数组
print(k);//正确:实参是含有10个整数的数组
16.1.1节(第578页)将要介绍我们应该如何编写这个函数,使其可以给引用类型的形参传递任意大小的数组。
3.6传递多维数组
在C++语言中实际上没有真正的多维数组(参见3.6节,第112页), 所谓多维数组其实是数组的数组。
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针(参见3.6节,第115页)。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:
//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print (int (*matrix)[10], int rowSize) { /*... */ }
上述语句将matrix声明成指向含有10个整数的数组的指针。
注意:再一次强调,*matix两端的括号必不可少。
int *matrix[10];//10个指针构成的数组
int (*matrix)[10];//指向含有10个整数的数组的指针
我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内:
//等价定义
void print(int matrix[][10], int rowSize) { /*... */ }
matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。
4.main:处理命令行选项
main函数是演示C开程序如何向函数传递数组的好例子。 到目前为止, 我们定义的 main函数都只有空形参列表:
int main() {... }
然而, 有时我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:
prog -d -o ofile data0
这些命令行选项通过两个(可选的)形参传递给main函数:
int main(int argc, char *argv []) {... }
第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc 表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:
int main (int argc, char **argv) {... }
其中argv指向char *。
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
以上面提供的命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:
argv [O] = "prog";//或者argv[O]也可以指向一个空字符串
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5J = 0;
5.含有可变形参的函数
有时我们无法提前预知应该向函数传递几个实参。例如,我们想要编写代码输出程序产生的错误信息,此时最好用同一个函数实现该项功能,以便对所有错误的处理能够整齐划一。然而,错误信息的种类不同,所以调用错误输出函数时传递的实参也各不相同。
为了编写能处理不同数噩实参的函数,C++11新标准提供了两种主要的方法:如果所 有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于它的细节将在16.4节(第618页)介绍。
C++还有一种特殊的形参类型(即省略符),可以用它传递可变数组的实参。不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。
5.1initializer _list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用 initializer_list类型的形参。initializer_lis是一种标准库类型,用于表示某种特定类型的值的数组(参见3.5节,第101页)。initializer_lis类型定义在同名的头文件中,它提供的操作如表6.1所示。
表 6.1: initializer_list 提供的操作 | |
---|---|
initializer_list<T> lst | 默认初始化; T类型元素的空列表 |
initializer_list<T> lst{a,b,c... }; | lst的元素数益和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const |
lst2(lst) | 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 |
lst2 = lst | |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向1st中首元素的指针 |
lst.end() | 返回指向lst中尾元素下一位置的指针 |
和vector一样,initializer_list也是一种模板类型(参见 3.3 节, 第 86 页)。定义initializer_list对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls; //initializer_list的元素类型是string
initializer_list<int> li;//initializer_list的元素类型是 int
和 vector 不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
我们使用如下的形式编写输出错误信息的函数, 使其可以作用于可变数量的实参:
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg!= il.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
作用于initializer_list对象的 begin 和 end 操作类似于 vector 对应的成员(参见 3.4.1 节, 第 195 页)。 begin( )成员提供一个指向列表首元素的指针, end()成员提供一个指向列表尾后元素的指针。 我们的函数首先初始化beg令其表示首元素, 然后依次遍历列表中的每个元素。在循环体中,解引用 beg 以访问当前元素并输出它的值。
如果想向作用于initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:
//expected和actual是string对象
if (expected != actual)
error_msg({"functionX", expected, actual});
else
error_msg ({ "functionX", "okay"});
在上面的代码中我们调用了同一个函数 error_msg,但是两次调用传递的参数数量不同:次调用传入了三个值,第二次调用只传入了两个。
含有 initializer_list 形参的函数也可以同时拥有其他形参。例如,调试系统可能有个名为 ErrCode 的类用来表示不同类型的错误,因此我们可以改写之前的程序,使其包含一个initializer_list形参和一个 ErrCode 形参:
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << ": ";
for (const auto &elem : il)
cout << elem << " ";
cout << endl;
}
因为initializer_list包含begin和end成员,所以我们可以使用范围for循环(参见5.4.3节, 第167页)处理其中的元素。 和之前的版本类似,这段程序遍历传给旦形参的列表值,每次迭代时访问一个元素。
为了调用这个版本的error_msg函数, 品要额外传递一个ErrCode 实参:
if(expected != actual)
error_msg(ErrCode (42), { "functionX", expected, actual});
else
error_msg(ErrCode (0), {"functionX", "okay"});
5.2省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs。
警告:
省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
void foo (parm_list, ...);
void foo (...) ;
第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。 省略符形参所对应的实参无须类型检查。 在第一种形式中, 形参声明后面的逗号是可选的。
参考资料
参考资料来源于黑马程序员、C++ Primer等