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

C++ 头文件使用规范建议相关推荐

  1. linux 内核头文件 linux kernel header

    1.内核头文件的发展历史         在 Linux 2.2/2.4 的纯真年代,内核头文件一直保持着 Unix 世界的"KISS"传统,只需将内核源码树中的头文件直接复制到 ...

  2. C/C++头文件规整

    C/C++头文件规整 规范如下 规范如下 C/C++的程序通常有两个部分,一个是.h头文件,是通常.c实现文件头上引入的外部引入(include)的程序接口.自从David Parnas提出信息掩蔽原 ...

  3. YCM代码补全插件找不到c++头文件

    开发环境: Ubuntu22.04 LTS x86_64架构Intel CPU YCM(YouCompleteMe)是一款用于vim编辑器的强大代码补全插件. YCM提供许多强大功能: 代码补全: 语 ...

  4. C语言编程规范 — 头文件、函数

    0 编码规范说明 0.1 前言 为了提高产品或项目代码质量,软件开发人员编写出简洁.可靠.可维护.可测试.高效.可移植的代码,树立良好的编程规范是非常有必要的,良好的编程规范能提高编程效率,规避很多编 ...

  5. 【Android NDK 开发】Visual Studio 2019 使用 CMake 开发 JNI 动态库 ( 动态库编译配置 | JNI 头文件导入 | JNI 方法命名规范 )

    文章目录 I . JNI 与 NDK 区别 II . Visual Studio 编译动态库 III. 配置 导入 jni.h 头文件 IV . IntelliJ IDEA Community Edi ...

  6. C/C++ 为什么#ifndef 头文件 要用大写加下划线?(这就是一种约定俗成的规范)

    如图,防止头文件重复包含,为什么要用大写加下划线?为什么不是直接写peripherals.h,我已经在问了 解释: 首先,test.h不是合法的C标识名:合法的C标识名只能由字母.数字和下划线组成,且 ...

  7. 使用头文件的原因和规范

    原因 通过头文件来调用库功能.在很多场合,源代码不便(或不准)向用户公布,只 要向用户提供头文件和二进制的库即可.用户只需要按照头文件中的接口声明来调用库 功能,而不必关心接口怎么实现的.编译器会从库 ...

  8. C++:头文件规范/模板

    某个摄像头模块的Demo头文件,学习一下头文件规范 例1. #ifndef __HCAMERA_H__ #define __HCAMERA_H__#define HCAMERA_API extern ...

  9. 五十、头文件和源文件的区别和规范(extern)

    头文件和源文件的区别和规范(extern) 文章目录 头文件和源文件的区别和规范(extern) (一)源文件为什么要包含自己的头文件 (二)为什么定义一定要放在.c文件里 (三)头文件和源文件的规范 ...

最新文章

  1. ELASTIC SEARCH 性能调优
  2. 推荐一位玩自动化的 Python 爱好者
  3. keras入门之手写字识别python代码
  4. ASP.Net上传文件带进度条、显示剩余时间!
  5. 直接拿来用!超实用的Java数组技巧攻略
  6. 测试思路系列:《谷歌的软件测试之道》读书笔记
  7. 中国能源消费结构(2003-2019年)
  8. Linux系统终于可以安装正版的QQ了
  9. U盘病毒专杀工具(usbcleaner)(绿色版)
  10. 南京大学计算机考研2022,2022考研策略解读:南京大学计算机专业考研建议与备考指导...
  11. 西门子PLC S7-1200程序实例,版本博图V15及以上 西门子1200与安川机器人TCP/IP通讯,包含机器人GSD文件
  12. C/C++中使用PlaySound()播放音乐
  13. 宏碁台式计算机u盘启动,宏基台式机bios设置u盘启动方法
  14. 计算机语言中beta是什么意思,Tea语言迎来1.0第一个Beta版本
  15. python编程游戏-9个Python编程小游戏,有趣又好玩,简直太棒了
  16. RANSAC的实现与应用
  17. 首先下载安装data.table包_首次揭秘“超级签”与企业包行业内幕!
  18. PhalApi+Gearman,接口MQ异步队列任务的完整开发教程
  19. 物联网的七大通信协议
  20. 开关电源雷击浪涌整改_一种防雷击浪涌的开关电源电路设计

热门文章

  1. SQLServer之修改存储过程
  2. Spring Aop pointcut expression表达式解析
  3. Java API —— ArrayList类 Vector类 LinkList类
  4. android四大组件小整
  5. Session重点整理
  6. window启动过程讲解--PPT截图[张银奎]
  7. 开源信息系统开发平台之OpenExpressApp框架 1.1 春节飞虎版发布
  8. 相聚“金巴蜀”、付诸笑谈中
  9. [导入]七大千年数学难题
  10. linux串口 demo 解析原创 代码没有完全测试