在C ++中分离声明和定义有什么优缺点?

时间:2009-03-14 11:18:22

标签: c++ language-design declaration definition

在C ++中,函数,变量和常量的声明和定义可以像这样分开:

function someFunc();

function someFunc()
{
  //Implementation.
}

事实上,在类的定义中,通常就是这种情况。通常使用.h文件中的成员声明类,然后在相应的.C文件中定义这些类。

有什么优点?这种方法的缺点是什么?

11 个答案:

答案 0 :(得分:14)

历史上这是为了帮助编译器。在使用它们之前,你必须给它一个名单 - 无论是实际用法,还是前向声明(C的默认功能原型)。

现代语言的现代编译器表明这不再是必需品,因此C&这里的C ++(以及Objective-C,可能还有其他)语法是组织上的包袱。事实上,这是C ++的一个大问题,即使添加适当的模块系统也无法解决。

缺点是:许多重度嵌套的包含文件(我之前跟踪过包含树,它们非常庞大)以及声明和定义之间的冗余 - 所有这些都导致更长的编码时间和更长的编译时间(比较之间的编译时间)可比较的C ++和C#项目?这是差异的原因之一)。必须为您提供的任何组件的用户提供头文件。 ODR违规的可能性。依赖于预处理器(许多现代语言不需要预处理器步骤),这使得您的代码更加脆弱,并且难以解析工具。

优点:不多。您可能会争辩说,为了文档目的,您可以在一个地方获得一组功能名称 - 但是大多数IDE现在都具有某种代码折叠功能,而且任何规模的项目都应该使用doc生成器(例如doxygen)。使用更清晰,无预处理器,基于模块的语法,工具可以更轻松地遵循您的代码并提供更多,因此我认为这种“优势”只是没有实际意义。

答案 1 :(得分:11)

这是C / C ++编译器如何工作的人工制品。

当源文件被编译时,预处理器将每个#include-statement替换为包含文件的内容。之后,编译器才会尝试解释此连接的结果。

编译器然后从头到尾检查结果,尝试验证每个语句。如果一行代码调用之前未定义的函数,它将放弃。

但是,当涉及到相互递归的函数调用时,存在一个问题:

void foo()
{
  bar();
}

void bar()
{
  foo();
}

此处foo将无法编译,因为bar未知。如果您切换两个函数,bar将无法编译,因为foo未知。

如果您单独声明和定义,您可以按照自己的意愿订购这些功能:

void foo();
void bar();

void foo()
{
  bar();
}

void bar()
{
  foo();
}

这里,当编译器处理foo时,它已经知道一个名为bar的函数的签名,并且很高兴。

当然编译器可以以不同的方式工作,但这就是它们在C,C ++和某种程度上的Objective-C工作方式。

缺点:

没有直接。如果你正在使用C / C ++,那么这是做事的最佳方式。如果你有一个语言/编译器可供选择,那么也许你可以选择一个不是问题的地方。将声明拆分为头文件时唯一要考虑的是避免相互递归的#include-statements - 但这就是包含守卫的内容。

优点:

  • 编译速度:由于所有包含的文件被连接在一起然后被解析,减少包含文件中的代码数量和复杂性 将缩短编译时间。
  • 避免代码重复/内联:如果您在头文件中完全定义了一个函数,那么包含此头文件并引用此函数的每个目标文件都将包含它自己的该函数版本。另外,如果您想要内联,则需要将完整定义放入头文件中(在大多数编译器上)。
  • 封装/清晰度:一个定义良好的类/函数集以及一些文档应该足以让其他开发人员使用您的代码。 (理想情况下)他们不需要理解代码是如何工作的 - 那么为什么要求他们筛选代码呢? (当然,反对它可能对他们访问实现有用的仍然有用)。

当然,如果你根本不想公开一个函数,你通常仍然可以选择在实现文件而不是标题中完全定义它。

答案 2 :(得分:3)

