我可以编写没有标题的C ++代码(重复的函数声明)吗?

时间:2009-06-16 13:55:00

标签: c++ header header-files

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

25 个答案:

答案 0 :(得分:57)

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

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

更新150120:

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

答案 1 :(得分:37)

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

首先是“是”。

在GCC中,您可以这样做:

// foo.cph

void foo();

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

这有预期的效果:您将标题和来源合并为一个文件,可以包含和链接。

然后使用no:

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

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

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

当所有文件都是头文件和源文件的组合时,每次更改都会触发重新编译所有源文件。 C ++现在还不知道它的快速编译时间,想象一下每次必须重新编译整个项目时会发生什么。然后将其推断为具有复杂依赖性的数百个源文件的项目......

答案 2 :(得分:27)

很抱歉,但是没有用于消除C ++标题的“最佳做法”:这是一个糟糕的主意,期间。如果你恨他们那么多,你有三个选择:

  • 熟悉C ++内部和您正在使用的任何编译器;你会遇到与普通C ++开发人员不同的问题,你可能需要在没有很多帮助的情况下解决它们。
  • 选择一种可以“正确”使用而不会感到沮丧的语言
  • 获取一个工具为您生成它们;你仍然会有标题,但是你节省了一些打字工作

答案 3 :(得分:10)

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

  

通常,C ++类有两种   files:头文件和   定义文件。我们应该在哪里写   断言:在头文件中,   因为断言是规范吗?   或者在定义文件中,因为它们   可执行吗?或者在两者中,跑步   不一致的风险(和   重复工作)?我们推荐,   相反,我们放弃了   传统风格,并取消   定义文件,仅使用   头文件,好像所有函数都是   内联定义,非常像Java   和埃菲尔做的。

     

这是如此激烈   改变它的C ++正常性   冒着破坏这种努力的风险   一开始。另一方面,维持   每个类的两个文件是这样的   尴尬,迟早是一个C ++   发展环境会出现   从我们这里隐藏,允许我们   没有,专注于我们的课程   不得不担心他们在哪里   存储

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


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

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

答案 4 :(得分:8)

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

目前,C ++头文件是一个非常邪恶的东西。我不喜欢他们,但没有办法解决他们。我很乐意看到有关这个问题的一些改进和新想法。

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

答案 5 :(得分:8)

我见过像你这样的人write everything in the headers。这给你想要的属性只需要编写一次方法配置文件。

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

答案 6 :(得分:5)

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

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

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

答案 7 :(得分:5)

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

(但是,正如Richard指出的那样,这似乎限制了您使用某些C ++功能。请参阅Richard的答案here right in this thread。)

答案 8 :(得分: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;
}

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

答案 9 :(得分:3)

目前还没有人在Visual Studio 2012中提到过Visual-Assist X.

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

  • “创建声明”将函数声明从当前函数复制到.hpp文件中。
  • “Refactor..Change signature”允许您使用一个命令同时更新.cpp和.h文件。
  • Alt-O允许您立即在.cpp和.h文件之间切换。

答案 10 :(得分:2)

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

您将面临一些非常具体的限制。其中一个是你将无法使用循环引用(你将无法让类Parent包含指向类ChildNode实例的指针,而类ChildNode也包含指向类Parent实例的指针。必须是一个或另一个。)

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

