Advanced C Programming

#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
 5
 6
 7
 8
 9
10
int MyCmp(int a, int b);        // 函数大驼峰,参数小驼峰

enum MyColor {                  // 枚举类型大驼峰
    BLACK,                      // 枚举值全大写,下划线分割
    WHITE
} g_bgColor = WHITE;            // 全局变量,带g_前缀小驼峰

int XXX_YYY_FuncName(void);     // 函数,两级前缀,大模块加小模块

const int NAME_MAX_LEN = 100;   // 常量全大写下划线分割

#全局变量

  1. 少用全局变量

  2. 尽量集中使用

    相关的全局变量应该定义到一个结构体中

    1
    2
    3
    4
    5
    
     typedef struct {
         int aCount;
         int bCount;
         int curTotalCount;
     } SystemState;
    
  3. 尽量不跨文件

    1
    
    static SystemState g_systemState = {0}; // 静态全局变量,仅在本文件可见
    
  4. 全局变量应该与相关的操作函数放在一起

    1
    2
    3
    4
    5
    
    void InitSystemState() {
        g_systemState.aCount = 0;
        g_systemState.bCount = 0;
        g_systemState.curTotalCount = 0;
    }
    
  5. 禁止跨模块,不应该用作接口,应使用函数接口

     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;
    }
    

#

#完备括号

错误写法:

1
#define MAX(a, b) a > b ? a : b

正确写法:

1
2
3
4
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
int a = 2;
int b = SQUARE(a + 1);  // 预期 (2 + 1)*(2 + 1) = 9,若无括号就变成 a + 1 * a + 1 = 4

#多条语句使用do-while(0)包裹

错误写法:

1
2
3
#define ABC(a)      \
        Func1(a);   \
        Func2(a);

正确写法:

1
2
3
4
#define ABC(a) do {     \
        Func1(a);       \
        Func2(a);       \
    } while(0)

#不以分号结束

错误写法:

1
#define ABC(a) Func(a);

正确写法:

1
2
3
4
// 宏调用就像函数调用一样
#define ABC(a) Func(a)

ABC(123); // 展开为Func(123);

#慎用return、continue等改变程序流程的语句

  • 宏是文本替换,没有作用域和类型检查
  • 使用流程控制语句可能导致程序逻辑混乱、难以阅读和维护

错误写法:

1
#define CHECK(x) if (!(x)) return -1;

推荐写法,用函数代替宏,具备作用域、类型检查和调试优势:

1
2
3
4
static inline int check(int x) {
    if (!x) return -1;
    return 0;
}

#存储类型

#概念

四种存储类型
static
描述静态变量,可以描述函数内局部变量,不随函数结束而自动释放;也可以描述全局变量,文件外部不能访问
auto
描述自动变量,可省略
extern
描述全局变量,可以被文件外部访问
register
描述寄存器变量,仅建议,提高访问速度

#应用场景

使用变量时,需要考虑变量的生命周期;是否需要被共享;被共享的范围。

#作用域

作用域指可以引用变量的那部分程序文本。

两种作用域
块作用域
文件作用域
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

int g_var;

int main() {
    g_var = 0;
    int var = 1;
    {
        int var = 2;
    }
}

var是局部变量,具有块作用域。g_var是全局变量,具有文件作用域。

作用域与变量定义的位置有关,存储类型staticexternautoregister都没有改变作用域。autoregister只能在块作用域中使用。

#同名变量

局部变量和全局变量重名,作用域重叠,就近优先,局部优先全局,子块优先父块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int var = 1;

int main() {
    printf("#1: &var = %p, var = %d\n", &var, var);
    int var = 2;
    printf("#2: &var = %p, var = %d\n", &var, var);
    {
        int var = 3;
        printf("#3: &var = %p, var = %d\n", &var, var);
    }
    return 0;
}
1
2
3
4
运行结果:
#1: &var = 00007FF73B894010, var = 1
#2: &var = 00000071CCDFFC9C, var = 2
#3: &var = 00000071CCDFFC98, var = 3

全局变量重名,同一个文件,编译报错变量类型冲突。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int g_var = 1;
long long g_var = 1;

