65.9K
CodeProject 正在变化。 阅读更多。
Home

链接器错误,CString、ATL、MFC 和您!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (22投票s)

2003年5月4日

7分钟阅读

viewsIcon

249942

downloadIcon

1058

由于 CStringT 模板类和 ATL 与 MFC 问题导致的链接器错误

Sample Image - maximum width is 600 pixels

引言

场景:创建一个基于 ATL 的类库,它既可以被基于 ATL 的控制台应用程序使用,也可以被基于 MFC/ATL 的 GUI 应用程序使用。该类库不能包含任何 MFC 链接。GUI 应用程序必须使用 ATL。控制台应用程序不得使用任何 MFC,或链接到任何使用 MFC 的库。

好的,这听起来像是一个非常简单的任务,但是如果你不习惯使用一些链接器设置,你很快就会陷入困境。你总是可以通过在链接器命令行选项中添加 /FORCE:MULTIPLE 来走捷径,但那样每次编译时,你都会看到大量的链接器警告。这不是通往光明之路的方法!我们的目标是制作一个行为良好的库,它可以在不指定任何特殊选项的情况下链接,除了导入库和包含头文件。

术语

我将把任何链接到库的应用程序称为“消费者”。消费者将 #include 库头文件,然后在链接阶段链接到库文件。在示例中,ConsoleAppApplication 都是消费者。

了解你的敌人(你的链接器错误宿敌)

图 0:设置构建配置。

如果你用“错误百出”的配置设置(图 0)编译示例项目,你将看到大量令人讨厌的错误消息。LNK2005LNK2019LNK4098LNK1169LNK1120

LNK2005 <symbol> 已在 <library> 中定义(<生成错误的源文件>)
LNK2019 未解析的外部符号“<symbol signature>”在函数 <function signature> 中引用
LNK4098 默认库“<library name>”与其他库的使用冲突;请使用 /NODEFAULTLIB:library
LNK1169 发现一个或多个重复定义的符号
LNK1120 <X> 个未解析的外部符号

“错误百出”配置中可能出现的链接器消息表。

清除垃圾

让我们从大家最不喜欢的、经常出现的 LNK4098 开始。这个错误经常与 LNK2005 结伴而行。你可以通过将库的链接器选项设置为与将使用你的库的最受限制的应用程序匹配来修复这两个错误。在这种情况下,MFC GUI 应用程序要求它与多线程 DLL 链接。如果你在调试设置中编译,你必须与多线程调试 DLL 链接(参见图 1)。一旦你为你的库设置了这个选项并重新编译它,你将看到 LNK4098LNK2005 迅速撤退。因为你还将把这个库与 ATL 控制台应用程序一起使用,所以你必须也将控制台应用程序的运行时库设置为多线程 DLL。注意:如果你正在使用的其他库暴露的函数签名与你的库中的函数签名相似,那么你仍然可能会得到 LNK2005 错误。找到有问题的库,并将其从你的项目中删除。如果它是一个默认库,并且你确定你不需要它,你可以将其添加到忽略默认库列表中(参见图 2)。

图 1:将运行时库设置为与消费者应用程序使用的运行时库匹配。如果你在发布配置中设置此选项,则应选择多线程 DLL (/MD)。

图 2:有时,在没有其他选择的情况下,你可以忽略编译器抱怨的库。这不是通往光明之路的方法。

现在,你可以尝试在“错误较少”配置中重新编译示例,你会发现我们已成功消除了 LNK4098LNK2005。你会立即注意到我们仍然没有摆脱 LNK2019LNK1120 错误。真正奇怪的是,ConsoleApp 链接到库没有任何问题,并且从 ConsoleApp 调用库与从 Application 调用库是相同的。这里发生的事情是,在 atlstr.h(包含 CString 的 ATL 定义的头文件)的底层,VC++ 7 中 CString 的定义与 VC++ 6 中的定义截然不同,它是一个模板类,而 VC++ 6 的实现只是一个类。问题是 MFC 的 CString 默认模板与 ATL 使用的不同。我们需要做的是让库和消费者应用程序都使用相同的链接器签名。然后,链接器可以在链接时在库中找到签名,你就不会得到那些讨厌的 LNK2019LNK1120 错误。

MFC 将 CString 定义为

typedef ATL::CStringT< TCHAR, StrTraitMFC< TCHAR > > CString;

而 ATL 将 CString 定义为(当定义了 _ATL_CSTRING_NO_CRT 时)

typedef CStringT< TCHAR, StrTraitATL< TCHAR > > CString;

StrTrait 中的这种差异是所有 LNK2019LNK1120 问题的原因。

