本文件用于指导软件编码人员进行规范性编码,便于部门/公司内部统一软件代码形式,逐步向规范性、高质量软件代码迭代。
本文档适用于c/c++、c#等编程语言,文档内未明确约定规范的编程语言,可以参考其他语言开展编码,同时,本文档应随实际情况进行扩充。

格式

每个人都可能有自己的代码风格和格式,因人,系统而异各有优缺点。但整个项目服从统一的编程风格是很重要的,只有这样才能让所有人轻松地阅读和理解代码。每个人未必能同意下述的每一处格式规则,而且其中的不少规则需要一定时间的适应。

行长度

每一行代码字符数不超过 80,把 22 寸的显示屏都占完,怎么也说不过去。80 行限制事实上有助于避免代码可读性失控,比如超多重嵌套块,超多重函数调用等等。

文件编码

尽量不使用非 ASCII 字符,诺项目中只需要支持中文,使用GB2312,GBK编码即可,否则请使用UTF-8 编码,因为很多工具都可以理解和处理 UTF-8 编码。

尽量不将字符串常量耦合到代码中,比如独立出资源文件,即使是英文,也不应将用户界面的文本硬编码到源代码中,这不仅仅是风格问题了。

空格还是制表位

UNIX/Linux 下无条件使用空格,MSVC 的话使用 Tab 也无可厚非,每次缩进 4 个空格。

<!--
9。8。条件语句

倾向于不在圆括号内使用空格。关键字 ifelse 另起一行。

对基本条件语句有两种可以接受的格式。一种在圆括号和条件之间有空格,另一种没有。

最常见的是没有空格的格式。哪一种都可以,最重要的是 保持一致。如果你是在修改一个文件,参考当前已有格式。如果是写新的代码,参考目录下或项目中其它文件。还在犹豫的话,就不要加空格了。

9。9。循环和开关选择语句

总述

switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {}continue

说明

switch 语句中的 case 块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。

如果有不满足 case 条件的枚举值,switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理,编译器将给出 warning)。如果 default 应该永远执行不到,简单的加条 assert:

。。code-block:: c++

