关于我个人的编码规范(C/C++)

前言

在当今这个代码如诗的时代,每一行字符都不再仅仅是逻辑与指令的堆砌,它们是思想的载体,是创新的脉络,是协作的桥梁。编程,这一门独特的语言艺术,不仅要求我们精准地传达机器可执行的指令,更期望我们在编织这些数字世界基石的同时,展现出一种对美的追求和对同行的尊重。正因如此,编码风格规范的重要性日益凸显,它不仅是技术严谨性的体现,更是团队协作效率与代码可维护性的重要保障。

我之所以提笔撰写这篇关于编码风格规范的文章,是源自于一个日益显著的观察:在这个信息爆炸、开源共享成为常态的互联网环境中,代码的多样性和随意性似乎成了一把双刃剑。一方面,它激发了无限的创造力;另一方面,却也导致了编码风格的参差不齐。从网络的广阔舞台到日常工作的点滴,不规范的编码风格如同迷雾,遮蔽了代码的清晰度,增加了理解与维护的难度,甚至在无形中设置了沟通的障碍。

身边同事的代码,网上的开源项目,无一不在提醒我们,编码风格规范不仅仅是一套规则那么简单,它是编程文化的体现,是工程师素养的标志。缺乏统一和规范的编码习惯,就如同一座城市缺少规划,虽充满活力却杂乱无章,让每一个试图在其间穿行的人感到困惑与挑战。

希望通过这篇文章,能够唤醒每一位程序员对编码之美、对团队合作精神的深刻认识,让我们共同努力,将代码编织成既功能强大又赏心悦目的艺术品,为数字世界的建设添砖加瓦。让我们一起踏上这段旅程,探索编码风格规范的奥秘,共创更加高效、优雅的编程未来。

代码风格是因人而异的, 而且我不愿意把自己的观点强加给任何人,但这就像我去做任何事情都必须遵循的原则,我也希望在绝大多数事上保持这种的态度。所以综合参考了相关文献,在我工作时要求的编码规范基础上,写了这篇关于 C/C++ 语言编程风格的规则,希望对大家有所帮助。

本文参考了《Linux 内核代码风格》和我在某国产手机工作时的《Android 系统编码规范(C/C++)》

一、文件结构

1. 版权和版本声明(不是必须,但是我建议看看)

版权和版本的声明位于头文件和源文件的开头,其中包括版权、文件名称、功能描述、版本、创建日期、作者、修改记录。版权和版本的声明位于头文件和源文件的开头,内容有:

  1. 版权信息;
  2. 文件名称;
  3. 功能简短描述;
  4. 版本号;
  5. 创建日期;
  6. 作者;
  7. 修改记录。

具体内容如下,格式不局限于此,但上述信息必须包含在内:

/********************************************************************
 * Copyright (c) 201X- 201X XXXXXXXXX, Ltd.
 * File: example.c
 * Description: This file contains the implementation of a simple example program.
 * Version: 1.0
 * Date: 2023-3-31
 * Author: zhengxinyu13@qq.com
 * ---------- Revision History ----------
 * <version> <date> <author> <desc>
 * Revision 2.0, 2024-5-13, zhengxinyu13@qq.com
 *   Modified to be suitable to the new coding rules in all functions.
 * Revision 1.5, 2023-7-24, zhengxinyu13@qq.com
 *   Some features have been updated.
 ********************************************************************/

其中版权、文件名称、功能描述、版本、创建日期、作者这六项是竖向排列,修改记录中的版本号、修改日期、作者、修复说明是横向排列,且修改记录为倒叙(类似 git log 的记录)。

不过对于我个人已经发布到网上的代码而言,我通常会删掉版权一栏,已经是开源的代码了,就没有版权一说了,所以我个人使用的声明如下:

/********************************************************************
 * File: 
 * Description: 
 * Version: 
 * Date: 
 * Author: zhengxinyu13@qq.com
 * ---------- Revision History ----------
 * <version> <date> <author> <desc>
 * 
 ********************************************************************/

[!NOTE]

