上一节讨论了C语言函数返回超多值的两种方法。说是两种方法,但其实归根结底,都是尽量将多个返回值放在一片内存里,然后通过指针访问它们。这种情况下,C语言函数只需要能够将这片内存的起始地址返回给调用者就可以了。
从这也能看出C语言指针的强大,一个指针和恰当的数据结构,C语言程序能够传递任意多的数值。当然了,C语言指针使用起来也是非常灵活的,从之前两节能够看出,我们甚至可以使用函数的参数作“返回值”使用:
void fun(int *a)
{
*a = 3;
}
int caller()
{
int x = 0;
fun(&x);
// x 等于 3
return x;
}
上面这段C语言代码中,caller() 函数在调用 fun() 函数时,将自己局部变量 x 的地址传递给 fun() 了,所以 fun() 可以直接修改 x 的值,这看起来很像 fun() 函数“返回”了一个值。
参数式返回值存在的“陷阱”
但是,根据上一节最后的讨论,这样的代码风格需要保证 caller() 在 fun() 完成之后返回。因为如果 caller() 提前返回,它的局部变量 x 会被系统回收,这样一来,fun() 函数将操作一个“野指针”,带来的危害性是不言而喻的。
看到这里,可能有读者会感到迷惑,caller() 调用了 fun(),它必须在 fun() 返回之后才会返回啊,怎么可能会在 fun() 返回之前返回呢?就上面这段C语言代码而言,caller() 的确不会在 fun() 完成之前返回,但是如果 fun() 创建了一个线程,且该线程继续使用指向 caller() 中局部变量 x 的 a,一切就不一样了。
请看下面这段C语言代码:
void *thread(void *p)
{
pthread_detach(pthread_self());
sleep(1);
*((int*)p) = 2;
}
void fun(int *a)
{
pthread_t pid;
pthread_create(&pid, NULL, thread, a);
}
int caller()
{
int x = 0;
fun(&x);
// x 等于 3
return x;
}
这里为了讨论主题,C语言代码没有做错误处理。
fun() 函数创建线程函数很快就可以完成,但是假如它创建的线程函数 thread() 稍后才会使用 caller() 的局部变量 x,就会出现 caller() 返回导致局部变量 x 被系统回收后,仍然有C语言函数使用它,这就会引起比较严重的错误。
当然了,上面这段C语言代码并没有什么实际意义,但是C语言的指针不仅仅可以用于“返回”值,也可以用于传递参数,假设 caller() 产生若干个参数,需要传递给 fun(),并且C语言程序不希望 fun() 阻塞,那么相应的C语言代码应该如下写:
struct S{
char c;
int i;
double d;
};
void *thread(void *param)
{
pthread_detach(pthread_self());
sleep(1);
struct S *p = (struct S*)param;
...
p->i = p->c +3;
p->d = 0.22;
process(p);
}
void fun(struct S *s)
{
pthread_t pid;
pthread_create(&pid, NULL, thread, s);
}
int caller()
{
...
struct S s;
s->c = 3;
s->i = 4;
s->d = 5.5;
fun(&s);
return 0;
}
但是这样的代码依旧会存在上面分析的那种隐患,caller() 退出后局部变量 s 被回收,会导致 thread() 产生严重错误,因为 thread() 的参数 param 仍然指向 s。
解决问题
仔细思考下,不难发现,问题的根源在于 caller() 退出 s 被回收,导致 thread() 操作野指针。如果传递给 fun() 的参数不会被回收,显然就不会存在这个问题了。
我们已经知道C语言中的全局变量不是存在栈区的,它的生命周期与程序一致,因此如果下面这样的C语言代码应该是没有问题的:
struct S s;
int caller()
{
...
fun(&s);
return 0;
}
只不过全局变量在C语言多线程编程中需要小心的做好同步保护,使用起来比较麻烦,所以在实际的C语言项目开发中,除非逼不得已,一般都不轻易使用全局变量,更推荐的做法是下面这样的,请看:
int caller()
{
...
struct S *s = (struct S*)malloc(sizeof(struct S));
fun(s);
return 0;
}
上面这段C语言代码使用 malloc() 在堆上分配了一块内存给 fun() 使用,caller() 退出后,系统不会自动回收堆,因此也就避免了上面的“陷阱”,并且 s 也不是全局变量,私密性比较好。
void *thread(void *param)
{
...
p->i = p->c +3;
p->d = 0.22;
process(p);
free(p);
}
不过要小心,在使用完毕 s 后,要记得释放,否则会导致所谓的“内存泄露”。
小结
本节主要讨论了C语言函数利用参数“返回”值时的陷阱,其实归根结底,就是C语言指针的使用注意事项,一定要避免操作“野指针”,这就要求C语言程序员始终知道自己分配的内存的生命周期。看到这里,不知道大家发现没有,如果把编程比作开车,那么C语言编程显然就是“手动挡”,全权交给程序员把握。
最后的那个比喻非常形象