1 |
do while (0)
可以使得在该宏后添加分号的语义与单个表达式的语义一致。
1 |
|
1 | free(NULL); // 在C标准中,这是合法的,不会发生任何操作 |
If ptr is a null pointer, no action occurs.
So, no need to check for NULL before free
, it only adds more dummy code to read and is thus a bad practice.
1 |
offsetof
在给定结构体类型和一个成员名时,计算结构体中该成员相对于首地址的偏移量。这里加了取地址,从而不会解引用0->MEMBER
。container_of
在给定结构体类型,以及其中一个成员的名称及其地址时,得到结构体的首地址。这里({x;y;})
是GNU的扩展,它会计算每个表达式并返回最后一个表达式的值。此外第一行只是用于类型检查,其中typeof
也是GNU的扩展,类似C++中的decltype
。以上宏可以用于实现无侵入式的链表。
¶匿名枚举
https://stackoverflow.com/questions/7147008/the-usage-of-anonymous-enums
1 | enum { color = 1 }; |
相比于#define
更安全,相比于const int
,则不会占用空间,并且是编译期求值,可以放在类的内部(即等价于static const int
)。
¶jmp
setjmp(jmp_buf buf)
:在buf中保存当前的执行上下文,并返回0。longjmp(jmp_buf buf, i)
:加载buf保存的上下文,即恢复到上次setjmp
的位置,并使下次setjmp
返回i。
在C语言中实现错误处理(try catch)、协程等。
¶零长数组
也叫柔性数组。
1 | struct line { |
1 |
|
1 |
|
¶位域
1 | struct S |
位域成员不能取地址,也就不能用指针指向它,因为它可能不是起始于某个字节。常量引用可以绑定到位域成员。此外也不能使用sizeof
。
只有integral和enumeration类型可以指定位域。
¶restrict
类似于const和volatile的类型限定符,仅可作用于指针类型(在C++中没有类似的关键字),向编译器承诺仅能通过该指针访问指向的变量,从而进行更多优化。
比如在memmove中,src
和dst
指向的区域可能重叠,此时就不能加restrict
。而memcpy如果不考虑重叠问题,可以加restrict
关键字进行更多优化
由于在C++中,restrict
怎么处理类不好解决,所以C++标准没有添加。但主流编译器都支持__restrict
扩展,但显然不同编译器的语义可能不同。
注意getchar()
的返回值是int,要么是unsigned char,要么是EOF(通常是-1),因此应该写int c = getchar()
。
¶*alloc
void* calloc( std::size_t num, std::size_t size )
用于分配num个大小为size的元素,并且保证零初始化。而malloc
不会对分配的内存进行任何初始化。另一个calloc
的优点是它可以避免num * size
发生溢出的情况。通常calloc
会比malloc
慢。TODO:mmap会返回全零的页面。
void* alloc(size_t size)
在当前函数的栈帧内分配少量内存,并在函数返回时释放。使用场景是可变数组(以变量为长度)和可边长参数列表(va_list)。
¶I/O缓冲
- 全缓冲:仅在缓冲区满时才进行实际I/O操作
- 行缓冲:在遇到换行符时才进行实际I/O操作
- 无缓冲:不进行缓冲
stdint
和stdout
在非交互设备下是全缓冲的,而stderr
是无缓冲或行缓冲的。所以像日志这种输出到stderr
的没必要在每次输出后再刷新一下缓冲区(如果输出会换行的话)。
文本文件和二进制文件的区别是文本文件会对诸如换行符等进行处理(在windows下应为\r\n
,即回车+换行),而二进制文件只是当做普通的字节(因此,在*nix下两者没有区别)。
¶信号
int raise(int sig)
产生一个信号,成功时返回0。
void (*signal(int sig, void (*handler)(int)))(int)
对一个信号注册一个回调函数。特别地,SIG_DFL
会采用实现定义的默认行为,SIG_IGN
会忽略信号。
SIGABRT
:异常退出,如abort
SIGFPE
:算术异常,如除0和溢出SIGINT
:中断 (CTRL+C
;CTRL+D
表示EOF,用于交互程序;CTRL+Z
发送TSTP
,让程序在后台运行,可以用fg
恢复)SIGTERM
:终止请求 (kill [15]
)SIGSEGV
:段错误
¶可变参数列表
1 | va_list ap; |
¶div
div_t div(int x, int y)
返回的div_t
是有成员quot
和rem
的结构体。
double fmod(double x, double y)
计算浮点数模数。
double modf(double arg, double* ap)
返回小数部分,并在ap
中存储整数部分。
size_t strspn( const char *dest, const char *src )
返回以dest
起始的最长前缀,其只包含src
中的字符。
char *strpbrk( const char *dest, const char *breakset )
返回第一个在breakset
中出现的字符的位置
char *strtok( char *str, const char *delim )
根据delim
对str
进行split。
1 | char *token = strtok(input, " "); |
每次调用会从上个token
结束的位置开始查找不包含delim
的token
,并将结束位置永久置为’\0’后返回(因此传入的参数不是const的),每次strtok
的delim
可以不同。
¶sizeof
sizeof(type)
或sizeof expression
,表达式是可以不加括号的,但由于优先级问题以及保持一致性原则,建议永远加括号。如sizeof a + b
其实是sizeof(a) + b
。
¶函数指针
1 | void (*fp)(int) = &f; |
这两种写法都是可以的,函数名称(可以认为是函数指针常量,类比数组)和指针混用可以理解为一种语法糖,函数指针与其它指针的区别是它指向代码而不是数据。
void *__builtin_return_address(unsigned int level)
:返回当前函数或其调用者之一的返回地址void *__builtin_frame_address(unsigned int level)
: 返回函数栈帧地址(帧是堆栈上保存局部变量和寄存器的区域,帧地址是函数对应堆栈区域的起始地址)
分别在main执行前和main执行后运行。
1 | __attribute__((constructor)) |
¶format macro constants
https://en.cppreference.com/w/c/types/integer#Format_macro_constants
1 |
|
¶宏展开规则
1 |
- 如果当前宏是函数形式,则先对实参进行宏展开
- 如果当前宏包含#或者##,则直接替换 (inhibits expansion)
- 被替换后会对替换的结果进行重新扫描,而如果替换后是一个递归宏(替换结果正是又该宏产生的),不会再进行替换(被标记会"to be ignored")
1 |
|
这里EXAMPLE(5)
先被替换为EXAMPLE_ EMPTY()(5-1) (5)
,然后重新扫描后把EMPTY
替换为空,然后就结束了。
再套一层SCAN
就会强迫对整个进行扫描,于是进一步变成EXAMPLE(5-1) (5)
,然后再触发一次替换。
一个细节是函数调用中函数名和括号间是可以有空白符的,会被忽略掉。
¶Duff’s device
通过交错do-while
和switch
语句实现手动循环展开。
这里非常巧妙地利用了switch只检查第一个匹配的case,然后就永远执行fall through,因此先把多余的比特拷贝,然后执行n次字节拷贝。
当然,memcpy
肯定会更快,因为有更多系统级优化。
1 | send(to, from, count) |
¶error/warning
1 |
¶GNU IFUNC
Indirect Function
允许为一个函数创建多个实现,并在运行时根据解析器函数进行选择。
比如说memcpy函数,通常会根据当前的指令集架构选择特定的优化版本(其主要作用就是屏蔽底层架构,同一个接口,但可以为不同的架构有针对性的实现)。
1 | readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep 'IFUNC' |
1 | /* Dispatching via IFUNC ELF Extension */ |
我们需要定义一个解析器函数,该函数会在对应函数第一次运行时被调用一次。
myfunc
的符号类型是STT_GNU_IFUNC
,在第一次被调用时,其地址会被解析成相应的解析器函数(存在GOT),之后再根据解析器函数的返回地址调用实际函数(修改PLT)。
¶weak alias
将f
作为_f
的别名。可以用于hook,比如讲malloc声明为自己实现的函数的别名。
1 | void __f () { /* Do something. */; } |
¶rtdsc
x86的rtdsc指令返回CPU自启动以来的时钟周期数,即处理器的时间戳。
具体地,CPU在通电启动后,会重置寄存器EDX,EAX,然后在每个时钟周期更新EDX(高位)和EAX(低位)。
注意,在多核环境下,不同CPU的时间戳显然是存在差异的,因此如果程序发生跨核调度,就可能出现问题。
1 | uint64_t current_cycles() { |