第一部分
C和ASM作为基础, 要长相看,莫相忘......
1. 数据类型
1.1 整数
以下是基本整数关键字:
* char : 有符号8位整数;
* short : 有符号16位整数;
* int : 有符号32位整数;
* long : 在32位系统上是32位整数, 在64位系统上则是64位整数;
* long long : 有符号64位整数;
* bool : _Bool类型, 8位整数, 在<stdbool.h>种定义了宏bool, true, false, 方便使用。
由于在不同系统上, char可能代表有符号或无符号8位整数, 因此建议使用unsigned char / signed char来表示具体的类型。
在<stdint.h>中定义的一些看上去更明确的整数类型:
typedef signed char int8_t; typedef short int int16_t; typedef int int32_t; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; #if __WORDSIZE == 64 typedef long int int64_t; typedef unsigned long int uint64_t; #else __extension__ typedef long long int int64_t; typedef unsigned long long int uint64_t; #endif
还定义了各种整数类型的大小限制:
/* Limits of integral types. */ /* Minimum of signed integral types. */ # define INT8_MIN (-128) # define INT16_MIN (-32767-1) # define INT32_MIN (-2147483647-1) # define INT64_MIN (-__INT64_C(9223372036854775807)-1) /* Maximum of signed integral types. */ # define INT8_MAX (127) # define INT16_MAX (32767) # define INT32_MAX (2147483647) # define INT64_MAX (__INT64_C(9223372036854775807)) /* Maximum of unsigned integral types. */ # define UINT8_MAX (255) # define UINT16_MAX (65535) # define UINT32_MAX (4294967295U) # define UINT64_MAX (__UINT64_C(18446744073709551615))
字符常量默认位一个 int 整数, 但编译器可以自行决定将其解释为 char 或 int :
char c = 'a'; printf("%c, size(char)=%d, size('a')=%d;\n", c, sizeof(c), sizeof('a')); // 输出: a, size(char)=1, size('a')=4;
指针是个很有特殊用途的整数, <stdint.h>中同样给出了其类型定义:
/* Types for `void *' pointers. */ #if __WORDSIZE == 64 # ifndef __intptr_t_defined typedef long int intptr_t; # define __intptr_t_defined # endif typedef unsigned long int uintptr_t; #else # ifndef __intptr_t_defined typedef int intptr_t; # define __intptr_t_defined # endif typedef unsigned int uintptr_t; #endif
不过, 在代码中,我们通常菜用sizeof(char*)这样的用法, 省去处理32位和64位指针的区别。
我们可以使用不同的后缀来表示整数常量类型:
printf("int size=%d;\n", sizeof(1)); // 输出: int size=4; printf("unsigned int size=%d;\n", sizeof(1U)); // 输出: unsigned int size=4; printf("long size=%d;\n", sizeof(1L)); // 输出: long size=4; printf("unsigned long size=%d;\n", sizeof(1UL)); // 输出: unsigned long size=4; printf("long long size=%d;\n", sizeof(1LL)); // 输出: long long size=8; printf("unsigned long long size=%d;\n", sizeof(1ULL)); // 输出: unsigned long long size=8;
<stdint.h>中定义的一些辅助宏:
# if __WORDSIZE == 64 # define __INT64_C(c) c ## L # define __UINT64_C(c) c ## UL # else # define __INT64_C(c) c ## LL # define __UINT64_C(c) c ## ULL # endif ... /* Signed. */ # define INT8_C(c) c # define INT16_C(c) c # define INT32_C(c) c # if __WORDSIZE == 64 # define INT64_C(c) c ## L # else # define INT64_C(c) c ## LL # endif /* Unsigned. */ # define UINT8_C(c) c # define UINT16_C(c) c # define UINT32_C(c) c ## U # if __WORDSIZE == 64 # define UINT64_C(c) c ## UL # else # define UINT64_C(c) c ## ULL # endif
注: 宏定义种的"##"运算符将左右两侧的操作数接合在一起, 作为一个记号!
1.2 浮点数
C 提供了不同精度的浮点数:
* float : 32位浮点数, 精度为 6 位小数;
* double : 64位浮点数, 精度为 15 位小数;
* long double : 80位浮点数, 精度为 19 位小数。
浮点数默认类型为 double , 可以添加后缀 F 来表示 float , L 表示 long double , 可以局部忽略。
printf("float %f size=%d\n", 1.F, sizeof(1.F)); // 输出: float 1.000000 size=4 printf("double %f size=%d\n", .123, sizeof(.123)); // 输出: double 0.123000 size=8 printf("long double %Lf size=%d\n", 1.234L, sizeof(1.234L));// 输出: long double 1.234000 size=12 # 对齐
C99 提供了复数支持, 用两个相同类型的浮点数分别表示复数的实部和虚部。
直接在float、double、long double 后添加_Complex 即可表示复数, 在 <complex.h> 中定义了 complex 宏使得显示更统一美观。
#include <complex.h> printf("float complex size=%d\n", sizeof((float complex)1.0)); // 输出: float complex size=8 printf("double complex size=%d\n", sizeof((double complex)1.0)); // 输出: double complex size=16 printf("long double complex size=%d\n", sizeof((long double complex)1.0)); // 输出: long double complex size=24
1.3 枚举
enum color { black, red = 5, green, yellow }; enum color b = black; printf("black = %d\n", b); // 输出: black = 0 enum color r = red; printf("red = %d\n", r); // 输出: red = 5 enum color g = green; printf("green = %d\n", g); // 输出: green = 6 enum color y = yellow; printf("yellow = %d\n", y); // 输出: yellow = 7
注: 枚举成员的值可以相同!
通常省略枚举标签来代替宏定义常量:
enum { BLACK = 1, RED, GREEN = 1, YELLOW }; printf("black = %d\n", BLACK); // 输出: black = 1 printf("red = %d\n", RED); // 输出: red = 2 printf("green = %d\n", GREEN); // 输出: green = 1 printf("yellow = %d\n", YELLOW); // 输出: yellow = 2
2. 字面值
字面值(literal)是源码种用来描述固定值的记号(token), 可以是整数、浮点数、字符和字符串。
2.1 整数常量
常见的有: 十进制常量、八进制常量(0开头)、十六进制常量(0x/0X开头)。
常量的类型很重要, 主要通过后缀来区分类型:
0x200 -> int 200U -> unsigned int 0L -> long 0xf0f0UL -> unsigned long 0777LL -> long long 0xFFULL -> unsigned long long
2.2 浮点常量
浮点数常量常用十进制数、十六进制数来表示 。
默认浮点常量是 double 型, 可以使用 F 后缀来表示 float 型, 用 L 后缀来表示 long double 型。
2.3 字符常量
字符常量默认是 int 型, 除非使用前置 L 来表示 wchar_t 宽字符类型。
在 Linux 系统中, 默认字符集是 UTF-8 , 可以使用 wctomb 等函数进行转换。wchar_t 默认是 4 字节长度, 足以容纳所有 UCS-4 Unicode 字符。
#include <stdio.h> #include <locale.h> #include <stdlib.h> int main(int argc, char* argv[]) { setlocale(LC_CTYPE, "en_US.UTF-8"); char buf[128] = {}; wchar_t wc=L'中'; int len = wctomb(buf, wc); printf("%d\n", len); for(int i=0; i<len; i++) { printf("0x%02X ", (unsigned char)buf[i]); } printf("\n"); return 0; }
2.4 字符串常量
C语言种的字符串是一个以 NUL ( '\0' )结尾的 char 数组。
空字符串在内存中占用一个字节, 存储一个 NUL 字符。需要注意的是: strlen 和 sizeof 表示的含义不同。
char s1[] = "Hello, World!"; // 占用 14 字节存储空间 char *s2 = "Hello, C!"; // 占用 10 字节存储空间
同样可以使用 L 前缀声明一个宽字符串:
#include<stdio.h> #include <locale.h> #include <stdlib.h> int main(int argc, char* argv[]) { setlocale(LC_CTYPE, "en_US.UTF-8"); char buf[128] = {}; wchar_t *ws = L"中国人"; printf("%ls\n", ws); size_t wslen = wcstombs(buf, ws, 128); for(int i=0; i<wslen; i++) { printf("0x%02X ", (unsigned char)buf[i]); } printf("\n"); return 0; }
和 char 字符串类似, wchar_t 字符串以一个 4 字节的 NUL 结束 :
#include<stdio.h> #include <wchar.h> int main(int argc, char* argv[]) { wchar_t ws[] = L"中国人"; printf("len %d, size %d\n", wcslen(ws), sizeof(ws)); // 输出: len 3, size 16 unsigned char *b = (unsigned char*)ws; int len = sizeof(ws); for(int i=0; i<len; i++) { printf("%02X ", b[i]); // 输出: 2D 4E 00 00 FD 56 00 00 BA 4E 00 00 00 00 00 00 } printf("\n"); return 0; }
编译器会自动连接相邻的字符串, 方便我们在宏或者代码种更好地处理字符串:
#define WORLD "world!" char *s = "Hello" " " WORLD "\n";
对于源码种超长的字符串, 除了使用相邻字符串外, 还可以用 "\" 在行尾换行:
char *s1 = "Hello" "World!"; char *s2 = "Hello \ World!";
注意: "\"之后只能是一个换行符, 且下一行左侧的空白符也会作为字符串的一部分。
3. 类型转换
当参与运算的几个操作数类型不同时, 就需要进行类型转换。通常编译器会做某些自动的隐式转换操作, 在不丢失信息的前提下, 将位宽"窄"的操作数转换位"宽"类型。
3.1 算术类型转换
编译器默认的隐式转换等级:
long double > double > float > long long > long > int > char > _Bool
浮点数的等级比任何类型的整数等级都高 ; 有符号整数和其等价的无符号类型等级相同。
在表达式中, 可能会将 char 、 short 当做默认 int (unsigned int) 类型操作数, 但 float 并不会自动转换为默认的 double 类型。
char a = 'a'; char c = 'c'; printf("%d\n", sizeof(c-a)); // 输出: 4 printf("%d\n", sizeof(1.5F - 1)); // 输出: 4
当包含无符号操作数时 , 需要注意提升后类型是否能容纳无符号类型的所有值:
long a = -1L; unsigned int b = 100; printf("%ld\n", a > b ? a : b); // 输出: -1
上述结果说明: 尽管 long 等级比 unsigned int 高, 但在 32 位系统中, 它们都是 32 位整数, 且 long 并不足以容纳 unsigned int 的所有值, 因此编译器会将这两个操作数都转换为 unsigned long 型, 也就是高等级的无符号数版本 , 如此 (unsigned long) a 就变成了一个 很大的整数。
其他隐式转换还包括:
* 赋值和初始化时 , 右操作数总是被转换成左操作数类型;
* 函数调用时, 总是将实参转换为形参类型;
* 将 return 表达式结果转换为函数返回类型;
* 任何类型的 0 值 和 NULL 指针都视为 _Bool false, 反之为 true 。
将宽类型转换为窄类型时, 编译器会尝试丢弃高位或者四舍五入等手段返回一个"近似值"。
3.2 非算术类型转换
(1) 数组名或表达式通常被当做指向第一个元素的指针, 除非是以下情况:
* 被当做 sizeof 操作数;
* 使用 & 运算符返回"数组指针";
* 字符串常量用于初始化 char / wchar_t 数组。
(2) 可以显示地将指针转换成任何其他类型指针:
(3) 任何指针都可以隐式转换为 void 指针, 反之亦然;
(4) 任何指针都可以隐式转换为类型更明确的指针(包括 const 、volatile 、restrict 等限定符);
int x = 123, *p = &x; const int *p2 = p;
(5) NULL 可以被隐式转换为任何类型指针。
(6) 可以显示将指针转换为整数, 反向转换亦可。
int x = 123, *p = &x; int px = (int)p; printf("%p, %x, %d\n", p, px, *(int*)px); // 输出: 0xbfc1389c, bfc1389c, 123
4. 运算符
4.1 复合字面值
C99新增的内容, 可以直接使用该语法声明一个结构或数组指针:
(类型名称){初始化列表}
示例:
int *i = &(int){123}; // 整型, 指针 int *x = (int[]){1, 2, 3, 4}; // 数组, 指针 struct data_t *data = &(struct data_t){.x=123}; // 结构体, 指针 func(123, &(struct data_t){.x=123}); // 函数参数, 结构体指针参数
如果是静态变量或全局变量 , 那么初始化列表必须是编译期常量。
4.2 sizeof
返回操作数占用内存空间大小, 单位为字节(byte) , sizeof 返回值 是 size_t 类型, 操作数可以是类型和变量。
注意: 不要用 int 代替 size_t , 因为在 32 位和 64 位平台上 size_t 长度不同。
4.3 逗号表达式
逗号是一个二元运算符, 确保操作数从左到右被顺序处理, 并返回右操作数的值和类型。
int i = 1; long long x = (i++, (long long)i); printf("%lld\n", x); // 注意: long long 类型的输出格式为 "%lld"
4.4 优先级
C语言的优先级是个大麻烦, 不要吝啬使用"()" 。
优先级列表(从高到底):
如果表达式种多个操作符具有相同优先级, 那么结合律决定了组合方式是从左还是从右开始。
一些容易引起误解的运算符优先级:
(1) "." 优先级高于"*"
*p.f <==> *(p.f)
(2) "[]" 优先级高于 "*"
int *ap[] <==> int *(ap[])
(3) "==" 和 "!=" 高于位操作符
val & mask != 0 <==> val & (mask != 0)
(4) "==" 和 "!=" 高于赋值符
c = getchar() != EOF <==> c = (getchar() != EOF)
(5) 算术运算符高于移位运算符
msb << 4 + lsb <==> msb <<(4 + lsb)
(6) 逗号运算符在所有运算符中优先级最低
i = 1, 2 <==> ( i = 1 ), 2
5. 语句
5.1 语句块
语句块代表了一个作用域, 在语句块内声明的自动变量超出范围后立即被释放。除了用"{...}" 表示一个常规语句块外, 还可以直接用于复杂的赋值操作, 这在宏中经常使用。
int i = ({ char a = 'a'; a++; a;}); printf("%d\n", i);
最后一个表达式被当做语句块的返回值。其对应的宏版本如下:
#define test() ({ \ char _a = 'a'; \ _a++; \ _a;}) int i = test(); printf("%d\n", i);
在宏里使用变量通常会添加下划线前缀 , 以避免展开后跟上层语句块的同名变量冲突。
5.2 循环语句
C支持while、for、do ... while 几种循环语句。
size_t get_len(const char* s) { printf("%s\n", __func__); return strlen(s); } int main(int argc, char* argv[]) { char *s = "abcde"; for (int i = 0; i < get_len(s); i++) { printf("%c\n", s[i]); } printf("\n"); return EXIT_SUCCESS; }
注意: 上面这个例子种, 循环导致get_len函数被多次调用。
5.3 选择语句
除了 if ... else if ... else ... 和 switch { case ... } 还有 ...
GCC支持 switch 范围扩展:
int x = 1; switch (x) { case 0 ... 9: printf("0..9\n"); break; case 10 ... 99: printf("10..99\n"); break; default: printf("default\n"); break; } char c = 'C'; switch (c) { case 'a' ... 'z': printf("a..z\n"); break; case 'A' ... 'Z': printf("A..Z\n"); break; case '0' ... '9': printf("0..9\n"); break; default: printf("default\n"); break; }
5.4 无条件跳转
无条件跳转关键字: break, continue, goto, return。
goto 仅可在函数内跳转, 常用于跳出嵌套循环。如果在函数外跳转, 可用 longjmp 。
5.4.1 longjmp
setjmp 将当前位置的相关信息(堆栈帧、寄存器等)保存到一个 jmp_buf 结构中,并返回 0 。当后续代码执行 longjmp 跳转时, 需要提供一个状态码。代码执行序将返回 setjmp 处,并返回longjmp所提供的状态码。
#include<stdio.h> #include <stdlib.h> #include <stdbool.h> #include <setjmp.h> void test(jmp_buf *env) { printf("... %s ...\n", __func__); longjmp(*env, 10); } int main(int argc,char* argv[]) { jmp_buf env; int ret = setjmp(env); // 执行 longjmp 将返回到该位置, ret 等于 longjmp 所提供的状态码 if(ret == 0) { test(&env); } else { printf("... %s ...\n", __func__); } return EXIT_SUCCESS; // 输出: ... test ...\n ... main ...\n }
6. 函数
函数只能被定义一次, 但可以被多次"声明"和"调用"。
6.1 嵌套
GCC 支持嵌套函数扩展。
#include<stdio.h> #include <stdlib.h> typedef void(*func_t)(); func_t test() { void func1() { printf("%s\n", __func__); } return func1; } int main(int argc, char* argv[]) { test()(); // 输出: func1 return EXIT_SUCCESS; }
内层函数可以"读写"外层函数的参数和变量, 外层变量必须在内嵌函数之前定义:
#include<stdio.h> #include <stdlib.h> #define pp() \ ({ \ printf("%s: x = %d(%p), y = %d(%p), s = %s(%p);\n", __func__, x, &x, y, &y, s, s); \ }) void test2(int x, char *s) { int y = 88; pp(); void func1() { x++; y++; pp(); } func1(); x++; func1(); pp(); } int main(int argc, char* argv[]) { test2(1234, "abcd"); return EXIT_SUCCESS; }
输出:
test2: x = 1234(0xbfa00c9c), y = 88(0xbfa00c98), s = abcd(0x8048679);
func1: x = 1235(0xbfa00c9c), y = 89(0xbfa00c98), s = abcd(0x8048679);
func1: x = 1237(0xbfa00c9c), y = 90(0xbfa00c98), s = abcd(0x8048679);
test2: x = 1237(0xbfa00c9c), y = 90(0xbfa00c98), s = abcd(0x8048679);
6.2 类型
注意区分定义"函数类型"和"函数指针类型"的区别。函数名是一个指向当且函数的指针。
#include<stdio.h> #include <stdlib.h> typedef void (func_t)(); // 函数类型 typedef void (*func_ptr_t)(); // 函数指针类型 void test() { printf("%s\n", __func__); } int main(int argc, char* argv[]) { func_t *func1 = test; // 声明一个指针 func_ptr_t func2 = test; // 已经是指针类型了 void (*func3)(); // 声明一个包含函数原型的函数指针变量 func3 = test; func1(); func2(); func3(); return EXIT_SUCCESS; }
6.3 调用
C 函数默认采用 cdecl 调用约定, 参数从右往左入栈 , 且由调用者负责参数入栈和清理。
C 语言中所有对象, 包括指针本身都是"复制传值"传递 , 但可以通过传递“指针的指针”来实现传出参数。
6.4 修饰符
C99 修饰符:
* extern : 默认修饰符, 用于函数表示"具有外部链接的标识符", 这类函数可用于任何程序文件。用于变量声明, 表示该变量在其他单元种定义。
* static : 使用该修饰符的函数仅在其所在编译单元(源码文件)种可用, 还可以表示函数类的静态变量。
* inline : 修饰符 inline 建议编译器将函数代码内联到调用处, 但编译器可自主决定是否完成。通常包含循环或递归的函数不能被定义位inline函数。
GNU inline 相关说明:
* static inline : 内链接函数, 在当前编译单元内内联。不过,-O0 时依然是 call 。
* inline : 外链接函数, 当前单元内联, 外部单元为普通外链接函数(头文件种不能添加 inline 关键字)。
注意: inline 关键字只能用在函数定义处。inline必须用于函数定义,对于函数声明,inline不起作用。
6.5 可选性自变量
* va_start : 通过可选字变量前的一个参数位置来初始化 va_list 自变量类型指针;
* va_arg : 获取当前可选自变量值, 并将指针移动到下一个可选自变量;
* va_end : 当不再需要自变量指针时调用;
* va_copy : 用现存的自变量指针(va_list)来初始化另一指针。
#include<stdio.h> #include <stdlib.h> #include <stdarg.h> // 指定自变量数量 void test1(int count, ...) { va_list args; va_start(args, count); for(int i=0; i<count; i++) { int value = va_arg(args, int); printf("%d\n", value); } va_end(args); } // 以 NULL 为结束标记 void test2(const char* s, ...) { printf("%s\n", s); va_list args; va_start(args, s); char *value = NULL; do { value = va_arg(args, char*); if(value) { printf("%s\n", value); } } while(value != NULL); va_end(args); } // 直接将 va_list 传递给其他可选自变量函数 void test3(const char* format, ...) { va_list args; va_start(args, format); vprintf(format, args); va_end(args); } int main(int argc, char* argv[]) { test1(3, 11, 22, 33); test2("Hello", "AA", "BB", "CC", NULL); test3("%s, %d", "Hello, C!", 1234); return EXIT_SUCCESS; }
va_copy 是 C99 中是支持的, 若编译器不支持, 可选的解决办法:
#ifndef va_copy # ifdef __va_copy # define va_copy(DEST,SRC) __va_copy((DEST),(SRC)) # else # define va_copy(DEST, SRC) memcpy((&DEST), (&SRC), sizeof(va_list)) # endif #endif
7. 数组
7.1 可变长度数组
如果数组具有自动生存周期, 且没有 static 修饰符, 那么可以用非常量表达式来定义数组。
int test(int n) { int x[n]; for(int i=0; i<n; i++) { x[i] = i; } struct data { int x[n]; } d; printf("%d\n", sizeof(d)); }
7.2 下标存储
x[i] 相当于*(x+i) , 数组名默认为指向第一元素的指针。
C 不会对数组下标索引进行范围检查, 编码时需要注意边界检查。
数组名默认是指向第一元素指针的常量, 而 &x[i] 则返回 int* 类型的指针, 指向目标序号元素。
7.3 初始化
除了使用下标初始化外, 还可以直接使用初始化器。
int x[] = {1, 2, 3};
初始化规则:
* 如果数组为静态生存周期, 那么初始化器必须是常量表达式;
* 如果提供初始化器, 那么可以不提供数组长度,由初始化器的最后一个元素决定;
* 如果同时提供长度和初始化器,那么没有提供初始值的元素都被初始化为 0 或 NULL。
可以在初始化器种初始化特定的元素:
int x[] = {1, 2, [6] = 10, 11};
7.4 字符串
字符串是以 '\0' 结尾的 char 数组。
7.5 多维数组
元素为数组的数组, 注意元素是数组, 并不是数组指针。
多维数组的第一个维度下标可以不指定。
7.6 数组参数
当数组作为函数参数时, 总是被隐式转换为指向数组第一元素的指针, 也就是再也不能使用 sizeof 获得数组的实际长度了。
8. 指针
8.1 void 指针
void* , 又称 万能指针 , 可以代表任何对象的地址,但没有该对象的类型。必须在转型后,才能进行对象操作。void * 指针可以与其他任何类型指针进行隐式转换。
#include <stdio.h>
#include <stdlib.h>
void test (void *, size_t len) { unsigned char *cp = p; for(int i=0; i<len; i++) { printf("%02x", *(cp+i)); } printf("\n"); } int main(int argc, char* argv[]) { int x = 0x00112233; test(&x, sizeof(x)); // 大端模式: 33 22 11 00 return EXIT_SUCCESS; }
8.2 初始化指针
可以用初始化器初始化指针:
* 空指针常量 NULL;
* 相同类型的指针, 或者指向限定符较少的相同类型指针;
* void 指针。
非自动周期指针变量或者静态生存期指针变量必须用编译期常量表达式初始化, 比如函数名等。
8.3 指针运算
(1) 对指针进行相等或不等运算来判断是否指向同一对象;
(2) 对指针进行加法运算获取数组第 n 个元素指针;
(3) 对指针进行减法运算, 以获取指针所在元素的数组索引序号;
(4) 对指针进行大小比较运算, 相当于判断数组索引序号大小;
(5) 可以直接使用 &x[i] 获取指定序号因素的指针。
注: [] 优先级比 & 高, * 运算优先级比算术运算符高。
8.4 限定符
限定符 const 可以声明 "类型为指针的常量" 和 "指向常量的指针"。区别在于 const 修饰的是 p 还是 *p。
指针常量: 指针本身为常量, 不可修改, 但可修改目标对象;
常量指针: 目标对象为常量, 不可修改, 但可修改指针。
具有 restrict 限定符的指针被称为 限定指针。告诉编译器在指针生存期内, 只能通过该指针修改对象, 但编译器可自主决定是否采纳该建议。
8.5 数组指针
指向数组本事的指针, 而非指向第一元素的指针。
int x[] = { 1, 2, 3}; int (*p)[] = &x;
8.6 指针数组
元素是指针的数组, 通常用于表示字符串数组或交错数组。
int * x[3] = { }; x[0] = (int[]){ 1 }; x[1] = (int[]){ 2, 22}; x[2] = (int[]){3, 33, 33};
9. 结构
9.1 不完整结构
结构类型无法把自己作为成员类型, 但可以包含"指向自己类型"的指针成员。
typedef struct node_t { struct node_t *prev; struct node_t *next; void *value; } node_t;
9.2 匿名结构
在结构体内部使用匿名结构体成员, 是一种很常见的做法。
typedef struct { struct { int length; char chars[100]; } s; int x; } data_t; int main(int argc, char * argv[]) { data_t d = { .s.length = 100, .s.chars = "abcd", .x = 1234 }; printf("%d\n%s\n%d\n", d.s.length, d.s.chars, d.x); struct { int a; char b[100]; } d = { .a = 100, .b = "abcd" }; printf("%d\n%s\n", d.a, d.b); return EXIT_SUCCESS; }
9.3 成员偏移量
利用 <stddef.h> 中定义的 offsetof 宏可以获取结构体成员的偏移量。
#include <stdio.h> #include <stddef.h> typedef struct data_t { int x; short y[3]; long long z; } data_t; int main(int argc,char *argv[]) { printf("x %d\n", offsetof(data_t, x)); // 输出: 0 printf("y %d\n", offsetof(data_t, y)); // 输出: 4 printf("y[1] %d\n", offsetof(data_t, y[1])); // 输出: 6 printf("z %d\n", offsetof(data_t, z)); // 输出: 12 return 0; }
注意: 需要考虑边界字节对齐!
9.4 定义
结构体类型的定义有多种灵活的方式:
int main(int argc, char* argv[]) { // 直接定义结构类型和变量 struct { int x; short y; } a1 = {1, 2}, a2 = { }; printf("a.x = %d, a.y = %d\n", a1.x, a1.y); // 函数内部定义结构类型 struct data { int x; short y; }; struct data b = {.y = 3}; printf("b.x = %d, b.y = %d\n", b.x, b.y); // 复合字面值 struct data *c = &(struct data){1, 2}; printf("c.x = %d, c.y = %d\n", c->x, c->y); // 直接将结构体类型定义放在复合字面值中 void *p = &(struct data2 { int x; short y; }){11, 22}; // 相同内存布局的结构体可以直接转换 struct data *d = (struct data*)p; printf("d.x = %d, d.y = %d\n", d->x, d->y); return 0; }
9.5 初始化
结构体的初始化和数组一样简洁方便, 包括使用初始化器初始化特定的某些成员。未被初始化器初始化的成员将被设置为 0。
typedef struct { int x; short y[3]; long long z; } data_t; int main(int argc, char* argv[]) { data_t d1 = {}; data_t d2 = {1, {11, 22, 33}, 2LL}; data_t d3 = {.z = 3LL, .y[2] = 2};
return 0; }
9.6 弹性结构成员
又称"不定长结构", 就是在结构体尾部声明一个未指定长度的数组。
用 sizeof 运算符计算所占字节数时 , 该数组不会被计入结果。
#include <stdio.h> #include <string.h> typedef struct string { int length; char chars[]; } string; int main(int argc, char* argv[]) { int length = sizeof(string) + 10; // 计算存储一个10字节长度字符串(含'\0')所需的字节数 char buf[length]; string *s = (string*)buf; // 转换成 struct string 指针 s->length = 9; strcpy(s->chars, "123456789"); printf("%d\n%s\n", s->length, s->chars); return 0; }
考虑到不同编译器和 ANSI C 标准的问题, 也可用 char chars[1] 或 char chars[0] 来代替。
注意: 对这类结构体进行拷贝时, 尾部结构成员不会被复制。而且, 不能直接对弹性成员进行初始化。
string s2 = *s; // 复制 struct string *s printf("%d\n%s\n", s2.length, s2.chars); // s2.length 正常, s2.chars 为 ""
10. 联合
联合与结构的区别在于: 联合每次只能存储一个成员, 联合的长度由最宽成员类型决定。
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { int type; union { int ivalue; long long lvalue; } value; }data_t; int main(int argc, char* argv[]) { data_t d1 = {0x8899, .value.lvalue = 0x1234LL}; data_t d2; memcpy(&d2, &d1, sizeof(d1)); return EXIT_SUCCESS; }
当然, 可以用指针来实现上例功能, 但 union 会将数据内嵌在结构体中, 这对于进行 memcpy 等操作更加方便快捷, 而且无需进行指针类型转换。
可以使用初始化器初始化联合, 如果没有指定成员修饰符, 则默认为第一个成员。
union value_t { int ivalue; long long lvalue; }; union value_t v1 = {10}; printf("%d\n", v1.ivalue); union value_t v2 = {.lvalue=20LL}; printf("%lld\n", v2.lvalue); union value2_t {char c; int x;} v3 = {.x = 100;}; printf("%d\n", v3.x);
一个常用的联合用法:
union { int x; struct { char a, b, c, d; } bytes; } n = { 0x12345678 }; // 判断大小端模式 printf("%#x => %x, %x, %x, %x\n", n.x, n.bytes.a, n.bytes.b, n.bytes.c, n.bytes.d);
// 大端模式: 0x12345678 => 78, 56, 34, 12; 小端模式: 0x12345678 => 12, 34, 56, 78
11. 位字段
常见用法: 将结构或联合的多个成员"压缩存储"在一个字段中, 以节约内存。
#include <stdio.h> #include <stdlib.h> int main(int argc, char* argv[]) { struct { unsigned int year : 22; unsigned int month : 4; unsigned int day : 5; } d = {2015, 4, 15}; printf("size: %d\n", sizeof(d)); // 输出: size : 4 printf("year = %u, month = %u, day = %u\n", d.year, d.month, d.day); // 输出: year = 2015, month = 4, day = 15 return EXIT_SUCCESS; }
使用 位字段 作标志位, 比用位移运算符更直观, 更节省内存。
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> int main(int argc, char* argv[]) { struct { bool a : 1; bool b : 1; bool c : 1; } flags = {.b = true}; printf("size : %d\n", sizeof(bool)); printf("%s\n", flags.b ? "b.T" : "b.F"); printf("%s\n", flags.c ? "b.T" : "b.F"); return EXIT_SUCCESS; }
注意: 不能对位字段成员使用 offsetof !
12. 声明
声明(declaration) 表明了目标原型,可以在多处对同一个目标进行声明, 但只能有一个定义(definition)。定义将创建对象实体, 为其分配内存空间, 而声明不会。
* 声明结构、联合或枚举等用户自定义类型(UDT);
* 声明函数;
* 声明并定义一个全局变量;
* 声明一个外部变量;
* 用 typedef 为已有类型声明一个新名字。
如果声明函数时同时给出函数体, 则此函数的声明同时也是定义。
如果声明对象时同时为该对象分配内存, 那么该对象声明的同时也是定义。
12.1 类型修饰符
C99 定义的类型修饰符:
* const : 常量修饰符, 定义后不能在修改;
* volatile : 目标可能被其他线程或事件修改, 使用该变量之前, 都必须从主存中重新获取;
* restrict : 修饰指针, 除了该指针, 不能使用其他任何方式修改目标对象。
12.2 链接类型
12.3 隐式初始化
具有静态生存周期的对象, 会被初始化为默认值0 (指针类型为NULL)。
13. 预处理
预处理指令以#开头(其前面可以有空白符), 通常独立一行, 但也可以用"\"续行。
13.1 常量
编译器在预处理阶段进行宏展开:
$ gcc -E main.c
13.2 宏函数
利用宏可以定义伪函数, 通常用 ({...}) 来组织多行语句, 最后一个表达式作为返回值(无 return, 但以“;”j结束):
#define test(x, y) ({ \ int _z = (x) + (y); \ _z; \ }) printf("%d\n", test(1, 2)); // 宏展开: printf("%d\n", ({ int _z = (1) + (2); _z; }));
13.3 可选性变量
__VA_ARGS__ 标识符要来表示一组可选性自变量。
#define println(format, ...) ({ \ printf(format "\n", __VA_ARGS__); \ }) println("%s, %d", "string", 1234); // 宏展开: ({ printf("%s, %d" "\n", "string", 1234); });
13.4 字符串化运算符
一元运算符'#' 可将一个宏参数转换为字符串。
#define stringify(name) #name printf("%s\n", stringify(main)); // 宏展开: printf("%s\n", "main"); printf("%s\n", stringify("main")); // 宏展开: printf("%s\n", "\"main\"");
可见, 字符串化运算符#会自动进行转义操作。
13.5 粘贴记号运算符
二元运算符"##" 可将左右操作数结合为一个记号。
#define tokenize(name, index) ({ \ int i, len = sizeof(name##index) / sizeof(int); \ for(i=0; i<len; i++) { \ printf("%d\n", name##index[i]); \ } \ }) int x1[] = {1, 2, 3}; int x2[] = {11, 22, 33, 44, 55};
tokenize(x, 1); // 宏展开: ({ int i, len = sizeof(x1) / sizeof(int); for(i=0; i<len; i++) { printf("%d\n", x1[i]); } }); tokenize(x, 2); // 宏展开: ({ int i, len = sizeof(x2) / sizeof(int); for(i=0; i<len; i++) { printf("%d\n", x2[i]); } });
13.6 条件编译
可以使用"#if ... #elif ... #else ... #endif"、#define 、#undef进行条件编译。
13.7 typeof
使用GCC扩展的 typeof 可以获取参数的类型:
#define test(x) ({ \ typeof(x) _x = (x); \ _x += 1; \ _x; \ }) float f1 = 0.5F; float f2 = test(f1); printf("%f\n", f2);
13.8 其他
预处理阶段常量:
* #error "message" : 定义一个编译器出错信息
* __DATE__ : 编译日期字符串;
* __TIME__ : 编译时间字符串;
* __FILE__ : 当前源码文件名;
* __LINE__ : 当前源码行号;
* __func__ : 当且函数名称。
14. 调试
要习惯使用assert宏进行函数参数和执行条件判断, 这可省却很多麻烦!
#include <assert.h> int main(int argc, char* argv[]) { void test_assert(int x) { assert(x > 0); // 宏展开: ((x > 0) ? (void) (0) : __assert_fail ("x > 0", "<file>.c", <line>, __PRETTY_FUNCTION__)); printf("%d\n", x); } test_assert(-1); return EXIT_SUCCESS; }
编译 Release 版本, 记得加上 -DNDEBUG 参数:
$ gcc -E -DNDEBUG main.c
<assert.h> 中有关 assert 的定义:
#if defined __cplusplus && __GNUC_PREREQ (2,95) # define __ASSERT_VOID_CAST static_cast<void> #else # define __ASSERT_VOID_CAST (void) #endif /* void assert (int expression); If NDEBUG is defined, do nothing. If not, and EXPRESSION is zero, print an error message and abort. */ #ifdef NDEBUG # define assert(expr) (__ASSERT_VOID_CAST (0)) __BEGIN_DECLS /* This prints an "Assertion failed" message and aborts. */ extern void __assert_fail (const char *__assertion, const char *__file, unsigned int __line, const char *__function) __THROW __attribute__ ((__noreturn__)); ... __END_DECLS # define assert(expr) \ ((expr) \ ? __ASSERT_VOID_CAST (0) \ : __assert_fail (__STRING(expr), __FILE__, __LINE__, __ASSERT_FUNCTION)) # if defined __cplusplus ? __GNUC_PREREQ (2, 6) : __GNUC_PREREQ (2, 4) # define __ASSERT_FUNCTION __PRETTY_FUNCTION__ # else # if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L # define __ASSERT_FUNCTION __func__ # else # define __ASSERT_FUNCTION ((const char *) 0) # endif # endif #endif /* NDEBUG. */
<cdefs.h> 中定义的有关的宏:
...
/* For these things, GCC behaves the ANSI way normally, and the non-ANSI way under -traditional. */ #define __CONCAT(x,y) x ## y #define __STRING(x) #x ... /* C++ needs to know that types and declarations are C, not C++. */ #ifdef __cplusplus # define __BEGIN_DECLS extern "C" { # define __END_DECLS } #else # define __BEGIN_DECLS # define __END_DECLS #endif /* The standard library needs the functions from the ISO C90 standard in the std namespace. At the same time we want to be safe for future changes and we include the ISO C99 code in the non-standard namespace __c99. The C++ wrapper header take case of adding the definitions to the global namespace. */ #if defined __cplusplus && defined _GLIBCPP_USE_NAMESPACES # define __BEGIN_NAMESPACE_STD namespace std { # define __END_NAMESPACE_STD } # define __USING_NAMESPACE_STD(name) using std::name; # define __BEGIN_NAMESPACE_C99 namespace __c99 { # define __END_NAMESPACE_C99 } # define __USING_NAMESPACE_C99(name) using __c99::name; #else /* For compatibility we do not add the declarations into any namespace. They will end up in the global namespace which is what old code expects. */ # define __BEGIN_NAMESPACE_STD # define __END_NAMESPACE_STD # define __USING_NAMESPACE_STD(name) # define __BEGIN_NAMESPACE_C99 # define __END_NAMESPACE_C99 # define __USING_NAMESPACE_C99(name) #endif
...
附录 A : 指针概要
1. 指针常量
2. 常量指针
3. 指针的指针
4. 数组指针
5. 指针数组
6. 函数指针
附录 B : 数组指针
附录 C : 指针数组
附录 D : 函数调用分析