我最近发布了一个问题,询问什么行为将构成C++的禅宗.我收到了很好的答案,但我无法理解一个建议:
你如何确保你的头文件是自给自足的?
任何其他与C/C++中头文件的设计和实现相关的建议或最佳实践都将受到欢迎.
编辑:我发现这个问题涉及我的"最佳实践"部分.
sho*_*osh 33
一个自给自足的头文件是一个不依赖于它包含在哪里以正常工作的上下文的文件.如果你确定在使用之前#include或定义/声明了所有内容,那么你就拥有了一个自给自足的标题.非自足标头
的示例可能是这样的:
----- MyClass.h -----
class MyClass
{
MyClass(std::string s);
};
Run Code Online (Sandbox Code Playgroud)
-
---- MyClass.cpp -----
#include <string>
#include "MyClass.h"
MyClass::MyClass(std::string s)
{}
Run Code Online (Sandbox Code Playgroud)
在这个例子中,MyClass.hstd::string首先使用#including.为此,在MyClass.cpp中你需要把它放在#include <string>前面#include "MyClass.h".
如果MyClass的用户没有这样做,他将收到一个错误,即不包括std :: string.
保持标题自足可以经常被忽略.例如,你有一个巨大的MyClass标题,你添加了另一个使用std :: string的小方法.在这个类目前使用的所有地方,在MyClass.h之前已经是#included.然后有一天你将#include MyClass.h作为第一个标题,突然你在一个你甚至没有碰过的文件中都有这些新错误(MyClass.h)
小心地保持你的标题是自给自足有助于避免这个问题.
Jon*_*ler 21
美国宇航局戈达德太空飞行中心(GSFC)发布了解决这一问题的C和C++编程标准.
假设您有一个包含源文件perverse.c及其标头的模块perverse.h.
有一种非常简单的方法可以确保标头是自包含的.在源文件中,您包含的第一个标头是模块的标头.如果它像这样编译,标题是自包含的(自给自足).如果没有,请修复标题,直到它(可靠地1)自包含.
#ifndef PERVERSE_H_INCLUDED
#define PERVERSE_H_INCLUDED
#include <stddef.h>
extern size_t perverse(const unsigned char *bytes, size_t nbytes);
#endif /* PERVERSE_H_INCLUDED */
Run Code Online (Sandbox Code Playgroud)
几乎所有标题都应该受到保护以防止多重包含.(标准<assert.h>标题是规则的明确例外 - 因此是"几乎"限定符.)
#include "perverse.h"
#include <stdio.h> // defines size_t too
size_t perverse(const unsigned char *bytes, size_t nbytes)
{
...etc...
}
Run Code Online (Sandbox Code Playgroud)
请注意,即使在项目标题之前包含标准标题通常也是一个好主意,但在这种情况下,模块header(perverse.h)在所有其他标题之前的可测试性至关重要.我允许的唯一例外是在模块头之前包含一个配置头; 然而,即使这是可疑的.如果模块头需要使用(或者可能只是'可以使用')来自配置头的信息,它应该包括配置头本身,而不是依赖于使用它的源文件.
脚注1:Steve Jessop对Shoosh答案的评论是为什么我将括号内的(可靠)评论放入我的'修复'评论中.他说:
另一个使这一点困难的因素是C++中的"系统头可以包含其他头"规则.如果
<iostream>包含<string>,则很难发现您忘记包含<string>在某些标题中[不]使用<iostream>[或<string>].单独编译头文件没有错误:它在这个版本的编译器上是自给自足的,但在另一个编译器上它可能不起作用.
预编译头的GCC规则允许每个翻译单元只有一个这样的头,它必须出现在任何C令牌之前.
只有在满足以下条件时才能使用预编译的头文件:
- 在特定编译中只能使用一个预编译头.
- 一旦看到第一个C令牌,就无法使用预编译的头.您可以在预编译头之前使用预处理程序指令; 你甚至可以在另一个标题内包含一个预编译的标题,只要在#include之前没有C标记.
- [...]
- 在包含预编译头之前定义的任何宏必须以与生成预编译头的方式相同的方式定义,或者不得影响预编译头,这通常意味着它们根本不会出现在预编译头中.
对于第一个近似值,这些约束意味着预编译头必须是文件中的第一个.第二个近似值表明,如果'config.h'只包含#define语句,它可能出现在预编译头之前,但更可能的是(a)config.h中的定义会影响其余的代码,并且(b)预编译头部无论如何都需要包含config.h.
我工作的项目没有设置为使用预编译的标题,GCC定义的约束加上由不同的编码人员进行了20多年的强化维护和扩展所引起的无政府状态意味着添加它们将非常困难.
鉴于GSFC指南和GCC预编译头之间的要求不同(并假设预编译头正在使用中),我认为我将使用单独的机制确保头的自包含和幂等性.我已经为我正在处理的主要项目执行此操作 - 重新组织标题以满足GSFC指南并不是一个简单的选项 - 我使用的脚本chkhdr如下所示.您甚至可以将此作为头文件目录中的"构建"步骤 - 确保所有头文件都是自包含的"编译"规则.
我使用此chkhdr脚本来检查标头是否是自包含的.虽然shebang说'Korn shell',但实际上代码对Bash甚至原始(System V-ish)Bourne Shell来说都是可以的.
#!/bin/ksh
#
# @(#)$Id: chkhdr.sh,v 1.2 2010/04/24 16:52:59 jleffler Exp $
#
# Check whether a header can be compiled standalone
tmp=chkhdr-$$
trap 'rm -f $tmp.?; exit 1' 0 1 2 3 13 15
cat >$tmp.c <<EOF
#include HEADER /* Check self-containment */
#include HEADER /* Check idempotency */
int main(void){return 0;}
EOF
options=
for file in "$@"
do
case "$file" in
(-*) options="$options $file";;
(*) echo "$file:"
gcc $options -DHEADER="\"$file\"" -c $tmp.c
;;
esac
done
rm -f $tmp.?
trap 0
Run Code Online (Sandbox Code Playgroud)
碰巧我从来不需要将任何包含空格的选项传递给脚本,因此代码在处理空格选项时听起来不合理.在Bourne/Korn shell中处理它们至少会使脚本更复杂,没有任何好处; 使用Bash和数组可能会更好.
用法:
chkhdr -Wstrict-prototypes -DULTRA_TURBO -I$PROJECT/include header1.h header2.h
Run Code Online (Sandbox Code Playgroud)