我将要使用的解决方案来自这篇 MSDN 文章,它展示了类似的策略,但不幸的是,MSDN 上的方法使库链接到 MFC,而我们不希望我们的 ATL 库这样做。我将要做的是改变库中的函数,在方法调用中改用 CAtlString 而不是 CString。如果你查看 CAtlString 的定义,你会注意到它只是 CStringT#define,带有一个特定的模板。因为 CAtlString 明确声明了参数的签名,这使得链接器能够找到库中的函数,因为它正在寻找一个带有 CStringT< TCHAR, StrTraitATL< TCHAR > > 类型参数的函数。

此外,我正在库的头文件中包含 atlstr.h。这样,当头文件包含在消费者应用程序中时,CAtlString 将为头文件定义,并且你不会收到 CAtlString 的未定义符号链接器错误。这也确保了消费者应用程序不需要任何奇怪的配置更改;只需包含并使用即可!要查看应用程序正确链接,请将配置更改为“Debug”或“Release”并编译它。

代码继续

由于附件样本中有三个独立的项目,并且它们大多处于默认状态,我不会详细介绍它们。让我们重点介绍一下,并查看源代码中的其他杂项注释。

#pragma once

// Include this here, so that any consumer code will automatically import 
// atlstr so that StrTraitATL will be defined for this header.
#include <atlstr.h>

class CHelloLib
{
public:
    CHelloLib(void);
    ~CHelloLib(void);

#ifndef _ERRORS_GALORE
    // By specifying CAtlString as the parameter token, the linker will 
        // know to use the right ATL version of CStringT, and thus find this 
        // function's signature. 
    void HelloWorld(CAtlString message);
#else
    // Cause LNK2019 by letting the linker try to guess which CStringT 
        // template to use to resolve this function signature. The linker 
        // will try to select the MFC version by default, and not find this 
        // function in the library. This causes the LNK2019.
    void HelloWorld(CString message);
#endif
};

上面,我们看到库头文件的源代码。当包含在消费者应用程序中时,该文件将包含 atlstr.h,以便定义 CAtlString

// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//
#pragma once#define WIN32_LEAN_AND_MEAN //Exclude rarely-used stuff from 
                                        // Windows headers
// TODO: reference additional headers your program requires here

#include <atlbase.h>
#include <atlstr.h>
// Define CAtlString as CString so that the IDE will show the intellesense 
// for CString instead of showing nothing for CAtlString. CAtlString will 
// still be used in library generation so that the right linking can be done.
#define CAtlString CString

这是库的 stdafx.h。你应该注意底部我将 CAtlString 定义为 CString。这似乎适得其反,因为在 HelloLib.h 头文件中,我刚刚将所有 CString 更改为 CAtlString!但是,如果没有这个 #define,智能感知似乎不想显示我如此喜欢的那种快速选择函数列表。因此,通过在(且仅限库)的 stdafx.h 中进行此转换,智能感知被启用。当库编译时,编译器会自动选择 ATL 的正确 CStringT 定义,因为这是一个基于 ATL 的项目。我将此 #define 放在 stdafx.h 中,以便它不会包含在将由消费者使用的库头文件中。这样,当消费者包含库时,CAtlString 声明就会完全生效。

// ConsoleApp.cpp : Defines the entry point for the console application.
//

#ifdef _DEBUG
    #pragma comment(lib, "..\\Library\\Debug\\Library.lib")
#else
    #pragma comment(lib, "..\\Library\\Release\\Library.lib")
#endif 

#include "stdafx.h"
#include "..\Library\HelloLib.h"

int _tmain(int argc, _TCHAR* argv[])
{
    printf("Calling into library.\n");

    CHelloLib hw;
    hw.HelloWorld("I was called from a console application.");

    return 0;
}

上面,ConsoleApp.cpp 的代码向您展示了如何在包含库的调试版本和发布版本之间进行选择。我发现这是一个非常方便的技巧,因为它指示链接器引入一个库,而无需调整项目设置!应用程序演示也使用了相同的技巧,所以我就不在这里重复了。

关注点

我认为 ATL/MFC 有点问题,因为它们是可以混合使用的技术,它们也应该能够相互配合。上述解决方案相当粗糙,尽管它能简单地完成工作。我更希望 ATL 或 MFC 能够检查是否包含了其中一个,并使用正确的签名,但这也许可以作为 VC++ 8 的目标...

历史

  • 2003 年 4 月 11 日 - 初次创建/光明之路探索
  • 2003年4月15日——我没我想象的那么聪明/路径突然变得模糊...不得不改变几件事以避免在导入库头文件时在MFC应用程序中重新定义CString。路径现在又清晰可见了。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.