C++ 函数实参传递 (argument passing)

C++ 函数实参传递 (argument passing)

argument [ˈɑːɡjumənt]:n. 实参
parameter [pəˈræmɪtə(r)]:n. 形参

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。

当形参是引用类型时,它对应的实参被引用传递 (passed by reference) 或者函数被传引用调用 (called by reference)。和其它引用一样,引用形参也是它绑定的对象的别名,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。这样的实参被值传递 (passed by value) 或者函数被传值调用 (called by value)。

1. 传值参数 (passing arguments by value)

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。

int n = 0;
int i = n;  // i is a copy of the value in n
i = 42;     // value in i is changed, n is unchanged

传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。

1.1. 指针形参

指针的行为和其它非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。

int n = 0, i = 42;
int *p = &n, *q = &i;  // p points to n, q points to i
*p = 42;               // value in n is changed, p is unchanged
p = q;                 // p now points to i, values in i and n are unchanged

指针形参的行为与之类似:

// function that takes a pointer and sets the pointed-to value to zero
void reset(int *ip) {
	*ip = 0;  // changes the value of the object to which ip points
	ip = 0;   // changes only the local copy of ip, the argument is unchanged (只改变了 ip 的局部拷贝,实参未被改变)
}

调用 reset 函数之后,实参所指的对象被置为 0,但是实参本身并没有改变:

#include <iostream>

// function that takes a pointer and sets the pointed-to value to zero
void reset(int *ip) {
	*ip = 0;  // changes the value of the object to which ip points (改变指针 ip 所指对象的值)
	ip = 0;   // changes only the local copy of ip, the argument is unchanged (只改变了 ip 的局部拷贝,实参未被改变)
}

int main() {
	int i = 42;
	reset(&i);  // changes i but not the address of i
	std::cout << "i = " << i << std::endl;  // prints i = 0

	return 0;
}

i = 0
请按任意键继续. . .

在 C++ 语言中,建议使用引用类型的形参替代指针。

2. 传引用参数 (passing arguments by reference)

对于引用的操作实际上是作用在引用所引的对象上:

int n = 0, i = 42;
int &r = n;  // r is bound to n (i.e. r is another name for n)
r = 42;      // n is now 42
r = i;       // n now has the same value as i
i = r;       // i has the same value as n

引用形参的行为与之类似。通过使用引用形参,允许函数改变一个或多个实参的值。

和其它引用一样,引用形参绑定初始化它的对象。当调用这一版本的 reset 函数时,val 绑定我们传给函数的实参对象,此时改变 val 也就是改变 val 所引对象的值。调用这一版本的 reset 函数时,我们直接传入对象而无须传递对象的地址:

#include <iostream>

// function that takes a reference to an int and sets the given object to zero
void reset(int &val) {
	// val is just another name for the object passed to reset
	val = 0;  // changes the value of the object to which val refers
}

int main() {
	int num = 42;
	reset(num);  // num is passed by reference, the value in num is changed
	std::cout << "num = " << num << std::endl;  // prints num = 0

	return 0;
}

num = 0
请按任意键继续. . .

在上述调用过程中,形参 val 仅仅是 num 的又一个名字。在 reset 内部对 val 的使用即是对 num 的使用。

2.1. 使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型 (包括 IO 类型在内) 根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

一个函数比较两个 std::string 对象的长度。因为 std::string 对象可能会非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择。如果函数无须改变引用形参的值,最好将其声明为常量引用。

// compare the length of two strings
bool isShorter(const std::string &s1, const std::string &s2) {
	return s1.size() < s2.size();
}

2.2. 使用引用形参返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。

3. const 形参和实参 (const parameters and arguments)

顶层 (top-level) const 作用于对象本身。

const int ci = 42;   // we can not change ci, const is top-level
int i = ci;          // 正确: when we copy ci, its top-level const is ignored
int * const p = &i;  // const is top-level, we can not assign to p (const 是顶层的,不能给 p 赋值)
*p = 0;              // 正确: changes through p are allowed, i is now 0

当用实参初始化形参时会忽略顶层 const,形参的顶层 const 被忽略了。当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。调用 fun 函数时,既可以传入 const int 也可以传入 int。

