华为C++编程规范摘录

3 分钟阅读

华为C++语言编程规范

规则

3.7.1 if/循环语句必须使用大括号

if (...)
{
    return false;   ///< Good
}

while(true)
{
    DoSomething();  ///< Good
}

4.4.3 不用的代码段直接删除,不要注释掉

被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入容易被忽略的缺陷。正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。使用版本控制来,记录代码。

5.2.3 禁止通过声明的方式引用外部函数接口、变量

只能通过包含头文件的方式使用其他模块或文件提供的接口。通过extern声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。同时这种隐式依赖,容易导致架构腐化。

5.2.4 禁止在extern "C"中包含头文件

extern "C" 中包含头文件,有可能导致extern "C"嵌套,部分编译器对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。

7.1.1 类的成员变量必须显式初始化

如果类有成员变量,没有定义构造函数,有没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。 如果类的成员变量具有默认构造函数,那么可以不需要显式初始化

7.1.3 如果不需要拷贝构造函数、赋值操作符,请明确禁止。

可以将拷贝构造函数或者赋值操作符设置为private,并且不实现。 C++11以后可以使用关键字delete, 来删除该成员函数。

7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止

7.1.6 禁止在构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用当前对象的虚函数,会导致未定义的行为。在C++中,一个基类一次只构造一个完整的对象。

7.2.1 基类的析构函数应该声明为virtual

虚析构函数

7.2.2 禁止虚函数使用缺省参数值

在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。

7.2.3 禁止重新定义继承而来的非虚函数

因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可获得正确的结果。

8.1.1 避免函数过长,函数不超过50行(非空非注释)

函数应该可以一屏显示完(50行以内), 只做一件事情,而且把它做好。 过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。 例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。 即使一个长函数现在工作的很好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的BUG。建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。

9.1.1 不允许使用宏来表示常量

宏是简单的文本替换,在预处理阶段时完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名;宏没有类型检查,不安全;宏没有作用域。

#define MAX_MSI_SDN_LEN 20  ///< Bad

const int MAX_MSI_SDN_LEN = 20; ///< Good
/// C++11以上的版本,可以使用`constexpr`
constexpr int MAX_MSI_SDN_LEN = 20;

9.1.2不允许使用魔鬼数字

魔鬼数字是看不懂、难以理解的数字。 例如type = 12,看不懂,但改成mouthsCount = yearsCount * 12, 就容易理解。 数字0有时候也难以理解。status = 0并不能表达是什么状态。 解决途径: 对于局部使用的数字,可以增加注释说明,对于多处使用的数字,必须定义const常量,并通过符号命名自注释。 禁止出现下列情况: 没有通过符号来解释数字含义,如const int ZERO = 0,符号命名限制了其取值,如const int XXX_TIMER_INTERVAL_300MS = 300, 直接使用XX_TIMER_INTERVAL_MS来表示该常量是定时器的时间间隔。

9.1.3 常量应该保证单一职责

一个常量只用来标识一个特定功能,即一个常量不能有多种用途。

好的例子。

const int A_MAX_MSI_SDN_LEN = 20;
const int B_MAX_MSI_SDN_LEN = 20;

namespace Namespace1
{
    const int MAX_MSI_SDN_LEN = 20;
}
namespace Namespace2
{
    const int MAX_MSI_SDN_LEN = 20;
}

9.1.4 禁止用memcpy_smemset_s初始化非POD对象

POD类型主要包括int, char, float, double, enumeration, void, pointer等原始类ing以及聚合类型,不能使用封装和面向对象特性(如用户定义的构造/赋值/析构函数、基类、虚函数) 由于非POD类型比如非聚合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。 即使对聚合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡使用memcpy_smemset_s

9.2.1 含有变量自增或自减运算的表达式中禁止再次引用该变量

