专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!
目录
- 6.2 参数传递
- 传值参数
- 指针形参
- 传引用参数
- 使用引用避免拷贝
- 使用引用形参返回额外信息
- const形参和实参
- 指针或引用形参与const
- 尽量使用常量引用
- 数组形参
- 使用标记指定数组长度
- 使用标准库规范
- 显式传递一个表示数组大小的形参
- 数组形参和const
- 数组引用形参
- 传递多维数组
- main:处理命令行选项
- 含有可变形参的函数
- initializer_list形参
- 省略符形参如
6.2 参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
形参初始化的机理与变量初始化一样。
和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。
传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值:
int n = 0;// int类型的初始变量
int i = n;//工是n的值的副本
i = 42; //i的值改变;n的值不变
传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。例如,在fact函数内对变量val执行递减操作:
ret* = val--; // 将val的值域减1
尽管fact函数改变了val的值,但是这个改动不会影响传入fact的实参.调用fact(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=03//改变指针ip所指对象的值
ip=0//只改变了ip的局部拷贝,实参未被改变
}
调用reset函数之后,实参所指的对象被置为0,但是实参本身并没有改变:
int i = 42;
reset(&i); //改变i的值而非i的地址
cout<<"i = "<< i << endl; //输出i=0
熟悉C的程序员常常使用指针类型的形参访问函数外部的对象,在C++语言中,建议使用引用类型的形参替代指针。
传引用参数
回忆过去所学的知识,我们知道对于引用的操作实际上是作用在引用所引的对象上
int n = 0,i=42;
int&r = n; //i绑定了n(即i是n的另一个名字)
i=423;//现在n的值是42
r = i; //现在n的值和i相同
i = r; //i的值和n相同
引用形参的行为与之类似。通过使用引用形参,允许函数改变一个或多个实参的值。举个例子,我们可以改写上一小节的reset程序,使其接受的参数是引用类型而非指针:
//该函数接取一个int对象的引用,然后将对象的值置为0
void reset(int i)//i是传给reset函数的对象的另一个名称
{
i=0; //改变了i所引对象的值
}
和其他引用一样,引用形参绑定初始化它的对象。当调用这一版本的reset函数时,i绑定我们传给函数的int对象,此时改变i也就是改变i所引对象的值。此例中,被改变的对象是传入reset的实参。
调用这一版本的reset函数时,我们直接传入对象而无须传递对象的地址:
int j=42;
reset(j);//了采用传引用方式,它的值被改变
cout<< "j=" << j <<endl;//输出于=0
在上述调用过程中,形参i仅仅是j的又一个名字。在reset内部对i的使用即是对j的使用。
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。举个例子,我们准备编写一个函数比较两个string对象的长度。因为string对象可能会非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择。又因为比较长度无须改变string对象的内容,所以把形参定义成对常量的引用
//比较两个string对象的长度
bool isShorter(const string&s1,const string&s2)
{
return s1.size()<s2.size();
}
当函数无须修改引用形参的值时最好使用常量引用。
加果函就无须改变引用形参的值,最好将其声明为常量引用。
使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。
该如何定义函数使得它能够既返回位置也返回出现次数呢?一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存守符出现的次数:
//返回s中c第一次出现的位置家引
//引用形参 occeurs 负责统计c出现的总次数
string::size_type find_char(const string&s,char c, string::size_type&occurs){
auto ret=s.size();//第一次出现的位置(如果有的话)
oceurs=0;//设置表示出现次数的形参的值
for(decltype(ret)i=0;i!=s.size();++i)1
if(s[i]=c){
if(ret==s.size())
ret=i;//记录c第一次出现的位置
++occur;//将出现的次数加1
}
return ret;//出现次数通过ccours隐式地返回
}
当我们调用find_char函数时,必须传入三个实参:作为查找范围的一个string对象、要找的字符以及一个用于保存字符出现次数的size_type对象。假设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()而ctr等于0。
const形参和实参
当形参是const时,顶层const作用于对象本身:
const int ci=42;//不能改变c,const是顶层的
int i = ci; //正确;当拷贝ci时,忽略了它的顶层const
int*const p = &i;//const是顶层的,不能给p赋值
*p = 0;//正确:通过p改变贞象的肉容是允许的,现在i变成了0
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:
voi fcn(const int){/*fcn能够读取,但是不能向i写值*/}
调用fcn函数时,既可以传入const int也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:
void fcn(const int){/*fcn能够读取,但是不能向i写值*/}
void fcn(int i){/*…*/】//错误:重复定义了fcn(int)
在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
指针或引用形参与const
形参的初始化方式和变量的初始化方式是一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
int i=42;
const int*cp=&i;//正确:但是cp不能改变i
const int &r=i;//正确:但是r不能改变i
const int &r2=42;//正确
int*p=cp;//错误:p的类型和cp的类型不匹配
int&r3 = r;//错误:r3的类型和的类型不匹配
int&r4= 42; //错误:不能用字面值初始化一个非常量引用
将同样的初始化规则应用到参数传递上可得如下形式:
int i=0;
const int ci=i;
string::size_type ctr=0;
reset(&i);//调用形参类型是int*的reset函数
reset(&ci)}//错误:不能用指向const int对象的指针初始化int*
reset(i);//调用形参类型是intg的reset函数
reset(ci);//错误:不能把普通引用绑定到const对象ci上
reset(42);//错误:不能把普通应用绑定到字面值上
reset(ctr);//错误:类型不匹配,ct是无符号类型
//正确:find_chaz的第一个形参是对常量的引用
find_char(“Hello World!“,'o',ctr);
要想调用引用版本的reset,只能使用int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。类似的,要想调用指针版本的reset只能使用int*。
另一方面,我们能传递一个字符串字面值作为find_char的第一个实参,这是因为该函数的引用形参是常量引用,而C++允许我们用字面值初始化常量引用。
尽量使用常量引用
把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会
极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者霁要类型转换的对象传递给普通的引用形参。
这种错误绝不像看起来那么简单,它可能造成出人意料的后果。的find_char函数为例,那个函数(正确地)将它的string类型的形参定义成常量引用。假如我们把它定义成普通的string:
//不良设计:第一个形参的类型应该是conststringg
string::size_type find_char(string&s,charc,
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是一个句子
strtng::size_type ctr=0;
return find_char(8,'.',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。
数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
//尽管形式不同,但这三个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};
print(&i);//正确:&i的类型是int*
print(j);//正确:了转换成int*并指向j[0]
如果我们传给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
和其他使用数组的代码一样,以数组作为形参的函数必须确保使用数组时不会越界。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
使用标记指定数组长度
管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串,C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个宏字符。函数在处理C风格字符串时遇到空字符停止:
void print(const char* cp)
{
if(cp)//若cp不是一个空指针
while(*cp)//只要指针所指的字符不是空字符
cout<<*cp++;//输出当前字符并将指针向前移动一个位置
}
这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像int这样所有取值都是合法值的数据就不太有效了。
使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发,关于其细节将在第I部分详细介绍。使用该方法,我们可以按照如下形式输出元素内容:
void print(const int*beg,const int*end)
{
//输出beg到end之间(不含end)的所有元素
while(beg=end)
cout<<*beg++<<endl;//输出当前元素并将指针向前移动一个位置
}
while循环使用解引用运算符和后置递减运算符输出当前元素并在数组内将beg向前移动一个元素,当beg和end相等时结束循环。
为了调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置:
```cpp
int j[2] ={0,1};
//j转换成指向它首元素的指针
//第二个实参是指向j的尾后元素的指针
print(begin(j),end(j))//begin和end函数
只要调用者能正确地计算指针所指的位置,那么上述代码就是安全的。在这里,我们使用标准库begin和end函数提供所需的指针。
显式传递一个表示数组大小的形参
第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在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函数时必须传入这个表示数组大小的值:
```cpp
int j[]={0,1};//大小为2的整型数组
print(j,end(j)-begin(j))
只要传递给函数的size值不超过数组实际的大小,函数就是安全的。
数组形参和const
我们的三个print函数都把数组形参定义成了指向const的指针。当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:
//正确:形参是数组的引用,维度是类型的一部分
void print(int(&arr)[10])
{
for(auto elem:arr)
cout << elem << endl;
}
因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print函数的可用性,我们只能将函数作用于大小为10的数组:
int i= 0,j[2]={0,1};
int k[10]={0,1,2,4,5,6,7,8,9};
print(&i);//错误:实参不是含有10个整数的数组
print(j);//错误:实参不是含有10个整数的数组
print(k);//正确:实参是含有10个整数的数组
传递多维数组
我们曾经介绍过,在C++语言中实际上没有真正的多维数组所谓多维数组其实是数组的数组。
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,
指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:
//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int(*matrix)[10],int rowSize){}//
上述语句将matrix声明成指向含有10个整数的数组的指针。
再一次强调,*matrix两端的括号必不可少:
int* matrix[10]; // 10个指针构成的教组
int(*matrix)[10];//指向吴有10个整敬的数组的指针
我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内:
//等价定义
void print(int matrix[][10],int rowSize){/*...*/}
matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。
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[0]="prog"; //或者argv[0]也可以指向一个空字符串
argv[1]="-d";
argv[2]="-o";
argv[3]="ofile";
argv[4]="data0";
argv[5]=0;
当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
含有可变形参的函数
有时我们无法提前预知应该向函数传递几个实参。例如,我们想要编写代码输出程序产生的错误信息,此时最好用同一个函数实现该项功能,以便对所有错误的处理能够整齐划一。然而,错误信息的种类不同,所以调用错误输出函数时传递的实参也各不相同。
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_1ist的标准库类型;如果实参的类型不园,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。
C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。
initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示棠种特定类型的值的数组。initialtzer_list类型定义在同名的头文件中。
表6.1:inttializer_list提供的操作
initializer_list lst | 默认初始化;0类型元素的守列表 |
initializer_lists 1st{a,b,c…} | lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const |
lst2 lst2=lst | 拷贝或赋值一个initializer_1ist对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向lst中首元素的指针 |
lst.end() | 返回指向lst中尾元素下一位置的指针 |
和vector一样,initializer 1ist也是一种模板类型。定义initializer_1ist对象时,必须说明列表中所含元素的类型:
initializer_list<string>1s; //initializer_litst的元素类型是string
initializer_list<int> li; //initializer_list的元素类型是int
和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
我们使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:
void error_msg(initializer_list<string>i1)
{
for(auto beg=i1.begin();beg!=i1.end();++beg)
cout<<*beg<<" ";
cout<<endl;
}
作用于initialtzer_list对象的begin和end操作类似于vector对应的成员。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,initializer_list<string>i1)
{
cout<K<e.msg()<< ":";
for(const auto&elem:11)
cout<<elem<<" ";
cout<<endl;
}
因为initializer_list包含begin和end成员,所以我们可以使用范围for循环处理其中的元素。和之前的版本类似,这段程序遍历传给11形参的列表值,每次迪代时访问一个元素。
为了调用这个版本的erro_rmsg函数,需要额外传递一个ErrCode实参:
```cpp
if(expected!=actual)
error_msg(ErrCode(42),{"functitonX",expected,actual});
else
error_msg(ErzCode(0),{"EuncttonX","okay"});
省略符形参如
省略符形参是为了便于C++程序访问树些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编详器文档会描述如何使用varargs。
省略符形参应该仅仅用于C和C++通用的类型.特别应该注意的是,大多数类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
void foo(parm_list,...);
void foo(...);
第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。