指针与函数传参的思考
先看代码,这个代码很简单,看你能不能准确地说出答案。
#include <stdio.h>
struct A {
int a;
};
struct A g_ta = {
.a = 1,
};
struct A g_tb = {
.a = 2,
};
void fun1(struct A * p1)
{
p1->a = 3;
}
void fun2(struct A * p2)
{
p2 = &g_tb;
}
void fun3(struct A ** p3)
{
*p3 = &g_tb;
}
int main()
{
struct A *p = &g_ta;
printf("p->a = %d\n",p->a);
fun1(p);
printf("p->a = %d\n",p->a);
fun2(p);
printf("p->a = %d\n",p->a);
fun3(&p);
printf("p->a = %d \n",p->a);
return 0;
}
gcc编译,运行结果如下:
p->a = 1
p->a = 3
p->a = 3
p->a = 2
对了吗?如果你对了,说明你对指针和函数参数传递已经理解。
如果你和我一样,答案和打印结果相悖,继续看。
第一行和第二行输出应该没问题,很简单的,是通过指针方式进行传值的,所以改变形参的值,实参也会跟着改变。
再看第三行输出,为什么经过fun2 后值不是p->a = 2,而是p->a = 3?fun2不也是通过指针方式来进行传值的吗?怎么没有改变p的值呢?
再看第四行输出,p->a = 2?怎么形参为指针的指针就对了呢?
带着疑问看下面:
首先不要觉得指针是个很神奇的东西,我之前一直对指针不理解,或者是理解不透彻,这两天在写电子书的代码,感觉对指针这个东西有了些顿悟,我可以告诉你指针就是一个变量,他和别的变量(比如int,char)没有什么本质的区别,都是一个内存A里存放变量的值!唯一区别就是指针被多设计了一个 * 号,该 * 号的意思就是将这个内存A里面的值当成另一个内存B的地址,并取出这个内存B的值,说到这里,这就是指针的全部!
下面是函数参数传递,回头看我说的一句话“通过指针方式进行传值的,所以改变形参的值,实参也会跟着改变”,这句话其实是障眼法,学C语言的时候老师就告我们,值传递方式,形参改变不能影响实参,而地址传递传递是可以的,这句话不能算错,但是不能说对,地址传递方式形参改变确实能影响实参,但是也是要看改变的是什么,影不影响实参是有道理的!
为了更好得说明,我将内存抽块象成如下小方块,一个方块代表一块内存。
另注,1. 我将一个结构体看成一块小内存,2. 变量的内存地址是我假设的。3.地址就是指此块内存的地址,每块内存都有自己地址。
变量 |
值 |
地 址 |
现在开始分析代码:
首先申明两个struct A全局变量,其内存模型如下,(地址是我假设的,以下都是,不再赘述)
g_ta
g_ta |
1 |
0x10 |
g_tb
g_tb |
2 |
0x11 |
在main数里面申明了结构体A指针,其指向g_ta,其内存模型如下
P
p |
0x10 |
0x50 |
注意看,p的值就是 0x10,g_ta的地址,没什么特别的。
现在开始分析fun2,fun2弄懂了fun1自然就理解了,首先p经过了fun1之后,将g_ta里面的值变成了3,所以,现在他们内存情况如下:(这三块内存方格是分开的,贴到这里就连在一起了,以下同样,不再赘述。。。)
g_ta g_tb p
g_ta |
3 |
0x10 |
g_tb |
2 |
0x11 |
p |
0x10 |
0x50 |
调用fun2(p);
| |
\/
void fun2(struct A * p2)
{
p2 = &g_tb;
}
进入函数fun2,会生成一个和形参类型相同副本,其将实参值拷贝,即结构体指针p2,其内存模型如下
p p2
p |
0x10 |
0x50 |
p2 |
0x10 |
0xa0 |
可以看到,他和指针p的不同就是内存地址不同,而他们的值是相同的,都表示g_ta的地址。
p2 = &g_tb;
这句就是将g_tb的地址赋值给p2,那么p2内存模型就会变成如下:
p p2
p |
0x10 |
0x50 |
p2 |
0x11 |
0xa0 |
好了,p2的值不是改变了吗,p2确实指向了g_tb了啊?怎么打印得不对呢?哈哈,你再看看,printf打印得是p啊,p指向的还是是g_ta,当然输出的是3了啊!p2在退出fun2时候就自动销毁了!
跟着内存模型来看,很容易理解吧,那下面继续来看fun3,fun3为什么就能正确地打印出g_tb的值呢?继续用内存模型来分析:
此时内存情况如下:
g_ta g_tb p
g_ta |
3 |
0x10 |
g_tb |
2 |
0x11 |
p |
0x10 |
0x50 |
执行函数fun3(&p);
| |
\/
void fun3(struct A ** p3)
{
*p3 = &g_tb;
}
经过fun3,将p的地址 0x50传给了fun3,这个0x50没有什么特别得,他就是一个数值,对于fun3来说他根本不知道这个0x50代表什么意思!所以我们就要告诉fun3,这个0x50是个内存地址,这个地址内存里面的内容仍然是一个地址!转变成C语言来理解就是要设计一个形参,这个形参能够进行两次取内存值的操作,好,struct A ** p3就应运而生了!
p3:指针的指针,感觉好复杂啊!其实一点都不复杂,说到底p3就是一个指针嘛,不过他指向的内存里面的数据也是指针类型罢了,既然p3还是一个指针那再来看看他的内存模型
g_ta g_tb p p3
g_ta |
3 |
0x10 |
g_tb |
2 |
0x11 |
p |
0x10 |
0x50 |
P3 |
0x50 |
0xa1 |
进过fun3,实参传递了p的地址0x50给p3,所以p3的值就是0x50,看清楚了!
执行*p3= &g_tb;
*p3就是取地址为0x50内存的值,地址为0x50的内存里面存放是谁的值啊?
对!就是p的值,所以这条代码其实改变了的是p的值,将g_tb的值赋给了p!
g_ta g_tb p p3
g_ta |
3 |
0x10 |
g_tb |
2 |
0x11 |
p |
0x11 |
0x50 |
P3 |
0x50 |
0xa1 |
好了,基本都已经清楚了,退出fun3,p3销毁,打印p指向的值,就是p->a = 2
分析已经结束了,还有fun1,可以自己用这种内存模型方法来自己分析一下 :)
总结:想要改变指针指向的内容,就传指针给函数,想要改变指针的值,那就得传递指针的地址了,相应的函数参数就要设计成指针的指针了!