void fun(const int i) { /* fun can read but not write to i */ }

忽略掉形参的顶层 const 可能产生意想不到的结果:

void fun(const int i) { /* fun can read but not write to i */ }
void fun(int i) { /* . . . */ }  // error: redefines fun(int)

在 C++ 语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为形参的顶层 const 被忽略掉了,所以在上面的代码中传入两个 fun 函数的参数可以完全一样。因此第二个 fun 是错误的,尽管形式上有差异,但实际上它的形参和第一个 fun 的形参没什么不同。

3.1. const + 指针或引用形参

形参的初始化方式和变量的初始化方式是一样的,可以使用非常量初始化一个底层 const 对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

int i = 42;
const int *cp = &i;  // 正确: but cp can't change i
const int &r = i;    // 正确: but r can't change i
const int &r2 = 42;  // 正确: C++ 允许我们用字面值初始化常量引用。
int *p = cp;         // 错误: types of p and cp don't match
int &r3 = r;         // 错误: types of r3 and r don't match
int &r4 = 42;        // 错误: can't initialize a plain reference from a literal (不能用字面值初始化一个非常量引用)

要想调用引用版本的 reset 函数,只能使用 int 类型的对象,而不能使用字面值、求值结果为 int 的表达式、需要转换的对象或者 const int 类型的对象。类似的,要想调用指针版本的 reset 函数只能使用 int*

// function that takes a reference to an int and sets the given object to zero
void reset(int &val) {
	// val is just another name for the object passed to reset
	val = 0;  // changes the value of the object to which val refers
}

// function that takes a pointer and sets the pointed-to value to zero
void reset(int *ip) {
	*ip = 0;  // changes the value of the object to which ip points
	ip = 0;   // changes only the local copy of ip, the argument is unchanged (只改变了 ip 的局部拷贝,实参未被改变)
}
int i = 0;
const int ci = i;
std::string::size_type ctr = 0;
reset(&i);   // calls the version of reset that has an int* parameter
reset(&ci);  // 错误: can't initialize an int* from a pointer to a const int object (不能用指向 const int 对象的指针初始化 int*)
reset(i);    // calls the version of reset that has an int& parameter
reset(ci);   // 错误: can't bind a plain reference to the const object ci (不能把普通引用绑定到 const 对象 ci 上)
reset(42);   // 错误: can't bind a plain reference to a literal (不能把普通应用绑定到字面值上)
reset(ctr);  // 错误: types don't match, ctr has an unsigned type (类型不匹配,ctr 是无符号类型)

3.2. 尽量使用常量引用

把函数不会改变的形参定义成 (普通的) 引用是一种比较常见的错误,这么做会给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。不能把 const 对象、字面值或者需要类型转换的对象传递给普通的引用形参。(We can not pass a const object, or a literal, or an object that requires conversion to a plain reference parameter.)

#include <iostream>

// returns the index of the first occurrence of c in str (返回 str 中 c 第一次出现的位置索引)
// the reference parameter num counts how often c occurs (引用形参 num 负责统计 c 出现的总次数)
std::string::size_type find_char(const std::string &str, const char &c, std::string::size_type &num) {
	auto ret = str.size();  // position of the first occurrence, if any (如果有的话,第一次出现的位置)
	num = 0;                // set the occurrence count parameter
	for (decltype(ret) i = 0; i != str.size(); ++i) {
		if (str[i] == c) {
			if (ret == str.size()) {
				ret = i;    // remember the first occurrence of c (记录 c 第一次出现的位置)
			}
			++num;          // increment the occurrence count (出现的次数加 1)
		}
	}

	// count is returned implicitly in num (出现的次数通过 num 隐式地返回)
	return ret;
}

int main() {
	std::string str("yongqiang");
	size_t num = 0;

	auto index = find_char(str, 'g', num);
	std::cout << index << " -> " << num << std::endl;

	return 0;
}

3 -> 2
请按任意键继续. . .

find_char 函数正确地将它的 std::string 类型的形参定义成常量引用 (const std::string &)。假如我们把它定义成普通的 std::string &

