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