C/C++大型项目编程规范

​ 最近在参与(或者说使用)某些项目的代码时,感觉其代码之简洁精炼,语言习惯之规范,外观之整洁,可读性之高令人赏心悦目。

​ 在其基础上进行增删改查后总是觉得自己的代码不够优雅,就好像一篇书法作品中掺杂几行我写的字。

​ 于是打算学习C/C++项目编程规范,不求优雅,只求规整可读,结构合理。

​ 基于华为C语言编程规范 在线wiki文档

代码总体原则

1、清晰第一

代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化

  • 优秀的代码可以自我解释(以我的水平可能做到比较难)

  • 常写注释,并且注释写的清晰

2、简洁为美

写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。

术语

原则:编程时必须坚持的指导思想。

规则:编程时强制必须遵守的约定。

建议:编程时必须加以考虑的约定。

说明:对此原则/规则/建议进行必要的解释。

示例:对此原则/规则/建议从正、反两个方面给出例子。

延伸阅读材料:建议进一步阅读的参考材料。

头文件

原则

不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。

如果引入了新的依赖,则一旦被依赖的头文件修改,任何直接和间接依赖其头文件的代码都会被重新编译。

原则1.1 头文件中适合放置接口的声明,不适合放置实现

头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

  • 内部使用的函数(相当于类的私有方法)声明不应放在头文件中
  • 内部使用的宏、枚举、结构定义不应放入头文件中。
  • 变量定义不应放在头文件中,应放在.c文件中。

否则多次依赖会重复定义

  • 变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

原则1.2 头文件应当职责单一。

原则1.3 头文件应向稳定的方向包含。

说明:头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

规则

规则1.1 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口。

说明:如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

规则1.2 禁止头文件循环依赖

​ 任何一个头文件的改变都会使得循环中的所有头文件重新编译

规则1.3 .c/.h文件禁止包含用不到的头文件。

规则1.4 头文件应当自包含。

“头文件应当自包含”是指头文件应该包含自身所需的所有内容,而不依赖于其他头文件。这样的头文件通常被称为”自包含头文件”。下面解释一下这个概念的意义:

  1. 独立性和可移植性:自包含头文件使得头文件本身更加独立,不依赖于其他头文件。这样做有助于提高代码的可移植性,因为当你在其他项目或环境中使用这个头文件时,不需要担心它依赖的其他头文件是否可用。
  2. 简化依赖关系:自包含头文件可以简化代码的依赖关系。如果一个头文件依赖于另一个头文件,而后者又依赖于其他头文件,这会形成复杂的依赖链。通过自包含头文件,可以减少这种依赖链,提高代码的可维护性。
  3. 避免重复包含:自包含头文件通常会包含预处理器指令来避免重复包含。这样可以确保在包含相同头文件多次时不会导致重复定义的问题。
  4. 提高效率:自包含头文件可以减少预处理器的工作量,因为它们不需要解析其他头文件的内容。这有助于提高编译效率。

规则1.5 总是编写内部#include保护符(#define 保护)。

所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H

规则1.6 禁止在头文件中定义变量。

说明:在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。

规则1.7 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量。

规则1.8 禁止在extern “C”中包含头文件。

extern "C" 是用于在 C++ 中声明 C 函数时的一种语法。它告诉编译器这些函数按照 C 语言的约定进行链接。

在 C++ 中,函数名的重载、名称修饰(name mangling)等特性会导致函数名在编译后被修改,这样的函数名在链接时可能无法与 C 代码中的函数名匹配。为了解决这个问题,C++ 提供了 extern "C",它告诉编译器不要对函数名进行 C++ 风格的名称修饰,而是按照 C 语言的规则进行链接。

函数

函数设计的精髓:编写整洁函数,同时把代码有效组织起来。

整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。

代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。

原则

原则2.1 一个函数仅完成一件功能。

说明:一个函数实现多个功能给开发、使用、维护都带来很大的困难。

将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。

原则2.2 重复代码应该尽可能提炼成函数

说明:重复代码提炼成函数可以带来维护成本的降低。

可以使用代码重复度检查工具

规则

规则2.1 避免函数过长,新增函数不超过50行(非空非注释行)。

规则2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层。

减少代码嵌套层数的方法

  1. 使用函数抽象:将嵌套的代码块提取成独立的函数,以便于重用和理解。
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
>python复制代码def main_function():
if condition:
process_items(items)
else:
handle_condition_not_met()

>def process_items(items):
for item in items:
if item_valid(item):
process_item(item)
else:
handle_invalid_item(item)

>def item_valid(item):
return item.condition

>def process_item(item):
# 处理item
pass

>def handle_invalid_item(item):
# 处理无效item
pass

>def handle_condition_not_met():
# 处理条件未满足情况
pass
  1. 使用早期返回:在函数内部,尽早返回结果,而不是在多层嵌套中处理。
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
>def main_function():
if not condition:
handle_condition_not_met()
return

for item in items:
if not item_valid(item):
handle_invalid_item(item)
continue
process_item(item)