std::string::size_type find_char(std::string &str, const char &c, std::string::size_type &num);

则只能将 find_char 函数作用于 std::string 对象,类似下面这样的调用

const std::string str("yongqiang");
size_t num = 0;
auto index = find_char(str, 'g', num);

将在编译时发生错误。

1>d:\vulkan_workspace\sample\sample\sample.cpp(24): error C2664: 'unsigned int find_char(std::string &,const char &,unsigned int &)': cannot convert argument 1 from 'const std::string' to 'std::string &'

假如其它函数正确地将它们的形参定义成常量引用,那么第二个版本的 find_char 无法在此类函数中正常使用。

bool is_sentence(const std::string &str) {
	// if there is a single period at the end of str, then str is a sentence (如果在 str 的末尾有且只有一个句号,则 str 是一个句子)
	std::string::size_type num = 0;
	return ((find_char(str, '.', num) == str.size() - 1) && (num == 1));
}

如果 find_char 的第一个形参类型是 std::string &,那么上面这条调用 find_char 的语句将在编译时发生错误。原因在于 str 是常量,但 find_char 被不正确地定义成只能接受普通引用。

解决该问题的一种思路是修改 is_sentence 的形参类型,但是这么做只不过转移了错误而已,结果是 is_sentence 函数的调用者只能接受非常量 std::string 对象了。正确的修改思路是改正 find_char 函数的形参。如果实在不能修改 find_char,就在 is_sentence 内部定义一个 std::string 类型的变量,令其为 str 的副本,然后把这个 std::string 对象传递给 find_char。

4. 数组形参 (array parameters)

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组,使用数组时通常会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

// each function has a single parameter of type const int* (每个函数都有一个 const int* 类型的形参)
void print(const int*);
void print(const int[]);    // shows the intent that the function takes an array (函数的意图是作用于一个数组)
void print(const int[10]);  // dimension for documentation purposes at best (这里的维度表示我们期望数组含有多少元素,实际不一定)

尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是 const int* 类型的。当编译器处理对 print 函数的调用时,只检査传入的参数是否是 const int* 类型:

int i = 0, j[2] = { 0, 1 };
print(&i);  // 正确: &i is int*
print(j);   // 正确: j is converted to an int* that points to j[0]

如果我们传给 print 函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。和其它使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。

4.1. 使用标记指定数组长度

管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是 C 风格字符串。C 风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。

函数在处理 C 风格字符串时遇到空字符停止:

// prints a null-terminated array of characters
void print(const char *cp) {
	if (cp) {
		// if cp is not a null pointer (若 cp 不是一个空指针)
		while (*cp) {
			// so long as the character it points to is not a null character (只要指针所指的字符不是空字符)
			std::cout << *cp++;  // print the character and advance the pointer (输出当前字符并将指针向前移动一个位置)
		}
	}
}

这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像 int 这样所有取值都是合法值的数据就不太有效了。

4.2. 使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。

// print ints in the given range
void print(const int *beg, const int *end) {
	// print every element starting at beg up to but not including end (输出 beg 到 end 之间,不含 end 的所有元素)
	while (beg != end) {
		std::cout << *beg++ << " ";  // print the current element and advance the pointer (输出当前元素并将指针向前移动一个位置)
	}
}

while 循环使用解引用运算符和后置递减运算符输出当前元素并在数组内将 beg 向前移动一个元素,当 beg 和 end 相等时结束循环。

为了调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置。

#include <iostream>

// prints a null-terminated array of characters
void print(const char *cp) {
	if (cp) {
		// if cp is not a null pointer (若 cp 不是一个空指针)
		while (*cp) {
			// so long as the character it points to is not a null character (只要指针所指的字符不是空字符)
			std::cout << *cp++;  // print the character and advance the pointer (输出当前字符并将指针向前移动一个位置)
		}
	}
}

// print ints in the given range
void print(const int *beg, const int *end) {
	// print every element starting at beg up to but not including end (输出 beg 到 end 之间,不含 end 的所有元素)
	while (beg != end) {
		std::cout << *beg++ << " ";  // print the current element and advance the pointer (输出当前元素并将指针向前移动一个位置)
	}
}

