在C++中,到底应该放什么内容在头文件和实现文件中?

5
我有一个使用自定义堆栈实现的示例。据我所知,.h 文件应该包含声明,而 cpp文件应该包含实现。 我在cplusplus.com/stack_example上找到了一个自定义堆栈的示例,它看起来类似以下内容。

Stack.h 文件:

#ifndef _STACK_H_
#define _STACK_H_

#include <iostream>
#include "Exception.h"

template <class T>
class Stack {
    public:
        Stack():top(0) {
            std::cout << "In Stack constructor" << std::endl;
        }
        ~Stack() {
            std::cout << "In Stack destructor" << std::endl;
            while ( !isEmpty() ) {
                pop();
            }
            isEmpty();
        }

        void push (const T& object);
        T pop();
        const T& topElement();
        bool isEmpty();

    private:
        struct StackNode {              // linked list node
            T data;                     // data at this node
            StackNode *next;            // next node in list

            // StackNode constructor initializes both fields
            StackNode(const T& newData, StackNode *nextNode)
                : data(newData), next(nextNode) {}
        };

        // My Stack should not allow copy of entire stack
        Stack(const Stack& lhs) {}

        // My Stack should not allow assignment of one stack to another
        Stack& operator=(const Stack& rhs) {}
        StackNode *top;                 // top of stack

};

现在我有一个问题。这个 .h 文件显然透露了一些实现细节。构造函数和析构函数都在 .h 文件中实现了。在我的理解中,它们应该在 .cpp 文件中实现。另外,还有那个 struct StackNode 也是在 .h 文件中实现的。它是否可能在 .cpp 文件中实现,并且只在头文件中声明?作为一个一般规则,如果它们在 .cpp 实现文件中,不是更好吗?以何种方式编写此代码,使其遵循 C++ 规则最佳?

6
如果您需要多个翻译单元使用某些内容,则将这些内容放入头文件中。如果您想要隐藏实现细节,请考虑使用“pimpl习惯用法”或其他不透明结构指针。 - Some programmer dude
3个回答

6

头文件和源文件在本质上并没有什么不同。

实际上,头文件可以包含与源文件相同的代码结构。唯一的区别是,按照惯例,头文件应该被其他文件 #include,而我们通常不会使用源文件(虽然如果我们感到冒险,也可以这样做)。

现在重要的是,如果一个文件被 多个 源文件 #include,会发生什么。请注意,这基本上等同于将相同的代码复制粘贴到多个 .cpp 文件中。在这种情况下,有些事情可能会让你陷入麻烦。

特别是,如果你在两个不同的源文件中得到了相同符号的两个定义,那么你的程序将无法形成良好的格式(根据单一定义规则),链接器可能会拒绝链接你的程序。

对于不同的源文件,这通常不是一个问题,除非你在不同的上下文中意外地重复使用一个名称。但是,一旦头文件进入图片(它们只是复制粘贴的代码),事情就开始变得不同了。你仍然可以在头文件中放置定义,但是如果该头文件然后被多个源文件调用,你就会违反单一定义规则。

因此,惯例是只将可以安全重复在多个源文件中的内容放入头文件中。这包括声明、内联函数定义和模板。

这里关键的认识可能是,头文件是一种在源文件之间共享代码的相当古老的工具。因此,它们严重依赖于用户足够聪明,不会搞砸事情。


5

关于头文件的内容,除了标准库头文件外,没有什么固定的规则。理论上,我们完全可以避免使用头文件,并在 .cpp 文件中复制和粘贴声明。