含有变量自增或自减的表达式中,如果再引用该变量,其结果在C++标准中未明确定义。会产生未定义的结果。 注意,运算次序的问题不能使用括号来解决,因为之不是优先级的问题。

x = b[i] + i++; ///< Bad: b[i]运算跟i++, 先后顺序并不明确。

正确的写法是将自增或自减运算单独放一行:

x = b[i] + i;
i++;    ///< Good, 单独一行

函数参数

Func(i++, i)    ///< Bad

i++;    ///< Good, 自增运算单独放一行
Func(i, i)

9.3.1 如果确定要使用类型转换,请使用由C++提供的类型转换,而不是C风格的类型转换

C++提供的类型转换操作比C风格更具有针对性,更易读,也更安全,C++提供的转换有:

  1. dynamic_cast: 主要用于继承体系下行转换,dynamic_cast具有类型检查的功能,请做好基类和派生类的设计,避免使用dynamec_cast来进行转换。
  2. static_cast: 和C风格相似可做值的强制转换,或上行转换(把派生类的指着或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。
  3. reinterpret_cast: 用于转换不相关的类型。reinterpret_cast强制编译器将某个类型对象的内存重新解释成另一种类型,这是一种不安全的转换,建议尽可能少用reinterpret_cast
  4. const_cast: 用于移除对象的const属性,使对象变得可修改,这样会破坏数据的不变性,建议尽可能少用。

9.5.1 不要保存std::stringc_str()返回的指针

C++标准中并未规定c_str()返回的指针持久有效,所以不要保存。

9.6.1 对于指针和引用类型的形参,如果是不需要修改的,请使用const

不变的值更易于理解/跟踪和分析, 把const作为默认悬念,在编译时会对其进行检查,使代码更安全。

void PrintInt(const int& i);

9.6.2 对于不会修改成员变量的成员函数请使用const修饰

尽可能将成员函数声明为const。访问函数应该总是const。只要不修改数据成员的成员函数,都声明为const。对于虚函数,应当从设计意图上考虑继承链上的所有类是否需要在此虚函数中修改数据成员,而不是仅关注单个类的实现。

class Foo
{
public:
    int PrintValue() const ///< 修饰成员函数, 不会修改成员变量
    {
        return value_;
    }
private:
    int value_;
}

建议

2.4.1 避免滥用typedef或者#define对基本类型起别名

除有明确的必要性,否则不要用typedef#define对基本数据类型进行重定义。优先使用<cstdint>头文件中的基本。 | 有符号类型 | 无符号类型 | 描述 | | ———- | ———- | ——————————– | | int8_t | uint8_t | 宽度恰为8的有、无符号整数类型 | | int16_t | uint16_t | 宽度恰为16的有、无符号整数类型 | | int32_t | uint32_t | 宽度恰为32的有、无符号整数类型 | | int64_t | uint64_t | 宽度恰为64的有、无符号整数类型 | | intptr_t | uintptr_t | 足以保存指针的有、无符号整数类型 |

3.1.1 行宽不超过120个字符

3.9.1 表达式换行要保持换行的一致性,运算符放行末

if(IsCorrect() &&
   IsValid() &&
   IsSomething()) {}

3.14.1 合理安排空行,保持代码紧凑

减少不必要的空行,可以显示更多的代码,方便代码阅读。

  • 根据上下内容的相关程度,合理安排空行
  • 函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行
  • 不适用连续三个空行,或更多
  • 大括号内的代码块行首之前和行尾之后不要加空行,但namespace的大括号内不做要求

5.2.1 尽量避免使用前置声明

6.1.1 对于cpp文件中不需要导出的变量,常量或者函数,请使用匿名namespace封装或者用static修饰

更加推荐使用匿名namespace

  1. static在C++中已经赋予了太多的含义,静态函数成员变量,静态成员函数, 静态全局变量,静态函数局部变量,每一种都有特殊的处理。
  2. static只能保证变量,常量和函数的文件作用域,但是namespace还可以封装类型等。
  3. 统一namespace来处理C++的作用域,而不需要同时使用staticnamespace来管理
  4. static修饰的函数不能用来实例化模板,而匿名namespace可以

注意: 不要.h中使用匿名namespace或者static

6.2.1 优先使用命名空间来管理全局函数,如果和某个class有直接关系的,可以使用静态成员函数

非成员函数放在名字控件内可避免污染全局作用域,也不要用类+静态成员方法来简单管理全局函数。如果某个全局函数和某个类有紧密联系,那么可以作为类的静态成员函数。 如果你需要定义一些全局函数,给某个cpp文件使用,那么请使用匿名namespace来管理。

6.4.1 尽量避免使用全局变量,考虑使用单例模式

全局变量是可以修改和读取的,那么这样会导致业务代码和这个全局变量产生数据耦合。

8.3.1 函数参数使用引用代替指针

引用比指针更安全,因为它一定非空,且一定不会再指向其他目标;引用不需要检查非法的NULL指针。

8.3.2 使用强类型参数,避免使用void*

一般认为C/C++是强类型语言,既然我们使用的是强类型语言,就应该保持这样的风格。好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。

8.3.3 函数的参数个数不超过5个

函数的参数过多,会使得该函数易于受外部变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。 如果超过可以考虑

  • 看能否拆分函数
  • 看能否将相关参数合在一起,定义结构体

9.1.1 一组相关的整型变量应定义未枚举。

枚举比#defineconst int更安全。编译器会检查参数值是否位于枚举取值范围内,避免错误发生。

9.1.2 变量使用时才声明并初始化

变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并初始化,非常方便地避免了此类低级错误。 在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:

  • 程序难以理解与维护: 变量定义与使用分离
  • 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(0)来初始化。如果变量在被赋予有效值以前使用,还会导致错误。

遵循变量作用域最小化原则与就近声明原则,使得代码更容易阅读,方便了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。

std::string name;   ///< Bad
name = "zhangsan";

std::string name("zhangsan");   ///< Good

9.2.1 表达式的比较,应当遵循左侧倾向于变化,右侧倾向于不变的原则

当变量与常量进行比较时,如果常量放左边, 如if(MAX == v)不符合阅读习惯,而if(MAX > v)更难以理解,应当按人的正常阅读、表达习惯,将常量放右边。

if(value == MAX)
{
}
if(value < MAX)
{
}

也有特殊情况, 如: if(MIN < value && value < MAX)用来描述区间时,前半段是常量在左的。 不用担心将==误写成=, 因为if(value = MAX)会有编译告警,其他静态检查工具也会报错。让工具去解决笔误问题,代码要符合可读性第一。

9.2.2 使用括号明确操作符的优先级

使用括号明确操作符的优先级,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。

x = a + b + ; ///< 操作符相同,可以不加括号
x = 1 << (2 + 3);   ///< 操作符不同, 需要括号
x = (a == b) ? a : (a - b);   ///< 操作符不同, 需要括号

9.3.1 避免使用dynamic_cast

  1. dynamic_cast依赖于C++的RTTI, 让程序员在运行时识别C++类对象的类型
  2. dynamic_cast的出现一般说明我们的基类和派生类设计出现了问题,派生类破坏了基类的七月,不得不通过
  3. dynamic_cast转换到子类进行特殊处理,这个时候更希望来改善类的设计,而不是通过dynamic_cast来解决问题

9.3.2 避免使用reinterpret_cast

reinterpret_cast用于转换不相关类型。尝试用reinterpret_cast将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。

9.3.3 避免使用const_cast

cosnt_cast用于移除对象的constvolatile性质。 使用const_cast转换后的指针或者引用来修改const对象,行为是未定义的。

/// 不好的例子
const int i = 1024;
int* p = const_cast<int*>(&i);
*p = 2048;  ///< 未定义行为

9.4.1 使用RAII特性来帮助跟踪动态分配

RAII是”资源获取就是初始化”的简写(Resource Acquisition Is Initialization), 是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 RAII的一般做法是这样的: 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内之中有效,最后在对象析构的时候释放资源,这种做法有两大好处:

  • 我们不需要显式地释放资源
  • 对象所需的资源在其生命期内始终有效。这样,就不必检查资源有效性问题,可以简化逻辑、提高效率。

9.5.1 使用std::string代替char*

说明: 使用string代替char*有很多优势,比如:

  1. 不用考虑结尾的'\0'
  2. 可以直接使用+, =, ==等运算符以及其他字符串操作函数
  3. 不需要考虑内存分配操作,避免了显式的newdelete, 以及由此导致的错误

需要注意的是

9.5.2 使用新的标准头文件

使用C++的标准头文件时,请使用<cstdlib>这样的,而不是<stdlib.h>

9.6.1 初始化后不会再修改的成员变量定义为const

class Foo
{
public:
    Foo(int length): dataLength_(length){}
private:
    const int dataLength_;
}

现代C++(since C++11)

规则

10.1.1 在重写虚函数时请使用overridefinal关键字

overridefinal关键字都能保证函数是虚函数,且重写了基类的虚函数。如果子类函数与基类函数圆形不一致,则产生编译错误。final还保证u函数不会再被子类重写。 使用overridefinal还保证虚函数不会再被子类重写。 使用overridefinal关键字后,如果修改了基类函数原型,但忘记修改子类重写的虚函数,在编译期就可以发现,也可以避免有多个子类时,重写虚函数的修改遗漏。

10.1.2 使用delete关键字删除函数

相比于将类成员函数声明为private但不实现, delete关键字更明确,且适用范围更广。

class Foo
{
private:
    Foo(const Foo&);    ///< 只看头文件不知道拷贝构造函数是否被删除
};

class Foo
{
public:
    /// 明确删除拷贝赋值符
    Foo& operator=(const Foo&) = delete;
};

另外,delete关键字还支持删除非成员函数。

template<typename T>
void Process(T value)

template<>
void Process<void> = delete;

10.1.3 使用nullptr, 而不是NULL0

#define NULL ((void*)0)

char* str = NULL;   ///< 错误: void* 不能自动转为 char*

void(C::pmf)() = &C::Func;
if(pmf == NULL) {} ///< 错误: void* 不能自动转换为指向成员函数的指针

如果把NULL定义为0, 或者在需要空指针的地方直接使用0。这样引入了另外的问题

auto result = Find(id);
if(result == 0)
{
    /// 无法判断返回的是整数还是空指针
}

重载也会出现重载的问题。

void F(int);
void F(int*`);
F(NULL); ///< 调用F(int), 而不是F(int*)

另外sizeof(NULL) == sizeof(void*)并不总是成立的。

nullptr的又是不仅仅是在字面上代表了空指针,使代码清晰,而且它不再是一个整数类型。 nullptrstd::nullptr_t类型, 而std::nullptr_t可以隐式的转换为所有的原始指针类型,这使得nullptr可以表现成指向任意类型的空指针。

void F(int);
void F(int*`);
F(nullptr); ///< 调用F(int*)

auto result = Find(id);
if(result == nullptr)
{
    /// 正确的判断
}

建议

10.1.1 合理使用auto

  • auto可以避免编写冗长、重复的类型名,也可以保证定义变量时初始化
  • auto类型推导规则复杂,需要仔细理解
  • 如果能够使代码更清晰,继续使用明确的类型,且旨在局部变量使用auto
/// 避免冗长的类型名
std::map<std::string, std::pair<std::vector<int>, int>> iter = m.find(val);
auto iter = m.find(val);

/// 保证初始化
int x;  ///< 编译正确,没有初始化
auto x; ///< 编译失败,没有初始化

更新时间: