C语言拾遗

关键字(Keyword)

asm

asm是gcc提供的用于内联汇编(Inline Assembly)的扩展语法.

asm不支持-std=c99,可以使用__asm__-std=gnu99

其中,"=r"为操作数的约束(Constraint),"="表示允许覆盖该操作数,"r"表示允许使用寄存器.

1
2
3
int c;
asm("movl $0, %0; addl $1, %0":"=r"(c));
printf("%d\n", c); // 1

在MSVC中则可使用_asm__asm

1
2
3
4
5
6
int a = 5, b;
__asm {
mov eax, a
mov b, eax
}
printf("%d\n", b); // 5

case

可以用...指定case的范围.

1
2
3
case 'A' ... 'Z':
printf("UPPER!\n");
break;

enum

将整数强制转换为枚举量是允许的.

1
2
3
4
enum Weekday { SUN, MON, TUE, WED, THU, FRI, SAT } day;
day = (enum Weekday)2; // Correct!
day = 2; // ERROR!
int a = SUN; // Correct!

C23中,可以为枚举指明底层类型.

1
2
3
enum : size_t {
BUFFER_LENGTH = 1024
}

goto

gcc中可以用__label__定义局部标签(Local Label).

You can get the address of a label defined in the current function (or a containing function) with the unary operator&&. The value is a constant and has type void*.

1
2
3
const void* arr[] = { &&label1, &&label2, &&label3 };
...
goto *arr[i];

sizeof

sizeof是编译时行为,运行时不执行.

1
2
3
int a = 5;
sizeof(a++);
printf("%d\n", a); // 5

typedef

在语法分析时,typedef和存储类型指示符是等价的,它不能和存储类型指示符同时使用.

1
2
const int typedef CINT; // Correct!
typedef static int SINT; // ERROR!

typeof

1
2
3
4
5
#define max(a,b) ({ \
typeof (a) _a = (a); \
typeof (b) _b = (b); \
_a > _b ? _a : _b; \
})

字符串(String)

字面值常量(Literal)

字面值常量可直接进行拼接操作,第一个字符串中的\0将被第二个字符串的第一个字符取代.

1
2
3
const char* s1 = "Hello " "World!\n";
const char* s2 = "Hello World!\n";
printf("%d\n", s1 == s2); // 1

可对字面值常量寻址.

1
char c = "0123456789"[n % 10];

格式(Formatting)

%p

1
printf("%p\n", pointer);

%m

1
printf("%m\n"); // Success
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <errno.h>

int main(void) {
FILE * fp;
char filename[] = "illegal.txt";
fp = fopen(filename, "rb");
if (fp==NULL) {
fprintf(stderr, "%m\n", filename); // No such file or directory
}
else {
fclose(fp);
}
return 0;
}

%n

现在多数编译器已不支持.

1
2
int n;
printf("Hello\n%n", &n); // 6

联合体(Union)

1
2
3
4
5
static union {
char c[4];
unsigned long l;
} endian_test = { {'l', '?', '?', 'b'} };
#define ENDIANNESS ((char)endian_test.l)

结构体(Structure)

gcc允许没有成员的结构体,其长度为零.

1
struct empty { };

C99允许如下的结构体初始化.

1
div_t answer = {.quot = 2, .rem = -1 };

数组(Array)

若数组名用于sizeof的操作数,则表示这个数组类型;而sizeof以外的其他场合,数组名总会被转换为指向该数组起始元素的指针.将数组作为函数参数也是毫无意义的,参数的数组声明总是被转换为相应的指针声明.

任何一个数组下标运算都等同于一个对应的指针运算.假定有数组a和整型i,由于a+ii+a的含义相同,因此a[i]i[a]具有相同的含义,但不推荐后者的写法.

对于二维数组,可定义「行指针」char (*p)[LEN];和「列指针」char* p;

C99允许形如(int[]){0}的匿名数组,允许形如int a[10] = {[1 ... 5] = 1};的初始化.

零长数组(Zero-Length Array)

GNU C允许长度为零的数组.零长数组可在结构体的最后一个元素以指示变长对象.

1
2
3
4
5
6
7
struct line {
int length;
char contents[0];
};

struct line* thisline = (struct line *)malloc(sizeof(struct line) + this_length);
thisline->length = this_length;

变长数组(VLA; Variable Length Array)

C99特性,编译期无法知道变长数组大小,周期属auto类,声明时不可初始化.

1
2
3
int get(size_t n, int a[n][n], size_t i, size_t j) {
return a[i][j];
}

可以书写上例中get()省略参数名的原型.

1
int get(int, int[*][*], int, int);

函数(Function)

C中允许嵌套函数,且可以捕获外部变量,注意C++不允许嵌套函数.

