【问题标题】:Separating C++ Class Code into Multiple Files, what are the rules?将 C++ 类代码分成多个文件,规则是什么?
【发布时间】:2013-09-03 08:14:35
【问题描述】:

思考时间 - 为什么要拆分文件?

正如标题所示,我遇到的最终问题是多个定义链接器错误。我实际上已经解决了这个问题,但是我没有以正确的方式解决这个问题。在开始之前,我想讨论一下将一个类文件拆分为多个文件的原因。我已尝试将所有可能的情况放在这里 - 如果我错过了任何情况,请提醒我,我可以进行更改。希望以下是正确的:

原因 1 为了节省空间:

你有一个包含所有类成员的类声明的文件。您在此文件周围放置#include 保护(或#pragma once)以确保在将文件#include 到两个不同的头文件中时不会发生冲突,然后将它们包含在源文件中。您可以使用此类中声明的任何方法的实现来编译一个单独的源文件,因为它会从您的源文件中卸载许多代码行,这会稍微清理一下内容并为您的程序引入一些顺序。

示例:如您所见,可以通过将类方法的实现拆分到不同的文件来改进以下示例。 (一个 .cpp 文件)

// my_class.hpp
#pragma once

class my_class
{
public:
    void my_function()
    {
        // LOTS OF CODE
        // CONFUSING TO DEBUG
        // LOTS OF CODE
        // DISORGANIZED AND DISTRACTING
        // LOTS OF CODE
        // LOOKS HORRIBLE
        // LOTS OF CODE
        // VERY MESSY
        // LOTS OF CODE
    }

    // MANY OTHER METHODS
    // MEANS VERY LARGE FILE WITH LOTS OF LINES OF CODE
}

原因 2 为了防止多定义链接器错误:

也许这是您将实现与声明分开的主要原因。在上面的示例中,您可以将方法体移动到类外部。这将使它看起来更加干净和结构化。但是,根据这个question,上面的例子有隐含的inline 说明符。将实现从类内移到类外,如下例所示,将导致链接器错误,因此您要么内联所有内容,要么将函数定义移至 .cpp 文件。

示例:_如果您不将函数定义移动到 .cpp 文件或将函数指定为内联,则以下示例将导致“多定义链接器错误”。

// my_class.hpp
void my_class::my_function()
{
    // ERROR! MULTIPLE DEFINITION OF my_class::my_function
    // This error only occurs if you #include the file containing this code
    // in two or more separate source (compiled, .cpp) files.
}

解决问题:

//my_class.cpp
void my_class::my_function()
{
    // Now in a .cpp file, so no multiple definition error
}

或者:

// my_class.hpp
inline void my_class::my_function()
{
    // Specified function as inline, so okay - note: back in header file!
    // The very first example has an implicit `inline` specifier
}

原因 3 您想再次节省空间,但这次您使用的是模板类:

如果我们正在使用模板类,那么我们不能将实现移动到源文件(.cpp 文件)。 (我假设)标准或当前编译器当前不允许这样做。与上面原因2的第一个示例不同,我们可以将实现放在头文件中。根据这个question 原因是模板类方法也隐含了inline 说明符。那是对的吗? (似乎有道理。)但似乎没有人知道我刚才提到的问题!

那么,下面的两个例子是一样的吗?

// some_header_file.hpp
#pragma once

// template class declaration goes here
class some_class
{
    // Some code
};

// Example 1: NO INLINE SPECIFIER
template<typename T>
void some_class::class_method()
{
    // Some code
}

// Example 2: INLINE specifier used
template<typename T>
inline void some_class::class_method()
{
    // Some code
}

如果您有一个模板类头文件,由于您拥有的所有功能而变得庞大,那么我相信您可以将函数定义移动到另一个头文件(通常是 .tpp 文件?)然后@ 987654330@ 在包含类声明的头文件末尾。但是,您不得在其他任何地方包含此文件,因此应使用 .tpp 而不是 .hpp

我假设您也可以使用常规类的内联方法来做到这一点?这也允许吗?

提问时间

所以我在上面做了一些陈述,其中大部分与源文件的结构有关。我认为我所说的一切都是正确的,因为我做了一些基础研究并“发现了一些东西”,但这是一个问题,所以我不确定。

归结为,您将如何在文件中组织代码。我想我已经找到了一个永远有效的结构。

这是我想出的。 (这是我的类代码文件组织/结构标准,如果你喜欢。不知道它是否很有用,这是问的重点。)

  • 1:.hpp 文件中声明类(模板或其他),包括所有方法、友元函数和数据。
  • 2:.hpp 文件的底部,#include 是一个.tpp 文件,其中包含任何inline 方法的实现。创建.tpp 文件并确保所有方法都指定为inline
  • 3: 所有其他成员(非内联函数、友元函数和静态数据)应在.cpp 文件中定义,#includes 顶部的.hpp 文件以防止错误像“尚未宣布 ABC 类”。由于此文件中的所有内容都有外部链接,因此程序将正确链接。

行业中是否存在这样的标准?我提出的标准是否适用于所有情况?

【问题讨论】:

  • 顺便说一句,我略读了你的问题,因为它太长了。关于内联模板功能。 inline 关键字不是必需的(您仍然可以将它们放在头文件中)并且没有任何保证效果。使用关键字可能会使编译器更有可能内联函数,但没有什么是确定的。这些模板函数不是隐式内联函数。正如答案的 cmets 中所讨论的那样,该问题的答案是错误的。
  • @NeilKirk 啊,谢谢,肯定有很多分歧。
  • 忽略将 inline 放在那里,这个问题永远不会困扰你 :)
  • 我想我回答错了问题,忽略我的回答。
  • @NeilKirk inline 关键字是必需的,如果函数是在头文件中的类定义之外定义的(并且通常不赞成将函数定义放在类定义中)。

