通过一个基本原则和两个例子讲述传值引用和传指针引用的区别,并且从系统底层解析这两种调用的区别
acmore
2017.11.14
1. 问题概述
在C语言调用函数时,有两种传参数的方法,一种是传值,另一种是传参数。对于C语言的初学者(甚至是一些C语言的熟练使用者)而言,区别这两种形式往往比较困难,再加上C语言有指针,以及指针的指针等等,复杂多变的形式会使得这个过程更加困难。
很多初学者会尝试通过形式来记忆这两种方式,比如函数声明中有*的就是传指针,没有的就是传值,但是如果传入的参数本身就是一个指针,无论是传值还是传指针,函数声明都会有*,这种时候原有的形式记忆不再有效,需要再次记忆另外一套形式。形式是无穷无尽的,只有明白指针类型的本质,以及在函数调用过程中系统里到底发生了什么,才能真正理解这两种方式。
2. 简单认识
现在我们有两个函数,作用都是修改一个int类型的值,分别如下:
#include
void modify1(int a) {
a = 10086;
}
void modify2(int *a) {
*a = 10086;
}
int main() {
int x = 1;
printf("%d\n", x);
modify1(x);
printf("%d\n", x);
int y = 1;
printf("%d\n", y);
modify2(&y);
printf("%d\n", y);
return 0;
}
运行程序,会输出什么呢?
1
1
1
10086
这个程序很简单,我们可以轻易看出modify1是传值引用,modify2是传指针引用。传值引用不会改变调用者的值,而传指针会改变调用者的值,并且在形式上,函数声明中有*,函数调用有&。因此我们这样记忆:只要改变了调用者的值,并且有*的函数就是传指针引用。
真的是这样吗?
3. 深入理解
下面我们定义一个结构体,这个结构体含有两个整数值,然后类似地写出两个修改函数:
#include
#include
typedef struct Value {
int a;
int b;
}Value;
void modify1(Value *v) {
v->a = 10086;
v->b = 10010;
}
void modify2(Value **v) {
(*v)->a = 10086;
(*v)->b = 10010;
}
int main() {
Value *v1 = (Value *) malloc(sizeof(Value));
v1->a = v1->b = 1;
printf("%d\t%d\n", v1->a, v1->b);
modify1(v1);
printf("%d\t%d\n", v1->a, v1->b);
Value *v2 = (Value *) malloc(sizeof(Value));
v2->a = v2->b = 1;
printf("%d\t%d\n", v2->a, v2->b);
modify2(&v2);
printf("%d\t%d\n", v2->a, v2->b);
return 0;
}
输出结果如下:
1 1
10086 10010
1 1
10086 10010
这次我们再按照之前的经验去判断,好像有点行不通,因为在对modify1和modify2的调用上虽然像是一个传值一个传指针,但是两个函数的声明中都有*,并且v1和v2的值都被改变了,经验不再适用!
这种时候我们应该回归底层,老老实实从系统层面理解在程序执行过程中到底发生了什么,我们首先应该记住的一个最重要的原则是:
- 指针类型和基本类型一样,本质没有什么区别
我们拿类型int和Value *做比较,根据上述原则,便有以下几条原则:
int在内存中占有一块空间,那么Value *同样会在内存中占有一块空间
可以得到一个指针指向int,那么也可以有一个指针指向Value *
使用&可以取得int的指针,那么同样可以使用&取得Value *的指针
传值引用时,有变量int a和Value* v,下边两种写法都是传值引用
操作int时,函数的参数类型就是int,调用函数时传入a
同理在操作Value *时,函数的参数类型就是Value *,调用函数时传入v
传指针引用时,下边两种写法都是传指针引用
操作int时,函数的参数类型是int *,调用函数时传入&a
操作Value *时,函数的参数类型是Value **,调用函数时传入&v
根据以上原则,不要把指针当做一个特殊的类型,把指针类型那一堆当成一个整体,传值时直接丢进去,传指针时就在后边加一个*,在对应的调用前加上&即可。
4. 问题拓展
现在我们要完成一个这样的函数:它可以代替malloc函数来对我们刚刚声明的一个指针进行赋值,在第三部分中我们了解到,在操作指针类型时,传值引用一样可以改变调用者的值,所以我们写出了下边的程序:
#include
#include
typedef struct Value {
int a;
int b;
}Value;
void create(Value *v) {
v = (Value *) malloc(sizeof(Value));
v->a = 10086;
v->b = 10010;
}
int main() {
Value *v1;
create(v1);
printf("%d\t%d", v1->a, v1->b);
return 0;
}
结果输出如下:
-16219251 -970326017
并没有符合我们的期待!说好的操作指针就可以高枕无忧了呢?这个时候我们需要从系统底层了解传值和传指针时到底发生了什么,如下图所示
首先v1的值是(p1p2p3p4p5p6p7p8)(32位系统为例),当调用create函数时,就是图上的①过程,这个时候v1的值被拷贝到v中,这时候如果直接对v操作是没有问题的,可以改变v1的值,因为它们俩指向的是同一块内存空间(事实上此时还没有指向任何有意义的空间),但是在随后的赋值语句中,即②过程,v的值立刻被替换为(k1k2k3k4k5k6k7k8),之后对v进行的操作其实是对(k1k2k3k4k5k6k7k8)指向的空间的操作,这时候根本没v,也就是(p1p2p3p4p5p6p7p8)指向的空间什么事,因此不能达到我们想要的效果。如果想要完成修改,仍需要传指针引用,修改后的代码如下:
#include
#include
typedef struct Value {
int a;
int b;
}Value;
void create(Value **v) {
(*v) = (Value *) malloc(sizeof(Value));
(*v)->a = 10086;
(*v)->b = 10010;
}
int main() {
Value *v1;
create(&v1);
printf("%d\t%d", v1->a, v1->b);
return 0;
}
5. 最后总结
指针没有什么特殊的,基本数据类型怎么对待,指针就怎么对待
要理解函数调用过程中进程地址空间中的变化,它能直观地体现传值引用和传指针引用的区别,并解决在使用过程中出现的问题
不要死记硬背形式,任何问题尝试回归底层理解