K&R风格函数定义(K&R Style Function Definition)

现在多数编译器已不支持这种旧式风格函数定义.

1
2
3
int isvowel(c) char c; {
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}

可变参数函数(Variadic Function)

stdarg.h

内存(Memory)

三种存储类型及其使用时机如下.

  • 自动存储(Automatic Storage):通常称为栈(Stack),存储局部变量,函数被调用时创建自动存储区域,函数返回时该区域被删除,函数的返回值被复制到调用它的函数的自动存储区域中,这意味着返回指向局部变量的指针是不安全的;
  • 分配的存储(Allocated Storage):通常称为堆(Heap),生命周期由malloc()free()
  • 静态存储(Static Storage):存储全局变量,在程序启动时分配,在程序的整个生命周期有效.

位运算(Bitwise Operation)

获取整型a的第b位.

1
int getbit(int a, int b) { return (a >> b) & 1; }

将整型a的第b位设置为0.

1
int unsetbit(int a, int b) { return a & ~(1 << b); }

将整型a的第b位设置为1.

1
int setbit(int a, int b) { return a | (1 << b); }

将整型a的第b位取反.

1
int flapbit(int a, int b) { return a ^ (1 << b); }

表达式(Expression)

三元表达式?:可以二元使用,x ?: y表示若x非零则为x,否则为y,即x ? x : y

GNU C将括号语句块视为表达式.若语句块中最后的表达式无值(void)则报错.

1
printf("%d\n", ({1; 2; 3;}) == 3); // 1

表达式求值可能导致副作用(Side Effect),如访问volatile左值对象、修改对象、调用库I/O函数,或调用执行上述任何操作的函数.

序列点(Sequence Point)被定义为程序中的一些执行点,在该点之前的求值的所有副作用已经发生,在该点之后的求值的所有副作用仍未开始.

未定义行为(UB; Undefined Behavior)

访问未定义局部变量是未定义行为.

1
2
int x;
printf("%d", x); // UB

有符号整数算术溢出是未定义行为.

1
2
int x = INT_MAX;
x++; // UB

访问或解引用空指针是未定义行为.

1
2
3
int* p = NULL;
printf("%d", *ptr); // UB
*p = 0; // UB

无关地址的比较是未定义行为.

1
2
3
int a = 0;
int b = 0;
return &a < &b; // UB

数组索引越界是未定义行为.

1
2
3
char a[5];
a[5] = 10; // UB
a[-1] = 5; // UB

修改字符串字面量是未定义行为.

1
2
char* p = "wikipedia";
p[0] = 'W'; // UB

整数除以零是未定义行为.

1
2
int x = 1;
int y = x / 0; // UB

对某个对象非顺序点修改,或跨顺序点修改时,读取该对象的值用于除「确定其存储的值」之外的其他任何目的,都属于未定义行为.

1
2
3
int f(int i) {
return i++ + i++; // UB
}
1
a[i] = i++; // UB
1
printf("%d %d\n", ++n, power(2, n)); // UB

对某值进行负数位移,或位移值大于等于该值总位数,都属于未定义行为.

1
2
int num = -1;
unsigned int val = 1 << num; // UB
1
2
int num = 32;
unsigned int val = 1 << num; // UB

返回值函数的结尾没有return语句,调用时会产生未定义行为.

1
int f() {} // UB

注释(Comment)

利用行注释和块注释的特性,仅更改一个符号就能快速切换注释区域.

1
2
3
4
5
//*
int foo(); // this code is currently active
/*/
int bar(); // this code is currently disabled
//*/
1
2
3
4
5
/* <- just remove the leading slash
int foo(); // this code is now disabled
/*/
int bar(); // this code is now active
//*/

达夫设备(Duff's Device)

过去为提高效率而设计,现在已不必使用,但展现了神奇的语法规范.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void send(char* to, char* from, int count) {
int 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);
}
}

平方根倒数速算法(Fast Inverse Square Root)

出自游戏「雷神之锤III竞技场(Quake III Arena)」的源代码,内容如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
float Q_rsqrt(float number) {
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = *(long*) &y; // evil floating point bit level hacking
i = 0x5f3759df - (i >> 1); // what the fuck?
y = *(float*) &i;
y = y * (threehalfs - (x2 * y * y)); // 1st iteration
// y = y * (threehalfs - (x2 * y * y)); // 2nd iteration, this can be removed
return y;
}

平方根倒数速算法说明

由于需要求所给数的平方根倒数,默认所给数为正数且是规格化浮点数.以表示尾数区域对应的无符号数,表示阶码区域对应的无符号数,y二进制浮点数算数标准IEEE754下对应的实际值如下式.

