五读《Expert C Programming》
2017年03月05日 C

闲着没事儿在书架上找书看,又看到了翻得有些旧的《C专家编程》拿起来又翻了一遍,大二买的书,前后看了四遍,这是第五遍,每次看看都能有一点儿新的收获。

#pragma

是来源来Ada语言的一个编译器指示符,#pragma是由编译器来指定的具体效果,书上记录了一个很意思的关于GNU C 1.3.4版本编译器#pragma指示符的效果:

GNU C 1.3.4编译器在遇到#pragma指示符的时候,首先会尝试运行”hack”游戏,如果失败,会再尝试运行”Rogue”游戏,如果还失败,就继续尝试打开Emacs编辑器并运行”汉诺塔”游戏,最后再失败,编译器才报错。

「Rogue」: Unix上的迷宫探索游戏,具体可参见此Wiki

「Hack」: 地牢探索游戏,具体参见Wiki

几个经常用的#pragma

  1. 编译时打印消息 #pragma message("compile message")

  2. 屏蔽特定编译警告

    1
    2
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "具体的警告字符串"
  3. 微软系的msvc使用方法和GCC的不同

    1
    2
    #pragma  warning( push ) 
    #pragma warning( disable: 具体的警告代码数字 ) // 可以写多个警告的代码
  4. 设置内存对齐 #pragma pack(内存对齐值)

    1
    #pragma  pack(push 4) // 设置4个字节内存对齐
  5. gcc的pragma文档可以在这里找到。

NUL与NULL

平时用习惯了,真正说一下二个的区别可能并不能说得特别清楚。

NUL表示ASCII码结束的字符,即是'\0'的字符名字,就是空字符的意思,但是不要误认为这是C的的一个宏定义,这样写是不正确的:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[])
{
char str[8] = "Hello";
str[5] = NUL; // 并没有预定义的一个叫NUL的宏
printf("%s\n", str);
return(0);
}

NULL表示0,内存的0位置(内存地址较低的位置都是操作系统预留的),表示空指针,即指针没有指向任何内容,指针初始化为空的时候都使用NULL赋值。

setjmp longjmp

C语言中比goto更强大的改变控制流的机制,平常很少用,经常忘记用法。

setjmp(jmp_buf j)是一个宏,调用处就是标记当前的调用环境的上下文,保存当前的调用堆栈,用来调用longjmp的时候将控制流返回到这里再次执行,第一次调用setjmp时一定返回0。

longjmp(jmp_buf j, int return_value)这里的j就是setjmp时设置jmp_buf(保证jmp_buf不变),return_value就是指定控制流返回setjmp时,setjmp再调用一遍时返回的值。

这个强大控制流转移机制,可以实现C语言的异常处理,看个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <setjmp.h>

static jmp_buf context;

void memory_handler(void *mem_pointer)
{
if (!mem_pointer)
{
longjmp(context, 1);
}
// other logic ...
}

int main(int argc, char *argv[])
{
void *pointer = NULL;
int rval = setjmp(context);
if (rval)
{
printf("return value %d\n", rval);
switch (rval)
{
case 1:
printf("nullpointerexception check memory alloc\n");
break;
// ... other handler
}
}
else
{
memory_handler(pointer);
}
return 0;
}

memory_handler发现pointer是空指针之后重新将控制流程移回setjmp带着新的返回值1,然后就进入第一个分支对应的处理语句,最终输出nullpointerexception check memory alloc

goto语句只能在同一个函数中跳转到一个指定标签处的代码,而longjmp可以跨越函数跳转,是C中一个强大的机制。

使用setjmp时需要注意一点,如果setjmp不在main函数中调用而是在其他自定义函数中调用,那一旦这个自定义调用的函数返回,对应记录的调用上下文环境的jmp_buf也就失效了,longjmp再调用时,就回不到jmp_buf记录的控制流处了,像下面这个例子,代码不会有任何输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <setjmp.h>

static jmp_buf context;

void longjmp_exp()
{
longjmp(context, 2);
}
int setjmp_within_func()
{
return setjmp(context);
}

int main(int argc, char *argv[])
{
int rval = setjmp_within_func();
if (rval)
{
printf("return value %d\n", rval);
}
else
{
longjmp_exp();
}
return 0;
}