>def item_valid(item):
return item.condition

>def process_item(item):
# 处理item
pass

>def handle_invalid_item(item):
# 处理无效item
pass

>def handle_condition_not_met():
# 处理条件未满足情况
pass
  1. 使用异常处理:适用于处理特殊情况或错误的情况。
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
>def main_function():
try:
if condition:
for item in items:
process_item(item)
else:
raise ConditionNotMetError
except ConditionNotMetError:
handle_condition_not_met()

>def process_item(item):
if not item_valid(item):
raise InvalidItemError
# 处理item

>def item_valid(item):
return item.condition

>def handle_condition_not_met():
# 处理条件未满足情况
pass

>class ConditionNotMetError(Exception):
pass

>class InvalidItemError(Exception):
pass

这些方法可以帮助将嵌套降低到合理的水平,使代码更易读、更易维护。

规则2.3 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。

可能用不到。

说明:可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量。

规则2.4 对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。

规则2.5 对函数的错误返回码要全面处理。

规则2.6 设计高扇入,合理扇出(小于7)的函数。

说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。

建议2.1 函数不变参数使用const。

建议2.2 函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用。

建议2.4 函数的参数个数不超过5个。

建议2.5 除打印类函数外,不要使用可变长参函数。

3标识符命名与定义

通用命名规则

  1. unix like风格

    单词用小写字母,每个单词直接用下划线‘_’分割,例如text_mutex,kernel_text_address。

  2. Windows风格

    大小写字母混用,单词连在一起,每个单词首字母大写

  3. 匈牙利命名法

    匈牙利命名主要包括三个部分:基本类型、一个或更多的前缀、一个限定词。

原则

原则3.1标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。

原则3.2 除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音。

示例:一些常见可以缩写的例子:

  • argument 可缩写为 arg
  • buffer 可缩写为 buff
  • clock 可缩写为 clk
  • command 可缩写为 cmd
  • compare 可缩写为 cmp
  • configuration 可缩写为 cfg
  • device 可缩写为 dev
  • error 可缩写为 err
  • hexadecimal 可缩写为 hex
  • increment 可缩写为 inc、
  • initialize 可缩写为 init
  • maximum 可缩写为 max
  • message 可缩写为 msg
  • minimum 可缩写为 min
  • parameter 可缩写为 para
  • previous 可缩写为 prev
  • register 可缩写为 reg
  • semaphore 可缩写为 sem
  • statistic 可缩写为 stat
  • synchronize 可缩写为 sync
  • temp 可缩写为 tmp

规则

规则3.1 产品/项目组内部应保持统一的命名风格。

示例:

add/remove begin/end create/destroy insert/delete first/last get/release increment/decrement put/get add/delete lock/unlock open/close min/max old/new start/stop next/previous source/target show/hide send/receive source/destination copy/paste up/down

建议

建议3.2 尽量避免名字中出现数字编号,除非逻辑上的确需要编号。

建议3.3 标识符前不应添加模块、项目、产品、部门的名称作为前缀。

建议3.4 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。

文件命名规则

建议

建议3.6 文件命名统一采用小写字符。

变量命名规则

规则

规则3.2 全局变量应增加“g_”前缀。

规则3.3 静态变量应增加“s_”前缀。

规则3.4 禁止使用单字节命名变量,但允许定义i、j、k作为局部循环变量。

建议

建议3.7 不建议使用匈牙利命名法。

建议3.8 使用名词或者形容词+名词方式命名变量。

函数命名规则

建议

建议3.9 函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构。

宏的命名规则

规则

规则3.5对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线‘_’的方式命名(枚举同样建议使用此方式定义)。

规则3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线‘_’开头和结尾。

变量

原则

原则4.1 一个变量只有一个功能,不能把一个变量用作多种用途。

我怎么感觉在某些情况下无妨啊,开多了变量会占用性能吧?

也就是说:重要性:代码的可读性、可维护性和减少错误>性能消耗

下列答案基于chatGPT

遵循“一个变量只有一个功能”的原则确实会增加一些变量的数量,但这通常不会显著增加系统开销,特别是在现代编程环境和硬件条件下。这是因为:

  1. 内存开销相对较小

    • 现代计算机的内存容量通常非常大,增加几个变量所占用的内存是非常小的。这种内存开销在绝大多数应用场景下可以忽略不计。
  2. 编译器优化

    • 现代编译器和解释器非常智能,它们可以优化代码,将不必要的变量删除或者合并,从而减少内存开销。例如,在优化编译过程中,编译器会尝试最小化变量的存储空间,并进行变量复用等优化操作。
  3. 代码可读性和维护性优先

    • 代码的可读性和可维护性在实际开发中比微小的性能优化更重要。可读性好的代码更容易被团队中的其他开发人员理解和维护,从而降低了开发和维护的成本。
  4. 开发效率和错误减少

    • 使用单一功能变量可以减少代码中的错误,使开发过程更加顺利。这种减少错误和提升开发效率的好处往往远远超过因为增加变量数量而带来的微小性能损失。

