【问题标题】:Should C++ eliminate header files? [closed]C++ 应该消除头文件吗? [关闭]
【发布时间】:2010-10-19 15:54:12
【问题描述】:

许多语言,例如 Java、C#,不将声明与实现分开。 C# 有一个分部类的概念,但实现和声明仍然保留在同一个文件中。

为什么 C++ 没有相同的模型?有头文件是不是更实用?

我指的是当前和即将发布的 C++ 标准版本。

【问题讨论】:

  • 我相信 C++ 中的头文件没有真正的理由。存储在其中的所有信息都在 .cpp 中复制,并且可以自动提取(如 www.lazycplusplus.com 所示——免责声明:未使用)。问题在于编译速度,C#&Java 已经证明这是可以克服的。
  • 将其作为评论发布,因为我不确定我的信息:) 我不确定为什么这会被否决并关闭。这是一个很好的问题IMO。 ;)
  • 谢谢....我也不知道为什么它也被否决了
  • Brian:“保留一个空指针”是什么意思?您可能知道,C/C++ 中的静态分配数据需要在标头中声明后在 .cpp 中手动定义(除非它是文件范围的)。使用 LazyC++,您只需跳过标头声明并编写实现定义
  • Brian - 请参阅我对 Tim 的回复。在 LazyC++ 中,您实际上是在编写类定义(包括您想要的任何填充),您只是没有复制内容。顺便说一句,你的“保留”是一个不可移植的黑客,你最好用柴郡猫成语。

标签: c++ header-files


【解决方案1】:

头文件是语言的一个组成部分。没有头文件,所有的静态库、动态库,几乎任何预编译的库都变得毫无用处。头文件还可以更轻松地记录所有内容,并且可以查看库/文件的 API,而无需查看每一位代码。

它们还可以更轻松地组织您的程序。是的,您必须不断地从源代码切换到标头,但它们还允许您在实现中定义内部和私有 API。例如:

MySource.h:

extern int my_library_entry_point(int api_to_use, ...);

MySource.c:

int private_function_that_CANNOT_be_public();

int my_library_entry_point(int api_to_use, ...){
  // [...] Do stuff
}

int private_function_that_CANNOT_be_public() {

}

如果你#include <MySource.h>,那么你会得到my_library_entry_point

如果你#include <MySource.c>,那么你得到private_function_that_CANNOT_be_public

如果你有一个函数来获取密码列表,或者一个实现你的加密算法的函数,或者一个会暴露操作系统内部的函数,或者一个函数覆盖权限等。