switch (var) {
  case 0: {  // 2 空格缩进
    ...     // 4 空格缩进
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}

在单语句循环里,括号可用可不用:

for (int i = 0; i < kSomeNumber; ++i)
  printf("I love you\n");

for (int i = 0; i < kSomeNumber; ++i) {
  printf("I take it back\n");
}

空循环体应使用 {}continue,而不是一个简单的分号。

while (condition) {
  // 反复循环直到条件失效。
}
for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循环体。
while (condition) continue;  // 可 - contunue 表明没有逻辑。

while (condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一。-->

指针和引用表达式

句点或箭头前后不要有空格。指针/地址操作符 (*,&) 之后不能有空格。

// 好
    x = *p;
    p = &x;
    x = r.y;
    x = r->y;
    char *c;
    const string &str;

    // 坏
    char* c;
    const string& str;
    int x,*y;  // 不允许 - 在多重声明中不能使用 & 或 *
    char * c;  // 差 * 两边都有空格
    const string & str;  // 差 & 两边都有空格。

<!--
9。12。函数返回值

总述

不要在 return 表达式里加上非必须的圆括号。

说明

只有在写 x = expr 要加上括号的时候才在 return expr; 里使用括号。

。。code-block:: c++

return result;                  // 返回值很简单,没有圆括号。
// 可以用圆括号把复杂表达式圈起来,改善可读性。
return (some_long_condition &&
        another_condition);

。。code-block:: c++

return (value);                // 毕竟您从来不会写 var = (value);
return(result);                // return 可不是函数! -->

预处理指令

预处理指令不要缩进,从行首开始。即使预处理指令位于缩进代码块中,指令也应从行首开始。

// 好 - 指令从行首开始
    if (lopsided_score) {
#if DISASTER_PENDING      // 正确 - 从行首开始
    DropEverything();
# if NOTIFY               // 非必要 - # 后跟空格
    NotifyClient();
# endif
#endif
    BackToNormal();
    }

// 差 - 指令缩进
    if (lopsided_score) {
    #if DISASTER_PENDING  // 差 - "#if" 应该放在行开头
    DropEverything();
    #endif                // 差 - "#endif" 不要缩进
    BackToNormal();
    }

留白

水平/垂直留白不要滥用,越少越好,怎么易读怎么来。基本原则是:同一屏可以显示的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样难看,这取决于你的判断。

  • 函数体内开头或结尾的空行可读性微乎其微。
  • 在多重 if-else 块里加空行或许有点可读性。

这不仅仅是规则而是原则问题了,不在万不得已,不要使用空行。尤其是两个函数定义之间的空行不要超过 2 行,函数体首尾不要留空行,函数体中也不要随意添加空行。

添加冗余的留白会给其他人编辑时造成额外负担。因此,行尾不要留空格。如果确定一行代码已经修改完毕,将多余的空格去掉。

命名约定

最重要的一致性规则是命名管理。命名规则具有一定随意性,但相比按个人喜好命名,一致性更重要,所以无论你认为它们是否重要,规则总归是规则。

通用命名规则

函数命名,变量命名,文件命名要有描述性。少用缩写,尽可能使用描述性的命名。

别心疼空间,毕竟相比之下让代码易于新读者理解更重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉几个字母来缩写单词。

int price_count_reader;    // 无缩写
int num_errors;            // "num" 是一个常见的写法
int num_dns_connections;   // 人人都知道 "DNS" 是什么

int n;                     // 毫无意义。
int nerr;                  // 含糊不清的缩写。
int n_comp_conns;          // 含糊不清的缩写。
int wgc_connections;       // 只有贵团队知道是什么意思。
int pc_reader;             // "pc" 有太多可能的解释了。
int cstmr_id;              // 删减了若干字母。

注意,一些特定的广为人知的缩写是允许的,例如用 i 表示迭代变量和用 T 表示模板参数。

文件命名

文件名要全部小写,可以包含下划线 (_) 或连字符 (-),依照项目的约定。如果没有约定,那么 “_” 更好。

可接受的文件命名示例:

  • my_useful.c
  • my-useful.c
  • myuseful.c
  • myuseful_test.c

C 文件要以 .c 结尾,头文件以 .h 结尾。专门插入文本的文件则以 .inc 结尾.

不要使用已经存在于 /usr/include 下的文件名 (即编译器搜索系统头文件的路径),如 db.h

通常应尽量让文件名更加明确。http_server_logs.h就比logs.h要好。

内联函数定义必须放在 .h 文件中。如果内联函数比较短,就直接将实现也放在 .h 中。

<!--

类型命名

类型名称的每个单词首字母均大写,不包含下划线: MyExcitingClass,MyExcitingEnum。

说明

所有类型命名 —— 类,结构体,类型定义 (typedef),枚举,类型模板参数 —— 均使用相同约定,即以大写字母开始,每个单词首字母均大写,不包含下划线。例如:

// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// 类型定义
typedef hash_map PropertiesMap;

// using 别名
using PropertiesMap = hash_map;

// 枚举
enum UrlTableErrors { ... -->

变量命名

变量(包括函数参数)和数据成员名一律小写,单词之间用下划线连接。类的成员变量以下划线结尾,但结构体的就不用,如:

  • a_local_variable
  • a_struct_data_member
  • a_class_data_member_
string table_name;  // 好 用下划线。
string tablename;   // 好 全小写。

string tableName;  // 差 混合大小写

常量命名

声明为 constexpr 或 const 的变量,或在程序运行期间其值始终保持不变的,命名时以 “k” 开头,大小写混合。例如:

const int kDaysInAWeek = 7;

所有具有静态存储类型的变量 (例如静态变量或全局变量,参见 存储类型) 都应当以此方式命名。对于其他存储类型的变量,如自动变量等,这条规则是可选的。如果不采用这条规则,就按照一般的变量命名规则。

函数命名

常规函数使用大小写混合,取值和设值函数则要求与变量名匹配:

  • MyExcitingFunction()
  • MyExcitingMethod()
  • my_exciting_member_variable()
  • set_my_exciting_member_variable()

一般来说,函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”),没有下划线。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写 (例如,写作 StartRpc() 而非 StartRPC())。

AddTableEntry()
DeleteUrl()
OpenFileOrDie()

(同样的命名规则同时适用于类作用域与命名空间作用域的常量,因为它们是作为 API 的一部分暴露对外的,因此应当让它们看起来像是一个函数,因为在这时,它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节。)

取值和设值函数的命名与变量一致。一般来说它们的名称与实际的成员变量对应,但并不强制要求。例如 int count() 与 void set_count(int count)。

枚举命名

枚举的命名应当和 常量 或 宏 一致: kEnumName 或是 ENUM_NAME。

单独的枚举值应该优先采用 常量 的命名方式。但 宏 方式的命名也可以接受。枚举名 UrlTableErrors (以及 AlternateUrlTableErrors) 是类型,所以要用大小写混合的方式。

enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
    OK = 0,
    OUT_OF_MEMORY = 1,
    MALFORMED_INPUT = 2,
};

函数命名

  1. 函数命名使用小写英语单词和下划线组合的方式,单词之间用下划线分隔,要求能够直接表达函数功能;
  2. 每个函数必须明确形参和返回值,若函数无形参或返回值,则声明定义均为“void”;
  3. 函数声明与函数定义必须一致,禁止使用无形参名称的函数声明;

结构体或其他类型命名

  1. 结构体类型定义使用全小写英文单词,以下划线为间隔,以“_s”为结尾,例如“grd_air_msg_s”;
  2. 枚举类型定义使用全小写英文单词,以下划线为间隔,以“_enum”为结尾,例如“msg_id_enum”;

宏定义

a) 所有宏定义必须大写;
b) 头文件的预处理宏定义格式为头文件名称全部大写,且增加后缀“_H_”,例如文件example。h的头文件预处理宏定义如下:

