C ++是否应该消除头文件?[关闭]


72

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

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

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


13
我相信C ++中的头文件没有真正的原因。存储在其中的所有信息都将复制到.cpp中,并可以自动提取(如www.lazycplusplus.com所示-免责声明:尚未使用)。问题在于编译速度,C#&Java已证明这是可以克服的。

2
由于我不确定我的信息:)发布此为评论,所以我不确定为什么这被投票否决。IMO这是一个很好的问题。;)

1
谢谢...。我也不知道为什么也投票否定了

3
布莱恩:你什么意思是“保留一个空指针”?如您所知,在头文件中声明了C / C ++中的静态分配数据后,需要在.cpp中手动定义它(除非它是文件作用域的)。使用LazyC ++,您只需跳过标头声明并编写实现定义

3
Brian-请参阅我对Tim的回复。在LazyC ++你实际编写类定义(包括任何你想要的填充),你就是不能复制的东西。顺便说一句,您的“预订”是一个不可移植的技巧,您最好选择柴郡猫这个成语。

Answers:


23

我通常在C#和C ++之间切换,而C#中缺少头文件是我最大的烦恼之一。我可以看一下头文件,了解有关类的所有知识,包括什么叫它的成员函数,它们的调用语法等,而无需遍历实现该类的代码页。

是的,我知道部分类和#region,但这并不相同。局部类实际上使问题变得更糟,因为一个类定义分布在多个文件中。就#region而言,它们似乎从来没有按照我现在想要的方式进行扩展,因此我必须花时间扩展那些小的优点,直到我正确地看待。

也许如果Visual Studio的intellisense对于C ++更好用,我就没有令人信服的理由不得不如此频繁地引用.h文件,但是即使在VS2008中,C ++的intellisense也无法触及C#。


146
您正在完成机器的工作,以提取文档并喜欢它。哇...

25
在Java / C#中,您可以使用Interfaces来显示api。与头文件相比,这是显示API的一种更好的方法。单独的类文件用作实现。
KitsuneYMG

12
是的,但无需费劲,C ++头文件将实现细节和公共合同混合在一起。
2009年

20
让我们看看您使用标头来检查std :: vector的接口...或任何其他std集合类...还是在boost中完全没有任何东西...所有这些对于此目的都是完全无用的。
罗曼·斯塔科夫

18
任何适当的IDE都会为您提供有关类和方法所需的所有信息。由于您提到的是VS2008,因此使用对象浏览器可以提供完整的文档以及轻松的搜索和浏览。您还需要什么?您认为浏览头文件页面更容易吗?
塔里克


33

头文件允许独立编译。您无需访问甚至不需要实现文件即可编译文件。这可以简化分布式构建。

这也使SDK的制作变得容易一些。您可以只提供标题和一些库。当然,还有其他语言可以使用的解决方法。


13
显然,轻松并不是标题的原因,因为:(1)它们难以维护,它们是重复的代码,(2)对于SDK,可以自动提取它们。我认为分布式构建是正确的观点。

2
您可以轻松进行独立编译而无需头文件。
比尔K

11
我认为C ++标头不会将实现与接口分开。为了获得真正的分离,您必须使用PIMPL或类似的摘要。
KitsuneYMG

1
尽管从理论上讲您是正确的-如果头文件很轻并且仅声明接口-在实践中,头文件很繁重,并且带有条件编译项。
Randolpho

3
标头以一种相当原始的方式启用了单独的编译和SDK,但是不需要它们来支持这些内容-.NET和Java在不使用标头的情况下都支持。
迈克尔·伯

32

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

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


5
如果Stroustrup先生发明了头文件,我会更加重视。然后,他会承认自己的错误,而不是对他明确指出的语言保持口头兼容的目标。这就像决定做一个章鱼,然后抱怨要戴多少只触手一样。
克里斯·卢茨

如果Stroustrup先生可以编写编译器,我会更认真地对待他。他可以将所需的任何信息放入目标文件中,而不管格式或剥离方式如何。
kmarsh

2
C ++标头很难看。在编写良好的C代码中,标头很棒-源文件中的私有实现细节是真正的私有,并且完全不会公开给任何人。
2009年

