C语言中指针的理解3:指针与函数传参

1.函数传参时不同类型参数的表现

1.1 普通变量作为函数形参

函数传参时,普通变量作为参数,形参和实参名字可以相同也可以不同。在子函数内部,形参的值等于实参,原因是函数调用时把实参的值赋值给了形参。这就是很多书上写的“传值调用”,相当于实参做右值,形参做左值。

举个栗子:

#include<stdio.h>

void func(int a)
{
    
    
	printf("in func,a = %d.\n",a);	
	printf("in func,&a = %p.\n",&a);	   
}

int main(int argc,char**argv)
{
    
    
	int b = 5;
	printf("in main,b = %d.\n",b);	
	printf("in main,&b = %p.\n",&b);

	func(b);
	
	return 0;
}

输出:

in main,b = 5.
in main,&b = 0x7ffc7d6526a4.
in func,a = 5.
in func,&a = 0x7ffc7d65267c.

可以看出,实参b的值被赋给了形参a,但是它们的地址并不一样,所以不是同一个变量。

1.2 数组作为函数形参

数组名作为形参传参时,实际传递是不是整个数组,而是数组的首元素的首地址,也就是整个数组的首地址。因为传参时是传值,所以这两个其实没区别。所以在子函数内部,传进来的数组名就等于是一个指向数组首元素首地址的指针。所以sizeof得到的是一个指针变量的大小。

在子函数内传参得到的数组首元素首地址,和外面得到的数组首元素首地址的值是相同的,即这两个指针指向同一个内存地址。这种特性叫做"传址调用",也就是调用子函数时传了地址(也就是指针),此时可以通过传进去的地址来访问实参。

数组作为函数形参时,[]里的数字是可有可无的。这是因为数组名做形参传递的实际只是个指针,根本没有数组长度这个信息。

1.3 指针作为函数形参

指针作为函数形参和数组作为函数形参是一样的,这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的。

1.4 结构体变量作为函数形参

结构体变量作为函数形参的时候,实际上和普通变量(类似于int之类的)传参时表现是一模一样的,所以说结构体变量其实也是普通变量而已。

但是因为结构体一般占用的内存空间都很大,所以如果直接用结构体变量进行传参,那么函数调用效率就会很低。因为在函数传参的时候需要将实参赋值给形参,该过程实际上是内存中数据的复制,所以当传参的变量越大调用时的效率就会越低。这个问题的解决思路就是不要传变量,而是传变量的指针(地址)进去,因为指针的大小是固定的。当然程序员也可以自己决定,非要传结构体变量过去C语言也是允许的,只是效率低了。从某个方面来说,这也是为什么C语言设计的时候数组传参默认是传的数组首元素首地址而不是整个数组了。

C++中,函数传参常传引用,而不是整个对象,其实也是这个原因。

2.传值调用与传址调用

举个栗子:

#include<stdio.h>

void swap1(int a,int b)
{
    
    
	int tmp=a;
	a=b;
	b=tmp;
}

void swap2(int* pa,int* pb)
{
    
    
	int tmp=*pa;
	*pa=*pb;
	*pb=tmp;
}

int main(int argc,char**argv)
{
    
    
	int a = 10,b = 20;
	printf("初始时a=%d,b=%d\n",a,b);
	
	swap1(a,b);//传a和b的值
	printf("swap1后,a=%d,b=%d\n",a,b);
	
	swap2(&a,&b);//传a和b的地址,&是取地址符
	printf("swap2后,a=%d,b=%d\n",a,b);
	
	return 0;
}

运行结果:

初始时a=10,b=20
swap1后,a=10,b=20
swap2后,a=20,b=10

传值调用描述的是这样一种现象:ab作为实参,自己并没有真身进入swap1()函数内部,而是将自己数据拷贝了子函数中的形参,副本具有和自己一样的值,但是是不同的变量,进入子函数swap1(),然后我们在子函数swap1()中交换的实际是副本而不是ab真身。所以在swap1()内部确实是交换了,交换的是函数内部的形参,但是实际外部的ab根本没有受影响。

swap2()ab真的被改变了,但是ab真身还是没有进入swap2()函数内,而是swap2()函数内部跑出来把外面的ab真身改了。实际上实参ab永远无法真身进入子函数内部,进去的只能是一份拷贝,但是在swap2()我们把ab的地址传进去给子函数了,于是乎在子函数内可以通过指针解引用方式从函数内部访问到外部的ab真身,从而改变ab

结论:这个世界上根本没有传值和传址这两种方式,C语言本身函数调用时一直是传值的,只不过传的值可以是变量名,也可以是变量的指针,通过变量名只能拷贝到变量的值,但是通过变量指针的拷贝,却可以通过指针的解引用来访问到指针指向的空间。

3.输入型参数与输出型参数

3.1 函数为什么需要形参与返回值

函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。函数体是函数的关键,由一对{}括起来,包含很多句代码,函数体就是函数实际做的工作。

形参列表和返回值:形参是函数的输入部分,返回值是函数的输出部分。对函数最好的理解就是把函数看成是一个加工机器(程序其实就是数据加工器),形参列表就是这个机器的原材料输入端,而返回值就是机器的成品输出端。其实如果没有形参列表和返回值,函数也能对数据进行加工,用全局变量即可。用全局变量来传参和用函数参数列表返回值来传参各有特点,在实践中都有使用。总的来说,函数参数传参用的比较多,因为这样可以实现模块化编程,而C语言中也是尽量减少使用全局变量。

全局变量传参最大的好处就是省略了函数传参的开销,所以效率要高一些。但是实际开发中用的最多的还是传参,如果参数很多传参开销非常大,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去。

3.2 函数传参中使用const指针

const一般用在函数参数列表中,用法是const int *p;,这句话的意义是指针变量p本身可变的,而p所指向的变量是不可变的。const用来修饰指针做函数传参,作用就在于声明在函数内部不会改变这个指针所指向的内容,所以给该函数传一个不可改变的指针(比如char *p = "linux";这种)不会触发错误;而一个未声明为const的指针的函数,给它传一个不可更改的指针的时候就要小心了。

3.3 函数如何向外部返回多个值

一般来说,函数的输入部分就是函数参数,输出部分就是返回值。问题是函数的参数可以有很多个,而返回值只能有1个。这就导致无法让一个函数返回多个值。

现实编程中,一个函数需要返回多个值是非常普遍的,因此完全依赖于返回值是不靠谱的,通常的做法是用参数来做返回。在典型的Linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败。

一个普遍做法是,编程中函数的输入和输出都是靠函数参数的,返回值只是用来表示函数执行的结果是对(成功)还是错(失败)。如果这个参数是用来做输入(让函数来接收需要处理的数据)的,就叫输入型参数;如果这个参数的目的是用来做输出(让外部接收函数处理结果)的,就叫输出型参数。

3.4 总结

看到一个函数的原型后,如何才能一眼看出来哪个参数做输入哪个做输出呢?

函数传参如果传的是普通变量而不是指针,那这个参数肯定是输入型参数;如果传指针就有2种可能性了,为了加以区别,通常的做法是:如果这个参数是做输入的,通常做输入的在函数内部只需要读取这个参数的值而不会需要更改它,那么就在指针前面加const来修饰;如果函数形参是指针变量并且还没加const,那么就表示这个参数是用来做输出型参数的,这个参数在函数中往往会被修改。

猜你喜欢

转载自blog.csdn.net/PecoHe/article/details/114262428