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;
}
1 |
|
如上例,在调用reset
函数时,使用实参初始化形参实际相当于int j=42; int &i=j;
因为形参为一个引用,并且被绑定到变量j
上,因此可以通过i
对变量j
的值进行修改。这样就可以替代指针的部分作用了,而且更简单。
同时,使用传引用调用还有其他一些好处: 1. 使用引用避免拷贝,提高效率(进行大的类类型对象的拷贝很低效);因为引用类型不是一个对象,而仅仅是一种绑定关系,为已存对象另取了一个名字而已。
- 可以传递额外信息,因为引用参数可以改变原变量值,所以并不是只有返回值可以传递信息了,参数也可以(这点指针形参也可以做到)。
注意的问题:
如上
int &i
类型的形参在传入实参时只能是变量,不能是字面值常量,因为其不能用字面值常量初始化;如果想要使得函数实参可以传入字面值常量,形参需要改成
const int &i
形式,底层const的引用类型可以使用字面值常量初始化,一般只要不会对参数进行修改,就将其设置成底层const的引用;
科普一下,const可以分为顶层const与底层const两种。一般对象只会有顶层const,表示对象本身是常量不能修改;而对于指针与引用变量除了顶层const外(表示自身是常量,一般只对指针而言,引用一般只关心底层const),还有底层const,表示自身指向或者引用的对象是常量。
例子: 1
2
3
4
5int 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;
1 |
|
如上,是主函数的带参数形式,此时在将源文件编译生成可执行文件后,运行时可以带参数。举个例子,比如编译生成的可执行文件叫做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$
1 |
|
如上可以知道,其中第一个参数int argc
为命令行中字符串的数量,后面char *argv[]
为一个数组,数组元素为一个指向char *
类型的指针,指向一个c风格的字符串。最后一个指针之后的元素值保证为0(因此不需要argc其实也能确定是否读完了参数)。
在这里了要科普一下(引用也一样): 1
2int *matrix[10]; //10个指针组成的数组
int (*matrix)[10] //一个指向含有十个整数的数组的指针*
优先级小于[]
,对于int (*matrix)[10]
可以按如下顺序来理解该声明的含义:
1. *matrix
表示对变量matrix
进行解引用操作;
(*matrix)[10]
表示解引用后将得到一个大小为10的数组;int (*matrix)[10]
表示数组中的元素是int类型。
同理,对于int *matrix[10]
可以按如下顺序来理解该声明的含义:
matrix[10]
表示matrix是一个大小为10的数组;*matrix[10]
表示数组元素是指针类型;int *matrix[10]
表示数组元素时候int的指针类型。
其实这么写可能比较易读,但是不方便,上述main函数其实还有一种写法是:
1
int main(int argc, char **argv){}
1 |
|
之所以有这第二种写法,是因为前文中提到过,数组是不能使用传值操作的,所以传递数组其实是将数组名转换成了指针,所以一个指针的数组其实在传值操作时被转换成了一个指针的指针。并且一般情况下,我写这第二种形式比较习惯一点。指针的指针。
参考资料
- [1] C++ Primer(第5版)