64

有没有办法不必编写两次函数声明(头文件),并且在用 C++ 编程时仍然保持相同的编译可伸缩性、调试的清晰度和设计的灵活性?

4

25 回答 25

65

使用Lzz。它需要一个文件并自动为您创建一个 .h 和 .cpp,所有声明/定义都在正确的位置。

Lzz 确实非常强大,可以处理 99% 的完整 C++ 语法,包括模板、特化等。

更新 150120:

较新的 C++ '11/14 语法只能在 Lzz 函数体中使用。

于 2009-06-16T14:18:45.370 回答
40

当我开始写 C 时,我也有同样的感觉,所以我也研究了这个。答案是肯定的,这是可能的,不,你不想这样做。

首先是肯定的。

在 GCC 中,您可以这样做:

// foo.cph

void foo();

#if __INCLUDE_LEVEL__ == 0
void foo() {
   printf("Hello World!\n");
}
#endif

这具有预期的效果:您将标头和源合并到一个可以包含和链接的文件中。

然后没有:

这仅在编译器可以访问整个源代码时才有效。在编写要分发但保持封闭源代码的库时,您不能使用此技巧。要么分发完整的 .cph 文件,要么必须编写一个单独的 .h 文件以与 .lib 一起使用。尽管也许您可以使用宏预处理器自动生成它。不过它会变得毛茸茸的。

以及为什么你不想要这个的原因 #2,这可能是最好的一个:编译速度。通常,只有当文件本身发生更改或其中包含的任何文件发生更改时,才需要重新编译 C 源文件。

  • C 文件可以经常更改,但更改只涉及重新编译已更改的文件。
  • 头文件定义接口,因此它们不应该经常更改。但是,当它们这样做时,它们会触发包含它们的每个源文件的重新编译。

当您的所有文件都是头文件和源文件的组合时,每次更改都会触发所有源文件的重新编译。即使到现在,C++ 也不以其快速的编译时间而闻名,想象一下当整个项目每次都必须重新编译时会发生什么。然后将其推断为一个包含数百个具有复杂依赖关系的源文件的项目......

于 2009-06-16T14:12:07.507 回答
29

抱歉,但在 C++ 中没有消除标头的“最佳实践”之类的东西:这是一个坏主意,句号。如果你那么讨厌他们,你有三个选择:

  • 熟悉 C++ 内部结构和您正在使用的任何编译器;您将遇到与普通 C++ 开发人员不同的问题,并且您可能需要在没有太多帮助的情况下解决这些问题。
  • 选择一种你可以“正确”使用而不会感到沮丧的语言
  • 获取为您生成它们的工具;你仍然会有标题,但你节省了一些打字工作
于 2009-06-16T14:07:38.233 回答
11

Pedro Guerreiro在他的文章Simple Support for Design by Contract in C++中说:

通常,C++ 类有两个文件:头文件和定义文件。我们应该在哪里写断言:在头文件中,因为断言是规范?或者在定义文件中,因为它们是可执行的?或者两者都冒着不一致(和重复工作)的风险?相反,我们建议我们放弃传统样式,并取消定义文件,只使用头文件,就好像所有函数都是内联定义的,就像 Java 和 Eiffel 所做的那样。

与 C++ 常态相比,这是一个如此巨大的变化,以至于它有可能在一开始就扼杀这项努力。另一方面,为每个类维护两个文件是如此尴尬,以至于迟早会出现一个 C++ 开发环境,它将对我们隐藏起来,使我们能够专注于我们的类,而不必担心它们的存储位置。

那是 2001 年。我同意了。现在是 2009 年,仍然没有出现“对我们隐藏,让我们专注于我们的课程的开发环境”。相反,长编译时间是常态。


注意:上面的链接现在似乎已经死了。这是对该出版物的完整参考,它出现在作者网站的“出版物”部分:

Pedro Guerreiro,C++ 合同设计的简单支持,TOOLS USA 2001,Proceedings,第 24-34 页,IEEE,2001。

于 2009-06-16T14:10:35.700 回答
8

没有实用的方法来绕过标题。您唯一能做的就是将所有代码放入一个大的 c++ 文件中。这将导致无法维持的混乱,所以请不要这样做。

目前,C++ 头文件是一种邪恶。我不喜欢他们,但没有办法绕过他们。不过,我很乐意看到有关该问题的一些改进和新想法。