3
如果Stroustrup先生摆脱了头文件,我会更加重视。
夸克

25
@克里斯·卢茨(Chris Lutz),喀什(Kmarsh),夸克(Quark)。您正在谈论(令人难以置信的是)一个在大多数人出生之前就开发过C ++的人。
09ToedSloth

17

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

实际上,这是个好主意。创建C时,C和Unix是一对。C移植了Unix,Unix运行了C。通过这种方式,C和Unix可以在平台之间迅速传播,而基于程序集的OS必须完全重写才能移植。

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

这些项目,指针,头文件等……实际上并没有提供优于其他系统的任何优势。通过将更多的精力投入到编译器中,您可以像完全相同的对象代码的指针一样轻松地编译参考对象。这就是C ++现在要做的。

C是一种很棒的简单语言。它的功能集非常有限,您无需费力即可编写编译器。移植通常很简单!我并不是要说这是一种不好的语言,而是C语言的主要目标,可能是它在创建时的主要目标可能会残留现在或多或少不必要的语言,但为了兼容起见,它们会保留下来。


似乎有些人真的不相信C是写给Unix的端口,所以在这里:(from

UNIX的第一个版本是用汇编语言编写的,但是Thompson的意图是将它用高级语言编写。

汤普森(Thompson)于1971年首次尝试在PDP-7上使用Fortran,但第一天就放弃了。然后,他写了一种非常简单的语言,称为B,后来在PDP-7上使用。它起作用了,但是有问题。首先,由于对实现进行了解释,因此它总是很慢。其次,基于面向字的BCPL的B的基本概念对于像新的PDP-11这样的面向字节的机器不合适。

里奇(Ritchie)使用PDP-11向B添加类型,有一段时间,B被称为“ New B”的NB,然后他开始为其编写一个编译器。Ritchie说:“因此C的第一阶段实际上是这两个阶段的短暂继承,首先是B的某些语言更改,实际上是在不对语法进行太多更改的情况下添加了类型结构;然后进行了编译器。”

他谈到用C重写UNIX时说:“第二阶段比较慢。” Thompson于1972年夏天开始,但是有两个问题:弄清楚如何运行基本的协同程序,即如何将控制从一个进程切换到一个进程。另一个; 由于原始版本的C没有结构,因此很难获得正确的数据结构。

里奇说:“事情的结合使肯在夏天放弃了。” “一年来,我添加了结构,并可能使编译器代码更好一些-更好了,因此在第二个夏天,那时我们共同努力,实际上用C重做了整个操作系统。”


这是我的意思的完美例子。从评论:

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

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

问题?为了像指针一样高效地实现数组,您必须向编译器添加大量的代码。

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

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

需要花很多精力(在编译器部分),以排除显式的i + src和i + dest添加项,并使其创建与此相同的代码:

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

在事实之后排除该变量“ i”是HARD。新的编译器可以做到这一点,但那时还不可能,并且在如此糟糕的硬件上运行的OS几乎不需要这样的优化。

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


5
指针仅仅是为了使编写编译器更容易?不会。指针之所以存在,是因为它们是对间接概念最简单的抽象。
亚当·罗森菲尔德

2
Pascal使编写编译器变得容易。使用简单的手工编码递归下降解析器,您几乎可以逃脱。C更复杂。
David Thornley,2009年

7
任何说编写C编译器很简单的人都从未编写过C编译器。

2
C是在操作系统中替换汇编语言的早期尝试。D&R希望能够在硅上进行实际编程。这就是为什么指针而不是其他抽象的原因。他们想要一种清晰的方法来精确地完成他们在汇编器中要做的事情。
大卫·桑利

2
关于Pascal,我的观点是,如果主要的C设计目标是易于实现的,那么K&R可以并且将会做得更好。这并不困难。
David Thornley,2009年

17

《 C ++的设计和演进》中,Stroustrup提出了另一个原因...

同一头文件可以具有两个或多个实现文件,这些文件可以由一个以上的程序员同时进行处理,而无需源代码控制系统。

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


1
即使使用源代码控制系统,处理单独的文件也可以确保简化合并。
kmarsh

10
同一头文件的+1可以具有两个或多个实现文件。这是在不滥用#ifdef-fu的情况下进行便携式应用程序的天赐之物。(只需使用myclass.o从依赖于平台的myclass-win.cpp/生成正确的makefile myclass-linux.cpp,而外部只是在乎myclass.h
Steve Schnepp

这在行为上是奇怪的,因此今天不应该再讨论。它的历史。
retif

但这并不能说明为什么需要标头。即使该语言没有头文件,您仍然可以处理两个实现文件。
安德鲁·帕尔默

@安德鲁:这是公认答案的补充。您可以为相同的接口使用单独的实现,而带有声明的标头被认为是实现此目标的一种好方法,而工程师则同时致力于实现。
阿布耶

14

如果您想要不带头文件的C ++,那么对您来说是个好消息。

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

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


2
无论是否使用D,D的关键在于它具有C ++的大多数功能集。您可能会争辩说C ++需要头文件,因为模板需要它们,或者因为在有效代码方面您需要某种方式。D的存在证明这两个都是错误的。
夸克

C ++是系统级语言。D不是。
法肯教授

即使它不能回答问题,此评论对我也真的很有帮助。
西蒙(Simon)

8

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

由于C ++是目前设计的,因此您需要原型(就像在C中一样)来静态检查引用外部函数和类的任何已编译代码。如果没有头文件,则在使用它们之前必须先键入这些类定义和函数声明。为了使C ++不使用头文件,您必须使用一种语言来支持Javaimport关键字之类的功能。那将是一个重要的补充,并且会改变;回答您是否可行的问题:我不这么认为-完全没有。



2

嗯,由于向后兼容性,C ++本身不应该消除头文件。但是,我认为它们总体上是一个愚蠢的想法。如果要分发封闭源库,则可以自动提取此信息。如果您想了解如何不使用类来查看实现,那就是文档生成器的用途,它们可以做得更好。


2

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

它可以通过接口来完成,但是如果您走那条路,那么您就隐含地说类在将实现与契约分离方面是不足的。

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

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

头文件令人费解,因为头文件表达了实现细节(例如私有变量)。

在转向Java和C#时,我发现如果一种语言需要IDE支持开发(例如,公共类接口可以在类浏览器中导航),那么这也许是一条声明,说明代码并不具有自身的优点,因为特别可读。

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

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


2

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

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

为新想法而编辑:

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


C ++需要一种方法来了解内存中的类布局,并不意味着它必然需要头文件。头文件是C ++获取信息的当前机制,而不是唯一可能的机制。
andref

2

这种分离的一个优点是,它很容易,仅查看界面,而不需要一个先进的编辑器


1

没有头文件,不存在任何语言。这是一个神话。

查看Java的任何专有库发行版(我没有C#经验,但是我希望它是相同的)。他们没有给您完整的源文件。他们只是给您一个文件,每个方法的实现都被遮盖({}{return null;}类似内容),并且隐藏起来可以摆脱一切。除了标题,您什么都不能叫。

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


Java编译器从.class文件中提取该信息。不仅如此:Sun JDK随附了大多数类的源代码(com.sun。*中的类除外)。
andref

@andref同意,大多数现代语言(通常是动态语言)都不带有正式的头文件
tomsoft 2014年

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

您会发现,如果拥有一个用于获取密码列表的功能,一个实现了加密算法的功能,一个可以公开操作系统内部的功能,或一个覆盖特权的功能,那将是一件非常糟糕的事情,等等


-1

哦,是的!

用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)轻松地在命令行上添加定义,并为定义名称使用相同的命名法。


2
完全错过了分离头文件和源文件的要点(嗯,差不多)。如果这样做,则更改ClassA的实现将导致ClassA.cpp的所有用户都需要重新编译(除非您的构建系统确实很聪明,这本身就需要花费成本)。
Mankarse'5

@Mankarse在类之间使用接口时(例如在Java或C#中),更改类的实现对编译依赖关系影响很小。
文森特

-3

我仍然不明白某些说法。将API和实现分开是一件好事,但是头文件不是API。那里有私人领域。如果添加或删除私有字段,则更改实现而不是API。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.