首页 > 解决方案 > 链接器如何处理 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++linkertoolchain

解决方案


链接器如何处理 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 规则只允许在所有翻译单元中定义一个,因此当链接器找到多个翻译单元时,它可以很容易地判断出你搞砸了。

可以避免吗(因为这似乎至少应该产生警告)?

我还没有看到任何可以诊断此违规行为的链接器。我的最佳建议是要有良好的命名规则(使用命名空间以避免名称冲突)和测试规则(以便在发生冲突时检测到错误行为)。


推荐阅读