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这个关键字。