腾讯安全实验室 c/c++安全指南
google c++ 代码规范
什么是 Alpha、Beta、RC、Release版, 以及版本号规范
文章目录
- 前言
- 1.文件结构
- 2.参数命名
- 3.函数命名
- 4.代码编写原则
- 4.1 【内存申请原则】谁创建谁释放,平级作用域内释放,并且如果是指针类型,记得释放时置为空指针,避免指向野指针
- 4.2 【结构体初始化原则】在创建结构体前/构造函数中初始化值
- 4.3 【函数实现原则】引用作为形参时,不能改变形参内容
- 4.4 【函数实现原则】函数传入的形参需要校验安全性和可用性!!!
- 4.5 【日志打印写法】使用format格式,诸如printf("%s),格式化风格,符号替换会操作比较单一,不会影响性能
- 4.6 【常用数值,避免硬编码】一些灵活多变的数值,比如定时刷新时间,最大值最小值这些尽量用可灵活设置的方法,比如宏定义,静态变量存储,避免写死在代码里(硬编码)
- 4.7 【if && 条件判断】大概率false结果优先放在前面,避免多个条件同时执行判断。减少性能开销。
- 4.8 【函数调用】优先使用带timeout参数的函数,减少cpu性能开销
- 4.9 【调用】优先使用notify/wait 而非轮询的方式,减少性能开销
- 4.10 【try/catch】尽量不使用try/catch 容易隐藏崩溃问题
- 4.11 【内存开辟原则】优先使用栈内存,其次堆内存,使用堆内存注意释放,最好用RAII/智能指针
- 4.12 【高频内存原则】高频内存,建议使用固定内存(成员变量、静态变量),注意线程安全问题
- 4.15 【函数设计】尽可能不要返回指针类型,避免接口返回的指针被滥用,内存存在被修改的风险。
- 4.16 【函数设计】回调函数尽可能不作耗时操作,比如堆栈大内存分配,耗时计算。
- 4.17 【字符串操作】字符串和字符数组的分配和赋值,最好考虑到大小长度的限制,避免操作过程中\0被移除,导致内存访问冲突。
- 4.18 【类型比较】不同类型的数值进行比较时,需要转为统一类型在进行比较,避免数据精度丢失。
- 4.19 【文件操作】文件打开后,句柄尽可能快速关闭,避免文件在一直在增长,读取文件会无法读取到文件尾,从而导致cpu升高,同时也避免句柄长时间不关闭,系统资源消耗以及句柄泄漏的风险
- 4.20 【内存操作】尽可能使用memcpy_s等安全函数进行内存操作
- 4.21【函数设计】遵从"快速返回原则",尽可能快的返回结果,尽可能减少if else嵌套,尽可能单分支判断,减少理解成本
- 4.22 【类的设计】通过基类指针析构子对象,没有虚析构函数是不符合规范的代码
- 5.代码格式
- 6.常见通俗规定
- 7.常见埋坑操作
- 8. 常用技巧
- 9. 英文缩写规则
- 10.性能建议
- 11. 代码规范化工具
- 12.编程规范思想
- 总结
前言
微软匈牙利代码风格和谷歌代码风格
微软风格=匈牙利风格=函数大驼峰+变量小驼峰+标明类型 例如InitTool(), mIsOpen, bOpen等
谷歌偏c风格 下划线很多 谷歌风格linux使用比较多,例如init_tool(),m_is_open, is_open等
1.文件结构
1.1 版权和版本的声明
文件头 全使用英文 减少中文编码乱码问题
/****************************************************************
* Copyright (c) 2021 by XXX Company
*
* file description:Export To Be DLL Implement
* version: 1.0.0
* author: Shiver
* time: 2021 / 05 / 31
****************************************************************/
1.2 导出头文件命名 XXXAPI.h
项目名+API+.h 作为导出dll 的对应头文件 统一格式方便查找
1.3 常见项目结构
-ProjectName
--src
--include
--lib
--bin
--res
src 目录 存源码包括(*.cpp、*.h,*.cc,*.hpp)
include 目录 存引入第三方库的 *.h文件
lib 目录 存引入第三方库的*.lib、*.a 、*.so (静态库和动态链接库)
bin 目录 存引入第三方库的*.dll (动态库)
res 目录 存资源文件 例如*.jpg、*.icon
1.4 头文件 防止重复引入
#ifndef XXX_H
#def XXX_H
// 主要内容
#endif
注:尽量不要使用#pragram once,不利于跨平台 貌似只有windows有这个宏
1.5 导出接口命名
格式:
返回类型+“ ”+公司简称+“_”+模块简称+“_"+函数名(动+宾结构)+“(”+形参列表+“)”;
例如:
int ByteDance_Decoder_CreateDecoder(int a, int b);
当然导出的接口 自然有一些导出宏之类的 WINAPI 、extern "C"等 记得定义为宏 统一添加
1.6 类和虚类命名
类(Class)以C开头,例如CFactory
虚类(接口Interface)以I开头,例如IFactory
常用包含功能含义的类命名可以参考:
优秀开源软件的类,都是怎么命名的
2.参数命名
2.1 局部变量
int: nXXX iXXX
指针:pXXX
char数组:szXXX
wchar_t数组:wszXXX
2.2 静态变量
s:static 静态变量
int:s_iXXX
2.3 全局变量
g:glogbal 全局变量
int: g_iXXX;
2.4 成员变量
m:member 成员
int:m_iXXX
变量类型 | 该类型变量的前缀 | 类型意义 |
---|---|---|
局部变量 | 无 | 例如 int类型 nXXX |
静态变量 | s_ | 例如 int类型 g_iXXX |
全局变量 | g_ | 例如 int类型 g_iXXX |
成员变量 | m_ | 例如 int类型 m_iXXX |
2.5 常见变量命名
表一 基本类型及前缀规范
类型名定义 | 该类型变量的前缀 | 类型意义 |
---|---|---|
int8_t | n | 有符号的8bit整数 |
uint8_t | n | 无符号的8bit整数 |
int16_t | n | 有符号的16bit整数 |
uint16_t | n | 无符号的16bit整数 |
int32_t | n | 有符号的32bit整数 |
uint32_t | n | 无符号的32bit整数 |
int64_t | n | 有符号的64bit整数 |
uint64_t | n | 无符号的64bit整数 |
void | v | void类型 |
float32 | f | 32位浮点数 |
float64 | ||
double | f | 64位浮点数 |
FILE* | pf | 文件指针 |
char* | ||
char[] | sz | 字符指针,或者字符数组 |
char | ch | 字符 |
wchar | wch | 宽字符 |
wchar* | ||
wchar[] | wsz | 宽字节数组 |
string | str | 字符串 |
wstring | wstr | 宽字符串 |
vector | vec | vector容器 |
list | list | list容器 |
map | map | map容器 |
HANDLE | h | 句柄 |
thread | h | 线程句柄 |
DWORD | dw | 双字 |
WORD | w | 单字 |
bool/BOOL | b | 布尔变量 |
函数指针 | pfunc | 函数指针 |
迭代器 | itr | 容器迭代器 |
表2 变量属性公共前缀规则
前缀 | 前缀意义 |
---|---|
arr | 数组类型变量 |
p | 指针类型变量 |
t | 自定义类型变量(自定义类型包括结构、联合、枚举型) |
ap | 存放指针的数组 |
at | 存放自定义类型变量的数组 |
pa | 指向数组的指针 |
pp | 指向指针的指针变量(两维指针) |
pt | 指向自定义类型的指针变量 |
2.6 形参和实参命名
形参 最好能标识方向以及参数安全,避免被修改
输入形参:
void test(int nInParam);
void test(const type *nInParam); //有时传只能是避免结构体拷贝过于消耗性能,所以此时要注意参数安全,避免函数调用完后参数被修改。
输出形参:
void test(int &nOutParam);
或者用windows自带的宏表示参数__IN__
,__OUT__
3.函数命名
3.1 大驼峰命名
void ThisIsFunction();
3.2 动词+名词 结构
全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。
例如:
InitModule()
SetHeight()
StartAction()
StopAction()
DestoryModule()
类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
class Shoes {
void Wear(); // wear the shoes
void TakeOff(); // take off the shoes
void Drop(); // drop the shoes
}
3.3 单个对象用 is has, 多个对象的集合用复数形态
比如
deleteFile,moveFiles,getChildren,单数复数表达的含义是不同的,file表示操作单个文件,files表示可以接受文件集合,children表示子节点不止一个。
3.4 用时态语态可以传达更多信息,增强可读性。
对于正在发生的事情用进行时,doing;
对于一个做完的事情用过去式 did done。比如当状态发生变化的时候,
对于正在改变但是还没彻底变完,xxxChanging,
对于已经变了 xxxChanged 表示真的变了。
4.代码编写原则
4.1 【内存申请原则】谁创建谁释放,平级作用域内释放,并且如果是指针类型,记得释放时置为空指针,避免指向野指针
{
int *pNum = new int();
{
int *pNum2 = new int();
delete pNum2;
pNum2 = nullptr;
}
delete pNum;
pNum2 = nullptr;
}
4.2 【结构体初始化原则】在创建结构体前/构造函数中初始化值
struct test
{
int a;
int b;
test(){
a = 0;
b = 0;
}
}
struct test testa;
memset(testa,0,sizeof(testa));
声明初始化以及构造初始化 有利于避免野指针。
4.3 【函数实现原则】引用作为形参时,不能改变形参内容
禁止用法:
void func(std::string &name){
name = "!23"
}
正确用法:
void func(const std::string &name){
}
根据意图 对形参类型 进行控制:
- 如果是只读,那么传不可变引用,对应const T&。
- 如果是改变,就传可变引用,对应T&。
- 如果是转移所有权,那么就需要T&&。
4.4 【函数实现原则】函数传入的形参需要校验安全性和可用性!!!
验证参数可用性和安全性非常重要,
处理对应的错误情况的处理方式:抛异常,还是返回false -> 都是决定软件的体验是否流畅的因素
比如:
直接抛异常并且捕获异常,会避免崩溃。
但是直接崩溃生成dump有利于排查问题。
禁止用法:
void func(Point *p){
use(p); // wrong
}
正确用法1:
void func(Point *p){
if (p == nullptr)
{
return
}
use(p); // right
}
正确用法2:
使用断言Assert 在开发阶段发现问题 也是极好的
void func(Point *p){
Assert(p);
use(p); // right
}
4.5 【日志打印写法】使用format格式,诸如printf("%s),格式化风格,符号替换会操作比较单一,不会影响性能
4.6 【常用数值,避免硬编码】一些灵活多变的数值,比如定时刷新时间,最大值最小值这些尽量用可灵活设置的方法,比如宏定义,静态变量存储,避免写死在代码里(硬编码)
4.7 【if && 条件判断】大概率false结果优先放在前面,避免多个条件同时执行判断。减少性能开销。
正确范例:
bool always_false = false;
bool sometimes_false = false;
if (always_false && sometimes_false) {
// dosomething
}
4.8 【函数调用】优先使用带timeout参数的函数,减少cpu性能开销
4.9 【调用】优先使用notify/wait 而非轮询的方式,减少性能开销
4.10 【try/catch】尽量不使用try/catch 容易隐藏崩溃问题
函数 使用 noexcept 可以用来阻止异常的传播和扩散
4.11 【内存开辟原则】优先使用栈内存,其次堆内存,使用堆内存注意释放,最好用RAII/智能指针
4.12 【高频内存原则】高频内存,建议使用固定内存(成员变量、静态变量),注意线程安全问题
4.15 【函数设计】尽可能不要返回指针类型,避免接口返回的指针被滥用,内存存在被修改的风险。
4.16 【函数设计】回调函数尽可能不作耗时操作,比如堆栈大内存分配,耗时计算。
4.17 【字符串操作】字符串和字符数组的分配和赋值,最好考虑到大小长度的限制,避免操作过程中\0被移除,导致内存访问冲突。
正确写法:
string.assign(char_arr, char_size);
错误写法:
string.assign(char_arr);
4.18 【类型比较】不同类型的数值进行比较时,需要转为统一类型在进行比较,避免数据精度丢失。
例如:unsigned char 和 char 范围是不同的,char a = 255, 对应unsigned char a = -1;
所以他俩进行比较时 需要注意是否会精度丢失
以及同一数值的比较错误
。
4.19 【文件操作】文件打开后,句柄尽可能快速关闭,避免文件在一直在增长,读取文件会无法读取到文件尾,从而导致cpu升高,同时也避免句柄长时间不关闭,系统资源消耗以及句柄泄漏的风险
4.20 【内存操作】尽可能使用memcpy_s等安全函数进行内存操作
4.21【函数设计】遵从"快速返回原则",尽可能快的返回结果,尽可能减少if else嵌套,尽可能单分支判断,减少理解成本
快速返回原则:这种模式的指导思想是:在函数的开头,优先处理所有的异常、边缘或无效情况,并立即返回。 这样,函数的主体部分就可以专注于处理“快乐路径(Happy Path)”,即最核心、最正常的逻辑。
错误范例:
void main(int a, int b){
if (a == b){
...
return;
} else{
if(a > 0){
...
} else{
...
}
}
}
正确范例:
void main(int a, int b){
// 在函数的开头,优先处理所有的异常、边缘或无效情况,并立即返回 //
if (a > 0){
...
return;
}
if (a == b){
...
return;
}
if ( a<0){
...
return;
}
}
- 扁平化结构:嵌套层级大大减少,代码从一棵“树”变成了一条“直线”。
- 降低认知负荷:我们可以像读一个清单一样阅读代码。检查完一个条件,如果不满足就结束。我们不需要在脑海中保留“如果…那么…否则…”的上下文。
- 关注点分离:函数的开头部分是“防御性”的,负责过滤掉无效输入。函数的主体部分则可以专注于核心业务逻辑,代码意图更加清晰。
- 可维护性增强:当我们需要增加一个新的条件(比如“黑名单用户”),我们只需在函数的开头添加一个新的卫语句即可,而无需在复杂的 else 嵌套中找到正确的位置。
4.22 【类的设计】通过基类指针析构子对象,没有虚析构函数是不符合规范的代码
可能会导致崩溃
正确案例:
#include <vector>
struct base {
virtual ~base() {}
};
struct derived : base {
std::vector<int>::iterator m_iter;
derived( std::vector<int>::iterator iter ) : m_iter( iter ) {}
~derived() {}
};
int main() {
std::vector<int> vec{1,2,3,4};
base * pb = new derived( vect.begin() );
delete pb;
}
错误案例:
#include <vector>
struct base {
// virtual ~base() {}
};
struct derived : base {
std::vector<int>::iterator m_iter;
derived( std::vector<int>::iterator iter ) : m_iter( iter ) {}
~derived() {}
};
int main() {
std::vector<int> vec{1,2,3,4};
base * pb = new derived( vect.begin() );
delete pb;
}
5.代码格式
5.1 一行只申明一个变量,且声明后马上初始化
错误范例:
int a,b,c;
正确范例:
int a = 0;
int b = 0;
int c = 0;
char *buffer = new char[260];
memset(buffer,0,260);
5.2 代码块之间用 多使用换行 分隔
错误范例:
int a;
int b;
int c;
if(b==2){
...
}
正确范例:
int a;
int b;
int c;
if(b == 2)
{
...
}
5.3 操作符与变量之间 多使用 空格 分隔
错误范例:
if(b==0){...}
a>b?0:1;
正确范例:
if(b == 0){
...
}
a > b ? 0 : 1;
5.4 判断语句和循环语句 内容必须用{}包裹
错误范例:
if(a==0) return a;
while(1) return 0;
正确范例:
if(a==0)
{
...
}
while(1)
{
return 0;
}
5.5 {}包裹的内容必须有层次感 通过一层一个tab
错误范例:
if(a==0)
{
while(1)
{
return 0;
}
return a;
}
正确范例:
if(a == 0) //1 层
{
while(1) // 2层
{
return 0;//3 层
}
return a;//2层
}
5.6 编译器尽量设置 1个tab=4个空格
5.7 操作符和变量之间 必须用空格分割开来
a = b
test(a, b)
6.常见通俗规定
6.1 函数形参顺序 先dst,后src (可选)
void *memcpy(void *destin, void *source, unsigned n);
6.2 长循环放内部,短循环放外部
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
例如示例1的效率比示例2的高。
示例1
for(int i = 0; i < 10;i++){
printf("for out");
for(int j = 0; j < 100;j++){
printf("for in");
}
}
示例2
for(int i = 0; i < 100;i++){
printf("for out");
for(int j = 0; j < 10;j++){
printf("for in");
}
}
6.3 使用const static 代替 #define (可选)
宏定义可减少一定内存开销,转移到代码段。
const static 会产生内存开销。
6.4 执行命令时 文件路径 记得用转义字符 \" \"圈起来路径
6.5 路径中的斜杠 默认使用反斜杠 /
windows \\等于/
linux /就是/
6.6 四个空格等于一个tab 但一般用4个空格 一个tab某平台等于8个空格
7.常见埋坑操作
7.1 文件路径
1.文件路径字符串请用""圈起来,避免有空格情况以及部分系统无法识别相关命令
2.文件路径字符串如果有中文请使用Unicode编码
7.2 注释中包含中文时,请在注释结尾使用英文句号,不然编译可能存在问题。
错误:
//测试
正确:
//测试.
或者
//
//测试
//
原因:使用中文一般文件就默认识别为多字节,多字节文件,会识别代码也为多字节,会读取多个字节为一个字符,导致代码编译出错 或可能导致 代码没有编译进二进制文件中。
常见解决方法:
- 将文件改为UTF-8 BOM (BOM的来历)
7.3 byte转字符串时,注意追加多一个字符串单位作为填充结尾符\0
int len = 10;
unsigned char * sz_str = malloc((len+1) * sizeof(char));
memset(sz_str, 0, (len+1) * sizeof(char));
//unsigned char * wsz_str = malloc((len+1) * sizeof(wchar_t));
//memset(sz_str, 0, (len+1) * sizeof(wchar_t));
否则可能会因为越界内存导致访问冲突,(当然这是随机的,根据越界内存决定,越界内存可以访问则不会报错,但最好统一处理,毕竟内存是不可预知的)
7.4 字节流 取地址+偏移量时,注意偏移量单位
const unsigned char * data = {0x1,0x2}
char* byte1 = data + 1; // 默认偏移单位为 const unsigned char
int* int1 = (int*)data + 1; // 此时默认单位为int
8. 常用技巧
8.1 vs设置tab为4个空格
跨平台代码统一使用空格作为格式化代码的符号
(但是像vim编辑器 默认是以tab \t作为分隔符号)
8.2 文本换行采用unix LF 而非 windows CRLF
不然可能会有调试代码时 出现错行的问题,即源代码和debug断点位置没匹配上(断点错位,断点无效等)的问题
跨平台代码统一使用LF
调试代码错位时,可以试试更改文件编码为(utf-8带签名)
UTF-8有带签名和无签名
在VS中编码保存时,文件编码的选择里的UTF-8有带签名和无签名2种,那么到底有什么区别呢?
带签名即文件头含BOM信息,不带则没有。
带了的好处是,只要支持多编码的编辑器都能正确识别出文件编码。
不带的话,就不一定了,就有可能识别错别。
总之呢,这个BOM信息也就那么三字节,没必要省这么一点点空间,要用UFT-8就选带签名,这样就不会错了。
8.3 建议编写插拔式代码 而非侵入式代码
原代码:
int a = 0;
int b = 0;
if (a + b > 0 ){
return;
}
错误:
```cpp
int a = 0;
int b = 0;
// 修改了原有代码,并且有可能会影响原有代码逻辑
if (a > 0 && b > 0 && a + b > 0 ){
return;
}
正确:
int a = 0;
int b = 0;
//
// 可随时注释 避免侵入原有代码
//
if (a > 0 && b > 0 ){
return;
}
if (a + b > 0 ){
return;
}
能不改原代码就不改原代码;
能用代理模式 尽量用代理模式;
8.4 建议对于字符串的操作,首先将字符串转为统一格式,比如大小写一致
规避几种情况:
- 操作系统不同 获取值有可能不同,比如windows系统 进程名可能是大写也可能是小写导致你直接等号比较会可能比较错误,而且linux文件是大写文件和小写文件是认为是不同的文件。
8.5 对于本地/远程调试,建议经常性检查符号文件是否最新,即使本应匹配,但由于时间隔了几小时,也可能导致无法二进制文件和符号文件不匹配导致无法调试
9. 英文缩写规则
单词缩写
- 采用去元音(a,e,i,o,u),比如 service=srvc, test=tst
- 采用仅开头,比如address= addr
短语缩写
- 采用一个单词全写或仅开头(不超过5个字母),后面单词缩写:比如compmgmt.msc、taskschd.msc、eventvwr.msc
推荐生成缩写的网站:https://www.allacronyms.com/
10.性能建议
【数组】 低纬度能用数组尽量用数组,vector容器少用
原因:vector高纬度,不同维度的内存是不连续的,内存越是连续,命中缓存的概率越高,访问速度越快。
【条件判断】参考4.17 不容易命中的条件放在前面,容易命中的条件放在后面
【循环】参考4.20 尽量小循环在外,大循环在内,避免切换循环导致开销。
【内存】参考4.12 高频内存尽量用固定内存 避免频繁malloc导致开销
注意固定内存,对于高频率访问时,记得对有概率获取值失败的内存进行初始化,否则内存可能是旧值导致逻辑无法正确运行
【编译优化】打开O2级优化
11. 代码规范化工具
clang-format插件
cpplint google风格检查工具
12.编程规范思想
进攻性编程,防御性编程以及契约性编程
组合 > 继承
总结
好的编程风格有利于快速理解代码,好的编码习惯也是编码的基本。