如何以及何时将C++代码拆分为头文件和源文件

29
我有一个小应用程序,全部都在一个文件中。我想将它转换为单独的较小文件。我如何知道如何分离这些内容?代码应该在哪个不可见的边界处分离?
另外,头文件的作用是什么?是为了提前声明方法和类,以便我可以在编译期间链接器包含它们之前在我的代码中使用它们吗?

编程技巧:C/CPP头文件约定在C和C++中组织代码文件 - Dustin Getz
4个回答

28

头文件应包含类和函数声明。

源文件包含类和函数定义。

通常(即更易读)每个头文件只有一个声明,每个源文件只有一个定义,但是对于小的(即更简单的帮助程序)对象,有时会将它们与相关的更实质性的对象分组。

示例:菜单类

Menu.h:     Contains the Menu declaration.
Menu.cpp:   Contains the Menu definition.

头文件包含声明的原因是可以从多个源文件中包含它们,因此每个源文件都具有每个类和函数的完全相同的定义。

可以这样考虑:
如果没有头文件,则需要在每个源文件中拥有类和/或函数声明(不带定义),这意味着每个文件中都有相同的声明副本。因此,如果您修改了一个类,则需要在每个文件中进行相同的修改。通过使用头文件,您可以将声明放在一个地方,因此只需修改一个对象。


1
请注意,struct Menu { int bar; void foo(); }; 是类Menu的定义,但是成员函数foo的声明,而bar对象成员的定义。你的答案听起来像是类Menu的声明。 - Johannes Schaub - litb
同时头文件通常会包含函数定义。例如:“小”成员函数旨在内联;声明为“static inline”的自由函数;函数模板。 - Steve Jessop
这是一个针对初学者的课程,教授如何将代码适当地分散到不同的文件中。 - Martin York
在这种情况下,我会放过你的函数定义,但即使是初学者课程也应该清楚类声明和类定义之间的区别,以及头文件经常包含类定义。 - Steve Jessop

24

首先,您不应该将任何不需要其他文件可见的内容放入头文件中,除非需要它的文件。接下来,让我们定义一些我们需要的东西。

翻译单元

翻译单元是当前正在编译的代码以及直接或间接包含在其中的所有代码。一个翻译单元对应一个.o/.obj文件。

程序

这是将所有.o/.obj文件链接在一起形成的可以执行的二进制文件,用于生成进程。

有不同的翻译单元的主要原因是什么?

  1. 减少依赖关系,这样如果您更改一个类的一个方法,就不必重新编译程序的所有代码,而只需重新编译受影响的翻译单元。
  2. 通过具有翻译单元本地名称来减少可能的名称冲突,在将它们链接在一起时,其他翻译单元无法看到这些名称。

现在,如何将代码拆分为不同的翻译单元?答案是没有“所以你这样做!”但您必须根据情况进行考虑。通常很明显,因为您有不同的类,可以将它们放入不同的翻译单元中:

foo.hpp:

/* Only declaration of class foo we define below. Note that a declaration
 * is not a definition. But a definition is always also a declaration */
class foo;

/* definition of a class foo. the same class definition can appear 
   in multiple translation units provided that each definition is the same  
   basicially, but only once per translation unit. This too is called the  
   "One Definition Rule" (ODR). */
class foo {
    /* declaration of a member function doit */
    void doit();

    /* definition of an data-member age */
    int age;
};

声明一些自由函数和对象:

/* if you have translation unit non-local (with so-called extern linkage)  
   names, you declare them here, so other translation units can include  
   your file "foo.hpp" and use them. */
void getTheAnswer();

/* to avoid that the following is a definition of a object, you put "extern"  
   in front of it. */
extern int answerCheat;

foo.cpp:

/* include the header of it */
#include "foo.hpp"

/* definition of the member function doit */
void foo::doit() {
    /* ... */
}

/* definition of a translation unit local name. preferred way in c++. */
namespace {
    void help() {
        /* ... */
    }
}

void getTheAnswer() {
    /* let's call our helper function */
    help();
    /* ... */
}

/* define answerCheat. non-const objects are translation unit nonlocal  
   by default */
int answerCheat = 42;

bar.hpp:

/* so, this is the same as above, just with other classes/files... */
class bar {
public:
    bar(); /* constructor */
}; 

bar.cpp:

/* we need the foo.hpp file, which declares getTheAnswer() */
#include "foo.hpp"
#include "bar.hpp"

bar::bar() {
    /* make use of getTheAnswer() */
    getTheAnswer();
}

请注意,匿名命名空间中的名称(如上所示)不会发生冲突,因为它们似乎是翻译单元本地的。实际上,它们并不是,只是具有唯一的名称,以避免发生冲突。如果你真的想要(很少有理由这样做),使用翻译单元本地名称(例如,因为要与C兼容,所以C代码可以调用你的函数),你可以像这样做:
static void help() { 
    /* .... */
}

ODR 还规定,在一个程序中不能有超过一个对象或非内联函数的定义(类是类型而不是对象,因此不适用于它们)。 因此,您必须小心,不要将非内联函数放入头文件中,也不要将像“int foo;”这样的对象放入头文件中。当链接器尝试将包括这些头文件的翻译单元链接在一起时,这将导致链接器错误。

希望我能帮到你一点。虽然这是一个很长的答案,但确实存在某些错误。我知道翻译单元的严格定义另有规定(预处理器的输出)。但是,我认为将其包含在上述内容中并没有太大的价值,并且会让事情更加混乱。如果您发现真正的错误,请随时打我 :)


4
决定如何将代码分成不同的类/函数是编程的主要任务之一。有许多不同的指南可以告诉你如何做到这一点,我建议阅读一些关于C++和面向对象设计的教程以帮助你入门。
一些基本的指导原则包括:
  • 把一起使用的东西放在一起
  • 为领域对象(例如文件、集合等)创建类
  • 头文件允许你声明一个类或函数,然后在多个不同的源文件中使用它。例如,如果你在一个头文件中声明了一个类。
    // A.h
    class A
    {
    public:
        int fn();
    };
    

    您可以在多个源文件中使用此类:
    // A.cpp
    #include "A.h"
    int A::fn() {/* implementation of fn */}
    
    //B.cpp
    #include "A.h"
    void OtherFunction() {
        A a;
        a.fn();
    }
    

    所以头文件可以将声明与实现分离。如果您将所有内容(声明和实现)放在一个源文件中(例如A.cpp),然后尝试在第二个文件中包含它,那么……
    // B.cpp
    #include  "A.cpp" //DON'T do this!
    

    如果您尝试编译B.cpp,但在链接程序时出现重复定义对象的错误提示 - 这是因为您有多个A的实现副本。


    -1
    建议: 1. 现在就为您的应用程序准备好设计。 2. 基于设计,创建必要的对象并使它们相互交互。 3. 重构或完全更改现有代码以适应新创建的设计。
    头文件提供了一个接口,供其他可能使用其功能的类使用。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接