c语言和c++语言中函数参数的传递

其实关于函数参数的传递,我一直以来的理解还算到位的。但是经常会有一些稀奇古怪的写法会让我有点懵圈,仔细想清楚了,就会发现都是一样的道理,不过要是我来写我可能会那样写,而不这样写,在这里稍微吐槽一下c语言关于数组指针的很多写法,真是很难理解了。写这个博客希望自己从此不要被很多拗口的写法吓住,抓住本质的东西,写自己的代码。

1 函数参数传递的本质

在调用一个函数时进行参数传递(不只是讲参数列表里的参数,包括函数返回值的参数传递),其本质上进行的工作都是一样的,即使用实参初始化形参

实参与形参本质上是两个完全不同的变量,它们之间并没有更深入的联系,仅仅只是变量与初始值的关系而已。

1.1 传值参数(包括传指针)

很普通的那种,大家都了解的差不多。 > 在此处需要强调一下,所谓传值,其实是指在使用实参初始化形参时,将实参的值拷贝一份到形参。此处我将传指针也归纳到了传值这边,因为都有拷贝操作。但是此处需要稍微提一下,有几种类型(也许还有其他?以后遇到会补充)是不能通过这种形式进行拷贝的(也就是不能进行真正意义上的传值操作),那就是数组与函数(还有IO对象如cin、cout等)。所以当参数列表或者返回值类型中如果出现数组名与函数名(只要参数类型不是引用),编译器会自动将其转换成常量指针类型,然后再使用这个常量指针进行传值操作。

例子(函数指针):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<string>
using std::string;
bool useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &)){
return pf(s1,s2);
}
//细节:函数的类型只与函数的参数还有返回类型有关,与函数名无关
bool lengthCompare(const string &s1, const string &s2){
if(s1.size()>s2.size()) return true;
else return false;
}
int main(){
//此处自动将函数lengthCompare转换成指向该函数的指针
useBigger("1234","123456",lengthCompare);
}

1.2 传引用参数

传引用的方式也是使用实参初始化形参,但是它与传值完全不同,它并没有拷贝操作,而是利用引用的特点,将引用类型的形参绑定到实参上。从而达到可以直接操作实参的效果。c语言中不存在引用,所以需要使用指针来完成类似的操作。(多嘴一句,引用在编译器底层其实是通过常量指针实现的)

一个经典的例子如下:

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
void reset(int &i){
i=0;
}
int main(){
int j=42;
reset(j);
cout<<"j="<<j<<endl;
}

如上例,在调用reset函数时,使用实参初始化形参实际相当于int j=42; int &i=j;因为形参为一个引用,并且被绑定到变量j上,因此可以通过i对变量j的值进行修改。这样就可以替代指针的部分作用了,而且更简单。 

同时,使用传引用调用还有其他一些好处: 1. 使用引用避免拷贝,提高效率(进行大的类类型对象的拷贝很低效);因为引用类型不是一个对象,而仅仅是一种绑定关系,为已存对象另取了一个名字而已。

  1. 可以传递额外信息,因为引用参数可以改变原变量值,所以并不是只有返回值可以传递信息了,参数也可以(这点指针形参也可以做到)。

注意的问题:

  1. 如上int &i类型的形参在传入实参时只能是变量,不能是字面值常量,因为其不能用字面值常量初始化;

  2. 如果想要使得函数实参可以传入字面值常量,形参需要改成const int &i形式,底层const的引用类型可以使用字面值常量初始化,一般只要不会对参数进行修改,就将其设置成底层const的引用;

科普一下,const可以分为顶层const与底层const两种。一般对象只会有顶层const,表示对象本身是常量不能修改;而对于指针与引用变量除了顶层const外(表示自身是常量,一般只对指针而言,引用一般只关心底层const),还有底层const,表示自身指向或者引用的对象是常量。

例子:

1
2
3
4
5
int i = 0;
int *const p1 = &i; //不能改变p1的值,顶层const
const int ci = 42; //顶层const
const int *p2 = &ci; //底层const
const int &r1 = i; //底层const,不能通过r1改变i的值

1.3 main函数参数:处理命令行选项

1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main(int argc, char *argv[]){
cout<<"argc = "<< argc << endl;
for(int i=0;i<argc;i++)
cout<<"argv["<< i << "] = "<< argv[i] <<endl;
return 0;

如上,是主函数的带参数形式,此时在将源文件编译生成可执行文件后,运行时可以带参数。举个例子,比如编译生成的可执行文件叫做main_arg,则可以输入如下命令执行:

./main_arg -o -d data0 

输出如下结果

1
2
3
4
5
6
7
8
xhy@ubuntu:~/cpp_learn/6/ch06$ ./main_arg -o -d data0 
argc = 4
argv[0] = ./main_arg
argv[1] = -o
argv[2] = -d
argv[3] = data0
xhy@ubuntu:~/cpp_learn/6/ch06$

如上可以知道,其中第一个参数int argc为命令行中字符串的数量,后面char *argv[]为一个数组,数组元素为一个指向char *类型的指针,指向一个c风格的字符串。最后一个指针之后的元素值保证为0(因此不需要argc其实也能确定是否读完了参数)。

在这里了要科普一下(引用也一样):

1
2
int *matrix[10];	//10个指针组成的数组
int (*matrix)[10] //一个指向含有十个整数的数组的指针
这两种书写形式含义是不一样的。其中*优先级小于[],对于int (*matrix)[10]可以按如下顺序来理解该声明的含义: 1. *matrix表示对变量matrix进行解引用操作;

  1. (*matrix)[10]表示解引用后将得到一个大小为10的数组;

  2. int (*matrix)[10]表示数组中的元素是int类型。

同理,对于int *matrix[10]可以按如下顺序来理解该声明的含义:

  1. matrix[10]表示matrix是一个大小为10的数组;

  2. *matrix[10]表示数组元素是指针类型;

  3. int *matrix[10]表示数组元素时候int的指针类型。

其实这么写可能比较易读,但是不方便,上述main函数其实还有一种写法是:

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

之所以有这第二种写法,是因为前文中提到过,数组是不能使用传值操作的,所以传递数组其实是将数组名转换成了指针,所以一个指针的数组其实在传值操作时被转换成了一个指针的指针。并且一般情况下,我写这第二种形式比较习惯一点。指针的指针。

参考资料

  • [1] C++ Primer(第5版)

c语言和c++语言中函数参数的传递
http://line.com/2018/09/09/2018-09-09-c-cpp-argu-passing/
作者
Line
发布于
2018年9月9日
许可协议