ifndef EXAMPLE_H_

define EXAMPLE_H_

/**/

endif

宏命名

你并不打算使用宏,对吧? 如果你一定要用,像这样命名:

MY_MACRO_THAT_SCARES_SMALL_CHILDREN

通常不应该使用宏。如果不得不用,其命名像枚举命名一样全部大写,使用下划线:

#define ROUND(x) ...
#define PI_ROUNDED 3.0

变量命名

  1. 变量命名使用英文单词或缩写,禁止使用汉语拼音;
  2. 变量命名使用驼峰命名,变量的首个单词字母小写,后续单词首字母大写,单词/缩写尽量使用行业内通用的命名方式;
  3. 变量命名使用前缀标识类型及作用域,采用前缀的格式为“AB_varNameExample”,其中AB为前缀,说明见下表。
前缀说明
Ag全局变量
s静态变量
m成员变量
p函数形参
缺省局部变量
Bbint8_t
Buint8_t
hint16_t
Huint16_t
iint32_t
Iuint32_t
ffloat
Fdouble
qint64_t
Quint64_t
cclass 类
sstruct 结构体
nunion 联合体

<!--

操作符

。。code-block:: c++

// 赋值运算符前后总是有空格。
x = 0;

// 其它二元操作符也前后恒有空格,不过对于表达式的子式可以不加空格。
// 圆括号内部没有紧邻空格。
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// 在参数和一元操作符之间不加空格。
x = -5;
++x;
if (x && !y)
  ...

