第6章 6.2 参数传参
文章目录
当形参是 引用类型 时,则称它对应的实参被 引用传递 或者函数被 传引用调用。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们称这样的实参被 值传递 或者函数被 传值调用。
6.2.1 传值参数
当初始化一个非引用类型的变量时,初始值被 拷贝 给变量,此时 对变量的改动不会影响初始值。同理,如果一个函数被 传值调用,那么函数对形参做的所有操作都不会影响实参:
void display(int i) {
i++; // 该语句并不会影响传入display的实参,即main函数中的变量i
}
int main()
{
int i = 3;
display(i);
cout << i << endl; // 输出的i为3
return 0;
}
【指针形参】
当一个函数被 传值调用 时,如果形参的类型是指针,那么函数 可以通过指针访问并修改它所指的对象。但是,由于参数是 值传递,这意味着 实参的值 被 拷贝 给了 形参,拷贝之后,两个指针是不同的指针,因此函数对形参的改动不会影响实参。例如:
void display(int *i) {
*i = 2; // 改变了指针i所指对象的值
i = 0; // 只改变了i的局部拷贝,实参没有改变
}
int main()
{
int i = 3;
display(&i); // 改变i的值,但没改变i的地址
cout << i << endl; // 输出的是 2
return 0;
}
6.2.2 传引用参数
使用 引用形参,允许函数改变 一个或多个实参的值。和其他引用一样,引用形参绑定初始化它的对象。例如:
void display(int &i) { // 传值引用
i = 0; // 改变了其绑定的对象的值
}
int main()
{
int i = 3;
display(i);
cout << i << endl; // 输出为0
return 0;
}
【使用引用避免拷贝】
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过 引用形参 访问该类型的对象。例如:
// 比较两个字符串的长度
bool cmp(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
上例中,因为 string 对象可能很长,为了避免直接拷贝,这时可以采用引用形参。又因为这里只是进行比较,而无需改变 string 对象的内容,因此可以把形参定义为对常量的引用。
【使用引用形参返回额外信息】
一个函数只能返回一个值。如果向让函数同时返回多个值,可以采用2中方法:
(1)定义一个新的数据类型;
(2)给函数传入一个或多个额外的引用实参,令其保存想要返回的值。
6.2.3 const形参和实参
首先先来回顾 顶层const,顶层const作用于对象本身:
const int i = 1; // 顶层const,i的值不能改变
int j = i; // 正确:当拷贝i的时候,忽略了它的顶层const
和其他初始化过程一样,当用实参初始化形参时,也会忽略掉实参的顶层const。对于形参有顶层const的函数来说,传给它常量对象或非常量对象都可以,例如:
void func(const int i) {
cout << i << endl;
}
int main() {
const int j = 2;
int k = 3;
func(j); // 正确
func(k); // 正确
return 0;
}
6.2.4 数组形参
首先,要明确数组具有两个特殊的性质:
(1)不允许拷贝数组,因此我们无法以值传递的方式使用数组参数。
(2)使用数组时通常会将其转换成指针,所以当我们为一个函数传递一个数组时,实际上传递的是指向数组首元素的指针。
虽然不可以以值传递的方式传递数组,但可以把形参写成类似数组的形式:
// 下面是3个函数的声明
// 尽管形式不同,但这三个函数是等价的
void func(const int *);
void func(const int []); // 可以看出,函数的意图是作用于一个数组
void func(const int [10]); // 这里的维度是我们期望数组中有几个元素,实际并不一定需要是这个维度
尽管上述3个函数的形式不同,但它们是等价的:每个函数的唯一形参都是 const int*
类型的。当编译器调用func
函数时,只会检查传入的参数类型是否为 const int*
,也就是说,如果传进来的参数是一个数组,那么编译器会自动将参数转换成指向数组首元素的指针,因此数组的大小对函数调用并没有影响。例如:
void func(const int a[10]) {
cout << a[0] << endl;
}
int main() {
int i = 9;
int j[10] = {1, 2, 3};
func(&i); // 正确:&i的类型是 int*
func(j); // 正确:j转换成int*并指向j[0]
return 0;
}
【数组形参的尺寸控制】
由于数组是以指针的形式传递给函数的,因此一开始函数并不知道数组的确切尺寸,因此调用函数的时候需要提供一些额外的信息。管理指针形参通常采用三种技术:使用标记指定数组长度、使用标准库规范、显示传递一个表示数组大小的形参。
【使用标记指定数组长度】
管理数组实参的第一种方法,就是要求 数组本身包含一个结束标记。这种方法适用于那些有明显结束标记且该标记不会跟普通数据混淆的情况,因此并不适用诸如 int 类型的数组。
该方法的典型实例是 C 风格的字符串,C 风格的字符串存储在字符数组里时,在最后一个字符后面会跟一个空字符,这样函数在处理字符数组时,只要遇到空字符就停止:
void display(char a[]) {
char *tp = a;
while(tp) {
cout << *tp++;
}
}
[使用标准库规范]
管理数组实参的第二种方法是 传递指向数组首元素和尾元素的指针:
void func(int *b, int *e) {
int *i;
for(i=b; i<e; i++) {
cout << *i << " ";
}
cout << endl;
}
int main()
{
int b[10] = {1, 2};
func(begin(b), end(b));
return 0;
}
【显示传递一个表示数组大小的形参】
管理指针形参的第三种方法就是传递一个表示数组大小的形参:
void func(int a[], int len) {
int i;
for(i=0; i<len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int b[10] = {1, 2};
func(b, 2);
return 0;
}
【数组引用形参】
当函数的形参是 数组的引用 时,该引用形参绑定到对应的实参上,也就是绑定到数组上。
在使用数组引用形参时,需要注意:
(1)由于数组的维度是构成数组类型的一部分,因此数组引用形参中要 指明数组的大小;
(2)由于在形参中指明其所引用的数组的大小,所以只能将函数作用于指定大小的数组。
例如:
void func(int (&a)[]); // 错误:没有指明数组的大小
void func(int (&a)[10]); // 正确:a是一个维度为10的整型数组的引用
int b[10], c[4];
func(b); // 正确:b是含有10给整型的数组
func(c); // 错误:c不是含有10个整型的数组
【传递多维数组】
多维数组其实就是数组的数组。
当将多为数组传递给函数的时候,真正传递的是指向数组首元素的指针,而数组首元素本身就是一个数组,因此该指针是一个指向数组的指针:
// 下面两种声明是等价的
// matrix指向一个二维数组的首元素,该首元素是一个维度为10的整型数组
void func(int (*matrix)[10]);
void func(int matrix[][10]); // 编译器会忽略第一个维度
【交换两个int指针】
编写一个函数,令其交换两个 int 指针,需要使用到 指针引用:
// i和j是对指针的引用
void display(int *&i, int *&j) {
int *temp = i;
i = j;
j = temp;
}
int main()
{
int a = 3, b = 4;
int *c = &a, *d = &b;
cout << *c << " " << *d << endl;
display(c, d); // 正确
display(&a, &b); // 错误:引用的初始值不能是字面值
cout << *c << " " << *d << endl;
return 0;
}
6.2.6 含有可变形参的函数
为了编写 能处理不同数量实参 的函数,C++11 新标准提供了一个名为 initializer_list
的标准库类型。
【initializer_list 形参】
如果 函数的实参数量未知但是全部实参的类型都相同,那就可以使用 initializer_list
类型的参数。initializer_list
是一种标准库类型,用于表示某种特定类型的值的数组。它和 vector 类似,是一种模板类型,因此在定义 initializer_list
对象的时候需要说明列表中包含元素的类型:
initializer_list<string>il; // initializer_list里的元素的类型是string
initializer_list
有以下几个要注意的地方:
(1)和 vector 不同的是,initializer_list
对象中的元素永远是常量值,不可以改动;
(2)initializer_list
对象的 begin 成员可以提供指向列表首元素的指针,end 成员可以提供一个指向列表尾后元素的指针;
(3)如果想向 initializer_list
形参中传递一个值的序列,则必须把序列放在一对 花括号 里。
例如:
void display(initializer_list<int> il) {
for(auto beg = il.begin(); beg != il.end(); ++beg) {
cout << *beg << " ";
}
cout << endl;
}
int main()
{
display({1, 2, 3}); // 输出:1 2 3
display({5}); // 输出:5
return 0;
}
【省略符形参】
省略符形参是为了便于 C++ 程序访问某些特殊的 C 代码而设置的,这些代码使用了名为 varargs 的 C 标准库功能。省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表中的最后一个位置,它的形式无外乎以下两种:
// 第一种:指定了func函数的部分形参的类型
// 对于这些指定了类型的形参,会对其对应的实参执行类型检查
// 省略符对应的实参无需类型检查
void func(parm_list, ...);
// 第二种:全省略
void func(...);
例子:
void display(int i, ...) {
cout << i << endl;
}
int main()
{
int i = 4;
string s = "hehe";
display(i, s); // 输出:4
return 0;
}