`

关于"符号已定义"的链接错误

    博客分类:
  • C++
阅读更多

在写C++程序的时候,在编译和连接的时候,经常容易看到:

LNK2005:symbol already defined

LNK1169:one or more multiply defined symbols found

之类的错误(这个是连接时报错)今天在网上看到一篇文章,是讲这方面相关的。


在C++源程序编译为exe(二进制文件)的时候,会经历两个阶段:

1.编译器把源文件编译成汇编代码,汇编器把它又翻译成机器指令,最后会得到一个.obj文件。

2.连接器会把这些.obj文件根据规则连接成为一个整体生成一个.exe文件。


符号:

符号是什么东西?它可以是一个变量,也可以是一个方法,也可是一个运算符,类之类的。其实就是一个有含义的名字。

强符号:在内存中已经开辟空间的,方法,已经初始化过的变量,之类。

弱符号:在内存中没有开辟空间的,类似变量的声明,和未初始化的定义(这个比声明强)。

在连接的时候,碰到同一个符号的时候,优先保存强符号,所以定义的时候强符号的只能有一个,不然连接的时候会提示符号名冲突。

 

链接库:(动态,静态)

在我们重用第三方代码的时候,如果以目标文件的形式重用代码的话,会非常麻烦。所以,在使用第三方代码的时候,通常会有一个链接库暴露那些能够提供给我们使用的符号。

 

符号解析:

链接器按照所有目标文件和库文件出现在命令行中的顺序从左至右依次扫描它们,在此期间它要维护若干个集合:

1.集合E是将被合并到一起组成可执行文件的所有目标文件集合;

2.集合U是未解析符号(unresolved symbols,比如已经被引用但是还未被定义的符号)的集合;

3.集合D是所有之前已被加入到E的目标文件定义的符号集合。一开始,E、U、D都是空的。

符号解析规则:

1.对命令行中的每一个输入文件f,链接器确定它是目标文件还是库文件,如果它是目标文件,就把f加入到E,并把f中未解析的符号和已定义的符号分别加入到U、D集合中,然后处理下一个输入文件。

2.如果f是一个库文件,链接器会尝试把U中的所有未解析符号与f中各目标模块定义的符号进行匹配。如果某个目标模块m定义了一个U中的未解析符号,那么就把m加入到E中,并把m中未解析的符号和已定义的符号分别加入到U、D集合中。不断地对f中的所有目标模块重复这个过程直至到达一个不动点(fixed point),此时U和D不再变化。而那些未加入到E中的f里的目标模块就被简单地丢弃,链接器继续处理下一输入文件。

3.如果处理过程中往D加入一个已存在的符号,或者当扫描完所有输入文件时U非空,链接器报错并停止动作。否则,它把E中的所有目标文件合并在一起生成可执行文件。

 

其它:

VC带的编译器名字叫cl.exe,它有这么几个与标准程序库有关的选项: /ML、/MLd、/MT、/MTd、/MD、/MDd。这些选项告诉编译器应用程序想使用什么版本的C标准程序库。/ML(缺省选项)对应单线程静态版的标准程序库(libc.lib);/MT对应多线程静态版标准库(libcmt.lib),此时编译器会自动定义_MT宏;/MD对应多线程DLL版(导入库msvcrt.lib,DLL是msvcrt.dll),编译器自动定义_MT和_DLL两个宏。后面加d的选项都会让编译器自动多定义一个_DEBUG宏,表示要使用对应标准库的调试版,因此/MLd对应调试版单线程静态标准库(libcd.lib),/MTd对应调试版多线程静态标准库(libcmtd.lib),/MDd对应调试版多线程DLL标准库(导入库msvcrtd.lib,DLL是msvcrtd.dll)。虽然我们的确在编译时明白无误地告诉了编译器应用程序希望使用什么版本的标准库,可是当编译器干完了活,轮到链接器开工时它又如何得知一个个目标文件到底在思念谁?为了传递相思,我们的编译器就干了点秘密的勾当。在cl编译出的目标文件中会有一个专门的区域(关心这个区域到底在文件中什么地方的朋友可以参考COFF和PE文件格式)存放一些指导链接器如何工作的信息,其中有一种就叫缺省库(default library),这些信息指定了一个或多个库文件名,告诉链接器在扫描的时候也把它们加入到输入文件列表中(当然顺序位于在命令行中被指定的输入文件之后)。说到这里,我们先来做个小实验。写个顶顶简单的程序,然后保存为main.c :

/* main.c */
int main() { return 0; }

用下面这个命令编译main.c(什么?你从不用命令行来编译程序?这个......) :

cl /c main.c

/c是告诉cl只编译源文件,不用链接。因为/ML是缺省选项,所以上述命令也相当于: cl /c /ML main.c 。如果没什么问题的话(要出了问题才是活见鬼!当然除非你的环境变量没有设置好,这时你应该去VC的bin目录下找到vcvars32.bat文件然后运行它。),当前目录下会出现一个main.obj文件,这就是我们可爱的目标文件。随便用一个文本编辑器打开它(是的,文本编辑器,大胆地去做别害怕),搜索"defaultlib"字符串,通常你就会看到这样的东西: "-defaultlib:LIBC -defaultlib:OLDNAMES"。啊哈,没错,这就
是保存在目标文件中的缺省库信息。我们的目标文件显然指定了两个缺省库,一个是单线程静态版标准库libc.lib(这与/ML选项相符),另外一个是oldnames.lib(它是为了兼容微软以前的C/C++开发系统)。

VC的链接器是link.exe,因为main.obj保存了缺省库信息,所以可以用

link main.obj libc.lib

或者

link main.obj

来生成可执行文件main.exe,这两个命令是等价的。但是如果你用

link main.obj libcd.lib

的话,链接器会给出一个警告: "warning LNK4098: defaultlib "LIBC" conflicts with use of other libs; use /NODEFAULTLIB:library",因为你显式指定的标准库版本与目标文件的缺省值不一致。通常来说,应该保证链接器合并的所有目标文件指定的缺省标准库版本一致,否则编译器一定会给出上面的警告,而LNK2005和LNK1169链接错误则有时会出现有时不会。那么这个有时到底是什么时候?呵呵,别着急,下面的一切正是为喜欢追根究底的你准备的。

建一个源文件,就叫mylib.c,内容如下:

/* mylib.c */
#include <stdio.h>

void foo()
{
   printf("%s","I am from mylib!\n");
}

cl /c /MLd mylib.c

命令编译,注意/MLd选项是指定libcd.lib为默认标准库。lib.exe是VC自带的用于将目标文件打包成程序库的命令,所以我们可以用

lib /OUT:my.lib mylib.obj

将mylib.obj打包成库,输出的库文件名是my.lib。接下来把main.c改成:

/* main.c */
void foo();

int main()
{
   foo();
   return 0;
}

cl /c main.c

编译,然后用

link main.obj my.lib

进行链接。这个命令能够成功地生成main.exe而不会产生LNK2005和LNK1169链接错误,你仅仅是得到了一条警告信息:"warning LNK4098: defaultlib "LIBCD" conflicts with use of other libs; use /NODEFAULTLIB:library"。我们根据前文所述的扫描规则来分析一下链接器此时做了些啥。

一开始E、U、D都是空集,链接器首先扫描到main.obj,把它加入E集合,同时把未解析的foo加入U,把main加入D,而且因为main.obj的默认标准库是libc.lib,所以它被加入到当前输入文件列表的末尾。接着扫描my.lib,因为这是个库,所以会拿当前U中的所有符号(当然现在就一个foo)与my.lib中的所有目标模块(当然也只有一个mylib.obj)依次匹配,看是否有模块定义了U中的符号。结果mylib.obj确实定义了foo,于是它被加入到E,foo从U转移到D,mylib.obj引用的printf加入到U,同样地,mylib.obj指定的默认标准库是libcd.lib,它也被加到当前输入文件列表的末尾(在libc.lib的后面)。不断地在my.lib库的各模块上进行迭代以匹配U中的符号,直到U、D都不再变化。很明显,现在就已经到达了这么一个不动点,所以接着扫描下一个输入文件,就是libc.lib。链接器发现libc.lib里的printf.obj里定义有printf,于是printf从U移到D,而printf.obj被加入到E,它定义的所有符号加入到D,它里头的未解析符号加入到U。链接器还会把每个程序都要用到的一些初始化操作所在的目标模块(比如crt0.obj等)及它们所引用的模块(比如malloc.obj、free.obj等)自动加入到E中,并更新U和D以反应这个变化。事实上,标准库各目标模块里的未解析符号都可以在库内其它模块中找到定义,因此当链接器处理完libc.lib时,U一定是空的。最后处理libcd.lib,因为此时U已经为空,所以链接器会抛弃它里面的所有目标模块从而结束扫描,然后合并E中的目标模块并输出可执行文件。

上文描述了虽然各目标模块指定了不同版本的缺省标准库但仍然链接成功的例子,接下来你将目睹因为这种不严谨而导致的悲惨失败。

修改mylib.c成这个样子:

#include <crtdbg.h>

void foo()
{
   // just a test , don't care memory leak
   _malloc_dbg( 1, _NORMAL_BLOCK, __FILE__, __LINE__ );
}

其中_malloc_dbg不是ANSI C的标准库函数,它是VC标准库提供的malloc的调试版,与相关函数配套能帮助开发者抓各种内存错误。使用它一定要定义_DEBUG宏,否则预处理器会把它自动转为malloc。继续用

cl /c /MLd mylib.c
lib /OUT:my.lib mylib.obj

编译打包。当再次用

link main.obj my.lib

进行链接时,我们看到了什么?天哪,一堆的LNK2005加上个贵为"fatal error"的LNK1169垫底,当然还少不了那个LNK4098。链接器是不是疯了?不,你冤枉可怜的链接器了,我拍胸脯保证它可是一直在尽心尽责地照章办事。

一开始E、U、D为空,链接器扫描main.obj,把它加入E,把foo加入U,把main加入D,把libc.lib加入到当前输入文件列表的末尾。接着扫描my.lib,foo从U转移到D,_malloc_dbg加入到U,libcd.lib加到当前输入文件列表的尾部。然后扫描libc.lib,这时会发现libc.lib里任何一个目标模块都没有定义_malloc_dbg(它只在调试版的标准库中存在),所以不会有任何一个模块因为_malloc_dbg而加入E,但是每个程序都要用到的初始化模块(如crt0.obj等)及它们所引用的模块(比如malloc.obj、free.obj等)还是会自动加入到E中,同时U和D被更新以反应这个变化。当链接器处理完libc.lib时,U只剩_malloc_dbg这一个符号。最后处理libcd.lib,发现dbgheap.obj定义了_malloc_dbg,于是dbgheap.obj加入到E,它里头的未解析符号加入U,它定义的所有其它符号也加入D,这时灾难便来了。之前malloc等符号已经在D中(随着libc.lib里的malloc.obj加入E而加入的),而dbgheap.obj又定义了包括malloc在内的许多同名符号,这引发了重定义冲突,链接器只好中断工作并报告错误。

现在我们该知道,链接器完全没有责任,责任在我们自己的身上。是我们粗心地把缺省标准库版本不一致的目标文件(main.obj)与程序库(my.lib)链接起来,导致了大灾难。解决办法很简单,要么用/MLd选项来重编译main.c;要么用/ML选项重编译mylib.c。

在上述例子中,我们拥有库my.lib的源代码(mylib.c),所以可以用不同的选项重新编译这些源代码并再次打包。可如果使用的是第三方的库,它并没有提供源代码,那么我们就只有改变自己程序的编译选项来适应这些库了。但是如何知道库中目标模块指定的默认库呢?其实VC提供的一个小工具便可以完成任务,这就是dumpbin.exe。运行下面这个命令

dumpbin /DIRECTIVES my.lib

然后在输出中找那些"Linker Directives"引导的信息,你一定会发现每一处这样的信息都会包含若干个类似"-defaultlib:XXXX"这样的字符串,其中XXXX便代表目标模块指定的缺省库名。

知道了第三方库指定的默认标准库,再用合适的选项编译我们的应用程序,就可以避免LNK2005和LNK1169链接错误。喜欢IDE的朋友,你一样可以到 "Project属性" -> "C/C++" -> "代码生成(code generation)" -> "运行时库(run-time library)" 项下设置应用程序的默认标准库版本,这与命令行选项的效果是一样的。

链接一个静态LIB,不在客户端代码中使用它的任何变量和代码,但要让这个LIB的全局变量被初始化的方法是:
这个链接库头文件应该这么写:
extern CMyClass *g_pObject ;
static void *__dummy = (void*)g_pObject ;

// lib.cpp
CMyClass *g_pObject = CMyClass::Instance() ; // Singleton

__dummy会出现在任何包含这个头文件的CPP文件的OBJ中,所以LINKER会把静态库中的g_pObject链接到Exe中,包括它的构造和析构

<iostream>很有意思,其中有一行
static ios_base::Init _Ios_init;
ios_base::Init是个类,在类的构造中判断构造是否第一次被调用,如果是,则初始化cout,cin,cerr等
在类的析构中判断这是不是最后一次构造,如果是,则调用cout.flush() .... (basic_ostream等的析构并没有调用flush)
具体怎么判断是否第一次调用构造,是否最后一次调用析构,那是用一个int的静态类成员来计算...

其实这样会增加exe文件的尺寸,降低程序启动速度....

分享到:
评论

相关推荐

    arm79汇编伪指令介绍

    使用 IMPORT 或 EXTERN 声明外部标号时,若连接器在连接处理时不能解释该符号,而伪指令中没有[WEAK]选项,则连接器会报告错误,若伪指令中有[WEAK]选项,则连接器不会报告错误,而是进行下面的操作: 1.如果该符号被 B ...

    代码语法错误分析工具pclint8.0

    选项还可以放在宏定义中,例如: #define DIVZERO(x) /*lint -save -e54 */ ((x) /o) /*lint -restore */ LINT的选项很多共有300多种,大体可分为以下几类: 1)错误信息禁止选项 该类选项是用于禁止生成某...

    单片机总结报告.doc

    (1)将wave6000仿真软件复位后有如下没注意到的语法错误.: 1.AX EQU 20H 错误提示:"行:1,错误334:重复定义:AX&lt;NONAME1.ASM&gt;" 错误原因是AX寄存器在仿真软件里的设置汇编预定义符号里已经定义过了。解决办法一 ...

    MC78XX和LM78XX及MC78XXA稳压器PCB设计教程.zip

    MC78XX/LM78XX/MC78XXA系列三端正调压器在TO-220/D-PAK封装中提供,并具有几个固定的输出电压,使其在...而ERC 检查需无任何错误项目,如线路连接有误、图件属性定义有误、图件少放/浮接、样板套用有误等,都会扣分。

    详解C语言中symlink()函数和readlink()函数的使用

    函数说明:symlink()以参数newpath 指定的名称来建立一个新的连接(符号连接)到参数oldpath 所指定的已存在文件. 参数oldpath 指定的文件不一定要存在, 如果参数newpath 指定的名称为一已存在的文件则不会建立连接. ...

    计算机基础试题-.doc

    30、下面关于鼠标器的表达中,错误的选项是〔鼠标器只能使用PS/2接口与主机连接〕 。 31、在使用〔Windows 98〕操作系统的PC机上第一次使用优盘时必须安装驱动程序。 32、为了提高处理速度,Pentium4处理器采取了一...

    php at(@)符号的用法简介

    } 如果连接数据库不成功的,前面的“@”就能把错误显示给抑制住,也就是不会显示错误,然后再抛出异常,显示自己定义的异常处理,添加这个只是为了让浏览者不看到,不友好的页面,并不能抑制住错误,只能抑制显示...

    c语言你知识点总结

    (程序编辑-程序编译-程序连接-程序运行) 第三节、标识符 1、标识符(必考内容): 合法的要求是由字母,数字,下划线组成。有其它元素就错了。 并且第一个必须为字母或则是下划线。第一个为数字就错了 预定义...

    VBScript 语言参考

    FormatCurrency 函数 返回的表达式为货币值格式,其货币符号采用系统控制面板中定义的。 FormatDateTime 函数 返回格式化为日期或时间的表达式。 FormatNumber 函数 返回格式化为数的表达式。 FormatPercent 函数...

    coc-css:coc.nvimCSS语言服务器扩展

    findDefinition查找给定位置处符号的定义。 findReferences在给定位置查找对该符号的所有引用。 findDocumentHighlights查找连接到给定位置的所有符号。 findDocumentSymbols提供给定文档中的所有符号doCodeActions...

    VBSCRIP5 -ASP用法详解

    FormatCurrency 函数 返回的表达式为货币值格式,其货币符号采用系统控制面板中定义的。 FormatDateTime 函数 返回格式化为日期或时间的表达式。 FormatNumber 函数 返回格式化为数的表达式。 FormatPercent 函数...

    VBScript 语言参考中文手册CHM

    FormatCurrency 函数 返回的表达式为货币值格式,其货币符号采用系统控制面板中定义的。 FormatDateTime 函数 返回格式化为日期或时间的表达式。 FormatNumber 函数 返回格式化为数的表达式。 FormatPercent 函数...

    C语言程序设计标准教程

    也可以用宏定义使一个符号常量来表示一个结构类型,例如: #define STU struct stu STU { int num; char name[20]; char sex; float score; }; STU boy1,boy2; 2. 在定义结构类型的同时说明结构变量。例如: ...

    WAP 无线应用协议

    1.7 术语定义 10 1.8 缩略语 11 1.9 参考标准 12 1.10 参考资料 13 第二部分 应用层 第2章 无线应用环境概述 15 2.1 范围 15 2.2 WAE 文档 15 2.2.1 WAE 文档集 15 2.2.2 文档结构 16 2.3 WAE 的工作计划 16 2.3.1 ...

    C语言深度解剖

    第一章 关键字 1.1 最宽恒大量的关键字auto 1.2 最快的关键字register 1.3 最不实名的关键字static 1.4 基本数据类型 1.5 最冤枉的关键字sizeof 1.6 if/else组合 1.7 switch/case组合 ...5.3 常见内存错误与对策

    VBSCRIPT中文手册

    FormatCurrency 函数 返回的表达式为货币值格式,其货币符号采用系统控制面板中定义的。 FormatDateTime 函数 返回格式化为日期或时间的表达式。 FormatNumber 函数 返回格式化为数的表达式。 FormatPercent 函数...

    vb Script参考文档

    FormatCurrency 函数 返回的表达式为货币值格式,其货币符号采用系统控制面板中定义的。 FormatDateTime 函数 返回格式化为日期或时间的表达式。 FormatNumber 函数 返回格式化为数的表达式。 FormatPercent 函数...

    gutentags_plus:在gutentags中使用gtags的正确方法

    如果当前工作目录已更改,请重置gtags-cscope连接(否则,它将在quickfix中获得错误的文件路径)。 Gutentags也可以自动连接gtags数据库,但是它正在尝试在更新后连接所有数据库。 结果,当您查询符号定义或引用时...

    oracle学习文档 笔记 全面 深刻 详细 通俗易懂 doc word格式 清晰 连接字符串

     数据定义语言Data Definition Language(DDL),用来建立数据库、数据对象和定义其列。例如:CREATE、DROP、ALTER等语句。  数据操作语言Data Manipulation Language(DML),用来插入、修改、删除、查询,可以...

    C语言精典版本C程序设计语言

    Dave Prosser回答了很多关于ANSI标准的细节问题。我们广泛地使用了Bjarne Stroustrup的C++的翻译程序来部分测试我们的程序。Dave Kristol为我们提供了一个ANSI C编译器进行最终测试。Rich Drechsler帮助我们进行了...

Global site tag (gtag.js) - Google Analytics