int main() {
    printf("&g_var = %p\n", &g_var);
    return 0;
}
1
2
3
4
5
6
.\test.c:4:11: error: conflicting types for 'g_var'; have 'long long int'
    4 | long long g_var = 1;
      |           ^~~~~
.\test.c:3:5: note: previous definition of 'g_var' with type 'int'
    3 | int g_var = 1;
      |     ^~~~~

不同文件,不提示。弱符号强符号,未初始化的全局变量为弱符号。

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
    10
    
      int 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),加减统一不需要设计减法器(可直接将二进制补码相加),符号位参与运算等优点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <limits.h>
#include <stdio.h>

int main() {
    int a = INT_MAX;
    printf("%d %d\n", a, a + 1); // 溢出

    unsigned int b = UINT_MAX;
    printf("%u %u\n", b, b + 1); // 回绕

    int c = 0x12345678;
    short d = (short)c;
    printf("%#x %#x\n", c, d); // 截断
    return 0;
}

1
2
3
4
5
输出结果:
PS F:\C++\VarifyCode> ./main
2147483647 -2147483648
4294967295 0
0x12345678 0x5678

#常量也需注意类型匹配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

#define ULL unsigned long long int

int main() {
    ULL a = 2147483647 + 1; // 发生溢出
    printf("%llu %#llx\n", a, a);

    ULL b = 2147483647ll + 1;   // 正确结果
    printf("%llu %#llx\n", b, b);

    printf("%zu %zu", sizeof(1), sizeof(1ll));
    return 0;
}

1
2
3
4
5
输出结果:
PS F:\C++\VarifyCode> ./main
18446744071562067968 0xffffffff80000000
2147483648 0x80000000
4 8

表达式2147483647 + 1中的两个操作数均为int类型,因此整个表达式在int范围内计算。此时2147483647INT_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

#整数提升

charshort等小类型在算数表达式中会被隐式提升为int类型。

1
2
3
4
5
6
7
8
9
#include <limits.h>
#include <stdio.h>

int main() {
    unsigned short a = USHRT_MAX, b = USHRT_MAX;
    unsigned int c = a + b;
    printf("USHRT_MAX + USHRT_MAX = %u\n", c);
    return 0;
}

两个unsigned short相加,先被隐式提升为int,所以中间值可以比unsigned short大。

整型提升的规则
charsigned charunsigned charshortunsigned shortbool 等“小于int”的整数类型参与运算时,会发生如下转换:
有符号类型(如 shortchar)会被提升为 int
无符号类型(如 unsigned short),如果其取值范围能完全放入 int,也被提升为 int;否则提升为 unsigned int

通用算数转换
当表达式中包含不同类型的操作数时,C会将它们提升为一个最大的公共类型,使得计算结果准确。
long double > double > float > unsigned long long > long long

#数组

#C99变长数组(Variable-Length Array)

C99之后,支持使用变量作为数组长度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <limits.h>
#include <stdio.h>

int g_n = 10;

int main() {
    int n = 3;
    int a[n];
    int b[g_n];
    a[0] = 1;
    b[0] = 2;
    printf("Value of a[0]: %d\n", a[0]);
    printf("Value of b[0]: %d\n", b[0]);
    printf("Size of int: %zu bytes\n", sizeof(int));
    printf("Size of array a: %zu bytes\n", sizeof(a));
    printf("Size of array b: %zu bytes\n", sizeof(b));

    return 0;
}

1
2
3
4
5
6
7
输出结果:
PS F:\C++\VarifyCode> ./main
Value of a[0]: 1
Value of b[0]: 2
Size of int: 4 bytes
Size of array a: 12 bytes
Size of array b: 40 bytes
  • VLA无法用作全局数组、静态数组
  • VLA内存分配调用的是alloca,也是分配在栈上,栈大小是有限制的,使用不当容易爆栈(stack overflow)
  • 需要编译器支持C99,GCC支持,MSVC不支持VLA,标准C++也不支持。应注意其使用上的局限性

#柔性数组(Flexible Array Member)

C语言结构体的最后一个成员可以定义为一个不指定大小的数组,其指向结构体最后一个元素之后的地址

 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
32
33
34
35
36
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int t;
    int l;
    int v[0];     // 必须是结构体最后一个成员,C99 int v[];
} Msg;

