11

我喜欢组织我的代码,所以理想情况下我希望每个文件一个类,或者当我有非成员函数时,每个文件一个函数。

原因是:

  1. 当我阅读代码时,我总是知道应该在哪个文件中找到某个函数或类。

  2. 如果每个头文件是一个类或一个非成员函数,那么我 include在头文件时不会包含一团糟。

  3. 如果我对一个函数做一个小的改动,那么只有那个函数需要重新编译。

但是,将所有内容拆分为许多头文件和许多实现文件会大大减慢编译速度。在我的项目中,大多数函数访问一定数量的模板化其他库函数。这样代码将被一遍又一遍地编译,每个实现文件一次。目前在一台机器上编译我的整个项目需要 45 分钟左右。大约有 50 个目标文件,每个目标文件都使用相同的昂贵编译头文件。

也许,每个头文件有一个类(或非成员函数)是否可以接受,但是将许多或所有这些函数的实现放入一个实现文件中,如下例所示?

// foo.h
void foo(int n);

// bar.h
void bar(double d);

// foobar.cpp
#include <vector>
void foo(int n) { std::vector<int> v; ... }
void bar(double d) { std::vector<int> w; ... }

同样,优点是我可以仅包含 foo 函数或仅包含 bar 函数,并且整个项目的编译会更快,因为foobar.cpp一个文件,所以std::vector<int>(这只是其他一些昂贵的示例compile 模板化构造)必须只编译一次,而不是如果我单独编译 afoo.cpp和两次bar.cpp。当然,我上面的理由 (3) 不适用于这种情况:在更改 foo(){...} 之后,我必须重新编译整个可能很大的 file foobar.cpp

我很好奇你的意见是什么!

4

9 回答 9

10

恕我直言,您应该将项目组合成逻辑分组并基于此创建文件。

当我编写函数时,通常有六个左右紧密相关的函数。我倾向于将它们放在一个头文件和实现文件中。

当我编写类时,我通常将自己限制为每个头文件和实现文件一个重量级类。我可能会添加一些便利功能或小型辅助类。

如果我发现一个实现文件长达数千行,这通常表明那里有太多内容,我需要将其分解。

于 2009-02-10T05:25:06.570 回答
8

在我看来,每个文件的一个功能可能会变得混乱。想象一下,如果 POSIX 和 ANSI C 标头以相同的方式制作。

#include <strlen.h>
#include <strcpy.h>
#include <strncpy.h>
#include <strchr.h>
#include <strstr.h>
#include <malloc.h>
#include <calloc.h>
#include <free.h>
#include <printf.h>
#include <fprintf.h>
#include <vpritnf.h>
#include <snprintf.h>

不过,每个文件一个类是个好主意。

于 2009-02-10T05:34:16.300 回答
4

我们使用每个文件一个外部函数的原则。但是,在这个文件中,可能有几个其他“帮助”函数位于未命名的命名空间中,用于实现该函数。

根据我们的经验,与其他一些评论相反,这有两个主要好处。首先是构建时间更快,因为模块只需要在修改其特定 API 时重新构建。第二个优点是通过使用通用命名方案,无需花时间搜索包含您希望调用的函数的标头:

// getShapeColor.h
Color getShapeColor(Shape);

// getTextColor.h
Color getTextColor(Text);

我不同意标准库是每个文件不使用一个(外部)函数的一个很好的例子。标准库永远不会改变并且具有明确定义的接口,因此上述两点均不适用于它们。

话虽如此,即使在标准库的情况下,拆分单个函数也有一些潜在的好处。首先是编译器可以在使用不安全版本的函数时生成有用的警告,例如strcpystrncpy,其方式类似于 g++ 用于警告包含 <iostream.h> 与 <iostream> 的方式。

另一个优点是,当我想使用memmove时,我不会再因为包含内存而陷入困境!

于 2009-02-10T10:50:05.660 回答
4

如果您正在制作一个静态库,则每个文件一个函数具有技术优势(我想这是像Musl-libc项目这样的项目遵循这种模式的原因之一)。

静态库与目标文件粒度链接,因此如果您有一个libfoobar.a由以下组成的静态库:

 foo.o
     foo1
     foo2
 bar.o
     bar

然后,如果您链​​接该bar函数的库,则bar.o存档成员将被链接,但不是该foo.o成员。如果您为 链接foo1,则该foo.o成员将被链接,从而带来可能不必要的foo2功能。