模板和转换


    // 尖括号(< and >) 不与空格紧邻,< 前没有空格,> 和 ( 之间也没有。
    vector<string> x;
    y = static_cast<char*>(x);

    // 在类型与指针操作符之间留空格也可以,但要保持一致。
    vector<char *> x;

头文件

每个 .c 文件应该有一个配套的 .h 文件,常见的例外情况包括单元测试和仅有 main() 函数的 .c 文件,正确使用头文件会大大改善代码的可读性和执行文件的大小、性能。

自给自足的头文件

头文件应该自给自足 (self-contained,也就是可以独立编译),也就是头文件的使用者和重构工具在导入文件时无需任何特殊的前提条件。

具体来说,头文件要有头文件防护符 (#define 防护符),并导入它所需的所有其它头文件,并以 .h 为扩展名。

内联函数

只把 10 行以下的小函数定义为内联 inline

滥用内联将拖慢程序。根据函数体积,内联可能会增加或减少代码体积。合理的经验法则是不要内联超过 10 行的函数。只要内联函数体积较小,内联函数可以令目标代码 (object code) 更加高效

#include 的路径及顺序

推荐按照以下顺序导入头文件: 配套的头文件,C 语言系统库头文件,C++ 标准库头文件,其他库的头文件,本项目的头文件。

#include "foo/server/fooserver.h"

#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "foo/server/bar.h"
#include "third_party/absl/flags/flag.h"

作用域

内部链接

若其他文件不需要使用 .c 文件中的定义,这些定义可以放入匿名命名空间 (unnamed namespace) 或声明为 static,以实现内部链接 (internal linkage)。但是不要在 .h 文件中使用这些手段。

局部变量

应该尽可能缩小函数变量的作用域 (scope),并在声明的同时初始化
我们提倡尽可能缩小变量的作用域,且声明离第一次使用的位置越近越好。

这样读者更容易找到声明,了解变量的类型和初始值。特别地,应该直接初始化变量而非先声明再赋值,比如:

int i;
i = f();     // 不好: 初始化和声明分离。
int i = f(); // 良好: 声明时初始化。
int jobs = NumJobs();
// 更多代码...
f(jobs);      // 不好: 初始化和使用位置分离。
int jobs = NumJobs();
f(jobs);      // 良好: 初始化以后立即 (或很快) 使用。

函数

输入和输出

我们倾向于按值返回,否则按引用返回。避免返回指针,除非它可以为空。

我们倾向于使用返回值而不是输出参数: 它们提高了可读性,并且通常提供相同或更好的性能

函数参数或者是函数的输入,或者是函数的输出,或兼而有之。非可选输入参数通常是值参或 const 引用,使用 const 指针来表示可选的其他输入. 使用非常量指针来表示可选输出和可选输入/输出参数.

在排序函数参数时,将所有输入参数放在所有输出参数之前。特别要注意,在加入新参数时不要因为它们是新参数就置于参数列表最后,而是仍然要按照前述的规则,即将新的输入参数也置于输出参数之前。

这并非一个硬性规定。输入/输出参数 (通常是类或结构体) 让这个问题变得复杂。并且,有时候为了其他函数保持一致,你可能不得不有所变通。

编写简短函数

我们倾向于编写简短,凝练的函数。

我们承认长函数有时是合理的,因此并不硬性限制函数的长度。如果函数超过 40 行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。

即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的 bug。使函数尽量简短,以便于他人阅读和修改代码。

在处理代码时,你可能会发现复杂的长函数。不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难,或者你只需要使用其中的一小段代码,考虑将其分割为更加简短并易于管理的若干函数。

前置自增和自减

对于迭代器和其他模板对象使用前缀形式 (++i) 的自增,自减运算符。

不考虑返回值的话,前置自增 (++i) 通常要比后置自增 (i++) 效率更高。因为后置自增 (或自减) 需要对表达式的值 i 进行一次拷贝。如果 i 是迭代器或其他非数值类型,拷贝的代价是比较大的。既然两种自增方式实现的功能一样,为什么不总是使用前置自增呢?

对简单数值 (非对象),两种都无所谓。对迭代器和模板类型,使用前置自增 (自减)。

const 用法

我们强烈建议你在任何可能的情况下都要使用 const。

在声明的变量或参数前加上关键字 const 用于指明变量值不可被篡改 (如 const int foo )。const 变量,数据成员,函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误。因此,我们强烈建议在任何可能的情况下使用 const:

  • 如果函数不会修改你传入的引用或指针类型参数,该参数应声明为 const。
  • 尽可能将函数声明为 const。访问函数应该总是 const。其他不会修改任何数据成员,未调用非 const 函数,不会返回数据成员非 const 指针或引用的函数也应该声明成 const。
  • 如果数据成员在对象构造之后不再发生变化,可将其定义为 const。

然而,也不要发了疯似的使用 const。像 const int * const * const x; 就有些过了,虽然它非常精确的描述了常量 x。关注真正有帮助意义的信息: 前面的例子写成 const int** x 就够了

const 的位置:
将 const 放在前面才更易读,因为在自然语言中形容词 (const) 是在名词 (int) 之前。我们提倡但不强制 const 在前。但要保持代码的一致性

预处理宏

使用宏时要非常谨慎,尽量以内联函数,枚举和常量代替之。

宏意味着你和编译器看到的代码是不同的。这可能会导致异常行为,尤其因为宏具有全局作用域。

宏可以做一些其他技术无法实现的事情,在一些代码库 (尤其是底层库中) 可以看到宏的某些特性 (如用 # 字符串化,用 ## 连接等等)。但在使用前,仔细考虑一下能不能不使用宏达到同样的目的。

下面给出的用法模式可以避免使用宏带来的问题; 如果你要宏,尽可能遵守:

  • 不要在 .h 文件中定义宏。
  • 在马上要使用时才进行 #define,使用后要立即 #undef。
  • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
  • 不要试图使用展开后会导致构造不稳定的宏,不然也至少要附上文档说明其行为。
  • 不要用 ## 处理函数,类和变量的名字。

sizeof

尽可能用 sizeof(varname) 代替 sizeof(type)。

使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新。您或许会用 sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。

代码风格

a) 操作符两边应加空格,例如“=”、“<”、“+”、“/”、“%”、“-”、“>”、“*”、“&”、“|”等;
b) 对于条件判断语句,有else if分支,则必须增加else分支;
c) 对于switch语句,必须有default分支,且每个case必须有break语句;
d) 禁止使用goto语句;
e) 对于语句块,使用“{”、“}”进行分隔,应单独换行,例如:
if (a > b)
{
/操作/
}
f) 使用tab或者空格进行代码缩进。

