通过一个基本原则和两个例子讲述传值引用和传指针引用的区别,并且从系统底层解析这两种调用的区别
- acmore
- 2017.11.14
1. 问题概述
在C语言调用函数时,有两种传参数的方法,一种是传值,另一种是传参数。对于C语言的初学者(甚至是一些C语言的熟练使用者)而言,区别这两种形式往往比较困难,再加上C语言有指针,以及指针的指针等等,复杂多变的形式会使得这个过程更加困难。
很多初学者会尝试通过形式来记忆这两种方式,比如函数声明中有*
的就是传指针,没有的就是传值,但是如果传入的参数本身就是一个指针,无论是传值还是传指针,函数声明都会有*
,这种时候原有的形式记忆不再有效,需要再次记忆另外一套形式。形式是无穷无尽的,只有明白指针类型的本质,以及在函数调用过程中系统里到底发生了什么,才能真正理解这两种方式。
2. 简单认识
现在我们有两个函数,作用都是修改一个int
类型的值,分别如下:
#include <stdio.h>
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 <stdio.h>
#include <malloc.h>
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 <stdio.h>
#include <malloc.h>
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
的值是
create
函数时,就是图上的①过程,这个时候v1
的值被拷贝到v
中,这时候如果直接对v
操作是没有问题的,可以改变v1
的值,因为它们俩指向的是同一块内存空间(事实上此时还没有指向任何有意义的空间),但是在随后的赋值语句中,即②过程,v
的值立刻被替换为
v
进行的操作其实是对
v
,也就是
#include <stdio.h>
#include <malloc.h>
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. 最后总结
- 指针没有什么特殊的,基本数据类型怎么对待,指针就怎么对待
- 要理解函数调用过程中进程地址空间中的变化,它能直观地体现传值引用和传指针引用的区别,并解决在使用过程中出现的问题
- 不要死记硬背形式,任何问题尝试回归底层理解