顺便说一句 - 一旦你习惯了它就不再那么糟糕了。C++(以及任何其他语言)有更多令人讨厌的东西。

于 2009-06-16T13:59:31.150 回答
8

我见过像你这样的人所做的就是将所有内容都写在标题中。这为您提供了只需编写一次方法配置文件的所需属性。

就我个人而言,我认为有很好的理由说明最好将声明和定义分开,但如果这让你感到困扰,那么有一种方法可以做你想做的事。

于 2009-06-16T14:07:03.757 回答
6

实际上,您必须编写两次函数声明(一次在头文件中,一次在实现文件中)。函数的定义(AKA 实现)将在实现文件中编写一次。

您可以在头文件中编写所有代码(这实际上是 C++ 通用编程中非常常用的做法),但这意味着每个包含该头文件的 C/CPP 文件都意味着从这些头文件重新编译实现。

如果您正在考虑使用类似于 C# 或 Java 的系统,那么在 C++ 中是不可能的。

于 2009-06-16T13:59:59.043 回答
6

有头文件生成软件。我从未使用过它,但它可能值得研究。例如,查看mkhdr!它应该扫描 C 和 C++ 文件并生成适当的头文件。

(但是,正如 Richard 指出的那样,这似乎限制了您使用某些 C++ 功能。请参阅 Richard 的答案,而不是在此线程中的此处。)

于 2009-06-16T14:17:41.190 回答
4

还没有人在 Visual Studio 2012 下提到 Visual-Assist X。

它有一堆菜单和热键,您可以使用它们来减轻维护标题的痛苦:

  • “创建声明”将函数声明从当前函数复制到 .hpp 文件中。
  • “重构..更改签名”允许您使用一个命令同时更新 .cpp 和 .h 文件。
  • Alt-O 允许您在 .cpp 和 .h 文件之间立即切换。
于 2013-06-01T14:48:17.453 回答
3

实际上...您可以将整个实现写在一个文件中。模板类都是在头文件中定义的,没有 cpp 文件。

您也可以使用您想要的任何扩展名进行保存。然后在#include 语句中,您将包含您的文件。

/* mycode.cpp */
#pragma once
#include <iostreams.h>

class myclass {
public:
  myclass();

  dothing();
};

myclass::myclass() { }
myclass::dothing()
{
  // code
}

然后在另一个文件中

/* myothercode.cpp */
#pragma once
#include "mycode.cpp"

int main() {
   myclass A;
   A.dothing();
   return 0;
}

您可能需要设置一些构建规则,但它应该可以工作。

于 2009-06-16T14:03:19.753 回答
3

C++ 20 模块解决了这个问题。不再需要复制粘贴!只需在单个文件中编写代码并使用“导出”导出内容。

export module mymodule;

export int myfunc() {
    return 1
}

在此处阅读有关模块的更多信息:https ://en.cppreference.com/w/cpp/language/modules

在撰写此答案时(2022 年 2 月),这些编译器支持它: 模块支持

有关支持的编译器,请参见此处: https ://en.cppreference.com/w/cpp/compiler_support

如果您想在 CMake 中使用模块,请参阅此答案: https ://stackoverflow.com/a/71119196/7910299

于 2020-10-08T06:42:10.290 回答
2

在阅读了所有其他答案后,我发现缺少在 C++ 标准中添加对模块的支持的正在进行的工作。它不会进入 C++0x,但目的是在以后的技术审查中解决它(而不是等待新标准,这需要很长时间)。

正在讨论的提案是N2073

不好的部分是即使使用最新的 c++0x 编译器,您也不会得到它。你将不得不等待。同时,您将不得不在仅标头库中定义的唯一性和编译成本之间做出妥协。

于 2011-03-16T10:51:17.963 回答
2

要提供 rix0rrr 的流行答案的变体:

// foo.cph

#define INCLUDEMODE
#include "foo.cph"
#include "other.cph"
#undef INCLUDEMODE

void foo()
#if !defined(INCLUDEMODE)
{
   printf("Hello World!\n");
}
#else
;
#endif

void bar()
#if !defined(INCLUDEMODE)
{
    foo();
}
#else
;
#endif

我不建议这样做,我认为这种结构表明以死记硬背为代价去除了内容重复。我想它使复制意大利面更容易?这并不是真正的美德。