注释

a) 注释目的是为了能够使自己或他人能够读懂代码,在文档不完善的实际情况下可以通过注释提高软件代码认知效率,因此注释应同时兼顾可读性、有效性和准确性;
b) 注释应使用中文,若开发环境不允许,则应使用能够表达含义的英文,严禁使用拼音或者拼音缩写;
c) 软件更改记录一般通过版本管理软件进行记录,因此注释重点关注软件功能、软件设计思想等,对于软件更改记录、bug 修改记录可减少关注。

编译(必要)

所有的源文件编译过程不得有编译错误,即 0 warning,0 error。很多人会容忍warning的存在,但这是不对的,可能项目存在定义但未使用的代码然后触发warning,虽然无关紧要,但是太多的时候会把真正的warning掩盖掉,比如if (x = y)这种写法会触发warning,如果你的项目本来就有几十个warning的时候,那么多这1个也很难去看到,于是你就错过了编译器帮你排查BUG的机会,当出现问题的时候需要花费额外的时间去排查。因此永远遵守0 warning,0 error能让编译器帮你识别很多问题;

建议)不应该注释掉代码

注释代码容易和真正的注释产生混淆,如果真的想暂时屏蔽一段代码,应该要明确标注出起点和终点,使用#if或者#ifdef起点然后#endif终点,但是我觉得最好的做法是删掉他们,利用git等代码管理工具来保存记录