对上式取底数为2的对数,整理得下式. 等价无穷小可知,加入校正系数使总体偏差最小,进而可得下式. 上式中即IEEE754中单精度浮点数形式对应的整数值,即i.代码第7行为二进制浮点数形式y至其对应二进制整数形式i的转换,各个位的内容不变,因此并非强制类型转换,转换目的是便于后续的移位操作;代码第9行逆转换为原来的二进制浮点数形式.

设解,取底数为2的对数,整理得下式. 式代入上式,整理得下式. 上式即代码第8行,右端第一项即魔数(Magic Number)0x5f3759df,第二项即-(i >> 1)

代码第10行和第11行使用牛顿迭代法优化近似值.式用于求的平方根倒数,以之构造如下式. 将上式代入牛顿迭代法的公式,整理得下式. 上式即代码第10行和第11行,右端即表达式y * (threehalfs - (x2 * y * y))

三字符组(Trigraph)

三字符组又译作三合字母,现在多数编译器默认忽略三字符组,需要-trigraphs手动开启.

1
2
3
4
5
int main(void)
??<
printf("what??!\n"); // what|
return 0;
??>

转义字符\?可防止对三字符组进行解释.

预处理器(Preprocessor)

可以用#include的方式初始化大型数组.

1
2
3
double a[SIZE][SIZE] = {
#include "values.txt"
}

若宏定义包含多条语句,可以使用do { } while(0)避免展开后的问题.

1
#define FUNC() do { foo(); bar(); } while(0)

__FILE____LINE__是内建于预处理器中的宏,它们会被扩展为所在文件的文件名和所处代码行的行号.

预处理器中有如下的特殊的操作符.

  • #:将宏参数的代码字符串转化为字符串常量;
  • ##:连接两个代码字符串;
  • #@:将宏参数的代码转换为字符常量.

可变参数宏(Variadic Macro)

C99/C++特性,使用...表示,__VA_ARGS__为替换列表,##__VA_ARGS__表示当可变参数为0时,去除前面的逗号.

借此可以书写宏的「参数数量重载」版本.

1
2
3
4
5
6
#define ONE(a) printf("1\n")
#define TWO(a, b) printf("2\n")
#define THREE(a, b, c) printf("3\n")

#define GET_MACRO(_1, _2, _3, NAME, ...) NAME
#define MACRO(...) GET_MACRO(__VA_ARGS__, THREE, TWO, ONE, ...)(__VA_ARGS__)

通用选择(Generic Selection)

C11特性,使用_Generic在编译时根据控制表达式类型选择表达式.

借此可以为不同类型的函数提供重载.

1
#define sin(X) _Generic((X), long double: sinl, default: sin, float: sinf)(X)

结构体打包指令(Structure-Packing Pragma)

结构体打包指令可更改结构体成员的最大对齐.

1
#pragma pack(1)

参数是较小的2的幂次,不指定参数将设置为默认值.

#pragma pack(push[,n])可将对齐设置送至内部堆栈,而#pragma pack(pop)恢复栈顶的设置并删除该堆栈条目,注意#pragma pack(n)不影响该内部堆栈.

最终,结构体占用空间也与数据在其中的排列顺序有关.

内置函数(Builtin)

内置函数以__builtin开头,这些函数接受unsigned int,也有对应的longlong long版本(在函数名后加上后缀即可,如__builtin_clzll).

__builtin_ffs(n)(Find First Signed)返回n的最后一位1从后向前的次序(n为0时返回0).

1
printf("%d\n", __builtin_ffs(4) == 3); // 1

__builtin_clz(n)(Count of Leading Zeros)返回前导0的个数.

1
inline int highbit(int x) { return 31 - __builtin_clz(x); }

__builtin_ctz(n)(Count of Trailing Zeros)返回后尾0的个数,与__builtin_clz相对.

__builtin_popcount(n)返回二进制表示中1的个数,即汉明权重(Hamming Weight).__builtin_parity(n)即二进制表示中1的个数的奇偶性(奇数个为1).

可以使用__builtin_expect为编译器提供分支预测信息以减少指令跳转带来的性能下降,从而优化代码.

1
2
#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

标准库(Standard Library)

time.h

time.h用于操作日期和时间.

纪元(Epoch)开始于UTC时间1970年1月1日00:00.time()可以获取纪元起的秒数的整数值time_ttime()的定义如下.

1
time_t time(time_t* arg);

time()既可将当前时间作为返回值,也可存储于arg指向的time_t对象(当arg为非空指针时).

可以通过如下代码测量实际用时.

其中:clock()返回程序执行起处理器时钟所用的时间;CLOCKS_PER_SEC是每秒走过的计时数,CLK_TCK与之等效,但被视为已过时.

1
2
3
4
5
clock_t start, stop;
start = clock();
MyFunc();
stop = clock();
double duration = ((double)(stop - start)/CLOCKS_PER_SEC);