可能还有其他方法可以防止在 (-ffunction-sections -fdata-sections--gc-sections) 中链接不需要的函数,但每个文件一个函数可能是最可靠的。

还有将少量相关函数/数据对象放在文件中的中间立场。这样,与 -ffunction-sections/-fdata-sections 相比,编译器可以更好地优化符号间引用,并且您仍然可以获得静态库的至少一些粒度。


  • 为了简单起见,我在这里忽略了 C++ 名称修改
于 2016-10-22T15:37:33.950 回答
1

您可以将您的一些函数重新声明为一个或多个类的静态方法:这为您提供了一个机会(也是一个很好的借口)将其中的几个分组到一个源文件中。

如果该源文件与目标文件是一对一的,并且链接器链接整个目标文件,则在一个源文件中具有或不具有多个函数的一个很好的理由是:如果可执行文件可能需要一个函数而不需要另一个函数,则将它们在单独的源文件中(以便链接器可以链接一个而不链接另一个)。

于 2009-02-10T05:34:37.813 回答
1

我还尝试在每个文件的函数中拆分文件,但它有一些缺点。有时函数往往会变得比它们需要的大(你不想每次都添加一个新的 .c 文件),除非你勤奋地重构你的代码(我不是)。

目前,我将一到三个函数放在一个 .c 文件中,并将所有 .c 文件分组为一个目录中的一个功能。对于我拥有的头文件Funcionality.hSubfunctionality.h这样我可以在需要时一次包含所有功能,或者如果不需要整个包,则可以只包含一个小实用程序功能。

于 2009-02-10T05:36:08.527 回答
1

我的一位老编程教授建议每隔几百行代码就分解模块以提高可维护性。我不再使用 C++ 开发,但在 C# 中,我将自己限制为每个文件一个类,只要没有与我的对象无关的文件大小,文件的大小就无关紧要。您可以使用#pragma 区域来优雅地减少编辑器空间,不确定C++ 编译器是否有它们,但如果有,那么肯定会使用它们。

如果我仍在使用 C++ 编程,我会使用每个文件的多个函数按用途对函数进行分组。所以我可能有一个名为“Service.cpp”的文件,其中包含一些定义该“服务”的函数。每个文件拥有一个函数反过来会导致遗憾地以某种方式重新回到您的项目中。

不过,有时并不需要每个文件包含数千行代码。函数本身最多不应该超过几百行代码。永远记住,一个函数应该只做一件事并保持最小。如果一个函数做不止一件事,它应该被重构为辅助方法。

拥有多个定义单个实体的源文件也没有什么坏处。即:'ServiceConnection.cpp''ServiceSettings.cpp',等等。

有时,如果我创建一个对象,并且它拥有其他对象,我会将多个类组合到一个文件中。例如,包含“ButtonLink”对象的按钮控件,我可能会将其组合到 Button 类中。有时我不这样做,但这是“当下的偏好”决定。

做最适合你的事情。在较小的项目上尝试不同的风格会有所帮助。希望这对您有所帮助。

于 2009-02-10T05:48:24.270 回答
1

对于标题部分,您应该将项目组合成逻辑分组并基于此创建您的头文件。恕我直言,这似乎并且非常合乎逻辑。

对于源代码部分,您应该将每个函数实现放在一个单独的文件中(在这种情况下静态函数是例外)。起初这似乎不合逻辑,但请记住,编译器知道函数,但链接器只知道 .o 和 .obj 文件及其导出的符号。这可能会显着改变输出文件的大小,这对于嵌入式系统来说是一个非常重要的问题。

签出 glibc 或 Visual C++ CRT 源代码树...

于 2009-02-10T07:14:38.803 回答
1

我可以看到您的方法有一些优点,但也有几个缺点:

  1. 包括一个包裹是一场噩梦。您最终可以使用 10-20 个包含来获得所需的功能。例如,想象一下如果 STDIO 或 StdLib 是以这种方式实现的。

  2. 浏览代码会有点痛苦,因为通常滚动文件比切换文件更容易。显然,太大的文件很难,但即使使用现代 IDE,也很容易将文件折叠到您需要的内容,并且其中很多都有功能快捷方式列表。

  3. 使文件维护是一种痛苦。

  4. 我是小功能和重构的忠实粉丝。当您增加开销时(创建一个新文件,将其添加到源代码管理,...),它鼓励人们编写更长的函数,而不是将一个函数分成三个部分,您只需制作一个大的。

于 2009-02-10T08:17:55.797 回答