头文件保护宏
每一个头文件都应该具有独一无二的保护宏,并保持命名规则的一致性,其中命名规则包括两种风格:
INCL_<PROJECT>_<MODULE>_<FILE>_H
全局唯一的随机序列码
第一种命名规则问题在于:当文件名重命名或移动目录时,需要同步修改头文件保护宏;推荐使用IDE
随机自动地生成头文件保护宏,其更加快捷、简单、安全、有效。
反例:
// thread/Runnable.h
// 因名称太短,存在名字冲突的可能性
#ifndef RUNNABLE_H
#define RUNNABLE_H
#include "base/Role.h"
DEFINE_ROLE(Runnable)
{
ABSTRACT(void run());
};
#endif
正例:
// cppunit/AutoRegisterSuite.h
#ifndef INCL_CPPUNIT_AUTO_REGISTER_SUITE_H
#define INCL_CPPUNIT_AUTO_REGISTER_SUITE_H
#include "base/Role.h"
struct TestSuite;
DEFINE_ROLE(AtuoRegisterSuite)
{
ABSTRACT(void add(TestSuite&));
};
#endif
正例:
// cppunit/AutoRegisterSuite.h
// IDE自动生成
#ifndef INCL_ADCM_LLL_3465_DCPOE_ACLDDDE_479_YTEY_H
#define INCL_ADCM_LLL_3465_DCPOE_ACLDDDE_479_YTEY_H
#include "base/Role.h"
struct TestSuite;
DEFINE_ROLE(AtuoRegisterSuite)
{
ABSTRACT(void add(TestSuite&));
};
#endif
下划线与驼峰
路径名一律使用小写、下划线或中划线风格的名称;文件名应该与程序主要实体名称相同,可以使用驼峰命名,也可以使用小写、下划线或中划线分割的名字;实现文件的名字必须和头文件保持一致;包含头文件时,必须保持路径名、文件名大小写敏感。
反例:
// 路径名htmlParser使用了驼峰命名风格
#include "htmlParser/core/Attribute.h"
正例:
// 正确的头文件包含
#include "html-parser/core/Attribute.h"
#include "yaml_parser.h"
最后,值得注意的是,团队内必须保持一致的命名风格。
大小写敏感
包含头文件时,必须保持路径名、文件名大小写敏感。因为在\ascii{Windows},其大小写不敏感,编译时检查失效,代码失去了可移植性,所以在包含头文件时必须保持文件名的大小写敏感。
假如存在两个物理文件名分别为SynchronizedObject.h, yaml_parser.h
的两个文件。
反例:
// 路径名、文件名大小写与真实物理路径、物理文件名称不符
#include "CppUnit/Core/SynchronizedObject.h"
#include "YAML_Parser.h"
正例:
#include "cppunit/core/SynchronizedObject.h"
#include "yaml_parser.h"
最后,值得注意的是,团队内必须保持一致的命名风格。
分隔符
包含头文件时,路径分隔符一律使用Unix
风格,拒绝使用Windows
风格;即采用/
而不是使用\
分割路径。
反例:
// 使用了Windows风格的路径分割符
#include "cppunit\core\SynchronizedObject.h"
正例:
// 使用了Unix风格的路径分割符
#include "cppunit/core/SynchronizedObject.h"
extern "C"
使用extern "C"
时,不要包括include
语句。
反例:
//oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#ifdef __cplusplus
extern "C" {
#endif
// 错误地将include放在了extern "C"中
#include "oss_common.h"
void* oss_alloc(size_t);
void oss_free(void*);
#ifdef __cplusplus
}
#endif
#endif
正例:
//oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#include "oss_common.h"
#ifdef __cplusplus
extern "C" {
#endif
void* oss_alloc(size_t);
void oss_free(void*);
#ifdef __cplusplus
}
#endif
#endif
兼容性
当以C
提供实现时,头文件中必须使用extern "C"
声明,以便支持C++
的扩展。
反例:
// oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#include "oss_common.h"
void* oss_alloc(size_t);
void oss_free(void*);
#endif
正例:
// oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#include "oss_common.h"
#ifdef __cplusplus
extern "C" {
#endif
void* oss_alloc(size_t);
void oss_free(void*);
#ifdef __cplusplus
}
#endif
#endif
上帝头文件
拒绝创建巨型头文件,将所有实体声明都放到头文件中,而仅仅将外部依赖的实体声明放到头文件中。
信息隐藏
实现文件也是一种信息隐藏的惯用技术,如果一些程序的实体不对外所依赖,则放在自己的实现文件中,一则可降低依赖关系,二则实现更好的信息隐藏。
对于上帝头文件,其很多声明和定义本来是不应该放到头文件,而应该放会实现文件以便实现更好地信息隐藏。
编译时依赖
巨型头文件必然造成了巨大的编译时依赖,不仅仅带来巨大的编译时开销,更重要的是这样的设计将太多的实现细节暴露给用户,导致后续版本兼容性的问题,阻碍了头文件进一步演进、修改、扩展的可能性,从而失去了软件的可扩展性。
include
顺序依赖
不要认为提供一个大而全的头文件会给你的用户带来方便,用户因此而更加困扰。对于一个巨大的头文件,其依赖关系很难一眼看清楚,其自满足性很难得到保证,用户在包含此头文件时,还要关心头文件之间的依赖关系,甚至关心include语句的顺序,但这样的代码实现是及其脆弱的。