本文首发于 2014-08-04 17:56:55

第一章 词法“陷阱”

1. =不同于==

if(x = y)
         break;

实际上是将y赋给x,再检查x是否为0。

如果真的是这样预期,那么应该改为:

if((x = y) != 0)
         break;

2. &和| 不同于 && 和 ||

3. 词法分析中的“贪心法”

编译器将程序分解成符号的方法是:从左到有一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符床是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。例如:

y = x/*p; 会被解析为:/* 注释符号

4. 整型常量

010(八进制数) 不同于 10(十进制)。

5. 字符与字符串

首先是单引号与双引号的区别:

  • 用单引号括起来的一个字符表示一个整数(ASCII码),而双引号括起来表示一个指针。

第二章 语法“陷阱”

1. 理解函数声明

弄懂(*(void(*)())0)(); //首地址为0的函数。

float (*h)(): h是一个指向返回值为浮点型的函数的指针

所以,(float (*)()) 表示一个“指向返回值为浮点型的函数的指针”的类型转换符。

fp(): 是(*fp)( )的简写。

*fp(): 是 *( (*fp) ( ) )的简写。

( *0 )( );

虽然上式编译器不认,但可以把0转换为指向“返回值为void的”函数的指针,所以0可变为: ( void(*) ( ) ) 0 ,代入(*0)(),得到:

(*( void(*) ( ) ) 0) ( )

该式子用等价于:

typedef void  ( *func ) ( );
( *( func ) 0 ) ( );

类似的,signal.h中对signal函数的声明:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

2. 运算符优先级的问题

image.png
image.png
image.png

3. 其他

主要是别多写分号,switch别忘了break,别写空else分支。

第三章 语义“陷阱”

1. 指针与数组

struct {
    Int p[4];
    Double x;
}b[17];

int calendar[12][31];
int (*p)[31];
sizeof(calendar):12*31=372

calendar[0] // 指向该一维数组,对应*p
calendar[0][0]
......
calendar[0][30]
calendar[1] // 指向该一维数组,对应*(p+1)
calendar[1][0]
......
calendar[1][30]
......
......
......
calendar[11] // 指向该一维数组,对应*(p+11)
calendar[11][0]
......
calendar[11][30]

2. 内存分配

free(r);

用malloc显式分配的空间,不会再退出本函数后自动释放掉,而是会等程序员显式释放后才消失。

注意检查,malloc分配的内存可能失败。

C语言中会自动地将作为函数参数的数组声明转换为对应的指针声明,如:

int strlen(char s[ ]){ }等价于int strlen(char *s){ }
但在其他情形下不会自动转换,也就是说不等价,如:
extern char hello[ ];和extern char *hello;完全不同。

边界计算
自己实现一个memcpy函数:

void memcpy(char *dest, const char *source, int k)
{
    while( --k >= 0 )
        *dest++ = *source++;
}

重点是:操作时一定要知道操作数据的长度。

整数溢出

  • 两个有符号整数相加会发生溢出。
  • 两个无符号整数相加不会发生溢出。
  • 一个有符号和一个无符号整数相加,因为有符号被自动转换成无符号,所以也不会溢出。

第四章 连接

编译器一般每次只处理一个文件。编译器的责任是把C源程序翻译成对连接器有意义的形式。

许多系统中的连接器是独立于C语言实现的,因此如果链接时候错误原因是与C语言相关的,连接器无法判断错误原因。但连接器能够理解机器语言和内存布局。

典型的连接器把由汇编器或编译器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体。

连接器通常把目标模块看成是由一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。static的不会与其它源程序文件中的同名函数或同名变量发生冲突。对于非satatic的函数或变量的名称冲突的解决办法将在后面讨论。

除了外部对象外,目标模块中还可能包括了对其他模块中的外部对象的引用,当连接器读入一个目标模块时,它必须解析出这些引用,并作出标记说明这些外部对象不再是未定义的。

连接器的输入是一组目标模块文件和库文件。输出是一个载入模块。

避免外部变量的函数的冲突和不一致等问题的办法:

每个外部对象只在一个头文件里声明,需要用到该外部对象的所有模块都应该包括这个头文件。

定义该外部对象的模块也应该包括这个头文件。

第五章 库函数

没什么好说的,就是apue的一些函数而已。

第六章 预处理器

宏定义:主要是理解宏不是函数,而是直接替换

  1. 不能忽视宏定义中的空格:

    #define f (x) ( (x)-1 ):因为f后面多了一个空格,所以f(x)代表(x) ( (x)-1 )
  2. 宏并不是函数,所以注意那些括号:

    #define abs(x) ( ( (x) >= 0)?(x):-(x) )
    #define max(a,b) ( (a)>(b)?(a):(b) )
  3. 宏并不是语句:

    #define assert(e) if (!e) assert_error(__FILE__, __LINE__)
  4. 宏不是类型定义

    • 错误用法:
    #define int_8_ int*
         int_8 a,b; //则a是指针,b是int型
    • 正确用法:应该用typedef
    typedef int * int_8_;

第七章 可移植性缺陷

主要是:

  1. 应对C语言标准的变更;
  2. 标识符名称的限制;
  3. 整数的大小;
  4. 字符是有符号整数还是无符号整数;
  5. 移位运算符;

    1. 在向右移位时,空出的位是由0填充还是1,还是由符号位的副本填充?如果被移位对象是无符号数,那么由0填充;如果是有符号数,那么是0或符号位的副本。
    2. 移位操作的位数允许的取值范围是什么?如果被移位对象的长度是n位,那么移位计数必须大于或等于0,而严格小于n。
  6. 移植性需考虑的地方:

    1. 机器的字符表不同。
    2. 有的机器是one's complement,有的机器是two's complement的。基于2的补码的计算机,所允许表示的附属取值范围要大于正数取值范围,所以有时取负值的运算会导致溢出。
    3. 各机器对取模运算的定义不同。

第八章 惯用与答案

将惯用的c == '\t'写作'\t' == c

一旦写错成=号,编译器就能检查出来。


欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。

欢迎关注公众号数据库内核

标题网址
GitHubhttps://dbkernel.github.io
知乎https://www.zhihu.com/people/...
思否(SegmentFault)https://segmentfault.com/u/db...
掘金https://juejin.im/user/5e9d3e...
CSDNhttps://blog.csdn.net/dbkernel
博客园(cnblogs)https://www.cnblogs.com/dbkernel

dbkernel
33 声望7 粉丝

目前从事云数据库MySQL数据库内核研发工作,曾做过Postgres-XC、Greenplum等分布式数据库的内核开发。热衷于研究主流数据库架构、源码,对关系型数据库 MySQL/PostgreSQL及分布式数据库有深入研究。