尽管如此,这更多是基于常识和经验的考虑。你会根据自己的意愿和需求将内容放入头文件或 .cpp 文件中。以下是一些使用案例:

  • 模板声明(比如你的例子)和实现通常放在头文件中。更多信息请参见此线程

  • 如果您认为/希望函数将在多个翻译单元中使用,则将函数声明放在头文件中。

  • 如果您不希望/认为函数会被其他翻译单元使用,则不要将函数声明放在头文件中。

  • 如果您想将一个函数定义为inline,并且希望该函数在不同的翻译单元中使用,则将其定义放在头文件中(如果是非成员函数,则在前面加上inline关键字)。

  • 如果您认为/希望该类型的对象将从不同的翻译单元中访问*,则将类声明(也称为前置声明)放在头文件中。

  • 如果您认为/希望该类仅在一个翻译单元中访问,则不要将类声明放在头文件中。

  • 如果您认为/希望该类型的对象将在不同的翻译单元中创建*,则将类定义(即整个类接口)放在头文件中。

  • 如果您希望该类型的对象仅在一个翻译单元中创建,则不要将类定义放在头文件中。

  • 如果要定义全局变量,则几乎不会将其定义放在头文件中(除非您想在单个翻译单元中包含该头文件)。

  • 如果您正在开发库,则将要向用户提供的那些函数和类的声明放入头文件中。

  • 如果您正在开发库,您将找到一种方法将您不想公开的实现细节放入.cpp文件中(如@Joachim Pileborg所建议的,参见pimpl技巧)。

  • 通常情况下,您不希望在头文件中放置使用声明或使用指令,因为它们会污染将#include头文件的那些翻译单元。

  • 尽可能不要#include其他头文件到您的头文件中;您肯定会更喜欢前向声明您需要使程序编译的内容。

  • 最后,粗略地说,您放在头文件中的东西越少,您的文件编译速度就越快;显而易见的是,您确实希望文件能够快速编译!


注意事项

上面我主要讲了类和函数,但是通常这些经验法则也适用于枚举和typedef声明。

头文件中的成员函数定义没有列入列表,因为它涉及到函数定义是在类定义内部还是外部,而不是.h.cpp文件之间的问题。

* 通过指针或引用使用,与创建相对应,我所说的访问


这是一个很有前途的列表,但(目前)遗漏了其中最有趣的情况之一 - 类/结构体定义。 - Tony Delroy
@TonyD,这比错过更糟糕,我混淆了类的“定义”和“声明”,所以答案是误导性的。我试图澄清这一点。请随意通过编辑或添加进一步的要点来改进当前列表。 - Paolo M

4
一个头文件中究竟包含了什么?
无论你学习的是什么,或者从你的设计出发,尽量少地放置内容到头文件中。头文件只需要最小限度的实现细节即可使用。简单的头文件通常也会使代码更易于使用,并且它们的包含相比大型头文件具有更小的编译时间。
当然,在C++中,头文件与源文件没有区别,取决于您的技能,将源代码制作成不同的文件以使项目更易于使用和理解。
在某些情况下,您被迫将内容放入头文件中(例如使用模板),如果没有被迫这样做,最好将内容放入头文件和源文件中。
编译器将把任何源文件(头文件或cpp)转换为二进制文件,唯一的要求是至少有一些“可编译”的代码(函数/方法的主体,即使为空)。
区分程序员:
如果您在头文件中放置了一个无法编译的函数签名,则必须编译该函数的主体才能实际运行程序(如果您没有在其他地方找到主体,您将获得链接错误)。
Header.h
#pragma once

// by including this file you are actually promising that
// you will later add also the body of this function.
int function(int a); 

Source.cpp

#include <liba>
#include <libb>
#include "Header.h"

// by compiling this file AND linking into your binaries
// you satisfy the promise by providing the real function
int function(int a){
     return liba::fun(a)+libb::fun(b);
}

这个有用吗?是的。每个包含的文件都会增加编译时间并添加代码依赖(如果您更改头文件,则必须重新编译程序,还有可能出现编译错误;但如果将东西分成2个文件,您可以拥有更简单的头文件,并且可以在不必重新编译大量文件的情况下更改函数中的小细节:在大型项目中,编译时间是一个真正的问题)。
以下两个示例是等价的(产生相同的汇编): main.cpp
#include "Header.h"
int main(){

    return function(3);
}

您仅包含了1个文件


main2.cpp

#include "Source.cpp" //note source.cpp here!
int main(){

    return function(3);
}

您正在包含Source.cppHeader.hlibalibb(可能还有更多)文件,这将导致编译时间慢4倍。


当然,头文件也可以让您在编程时更加轻松,避免重复定义。

双重包含

#include "Header.h"
#include "Header.h" //bad, but allowed!
int main(){

    return function(3);
}

双重包含

#include "Source.cpp"
#include "Source.cpp" //error! redefinition (not compile)
int main(){

    return function(3);
}

来自不同地方的重复包含

file1.cpp

#include "Source.cpp" // Bad! include Header.h

file2.cpp

#include "Source.cpp" //error symbol already define!

一个小修正:“头文件与源文件相同”,除非您使用VS和PCH。 - CoffeDeveloper

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