示例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 增加系统开销的担忧
total_items = 10
price = 5.99
total_cost = total_items * price

# 不好的做法:减少变量数量
x = 10
x = 5.99
x = x * x # 这样做容易引入错误,也降低了代码的可读性

# 正确做法:使用多个变量,增加代码可读性
total_items = 10
price_per_item = 5.99
total_cost = total_items * price_per_item

在这个示例中,尽管引入了多个变量,但它们使代码更容易理解和维护。变量名清晰地表达了它们的用途,减少了开发和调试中的潜在错误。

总结

遵循“一个变量只有一个功能”的原则,不仅不会显著增加系统开销,反而可以通过提升代码质量和开发效率,降低长远的开发和维护成本。因此,在大多数情况下,这种做法是非常值得的。实际开发中,尤其是在团队协作和大型项目中,应优先考虑代码的可读性和可维护性。

原则4.2 结构功能单一;不要设计面面俱到的数据结构。

原则4.3 不用或者少用全局变量。

规则

规则4.1 防止局部变量与全局变量同名。

规则4.2 通讯过程中使用的结构,必须注意字节序。

字节序的解释(基于chatGPT)

字节序(Byte Order)是指在计算机内存中存储多字节数据时,字节的排列顺序。不同计算机系统可能采用不同的字节序,主要有两种类型:大端序(Big-Endian)和小端序(Little-Endian)。

大端序(Big-Endian)

  • 定义:高位字节存储在低地址,低位字节存储在高地址。

  • 示例

    :假设有一个4字节(32位)的数值

    1
    0x12345678

    。在大端序系统中,它的存储顺序如下:

    1
    2
    makefile复制代码地址:   0x00  0x01  0x02  0x03
    值: 0x12 0x34 0x56 0x78

小端序(Little-Endian)

  • 定义:低位字节存储在低地址,高位字节存储在高地址。

  • 示例

    :同样的数值

    1
    0x12345678

    在小端序系统中的存储顺序如下:

    1
    2
    makefile复制代码地址:   0x00  0x01  0x02  0x03
    值: 0x78 0x56 0x34 0x12

为什么字节序很重要

在多字节数据传输或跨平台交互时,字节序的不同可能导致数据解释错误。例如,一个大端序系统发送数据到一个小端序系统,如果不做任何处理,小端序系统可能会错误地解读数据内容。

字节序的处理

  1. 网络字节序:网络协议(如TCP/IP)通常规定使用大端序,这被称为网络字节序(Network Byte Order)。
  2. 字节序转换函数:大多数编程语言提供了用于字节序转换的函数。例如,在C语言中,htons(Host TO Network Short)、htonl(Host TO Network Long)、ntohs(Network TO Host Short)、ntohl(Network TO Host Long)等函数用于在主机字节序和网络字节序之间进行转换。

示例代码

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
c复制代码#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>

void print_bytes(uint32_t value) {
uint8_t *bytes = (uint8_t *)&value;
for (int i = 0; i < 4; i++) {
printf("%02x ", bytes[i]);
}
printf("\n");
}

int main() {
uint32_t host_value = 0x12345678; // 主机字节序
uint32_t network_value = htonl(host_value); // 转换为网络字节序(大端序)

printf("Host byte order: ");
print_bytes(host_value); // 打印主机字节序

printf("Network byte order: ");
print_bytes(network_value); // 打印网络字节序

uint32_t converted_back = ntohl(network_value); // 转换回主机字节序
printf("Converted back to host byte order: ");
print_bytes(converted_back); // 打印转换回的主机字节序

return 0;
}

输出

1
2
3
vbnet复制代码Host byte order: 78 56 34 12 
Network byte order: 12 34 56 78
Converted back to host byte order: 78 56 34 12

总结

字节序在计算机内存中存储多字节数据时非常重要。理解和处理字节序问题,特别是在跨平台或网络通信中,确保数据正确传输和解释是至关重要的。通过明确协议中的字节序和使用合适的字节序转换函数,可以避免数据传输中的错误。

规则4.3 严禁使用未经初始化的变量作为右值。

建议

建议4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象。

建议4.2 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。

建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。

建议4.4 明确全局变量的初始化顺序,避免跨模块的初始化依赖。

建议4.5 尽量减少没有必要的数据类型默认转换与强制转换。

宏、常量

规则

规则5.1 用宏定义表达式时,要使用完备的括号。

规则5.2 将宏所定义的多条表达式放在大括号中。

规则5.3 使用宏时,不允许参数发生变化。

规则5.4 不允许直接使用魔鬼数字。

说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

建议

建议5.1 除非必要,应尽可能使用函数代替宏。

建议5.2 常量建议使用const定义代替宏。

建议5.3 宏定义中尽量不使用return、goto、continue、break等改变程序流程的语句。

质量保证

to be continued

程序效率

to be continued

注释

to be continued

排版与格式

to be continued