标准日期时间字符串的格式为Www Mmm dd hh:mm:ss yyyy\n.将时间转换为文本表示的函数如下.

  • asctime():将日历时间struct tm转换为文本表示;
  • ctime():将纪元起的时间time_t转换为文本表示.

strftime()可将日历时间转换为自定义文本表示,使用方法如下例.

1
2
if (strftime(buff, sizeof buff, "...", &my_tm)) puts(buff);
else puts("strftime failed");

时间格式修饰符

时间格式修饰符汇总如下.

格式修饰符 说明
%Y 以4位十进制数写年.
%y 以末2位十进制数写年.
%B 写完整月名.
%b 写缩略月名.
%m 以十进制数写月.
%U 以十进制数写年的星期(以星期日为首日).
%W 以十进制数写年的星期(以星期一为首日).
%j 以十进制数写年的第几日.
%d 以十进制数写月的第几日.
%A 写完整星期名.
%a 写缩略星期名.
%w 以十进制数写星期日期(其中星期日为0).
%H 以十进制数写时(24小时制).
%I 以十进制数写时(12小时制).
%M 以十进制数写分.
%S 以十进制数写秒.
%c 写标准日期时间字符串.

纪元起的时间与日历时间的相互转换如下.

  • gmtime():将纪元起的时间time_t转换为协调世界时的日历时间struct tm
  • localtime():将纪元起的时间time_t转换为本地时间表示的的日历时间struct tm
  • mktime():将日历时间struct tm转换为纪元起的时间time_t

stdarg.h

用于实现可变参数函数,内容如下.

  • va_list:指向参数列表,本质上是char*
  • va_start:初始化,需要提供va_list和函数最后一个明确的参数作为函数参数;
  • va_arg:得到列表的下一个变参值,需要提供va_list和参数的类型;
  • va_end:释放,设为无效指针,提供va_list即可.

典型的可变参数函数如printf(),标准库对printf()的定义如下.

1
int printf(const char *format, ...);

非标准库(Nonstandard Library)

conio.h

回显(Echo)指显示正在执行的批处理命令及执行的结果等.getch()用于不回显地获取字符,也常用于任意键继续的程序暂停.

而在Visual Studio中,使用返回两次的_getch()

When reading a function key or an arrow key, _getch() must be called twice; the first call returns 0 or 0xE0, and the second call returns the actual key code.

io.h

io.h提供如下内容.

  • _finddata_t:文件数据类型;
    • attrib:文件属性,如_A_ARCH(存档)、_A_HIDDEN(隐藏)、_A_NORMAL(正常)、_A_RDONLY(只读)、_A_SUBDIR(文件夹)、_A_SYSTEM(系统)等;
    • size:以字节为单位的文件大小;
    • name:文件名;
    • time_create:文件创建时间;
    • time_access:上次文件被访问时间;
    • time_write:上次文件被修改时间.
  • _findfirst:获取第一个文件,返回当前位置句柄,需要提供路径字符串和指向_finddata_t的指针作为函数参数;
  • _findnext:获取下一个文件,返回下一位置句柄,需要提供当前位置句柄和指向_finddata_t的指针作为函数参数;
  • _findclose:释放,提供句柄即可.

遍历目录下所有文件的代码如下.

1
2
3
4
5
6
7
8
9
intptr_t handle;
_finddata_t fileInfo;
if ((handle = _findfirst(path, &fileInfo)) == -1) {
exit(EXIT_FAILURE);
}
do {
...
} while (!_findnext(handle, &fileInfo));
_findclose(handle);

windows.h

SetConsoleTextAttribute()用于设置控制台文本属性,需要提供控制台缓冲区屏幕句柄和属性组合.

关于颜色的文本属性的解释,有RED、GREEN、BLUE三种原色,INTENSITY表示颜色高亮,FOREGROUND表示前景色即文本颜色,而BACKGROUND表示背景色.下例将文本颜色设置为亮紫色.

其中,GetStdHandle(STD_OUTPUT_HANDLE)用于获取标准输出句柄.

1
2
WORD attr = FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE;
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), attr);

文本边框属性

文本边框属性汇总如下.

Character Attributes Meaning
COMMON_LVB_GRID_HORIZONTAL Top horizontal.
COMMON_LVB_GRID_LVERTICAL Left vertical.
COMMON_LVB_GRID_RVERTICAL Right vertical.
COMMON_LVB_UNDERSCORE Underscore.

若需获取控制台文本属性,则需使用GetConsoleScreenBufferInfo()

1
2
3
CONSOLE_SCREEN_BUFFER_INFO csbiInfo;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbiInfo);
WORD attr = csbiInfo.wAttributes;