c ++前向声明,翻译单元,链接

时间:2017-04-26 21:06:42

标签: linker external declaration forward

On this site 我读到了:

class MyClass;
  

简单地声明“有这样一个类”,它的完整定义将“稍后”(在当前文件中,在编译时,或在链接时从其他文件)

我不确定如果我在链接时理解这个过程。我写了下面的代码,应该演示它。如果我错了,请纠正我。我不确定链接时的前向声明是如何工作的。

//first.h
-----------
class Second;

class First{
public:
    Second* ptr;
    First();
};

//first.cpp
-----------
#include "first.h"
extern Second second;
First::First(){ptr = &second;}

//second.h
----------
class Second{
public:
    Second(){};
};

//main.cpp
----------
#include "second.h"
Second second;

int main(int argc, char *argv[])
{
    return 0;
}

此代码已编译。如果行第二秒;注释,链接器抛出:未定义引用'秒'。 一些评论放在一起1)前向声明2)编译单元3)链接可能会有所帮助。

1 个答案:

答案 0 :(得分:2)

我认为您阅读的文档因其松懈而误导您:

class MyClass;

并不完全意味着有这样一个类,因为 创建类存在的唯一方法是定义它,声明是 不是定义。该声明将更好地理解为:假设有这样一个类

并不意味着该类的完整定义将来或将来不会出现。它' S 完整定义可能需要稍后才能成功编译。要么 不。如果完整的类定义确实需要稍后, 它需要来才能成功编译;因此在编译时,而不是链接时。

您可以激发的未定义引用链接错误 在Second second;中评论main.cpp只是一个问题 普通的旧的未定义的引用错误,例如你总是得到的 试图链接一个声明extern变量的程序 在某处被引用并且无处定义。它没有必要 与extern变量属于类类型的连接 - 相反 比如,int - 或者与前瞻性宣言的业务有关。

类的前向声明只是必要才能抢占 编译器尝试解析定义时的死锁 两个相互依赖且无法完成的类 在完成另一个之前的类定义。

一个基本的例子:我天真地写了两个类firstsecond 每个都有一个方法,使用另一个类的对象和调用 方法之一:

<强> first.h

#ifndef FIRST_H
#define FIRST_H

#include <string>
#include <iostream>
#include "second.h"

struct first {

    std::string get_type() const {
        return "First";
    }
    void use_a_second(second const & second) const {
        std::cout << second.get_type() << std::endl;
    }

};

#endif

<强> second.h

#ifndef SECOND_H
#define SECOND_H

#include <string>
#include <iostream>
#include "first.h"

struct second {

    std::string get_type() const {
        return "First";
    }
    void use_a_first(first const & first) const {
        std::cout << first.get_type() << std::endl;
    }
};

#endif

<强>的main.cpp

#include "first.h"
#include "second.h"

int main()
{
    first f;
    second s;
    f.use_a_second(s);
    s.use_a_first(f);
    return 0;
}

尝试编译main.cpp

$ g++ -c -o main.o -Wall -Wextra -pedantic main.cpp 
In file included from first.h:6:0,
                 from main.cpp:1:
second.h:13:19: error: ‘first’ has not been declared
  void use_a_first(first const & first) const {
                   ^~~~~
second.h: In member function ‘void second::use_a_first(const int&) const’:
second.h:14:22: error: request for member ‘get_type’ in ‘first’, which is of non-class type ‘const int’
   std::cout << first.get_type() << std::endl;
                      ^~~~~~~~
main.cpp: In function ‘int main()’:
main.cpp:9:8: error: expected unqualified-id before ‘.’ token
  second.use_a_first(first);

编译器受到阻碍,因为first.h包含second.h,和 反之亦然,因此它无法获得first之前的定义 获得second的定义,这需要定义first ...  而反之亦然

定义之前的每个类的转发声明 另一个,以及每个类的相应重构 一个定义和一个实现,让我们摆脱这种致命的拥抱:

first.h(已修复)

#ifndef FIRST_H
#define FIRST_H

#include <string>

struct second; // Declaration

struct first{
    std::string get_type() const {
        return "first";
    }
    void use_a_second(second const & second) const;
};

#endif  

second.h(已修复)

#ifndef SECOND_H
#define SECOND_H

#include <string>

struct first; //Declaration

struct second{
    std::string get_type() const {
        return "second";
    }
    void use_a_first(first const & first) const;

};

#endif

first.cpp(新)

#include <iostream>
#include "first.h"
#include "second.h"

void first::use_a_second(second const & second) const {
    std::cout << second.get_type() << std::endl;
}

second.cpp(新)

#include <iostream>
#include "first.h"
#include "second.h"

void second::use_a_first(first const & first) const {
    std::cout << first.get_type() << std::endl;
}

编译:

$ g++ -c -o first.o -Wall -Wextra -pedantic first.cpp
$ g++ -c -o second.o -Wall -Wextra -pedantic second.cpp
$ g++ -c -o main.o -Wall -Wextra -pedantic main.cpp

链接:

$ g++ -o prog main.o first.o second.o

执行命令

$ ./prog
second
first

这是前向类声明的唯一场景 需要可以在更广泛的情况下使用:请参阅When can I use a forward declaration?。只需要每一个需求 成功的编译,而不是链接。直到可以尝试联系 编译成功。

使用定义一词时,文档片段也是误导性的。该 类的定义在编译的上下文中意味着一件事 为了清楚起见,应该意味着什么。这意味着别的东西,松散地, 在联系的背景下,不应该意味着为了清晰起见。 在链接的背景下,我们最好只讨论实现 一个阶级 - 甚至这是一个需要资格的概念。

就编译器而言,一个类是定义的,如果它来自 开始到结束:

class foo ... {
    ...
};

没有错误,然后类定义是该范围的内容。一个完整的定义 当然,这并不意味着一个类有一个完整的实现。它 除了完整的定义之外,只有所有方法和 在其定义中声明的静态成员本身也在定义,或者 在类定义中内联;包含翻译中的外包 单位,或在其他翻译单位(可能在外部编译) 库)包含已编译的包含翻译单元的链接。 如果没有以这些方式之一提供任何这些成员定义 来链接时,将导致未解决的引用链接错误。那 是实现类的缺陷。