答案 11 :(得分: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

我不推荐这个,我想这个结构表明以死记硬背的重复为代价去除内容重复。我想这会使复制面食更容易?这不是一种美德。

与这种性质的所有其他技巧一样,对函数体的修改仍然需要重新编译所有文件,包括包含该函数的文件。非常谨慎的自动化工具可以部分避免这种情况,但是他们仍然需要解析源文件以进行检查,并且要仔细构造,以便在没有不同的情况下不重写它们的输出。

对于其他读者:我花了几分钟试图找出这种格式的包含警卫,但没有提出任何好的。评论

答案 12 :(得分:2)

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

正在讨论的提案是N2073

它的不好之处在于你不会得到它,即使使用最新的c ++ 0x编译器也是如此。你必须等待。与此同时,您必须在仅限标题库中的定义的唯一性与编译成本之间进行折衷。

答案 13 :(得分:1)

据我所知,没有。标题是C ++作为一种语言的固有部分。不要忘记,forward声明允许编译器只包含一个指向编译对象/函数的函数指针,而不必包含整个函数(你可以通过声明函数内联来解决这个问题(如果编译器感觉像这样)。 / p>

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

答案 14 :(得分:1)

C ++ 20模块解决了此问题。不再需要复制粘贴!只需将代码编写到一个文件中,然后使用“导出”导出内容即可。

export module mymodule;

export int myfunc() {
    return 1
}

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

在编写此答案时,这些编译器支持它: enter image description here

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

答案 15 :(得分:1)

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

从那以后,OOP征服了世界,世界更多的是定义然后实现。结果,包括头文件在使用一种语言时非常痛苦,在这种语言中,诸如STL中的基本集合用模板制作,这对于编译器来说是非常困难的工作。对于TDD,重构工具,一般开发环境,所有那些使用预编译头文件的魔法都没有多大帮助。

当然,C程序员并没有因此而受到太大的影响,因为他们没有编译器繁重的头文件,因此他们对非常直接的低级编译工具链感到满意。使用C ++,这是一个痛苦的历史:无尽的前向声明,预编译头,外部解析器,自定义预处理器等。

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

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

答案 16 :(得分:1)

完全可以在没有头文件的情况下开发。可以直接包含源文件:

#include "MyModule.c"

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

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

我目前正在为我的一个主要项目开发使用此方法。以下是我所经历的优势细分:

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

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

答案 17 :(得分:0)

学会识别头文件是一件好事。它们将代码如何从实现其实际执行操作的实现中分离出来。

当我使用某人的代码时,我现在想要浏览所有实现以查看类中的方法。我关心代码的作用,而不是它是如何做的。

答案 18 :(得分:0)

历史上的听证会文件被使用有两个原因。

  1. 在编译程序时提供符号以使用 库或其他文件。

  2. 隐藏部分执行;保持隐私。

例如,假设您有一个不想暴露给其他人的功能 程序的一部分,但希望在您的实现中使用。在那里面 在这种情况下,您可以在 CPP 文件中编写函数,但将其省略 的头文件。你可以用变量和任何东西来做到这一点 想要在您不想要的浸渍中保密 暴露于该源代码的编号。在其他编程中 lanugases 有一个“public”关键字,允许模块部分 避免暴露于程序的其他部分。在 C 和 C++ 中 在文件级别不存在这样的设施,因此使用头文件

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

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

#ifndef 库_H #define LIBRARY_H

// 在此处写下您的定义。

#endif

像这样评论 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 文件,然后 然后把它当作一个头文件,但写出实现 那个文件。

答案 19 :(得分:0)

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

阅读更多about C++,例如Programming using C++书,然后是C + 11标准n3337

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

如果您的C ++编译器是GCC,而您正在编译translation unit foo.cc,请考虑运行g++ -O -Wall -Wextra -C -E foo.cc > foo.ii;发出的文件foo.ii不包含任何预处理程序指令,可以用g++ -O foo.ii -o foo-bin编译成foo-bin executable(至少在Linux上)。另请参见Advanced Linux Programming

在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 ++代码生成器)并配置您的build automation工具(例如ninja-buildGNU make)来处理这种情况。请注意,GCC 10的源代码具有许多C ++代码生成器。

使用GCC,您有时可能会考虑编写自己的GCC plugin来分析您(或其他)C ++代码(例如,在GIMPLE级别)。另请参阅(2020年秋季)CHARIOTDECODER欧洲项目。您也可以考虑使用Clang static analyzerFrama-C++

答案 20 :(得分:0)

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

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

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

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

  • 包括警卫。好的,我们都知道怎么写这些但是在完美的系统中没有必要。

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

完美的系统可以与

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

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

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

答案 21 :(得分:0)

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

Catalin(原谅丢失的变音符号)还提出了一种更实用的方法,可以在头文件中定义您的方法。这实际上可以在大多数情况下工作..特别是如果你的头文件中有警卫,以确保它们只被包含一次。

我个人认为头文件+声明功能更适合于“了解”新代码,但我认为这是个人偏好......

答案 22 :(得分:0)

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

其中一些事情,虽然感觉很尴尬,但是让你获得更多,然后再见到眼睛。

作为一个例子考虑指针,按值/通过引用传递参数......等等。

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

答案 23 :(得分:0)

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

答案 24 :(得分:0)

你可以不用标题。但是,为什么花费精力试图避免仔细研究专家多年来开发的最佳实践。

当我写基本的时候,我非常喜欢行号。但是,我不会想到将它们塞进C ++中,因为那不是C ++方式。标题也是如此......我确信其他答案解释了所有的推理。