与这种性质的所有其他技巧一样,对函数体的修改仍然需要重新编译所有文件,包括包含该函数的文件。非常小心的自动化工具可以部分避免这种情况,但它们仍然必须解析源文件以进行检查,并仔细构建以不重写它们的输出(如果没有什么不同)。

对于其他读者:我花了几分钟试图找出这种格式的包含守卫,但没有想出任何好的东西。注释?

于 2009-06-17T03:52:23.430 回答
2

可以避免标题。完全地。但我不推荐它。

您将面临一些非常具体的限制。其中之一是您将无法拥有循环引用(您将无法让 Parent 类包含指向 ChildNode 类实例的指针,并且 ChildNode 类还包含指向 Parent 类实例的指针。它必须是其中之一。)

还有其他一些限制最终会使您的代码变得非常奇怪。坚持标题。你将学会真正喜欢它们(因为它们提供了一个关于类可以做什么的很好的快速概要)。

于 2009-06-16T14:26:38.957 回答
2

我理解你的问题。我会说 C++ 的主要问题是它从 C 继承的编译/构建方法。C/C++ 标头结构是在编码涉及较少定义和更多实现的时候设计的。不要向我扔瓶子,但这就是它的样子。

从那时起,OOP 征服了世界,世界更多的是定义而不是实现。结果,包含标头使使用一种语言工作非常痛苦,其中基本集合(例如 STL 中的集合)是使用模板制作的,而众所周知,编译器很难处理这些工作。当涉及到 TDD、重构工具、通用开发环境时,预编译头文件的所有这些魔法并没有太大帮助。

当然,C 程序员并没有受此困扰,因为他们没有大量编译器的头文件,因此他们对非常简单的低级编译工具链感到满意。对于 C++,这是一段痛苦的历史:无休止的前向声明、预编译的头文件、外部解析器、自定义预处理器等。

然而,许多人没有意识到 C++ 是唯一一种为高级和低级问题提供强大而现代的解决方案的语言。说你应该选择另一种具有适当反射和构建系统的语言很容易,但是我们必须为此牺牲低级编程解决方案,并且我们需要将低级语言混合的事情复杂化使用一些基于虚拟机/JIT 的解决方案。

我有这个想法已经有一段时间了,拥有一个基于“单元”的 c++ 工具链将是地球上最酷的事情,类似于 D 中的工具链。问题出现在跨平台部分:对象文件能够存储任何信息,这没问题,但是由于在 Windows 上,目标文件的结构与 ELF 不同,因此实现跨平台的解决方案来存储和处理中途将是一件很痛苦的事- 编译单元。

于 2010-09-14T10:58:25.080 回答
1

