menu XUJINKAI 的个人主页
create settings
home
主页
list
全部博文
  • Collection
  • About
  • assistant_photo
    个人项目
    person
    关于

    C语言踩坑记(一)

    Tags: C

    C语言是个很原始,同时很有趣的语言。通过C语言当中的各种踩坑填坑,能窥探到很多计算机底层原理,也能更透彻的理解其他高级语言特性的由来。

    typedef与类型转换

    曾经我对typedef这个关键字寄予厚望,代码中很多类似以下这种东西:

    typedef double Age;
    typedef double Weight;
    

    然后,我希望用这些新定义的类型作为参数类型,当传入错误的类型时,编译器可以给我警告,这样可以避免错误。

    Weight get_Weight();
    Age a = get_Weight(); // 希望能给我警告,然而并不会报错或警告
    

    但令人失望的是,typedef的作用仅仅是一个alias(别名),并不是新的类型,也不会有警告发生。

    typedef仅仅是一个高级版本的#define,他解决了#define的一些问题,但也仅此而已。

    后来在网上查到,为了实现定义新类型的效果,可以用结构体把值包起来。

    typedef struct {
        double Value;
    } Weight;
    

    我终于知道结构体struct in_addr里边为什么只有一个值了!

    函数中的静态变量

    以下代码,猜猜运行后会输出什么。

    #include <stdio.h>
    #include <string.h>
    
    static char *_fn(int n)
    {
        static char buf[32];
        memset(buf, 0, sizeof(buf));
        sprintf(buf, "%d", n);
        return buf;
    }
    
    int main()
    {
        printf("%s", _fn(1));
        printf(", %s, %s\n", _fn(2), _fn(3));
        return 0;
    }
    

    经过测试,结果可能是1, 2, 2(gcc),也可能是1, 3, 3(clang)

    原因很好解释,_fn函数返回的字符串地址是固定的,第二行printf会对_fn函数求两次值,于是第一次执行的结果被覆盖了。而求值顺序没有规定,所以不同的编译器会给出不同的结果。

    实践中,很多系统函数,例如inet_ntoa这个函数,都有这个问题,比如以下代码:

    printf(", %s, %s", inet_ntoa(*(struct in_addr*)&ip1), inet_ntoa(*(struct in_addr*)&ip2));
    

    如果夹杂在一大堆业务代码里,还挺难发现的。

    解决办法,就是永远不要(轻易)在函数里使用静态变量。对这类系统函数,就自己封装一层吧。

    扩展魔法:__attribute__((cleanup()))

    资源必须手动释放,是C语言永远的痛,幸亏,编译器提供了自动清理资源的扩展魔法。

    为了测试这个扩展的具体原理,我写了以下测试代码。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    // f是清理函数,函数类型为 void (*)(void*)
    // 注意:f的参数是指针的地址
    #define _CLEANUP_(f) __attribute__((cleanup(f)))
    
    // 定义一个自动free变量的宏
    #define AUTO_FREE _CLEANUP_(__malloc_free__)
    void __malloc_free__(void *addr)
    {
        printf("%p  free   %s\n", *(void **)addr, *(char **)addr);
        free(*(void **)addr);
        *(void **)addr = NULL;
    }
    
    char *my_malloc(const char *text, int count)
    {
        char *p = malloc(strlen(text) + 11);
        memcpy(p, text, strlen(text) + 1);
        if (count >= 0)
            sprintf(p + strlen(text), " %d", count);
        printf("%p  malloc %s\n", p, p);
        return p;
    }
    
    int main()
    {
        AUTO_FREE char *fun = my_malloc("function scope", -1);
        {
            AUTO_FREE char *block = my_malloc("block scope", -1);
        }
        for (size_t i = 0; i < 2; i++)
        {
            AUTO_FREE char *loop = my_malloc("loop", i);
        }
        {
            AUTO_FREE char *p1 = my_malloc("assign", 0); // 1, will not free
            p1                 = my_malloc("assign", 1); // 2
            // p1              = "can't free";           // 3, crash
        }
        AUTO_FREE char *p2 = NULL;
        p2                 = my_malloc("auto", -1); // works good
        return 0;
    }
    

    gcc和clang的行为一致,结果如下:

    0x7fffefc132a0  malloc function scope
    0x7fffefc136e0  malloc block scope
    0x7fffefc136e0  free   block scope
    0x7fffefc136e0  malloc loop 0
    0x7fffefc136e0  free   loop 0
    0x7fffefc136e0  malloc loop 1
    0x7fffefc136e0  free   loop 1
    0x7fffefc136e0  malloc assign 0
    0x7fffefc13700  malloc assign 1
    0x7fffefc13700  free   assign 1
    0x7fffefc13700  malloc auto
    0x7fffefc13700  free   auto
    0x7fffefc132a0  free   function scope
    

    可以得出结论,这个扩展会在作用域结束时,对申明的变量自动调用指定的清理函数。

    拿上文中block这个变量举例,等同于以下代码:

    {
        char *block = my_malloc("block scope", -1);
        // ...
        __malloc_free__(&block);
    }
    

    不过这其中有个问题,如果中途把变量改了,那之前的资源不会释放(注释1处,assign 0并未释放),更糟的是,如果把变量改成了无法释放的内容,那程序会崩掉(注释3处)。

    看来使用这个扩展也需要小心,一个办法是,把变量指针声明为const类型,避免修改。

    #define AUTO_FREE(T, var) _CLEANUP_(__malloc_free__) T const var
    // 同时,清理函数也要修改: void __malloc_free__(void const *addr)
    

    这样,使用AUTO_FREE(char*, p)声明的变量,会扩展为char *const类型,就无法对变量p再次赋值了。

    说到这里,也许之后可以聊聊const这个关键字。


    Disqus评论加载中。。。