#C语言编码规范
| 类型 | 命名风格 | 形式 |
|---|---|---|
| 函数,结构体/联合体,枚举,typedef定义的类型 | 大驼峰,或带模块前缀的大驼峰 | AaaBbb, XXX_AaaBbb |
| 局部变量,函数参数,宏参数,结构体/联合体成员 | 小驼峰 | aaaBbb |
| 全局变量 | 带'g_'前缀的小驼峰 | g_aaaBbb |
| 宏(非函数式),枚举值,goto标签 | 全大写下划线分割 | AAA_BBB |
| 函数式宏 | 全大写下划线分割,或大驼峰,或带模块前缀的大驼峰 | AAA_BBB, AaaBbb, XXX_AaaBbb |
| 常量 | 全大写下划线分割,或带'g_'前缀的小驼峰 | AAA_BBB, g_aaaBbb |
示例代码:
|
|
#全局变量
-
少用全局变量
-
尽量集中使用
相关的全局变量应该定义到一个结构体中
1 2 3 4 5typedef struct { int aCount; int bCount; int curTotalCount; } SystemState; -
尽量不跨文件
1static SystemState g_systemState = {0}; // 静态全局变量,仅在本文件可见 -
全局变量应该与相关的操作函数放在一起
1 2 3 4 5void InitSystemState() { g_systemState.aCount = 0; g_systemState.bCount = 0; g_systemState.curTotalCount = 0; } -
禁止跨模块,不应该用作接口,应使用函数接口
1 2 3 4 5 6 7 8 9 10 11 12/* 同一接口的不同版本 */ int GetTotalCount() { return g_systemState.curTotalCount; // 将全局变量的值通过接口API方式暴露出去 } int GetTotalCount() { return g_systemState.aCount + g_systemState.bCount; } int GetTotalCount() { int aCount = GetACount(); int bCount = GetBCount(); // 分别使用两个全局变量操作函数得到两个中间量 return aCount + bCount; }
#宏
#完备括号
错误写法:
|
|
正确写法:
|
|
#多条语句使用do-while(0)包裹
错误写法:
|
|
正确写法:
|
|
#不以分号结束
错误写法:
|
|
正确写法:
|
|
#慎用return、continue等改变程序流程的语句
- 宏是文本替换,没有作用域和类型检查
- 使用流程控制语句可能导致程序逻辑混乱、难以阅读和维护
错误写法:
|
|
推荐写法,用函数代替宏,具备作用域、类型检查和调试优势:
|
|
#存储类型
#概念
- 四种存储类型
static- 描述静态变量,可以描述函数内局部变量,不随函数结束而自动释放;也可以描述全局变量,文件外部不能访问
auto- 描述自动变量,可省略
extern- 描述全局变量,可以被文件外部访问
register- 描述寄存器变量,仅建议,提高访问速度
#应用场景
使用变量时,需要考虑变量的生命周期;是否需要被共享;被共享的范围。
#作用域
作用域指可以引用变量的那部分程序文本。
- 两种作用域
- 块作用域
- 文件作用域
|
|
var是局部变量,具有块作用域。g_var是全局变量,具有文件作用域。
作用域与变量定义的位置有关,存储类型static、extern、auto、register都没有改变作用域。auto和register只能在块作用域中使用。
#同名变量
局部变量和全局变量重名,作用域重叠,就近优先,局部优先全局,子块优先父块。
|
|
|
|
全局变量重名,同一个文件,编译报错变量类型冲突。
|
|
|
|
不同文件,不提示。弱符号强符号,未初始化的全局变量为弱符号。
Depend on compiler,我在自己电脑上测试即使弱符号也会报错。相关编译选项-fno-common。
#存储期限
- 即变量的生命周期
- 局部变量在块开始前创建,块结束后销毁,存储在栈内存或者寄存器中。
- 全局变量在程序开始前创建,程序结束后销毁,存储在bss段、data段和rodata段中。
static可以描述局部变量,变成静态局部变量,也是程序开始前创建,程序结束后销毁。static也可以描述全局变量,但不影响存储期限,影响链接阶段。extern主要用于声明,不分配内存,不涉及存储期限。但如果带初始化的extern不是声明而是定义。
#随机数
-
rand()函数产生的伪随机数随机性不好,是可以预测的1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17/* genRandom.c */ #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { int key, key1; srand(time(NULL)); key = rand(); key1 = rand(); printf("Generated keys: %d, %d\n", key, key1); key = rand(); getchar(); // Wait for user input before exiting printf("Next random key: %d\n", key); return 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 25 26 27 28 29 30 31/* main.c */ #include <stdio.h> #include <stdlib.h> #include <time.h> #define TRY_MAX 1000 int main() { int now = time(NULL); int input_key1, input_key2, key1, key2; printf("Enter previous two keys:\n"); scanf("%d %d", &input_key1, &input_key2); printf("Input key: %d, %d\n", input_key1, input_key2); // 循环遍历可能的种子值:从当前时间前60秒到当前时间前1秒 // 目的是覆盖可能生成密钥的时间范围(考虑程序执行延迟) for (int seed = now - 60; seed < now; seed++) { srand(seed); key1 = rand(); for (int i = 0; i < TRY_MAX; i++) { key2 = rand(); if (key1 == input_key1 && key2 == input_key2) { printf("Found, seed: %d, next rand: %d\n", seed, rand()); } key1 = key2; } } getchar(); return 0; }
1 2 3 4 5 6 7 8 9 10 11输出结果: PS F:\C++\VarifyCode> .\genRandom Generated keys: 22387, 29594 Next random key: 22402 PS F:\C++\VarifyCode> .\main.exe Enter previous two keys: 22387 29594 Input key: 22387, 29594 Found, seed: 1753110011, next rand: 22402 -
Unix/Linux平台采用读取/dev/random文件的方式获取随机数:
1 2 3 4 5 6 7 8 9 10int GetRandom() { int ret = 0; int num, fd; fd = open("/dev/random", O_RDONLY); if (fd > 0) { read(fd, &ret, sizeof(int)); } close(fd); return ret; } -
Windows平台推荐使用
CryptGenRandom()来生成随机数 -
可以使用硬件随机数来替代
#溢出、回绕、截断
原码表示:最高位为符号位,其余位表示数值
-15 = 1000 1111
反码表示:符号位不变,其余位取反-15 = 1111 0000
补码表示:在反码基础上+1。最高位对应值为负数-15 = 1111 0001。计算:-15 = 1 + 16 + 32 + 64 - 128。
计算机使用补码进行标识,因为补码具有0的表示唯一(原码反码可以表示出正负0),加减统一不需要设计减法器(可直接将二进制补码相加),符号位参与运算等优点。
|
|
|
|
#常量也需注意类型匹配
|
|
|
|
表达式2147483647 + 1中的两个操作数均为int类型,因此整个表达式在int范围内计算。此时2147483647是INT_MAX,加一后发生了有符号整型溢出,结果变为-2147483648,其补码表示为0x80000000。
该值在赋给unsigned long long类型的变量a时,会发生类型提升。由于原值是int类型(有符号),提升时进行符号扩展:高32位会全部补1,结果是0xffffffff80000000。
零拓展:当无符号类型转换为更大位数时,高位填
0
例如:
(unsigned char)255 = 1111 1111 = (short)255 = 0000 0000 1111 1111
符号拓展:当有符号类型转换为更大位数时,高位复制符号位(MSB-Most Significant Bit-最高有效位)
例如:
(char)-1 = 1111 1111 = (short)-1 = 1111 1111 1111 1111
#整数提升
char、short等小类型在算数表达式中会被隐式提升为int类型。
|
|
两个unsigned short相加,先被隐式提升为int,所以中间值可以比unsigned short大。
整型提升的规则
当char、signed char、unsigned char、short、unsigned short、bool等“小于int”的整数类型参与运算时,会发生如下转换:
有符号类型(如short、char)会被提升为int
无符号类型(如unsigned short),如果其取值范围能完全放入int,也被提升为int;否则提升为unsigned int通用算数转换
当表达式中包含不同类型的操作数时,C会将它们提升为一个最大的公共类型,使得计算结果准确。
long double>double>float>unsigned long long>long long
#数组
#C99变长数组(Variable-Length Array)
C99之后,支持使用变量作为数组长度
|
|
|
|
- VLA无法用作全局数组、静态数组
- VLA内存分配调用的是alloca,也是分配在栈上,栈大小是有限制的,使用不当容易爆栈(stack overflow)
- 需要编译器支持C99,GCC支持,MSVC不支持VLA,标准C++也不支持。应注意其使用上的局限性
#柔性数组(Flexible Array Member)
C语言结构体的最后一个成员可以定义为一个不指定大小的数组,其指向结构体最后一个元素之后的地址。
|
|
输出结果:
PS F:\C++\VarifyCode> ./main
sizeof(Msg) = 8
Message initialized with type 1 and length 10
v[0] = 0
v[1] = 1
v[2] = 2
v[3] = 3
v[4] = 4
v[5] = 5
v[6] = 6
v[7] = 7
v[8] = 8
v[9] = 9
- 结构体后可以根据
l(length)的情况,有时需要8个int有时需要10个int,可以自己申请,并通过v指针访问 v在结构体中不占用结构体大小,sizeof(msg) = 8(int + int)- 在
tlv这种数据结构中用得很多
#内存模型
#可执行文件的布局
以Unix标准二进制文件ELF(Executable and Linking Format)文件来进行说明。
在类Unix系统上,可以是用readelf命令来分析ELF文件。以下是一些常用用法:
|
|
#.text
包含实际要执行的机器指令
|
|
|
|
#.rodata
read-only data,存放全局常量或者字符串字面量
|
|
|
|
#.data
存放已经初始化的数据
|
|
|
|
#.bss
存放未初始化的全局变量以及静态变量,初始化为0等于未初始化,因为默认就是0。
|
|
|
|
不占据目标文件的大小,array[100]和array[10000]二进制空间不变,均为15872
int array[100];
~/C$ size main
text data bss dec hex filename
1219 544 440 2203 89b main
~/C$ ll main
-rwxr-xr-x 1 e1tts e1tts 15872 Jul 25 16:57 main*int array[10000];
~/C$ size main
text data bss dec hex filename
1219 544 40040 41803 a34b main
~/C$ ll main
-rwxr-xr-x 1 e1tts e1tts 15872 Jul 25 16:58 main*
全局变量初始化后就会放入data段,需要占用二进制空间
int arrary[100] = {1};
~/C$ ll main
-rwxr-xr-x 1 e1tts e1tts 16288 Jul 25 17:04 main*
~/C$ size main
text data bss dec hex filename
1219 960 16 2195 893 mainint arrary[10000] = {1};
~/C$ ll main
-rwxr-xr-x 1 e1tts e1tts 55888 Jul 25 17:04 main*
~/C$ size main
text data bss dec hex filename
1219 40560 16 41795 a343 main
#const类型的全局变量存储
|
|
|
|
未初始化的const变量存储在.bss还是.rodata是未定义行为,不同机器、优化选项、编译器版本、目标平台ABI可能都会影响,并未作规定。
#进程的内存布局
进程中的内存布局与ELF文件的布局是相似的。运行可执行文件后,操作系统会将ELF文件中的Section拷贝到内存中去,同时给出Heap和Stack空间。