欢迎各位关注我的公众号:程序员JC。我记得很早之前写过一篇关于编码规范的文章。但是之前那篇可能不系统全面,所以这次打算系统全面的总结一下。为什么我会再次写编码规范的文章呢?俗话说的好:工欲善其事,必先利其器。想要写出优雅的程序代码,必须要有好的规范和约束。
为什么要规范?代码规范的意义到底在哪?规范是多数程序员共同遵守的一种编码风格,因为我们实际开发过程中都是以团队合作形式开发,而且几乎每个程序员都有自己的个人风格和习惯,那么这时要想高效率开发,规范约束则很有必要了。其实做任何事都应该有规范规则,上至高层管理,下至基层实施,各线统一有条不紊的进行,才能事半功倍。如果没有任何规范约束,在多人合作中往往就表现的越混乱(即熵增),事倍功半。
好吧,不扯远了。还是说代码规范吧。如果你写的代码是一直是自己编写和维护,以后也不打算公布出去,那么这时按照自己风格编写,我个人觉得没有任何问题,因为规范规则往往是运用在多人合作上的。有的同僚可能会说到,是自己不太优秀,优秀的程序员写的代码都不是很规范?首先不可否认的是确实有那种才智过人的程序员开源出来的代码不是很规范,但依然会有很多人会花大力气去阅读。就是因为这种程序员太过优秀,写的东西都是干货硬货,大多数人写不出来。但并不是说明他们的代码很规范,而且我们绝大多数人没有他们那种能力,所以更得好好遵守规范约束。
程序软件是一门工程学,并非说会coding就行了。此前本人在网上找各种代码资料,很多花了钱才弄到的代码和资料不仅不能运行,而且极不规范。所以很多公布出去的代码没有实际意义,只是为了赚取积分什么的。所以希望和各位一起遵守规范约束,写出优雅可运行的代码。做那coding世界中的一股清流,哈哈。
没有任何一种风格适用所有程序员,所以得综合取舍。下面结合我自身以及《高质量c++》一书对编码注意事项进行说明,针对C++代码提供API文档生成工具和方法(见最后),其他编程语言也可以做参考
注释规范
1、文件头部注释
应该有概括说明和修改记录信息。样例见下:
/**
* @file xxx.h/xxx.cpp
* @brief 文件概括...
* @copyright Copyright (c) 2021 JackChen
* @par 修改记录:
* <table>
* <tr><th>修改日期 <th>修改人 <th>描述
* <tr><td>2021-05-01 <td>JackChen <td>创建第一版
* </table>
*/
意义:便于快速知道该文件的功能有大概哪些,便于后期由于修改导致bug出现,用于版本和责任人追溯
2、函数注释
应该有功能说明和输入输出参数说明信息。样例见下:
/**
* @brief 这是个是函数功能描述
* @param[in] param1 参数1
* @param[out] param2 参数2
* @return 函数执行结果
* - -1 失败
* - 0 成功
*/
int func1(const string& param1, int ¶m2);
/**
* @brief 这是个是函数功能描述
* @return 无返回值
*/
void Func2(void);
意义:便于后期维护的人修改,尤其是输入输出参数说明以及返回值说明
3、复合对象注释
应该有复合对象概括说明信息。样例见下:
/**
* @brief
* 这是枚举说明
*/
enum RuleEnum
{
};
/**
* @brief
* 这是结构体说明
*/
struct RuleStruct
{
};
/**
* @brief
* 这是联合结构说明
*/
union RuleUnion
{
};
/**
* @brief
* 多例/单例/无实例。这是对象说明
*/
class Rule
{
};
意义:能快速知道该类的大概功能,便于后期架构维护和调整
4、变量及其他注释
针对xxx.h 文件中的变量及其他说明,注释符一律用///,样例见下:
///宏定义说明
#define MACRO_VALUE 0
///常量说明
const int MACRO_VALUE2 = 0;
enum RuleEnum
{
///枚举成员
RULE_ENUM_1
};
struct RuleStruct
{
///结构成员1
int iValue;
};
union RuleUnion
{
///联合成员1
char* ptValue;
};
class Rule
{
private:
///实例对象
static Rule* ptVlaue;
///成员变量1
bool m_bValue;
};
针对xxx.cpp 文件中的变量及其他说明,注释符一律用//,样例见下:
int main(void)
{
//这是变量说明
int var = 0;
//逻辑说明
}
至此,关于注释相关已说明完。这里需要补充的是关于注释符号的说明,见下:
【xxx.h】文件中
///
- 用于变量和其他注释
/**
*/
- 用于复合对象注释
【xxx.cpp】文件中
//
- 用于变量和其他注释
/*
*/
- 用于代码片段注释
之所以注释符号有所区分,这是因为xxx.h文件是暴露在外,xxx.cpp文件是对内的。所以为了方便使用三方工具(微软官方 doxygen)生成API文档(*.chm),请严格按照此注释风格类型。上述有部分关于doxygen的标签没有给出,具体请参见规范代码中的注释,里面囊括了所有关于doxygen的标签(见最后)。API文档及类型截图见下:
命名规范
1、变量命名
在说具体命名前,先给出各关键字以及其他结构(STL)的的前缀:
- int - i/n
- short - s
- unsigned - u
- bool - b
- char - ch
- long - l
- long long - ll
- struct - sct
- enum - eum
- union - uon
- vector - v
- list - list
- map - map
- deque - de
- set - set
- queue - que
- stack - stk
- string - str
- 指针 - pt
- 函数指针/多维指针/对象指针 - lp
- 数组 - arr
- 对象 - obj
//上述前缀哪些需要结合,以及结合顺序
- unsined int/char/long/short - ui/uch/ul/us
全局变量:g_ | 类型前缀(小写) | 有意义的名字(单词开头大写)
int g_iAge;
unsigned char g_uchSex;
vector<int> g_vFriend;
成员变量:m_ | 类型前缀(小写) | 有意义的名字(单词开头大写)
//class
string m_strName;
int m_arrGrade[6];
void* m_lpDataPro;
char* m_ptData;
//struct 类型前缀(小写) | 有意义的名字(单词开头大写)
string strName;
int arrGrade[6];
void* lpDataPro;
char* ptData;
//union 类型前缀(小写) | 有意义的名字(单词开头大写)
string strName;
int arrGrade[6];
void* lpDataPro;
char* ptData;
//enum 有意义的名字(全员大写,单词间下划线分开)
TEST_ENUM_1
TEST_ENUM_2
临时变量&形参变量:有意义的名字(开头小写,后面遇见单词大写)
int height;
char dogSex;
静态变量:类型前缀(小写) | 有意义的名字(单词开头大写)
static int iHeight;
static void* ptInstance;
2、函数命名
普通函数:有意义的名字(单词开头小写)
void getSize(void);
void getWidth(void);
//普通函数是在某个特定cpp文件中,该函数仅仅在该cpp文件中调用。不跨cpp访问
全局函数:有意义的名字(单词开头大写)
void GetSize(void);
void GetWidth(void);
成员函数
//private 有意义的名字(开头小写,后面遇见单词大写)
void getSize(void);
void getWidth(void);
//protected 有意义的名字(开头小写,后面遇见单词大写)
void getSize(void);
void getWidth(void);
//public 有意义的名字(开头大写,后面遇见单词大写)
void GetSize(void);
void GetWidth(void);
//之所以有权限命名之分,是为了增加可读性(根据命名方可知道对外对内接口)
3、复合对象命名
对象(class):有意义的名字(单词开头大写)
class Node {};
结构体(struct):有意义的名字(单词开头大写) | Struct
struct NodeStruct {};
联合(union):有意义的名字(单词开头大写) | Union
union NodeUnion {};
枚举(enum):有意义的名字(单词开头大写) | Enum
enum NodeEnum {};
4、其他命名
常量&宏定义:有意义的名字(全部大写,单词下划线_分开)
#define MAX_BUFFER_SIZE 1024
const int SCREEN_WIDTH = 1920;
命名空间:有意义的名字(全部小写,单词下划线_分开)
namespace thread_sdk {}
语法逻辑及技巧规范
1、空格、空行
在实际逻辑代码中适当的加入空格和空行,能增加代码的可读性和优雅性,方便后期维护。下面给出一段这是我很早之前的代码,和现在的对比截图:
可以看出右侧的代码比左侧的代码更优雅,更具有可读性。下面给出空格、换行的部分具体应用位置(赋值表达式,代码域等)分别给出错误和正确示范:
//错误示范
#include<stdio.h>
int main(void)
{
int a=5;
char*p=nullptr;
if(a==5){}
for(int i=0;i<10;i++){}
}
//正确示范
#include <stdio.h>
int main(void)
{
int a = 5;
char* p = nullptr;
if (a == 5) {
}
for (int i = 0; i < 10; i++) {
}
}
2、if判断
主要包含两种 if 语句判断的时候两种情形,减少非语法错误的隐藏bug以及代码严谨。这两种情形分别是:左侧值判断和零值判断。
左侧值判断:
//普通的右侧值相等判断(如果少些一个等号,编译器并不会报语法错,很容易出现隐藏bug)
if (a == 5) {}
//左侧值判断(如果少些一个等号,编译器会包语法错误,减少隐藏bug)
if (5 == a) {}
零值判断:
//非严谨的判零写法,int、bool、float
int a = 1;
if (!a) {}
bool a = true;
if (!a) {}
float a = 1.f;
if (!a) {}
//严谨的写法(只有bool型变量才可直接用于判断,因为bool型的取值只有true/false)
int a = 1;
if (0 == a) {}
bool a = true;
if (!a) {}
float a = 1.f;
if (a >= -0.000001f && a <= 0.000001f) {}
//这里需要特别说明的是,Windows系统API提供了一个BOOL型变量TRUE/FALSE,实质是int型,并非正真意义上的bool型
3、赋值语句&while恒真
这里主要说明赋值语句的变量初始化,和while循环的恒真的严谨。
赋值语句:
//非严谨写法(声明变量不赋值,多个变量并行声明)
int a, b, c;
int d = 0, e = 0;
//严谨写法(声明变量赋初值,每个赋值语句各占一行)
int a = 0;
int b = 0;
int c = 0;
int d = 0;
int e = 0;
while恒真
//非严谨写法
while (1) {}
//严谨写法(这里需要说明的是恒真尽量用bool值,但是恒假的可以用0)
while (true) {}
4、宏{}do while(0)的使用
{}do while(0)的用法一般用于宏块代码中,是为了遵循我们在引用宏块代码的书写规范,并且减少非语法错误的隐藏bug
//非严谨写法
#define TEST_CODE int a = 0; a = 5;
if (true)
TEST_CODE;
//上述调用方式,会出现语法错误;并且编译器会警告多一个分号
//
//严谨写法
#define TEST_CODE { int a = 0; a = 5; } while (0)
if (true)
TEST_CODE;
5、指针及地址
都说C/C++语言的难点在于指针。我个人觉的最好用的也是指针,指针变量、虚拟地址和物理地址之间的想换转换妙用。下面给出示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//十六进制字符串转整型
long HexStrToInt(char s[])
{
long ret = 0;
int len = strlen(s);
for (int i = 0; i < len; i++) {
int temp = 0;
if (s[i] >= 'A' && s[i] <= 'F') {
temp = s[i] - 'A' + 10;
}
else if (s[i] >= 'a' && s[i] <= 'f') {
temp = s[i] - 'a' + 10;
}
else {
temp = s[i] - '0';
}
ret = ret * 16 + temp;
}
return ret;
}
int main(void)
{
//普通变量
int val1 = 10;
char szTemp1[12] = { 0 };
sprintf(szTemp1, "%p", &val1);
printf("val1 变量地址:%s\n", szTemp1);
printf("val1 的值:%d\n", val1);
//字符串转整形地址
long addr1 = HexStrToInt(szTemp1);
int* val1Bak = (int*)(addr1);
*val1Bak = 12;
printf("val1 的值:%d\n", val1);
//
//指针变量
int* val2 = new int;
*val2 = 100;
char szTemp2[12] = { 0 };
//sprintf(szTemp2, "%p", &val2);
//printf("val2 变量地址:%s\n", szTemp2);
sprintf(szTemp2, "%p", val2);
printf("val2 变量地址:%s\n", szTemp2);
printf("val2 的值:%d\n", *val2);
//字符串转整形地址
long addr2 = HexStrToInt(szTemp2);
int* val2Bak = (int*)(addr2);
*val2Bak = 102;
printf("val2 的值:%d\n", *val2);
delete val2;
return 0;
}
上述代码片段执行结果见下:
可见字符串转为指针后依然可以使用,但是这里需要说明的是。这里的地址并非实际物理地址,而是虚拟地址。也就是说在该变量的声明周期内,直接通过地址可以进行修改。那么应用范围?我这里提供一种应用范围,就是如果想自己实现一种脚本语言并通过C++语言调用,这里可能会用到这种情况(就是通过函数名映射函数地址指针)。
6、五构一析,三五法则
五构一析是指普通构造,拷贝构造,赋值拷贝、移动拷贝构造、移动赋值拷贝以及析构函数。
三五法则:
三法则(C++98) - 析构函数、拷贝构造、赋值拷贝
五法则(C++11) - 析构函数、拷贝构造、赋值拷贝、移动构造、移动赋值
//五构一析
class A
{
public:
A(void); //普通构造
A(const A &obj); //拷贝构造
A& operator =(const A &obj); //赋值拷贝
A(const A &&obj); //移动拷贝构造(可选)
A& operator =(const A &&obj); //移动赋值拷贝(可选)
~A(void); //析构
}
//这里需要对权限进行说明如下:
//- 如果对象无实例,建议将构造析构函数声明为私有权限,并将构造析构显示声明为 =delete
//- 如果对象单例,建议将构造析构函数声明为私有权限,如果构造析构无需重新定义,则将其声明为 =default
//- 如果对象多例,构造析构必须声明为公有权限,如果构造析构无需重新定义,则将其声明为 =default
//- 如果对象有派生类关系,则将析构声明为virtual
7、宏与模板
首先想说明的是宏块代码和模板是作用于预处理阶段,二者其实本质都是拷贝代码。从可读性及书写上来说模板比宏块代码更优雅一些,而且至C++11起,类模板支持了变参。在实际编写代码过程中,在某些情况下如何合理的应用宏和模板能大大节省书写时间以及提高整体优雅性,但调试可能不是很友好(所以凡事都有好有坏嘛)。下面给出宏和模板示例:
//宏块代码
#define MAX_FUNC(type) type max(type val1, type val2) \
{ \
return val1 > val2 ? val1 : val2; \
}
MAX_FUNC(int)
MAX_FUNC(long)
MAX_FUNC(float)
MAX_FUNC(double)
//模板代码
template<class T>
T max(T val1, val2)
{
return val1 > val2 ? val1 : val2;
}
///所以从上述示例看,模板比宏块代码更优雅
8、头文件引用
为了避免减少头文件相互引用的情况,请尽量将引用头文件放在cpp中引用。还有就是引用头文件的顺序和防止头文件定义。
//头文件引用顺序(自定义文件 > .h库文件 > 库文件 > C头文件)
#include "header.h"
#include <stdio.h>
#include <string>
extern "C" {
#include <libavformat/avformat.h>
}
#pragam comment(lib, "winmm.lib")
using namespace std;
///
//防止头文件重新定义
//方式1
#ifndef __HEADER_DEFINE_H__
#define __HEADER_DEFINE_H__
#endif
//方式2
#pragam once
//#ifndef 和 #pragam once 是两种防止头文件重新定义方式,但是前者是由C语言,后者是由编辑器提供
其他
- 开始coding前将ide的tab键设置为替换4个空格,这是为了方便跨各种ide之间的移植。
- 编译之前将编译器设置为最严格的模式,一个程序编译完成,不应出现任何警告。如果是由于三方sdk源码出现的警告则忽略
- 请使用严格的c++语法编写,就拿vs自带的编译器来说。并非完全严格标准的c++语法,这就是为什么有的低版本工程迁移至高版本工程之后编译不过以及换成其他编译器(如:gcc/g++)也编译不过。这就是语法不严格导致。
- 代码文件和pe文件字符集请保持一致,避免由于中文乱码而引发隐藏bug。代码文件编码和pe文件编码是两回事。
- 32位和64位程序平台问题,特别是指针和部分API的返回值问题可能导致溢出问题
- Debug和Release版本问题,在多线程中可能会出现运行结果不一致的问题。比如C++的关键字volatile的用法,Debug含有调试信息,Release是优化后的程序,但优化其中的一个原则是保持输入输出一致,也就是说Debug切换为Release版本之后,在输入输出之间的代码顺序可能会发生变化。这就是在多线程中可能出现问题的原因之一
- 有效的使用const修饰参数和成员函数,能提高程序的严谨和优雅性
上述就是根据我个人的经验以及《高质量C++》一书的感悟。任何事做到极致就是艺术,哈哈。好的代码是极具艺术感。最后,还是附上前人说过的一句话,望和各位同僚共勉之。
能长期写出稳定的高质量程序的程序员称为编程老手;能长期写出稳定的高难度,高质量程序的程序员称为编程高手
高质量C++:https://pan.baidu.com/s/1DfX1D3E_5D212o25VTYJhQ 提取码:nzmx
规范:https://pan.baidu.com/s/1DqGIPK9DTeVMZNiGB0kJOA 提取码:lpjt
API文档工具:https://pan.baidu.com/s/1GmsAgIJF6WKNpew4BGaD1w 提取码:t6s9