将声明和定义分离为C ++头文件和源文件有两个主要优点。首先,当您的班级/职能/任何#include d在多个地方时,您可以避免One Definition Rule出现问题。其次,通过这种方式,您可以分离界面和实现。您的类或库的用户只需要查看头文件以编写使用它的代码。您还可以使用Pimpl Idiom更进一步,并使其在每次库实现更改时都不必重新编译用户代码。

您已经提到了.h和.cpp文件之间代码重复的缺点。也许我已经编写了很长时间的C ++代码,但我认为 不好。每次更改函数签名时都必须更改所有用户代码,那么还有一个文件是什么?当你第一次写一个类而且你必须从头文件中复制并粘贴到新的源文件时,这只会很烦人。

实践中的另一个缺点是,为了编写(和调试!)使用第三方库的优秀代码,您通常必须在其中查看。这意味着即使您无法更改源代码,也可以访问源代码。如果您拥有的只是头文件和编译的目标文件,则很难确定该错误是您的错还是他们的错。此外,查看源代码可让您深入了解如何正确使用和扩展文档可能无法涵盖的库。不是每个人都有他们的库MSDN。优秀的软件工程师有一种讨厌的习惯,就是用你从未梦想过的代码做事。 ; - )

答案 3 :(得分:2)

标准要求在使用函数时,声明必须在范围内。这意味着,编译器应该能够针对原型(头文件中的声明)验证您传递给它的内容。当然,对于可变参数的函数 - 这些函数不验证参数。

想想C,当不需要时。那时,编译器没有将返回类型规范视为默认为int。现在,假设你有一个函数foo()返回一个指向void的指针。但是,由于您没有声明,编译器会认为它必须返回一个整数。例如,在一些摩托罗拉系统上,integeres和指针将在不同的寄存器中返回。现在,编译器将不再使用正确的寄存器,而是将指针强制转换为另一个寄存器中的整数。你尝试使用这个指针的那一刻 - 所有的地狱都会破裂。

在标题中声明函数很好。但请记住,如果您在标头中声明并定义,请确保它们是内联的。实现此目的的一种方法是将定义放在类定义中。否则添加inline关键字。当标题包含在多个实现文件中时,您将遇到ODR违规。

答案 4 :(得分:1)

你基本上有两个关于类/函数/看法的视图:

声明,声明名称,参数和成员(在结构/类的情况下),以及定义函数功能的定义。

其中一个缺点是重复,但一个很大的优点是你可以将你的函数声明为int foo(float f)并将细节留在实现(=定义)中,所以任何想要使用你的函数foo的人只包括你的头文件和指向库/目标文件的链接,因此库用户和编译器只需要关心定义的接口,这有助于理解接口并加快编译时间。

答案 5 :(得分:1)

我还没有看到的一个优势: API

任何非开源(即专有)的库或第三方代码都不会与分发一起实施。大多数公司都不愿意放弃源代码。简单的解决方案,只需分发允许使用DLL的类声明和函数签名。

免责声明:我不是说这是对的,错的还是有道理的,我只是说我已经看过很多了。

答案 6 :(得分:0)

<强>优势

只需包含声明,就可以从其他文件中引用类。然后可以在编译过程中将定义链接起来。

答案 7 :(得分:0)

前向声明的一大优点是,在仔细使用时,可以减少模块之间的编译时依赖性。

如果ClassA.h需要引用ClassB.h中的数据元素,您通常只能在ClassA.h中使用前向引用并在ClassA.cc中包含ClassB.h而不是ClassA.h中的ClassB.h,从而减少编译时依赖。

对于大型系统,这可以节省大量时间。

答案 8 :(得分:0)

  1. 分离可以提供干净,整洁的程序元素视图。
  2. 可以在不透露来源的情况下创建和链接到二进制模块/库。
  3. 链接二进制文件而不重新编译源。

答案 9 :(得分:0)

如果正确完成,只有实现发生变化时,这种分离会缩短编译时间。

答案 10 :(得分:-1)

<强>缺点

这导致了很多重复。大多数功能签名需要放在两个或更多(如Paulious所说)的地方。