标签: c++ class include


【解决方案1】:

你的三点听起来很对。这是做事的标准方式(虽然我以前没有见过 .tpp 扩展名,通常是 .inl),虽然我个人只是将内联函数放在头文件的底部而不是单独的文件中。

这是我整理文件的方式。我省略了简单类的前向声明文件。

myclass-fwd.h

#pragma once

namespace NS
{
class MyClass;
}

myclass.h

#pragma once
#include "headers-needed-by-header"
#include "myclass-fwd.h"

namespace NS
{
class MyClass
{
    ..
};
}

myclass.cpp

#include "headers-needed-by-source"
#include "myclass.h"

namespace
    {
    void LocalFunc();
}

NS::MyClass::...

根据偏好将 pragma 替换为 header 保护..

这种方法的原因是减少头文件依赖,这会减慢大型项目的编译时间。如果您不知道,可以转发声明一个类以用作指针或引用。只有在构造、创建或使用类的成员时才需要完整的声明。

这意味着使用该类的另一个类(通过指针/引用获取参数)只需将 fwd 标头包含在其自己的标头中。然后将完整的标头包含在第二个类的源文件中。这大大减少了你在拉入一个大头时得到的不需要的垃圾量,它拉入另一个大头,拉入另一个......

下一个技巧是未命名的命名空间(有时称为匿名命名空间)。这只能出现在源文件中,它就像一个隐藏的命名空间,只对该文件可见。您可以在此处放置仅由源文件使用的本地函数、类等。如果您在两个不同的文件中创建具有相同名称的内容,这可以防止名称冲突。 (例如两个局部函数 F,可能会给出链接器错误)。

【讨论】:

  • 啊,是的,关于匿名命名空间和前向声明头文件的好技巧。我可以看到很多时候前向声明特别有用。谢谢!
  • @NeilKirk:为什么在 myclass.h 中包含 myclass-fwd.h?我会认为这是多余的。
  • @quamrana 没有理由,只是为了保持一致。
  • @quamrana 我非常喜欢这样,因为它提醒您在某个地方有一个文件,该文件可能在您的工作区中未打开,其中仅包含一个类预声明。
【解决方案2】:

将接口与实现分开的主要原因是,当实现中的某些内容发生变化时,您不必重新编译所有代码;您只需重新编译更改的源文件。

至于“声明类(模板或其他)”,template不是classtemplate 是一种用于创建 类的模式。不过,更重要的是,您在标题中定义一个类或模板。类定义包括其成员函数的声明,非 inine 成员函数在一个或多个源文件中定义。内联成员函数和所有模板函数都应该在头文件中定义,通过任何你喜欢的直接定义和#include指令的组合。

【讨论】:

  • “至于“声明类(模板或其他)”,模板不是类。模板是创建类的模式。 - 是的,我知道
【解决方案3】:

行业中是否存在这样的标准?

是的。再说一次,与你所表达的编码标准有很大不同的编码标准也可以在工业中找到。毕竟,您在谈论编码标准,编码标准从好到坏到丑陋。

我提出的标准是否适用于所有情况?

绝对不是。例如,

template <typename T> class Foo {
public:
   void some_method (T& arg);
...
};

这里,类模板Foo的定义对模板参数T一无所知。如果对于某些类模板,方法的定义因模板参数而异怎么办?您的规则 #2 在这里不起作用。

另一个例子:如果相应的源文件很大,一千行或更长怎么办?有时在多个源文件中提供实现是有意义的。一些标准走到了极端,每个文件都规定了一个功能(个人意见:是的!)。

在一千多行的源文件的另一个极端是没有源文件的类。整个实现在标题中。对于仅标头的实现,有很多话要说。如果不出意外,它可以简化,有时甚至是显着地简化链接问题。

【讨论】:

  • 标头仅如:在类大括号内声明和定义的所有方法({})?我明白你关于防止链接器错误的观点,但是任何比一行代码更复杂的成员,例如,比{ return m_time; } 更复杂的成员都会产生可怕的代码。
  • 仅头文件,因为所有方法都在某个头文件中定义。无论是在类内部,作为类外部但仍位于同一个头文件中的行外定义,还是第一个头文件包含的其他文件,都是一个不同的问题。关键点:没有需要单独编译和链接进去的源文件。比如很多Boost都是header-only实现。
  • 至于比{ return something; } 更复杂的东西不属于类定义:这是你的意见。其他人有其他意见。我个人不介意在类定义中看到一个简短的(但不是单行的)。我个人的规则:如果我在类中放置一个函数定义,如果我认为这样做会帮助其他人理解该类,那是我的意见。在将个人偏好问题放入编码标准时要非常小心。它会让人们忽视你的标准。
  • 我想我在某种程度上误解了你......你是否建议将类成员函数定义放在与类相同的头文件中,但在类之外......就像你一样必须输入&lt;return type&gt; [class_name]::[function_name]() { // Code here }
  • 你完全误解了我的意思。在仅标头实现中没有要编译的源文件。所需的一切,包括函数定义,都在某个标头中。您的 .tpp 文件是一个头文件,只是带有非标准后缀。它仍然是#included,而不是编译。至于应该将那些定义在头文件中的成员函数放在哪里(在类定义中与外联但如果需要符合一个定义规则则使用内联限定),这是个人喜好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-06
  • 2016-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多