C语言拾遗
关键字(Keyword)
asm
asm
是gcc提供的用于内联汇编(Inline Assembly)的扩展语法.
asm
不支持-std=c99
,可以使用__asm__
或-std=gnu99
.
其中,
"=r"
为操作数的约束(Constraint),"="
表示允许覆盖该操作数,"r"
表示允许使用寄存器.
1 | int c; |
在MSVC中则可使用_asm
或__asm
.
1 | int a = 5, b; |
case
可以用...
指定case
的范围.
1 | case 'A' ... 'Z': |
enum
将整数强制转换为枚举量是允许的.
1 | enum Weekday { SUN, MON, TUE, WED, THU, FRI, SAT } day; |
C23中,可以为枚举指明底层类型.
1 | enum : size_t { |
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 | const void* arr[] = { &&label1, &&label2, &&label3 }; |
sizeof
sizeof
是编译时行为,运行时不执行.
1 | int a = 5; |
typedef
在语法分析时,typedef
和存储类型指示符是等价的,它不能和存储类型指示符同时使用.
1 | const int typedef CINT; // Correct! |
typeof
1 |
字符串(String)
字面值常量(Literal)
字面值常量可直接进行拼接操作,第一个字符串中的\0
将被第二个字符串的第一个字符取代.
1 | const char* s1 = "Hello " "World!\n"; |
可对字面值常量寻址.
1 | char c = "0123456789"[n % 10]; |
格式(Formatting)
%p
1 | printf("%p\n", pointer); |
%m
1 | printf("%m\n"); // Success |
1 |
|
%n
%n
现在多数编译器已不支持.
1 | int n; |
联合体(Union)
1 | static union { |
结构体(Structure)
gcc允许没有成员的结构体,其长度为零.
1 | struct empty { }; |
C99允许如下的结构体初始化.
1 | div_t answer = {.quot = 2, .rem = -1 }; |
数组(Array)
若数组名用于sizeof
的操作数,则表示这个数组类型;而sizeof
以外的其他场合,数组名总会被转换为指向该数组起始元素的指针.将数组作为函数参数也是毫无意义的,参数的数组声明总是被转换为相应的指针声明.
任何一个数组下标运算都等同于一个对应的指针运算.假定有数组a
和整型i
,由于a+i
和i+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 | struct line { |
变长数组(VLA; Variable Length Array)
C99特性,编译期无法知道变长数组大小,周期属auto类,声明时不可初始化.
1 | int get(size_t n, int a[n][n], size_t i, size_t j) { |
可以书写上例中get()
省略参数名的原型.
1 | int get(int, int[*][*], int, int); |
函数(Function)
C中允许嵌套函数,且可以捕获外部变量,注意C++不允许嵌套函数.
K&R风格函数定义(K&R Style Function Definition)
现在多数编译器已不支持这种旧式风格函数定义.
1 | int isvowel(c) char c; { |
可变参数函数(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 | int x; |
有符号整数算术溢出是未定义行为.
1 | int x = INT_MAX; |
访问或解引用空指针是未定义行为.
1 | int* p = NULL; |
无关地址的比较是未定义行为.
1 | int a = 0; |
数组索引越界是未定义行为.
1 | char a[5]; |
修改字符串字面量是未定义行为.
1 | char* p = "wikipedia"; |
整数除以零是未定义行为.
1 | int x = 1; |
对某个对象非顺序点修改,或跨顺序点修改时,读取该对象的值用于除「确定其存储的值」之外的其他任何目的,都属于未定义行为.
1 | int f(int i) { |
1 | a[i] = i++; // UB |
1 | printf("%d %d\n", ++n, power(2, n)); // UB |
对某值进行负数位移,或位移值大于等于该值总位数,都属于未定义行为.
1 | int num = -1; |
1 | int num = 32; |
返回值函数的结尾没有return
语句,调用时会产生未定义行为.
1 | int f() {} // UB |
注释(Comment)
利用行注释和块注释的特性,仅更改一个符号就能快速切换注释区域.
1 | //* |
1 | /* <- just remove the leading slash |
达夫设备(Duff's Device)
过去为提高效率而设计,现在已不必使用,但展现了神奇的语法规范.
1 | void send(char* to, char* from, int count) { |
平方根倒数速算法(Fast Inverse Square Root)
出自游戏「雷神之锤III竞技场(Quake III Arena)」的源代码,内容如下.
1 | float Q_rsqrt(float number) { |
三字符组(Trigraph)
三字符组又译作三合字母,现在多数编译器默认忽略三字符组,需要-trigraphs
手动开启.
1 | int main(void) |
转义字符\?
可防止对三字符组进行解释.
预处理器(Preprocessor)
可以用#include
的方式初始化大型数组.
1 | double a[SIZE][SIZE] = { |
若宏定义包含多条语句,可以使用do { } while(0)
避免展开后的问题.
1 |
__FILE__
和__LINE__
是内建于预处理器中的宏,它们会被扩展为所在文件的文件名和所处代码行的行号.
预处理器中有如下的特殊的操作符.
#
:将宏参数的代码字符串转化为字符串常量;##
:连接两个代码字符串;#@
:将宏参数的代码转换为字符常量.
可变参数宏(Variadic Macro)
C99/C++特性,使用...
表示,__VA_ARGS__
为替换列表,##__VA_ARGS__
表示当可变参数为0时,去除前面的逗号.
借此可以书写宏的「参数数量重载」版本.
1 |
通用选择(Generic Selection)
C11特性,使用_Generic
在编译时根据控制表达式类型选择表达式.
借此可以为不同类型的函数提供重载.
1 |
结构体打包指令(Structure-Packing Pragma)
结构体打包指令可更改结构体成员的最大对齐.
1 |
参数是较小的2的幂次,不指定参数将设置为默认值.
#pragma pack(push[,n])
可将对齐设置送至内部堆栈,而#pragma pack(pop)
恢复栈顶的设置并删除该堆栈条目,注意#pragma pack(n)
不影响该内部堆栈.
最终,结构体占用空间也与数据在其中的排列顺序有关.
内置函数(Builtin)
内置函数以__builtin
开头,这些函数接受unsigned int
,也有对应的long
和long 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 |
标准库(Standard Library)
time.h
time.h
用于操作日期和时间.
纪元(Epoch)开始于UTC时间1970年1月1日00:00.time()
可以获取纪元起的秒数的整数值time_t
,time()
的定义如下.
1 | time_t time(time_t* arg); |
time()
既可将当前时间作为返回值,也可存储于arg
指向的time_t
对象(当arg
为非空指针时).
可以通过如下代码测量实际用时.
其中:
clock()
返回程序执行起处理器时钟所用的时间;CLOCKS_PER_SEC
是每秒走过的计时数,CLK_TCK
与之等效,但被视为已过时.
1 | clock_t start, stop; |
标准日期时间字符串的格式为Www Mmm dd hh:mm:ss yyyy\n
.将时间转换为文本表示的函数如下.
asctime()
:将日历时间struct tm
转换为文本表示;ctime()
:将纪元起的时间time_t
转换为文本表示.
strftime()
可将日历时间转换为自定义文本表示,使用方法如下例.
1 | if (strftime(buff, sizeof buff, "...", &my_tm)) puts(buff); |
纪元起的时间与日历时间的相互转换如下.
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 | intptr_t handle; |
windows.h
SetConsoleTextAttribute()
用于设置控制台文本属性,需要提供控制台缓冲区屏幕句柄和属性组合.
关于颜色的文本属性的解释,有RED、GREEN、BLUE三种原色,INTENSITY表示颜色高亮,FOREGROUND表示前景色即文本颜色,而BACKGROUND表示背景色.下例将文本颜色设置为亮紫色.
其中,
GetStdHandle(STD_OUTPUT_HANDLE)
用于获取标准输出句柄.
1 | WORD attr = FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE; |
若需获取控制台文本属性,则需使用GetConsoleScreenBufferInfo()
.
1 | CONSOLE_SCREEN_BUFFER_INFO csbiInfo; |