我们都知道,如果想在函数中修改实参的值,那么,你传递给函数的应该是这个值的地址。如果你想修改的是个指针,那么是传递指针的地址(二级指针),还是传递该指针呢?
1. C语言中的函数调用
讲之前,先弄清楚一些概念:主调函数,被调函数,实参,形参。
void fun(int p) // p 是形参,是在fun()内使用的形式参数
{
p = 12;
}
void fun2(int *parray) // p 是形参,是在fun()内使用的形式参数
{
parray[3] = 5;
}
int main(void)
{
int num = 0;
int array[10] = { 0 };
fun(num); // main()是主调函数,主动调用fun()
// fun()是被调函数,被main()调用
// num 是实参,是main()传递给fun()的实际参数
fun2(array);
return 0;
}
C函数的所有参数均以“传值调用”的方式进行传递,这意味着函数将获得参数的一份拷贝,这也就是为什么很多初学者在刚学习函数的,企图修改形参值不成功的原因。(没错,就是我。。。)就是这样一个机制,保证了函数可以随心所欲的修改形参,而不会担心改变了形参的值。
如果实参是个数组名,并在被调函数内部使用间接运算符(*)或者是下标([ ])修改形参的数组元素,那么实际上修改的是实参的元素。被调函数将直接访问主调函数中所传递的数组,并不会对数组进行复制。这个行为叫做”传址调用”。
数组做实参的这种行为似乎和传值调用规则相悖。但并不是这样,首先,地址就是一串数字,只是标记了内存中的某个位置,而指针所存储的就是这串数字。本例中,假设数组array的地址是0x112233,由于数组名是个指针,传递给被调函数的是这个指针的一个拷贝,且根据传值调用,所以被调函数fun2() 的形参 parray 的值也是0x112233,而学过指针都知道,运用数组名的下标运算,可以改变数组中的某个值,所以在被调函数fun2()中,将数组的第四个元素修改了,而实参和形参指向的为同一块地址(均以0x112233开始,长度为10*sizeof(int)),所以数组array内的值就在被调函数中被修改了。
2. 被调函数修改指针所指向的值
让我们重新写一个小例子,添加打印,查看被调函数是如何修改实参的。
void fun(int *p)
{
printf("p address:%p\n", p);
*p = 12;
}
int main(void)
{
int num = 2;
printf("num address:%p\n", &num);
fun(&num);
return 0;
}
运行:
可以看到&num 和 p指向都是num的位置,所以形参p可以在被调函数中修改num的值。
这次换数组试试。
void fun(int *p)
{
printf("p address:%p\n", p);
p[3] = 12;
}
int main(void)
{
int array[10] = { 0 };
printf("array address:%p\n", array);
fun(array);
return 0;
}
运行:
指向地址相同,与上面同理。
3. 被调函数修改指针的值
之前的例子,都是修改指针所指向位置的值,这次我们来修改指针。
void fun(int *p)
{
printf("p address:%p\n", p);
p = (int *)malloc(sizeof(int));
printf("p address:%p\n", p);
}
int main(void)
{
int *ptmp = NULL;
printf("ptmp address:%p\n", ptmp);
fun(ptmp);
printf("ptmp address:%p\n", ptmp);
*ptmp = 15;
free(ptmp);
return 0;
}
插入断点,开始运行:
好,接着运行:
程序在这里挂掉了,分析一下。
根据之前的例子,我们知道被调函数传指针,形参和实参指向同一块内存。本例中,ptmp 初始值为NULL,也就是0x000000,所以刚传入fun()时,p也为NULL。接下来动态分配空间,p指向了一块不为NULL的地址。跳出函数,发现ptmp仍然为NULL,接着间接运算符对NULL进行运算,所以程序宕掉。
那为什么这次没有修改实参的值?其实文章开头给的几个例子,都是通过传递指针,来修改指针所指向空间,我们得到的结论是这种方法是可行的。那么来看这个例子,ptmp 指向NULL,fun()通过拷贝ptmp ,使p也指向NULL,随后malloc分配内存,p选择不指向NULL,转为指向新分配的内存(这个负心的p),虽然p在一顿操作,但是这一切都跟ptmp无关,你又没有改变ptmp的指向,所以ptmp依旧为NULL。
此外,由于p是局部变量,函数结束他的生命周期就结束了(活该!!!)唯一指向malloc分配内存的指针找不到了,导致没有显式的释放这块内存,造成内存泄漏。
所以得出结论:想在被调函数中修改一级指针的指向(包括malloc新内存或者free已有内存),传入一级指针是不可行的。
那么如何修改指针的值(指向)呢?简单,传二级指针就行了
由于是测试,就没写参数检测了,不过实战的时候参数检测是很重要的
void fun(int **p)
{
printf("p address:%p\n", p);
printf("*p address:%p\n\n", *p);
*p = (int *)malloc(sizeof(int));
printf("p address:%p\n", p);
printf("*p address:%p\n\n", *p);
}
void funfree(int **p)
{
printf("p address:%p\n", p);
printf("*p address:%p\n\n", *p);
free(*p);
*p = NULL;
printf("p address:%p\n", p);
printf("*p address:%p\n\n", *p);
}
int main(void)
{
int *ptmp = NULL;
printf("ptmp address:%p\n", ptmp);
printf("&ptmp address:%p\n\n", &ptmp);
fun(&ptmp);
printf("ptmp address:%p\n", ptmp);
printf("&ptmp address:%p\n\n", &ptmp);
funfree(&ptmp);
return 0;
}
运行结果:
从结果上看,传递二级指针为实参,来修改一级指针的值是可行的。这个例子对于链表的创建和释放很有帮助,可以传递链表头的地址(二级指针)来进行创建和释放链表,就不需要再用返回值的方式。
4. 总结
一句话:
想在被调函数修改0级指针的值,实参传递1级指针,
想在被调函数修改1级指针的值,实参传递2级指针,
想在被调函数修改2级指针的值,实参传递3级指针,
想在被调函数修改3级指针的值,实参传递4级指针,
…