C语言陷阱与技巧23节,指针型参数的这个小“陷阱”,很多人中招

上一节讨论了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语言编程显然就是“手动挡”,全权交给程序员把握。

阅读更多:   杂谈 , C语言
仅有 1 条评论
  1. Redmond

    最后的那个比喻非常形象 icon_lol.gif

添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif