C++ 头文件使用规范建议

1.背景

拥有良好的编程规范和风格是一名程序员成熟的标志。规范的编码可以减少代码冗余,降低出错率,便于代码管理和代码交流等,事实上,其作用远不止这些,我们要牢记编码规范在心中啊。

C++ 具有很多强大的语言特性,但这种强大不可避免地导致它的复杂,而复杂性会使得代码更容易出现bug、难以阅诺和维护。因此,如何进行简洁高效地编码来规避 C++ 的复杂性,使得代码在有效使用C++ 语言特性的同时仍易于管理,变得异常重要。

使代码易于管理的方法之一是增强代码一致性,让别人可以读懂你的代码是很重要的,保持统一编程风格意味着可以轻松根据“模式匹配”规则推断各种符号的含义。创建通用的、习惯性用语和模式可以使代码更加容易理解,在某些情况下改变一下编程风格可能会是好的选择,但我们还是应该遵循一致性原则,尽量不要这样去做。

C++ 是一门包含大量高级特性的巨型语言,某些情况下,我们可以放弃使用某些特性简化代码,避免可能导致的各种问题。

2.头文件使用相关规范

头文件是 C/C++ 项目中编译单元源文件的组成部分,是大型项目不可或缺的一部分,我们必须面对它。使用头文件时,我们应该遵守如下几个规范:
(1)防止头文件在源文件中多次被包含;
(2)尽量减少头文件的相互依赖;
(3)合理的头文件包含顺序以及名称。

2.1 防止头文件在源文件中多次被包含

2.1.2 条件宏

所有头文件都应该使用条件宏#ifndef #define #endif防止头文件被多重包含(Multiple Inclusion),命名格式为:<PROJECT>_<PATH>_<FILE>_H

为保证唯一性,头文件的命名应基于其所在项目源代码树的全路径。例如,项目 foo 中的头文件 foo/src/bar/baz.h 按如下方式保护:

#ifndef FOO_BAR_BAZ_H
#define FOO_BAR_BAZ_H
... 
#endif // FOO_BAR_BAZ_H

2.1.2 #pragma once

#pragma once是编译指导指令,放在头文件的最开始位置,可以达到和条件宏一样的效果,即当头文件被重复包含时只编译一次,避免重定义错误。与条件宏的有两点区别:
(1)条件宏可以作用于代码段,#pragma once只能作用于文件;
(2)#pragma once不是 C++ 标准的一部分,一般由编译器提供保证,使用时可能受到编译器的实现限制,不过不用担心,因为目前主流的 C++ 编译器都是支持的,比如 VC++ 和 GNU C++。

用法示例如下:

// test.h
#pragma once
...

// test.cpp
#include "test.h"      // line 1
#include "test.h"      // line 2

2.1.3 _Pragma(“once”)

从 C++11 开始,标准定义了与预处理指令 #pragma 功能相同的操作符 _Pragma。_Pragma 操作符的格式如下:

_Pragma(字符串字面量)

使用_Pragma("once")可以达到与#pragma once相同的效果,与预处理指令 #pragma 不同的是,_Pragma 可以用在宏中,#pragma 指令不能用于宏定义中,因为编译器会将指令中的 # 解释为字符串化运算符 #。例如:

#define CONCAT(x) PRAGMA(concat on x)
#define PRAGMA(x) _Pragma(#x)

那么CONCAT(dablelv)最终等价于_Pragma("concat on dablelv"),因此,C++11 的_Pragma在使用上更加灵活。

注意,Mircrosoft 的 VC++ 并未遵守 C++11 标准实现_Pragma,而是实现了__pragma以达到相同的目的,GNU C++ 实现了_Pragma

2.2 尽量减少头文件的依赖

相信不少程序猿们都受过头文件的依赖之苦。当从另一个项目中的头文件移植到自己的项目中时,若想通过编译,发现这个头文件需要另外一个头文件,另外一个又需要其它的头文件…,让人头痛啊。这就是头文件依赖带来的不便。

2.2.1 前置声明(Forward Declarations)

使用前置声明可尽量减少头文件中#include的数量,也就是能依赖声明的就不要要依赖定义。

使用前置声明可以显著减少需要包含的头文件数量。举例说明:头文件中用到类File,但不需要访问File的声明,则头文件中叧需前置声明class File;无需#include "file/base/file.h"

在头文件如何做到使用类Foo而无需访问类的定义?
(1)将数据成员类型声明为Foo*Foo&
(2)参数、返回值类型为 Foo 的函数只提供声明,不定义实现;
(3)静态数据成员类型可以被声明为 Foo,因为静态数据成员的定义在类定义之外。

