c snippets

(18 mins to read)
1
2
3
4
5
#define die(msg)                                      \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)

do while (0)可以使得在该宏后添加分号的语义与单个表达式的语义一致。

1
2
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// 在sizeof中数组类型是不会退化的
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
2
3
4
5
#define offsetof(TYPE, MEMBER) ((int)&(((TYPE *)0)->MEMBER))

#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

offsetof在给定结构体类型和一个成员名时,计算结构体中该成员相对于首地址的偏移量。这里加了取地址,从而不会解引用0->MEMBERcontainer_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
2
3
4
5
6
7
8
9
struct line {
int length;
char contents[0]; // or char contents[]
};

struct line *thisline = malloc(sizeof(struct line) + this_length);
thisline->length = this_length;
assert(sizeof(contents) == 0);
// 在结构体外也能声明int a[0],但是不能int a[],其实类似于一个label
1
2
3
#define LIKELY(x) __builtin_expect(!!(x), 1) // x很可能为真
#define UNLIKELY(x) __builtin_expect(!!(x), 0) // x很可能为假
// __builtin_expect(exp, c)表示exp等于c的概率很大,用于引导CPU的分支预测
1
2
3
4
#include <linux/prefetch.h>
#define prefetch(x) __builtin_prefetch(x)
#define prefetchw(x) __builtin_prefetch(x,1)
__builtin_prefetch(const void *addr, int rw, int locality) // 主动将数据读取到addr中,rw=1表示可写,0表示只读

位域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct S
{
// will usually occupy 2 bytes:
// 3 bits: value of b1
// 5 bits: unused
// 2 bits: value of b2
// 6 bits: unused
unsigned char b1 : 3;
unsigned char :0; // start a new byte; 0只能作用在匿名的变量上,用于后续的强制对齐
unsigned char b2 : 2;
};
sizeof(S) == 2;
struct test {
// Unsigned integer member x
unsigned int x;
// Bit-field member y with 33 bits
unsigned int y : 33;
// Unsigned integer member z
unsigned int z;
};
sizeof(test) = 16;
std::uint8_t b : 1000; // 当指定的大小大于类型的大小时,值域会被类型限制
// 对于无符号类型的位域,溢出会自动取模,是预期行为
// 多余的位数仅仅充当padding

位域成员不能取地址,也就不能用指针指向它,因为它可能不是起始于某个字节。常量引用可以绑定到位域成员。此外也不能使用sizeof

只有integral和enumeration类型可以指定位域。

restrict

类似于const和volatile的类型限定符,仅可作用于指针类型(在C++中没有类似的关键字),向编译器承诺仅能通过该指针访问指向的变量,从而进行更多优化。

比如在memmove中,srcdst指向的区域可能重叠,此时就不能加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操作
  • 无缓冲:不进行缓冲

stdintstdout在非交互设备下是全缓冲的,而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+CCTRL+D表示EOF,用于交互程序;CTRL+Z发送TSTP,让程序在后台运行,可以用fg恢复)
  • SIGTERM:终止请求 (kill [15])
  • SIGSEGV:段错误

可变参数列表

1
2
3
4
va_list ap;
va_start(ap, arg); // 使ap指向最后一个命名参数arg
int va_arg(ap, int); // 移动到下一个参数,并用指定的type类型解释
va_end(ap);

div

div_t div(int x, int y)返回的div_t是有成员quotrem的结构体。

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 )根据delimstr进行split。

1
2
3
4
5
char *token = strtok(input, " ");
while(token) {
puts(token);
token = strtok(NULL, " "); // 传入NULL以继续处理第一次调用时的input
}

每次调用会从上个token结束的位置开始查找不包含delimtoken,并将结束位置永久置为’\0’后返回(因此传入的参数不是const的),每次strtokdelim可以不同。

sizeof

sizeof(type)sizeof expression,表达式是可以不加括号的,但由于优先级问题以及保持一致性原则,建议永远加括号。如sizeof a + b其实是sizeof(a) + b

函数指针

1
2
3
4
5
void (*fp)(int) = &f;
(*fp)(10);
void (*fp)(int) = f;
fp(10);
void g(void fp(int)); // 在函数参数中函数类型会隐式转为指针(就像数组名会退化为指针一样)

这两种写法都是可以的,函数名称(可以认为是函数指针常量,类比数组)和指针混用可以理解为一种语法糖,函数指针与其它指针的区别是它指向代码而不是数据。

