参数传递
前言
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名:也就是说,引用形参是它对应的实参别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。
一、传值参数
初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,变量的改变不会影响初始值:
int n = 0; //int类型的初始变量
int i = n; //i是n的值的副本
i = 42; //i的值改变;n的值不变
传值参数,函数对形参做的所有操作都不会影响实参:
#include<iostream>
void sum(int i)
{
int b = i--;
std::cout<<b<<std::endl;
}
尽管sum函数改变了i的值,但是这个改动不会影响传入sum的实参。调用sum(i)不会改变i的值。
二、指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
int n =0, i=42;
int *p =&n, *q=&i; //p指向n;q指向i
*p =42; //n的值改变;p不变
p=q; //p现在指向了i;但是i和n的值不变
指针形参的行为与之类似:
//该函数接受一个指针,然后将指针所指的位置为0
void reset(int *ip)
{
*ip=0; //改变指针ip所指对象的值
ip=0; //只改变了ip的局部拷贝,实参未被改变
}
调用reset函数之后,实参所指的对象被置为0,但是实参本身并没有改变:
int i = 42;
reset(&i); //改变i的值而非i的地址
cout<<"i="<<i<<endl; //输出i=0
三、传引用参数
引用的操作实际上是作用在引用所引的对象上:
int n =0,i=42;
int &r=n; //r绑定了n(即r是n的另一个名字)
r=42; //现在n的值是42
r=i; //现在n的值和i相同
i=r; //i的值和n相同
引用形参的行为与之类似。通过使用引用形参,允许函数改变一个或多个实参的值。
//改写上一个reset程序,使其接受的参数是引用类型而非指针
void reset(int &ip) //ip是传给reset函数的对象的另一个名字
{
ip=0; //改变了ip所引对象的值
}
和其他引用一样,引用形参绑定初始化它的对象。当调用reset的函数时,ip绑定我们给传函数的int对象,此时改变ip也就是改变ip所引对象的值。被改变的对象时传入reset的实参。
int j = 42;
reset(j); //j采用传引用方式,它的值被改变
cout<<"j="<<j<<endl; //输出j=0
形参ip仅仅是j的又一个名字。在reset内部队i的使用即是对j的使用。
1.使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有类类型根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
//编写一个函数比较两个string对象的长度
bool isShor(string &a,string &b)
{
return a.size()<b.size();
}
因为string对象可能会非常唱,所以应尽量避免直接拷贝它们,这时候引用形参是比较明智的选择。
ps:如果函数无须改变引用形参的值,最好将其声明为常量引用。
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()) //记录c第一次出现的位置
ret = i; //将出现的次数加1
++occurs;
}
}
return ret; //出现次数通过occurs隐式地返回
}
四、const形参和实参
形参是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时,传给它常量对象或者非常量对象都是可以的:
void fcn(const int i) {/*fcn能够读取i,但是不能向i写值*/}
1.尽量使用常量引用
把函数不会改变的形参定义成引用是一种比较常见的错误,会带给函数的调用者一种误导,即函数可以修改它的实参的值。此为,使用引用而非常量引用也会极大限制函数所能接受的实参类型。比如不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
五、数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有所影响,分别是:
1.不允许拷贝数组以及使用数组时(通常)会将其转换成指针。
2.因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数,因此数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
//尽管形式不同,但是这三个函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); //可以看出来,函数的意图是作用于一个数组
void print(const int[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定
当编译器处理对print函数的调用时,只检查传入的参数是否const int*类型:
int i=0,j[2]={0,1};
print(&i); //正确:&i的类型是int*
print(j); //正确:j转换成int*并指向j[0]
传给函数是一个数组,则实参自动转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
因为数组是以指针的形式传递给函数,所以一开始函数并不知道数组大小,我们可以提供一些额外的信息,管理指针形参有三种常用的技术。
1.使用标记指定数组长度
第一种方法:要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止:
void print(const char *cp)
{
if(cp) //若cp不是一个空指针
while(*cp) //只要指针所指的字符不是空字符
cout<<*cp++; //输出当前字符并指向指针向前移动一个位置
}
这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。
2.使用标准库规范
第二种方法:传递指向数组首元素和尾后元素的指针:
void print(const int *beg,const int *end)
{
//输出beg到end之间(不含end)的所有元素
while(beg!=end)
cout<<*bed++<<endl;
}
调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置:
int j[2] = {0,1};
//j转换成指向它首元素的指针
//第二个实参是指向j的尾后元素的指针
print(begin(j),end(j));
3.显示传递一个表示数组大小的形参
第三种方法:专门定义一个表示数组大小的形参,在C程序和过去的C++程序常常使用这种方法:
//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};
print(j,end(j)-begin(j));
4.数组引用参数
C++语音允许将变量定义成数组的引用,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10])
{
for(auto elem:arr)
cout<<elem<<endl;
}
&arr两端的括号必不可少:
f(int &arr[10]); //错误:将arr声明成了引用的数组
f(int (&arr)[10]); //正确:arr是具有10个整数的整数数组的引用