2.2.2 柴郡猫技术(Cheshire Cat Idiom)

减少头文件依赖不只有前置申明这一个方法,可以使用柴郡猫技术,又称为 PIMPL(Pointer to IMPLementation)、Opaque Pointer 等,是一种在类中只定义接口,而将私有数据成员封装在另一个实现类中的惯用法。该方法主要是为了隐藏类的数据以及减少头文件依赖,提高编译速度。

柴郡猫(Cheshire cat)是英国作家刘易斯·卡罗尔(Lewis Carroll,1832-1898)创作的童话《爱丽丝漫游奇境记(Alice’s Adventure in Wonderland)》中的虚构角色,形象是一只咧着嘴笑的猫,拥有能凭空出现或消失的能力,甚至在它消失以后,它的笑容还挂在半空中。 柴郡猫的能力和 PIMPL 的功能相一致,即柴郡猫(数据成员)消失了,给我们留下了笑容(指向数据成员的指针变量)。

比如使用 PIMPL 可以帮助我们节省程序编译的时间。考虑下面这个类:

// A.h
#include "BigClass.h"
#include "VeryBigClass.h"

class A
{
//...
private:
    BigClass big;
    VeryBigClass veryBig;
};

我们知道 C++ 中有头文件(.h)和实现文件(.cpp),一旦头文件发生变化,不管多小的变化,所有引用它的文件都必须重新编译。对于一个很大的项目,C++ 一次编译可能就会耗费大量的时间,如果代码需要频繁改动,那真的是不能忍受。这里如果我们把 BigClass big; 和 VeryBigClass veryBig;利用 PIMPL 封装到一个实现类中,就可以减少 A.h 的编译依赖,起到减少编译时间的效果:

// A.h
class A
{
public:
    // 与原来相同的接口
   
private:
    struct AImp;
    AImp* pAImp;
};

除了上述两种方法,使用接口类也可以达到降低头文件依赖的目的,可只依赖接口头文件,因为接口类是只有纯虚函数的抽象类,没有数据成员 [ 3 ] ^{[3]} [3]

2.2.3 不可避免的头文件依赖

如果你的类是 Foo 的子类,则必须为之包含头文件。

有时,使用指针成员(pointer members,如果智能指针更好)替代对象成员(object members)的确更有意义。然而,返样的做法会降低代码可读性及执行效率。如果仅仅为了少包含头文件,还是不要这样替代。

2.3 合理的头文件包含顺序以及名称

2.3.1 包含头文件的名称

项目内头文件应该按照项目源代码目彔树结构排列,尽量避免使用 UNIX 文件路径.(当前目录)和…(父目录)。例如google-awesome-project/src/base/logging.h应像这样被包含:

#include "base/logging.h"

这里在编译的时候,需要使用编译器的编译选项-I指定项目相对于编译器工作目录的相对路径或者绝对路径。即上面在使用 g++ 编译的时候使用 -Isrc 来指明相对于编译器工作目录的搜索目录。

还有一个需知就是:使用 include 包含头文件,使用相对路径时,相对的目录是编译器的工作目录。

关于搜索头文件的路径,编译器搜索顺序如下:
(1) include 自定义头文件,如#include “headfile.h” 搜索顺序为:
(a)先搜索源文件所在目录;
(b)然后搜索编译选项 -I 指定的目录;
(c)再搜索 g++ 的环境变量CPLUS_INCLUDE_PATH(gcc使用的是C_INCLUDE_PATH);
(d)最后搜索 g++ 的内定目录。

/usr/include
/usr/local/include
/usr/lib/gcc/x86_64-redhat-linux/4.1.1/include

各目录存在相同文件时,先找到哪个使用哪个。

(2)include 系统头文件或标准库头文件,如#include <headfile.h>
(a)先搜索编译选项 -I 指定的目录;
(b)然后搜索 g++ 的环境变量CPLUS_INCLUDE_PATH
(c)最后搜索 g++ 的内定目录。

/usr/include
/usr/local/include
/usr/lib/gcc/x86_64-redhat-linux/4.1.1/include

与上面的相同,各目录存在相同文件时,先找到哪个使用哪个。这里要注意,#include<>方式不会搜索源文件所在目录!

这里要说下 include 的内定目录,它不是由 PATH 环境变量指定的,而是由 g++ 的配置 prefix 指定的。prefix 的查看可以通过如下方式:

dablelv@TENCENT$ g++ -v
Using built-in specs.
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-libgcj-multifile --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --enable-plugin --with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre --with-cpu=generic --host=x86_64-redhat-linux
Thread model: posix
gcc version 4.1.2 20080704 (Red Hat 4.1.2-46)

