extern 关键字和多个翻译单元的使用
Posted
技术标签:
【中文标题】extern 关键字和多个翻译单元的使用【英文标题】:Usage of extern keyword and multiple translation units 【发布时间】:2013-05-05 23:04:26 【问题描述】:我正在阅读 Scott Meyer 的 Effective C++ 的第 4 项,他试图展示一个在不同翻译单元中使用静态非本地对象的示例。他强调了一个问题,即在一个翻译单元中使用的对象在使用之前不知道它是否已在另一个翻译单元中初始化。如果有人有副本,它在第三版的第 30 页。
例子是这样的:
一个文件代表一个库:
class FileSystem
public:
std::size_t numDisks() const;
....
;
extern FileSystem tfs;
在客户端文件中:
class Directory
public:
Directory(some_params);
....
;
Directory::Directory(some_params)
...
std::size_t disks = tfs.numDisks();
...
我的两个问题是:
1) 如果客户端代码需要使用tfs
,那么就会有某种包含语句。因此,该代码肯定都在一个翻译单元中吗?我看不出您如何引用不同翻译单元中的代码?程序肯定是一个翻译单元吗?
2) 如果客户端代码包含 FileSystem.h,extern FileSystem tfs;
行是否足以让客户端代码调用 tfs(我很欣赏初始化可能存在运行时问题,我只是在谈论编译时范围)?
编辑到第一季度
书上说这两段代码位于不同的翻译单元中。客户端代码如何使用变量tfs
,知道它们在不同的翻译单元中??
【问题讨论】:
“程序肯定是一个翻译单元吗?”一个翻译单元基本上由一个 cpp 文件(源文件)加上包含的文件(标题)减去条件包含(#if
)组成。因此,您可以将多个翻译单元组成一个程序(它们在最后一个翻译阶段由链接器链接在一起)。
@DyP 我不明白代码如何在没有标头的情况下链接到其他代码?
你需要有一个变量/函数的声明或者一个类的定义才能使用它。您可以通过#include
做到这一点,这实际上是将标头内容复制粘贴到源文件 (cpp) 中。链接器匹配使用外部链接声明的名称,例如在您的示例中,任何翻译单元中tfs
的声明与定义它的翻译单元中tfs
的定义。
@DyP 据我了解,使用#include 暗示我们在同一个翻译单元中?翻译单元是源文件,加上它#includes 到的所有头文件?
编译器从输入源文件开始。它需要一个源文件,解析所有#include
s 和#if
s,然后有一个翻译单元。然后它继续翻译那个翻译单元。对所有其他源文件(-> 许多 TU)重复(独立)。最后,将所有翻译单元(现在是目标文件)组合起来形成程序。
【参考方案1】:
这是一个简化的示例,说明跨多个 TU 的初始化可能会出现问题。
gadget.h:
struct Foo;
extern Foo gadget;
gadget.cpp:
#include <foo.h>
#include <gadget.h>
Foo gadget(true, Blue, 'x'); // initialized here
client.cpp:
#include <foo.h>
#include <gadget.h>
int do_something()
int x = gadget.frumple(); // problem!
return bar(x * 2);
问题是不能保证gadget
object 在do_something()
引用它时已经被初始化。只能保证一个 TU 中的初始化程序在该 TU 中的函数被调用之前完成。
(解决方案是将extern Foo gadget;
替换为Foo & gadget();
,在gadget.cpp中实现为 static Foo impl; return impl;
并使用gadget().frumple()
。)
【讨论】:
我向 Matteo 重复我的问题/评论:“是否在 main 的第一条语句之前完成具有静态存储持续时间的非局部变量的动态初始化是由实现定义的。如果初始化延迟到 main 的第一条语句之后的某个时间点,它应该发生在与要初始化的变量在同一翻译单元中定义的任何函数或变量的第一次 odr-use (3.2) 之前。 ” 注意“变量”这个词。请详细说明。 @Kerrek SB,我理解了多个 TU 的问题——我只是不明白一个程序是如何由多个 TU 组成的,以及关于外部使用的部分。 @user997112:我的示例程序由两个 TU 组成:gadget.cpp 和 client.cpp。【参考方案2】:这是来自标准 C++03 的示例(我添加了 a.h
和 b.h
标头):
[basic.start.init]/3
// a.h
struct A A(); Use() ;
// b.h
struct B Use() ;
// – File 1 –
#include "a.h"
#include "b.h"
B b;
A::A()
b.Use();
// – File 2 –
#include "a.h"
A a;
// – File 3 –
#include "a.h"
#include "b.h"
extern A a;
extern B b;
int main()
a.Use();
b.Use();
在进入 main 之前是否初始化 a 或 b 或者是否延迟初始化直到 a 在 main 中第一次使用是由实现定义的。特别是,如果 a 在进入 main 之前初始化,则不能保证 b 在被 a 的初始化使用之前被初始化,即在调用 A::A 之前。然而,如果 a 在 main 的第一条语句之后的某个时刻被初始化,则 b 将在其在 A::A 中使用之前被初始化。
【讨论】:
嗨(再次)我理解了多个 TU 之间的初始化问题。我不明白我们如何在一个程序中拥有多个 TU。如果 x.cpp 包含 y.h,则它们形成一个翻译单元 (TU1)。那么另一个翻译单元(TU2)如何使用来自 TU1 的任何代码,除非它们使用相同的标头?如果它们使用相同的标头,那么它们肯定不是单独的 TU? 是的,它们是独立的 TU,请参阅我对您问题的评论 :)【参考方案3】:1) 如果客户端代码需要使用 tfs,那么就会有某种 include 语句。因此,该代码肯定都在一个翻译单元中吗?我看不出您如何引用不同翻译单元中的代码?程序肯定是一个翻译单元吗?
翻译单元(大致)是预处理后的单个 .cpp 文件。编译单个翻译单元后,您将获得一个模块对象(通常具有扩展名 .o
或 .obj
);编译完所有 TU 后,链接器将它们链接在一起以形成最终的可执行文件。这通常被 IDE 隐藏(甚至被在命令行上接受多个输入文件的编译器所隐藏),但重要的是要理解构建 C++ 程序(至少)需要三个步骤:预编译、编译和链接。
#include
语句将包括类的声明和 extern
声明,告诉当前翻译单元类 FileSystem
是这样创建的,并且在某些翻译单元中,有一个变量 @ 987654327@ 类型为FileSystem
。
2) 如果客户端代码包含 FileSystem.h,则行 extern FileSystem tfs;足以让客户端代码调用 tfs
是的,extern
声明告诉编译器在某些 TU 中有一个这样定义的变量;编译器在目标模块中为其放置一个占位符,链接器在将各种目标模块捆绑在一起时,将使用实际的tfs
变量的地址(在其他一些翻译单元中定义 )。
请记住,当您编写extern
时,您只是声明一个变量(即您告诉编译器“相信我,某处有这个东西”),当您省略它时,您就是声明它和定义它(“有这个东西,你必须在这里创建它”)。
函数的区别可能更清楚:当您编写原型时,您声明一个函数(“某处有一个函数 x 接受此类参数并返回此类型”),当您实际编写时你正在定义它的函数(带有函数体)(“这就是这个函数实际所做的”),并且,如果你之前没有声明过它,它也算作一个声明。
关于如何实际使用/管理多个 TU,您可以查看this answer of mine。
【讨论】:
不错的答案。也许您想添加类似“除非名称被标记为extern
,否则声明就是定义”?
@DyP:我通常更喜欢反过来看:extern
标记“纯声明”,而“常规”变量定义也包含隐式声明。
我不认为最后一条语句是真的:“是否在main的第一条语句之前完成具有静态存储持续时间的非局部变量的动态初始化是由实现定义的。如果初始化延迟到 main 的第一个语句之后的某个时间点,它应该发生在与要初始化的变量在同一翻译单元中定义的任何函数或变量的第一次 odr-use (3.2) 之前。" [basic.start.init]/4
@DyP:这不是我在写的吗?初始化可以推迟,唯一的保证是反对在同一个 TU 中使用变量 - 如果另一个 TU 引用该变量,则不能保证它已经被初始化。
我的解释不同:“在同一个翻译单元中定义”,即在要初始化的变量的翻译单元中第一次使用任何变量之前以上是关于extern 关键字和多个翻译单元的使用的主要内容,如果未能解决你的问题,请参考以下文章