1

我有一个关于扩展已使用的标头、源和对象的问题。在理解我的意思之前,你只需要接受我想使用这个设计:

在我的一个项目中,我只在头文件中使用函数声明,并且对于每个定义,我使用一个单独的源文件,它将编译成一个单独的目标文件。

假设我在目录“src”中有一个名为 List 的非常简单的类。

标头可能如下所示:

文件: src/List.hpp

//[guard]
//[includes]

class List {
    void add(int value);
    void remove(int index);
    void clear();
};

现在这三个函数将具有单独的文件:

文件: src/List/add.cpp

void List::add(int value) {
    // Do something
}

想象另一个2。

这些将在某个时刻被编译,并且头文件其他编译的类中使用。

假设另一个名为 ABC 的类使用 List 的头文件。对于 ABC 类中的每个函数,都会生成一个目标文件。

现在我们要调整List的表头,我们不想改变一个函数,我们只想添加一个函数:

文件: src/List.hpp

//[guard]
//[includes]

class List {
    void add(int value);
    int find(int value);
    void remove(int index);
    void clear();
};

所以正在生成另一个源文件和目标文件,在这个例子中被称为:src/List/find.cpp 和 src/List/find.o

现在我的问题是,这是使用标头、源和对象的合法方式吗?这会产生问题,还是根本不可能?

另外,ABC 类中名为 List 的类是否仍与新创建的名为 List 的类相同?

4

3 回答 3

1

你的设计似乎可行。但是,我不会推荐它。而且您没有提到模板或标准容器。

我的感觉是

  • 实际上很重要(出于效率原因),有很多(通常很小)inline函数,特别是内联成员函数(如 getter、setter 等),通常包含在它们的class Class { ....}定义中。

  • 因此,一些成员函数应该是inline,或者在类内部

    class Foo { 
      int _x;
      Foo(int x) : _x(x) {};
      ~Foo() { _x=0; };
      int f(int d) const { return _x + d; };
    }
    

然后所有的构造函数Foo::Foo(int)、析构函数Foo::~Foo和成员函数int Foo::f(int)都被内联

或在课程之后(对于机器生成的代码通常更容易),例如

    class Foo {
       int _x;
       inline Foo(int x);
       inline ~Foo();
       inline int f(int d) const;
    };

    Foo::Foo(int x) { _x = x; };
    Foo::~Foo() { _x = 0; };
    int Foo::f(int d) const { return _x+d; };

在这两种情况下,出于效率原因,您都需要内联(或者可能是链接时间优化,例如gcc -flto -O用于编译和链接)。

编译器只有在知道函数的定义(它们的主体)时才能内联函数。

  • 然后,每次你#include定义一些类。您需要以某种方式编译该内联函数定义。要么将它放在同一个标​​题中,要么该标题本身应该是#include其他文件(提供内联函数的定义)

  • 通常,尤其是在使用标准 C++ 库(以及使用标准容器,例如#include <vector>)时,您会得到很多系统头文件(间接包括在内)。在实践中,您不需要非常小的实现文件(即每个文件有几十行源代码是不切实际的)。

  • 同样,现有的 C++ 框架库会提取大量(间接)标头(例如#include <QtGui>,带来大量代码)。

  • 我建议至少拥有一千行的C++ 源文件(*.hh或者)。*.cc

看看预处理代码的大小,例如g++ -H -C -E……在实践中你会感到害怕:即使编译一个只有几十行源代码的小 C++ 文件,你也会有成千上万的预处理源代码行。

因此,我对千行源文件的建议:任何使用 C++ 标准库或某些 C++ 框架库(Boost、Qt)的较小文件都会从间接包含的文件中提取大量源代码行。

另请参阅此答案,为什么 Google(使用 D.Novillo)努力将预解析的标头添加到 GCC,为什么 LLVM/Clang(使用 C.Latner)需要 C 和 C++ 中的模块。以及为什么 Ocaml、Rust、Go ……有模块……

您还可以查看 GCC 生成的 GIMPLE 表示,或者使用MELT 探针(MELT 是一种扩展 GCC 的特定领域语言,探针是一个简单的图形界面,用于检查 GCC 的一些内部表示,例如 Gimple),或者使用-fdump-tree-all选项GCC(小心:该选项会产生数百个转储文件)。您也可以将-ftime-report传递给 GCC,以便在编译 C++ 代码时更多地了解它在哪里浪费时间。

对于机器生成的 C++ 代码,我建议生成更少的文件,但要使它们更大。生成数千个几十行的小 C++ 文件效率低下(使总构建时间太长):编译器将花费大量时间#include一次又一次地解析相同的 -d 系统头文件,并实例化相同的模板化类型(例如,当使用标准容器时)很多次。

请记住,C++ 允许每个源文件有多个类(与 Java 相反(内部类除外))。

此外,如果生成了所有 C++ 代码,则您实际上不需要生成头文件(或者您可能会生成一个 big *.hh),因为您的生成器应该知道在每个生成中真正使用了哪些类和函数,*.cc并且可以在其中生成只归档那些有用的声明和内联函数定义。

PS:请注意,inline(like register) 只是对编译器的一个(有用的)提示。它可以避免内联标记的函数inline(即使是隐含的,在class定义内部时)。它也可能内联一些未标记的函数inline。但是,编译器需要知道函数的主体才能内联它。

于 2013-05-18T11:11:39.737 回答
1

我相信一个函数是否被内联是由编译器决定的(见问题我将如何知道内联函数是否在它被调用的地方被实际替换?)。要内联一个函数(尽管这并不一定意味着该函数在编译时绝对会被内联),您应该在其类中定义该函数,或者在头文件中在它之外定义您的函数之前使用命令“内联”。例如 :

inline int Foo::f(int d) const { return _x+d; };
于 2013-05-18T12:10:48.090 回答
0

是的,这很好用。这就是静态库的实现方式,因为这使链接器更容易不拉入未使用的东西。

于 2013-05-18T15:38:00.473 回答