??很多人在实际项目中暂时取消某个功能就是使用注释,因为注释一遍都是有快捷键的,很方便就能选中代码然后注释掉,但是由于真正的注释和这种行为的注释用法是一样的,会让其他人看这个代码产生混淆,它到底有没有作用,如果真的存在一个能符合语法的注释它真的是一个注释,而且混淆在这些屏蔽代码里面,那么程序员将很难分清代码行为。

void FuncA(void)
{
    uint8_t Cnt = 0;
    
    //符合Dir 4。4写法
#if 0
    Cnt++;
#endif

    //不符合Dir 4。4写法
//    Cnt++;
}

(必要)检查传递给库函数参数值的有效性

在调用库函数之前,必须先检查参数是否有效,再进行调用,而不是相信库函数会自己检查,原则上就是不相信其它库的接口在遇到非法数据时行为是正常的,都要假设是异常的。

??其实这种思维在Linux早已根深蒂固,Linux内核的设计思维之一就是不相信用户会正确使用内核函数,因此给用户加了各种权限,让用户需要通过各种验证后才能使用接口,所有我们在调用别人的库之前就需要检查入口,同时编写接口的时候也要做到入口参数的检查。

/* A。h */
extern void FuncA(uint8_t* pMsg);
extern uint8_t* GetMsgPointer(void);


/* B。c */
void FuncB(void)
{
    uint8_t* pM = GetMsgPointer();
    
    //符合Dir 4。11写法
    if (NULL != pM) {
        FuncA(pM);
    }

    //不符合Dir 4。11写法
    FuncA(pM);
}

循环

for 循环

禁止修改循环条件和步进长度

for 循环的基本框架结构如下,

for (i = start; i < end; ++i)
{
    /* In this area,user shall not change end and i*/
}

在该框架下,end 的值应该是确定的不可变,即在循环过程中不能更改循环结束条件。同时,i 的值也不应该在循环体中被改变。如果确实需要在循环过程中修改 end 或 i 的值,请选择使用 while 循环。

少用--

for 循环按照步进方向可以分为两种写法,

/* Increasing circulation */
for (i = start; i < end; ++i)
/* Decreasing circulation */
for (i = end-1; i >= start; --i)

但在实际实践中,还是要尽量避免--的写法,特别是在嵌入式底层开发中。在下述例证中,该 for 循环会由于无法达到 0,从而陷入死循环中

for (unsinged char i=50; i>=0; i--)

死循环的最好选择

在某些场景下,需要使用死循环语句,特别是主程序循环。死循环有两种写法,最好还是选用 for 循环,因为该语句编译器通常不会有警告。但是死循环在程序中通常都是危险的行为,如果需要死循环,请加以注释说明。

for(;;) {}
while(1) {}

do-while

在实际的编程中,do-while 语句的使用场景有限,因为该结构的循环特性与 for、while 不一致,使用不仔细或者按照 for、while 的习惯使用,可能引入隐性 BUG。不过在如下场景中,do-while 语句却是十分重要。该语句将 do-while 部分的代码打包成一个整体,在宏替换时,可以保证内部语句执行完整,且不受外部使用的影响。

#define MEMEM_DIM2_FREE(data) do {\
    MEM_FREE(data。UserPtr);\
    MEM_FREE(data。PhyPtr);\
} while(0)

另一个重要的使用方式就是需要随意终止执行流的场景中。

do {
    if (condition1 < condition2)
    {
        break;
    }

    ...

    if (condition3 < condition4)
    {
        break;
    }
} while(0);

while

