应该C ++消除头文件?
许多语言,比如Java,C#,都没有将声明与实现分开。 C#有一个部分类的概念,但是实现和声明仍然保留在同一个文件中。
为什么C ++没有相同的模型? 有头文件更实际吗?
我指的是当前和即将到来的C ++标准版本。
我经常在C#和C ++之间翻转,而C#中缺less头文件是我最大的宠儿之一。 我可以看一个头文件,了解我需要了解的一个类的所有信息 – 成员函数被调用的内容,调用语法等 – 而不必通过实现该类的代码的页面。
是的,我知道部分类和#区域,但它是不一样的。 部分类实际上使问题变得更糟,因为类定义分布在多个文件中。 就地区而言,他们似乎从未像现在这样以我想要的方式进行扩展,所以我必须花时间来扩展这些小的优势,直到我认为正确。
也许如果Visual Studio的intellisense为C ++工作的更好,我不会有一个令人信服的理由必须经常引用.h文件,但即使在VS2008中,C ++的智能感知也无法触及C#
向后兼容性 – 头文件不会被消除,因为它会中断向后兼容性。
头文件允许独立编译。 您不需要访问甚至不需要实现文件来编译文件。 这可以使分布式构build更容易。
这也使SDK可以更容易一些。 您可以只提供标题和一些库。 当然,有其他语言使用的方法。
即使Bjarne Stroustrup已经调用头文件kludge。
但是,如果没有包含必要的元数据(如Java类文件或.Net PE文件)的标准二进制格式,我没有看到任何方法来实现该function。 剥离的ELF或a.out二进制文件没有很多您需要提取的信息。 而且我不认为这些信息是存储在Windows XCOFF文件中的。
在C ++的devise和演变中 ,Stroustrup给出了另一个理由…
相同的头文件可以有两个或更多的实现文件,可以被多个程序员同时处理,而不需要源控制系统。
这些日子可能看起来很奇怪,但我猜这是C ++发明时的一个重要问题。
如果你想C ++没有头文件,那么我对你有好消息。
它已经存在,被称为D( http://www.digitalmars.com/d/index.html )
从技术上讲,D似乎比C ++好得多,但目前在许多应用程序中还不够主stream。
C是编写一个编译器很容易。 它根据这个原则做了很多东西。 指针的存在只是为了使编写一个编译器更容易,就像头文件一样。 C ++所带来的许多function都是基于与实现这些function的兼容性,从而使编译器的编写变得更加容易。
实际上这是个好主意。 当C被创build时,C和Unix是一对。 C移植Unix,Unix移植C,这样C和Unix就可以迅速从平台扩展到平台,而基于程序集的操作系统则需要完全重写才能移植。
在一个文件中指定接口的概念和在另一个文件中的实现并不是一个坏主意,但这不是C头文件。 它们只是一种限制编译器通过源代码进行传递的次数的方法,并允许对文件之间的合同进行一些有限的抽象,以便它们可以进行通信。
这些项目,指针,头文件等…实际上并没有比另一个系统提供任何优势。 通过把更多的精力放到编译器中,你可以像引用完全相同的目标代码那样容易地编译引用对象。 这就是C ++现在所做的。
C是一个伟大的,简单的语言。 它有一个非常有限的function集,你可以写一个编译器没有太多的努力。 移植它通常是微不足道的! 我并不是想说这是一种糟糕的语言或任何东西,只是C创build时的主要目标可能会留下语言中的残余,现在或多或less都是不必要的,但是为了兼容性,它们将会被保留下来。
似乎有些人并不真正相信C被写入Unix端口,所以在这里:( 从 )
UNIX的第一个版本是用汇编语言编写的,但汤普森的意图是用高级语言编写。
汤普森在1971年第一次尝试在PDP-7上使用Fortran,但在第一天后放弃了。 然后他写了一个他称之为B的简单语言,他开始使用PDP-7。 它工作,但有问题。 首先,由于解释的实施,总是会变得缓慢。 其次,基于面向字的BCPL的B的基本概念对于像新的PDP-11这样的面向字节的机器来说是不对的。
Ritchie使用PDP-11为B添加了一些types,有一段时间被称为NB为“New B”,然后他开始为它编写一个编译器。 “所以C的第一阶段实际上就是这两个阶段的短期继续,首先,一些语言从B变化,实际上,增加了语法上没有太多变化的types结构,并且编译器,”Ritchie说。
他说:“第二阶段是比较慢的。”汤普森在1972年夏天开始重新编写UNIX。但是有两个问题:弄清楚如何运行基本的协同例程,即如何将控制从一个进程切换到另一个; 并且由于C的原始版本没有结构,因此难以获得正确的数据结构。
里奇说:“事情的结合导致肯在夏天放弃。 “在这一年里,我增加了结构,并且可能使编译器代码稍微好一些 – 更好的代码 – 到明年夏天,那就是我们一起努力,实际上用C重做整个操作系统。
这是我的意思的完美例子。 来自评论:
指针只存在于编写一个编译器更容易? 指针是存在的,因为它们是对间接思想的最简单的抽象。 – 亚当·罗森菲尔德(一个小时前)
你是对的。 为了实现间接,指针是最简单可能的抽象实现。 他们绝不可能理解或使用最简单的方法。 数组要容易得多。
问题? 为了像指针一样高效地实现数组,你必须为你的编译器添加一大堆代码。
没有理由,他们不能没有指针deviseC,但与这样的代码:
int i=0; while(src[++i]) dest[i]=src[i];
编译器部分需要付出很多的努力来分解显式的i + src和i + dest,并且创build相同的代码:
while(*(dest++) = *(src++)) ;
在事实之后将variables“i”分解为HARD。 新的编译器可以做到这一点,但是当时这是不可能的,运行在那些蹩脚的硬件上的操作系统需要很less的优化。
现在很less有系统需要这种优化(我在最慢的平台之一 – 有线机顶盒上工作,而我们大部分的东西都是用Java编写的),在极less数情况下,你可能需要它,新的C编译器应该足够聪明,可以自行完成这种转换。
C ++的目标之一是成为C的超集,如果它不支持头文件,就很难这样做。 另外,如果你希望删除头文件,你也可以考虑把CPP(预处理器,而不是plus plus)完全删除。 C#和Java都没有用macros标准来指定macros预处理器(但是在某些情况下应该注意它们甚至可以用这些语言来使用)。
由于C ++是现在devise的,你需要原型 – 就像在C中一样 – 静态地检查任何引用外部函数和类的编译代码。 没有头文件,你必须在使用它们之前input这些类定义和函数声明。 对于C ++不使用头文件,你必须添加一个支持Java import
关键字之类语言的特性。 这将是一个重大的增加,并改变; 回答你是否实际的问题:我不这么认为 – 根本不是。
很多人都知道头文件的缺点,并且有一些想法可以将更强大的模块系统引入到C ++中。 你可能想看看Daveed Vandevoorde 在C ++中的Modules(Revision 5) 。
这种分离的一个优点是,只需查看界面, 而不需要高级编辑器就很容易。
那么,C ++本身不应该因为向后兼容性而消除头文件。 但是,我认为他们总体上是一个愚蠢的想法。 如果你想分发一个封闭的源代码库,这个信息可以自动提取。 如果你想了解如何使用一个没有查看实现的类,那么这就是文档生成器的用途,而且他们做的更好。
在实现文件的单独组件中定义类接口是有价值的。
这可以通过界面来完成,但是如果沿着这条路走下去,那么你就隐含地说,阶级在实施与合同分离方面是不足的。
Modula 2有正确的想法,定义模块和实现模块。 http://www.modula2.org/reference/modules.php
Java / C#的答案是相同的隐式实现(尽pipe面向对象)。
头文件是一个kludge,因为头文件expression实现细节(如私有variables)。
在转向Java和C#时,我发现如果一种语言需要IDE的开发支持(例如,公共类接口可以在类浏览器中导航),那么这可能是一个声明,代码并不是站在自己的优点特别可读。
我发现接口与实现细节的混合非常可怕。
至关重要的是,缺乏将公共类签名logging在一个独立于实现的简洁,好评的文件中的能力表明,语言devise是为了方便撰写而编写的,而非便于维护。 那么我现在正忙于Java和C#。
如果你想要这个永远不会发生的原因:它会打破几乎所有现有的C ++软件。 如果你看一些C ++委员会的devise文档,他们会看看各种替代scheme,看看它会破坏多less代码。
把开关语句变成半智能的东西要容易得多。 这只会打破一点点的代码。 这仍然不会发生。
编辑为新的想法:
使C ++头文件成为必要的C ++和Java的区别在于C ++对象不一定是指针。 在Java中,所有的类实例都是通过指针引用的,尽pipe看起来并不是这样。 C ++具有在堆和堆栈上分配的对象。 这意味着C ++需要知道对象将有多大以及数据成员在内存中的位置。
没有头文件没有语言存在。 这是一个神话。
查看任何专有的Java库发行版(我没有C#经验可言,但我期望它是相同的)。 他们不给你完整的源文件; 他们只是给你一个文件,每一个方法的执行空白( {}
或{return null;}
或类似的东西),他们可以隐藏隐藏的一切。 除了标题,你不能调用任何东西。
然而,没有任何技术上的原因,为什么一个C或C ++编译器可以将一个标记为合适的文件中的所有内容都作为extern
来计数,除非该文件是直接编译的。 但是,编译的代价是巨大的,因为C和C ++都不是快速parsing的,这是非常重要的考虑因素。 任何更复杂的头文件和源代码融合方法都会很快遇到技术问题,比如编译器需要知道对象的布局。
头文件是该语言的一个组成部分。 没有头文件,所有静态库,dynamic库,几乎任何预编译的库变得没用。 头文件也可以更容易地logging所有内容,并且可以查看库/文件的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
。
如果你有一个获取密码列表的函数,或者一个实现了你的encryptionalgorithm的函数,或者一个暴露操作系统内部函数的函数,或者一个覆盖特权的函数,你会发现这可能是一件非常糟糕的事情,等等
哦,是的!
在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"
当然,在命令行中添加define可以很容易地通过macros扩展(Visual Studio编译器)或自动variables(gnu make)添加,并使用相同的命名法定义名称。
尽pipe如此,我还是不明白一些说法。 API和实现的分离是非常好的事情,但是头文件不是API。 那里有私人领域。 如果添加或删除私有字段,则更改实现,而不是API。