【讨论】:

    【解决方案2】:

    我仍然不明白某些陈述的意义。 API和实现分离是一件非常好的事情,但是头文件不是API。那里有私人领域。如果您添加或删除私有字段,您会更改实现而不是 API。

    【讨论】:

      【解决方案3】:

      这种分离的一个优点是很容易只查看界面,而不需要高级编辑器

      【讨论】:

        【解决方案4】:

        向后兼容性 - 头文件不会被删除,因为它会破坏向后兼容性。

        【讨论】:

          【解决方案5】:

          如果你想知道这永远不会发生的原因:它会破坏几乎所有现有的 C++ 软件。如果您查看 C++ 委员会的一些设计文档,他们会查看各种替代方案,看看它会破坏多少代码。

          将 switch 语句更改为智能的东西会容易得多。那只会破坏一点代码。它仍然不会发生。

          为新想法编辑:

          C++ 和 Java 之间需要 C++ 头文件的区别在于 C++ 对象不一定是指针。在 Java 中,所有类实例都由指针引用,尽管看起来并非如此。 C++ 在堆和栈上分配了对象。这意味着 C++ 需要一种方法来了解对象的大小以及数据成员在内存中的位置。

          【讨论】:

          • C++ 需要一种方法来了解内存中的类布局并不意味着它一定需要头文件。头文件是 C++ 获取该信息的当前机制,而不是唯一可能的机制。
          【解决方案6】:

          The Design and Evolution of C++,Stroustrup 又给出了一个理由……

          同一个头文件可以有两个或多个实现文件,可以由多个程序员同时处理,而不需要源代码控制系统。

          这几天可能看起来很奇怪,但我想这是 C++ 发明时的一个重要问题。

          【讨论】:

          • 即使使用源代码控制系统,处理单独的文件也确实可以简化合并。
          • +1 表示同一个头文件可以有两个或多个实现文件。这是在不滥用#ifdef-fu 的情况下进行可移植应用程序的天赐之物。 (只需使用从依赖于平台的myclass-win.cpp / myclass-linux.cpp 生成正确的myclass.o 的makefile,而外部只关心myclass.h
          • 这确实很奇怪,现在根本不应该作为理由来讨论。这是一段历史。
          • 但这并不能说明为什么需要标头。即使语言没有头文件,你仍然可以处理两个实现文件。
          • @Andrew:这是对已接受答案的补充。您可以对相同的接口和带有声明的标头进行单独的实现,这被认为是一种好方法,而工程师同时致力于它的实现。
          【解决方案7】:

          甚至Bjarne Stroustrup 也将头文件称为杂牌。

          但如果没有包含必要元数据(如 Java 类文件或 .Net PE 文件)的标准二进制格式,我看不到任何实现该功能的方法。剥离的 ELF 或 a.out 二进制文件没有您需要提取的大量信息。而且我认为这些信息从未存储在 Windows XCOFF 文件中。

          【讨论】:

          • 如果 Stroustrup 先生发明了头文件,我会更加认真地对待他。然后他会承认自己的错误,而不是对他明确地将其作为保持向后兼容目标的语言进行谩骂。这就像决定做一只章鱼,然后抱怨你必须在它身上放多少触手。
          • 如果 Stroustrup 先生可以写一个编译器,我会更加认真地对待他。比他可以将他想要的任何信息放入目标文件中,而不管格式或剥离。
          • C++ 头文件很难看。在编写良好的 C 代码中,标头非常棒 - 源文件中的私有实现细节是真正私有的,根本不会向任何人公开。
          • 如果 Stroustrup 先生摆脱头文件,我会更加认真地对待他。
          • @Chris Lutz,Kmarsh,夸克。你在谈论(难以置信地无知)一个在你们大多数人出生之前就开发了 C++ 的人。
          【解决方案8】:

          很多人都意识到头文件的缺点,并且有想法将更强大的模块系统引入C++。 您可能想看看 Daveed Vandevoorde 的 Modules in C++ (Revision 5)

          【讨论】:

          • 不幸的是,它没有进入 C++0x 并被推迟到未来的 TR。
          【解决方案9】:

          在实现文件的单独组件中定义类接口是有价值的。

          这可以通过接口来完成,但是如果你走这条路,那么你就是在暗示类在将实现与契约分离方面存在缺陷。

          Modula 2 有正确的想法,定义模块和实现模块。 http://www.modula2.org/reference/modules.php

          Java/C# 的答案是相同的隐式实现(尽管是面向对象的。)

          头文件很杂乱,因为头文件表达了实现细节(例如私有变量)。

          在转向 Java 和 C# 时,我发现如果一种语言需要 IDE 支持进行开发(这样公共类接口可以在类浏览器中导航),那么这可能是一种代码不符合其具有特别可读性的优点。

          我发现接口与实现细节的混合非常可怕。

          至关重要的是,缺乏将公共类签名记录在独立于实现的简明注释文件中的能力,这表明语言设计是为了方便作者而编写的,而不是为了方便维护。好吧,我现在正在谈论 Java 和 C#。

          【讨论】:

            【解决方案10】:

            哦,是的!

            使用 Java 和 C# 编码后,每个类都有 2 个文件真的很烦人。所以我在想如何在不破坏现有代码的情况下合并它们。

            事实上,这很容易。只需将定义(实现)放在#ifdef 部分中,然后在编译器命令行上添加定义来编译该文件。就是这样。

            这是一个例子:

            /* File ClassA.cpp */
            
            #ifndef _ClassA_
            #define _ClassA_
            
            #include "ClassB.cpp"
            #include "InterfaceC.cpp"
            
            class ClassA : public InterfaceC
            {
            public:
                ClassA(void);
                virtual ~ClassA(void);
            
                virtual void methodC();
            
            private:
                ClassB b;
            };
            
            #endif
            
            #ifdef compiling_ClassA
            
            ClassA::ClassA(void)
            {
            }
            
            ClassA::~ClassA(void)
            {
            }
            
            void ClassA::methodC()
            {
            }
            
            #endif
            

            在命令行上,编译该文件

            -D compiling_ClassA
            

            其他需要包含ClassA的文件就可以了

            #include "ClassA.cpp"
            

            当然,在命令行上添加定义可以很容易地通过宏扩展(Visual Studio 编译器)或自动变量(gnu make)添加,并使用相同的定义名称命名法。

            【讨论】:

            • 完全错过了分离标头和源文件的要点(嗯,几乎)。如果您这样做,那么对 ClassA 实现的更改将导致 ClassA.cpp 的所有用户需要重新编译(除非您的构建系统非常智能,这本身就有成本)。
            • @Mankarse 在使用类之间的接口(如在 Java 或 C# 中)时,更改类的实现对编译依赖性的影响很小。
            【解决方案11】:

            C 旨在使编写编译器变得容易。它基于这一原则做了很多事情。指针的存在只是为了使编写编译器更容易,头文件也是如此。继承到 C++ 的许多东西都是基于与这些特性的兼容性,这些特性使编译器编写更容易。

            其实这是个好主意。在创建 C 时,C 和 Unix 是一对。 C 移植 Unix,Unix 运行 C。这样,C 和 Unix 可以迅速从一个平台传播到另一个平台,而基于汇编的操作系统必须完全重写才能移植。

            在一个文件中指定接口并在另一个文件中实现的概念一点也不坏,但这不是 C 头文件的含义。它们只是一种限制编译器必须通过您的源代码进行的传递次数的方法,并允许对文件之间的契约进行一些有限的抽象,以便它们可以通信。

            这些项目、指针、头文件等...与其他系统相比并没有真正提供任何优势。通过在编译器上投入更多精力,您可以像编译指向完全相同的对象代码的指针一样轻松地编译引用对象。这就是 C++ 现在所做的。

            C 是一种伟大而简单的语言。它的功能集非常有限,您可以轻松编写编译器。移植它通常是微不足道的!我并不是想说它是一种糟糕的语言或任何东西,只是 C 在创建它时的主要目标可能会在语言中留下一些现在或多或少不必要的残余物,但为了兼容性将保留下来。


            似乎有些人并不真正相信 C 是为移植 Unix 而编写的,所以在这里:(from)

            UNIX 的第一个版本是编写的 用汇编语言,但 Thompson 的 目的是写成 使用高级语言。

            Thompson 于 1971 年首次尝试使用 PDP-7 上的 Fortran,但放弃了 第一天之后。然后他写了一个 他称之为B的非常简单的语言, 他开始使用 PDP-7。它 工作,但有问题。 首先,因为实施是 解释,它总是 慢。二、B的基本概念, 这是基于面向单词的 BCPL,只是不适合 像新的面向字节的机器 PDP-11。

            Ritchie 使用 PDP-11 添加类型 到B,有一段时间被称为NB 为“新B”,然后他开始 为它写一个编译器。 “所以这样 C的第一阶段真的是这两个 连续的几个阶段,首先, 一些语言从 B 改变,真的, 添加类型结构 语法上有很大的变化;和做 编译器,”Ritchie 说。

            “第二阶段比较慢,”他说 用 C. Thompson 重写 UNIX 开始于 1972 年夏天,但 两个问题:弄清楚如何运行 基本的协程,也就是如何 将控制从一个进程切换到 其他;以及获取难度 正确的数据结构,因为 原版C没有 结构。

            "造成的事物的组合 肯放弃整个夏天,” 里奇说。 “一年来,我补充说 结构,并可能使 编译器代码稍微好一些—— 更好的代码——等等 夏天,那是我们制作 齐心协力,实际上做了重做 C语言的整个操作系统。”


            这是我的意思的一个完美例子。来自 cmets:

            指针的存在只是为了让编写编译器更容易?不,指针的存在是因为它们是对间接概念的最简单的抽象。 ——亚当·罗森菲尔德(一小时前)

            你是对的。为了实现间接,指针是实现的最简单的抽象。它们绝不是最容易理解或使用的。数组要容易得多。

            问题?要像指针一样高效地实现数组,您几乎必须在编译器中添加大量代码。

            没有理由他们不能设计没有指针的 C,而是使用如下代码:

            int i=0;
            while(src[++i])
                dest[i]=src[i];
            

            需要付出很多努力(在编译器部分)才能排除显式 i+src 和 i+dest 添加并使其创建与此相同的代码:

            while(*(dest++) = *(src++))
                ;
            

            事后分解变量“i”是困难的。新的编译器可以做到这一点,但在当时是不可能的,而且在糟糕的硬件上运行的操作系统几乎不需要像这样的优化。

            现在很少有系统需要这种优化(我在最慢的平台之一上工作——有线电视机顶盒,我们的大部分东西都是用 Java 编写的),在极少数情况下你可能需要它,新的 C 编译器应该足够聪明,可以自行进行这种转换。

            【讨论】:

            • 指针的存在只是为了让编写编译器更容易?不。指针的存在是因为它们是对间接概念的最简单的抽象。
            • Pascal 旨在简化编译器的编写。您几乎可以使用简单的手动编码递归下降解析器。 C 更复杂。
            • 任何说编写 C 编译器很简单的人从来没有编写过 C 编译器。
            • C 是在操作系统中替代汇编语言的早期尝试。 D&R 正在寻求在硅片上进行实际编程的能力。这就是为什么指针,而不是另一个抽象。他们想要一种清晰的方式来完成他们在汇编程序中所做的事情。
            • 我对 Pascal 的看法是,如果主要的 C 设计目标是易于实现,那么 K&R 可以而且会做得更好。做起来并不难。
            【解决方案12】:

            没有头文件就没有语言。这是一个神话。

            查看任何用于 Java 的专有库分发(我没有 C# 经验可言,但我希望它是相同的)。他们没有给你完整的源文件;他们只是给你一个文件,其中每个方法的实现都是空白的({}{return null;} 等)以及他们可以隐藏的所有内容。除了标题,你不能称之为任何东西。

            但是,没有技术原因,为什么 C 或 C++ 编译器可以将适当标记的文件中的所有内容都计为 extern,除非该文件是直接编译的。然而,编译的成本将是巨大的,因为 C 和 C++ 都不能快速解析,这是一个非常重要的考虑因素。任何更复杂的合并标头和源代码的方法都会很快遇到技术问题,例如编译器需要知道对象的布局。

            【讨论】:

            • Java 编译器从 .class 文件中提取该信息。不仅如此:Sun JDK 附带了大多数类的源代码(com.sun.* 中的除外)。
            • @andref 同意,大多数现代语言(通常是动态语言)都没有正式的头文件
            【解决方案13】:

            嗯,C++ 本身不应该因为向后兼容性而消除头文件。但是,我确实认为它们总体上是一个愚蠢的想法。如果要分发闭源库,可以自动提取此信息。如果您想了解如何在不查看实现的情况下使用类,这就是文档生成器的用途,它们的工作要好得多。

            【讨论】:

              【解决方案14】:

              如果你想要没有头文件的 C++,那么我有个好消息要告诉你。

              它已经存在并被称为 D (http://www.digitalmars.com/d/index.html)

              从技术上讲,D 似乎比 C++ 好很多,但目前它还不够主流,无法在许多应用程序中使用。

              【讨论】:

              • 无论你是否使用它,D 的关键点在于它具有 C++ 的大部分功能集。您可能会争辩说 C++ 需要头文件是因为模板需要它们,或者因为在涉及高效代码的情况下您以某种方式需要它们。 D 的存在表明这两个都是错误的。
              • C++ 是一种系统级语言。 D 不是。
              • 即使没有回答问题,这条评论对我真的很有帮助。
              【解决方案15】:

              C++ 的目标之一是成为 C 的超集,如果它不支持头文件,就很难做到这一点。而且,通过扩展,如果您希望删除头文件,您不妨考虑完全删除 CPP(预处理器,而不是 plus-plus); C# 和 Java 都没有在其标准中指定宏预处理器(但应注意,在某些情况下,它们甚至可以用于这些语言,甚至可以用于这些语言)。

              由于现在设计了 C++,因此您需要原型(就像在 C 中一样)来静态检查任何引用外部函数和类的编译代码。如果没有头文件,您必须在使用它们之前输入这些类定义和函数声明。对于不使用头文件的 C++,您必须在语言中添加一个功能,以支持 Java 的 import 关键字之类的东西。这将是一个重大的补充和改变;回答你是否可行的问题:我不这么认为——一点也不。

              【讨论】:

                【解决方案16】:

                我经常在 C# 和 C++ 之间切换,而 C# 中缺少头文件是我最大的烦恼之一。我可以查看一个头文件并了解我需要了解的关于一个类的所有信息——它的成员函数被称为什么、它们的调用语法等——而无需浏览实现该类的代码页面。

                是的,我知道部分类和#regions,但不一样。部分类实际上使问题变得更糟,因为类定义分布在多个文件中。就#regions 而言,它们似乎从未以我目前正在做的事情的方式扩展,所以我必须花时间扩展这些小优点,直到我得到正确的视图。

                也许如果 Visual Studio 的智能感知在 C++ 上工作得更好,我就没有令人信服的理由不得不如此频繁地引用 .h 文件,但即使在 VS2008 中,C++ 的智能感知也无法触及 C# 的

                【讨论】:

                • 您正在完成机器提取文档的工作,并且喜欢它。哇...
                • 在 Java/C# 中,您使用接口来显示 api。这是一种比头文件更好的显示 API 的方式。单独的类文件用作实现。
                • 是的,但是...无需额外努力,C++ 头文件混合了实现细节和公共合同。
                • 让我们看看你使用标题来检查 std::vector 的接口...或任何其他 std 集合类...或者绝对是 boost 中的任何东西...所有这些都是完全没用。
                • 任何合适的 IDE 都会为您提供您需要的关于类和方法的所有信息。由于您提到 VS2008,使用对象浏览器提供了完整的文档和轻松的搜索和浏览。你还需要什么?你觉得浏览头文件的页面更容易吗?
                【解决方案17】:

                头文件允许独立编译。您不需要访问甚至不需要实现文件来编译文件。这可以使分布式构建更容易。

                这也让 SDK 更容易完成。您可以只提供标题和一些库。当然,其他语言也有解决这个问题的方法。

                【讨论】:

                • 很明显,简单不是标题的原因,因为:(1) 它们难以维护,它们是重复的代码,(2) 对于 SDK,它们可以自动提取。我认为分布式构建是正确的。
                • 不用头文件也能轻松独立编译。
                • 我认为 C++ 头文件不会将实现与接口分开。为了得到真正的分离,你必须使用 PIMPL 或类似的抽象。
                • 虽然理论上你是对的——如果头文件很轻并且只声明接口——实际上,头文件很重并且充满了条件编译项.
                • Headers 以一种相当原始的方式启用单独的编译和 SDK,但它们不需要支持这些东西 - .NET 和 Java 在不使用 headers 的情况下都支持。
                猜你喜欢
                • 2016-12-02
                • 2010-10-03
                • 2021-07-19
                • 2012-12-25
                • 2011-09-10
                • 2010-09-16
                • 1970-01-01
                • 2013-08-05
                • 2011-09-01
                相关资源
                最近更新 更多