int InitMsg() {
    Msg *msg = (Msg *)malloc(sizeof(Msg) + 10 * sizeof(int));
    if (msg == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return -1; // Error
    }
    msg->t = 1;
    msg->l = 10;
    for (int i = 0; i < msg->l; i++) {
        msg->v[i] = i; // Initialize with some values
    }
    printf("Message initialized with type %d and length %d\n", msg->t, msg->l);
    for (int i = 0; i < msg->l; i++) {
        printf("v[%d] = %d\n", i, msg->v[i]);
    }
    free(msg);
    return 0; // Success
}

int main() {
    printf("sizeof(Msg) = %zu\n", sizeof(Msg));
    if (InitMsg() != 0) {
        return -1;
    }
    return 0;
}

输出结果:
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文件。以下是一些常用用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 查看 ELF 文件头(Header)信息。
# 包含 ELF 类型(可执行/共享库等)、目标架构、入口地址等。
readelf -h [文件]

# 查看 Section Headers信息
# 包括 .text, .data, .bss, .rodata, .symtab, .strtab 等段的偏移、大小、类型等信息。
readelf -S [文件]

# 查看某个section的十六进制内容
readelf -x [节名|节编号] [文件]
readelf -x .rodata a.out

# 查看符号表
readelf -s [文件]

# 查看动态链接信息
readelf -d [文件]

#.text

包含实际要执行的机器指令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main() {
    while (1) {
#if 0
        for (int i = 0; i < 10; i++) {
            i--;
        }
#endif
    }
    return 0;
}

1
2
3
4
5
6
7
# if 0开启前后只有text发生变化
~/C$ size main
   text    data     bss     dec     hex filename
   1219     544       8    1771     6eb main
~/C$ size main
   text    data     bss     dec     hex filename
   1242     544       8    1794     702 main

#.rodata

read-only data,存放全局常量或者字符串字面量

1
2
char *g_string1 = "Hello, World!";
const int g_int = 456123;

1
2
3
4
5
6
~/C$ objdump -t main | grep -e g_int -e g_string1
0000000000002014 g     O .rodata        0000000000000004              g_int
~/C$ objdump -s main | grep -A 5 .rodata
Contents of section .rodata:
 2000 01000200 48656c6c 6f2c2057 6f726c64  ....Hello, World
 2010 21000000 bbf50600

#.data

存放已经初始化的数据

1
2
3
4
5
int int1 = 456123;
int main() {
    static int s_int1 = 123456;
    return 0;
}

1
2
3
objdump -t main | grep -e int1 -e s_int1
0000000000004014 l     O .data  0000000000000004              s_int1.0
0000000000004010 g     O .data  0000000000000004              int1

#.bss

存放未初始化的全局变量以及静态变量,初始化为0等于未初始化,因为默认就是0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int g_num;
int g_ret = 0;
int main() {
    static int int1;
    static int int2 = 0;
    while (1) {

    }
    return 0;
}

1
2
3
4
5
~/C$ objdump -t main | grep -e int1 -e int2 -e g_num -e g_ret
000000000000401c l     O .bss   0000000000000004              int2.1
0000000000004020 l     O .bss   0000000000000004              int1.0
0000000000004018 g     O .bss   0000000000000004              g_ret
0000000000004014 g     O .bss   0000000000000004              g_num

不占据目标文件的大小,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 main

int 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类型的全局变量存储

1
2
3
const int g_a;
const int g_b = 0;
const int g_c = 1;

1
2
3
4
~/C$ objdump -t main | grep g_
0000000000002008 g     O .rodata        0000000000000004              g_b
0000000000002004 g     O .rodata        0000000000000004              g_a
000000000000200c g     O .rodata        0000000000000004              g_c

未初始化的const变量存储在.bss还是.rodata未定义行为,不同机器、优化选项、编译器版本、目标平台ABI可能都会影响,并未作规定。

#进程的内存布局

进程中的内存布局与ELF文件的布局是相似的。运行可执行文件后,操作系统会将ELF文件中的Section拷贝到内存中去,同时给出HeapStack空间。

updatedupdated2025-07-252025-07-25