在C ++中定义类之外的方法有什么意义?

时间:2015-02-21 05:21:56

标签: c++

作为一个试图学习C ++的Java人,我对你为什么要做这样的事情感到有点困惑

class question(){
private:
string ques;

public:
question(string ques)
}

question::question(string ques){
this->ques = ques;

相对
class question(){
private:
string ques;

public:
question(string ques){
this->ques = ques;
}

}

它是否与“内联”有关?我也不太清楚这意味着什么。

7 个答案:

答案 0 :(得分:3)

一个重要原因是减少实现更改时需要重新编译的文件数。假设您的问题类的接口定义是稳定的并且不会有太大变化,但您仍在努力实现其方法。

如果您将所有实现放在标题中,则必须编译包含标题的所有文件。经典的例子是基于模板的代码,它增加了编译时间,因为它们的方法体必须始终在头文件中可见。

答案 1 :(得分:1)

这是一个依赖性问题。

当您将#include个文件放入另一个文件时,所包含文件的整个内容将成为包含该文件的文件的一部分。请记住,#include预处理程序指令。它在调用编译器之前进行处理,它解析预处理器的输出。因此,编译器最终会看到一个统一代码,而不知道内容可能来自不同的来源。代码从头到尾按原样编译。例如:

Question.h

class question() {
private:
    string ques;

public:
    question(string ques){
        this->ques = ques;
    }
};

A.cpp

#include "Question.h"

void doSomething() {
    question q("something");
    ...
}

B.cpp

#include "Question.h"

void doSomethingElse() {
    question q("something else");
    ...
}

编译A.cppB.cpp时,预处理器会将Question.h的内容合并到其中,编译器会看到此代码:

A.cpp

class question() {
private:
    string ques;

public:
    question(string ques){
        this->ques = ques;
    }
};

void doSomething() {
    question q("something");
    ...
}

B.cpp

class question() {
private:
    string ques;

public:
    question(string ques) {
        this->ques = ques;
    }
};

void doSomethingElse() {
    question q("something else");
    ...
}

因此,您对Question.h所做的任何更改都会影响A.cppB.cpp的内容,因此必须重新编译它们。

您希望最小化在对给定文件进行更改时必须重新编译的文件数。通过将声明和实现分离为单独的.h.cpp文件,然后#include仅将.h文件分隔到其他文件中,对{{1}进行更改} file不会影响其他文件,因此不必重新编译它们,从而加快编译时间。只有对.cpp文件的更改才会导致重新编译它们。例如:

Question.h

.h

Question.cpp

class question() {
private:
    string ques;

public:
    question(string ques);
};

A.cpp

#include "Question.h"

question::question(string ques) {
    this->ques = ques;
}

B.cpp

#include "Question.h"

void doSomething() {
    question q("something");
    ...
}

编译时,编译器会看到:

Question.cpp

#include "Question.h"

void doSomething() {
    question q("something");
    ...
}

A.cpp

class question() {
private:
    string ques;

public:
    question(string ques);
};

question::question(string ques) {
    this->ques = ques;
}

B.cpp

class question() {
private:
    string ques;

public:
    question(string ques);
};

void doSomething() {
    question q("something");
    ...
}

您可以对class question() { private: string ques; public: question(string ques); }; void doSomethingElse() { question q("something else"); ... } 进行任何更改,只会{}重新编译Question.cppQuestion.cppA.cpp未被更改,因此不会重新编译,除非您更改B.cpp,在这种情况下它们是。{/ p>

此外,分离适合使用预编译头,这也有助于加快编译时间。 PCH由Question.h文件组成,这些文件不会随时间发生变化,因此可以编译一次,并在#include引用PCH的任何地方缓存和重复使用输出。只要PCH本身或其任何相关文件未被更改,只有在您更改其引用的其他内容时才会重新编译#include PCH的文件。

要考虑这种分离还有另一个方面。如果所有内容都在一个文件中声明并实现,然后您#include将该文件放入多个文件中,它们每个都会收到自己声明的全局变量的副本。例如:

Question.h

#include

A.cpp

class question() {
private:
    string ques;

public:
    question(string ques){
        this->ques = ques;
    }
};

int myGlobal; // <--

B.cpp

#include "Question.h"

void doSomething() {
    question q("something");
    ...
}

编译时,编译器会看到:

A.cpp

#include "Question.h"

void doSomethingElse() {
    question q("something else");
    ...
}

B.cpp

class question() {
private:
    string ques;

public:
    question(string ques){
        this->ques = ques;
    }
};

int myGlobal; // <--

void doSomething() {
    question q("something");
    ...
}

现在class question() { private: string ques; public: question(string ques){ this->ques = ques; } }; int myGlobal; // <-- void doSomethingElse() { question q("somethingElse"); ... } A.cpp都有自己的全局变量,它们具有相同的名称。这可能会导致链接器冲突。它可能无法完全链接。它可能决定丢弃一个并保留另一个。它可能决定保留两者。在后一种情况下,您最终会得到多个数据副本,如果某个代码部分在实际期望操作同一个名字的另一个变量时操作一个变量,则可能会在运行时导致细微的不一致。如果你不小心,这可能很难调试。

分离B.cpp.h文件,并在.cpp文件中将全局变量声明为extern,并在相应的{{1}中定义其内存存储} file,编译时只有一个变量,.h .cpp文件的其他文件只接收对该单个变量的引用。例如:

Question.h

#include

Question.cpp

.h

A.cpp

class question() {
private:
    string ques;

public:
    question(string ques);
};

extern int myGlobal; // <--

B.cpp

#include "Question.h"

int myGlobal = 0; // <--

question::question(string ques) {
    this->ques = ques;
}

编译时,编译器会看到:

Question.cpp

#include "Question.h"

void doSomething() {
    question q("something");
    ...
}

A.cpp

#include "Question.h"

void doSomethingElse() {
    question q("something else");
    ...
}

B.cpp

class question() {
private:
    string ques;

public:
    question(string ques);
};

extern int myGlobal; // <--

int myGlobal = 0; // <--

question::question(string ques) {
    this->ques = ques;
}

链接器(不是编译器)负责根据需要解析这些外部引用,因此最终可执行文件中只有一个变量可供所有相关代码访问。

答案 2 :(得分:0)

在C ++中,默认情况下,使用类定义的方法视为内联,但您也可以通过在类外部定义来使函数内联。类中的定义函数并不意味着编译器将始终视为内联。

要考虑函数是内联的,必须满足条件函数。你可以参考一些C ++书中的条件。

它由C ++提供,你可以在类中声明方法并在外部定义,可以提供这个东西,使代码更具可读性和可管理性。

答案 3 :(得分:0)

在java中,我们没有头文件,但java中有接口。但是在C ++和C中我们有头文件,扩展名为 .h /.hpp。因此,在C ++中,您可以将声明和定义或接口和实现保存在两个不同的文件中。如果您是库开发人员,则必须将头文件包含在库的包中。因此,您的库的用户将从头文件获取接口信息以供进一步使用。

如果在类声明系统中定义函数的定义/实现,可以决定将其视为内联函数,这取决于C ++中的某些条件。您可以在此处阅读内联条件的答案What is the criterion to be inline

答案 4 :(得分:0)

在C ++中,您有时希望将定义放在.cpp或.cc文件中,因为您想控制生成目标代码的位置。

如果您正在编写需要维护稳定ABI的共享库,则尤其为true。实际上,对于稳定的ABI,不要在标题中定义任何

因为如果你把代码放在头文件中,并允许编译器定义默认构造函数,析构函数和赋值运算符,它会将它们放入随机位置(对于那些不是链接器专家的人来说是随机的)。 / p>

Java的类模块系统确实更有意义。但C ++有很多来自C的包袱,这也是其中的一部分。

答案 5 :(得分:0)

减少编译时耦合。

C ++编译比Java更浪费时间。这就是我们想要限制编译器在编译实现时需要看到的代码的原因。

你会遇到其他一些技巧来实现这一点,比如pimpl-idiom,一个类完全隐藏了它在一个指向隐藏类的指针中的实现。

// SomeClass.h - only the visible interface
class SomeClass {
public:
     void foo();
     ...
private:
     class Impl;
     std::unique_ptr<Impl> impl;
};

/// SomeClass.cpp
class SomeClass::Impl {
public:
      void foo() {...}
};

SomeClass::SomeClass()
: impl(std::make_unique<Impl>())
{}

void SomeClass::foo() { impl->foo(); }

答案 6 :(得分:0)

所有6个(在撰写本文时)答案都是答案的一部分,所以我的答案是试着在一个答案中总结所有原因。

在我开始之前,先复习一下术语。要声明 C ++中的变量,函数或方法意味着它存在并描述它的类型,而定义它意味着提供它&#39的实施。

所以没有特别的顺序:

  1. 使得可以将方法声明与定义分开。从软件工程的角度来看,这是一个好主意 - 您可以指定类的接口与其实现分开。

  2. 减少编译时间。通常,包含类声明的类的接口放在.h(头文件)文件中,定义(实现)放在.c文件中。这样做的一个主要好处是它允许不同的组件单独编译,这反过来减少了其中一个组件发生变化时需要重新编译的文件数量。

    作为旁注:在头文件中只放置声明和定义不是强制性的。在某些情况下,您需要在头文件中定义 - 有关详细信息,请参阅下文。

  3. 允许为其他代码可以链接到的库定义ABI。可以将组件放入共享库中,然后可以从完全独立的程序中调用它们。在编写使用此类库的程序时,请包含头文件,以便编译器了解库中存在的类,方法,函数等。但是这个头文件不能包含该类的任何实际定义,否则对库的小修改会破坏正在使用它的代码。

  4. 允许类之间相互依赖,无需多遍编译过程。

    假设类A有一个调用类B方法的方法,而类B有一个调用类A的方法。如果你先声明了类A,那么编译器就不会知道类B的方法当它正在编译类A中方法的定义时。重新排序文件中的方法将无济于事,因为反之亦然。

    在两个类声明之后放置方法的定义意味着编译器在编译两者的定义之前就知道两个类中是否存在所有方法。

  5. 您可能现在想知道,为什么C ++甚至允许在类声明中定义方法。好吧,除了方便之外,还有一个关键原因:在某些情况下,您希望编译器可以内联&#39;一个方法。这意味着,在函数调用的站点,而不是生成机器代码来执行函数调用,而是插入函数本身的指令。例如,如果函数非常小,这可能是有益的,因此实际调用函数的开销并不是真正的保证。

    要做到这一点,函数的定义需要在被称为的每一个地方都被称为。由于调用站点可能位于完全不同的编译单元(.c文件)中,因此您需要将函数的实际定义放在标头内,以便编译器只需包含标头即可访问它。

    有两种方法可以做到这一点:

    1. 在类声明中定义函数:

      class foo {
      
      public:
          void bar() {
            doSomething();
          }
      
      };
      
    2. 在类声明之外定义函数,但仍在头文件中:

      class foo {
      
      public:
          void bar();
      }
      
      void inline foo::bar() {
          doSomething();
      }
      
    3. 请注意,在第二种情况下,关键字&#39; inline&#39;告诉编译器方法可以内联。在第一种情况下,由于在类声明中定义,该方法被隐式标记为内联。

      在这两种情况下,编译器都不必内联它 - 编译器最终决定这一点。

      内联关键字(隐式或其他)具有另一种效果:它回避了一个定义规则&#39;在C ++中,它声明只能有一个函数定义。否则,在创建多个.c文件引用相同标题的程序时会出现问题,因为它们都包含相同函数的定义。

      在我希望方法可以内联的情况下,我通常使用第一种样式,除非存在如上所述的交叉依赖性。另外,我避免使用任何一种样式在标题中编写长函数 - 如果它是一个很长的函数,那么内联不太可能有任何好处。

      另请注意,内联函数会使您的代码对更改更加脆弱。例如,您不应该为在您正在编写的库中公开的任何函数使用内联。