c++ - 链接器如何处理 C++ 标头中的定义?
问题描述
我想更好地了解链接器在构建 c++ 代码时是如何工作的。
如果我在多个 cpp 文件中定义一个函数或一个全局变量,我会收到多个定义的链接器错误。这是有道理的,因为我有多个版本,而链接器无法决定一个特定的版本。为了避免这种情况,只写/包含声明,(仅用于函数的签名,用于变量的 extern)。但是,我注意到您可以在类声明中定义方法,并且至少这里的大多数人认为对于琐碎的函数(如琐碎的 getter 和 setter)是可以接受甚至是好的做法,因为它允许编译器内联这些函数(并且,它是模板所必需的)。
在围绕“pragma once”的讨论中,我了解到在某些情况下,工具链将无法区分文件是否相同,因此原则上可能会发生两个 cpp 文件声明相同的类名来自不同的标头,但对此类仅标头方法的定义不同,不是吗?
我试图建立一个例子:main.cpp
#include <iostream>
#include "Class1.hpp"
#include "Class2.hpp"
using namespace std;
int main() {
Class1 c1;
Class2 c2(c1);
c1.set(1);
cout << c1.get() << endl;
c2.print();
return 0;
}
Class1.hpp:
#ifndef CLASS1_HPP
#define CLASS1_HPP
#warning Class1
class Class1 {
public:
void set(int i) { val = i; };
int get() {return val;};
int val=0;
};
#endif
Class1a.hpp
#ifndef CLASS1_HPP
#define CLASS1_HPP
#warning Class1a
class Class1 {
public:
void set(int i) { val = i; };
int get() {return -1*val;};
int val=0;
};
#endif
Class2.hpp:
#pragma once
#ifndef CLASS2_HPP
#define CLASS2_HPP
#include <iostream>
#include "Class1a.hpp"
using namespace std;
class Class2 {
public:
Class2(Class1 &c1) : c1(c1) {};
void print();
Class1& c1;
};
#endif
类2.cpp
#include "Class2.hpp"
void Class2::print() {
cout << c1.get() << endl;
}
但是,我得到以下输出:
$ g++ *.cpp; ./a.out
In file included from Class2.hpp:6:0,
from Class2.cpp:1:
Class1a.hpp:4:2: warning: #warning Class1a [-Wcpp]
#warning Class1a
^~~~~~~
-1
-1
我不太明白为什么 Class1(not-a) 从未被预编译器看到,尽管它首先包含在 main.cpp 中,所以我想我的问题延伸到那个...... [编辑:我无法重现预编译器问题不再出现,这现在产生与下面的代码相同的结果,正如我最初预期的那样]
编辑:删除 pragma 一次以避免进一步的混淆和偏差。
好的,因为人们似乎把这搞混了,这就是我所期望的预编译器的结果:
主.cpp:
#include <iostream>
using namespace std;
class Class1 {
public:
void set(int i) { val = i; };
int get() {return val;}; // <-- This line is different!
int val=0;
};
class Class2 {
public:
Class2(Class1 &c1) : c1(c1) {};
void print();
Class1& c1;
};
int main() {
Class1 c1;
Class2 c2(c1);
c1.set(1);
cout << c1.get() << endl;
c2.print();
return 0;
}
类2.cpp:
#include <iostream>
using namespace std;
class Class1 {
public:
void set(int i) { val = i; };
int get() {return -1*val;};
int val=0;
};
class Class2 {
public:
Class2(Class1 &c1) : c1(c1) {};
void print();
Class1& c1;
};
void Class2::print() {
cout << c1.get() << endl;
}
不知道为什么之前的预编译器不起作用。也许有人愿意解释,尽管这不是我的主要问题。而且,是的,我当然知道编写这样的代码是个坏主意,我只是想知道它是如何处理的。完全学术问题。
我现在发现可执行文件的输出取决于我为 g++ 声明 cpp 文件的顺序:
$ g++ main.cpp Class2.cpp
$ ./a.out
1
1
$ g++ Class2.cpp main.cpp
$ ./a.out
-1
-1
所以在某些时候,链接器似乎抓住了该方法的下一个最佳版本。为什么函数和变量似乎没有发生同样的情况,并且可以避免(因为这似乎至少应该产生警告)?
附加功能示例。主文件
#include <iostream>
using namespace std;
int get() {return 1;}
void print();
int main() {
cout << get() << endl;
print();
}
方法2.cpp
int get() { return -1; }
void print() {
cout << get() << endl;
}
在这里,多重定义被捕获:
$ g++ main.cpp method2.cpp
/tmp/ccjCKBLm.o: In function `get()':
method2.cpp:(.text+0x0): multiple definition of `get()'
/tmp/ccnvH0iR.o:main.cpp:(.text+0x0): first defined here
/tmp/ccnvH0iR.o: In function `main':
main.cpp:(.text+0x38): undefined reference to `print()'
collect2: error: ld returned 1 exit status
如果我将内联添加到函数中,则它会再次编译,但始终返回 1,尽管 g++ 的参数顺序与下面的获胜答案一致(没有双关语)。
解决方案
链接器如何处理 C++ 标头中的定义?
如果跨翻译单元有多个内联定义,则链接器选择一个,任何定义并丢弃其余定义。只需要一个,因为所有定义必须相同。
[pragma once] 可能会发生两个 cpp 文件从不同的头文件中声明相同的类名,但对此类仅头文件方法的定义不同,不是吗?
由于错误识别的 pragma 一次,这不会发生。标题的内容仍然是相同的,因此函数的定义是相同的。这种情况的问题是,在单个翻译单元中会有多个类型、非内联函数或变量的定义,这也违反了一个定义规则。幸运的是,这种类型的违规对于编译器来说是很容易诊断的。
我不太明白为什么预编译器从未见过 Class1(not-a)
它被预编译器看到。您可以从输出中看到它:
In file included from main.cpp:2:
./Class1.hpp:3:2: warning: Class1 [-W#warnings]
#warning Class1
^
1 warning generated.
In file included from Class2.cpp:1:
In file included from ./Class2.hpp:6:
./Class1a.hpp:3:2: warning: Class1a [-W#warnings]
#warning Class1a
不知道为什么那不起作用。
它没有用,因为你违反了 ODR。因此,您的程序格式错误。诊断此特定问题不需要实现(即工具链,即编译器、链接器等)。如果需要链接器来诊断问题,它必须检查每个翻译单元中的每个内联定义以确保它们是相同的。对于大型编译而言,这可能会变得非常昂贵。
为什么函数和变量似乎没有发生同样的情况
所有内联函数和内联变量都会发生同样的情况(内联变量是 C++17 中的新事物),而不仅仅是内联成员函数。对于非内联函数或非内联变量来说,这不是问题,因为 ODR 规则只允许在所有翻译单元中定义一个,因此当链接器找到多个翻译单元时,它可以很容易地判断出你搞砸了。
可以避免吗(因为这似乎至少应该产生警告)?
我还没有看到任何可以诊断此违规行为的链接器。我的最佳建议是要有良好的命名规则(使用命名空间以避免名称冲突)和测试规则(以便在发生冲突时检测到错误行为)。
推荐阅读
- javascript - 如何将此字符串解析为 json 对象?
- equality - 如何在 ojalgo 中测试两个 MatrixStore 的相等性直到一定程度?
- asp.net-mvc - 我认为 orchard cms V1.10.3 命令与最后一个 orchard 版本命令相比发生了变化。这是正确的?
- python - 为什么我的编码从 python 中的 UTF-8 中断?| PyCharm IDE
- c++ - 局部常量变量不是 constexpr 可评估的,但不知道为什么
- python - 如何使用 PySpark 将前 X 个单词从 SparseVector 获取到字符串数组
- python - 如何在 Python 中更快地计算中位数
- powershell - Powershell 将结果推荐到批处理脚本变量中
- entity-framework - 从对象的子列表中获取列表
- c# - 我可以依赖整个项目中枚举选项值的唯一性吗?