void *__builtin_return_address(unsigned int level):返回当前函数或其调用者之一的返回地址void *__builtin_frame_address(unsigned int level): 返回函数栈帧地址(帧是堆栈上保存局部变量和寄存器的区域,帧地址是函数对应堆栈区域的起始地址)

分别在main执行前和main执行后运行。

1
2
__attribute__((constructor))
__attribute__((destructor))

format macro constants

https://en.cppreference.com/w/c/types/integer#Format_macro_constants

1
2
#include <inttypes.h>
printf("num1 %" PRIi64 " num2 %" PRIu64, num_i64, num_u64);

宏展开规则

1
#define identifier(a1, a2, ..., an) replacement-list
  • 如果当前宏是函数形式,则先对实参进行宏展开
  • 如果当前宏包含#或者##,则直接替换 (inhibits expansion)
  • 被替换后会对替换的结果进行重新扫描,而如果替换后是一个递归宏(替换结果正是又该宏产生的),不会再进行替换(被标记会"to be ignored")
1
2
3
4
5
6
#define EMPTY
#define SCAN(x) x
#define EXAMPLE_() EXAMPLE
#define EXAMPLE(n) EXAMPLE_ EMPTY()(n-1) (n)
EXAMPLE(5)
SCAN(EXAMPLE(5))

这里EXAMPLE(5)先被替换为EXAMPLE_ EMPTY()(5-1) (5),然后重新扫描后把EMPTY替换为空,然后就结束了。

再套一层SCAN就会强迫对整个进行扫描,于是进一步变成EXAMPLE(5-1) (5),然后再触发一次替换。

一个细节是函数调用中函数名和括号间是可以有空白符的,会被忽略掉。

Duff’s device

通过交错do-whileswitch语句实现手动循环展开。

这里非常巧妙地利用了switch只检查第一个匹配的case,然后就永远执行fall through,因此先把多余的比特拷贝,然后执行n次字节拷贝。

当然,memcpy肯定会更快,因为有更多系统级优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
send(to, from, count)
register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}

error/warning

1
2
3
#error message // 生成一个编译错误消息,并停止编译
#warning message // 生成一个编译警告消息,但是不停止编译
#line number filename // 用于强制指定新的行号和编译文件名 (即重定义`__LINE__`和`__FILE__`)

GNU IFUNC

Indirect Function

允许为一个函数创建多个实现,并在运行时根据解析器函数进行选择。

比如说memcpy函数,通常会根据当前的指令集架构选择特定的优化版本(其主要作用就是屏蔽底层架构,同一个接口,但可以为不同的架构有针对性的实现)。

1
readelf -s /lib/x86_64-linux-gnu/libc.so.6  | grep 'IFUNC'
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
/* Dispatching via IFUNC ELF Extension */
# include <stddef.h>

extern void foo(unsigned *data, size_t len);

void foo_c(unsigned *data, size_t len) { /* ... */ }
void foo_sse42(unsigned *data, size_t len) { /* ... */ }
void foo_avx2(unsigned *data, size_t len) { /* ... */ }

int cpu_has_sse42() {
return __builtin_cpu_supports("sse4.2");
}
int cpu_has_avx2() {
return __builtin_cpu_supports("avx2");
}

void foo(unsigned *data, size_t len) __attribute__((ifunc ("resolve_foo")));

static void *resolve_foo(void)
{
if (cpu_has_avx2())
return foo_avx2;
else if (cpu_has_sse42());
return foo_sse42;
else
return foo_c;
}

我们需要定义一个解析器函数,该函数会在对应函数第一次运行时被调用一次。

myfunc的符号类型是STT_GNU_IFUNC,在第一次被调用时,其地址会被解析成相应的解析器函数(存在GOT),之后再根据解析器函数的返回地址调用实际函数(修改PLT)。

weak alias

f作为_f的别名。可以用于hook,比如讲malloc声明为自己实现的函数的别名。

1
2
void __f () { /* Do something. */; }
void f () __attribute__ ((weak, alias ("__f")));

rtdsc

x86的rtdsc指令返回CPU自启动以来的时钟周期数,即处理器的时间戳。

具体地,CPU在通电启动后,会重置寄存器EDX,EAX,然后在每个时钟周期更新EDX(高位)和EAX(低位)。

注意,在多核环境下,不同CPU的时间戳显然是存在差异的,因此如果程序发生跨核调度,就可能出现问题。

1
2
3
4
5
uint64_t current_cycles() {
uint32_t low, high;
asm volatile("rdtsc" : "=a"(low), "=d"(high));
return ((uint64_t)low) | ((uint64_t)high << 32);
}