天天看点

C++编程:函数的参数传递

作者:尚硅谷教育

函数在每次调用时,都会重新创建形参,并且用传入的实参对它进行初始化。形参的类型,决定了形参和实参交互的方式;也决定了函数的不同功能。

可以先回忆一下对变量的初始化:对一个变量做初始化,如果用另一个变量给它赋初值,意味着值的拷贝;也就是说,此后这两个变量各自一份数据,各自管理,互不影响。而如果是定义一个引用,绑定另一个变量做初始化,并不会引发值的拷贝;引用和原变量管理的是同一个数据对象。

int i1 = 0;

int i2 = i1;

i2 = 1; // i1的值仍然是0

int& i3 = i1;

i3 = 10; // i1的值也变为10

参数传递和变量的初始化类似,根据形参的类型可以分为两种方式:传值(value)和传引用(reference)。

C++编程:函数的参数传递

图片来源于网络,侵删

1. 传值参数

直接将一个实参的值,拷贝给形参做初始化的传参方式,就被称为“值传递”,这样的参数被称为“传值参数”。之前我们练习过的所有函数,都是采用这种传值调用的方式。

int square(int x)

{

return x * x;

}

int main()

{

int n = 6;

cout << n << "的平方是:" << square(n) << endl;

}

在上面平方函数的调用中,实参n的值(6)被拷贝给了形参x。

(1)传值的困扰

值传递这种方式非常简单,但是面对这样的需求会有些麻烦:传入一个数据对象,让它经过函数处理之后发生改变。例如,传入一个整数x,调用之后它自己的值要加1。这看起来很简单,但如果直接:

void increase(int x)

{

++x;

}

int main()

{

int n = 6;

increase(n); // n的值不会增加

}

这样做并不能实现需求。因为实参n的值是拷贝给形参x的,之后x的任何操作,都不会改变n。

(2)指针形参

使用指针形参可以解决这个问题。如果我们把指向数据对象的指针作为形参,那么初始化时拷贝的就是指针的值;复制之后的指针,依然指向原始数据对象,这样就可以保留它的更改了。

// 指针形参

void increase(int* p)

{

++(*p);

}

int main()

{

int n = 0;

increase( &n ); // 传入n的地址,调用函数后n的值会加1

}

2. 传引用参数

使用指针形参可以解决值传递的问题,不过这种方式函数定义显得有些繁琐,每次调用还需要记住传入变量的地址,使用起来不够方便。

(1)传引用方便函数调用

C++新增了引用的概念,可以替换必须使用指针的场景。采用引用作为函数形参,可以使函数调用更加方便。这种传参方式叫做“传引用参数”。之前的例子就可以改写成:

// 传引用

void increase(int& x)

{

++x;

}

int main()

{

int n = 0;

increase( n ); // 调用函数后n的值会加1

}

由于使用了引用作为形参,函数调用时就可以直接传入n的值,而不用传地址了;x只是n的一个别名,修改x就修改了n。对比可以发现,这段代码相比最初尝试写出的传值实现,只是多了一个引用声明&而已。

(2)传引用避免拷贝

使用引用还有一个非常重要的场景,就是不希望进行值拷贝的时候。实际应用中,很多时候函数要操作的对象可能非常庞大,如果做值拷贝会使得效率大大降低;这时使用引用就是一个好方法。

比如,想要定义一个函数比较两个字符串的长度,需要将两个字符串作为参数传入。因为字符串有可能非常长,直接做值拷贝并不是一个好选择,最好的方式就是传递引用:

// 比较两个字符串的长度

bool isLonger(const string & str1, const string & str2)

{

return str1.size() > str2.size();

}

(3)使用常量引用做形参

在上面的例子中,比较两个字符串长度,并不会更改字符串本身的内容,所以可以把形参定义为常量引用。

这样的好处是,既避免了对数据对象可能的更改,也扩大了调用时能传的实参的范围。因为之前讨论过常量引用的特点,可以用字面值常量对它做初始化,也可以用变量做初始化。

所以在代码中,一般要尽量使用常量引用作为形参。

3. 数组形参

之前已经介绍过,数组是不允许做直接拷贝的,所以如果想要把数组作为函数的形参,使用值传递的方式是不可行的。与此同时,数组名可以解析成一个指针,所以可以用传递指针的方式来处理数组。

比如一个简单的函数,需要遍历int类型数组所有元素并输出,就可以这样声明:

void printArray(const int*); // 指向int类型常量的指针

void printArray(const int[]);

void printArray(const int[5]);

由于只是遍历输出,不需要修改数组内容,所以这里使用了const。

以上三种声明方式,本质上是一样的,形参的类型都是const int *;虽然第三种方式指定了数组长度,但由于编译器会把传入的数组名解析成指针,事实上的数组长度还是无法确定的。

这就带来另一个问题:在函数中,遍历元素时怎样确定数组的结束?

(1)规定结束标记

一种简单思路是,规定一个特殊的“结束标记”,遇到这个标记就代表当前数组已经遍历完了。典型代表就是C语言风格的字符串,是以空字符’\0’为结束标志的char数组。

这种方式比较麻烦,而且太多特殊规定也不适合像int这样的数据类型。

(2)把数组长度作为形参

除指向数组的指针外,可以再增加一个形参,专门表示数组的长度,这样就可以方便地遍历数组了。

void printArray(const int* arr, int size)

{

for (int i = 0; i < size; i++)

cout << arr[i] << "\t";

cout << endl;

}

int main()

{

int arr[6] = { 1,2,3,4,5,6 };

printArray(arr, 6);

}

在C语言和老式的C++程序中,经常使用这种方法来处理数组。

(3)使用数组引用作为形参

之前的方法依赖指针,所以都显得比较麻烦。更加方便的做法,还是用引用来替代指针的功能。

C++允许使用数组的引用作为函数形参,这样一来,引用作为别名绑定在数组上,使用引用就可以直接遍历数组了。

// 使用数组引用作为形参

void printArray(const int(&arr)[6])

{

for (int num : arr)

cout << num << "\t";

cout << endl;

}

int main()

{

int arr[6] = { 1,2,3,4,5,6 };

printArray(arr);

}

这里需要注意的是,定义一个数组引用时需要用括号将&和引用名括起来:

int(&arr)[6] // 正确,arr是一个引用,绑定的是长度为6的int数组

// int & arr[6] // 错误,这是引用的数组,不允许使用

使用数组引用之后,调用函数直接传入数组名就可以了。

4. 可变形参

有时候我们并不确定函数中应该有几个形参,这时就需要使用“可变形参”来表达。

C++中表示可变形参的方式主要有三种:

  • 省略符(…):兼容C语言的用法,只能出现在形参列表的最后一个位置;
  • 初始化列表initializer_list:跟vector类似,也是一种标准库模板类型;initializer_list对象中的元素只能是常量值,不能更改;
  • 可变参数模板:这是一种特殊的函数,后面会详细介绍。

继续阅读