在安装 g++ 时,指定了 prefix,那么内定搜索目录就是:

prefix/include
prefix/local/include
prefix/lib/gcc/--host/--version/include

编译时可以通过 -nostdinc++ 选项屏蔽对内定目录的搜索。

2.3.2 包含头文件的顺序

项目中,当一个源文件包含多个不同类型的头文件,比如操作系统头文件、C 标准库、C++ 标准库、其它库的头文件、自己工程的头文件,对不同类型头文件包含时采用什么样的顺序,Google C++ 编程风格对头文件的包含顺序作出如下指示。

(1)为了加强可读性和避免隐含依赖,应使用下面的顺序:C 标准库、C++ 标准库、其它库的头文件、你自己工程的头文件。不过这里最先包含的是首选的头文件,即例如 a.cpp 文件中应该优先包含 a.h。首选的头文件是为了减少隐藏依赖,同时确保头文件和实现文件是匹配的。具体的例子是:假如你有一个cc文件(Linux 平台的 cpp 文件后缀为 cc)是 google-awesome-project/src/foo/internal/fooserver.cc,那么它所包含的头文件的顺序如下:

#include "foo/public/fooserver.h"  //首选的头文件放在第一位

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

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

隐含依赖又叫作隐藏依赖,即一个头文件依赖其它头文件。例如:

//A.h
struct BS bs;
...

//B.h
struct BS
{
....
};

//在A.c中,这样会报错
#include A.h 
#include B.h

//先包含B.h就可以
#include B.h
#include A.h

这样就叫作“隐藏依赖”。如果先包含 A.h 就可以发现隐藏依赖,所以各种规范都要求自身的头文件放在第一个,就能发现隐藏依赖。解决办法就是在 A.h 中包含 B.h,而不是在 A.c 中再包含。

(2)在包含头文件时应该加上头文件所在工程的文件夹名,即假如你有这样一个工程 base,里面有一个logging.h,那么外部包含这个头文件应该这样写:#include "base/logging.h",而不是#include "logging.h"

我们看到《Google C++ 编程风格指南》倡导原则背后隐藏的目的是:
(1) 为了减少隐藏依赖,源文件应该先包含其对应的头文件(本文称之为首选项);
(2)除了首选项外,遵循从一般到特殊的原则,头文件包含顺序依次为:OS SDK 、C 标准库、C++ 标准库、其它库的头文件、自己工程的头文件;
(3)之所以要将头文件所在工程目录列出,作用同名字空间一样,为了解决头文件重名问题。

假如 dir/foo.cpp 是项目中的源文件,其对应的头文件是 include/foo.h 的功能,foo.cpp 中包含头文件的次序如下:

dir2/foo2.h(优先位置)
系统调用头文件
C 库头文件 
C++ 库头文件 
其他库头文件
本项目内头文件

这种排序方式可有效减少隐藏依赖。当相同目录下需要包含多个头文件时,按照名称字母序来包含是不错的选择。

3.小结

(1)避免多重包含是编程时最基本的要求;
(2)前置声明是为了降低编译依赖,防止修改一个头文件引发蝴蝶效应;
(3)包含头文件的名称使用.和…虽然方便却易混乱,使用比较完整的项目路径看上去很清晰、有条理;
(4)包含头文件的次序除了美观之外,最重要的是可以减少隐藏依赖,将源文件对应的头文件放在最前面,可以及早发现隐藏依赖。


参考文献

[1] Google C++ 编程风格指南中文版
[2] C++接口类
[3] linux系统编译C++程序时头文件和库文件搜索路径
[4] C++头文件的包含顺序研究
[5] C/C++中的隐藏依赖
[6] 深入理解C++11[M].C2.1.3_Pragma 操作符.P22-22

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
华为C++语言编程规范包括以下几个方面的内容: 1. 程序块的分界符应独占一行并位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case语句中的程序都要采用如上的缩进方式。\[1\] 2. 文件的包含顺序应按照以下顺序:当前.cpp文件直接关联的文件、C库文件、C++库文件、其他项目的文件、本项目中的其他文件文件应向稳定的方向包含。\[2\] 3. 对于转换运算符和单参数构造函数,建议使用explicit关键字来明确指定其作用。这样可以避免隐式类型转换带来的潜在问题。\[3\] 以上是华为C++语言编程规范的一些主要内容。遵循这些规范可以提高代码的可读性和可维护性。 #### 引用[.reference_title] - *1* [华为C/C++编码规范](https://blog.csdn.net/weixin_67336587/article/details/130940891)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [C++编程规范(参考Google、华为)](https://blog.csdn.net/qq_39632811/article/details/124098935)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值