int main() {
	print("yongqiang!"); // calls first version of print
	std::cout << std::endl;

	// j is converted to a pointer to the first element in j (j 转换成指向它首元素的指针)
	// the second argument is a pointer to one past the end of j (第二个实参是指向 j 的尾后元素的指针)
	int j[2] = { 0, 1 };
	print(std::begin(j), std::end(j));  // library begin and end functions
	std::cout << std::endl;

	// equivalent call, directly calculate the begin and end pointers
	print(j, j + 2);
	std::cout << std::endl;

	return 0;
}

yongqiang!
0 1
0 1
请按任意键继续. . .

只要调用者能正确地计算指针所指的位置,那么上述代码就是安全的。在这里,我们使用标准库 begin 和 end 函数提供所需的指针。

4.3. 显式传递一个表示数组大小的形参

第三种管理数组实参的方法是专门定义一个表示数组大小的形参。

#include <iostream>

// "const int ia[]" is equivalent to "const int* ia"
// size is passed explicitly and used to control access to elements of ia
void print(const int ia[], size_t size) {
	for (size_t i = 0; i != size; ++i) {
		std::cout << ia[i] << std::endl;
	}
}

int main() {
	int j[] = { 0, 1 };  // int array of size 2

	print(j, std::end(j) - std::begin(j));

	return 0;
}

0
1
请按任意键继续. . .

这个版本的程序通过形参 size 的值确定要输出多少个元素,调用 print 函数时必须传入这个表示数组大小的值。只要传递给函数的 size 值不超过数组实际的大小,函数就是安全的。

4.4. 数组形参和 const

当函数不需要对数组元素执行写操作的时候,数组形参应该是指向 const 的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

4.5. 数组引用形参 (array reference parameters)

C++ 语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上。

// 正确: parameter is a reference to an array, the dimension is part of the type (形参是数组的引用,维度是类型的一部分)
void print(int (&arr)[10]) {
	for (auto elem : arr) {
		std::cout << elem << std::endl;
	}
}

&arr 两端的括号必不可少:

fun(int &arr[10])   // 错误: declares arr as an array of references
fun(int(&arr)[10])  // 正确: arr is a reference to an array of ten ints (arr 是具有 10 个整数的整型数组的引用)

因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了 print 函数的可用性,我们只能将函数作用于大小为 10 的数组。

int i = 0, j[2] = { 0, 1 };
int k[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
print(&i);  // 错误: argument is not an array of ten ints (实参不是含有 10 个整数的数组)
print(j);   // 错误: argument is not an array of ten ints (实参不是含有 10 个整数的数组)
print(k);   // 正确: argument is an array of ten ints (实参是含有 10 个整数的数组)

4.6. 传递多维数组

在 C++ 语言中实际上没有真正的多维数组,所谓多维数组其实是数组的数组。

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维 (以及后面所有维度) 的大小都是数组类型的 一部分,不能省略。

// matrix points to the first element in an array whose elements are arrays of ten ints (matrix 指向数组的首元素,该数组的元素是由 10 个整数构成的数组)
void print(int (*matrix)[10], int rowSize) { /* . . . */ }

上述语句将 matrix 声明成指向含有 10 个整数的数组的指针。

再一次强调,*matrix 两端的括号必不可少:

int *matrix[10];    // array of ten pointers (10 个指针构成的数组)
int (*matrix)[10];  // pointer to an array of ten ints (指向含有 10 个整数的数组的指针)

我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内。

// equivalent definition (等价定义)
void print(int matrix[][10], int rowSize) { /* . . . */ }

matrix 的声明看起来是一个二维数组,实际上形参是指向含有 10 个整数的数组的指针。

下标引用 ([]) 优先级高于间接访问 (*)。

References

https://yongqiang.blog.csdn.net/
(美) Stanley B. Lippman, (美) Josée Lajoie, (美) Barbara E. Moo 著, 王刚, 杨巨峰 译. C++ Primer 中文版[M]. 第 5 版. 电子工业出版社, 2013.
https://www.informit.com/store/c-plus-plus-primer-9780321714114

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yongqiang Cheng

梦想不是浮躁,而是沉淀和积累。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值