【C++基础】第三十五课:参数传递

传值参数,传引用参数,const形参和实参,数组形参,main:处理命令行选项,含有可变形参的函数

Posted by x-jeff on December 29, 2021

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.参数传递

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

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

2.传值参数

2.1.指针形参

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void f(int *p, int *t, int *(&z)) {
    int i = 100;
    t = &i;
    z = &i;
    *p = 2;
    cout << z << endl;//0x7ff7bdc31724
    cout << *z << endl;//100
}

int main() {
    int i = 42, v = 50;
    int *p = &i;
    int *t = &v;
    int *z = &v;
    cout << z << endl;//0x7ff7bdc317a4
    f(p, t, z);
    cout << i << endl;//2
    cout << v << endl;//50
    cout << *p << endl;//2
    cout << *t << endl;//50
    cout << *z << endl;//32759,这个需要额外注意!!!
    cout << z << endl;//0x7ff7bdc31724
    return 0;
}

3.传引用参数

1
2
3
4
5
6
7
8
9
void reset(int &i) {
    i = 0;
}

int main() {
    int j = 42;
    reset(j);
    cout << j << endl;//j=0
}

3.1.使用引用避免拷贝

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

举个例子,我们准备编写一个函数比较两个string对象的长度。因为string对象可能会非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择。又因为比较长度无须改变string对象的内容,所以把形参定义成对常量的引用:

1
2
3
4
5
//比较两个string对象的长度
bool isShorter(const string &s1, const string &s2)
{
	return s1.size() < s2.size();
}

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

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置,并统计其出现次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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())
				ret=i;
			++occurs;
		}
	}
	return ret;
}

4.const形参和实参

当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:

1
2
3
4
void fcn(const int i)
{
	/*fcn能够读取i,但是不能向i写值*/
}

调用fcn函数时,既可以传入const int也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:

1
2
void fcn(const int i) { /*...*/ }
void fcn(int) { /*...*/ } //错误:重复定义了fcn(int)

在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。

4.1.尽量使用常量引用

如果我们将find_char中的第一个参数改为string&:

1
string::size_type find_char(string &s, char c, string::size_type &occurs);

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

1
find_char("Hello World",'o',ctr);

将在编译时发生错误。

5.数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数,但是我们可以把形参写成类似数组的形式:

1
2
3
4
//尽管形式不同,但这三个print函数是等价的
void print(const int*);
void print(const int[]);
void print(const int[10]);//这里的维度表示我们期望数组含有多少元素,实际不一定

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

1
2
3
int i=0,j[2]={0,1};
print(&i);//正确:&i的类型是int*
print(j);//正确:j转换成int*并指向j[0]

如果我们传给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

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

管理数组实参的第一种方法是要求数组本身包含一个结束标记:

1
2
3
4
5
6
void print(const char *cp)
{
	if(cp) //若cp不是一个空指针
		while(*cp) //只要指针所指的字符不是空字符
			cout<<*cp++; //输出当前字符并将指针向前移动一个位置
}

5.2.使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针:

1
2
3
4
5
void print(const int *beg, const int *end)
{
	while(beg!=end)
		cout<<*beg++<<endl;
}

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

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

1
2
3
4
5
6
7
void print(const int ia[], size_t size)
{
	for(size_t i=0;i!=size;++i)
	{
		cout<<ia[i]<<endl;
	}
}

调用该print函数:

1
2
int j[]={0,1};
print(j,end(j)-begin(j));

5.4.数组引用形参

C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用:

1
2
3
4
5
void print(int (&arr)[10])
{
	for(auto elem : arr)
		cout<<elem<<endl;
}

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

1
2
f(int &arr[10]) //错误:将arr声明成了引用的数组
f(int (&arr)[10]) //正确:arr是具有10个整数的整型数组的引用

注意:我们只能将函数作用于大小为10的数组。

以后的博客我们会介绍如何编写这个函数,使其可以给引用类型的形参传递任意大小的数组。

5.5.传递多维数组

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

1
2
3
4
5
//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize)
{
	/*...*/
}

⚠️*matrix两端的括号必不可少:

1
2
int *matrix[10]; //10个指针构成的数组
int (*matrix)[10]; //指向含有10个整数的数组的指针

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

1
2
3
4
5
//等价定义
void print(int matrix[][10], int rowSize)
{
	/*...*/
}

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

6.main:处理命令行选项

到目前为止,我们定义的main函数都只有空形参列表:

1
int main() {...}

有时我们确实需要通过命令行选项给main传递实参,可以通过两个(可选的)形参传递给main函数:

1
int main(int argc, char *argv[]) {...}

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:

1
int main(int argc, char **argv) {...}
  • argc是argument count的缩写。
  • argv是argument vector的缩写。

假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:

1
prog -d -o ofile data0

此时,argc应该等于5,argv应该包含如下的C风格字符串:

1
2
3
4
5
6
argv[0]="prog";//或者argv[0]也可以指向一个空字符串
argv[1]="-d";
argv[2]="-o";
argv[3]="ofile";
argv[4]="data0";
argv[5]=0;//最后一个指针之后的元素值保证为0

⚠️当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字(路径),而非用户输入。

7.含有可变形参的函数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模版,以后的博客会详细介绍。

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。

7.1.initializer_list形参

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。其提供的操作见下:

和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型:

1
2
initializer_list<string> ls;
initializer_list<int> li;

⚠️和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

我们使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:

1
2
3
4
5
6
void error_msg(initializer_list<string> il)
{
	for(auto beg=il.begin();beg!=il.end();++beg)
		cout<<*beg<<" ";
	cout<<endl;
}

如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:

1
2
3
4
5
//expected和actual是string对象
if (expected != actual)
	error_msg({"functionX",expected,actual});
else
	error_msg({"functionX","okay"});

含有initializer_list形参的函数也可以同时拥有其他形参:

1
2
3
4
5
6
7
void error_msg(ErrCode e, initializer_list<string> il)
{
	cout<<e.msg()<<": ";
	for(const auto &elem : il)
		cout<<elem<<" ";
	cout<<endl;
}

因为initializer_list包含begin和end成员,所以我们可以使用范围for循环处理其中的元素。

7.2.省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。

⚠️参略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

1
2
void foo(parm_list, ...);
void foo(...);

第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdarg.h>

void ArgFunc(const char *str, ...) {
    va_list ap;
    int n = 3;
    char *s = NULL;
    int d = 0;
    double f = 0.0;
    va_start(ap, str);
    s = va_arg(ap, char*);
    d = va_arg(ap, int);
    f = va_arg(ap, double);//浮点最好用double类型,而不要用float类型;否则数据会有问题 
    va_end(ap);
    printf("%s is %s %d, %f", str, s, d, f);//The answer is Hello 345, 788.234000
}

int main() {
    ArgFunc("The answer", "Hello", 345, 788.234);
    return 0;
}

首先,如果要处理不定参数的函数要包含头文件stdarg.h。然后在处理不定参数的函数中先定义一个参数列表变量va_list apva_start(ap, str)表示在str参数之后开始获取参数。并且获取参数时需指明类型,例如va_arg(ap, char*),获取第一个参数”Hello”,并指明类型为char*

8.参考资料

  1. 省略符形参