链接器对定义的想法与C ++不同 编译器和更基础的。从链接器的角度来看, 一个C ++类实际上并不存在。对于链接器,类实现由编译器简化, 一堆符号和符号定义与它得到的基本不同 从任何语言编译器,无论语言是否处理。 对于成功而言,对链接器而言,重要的是输出二进制文件中引用的所有符号 在同一个二进制文件或请求的动态库中有 definitions 在联系中。符号(广义地)可以标识一些可执行代码或一些数据。 对于代码符号, definition 表示链接器的实现:定义是代表的代码,如果有的话。 对于数据符号, definition 表示链接器的:它表示所表示的数据(如果有)。

所以当片段说:

  

..它的完整定义将是&#34;稍后出现&#34; (在当前文件中,在编译时,或在链接时从某个其他文件)

这需要分开。

foo的完整定义必须稍后在编译中 翻译单元,在类型foo之前需要作为任何 else 的类型, 具体而言,基类的类型,或函数/方法参数,或对象 1 。 如果不满足此要求,将导致 compile 错误: -

  • 如果未完全定义任何基类,则无法完全定义类。
  • 如果函数或方法具有类型的参数,则无法完全定义它 尚未完全定义。
  • 对象不能存在任何未完全定义的类型。

如果foo 以后从未需要作为基类,参数或对象的类型, 然后,类foo的定义永远不需要遵循声明。

foo 的完整实现可能需要<或em ,或者 通过联系提供。由于链接器不了解类, 它并不知道不完整类的完整实现之间的区别。 您可以通过添加一个没有实现的方法来更改上面的类first

struct first{
    std::string get_type() const {
        return "first";
    }
    void use_a_second(second const & second) const;
    void unused();
};

程序将编译,链接和运行相同。自从 编译器没有发出void first::unused()的定义,因为 程序不会尝试调用void first::unused() 任何类型为first的对象,或使用其地址,不提及 void first::unused()出现在联系中。如果 我们将main.cpp更改为:

#include "first.h"
#include "second.h"

int main()
{
    first f;
    second s;
    f.use_a_second(s);
    s.use_a_first(f);
    f.unused();
    return 0;
}

然后链接器会在void first::unused()中找到main.o的来电 当然还有一个未解决的参考错误。但这只是 意味着链接无法提供实现 计划需求。它并不意味着类定义 first不完整。如果是的话,编译main.cpp会有 失败了,没有尝试过联系。

Takeway: -

  • 转发类声明可以避免编译时死锁 相互依赖的类定义,以及相应的重构。

  • 前向类声明可以避免未解析的引用链接 错误。这样的错误总是意味着执行 程序需要代码符号或数据符号的值 而不是由联系提供。类声明也不能添加 其中一个联系方式。它没有增加链接。它 只是指示编译器在上下文中容忍foo foo成为类名的必要和充分的地方。

  • Linkage无法在链接时提供类定义的任何部分 如果在前向类声明之后,类定义变为 必需的,因为在编译时或者需要完整的类定义 一点也不。 Linkage根本不能提供类定义的一部分; 只有类实现的元素。

<小时/> [1]要明确:

class foo;
foo & bar();
...
foo * pfoo;
...
foo & rfoo = bar();

只能通过class foo的声明进行编译,因为两者都没有 foo * pfoofoo & rfoo要求存在foo类型的对象: 指向foo的指针或引用foo不是foo

可是:

class foo;
...
foo f; // Error
...
foo * pfoo; 
...
pfoo->method(); // Error

无法编译,因为f必须是foo,而 pfoo所解决的对象 必须存在,因此如果通过该方法调用任何方法,则为foo 对象