嵌入式编程追求的是尽可能高的确定性,因此建议优先使用 for 循环。如果不适合使用 for 循环的场景,选择使用 while 循环。

注释选择

目前 C 语言有两种注释方式,使用///**/

1 // This is comments。
2 /* This is comments。*/

在高可靠性编程中,建议使用/**/,禁用//。因为/**/注释有开始和结束位置,而//是开放性语句。同时,/**/的写作负载高于//,使用//可以很轻易的注释一条语句,而该语句到底是否应该被注释,对于阅读代码的人来说是一种负担。

如果需要连续注释,建议/**/的注释写作如下格式,

/*
 * This is comments。
 */

关于注释位置问题,建议优先选择在语句上方进行注释,少用或者不用尾注释。代码的一个重要作用给程序员阅读,因此整体一致的风格往往能提高阅读舒适度。

/* Init modulation parameter */
Demo_FunInit(Demo_SystemData);

条件判断

对于不少于一个 else if 的语句,一定需要有 else 语句,哪怕这条语句没有不执行任何操作。这样做的目的是在多判断条件下,有 else 表明设计者认为所有条件已经考虑清楚。而没有 else 语句,代码阅读者不确定设计者是否考虑完所有的判断情况。

if (condition1 < condition2)
{
    ...
}
else if (condition3 < condition4)
{
    ...
}
else
{
    /*do nothing*/
}

对于范围判断,可以使用 if-else 结构,但是如果是单值判断,建议替换为 switch-case 结构。

/*Bad practice*/
if (val == 1)
{
    ...
}
else if (val == 10)
{
    ...
}
...

代码可以修改为,

/* Good pratice */
switch (val)
{
    case 1:
        ...
    case 10:
        ...
    ...
}

switch-case

switch-case 的基本句式为,

switch (val)
{
    case condition1:
        ...
        break;
    case condition2:
        ...
        break;

    ...

    default:
       ...
       break;
}

善用 switch 语句,可以增加代码的条理性,在某些场景下也能提高代码执行效率。需要注意,在所有 switch-case 中都必须有 default 路径。

如果有可能,用表驱动的方式替换 switch-case 语句,可以让代码更加简单。

变量

局部变量

局部变量必须初始化,这样能避免由于使用未初始化变量而引入的稀奇古怪的问题。

局部变量使用统一而区别于全局变量的命名方式。

禁止使用汉语拼音构建变量名称。

变量命名应该能体现变量的实际意义。

如果变量占用内存过大,应考虑将该变量转变为全局变量或使用动态内存分配方式,防止栈溢出。

遵守 C 语言编译规则,变量定义应该在作用域的开始位置;遵循 C++规则,可以在使用之前定义。如果代码要在不同的环境中编译,建议遵循 C 语言规则。

过多的局部变量可能意味着过长的代码或过于复杂的代码,此时需要考虑将函数拆分。

局部静态变量

虽然部分教程列举了局部静态变量相对于全局变量的优点,但在工程实践中,请禁用局部静态变量。

在高安全高可靠性领域的应用中,要求变量定义在特定的内存区域中,用于数据保护,此时局部静态变量对于这种场景而言是灾难性的存在。

在高性能领域,并发往往是常见的需求,此时局部静态变量就是暗藏的炸弹。

全局变量

全局变量应限制在模块内使用,禁止在模块之间使用。例如 ADC 模块定义一个全局变量 Adc_GlobalRegBuffer,该变量可以在 Adc.c,Adc_Hardware.c 中使用,但不能在 Dio.c 中使用。

在同一个模块中,只定义一个全局变量,全局变量的类型是结构体。模块中使用的全局变量均定义在该结构体中,当调试时拉一个变量就能观测到整个模块的运行状态。同时,这种构建方式,也方便定义变量在内存中的位置,或者将其转为局部变量。

最后修改:2024 年 08 月 07 日 08 : 46 AM
赏口饭吃,行行好吧,客官!