这里特殊说明一下,一般作者一栏都是写自己的个人邮箱,在软件开发领域,邮箱已经成为了一种标准的联系方式。它简单易用,几乎所有人都有邮箱,并且可以在全球范围内使用。相比电话等联系方式,邮箱提供了一种可以用于联系但又相对匿名的方式(毕竟谁也不希望电话号码被泄露,特别是中国人,电话卡绑定了太多东西了)。而且大部分代码托管平台的通知系统是基于电子邮件的。通过使用邮箱,开发者可以及时接收到有关项目的更新、问题、合并请求等重要信息。

2. 头文件结构

头文件除了版权和版本声明,还必须依次包含预处理块、函数和结构声明。头文件有三部分内容组成:

  1. [头文件开头处的版权和版本声明](#1. 版权和版本声明(不是必须,但是我建议看看));
  2. 预处理块,为了防止头文件被重复引用,应当使用 #ifndef / #define / #endif 结构产生预处理块;
  3. 函数和结构声明等,头文件中只存放 “声明” 而不存放 “定义”,以便于其他文件引用时不会出现重定义错误,inline 内联函数除外。

下面给出一个头文件的实例(假设这个文件叫 Bitmap.h):

#ifndef __BITMAP_H__
#define __BITMAP_H__

#include <SkBitmap.h>
#include "..."

namespace android {
    class Bitmap {
        public:
            Bitmap(JNIEnv* env, jbyteArray storageObj, void* address,
            	   const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable);
            size_t rowBytes() const;
            ...
    };
} // namespace android

#endif //__BITMAP_H__

这里要特别强调头文件中的 #include "..." ,我们只在自己的头文件中包含必须的其他头文件,属于源文件需要的头文件在源文件中包含。

[!NOTE]

使用 #ifndef / #define / #endif 结构时,命名宏除了字母全部大写之外,在宏前后都会多打两个下划线,这属于是行业编码风格约定俗成的规矩了,没有什么特殊意义。

3. 源文件结构

源文件除了版权和版本声明,还必须依次包含头文件的引用和程序的实现体。源文件有三部分内容组成:

  1. [头文件开头处的版权和版本声明](#1. 版权和版本声明(不是必须,但是我建议看看));
  2. 头文件引用;
  3. 程序的实现体(包括数据和代码)。

下面给出一个源文件的实例(假设这个文件叫 Bitmap.cpp):

#include <...>
#include "..."
...
#include “Bitmap.h”
...
namespace android {
    size_t rowBytes() const {
        return mRowBytes;
    }
    ...
} // namespace android

二、排版(以 K&R 风格为主)

1. 缩进与左花括号的位置

缩进的全部意义就在于清楚的定义一个控制块起止于何处,网上很多代码的缩进风格让人看的真的是很难受,建议统一设 Tab 为 4 个空格,采用 Tab 缩进。并且设置按下 Tab 后,自动把 Tab 转换为 4个空格(老程序员都懂)。

至于左花括号的位置,不同人有不同的看法,而我已经习惯了工作中的规范要求,所以我写程序块时,左花括号与它的语句同行,用空格隔开,右花括号与引用它们的语句左对齐,花括号之内的代码块在缩进位置处左对齐。而写函数时,左花括号则另起一行,与引用语句左对齐,其余跟程序块一样。具体如下:

void example_fun_1(void)
{
... // program code
}

void example_fun_2(void)
{
    for (...) {
    ... // program code
    }

    if (...) {
    ... // program code
    } else {
    ... // program code
    }

    switch (variable) {
        case value1:
            ... // program code
            break;
        case value1:
            ... // program code
            break;
        default:
            ... // program code
            break;
    }
}

关于左花括号的位置,也是一个涉及个人风格的问题,我选用这个写法,沿用了 KernighanRitchie(C 语言之父,两人一起编写了[《The C Programming Language》](https://baike.baidu.com/item/C程序设计语言/10640335?fromtitle=The C Programming Language&fromid=1838395))的编码风格,也就是所谓的 K&R 风格,不过我也没有全部照搬,例如函数就不是这个风格,只要用于区分程序块和函数。

2. 空行的插入

相对独立的程序块之间、变量说明之后必须加空行,空行起着分隔程序段落的作用,将使程序的布局更加清晰。

int ret = -1;

if (NULL == dst || NULL == proc) {
    perror();
	return false;
}

const jint *array = env->GetIntArrayElements(srcColors, NULL);
const SkColor *src = (const SkColor *)array + srcOffset;

3. 该分行就分行

切勿把多个短语句写在一行,一行代码只做一件事情,这样的代码容易阅读,并且便于写注释。

以下是不规范的写法:

rect.height = 0; rect.width = 0;

if (NULL == dst || NULL == proc) return;

以下是规范的写法:

rect.height = 0;
rect.width = 0;

if (NULL == dst || NULL == proc)
	return;

4. 花括号

这里又聊到了花括号,一般认为 iffordo-whilewhileswitch 等语句必须使用花括号括起来,从上面的示例也可以看出。

当然,也不是任何情况都要加上这个花括号,当只有一个单独的语句的时候,不用加不必要的大括号。如下:

if (condition)
	action();

或者这样:

if (condition)
	do_this();
else
	do_that();

不过,这并不适用于只有一个条件分支是单语句的情况,这时所有分支都要使用大括号:

if (condition) {
    do_this();
    do_that();
} else {
	otherwise();
}

这里只介绍了 if-else 语句,同样适用于 fordo-whilewhile 等语句。

5. 长语句分段

较长的语句必须分成多行书写,这里说的较长的语句,其实没有统一标准,我个人的标准是每行不超过 80 个字符(包括每次缩进的空格在内)。长表达式要在低优先级操作符处划分新行,操作符放在新行之首(以便突出操作符)。划分出的新行要进行适当的缩进,使排版整齐,语句可读。

例如:

// 判断语句有较长表达式时
if ((very_longer_variable1 >= (very_longer_varable2))
	&& (very_longer_varable3) <= (very_longer_varable4)
	&& (very_longer_varable5) <= (very_longer_varable6)) {
	dosomething();
}

// 函数参数较多或较长时
static void Bitmap_setPixels(JNIEnv* env, jobject, jlong bitmapHandle,
						   jintArray pixelArray, jint offset, jint stride,
						   jint x, jint y, jint width, jint height)
{
    ... // program code
}

// 循环语句有较长表达式时
for (very_longer_initialization;
	 very_longer_condition;
	 very_longer_update) {
	dosomething();
}

6. 空格

比较运算符、赋值运算符、算术运算符、逻辑运算符、位域运算符等双目运算符的前后必须加空格,采用这种松散方式编写代码的目的是使代码更加清晰。以下是各个示例:

  1. 逗号,分号只在后面加空格

    dosomething(parm1, parm2, parm3);
    
    for (i = 0; i < MAX_LENGTH; i++) {
    	... // program code
    }
    
  2. 双目操作符的前后加空格

    比较操作符,赋值操作符( =+=),算术操作符( +% 等),逻辑操作符( &&& 等),位域操作符( << 等)等双目操作符的前后加空格。

    if (current_time >= MAX_TIME_VALUE)
    	... // program code
    
    var1 = var2 + var3;
    var1 *= 2;
    var1 = var2 ^ 2;
    
  3. 结构体内部变量不加空格

    p->id = pid; // "->"前后不加空格
    str.id = pid; // "."前后不加空格
    
  4. 单目操作符前后不加空格

    *p = 'a'; 		 // 内容操作"*"与内容之间
    flag = !isEmpty; // 非操作"!"与内容之间
    p = &mem;		// 地址操作"&" 与内容之间
    i++; 		    // "++","--"与内容之间
    
  5. 部分关键字语句与括号间加空格

    ifforwhileswitch 等与后面的括号间应加空格,使 if 等关键字更为突出、明显。

    if (var1 >= var2 && var3 > var4)
    

7. 无用的代码必须删除

代码文件不用的代码不要用注释等方法来保留在文件中,除非你注释掉的代码非常有代表意义(比如对 Android 源码的修改;有的代码使用特殊,别人来修改你的代码可能犯你当初的错误,那么你这部分注释掉的错误代码可以保留,并增加说明),否则不再使用的代码必须删除。

三、注释

1. 注释用什么语言?

关于注释该用中文还是英文要具体看你这个代码的目的是什么。如果是开源到 GitHub 上的项目,建议用英文,如果是开源到 Gitee 上的项目,那么中英文都行。如果只是学习阶段,用中文也没什么,毕竟中国国民英文水平摆在那里,大家有目共睹,不能强迫一个英语本来就不好的程序员强行使用英文写注释(要是公司要求就没办法了)。如果用中文写注释,要注意编码格式的问题,建议使用 UTF-8 的输入格式。

注释的内容必须清楚准确,拒绝二义性!

2. 注释缩进

注释与所描述内容必须进行同样的缩排,这可使程序排版整齐,并方便注释的阅读与理解。如下:

void example_fun( void ) {
    /* code one comments */
    CodeBlock One
    /* code two comments */
    CodeBlock Two
}

3. 程序块结束注释

超出一屏的程序块的结束行和分支语句(条件分支、循环语句等)结束行右方必须加注释标记,以表明某程序块的结束。主要是当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。如下:

if (...) {
	... // program code
	while (index < MAX_INDEX) {
		... // program code
    } // end of while (index < MAX_INDEX)
} // end of if (...)

#ifdef __IDMAP_H__
...// program code
#else
...// program code
#endif // __IDMAP_H__

4. 两种注释的用法

早期 C 语言只有 /* */ 这种注释方式,最早使用 // 注释代码的是 Java,后来 C 语言也引入了 // 作为注释,其实两种注释产生的结构并没有什么区别。/* */ 更多用于多行的注释信息,例如前面提到的版权和版本信息等,就是用这种方法注释的。而 // 一般是单行代码进行注释。所以一般都是用 /* */ 注释程序块或者函数,注释必须位于其上方相近位置,放于上方则需与
其上面的代码用空行隔开。而 // 一般是写在单行代码的后面,只对一行代码作注释,如果注释内容过程,一行显示不下,也会考虑换成 /* */

5. switch 语句如无需 break,须加上明确的注释

switch 的每个 case 语句,默认要求有 break 返回;如果无需 break,必须加上明确的注释,这样比较清楚程序编写者的意图,有效防止无故遗漏 break 语句。

case CMD_UP:
	ProcessUp();
	break;
case CMD_DOWN:
    ProcessDown();
    break;
case CMD_FWD:
    ProcessFwd();
    if (...) {
    	...
    break;
    } else {
    	ProcessCFW_B(); // now jump into case CMD_A
    }
case CMD_A:
    ProcessA();
    break;
...
default:
    break;

6. 函数块的注释

在多数编程语言中,函数注释风格可能有所不同,但核心元素相似,通常包括函数的概述、参数说明、返回值说明、示例用法和注意事项等。例如,在 Java 或 C++ 中,可能会使用 Javadoc 或 Doxygen 风格的注释,其格式略有不同,但目的相同:

/**
 * @brief exampleFunction - A brief description of the function.
 * @param param1 (Type) Description of param1.
 * @param param2 (Type) Description of param2.
 * @return ReturnType Description of the return value.
 * @throws ExceptionType If something goes wrong, this exception is thrown.
 */
ReturnType ReturnType exampleFunction(Type param1, Type param2) {
    // Function implementation...
}

下面是各个部分的含义:

  • @brief: 简要描述函数的作用或功能。
  • @param: 参数说明,描述函数接受的参数及其含义。
  • @return: 返回值说明,描述函数返回的内容或类型。
  • @throws: 异常说明,描述函数可能抛出的异常或错误情况。

这种文档注释通常用于提供函数的详细信息,使得其他开发者能够快速了解函数的使用方法和预期行为。

四、命名规则

1. 变量与函数的命名

我对于变量名和函数名的命名法,通常根据开发的情况来决定的。一般做 C51 单片机和 Linux 驱动相关的开发,用下划线命名法。开发 STM32 单片机、Arduino 单片机和 Linux 应用层程序时,用小驼峰命名法,少部分 Linux 应用层开发用大驼峰命名法。如果涉及到 FreeRTOS 相关的开发,可能还会用到匈牙利命名法

命名法只是基本要求,比较重要的是,千万不要用拼音去命名,除非是一些人名或者地名(人名或者地名一般也不太会出现在代码中),英文差没关系,可以去查,现在工具非常多。

也可以使用一些程序员公认的英文缩写,目前约定俗成的有如下表所示的英语单词:

常见单词 惯用写法 含义
argument arg 传入的参数
buffer buf 缓冲存储区
clock clk 时钟
command cmd 命令
compare cmp 比较
configuration cfg / conf / config 配置
device dev 设备
error err 错误
hexadecimal hex 十六进制数的
increment inc 增量器
initalize init 初始化
maximum max 最大的
message msg 消息
minimum min 最小的
parameter para 参数
previous prev 上一个的
register reg 注册
semaphore sem 信号标志
statistic stat 统计数据
synchronize sync 同步器
temp tmp 临时变量
function fun 函数

函数的命名一般选择**动宾短语,**即动词+宾语,这样做可较好地说明函数的功能,而且函数名须准确描述函数的功能。

另外,禁止取名单个字符变量(如 i、j、k…),局部循环变量除外。

2. 宏定义和常数的命名

常量名、宏必须用大写字母和阿拉伯数字命名,并用下划线连接单词,不能有其他符号,也不能用阿拉伯数字开头。禁止任何未经定义的常量值直接被使用,常量值必须使用宏或const 常量定义,禁止在函数里直接使用。不然就是魔鬼数字

例如,表示一分钟多少秒这样的常量,一小时多少分钟这样的常量,都可以用宏定义或者关键字 const 定义,如下:

#define SECONDS_IN_MINUTE 60
#define MINUTES_IN_HOUR   60
#define HOURS_IN_DAY      24
#define DAYS_IN_YEAR      365
#define SECONDS_IN_YEAR (SECONDS_IN_MINUTE * MINUTES_IN_HOUR * HOURS_IN_DAY * DAYS_IN_YEAR)U

[!IMPORTANT]

魔鬼数字(Magic Number)指的是在源代码中直接出现的、没有明确解释或上下文说明的数值常量。它们缺乏清晰的命名,使得代码难以理解和维护。例如,直接在代码中写if (x == 42)而不解释42代表的具体意义,这里的42就可以被认为是魔鬼数字。最佳实践是通过命名常量(如 const int MAX_ATTEMPTS = 42;)来替换这些魔法数字,提高代码的可读性和可维护性。

五、其他

1. 结构体定义注意字节对齐

其实现在的内存空间可能真的不需要去考虑这个问题,主要是单片机开发,需要考虑内存空间不足的问题,合理排列结构中元素顺序可节省空间。

如下结构中的位域排列,将占较大空间(其大小为 6 Bytes),系统需要读取两次:

typedef struct EXAMPLE_STRU {
    unsigned char valid;
    unsigned Short person;
    unsigned char set_flg;
} EXAMPLE;

若改成如下形式,不仅可节省 2 字节空间(其大小为 4 Bytes),系统操作时仅需读取一次:

typedef struct EXAMPLE_STRU {
    unsigned char valid;
    unsigned char set_flg;
    unsigned Short person;
} EXAMPLE;

[!CAUTION]

对于 32 位系统,尽量使其 4 字节对齐;对于 64 位系统,尽量使其 8 字节对齐;特别是对于数组的结构体更是如此。

2. 定义变量时同时进行初始化

无数次的经验都在提醒,定义变量时必须同时进行初始化。特别是在 C/C++ 中引用未经赋值的指针,经常会引起系统崩溃。且不同编译系统对于局部变量值的初始化不同,不都为 0。如:VC 为 0xCC。

下面给出一些初始化变量的示例:

int index = 0;
boolean exit = FALSE;
char *name = NULL;
wchar address[10] = {0};
TPhonebookRecord record = {0};

3. 禁止浮点数直接进行等于或者不等于的比较操作

由于浮点型数据并没有准确的数值,所以不得进行相等(或不相等)比较。

对于如下语句,其判断语句的执行结果极有可能为 FALSE。

float value = 12.23;

if (value == 12.23) {
	...
}

如果需要对其进行等值判断,可采用类似如下方式:

float value = 12.23;

if (value >= 12.2299 && value <= 12.2301) {
	...
}

4. 超过两级的运算符表达式必须使用括号分离

为了保证运算符的准确性及阅读方便性,必须使用括号分离。

示例如下:

word = (high << 8) | low;

if ((var1 | var2) && (var1 & var3)) {
	...
}

5. 函数参数不能出现结构体

函数参数禁止出现结构体变量,必须是结构体指针的形式。函数的实参在运行时会进入栈空间的,如果直接传入结构体的话会无谓的消耗栈空间,引起程序不稳定,传入结构指针则没有这个问题。

6. 有参宏定义表达式都必须使用括号

用宏定义表达式时,所有参数都必须使用括号。每一级操作符对应的操作数、表达式都必须使用括号。整个表达式也必须使用括号。如下定义的宏都存在一定的风险:

#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)

正确的定义应为:

#define RECTANGLE_AREA(a, b) ((a) * (b))

7. 使用有参宏定义,禁止参数带自增自减运算符

如下用法可能导致错误:

#define SQUARE(a) ((a) * (a))
int var1 = 5;
int var2 = 0;
var2 = SQUARE(var1++); // 结果:var1 = 7,即执行了两次增1。

正确的用法是:

var2 = SQUARE(var1);
var1++; // 结果:var1 = 6,即只执行了一次增1。

好了,目前就总结这么多,编码规范是一个团队合作的基石,它确保了代码的可读性、可维护性和一致性。遵循本规范不仅能够提高代码质量,减少错误,还能够加速开发过程,提高团队协作效率。我们每个人都是团队的一员,遵守编码规范是对团队的承诺和贡献,让我们共同努力,为项目的成功贡献力量。

相关推荐

  1. 关于个人编码规范(C/C++)

    2024-05-14 05:44:11       31 阅读
  2. 关于经历

    2024-05-14 05:44:11       39 阅读
  3. 关于个人定位问题

    2024-05-14 05:44:11       37 阅读
  4. 2023年个人总结

    2024-05-14 05:44:11       50 阅读
  5. 个人关于背包问题·总结(三)

    2024-05-14 05:44:11       50 阅读
  6. 关于服务器被入侵个人日志(┬┬﹏┬┬)

    2024-05-14 05:44:11       43 阅读
  7. 个人关于ChatGPT用法及建议

    2024-05-14 05:44:11       31 阅读
  8. 个人关于Vue2组成见解

    2024-05-14 05:44:11       33 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-14 05:44:11       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-14 05:44:11       100 阅读
  3. 在Django里面运行非项目文件

    2024-05-14 05:44:11       82 阅读
  4. Python语言-面向对象

    2024-05-14 05:44:11       91 阅读

热门阅读

  1. CPU的的处理流程如何快速记忆

    2024-05-14 05:44:11       31 阅读
  2. 【Web后端】实现文件上传

    2024-05-14 05:44:11       28 阅读
  3. 【Redis7】10大数据类型之Set类型

    2024-05-14 05:44:11       34 阅读
  4. DORIS参数配置

    2024-05-14 05:44:11       34 阅读
  5. 一、Hive优化

    2024-05-14 05:44:11       34 阅读
  6. Docker安装oralce

    2024-05-14 05:44:11       30 阅读
  7. GitLab CI/CD的原理及应用详解(五)

    2024-05-14 05:44:11       34 阅读
  8. 构建树父类

    2024-05-14 05:44:11       33 阅读