3

考虑这个综合示例。我的 Visual Studio 2010 解决方案中有两个本机 C++ 项目。一个是控制台exe,另一个是lib。

lib中有两个文件:

// TImage.h

template<class T> class TImage
{
public:
  TImage()
  {
#ifndef _LIB
    std::cout << "Created (main), ";
#else
    std::cout << "Created (lib), ";
#endif
    std::cout << sizeof(TImage<T>) << std::endl;
  }

#ifdef _LIB
  T c[10];
#endif
};

void CreateImageChar();
void CreateImageInt();

// TImage.cpp

void CreateImageChar()
{
  TImage<char> image;
  std::cout << sizeof(TImage<char>) << std::endl;
}
void CreateImageInt()
{
  TImage<int> image;
  std::cout << sizeof(TImage<int>) << std::endl;
}

还有一个exe文件:

// main.cpp

int _tmain(int argc, _TCHAR* argv[])
{
  TImage<char> image;
  std::cout << sizeof(TImage<char>) << std::endl;

  CreateImageChar();
  CreateImageInt();

  return 0;
}

我知道,我实际上不应该这样做,但这只是为了了解正在发生的事情。那就是,会发生什么:

// cout:
Created (main), 1
1
Created (main), 1
10
Created (lib), 40
40

那么这到底是怎么发生的,链接器用TImage<char>exe 的版本覆盖了 lib 的版本TImage<char>?但是由于没有 exe 的版本TImage<int>,它保留了 lib 的版本TImage<int>?.. 这种行为是否标准化,如果是,我在哪里可以找到描述?

更新:下面给出的效果解释是正确的,谢谢。但问题是“这到底是怎么发生的”?..我希望得到一些链接器错误,比如“多重定义的符号”。所以最合适的答案来自Antonio Pérez 的回复

4

6 回答 6

2

模板代码创建重复的目标代码。

当您实例化模板时,编译器会复制您提供的类型的模板代码。因此,在TImage.cpp编译时,您将获得模板的两个版本的目标代码,一个用于char,一个用于int in TImage.o。然后main.cpp被编译,你会得到一个新版本的char in模板main.o。然后链接器碰巧使用了main.o always中的那个。

这解释了为什么您的输出会产生“已创建”行。但是看到第 3 行、第 4 行关于对象大小的不匹配有点令人费解:

Created (main), 1
10

这是由于编译器sizeof在编译时解析了运算符。

于 2011-07-29T07:24:07.533 回答
1

我在这里假设您正在构建一个静态库,因为您没有任何__decelspec(dllexport)extern "C"在代码中。这里发生的情况如下。TImage<char>编译器TImage<int>为您的 lib创建一个实例。它还为您的可执行文件创建一个实例。当链接器加入静态库和可执行文件的对象一起时,重复的代码将被删除。请注意,静态库像目标代码一样链接,因此如果您创建一个大的可执行文件或多个静态库和一个可执行文件,这没有任何区别。如果您要构建一个可执行文件,则结果将取决于对象链接的顺序;又名“未定义”。

如果将库更改为 DLL,则行为会发生变化。由于您正在调用 DLL 的边界,因此每个人都需要其TImage<char>. 在大多数情况下,DLL 的行为更像您期望的库工作。静态库通常只是一种方便,因此您无需将代码放入您的项目中。

注意:这仅适用于 Windows。在 POSIX 系统上,*.a 文件的行为类似于 *.so 文件,这让编译器开发人员非常头疼。

编辑:永远不要通过 DLL 边界传递 TImage 类。这将确保崩溃。这与微软的 std::string 实现在混合调试和发布版本时崩溃的原因相同。它们完全按照您仅使用 NDEBUG 宏所做的操作。

于 2011-07-29T08:06:36.003 回答
0

内存布局是一个编译时概念;它与链接器无关。该main函数认为 TImage 比CreateImage...函数小,因为它是用不同版本的 TImage 编译的。

如果CreateImage...函数在头文件中定义为内联函数,它们将成为 main.cpp 编译单元的一部分,因此将报告与报告相同的大小特征main

这也与模板以及它们何时被实例化无关。如果 TImage 是一个普通类,您会观察到相同的行为。

编辑:我刚刚注意到 cout 的第三行不包含“Created (lib), 10”。假设这不是一个错字,我怀疑发生的事情是CreateImageChar没有内联对构造函数的调用,因此使用的是 main.cpp 的版本。

于 2011-07-29T07:21:15.747 回答
0

当您使用它时,编译器将始终实例化您的模板 - 如果定义可用。

这意味着,它为所需的专业化生成所需的函数、方法等,并将它们放在目标文件中。这就是为什么您需要为您正在使用的特定专业化提供可用定义(通常在头文件中)或现有实例化(例如在另一个目标文件或库中)的原因。

现在,在链接时,可能会发生通常不允许的情况:每个类/函数/方法有多个定义。对于模板,这是特别允许的,编译器将为您选择一个定义。这就是你的情况正在发生的事情,你称之为“压倒一切”。

于 2011-07-29T07:21:15.173 回答
0

模板在编译过程中创建类的副本(即空间)。因此,当您使用太多模板时,智能编译器会尝试根据模板的参数化来优化它们。

于 2011-07-29T07:45:06.610 回答
0

在您的库中,您有一个TImage在构造时打印“lib”,并包含一个T. 其中有两种,一种用于int,一种用于char

在 main 中,您有一个TImage在构造上打印“main”,并且不包含T. 在这种情况下只有char版本;因为您从不要求int创建版本。

当你去链接时,链接器选择了两个TImage<char>构造函数中的一个作为官方的;它恰好选择了main的版本。这就是为什么您的第三行打印“main”而不是“lib”的原因;因为您正在调用该版本的构造函数。通常,您不在乎调用哪个版本的构造函数……它们都必须相同,但是您违反了该要求。

这是重要的部分:您的代码现在已损坏。在您的库函数中,您希望看到该数组,charTImage<char> 构造函数永远不会创建它。 此外,想象一下如果你说new TImage<char>within main,并将指针传递给你的库中的一个函数,它会被删除。 main分配一个字节的空间,库函数尝试释放十个。或者,如果您的CreateImageChar方法返回了TImage<char>它创建的...main将在堆栈上为返回值分配一个字节,并CreateImageChar用十个字节的数据填充它。等等。

于 2011-07-29T08:02:21.583 回答