据我所知,没有。标头是 C++ 作为一种语言的固有部分。不要忘记,前向声明允许编译器仅包含指向已编译对象/函数的函数指针,而不必包含整个函数(您可以通过声明内联函数来解决此问题(如果编译器喜欢它)。

如果你真的,真的,真的很讨厌制作标题,请编写一个 perl 脚本来自动生成它们。我不确定我会推荐它。

于 2009-06-16T14:02:14.560 回答
1

完全可以不用头文件进行开发。可以直接包含源文件:

#include "MyModule.c"

主要问题是循环依赖之一(即:在 C 中,您必须在调用它之前声明一个函数)。如果您完全自上而下地设计代码,这不是问题,但如果您不习惯这种设计模式,可能需要一些时间来围绕这种设计模式。

如果您绝对必须具有循环依赖关系,则可能需要考虑创建一个专门用于声明的文件并将其包含在其他所有内容之前。这有点不方便,但仍然比每个 C 文件都有一个头文件的污染少。

我目前正在为我的一个主要项目使用这种方法进行开发。以下是我体验过的优势的细分:

  • 源代码树中的文件污染要少得多。
  • 更快的构建时间。(编译器只生成一个目标文件,main.o)
  • 更简单的制作文件。(编译器只生成一个目标文件,main.o)
  • 无需“清洁”。每个构建都是“干净的”。
  • 更少的样板代码。更少的代码 = 更少的潜在错误。

我发现 Gish(Cryptic Sea 的一款游戏,Edmund McMillen)在其自己的源代码中使用了这种技术的变体。

于 2011-03-16T08:19:37.767 回答
0

Learn to recognize that header files are a good thing. They separate how codes appears to another user from the implementation of how it actually performs its operations.

When I use someone's code I do now want to have to wade through all of the implementation to see what the methods are on a class. I care about what the code does, not how it does it.

于 2009-06-21T17:37:21.777 回答
0

您可以仔细布局您的函数,以便所有依赖函数都在它们的依赖项之后编译,但正如 Nils 所暗示的那样,这是不切实际的。

Catalin(请原谅缺少的变音符号)还建议在头文件中定义方法的更实用的替代方法。这实际上可以在大多数情况下工作..特别是如果您在头文件中有保护以确保它们只包含一次。

我个人认为头文件+声明函数对于“了解”新代码更可取,但我认为这是个人偏好......

于 2009-06-16T14:03:37.553 回答
0

您可以不使用标题。但是,为什么要努力避免精心制定专家多年来开发的最佳实践。

当我写基本的时候,我很喜欢行号。但是,我不会想把它们塞进 C++ 中,因为那不是 C++ 的方式。标题也是如此……我相信其他答案可以解释所有原因。

于 2009-06-16T14:17:21.770 回答
0

出于实际目的,不,这是不可能的。从技术上讲,是的,你可以。但是,坦率地说,这是对语言的滥用,你应该适应这种语言。或者转向 C# 之类的东西。

于 2009-06-16T14:17:37.260 回答
0

最好的做法是使用头文件,一段时间后它会成长为你。我同意只有一个文件更容易,但它也可能导致错误的编码。

其中一些东西,虽然觉得别扭,但能让你得到更多的东西。

例如,考虑指针、按值/按引用传递参数……等。

对我来说,头文件允许我保持我的项目结构正确

于 2009-06-16T15:40:07.813 回答
0

由于重复,这已经“复活”了......

在任何情况下,标头的概念都是有价值的,即将接口与实现细节分开。标题概述了您如何使用类/方法,而不是它是如何使用的。

缺点是标题中的细节和所有必要的变通方法。这些是我看到的主要问题:

  • 依赖生成。修改头文件时,任何包含此头文件的源文件都需要重新编译。问题当然是确定哪些源文件实际使用它。当执行“干净”构建时,通常需要将信息缓存在某种依赖树中以供以后使用。

  • 包括警卫。好的,我们都知道如何编写这些,但在一个完美的系统中,这不是必需的。

  • 私人细节。在一个类中,您必须将私有详细信息放入标题中。是的,编译器需要知道类的“大小”,但在一个完美的系统中,它可以在稍后阶段绑定它。这会导致各种解决方法,例如 pImpl 和使用抽象基类,即使您只有一个实现只是因为您想隐藏依赖项。

完美的系统将与

  • 单独的类定义和声明
  • 这两者之间的明确绑定,因此编译器将知道类声明及其定义在哪里,并且会知道类的大小。
  • 您声明using class而不是预处理器#include。编译器知道在哪里可以找到一个类。完成“使用类”后,您可以使用该类名称而无需对其进行限定。

我很想知道 D 是如何做到的。

关于您是否可以在没有标头的情况下使用 C++,我会说不,您需要它们用于抽象基类和标准库。除此之外,没有它们你也可以过得去,尽管你可能不想这样做。

于 2011-03-16T11:47:33.270 回答
0

历史上使用听者文件有两个原因。

  1. 在编译要使用库或附加文件的程序时提供符号。

  2. 隐藏部分执行;保密。

例如,假设您有一个不想暴露给程序其他部分的功能,但想在您的实现中使用。在这种情况下,您将在 CPP 文件中编写函数,但将其保留在头文件之外。您可以使用变量以及任何想要在浸渍中保持私有且您不希望暴露于该源代码的编号的东西来执行此操作。在其他编程语言中,有一个“public”关键字可以防止模块部分暴露给程序的其他部分。在 C 和 C++ 中,文件级别不存在这样的工具,因此需要使用头文件。

头文件并不完美。使用“#include”只会复制您提供的任何文件的内容。当前工作树的单引号和系统安装的标头的 < 和 >。在系统安装的标准组件的 CPP 中,省略了“.h”;只是 C++ 喜欢做自己的事情的另一种方式。如果你想给 '#include' 任何类型的文件,它将被包含在内。它确实不是像 Java、Python 和大多数其他编程语言那样的模块系统。由于标头不是模块,因此需要采取一些额外的步骤才能从中获得类似的功能。Prepossesser(与所有#keywords 一起使用的东西)将盲目地包含您声明的每个需要在该文件中使用的内容,但是 C 或 C++ 希望在编译中只定义一个符号或含义。如果你使用图书馆,不是 main.cpp,而是在 main 包含的两个文件中,那么您只希望该库包含一次而不是两次。标准库组件经过特殊处理,因此您无需担心在任何地方都使用相同的 C++ 包含。为了让 Prepossesser 第一次看到你的库时它不会再次包含它,你需要使用一个听过的守卫。

听到警卫是最简单的事情。它看起来像这样:

#ifndef LIBRARY_H #define LIBRARY_H

// 在这里写下你的定义。

#万一

像这样评论 ifndef 被认为是好的:

#endif // LIBRARY_H

但是如果你不做评论,编译器不会在意,也不会伤害任何东西。

#ifndef 所做的只是检查 LIBRARY_H 是否等于 0;不明确的。当 LIBRARY_H 为 0 时,它提供 #endif 之前的内容。

然后#define LIBRARY_H 将LIBRARY_H 设置为1,因此下次预处理器看到#ifndef LIBRARY_H 时,它不会再次提供相同的内容。

(LIBRARY_H 应该是文件名,然后是 _ 和扩展名。如果你不写相同的东西,这不会破坏任何东西,但你应该保持一致。至少为#ifndef 输入文件名。否则,可能会混淆警卫的用途。)

真的没有什么想在这里发生的。


现在您不想使用头文件。

太好了,说你不关心:

  • 通过从头文件中排除它们来将它们私有化

  • 您不打算在库中使用此代码。如果您曾经这样做过,现在使用标头可能会更容易,因此您以后不必将代码重新组织到标头中。

  • 您不想在头文件中重复一次,然后在 C++ 文件中重复一次。

听者文件的目的可能看起来模棱两可,如果您不关心人们出于想象的原因说出它是错误的,那么请节省您的手,不要费心重复自己。

如何仅包含听者文件

#ifndef THING_CPP
#define THING_CPP

#include <iostream>

void drink_me() {
  std::cout << "Drink me!" << std::endl;
}

#endif  // THING_CPP

对于thing.cpp。

而对于 main.cpp 做

#include "thing.cpp"

int main() {
  drink_me();
  return 0;
}

然后编译。

基本上只需使用 CPP 扩展名命名包含的 CPP 文件,然后将其视为头文件,但在该文件中写出实现。

于 2021-07-09T15:17:32.210 回答
0

我可以编写没有标头的 C++ 代码吗

阅读有关 C++的更多信息,例如Programming using C++书,然后是 C+11 标准n3337

是的,因为预处理器(在概念上)生成没有标头的代码。

如果您的 C++ 编译器是GCC并且您正在编译您的翻译单元 foo.cc,请 考虑运行g++ -O -Wall -Wextra -C -E foo.cc > foo.ii;发出的文件foo.ii不包含任何预处理器指令,并且可以编译g++ -O foo.ii -o foo-binfoo-bin 可执行文件(至少在 Linux 上)。另请参阅高级 Linux 编程

在 Linux 上,以下 C++ 文件

// file ex.cc
extern "C" long write(int fd, const void *buf, size_t count);
extern "C" long strlen(const char*);
extern "C" void perror(const char*);
int main (int argc, char**argv)
{
   if (argc>1) 
     write(1, argv[1], strlen(argv[1]);
   else 
     write(1, __FILE__ " has no argument",
              sizeof(__FILE__ " has no argument"));
   if (write(1, "\n", 1) <= 0) {
     perror(__FILE__);
     return 1;
   }
   return 0;
}

可以使用GCC编译成g++ ex.cc -O ex-bin可执行文件ex-bin,执行时会显示一些内容。

在某些情况下,值得用另一个程序生成一些 C++ 代码

(可能是SWIGANTLRBisonRefPerSysGPP或您自己的 C++ 代码生成器)并配置您的构建自动化工具(例如ninja-buildGNU make)来处理这种情况。请注意,GCC 10的源代码有十几个 C++ 代码生成器。

使用GCC,您有时可能会考虑编写自己的GCC 插件来分析您(或其他人)的 C++ 代码(例如在GIMPLE级别)。另见(2020 年秋季)CHARIOTDECODER欧洲项目。您还可以考虑使用Clang 静态分析器Frama-C++

于 2020-10-08T06:53:21.567 回答