浅谈C++物理设计:代码规范

1033 查看

头文件保护宏

每一个头文件都应该具有独一无二的保护宏,并保持命名规则的一致性,其中命名规则包括两种风格:

  • 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语句的顺序,但这样的代码实现是及其脆弱的。