`
tinggo
  • 浏览: 44910 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

链接器都干了些什么

阅读更多
目前项目在不停的增长,我想还是在它规模仍旧很小的时候把它的模块分清楚,不同模块分到不同的 projects 里面,这里面出现了很多问题,也反映了我知识上的很多不足。

1 , project 最后的输出要设置清楚,有的是 static lib ,有的是 dll ,有的是 exe ,不一样的输出要设置好,它们都是 linker 的成果,但是以不同的方式应用。

2 , project dependency 设置好, build order 什么的,通过这些把一系列的 project 联系起来。

3 , project 之间的联系就通过之间的 lib , dll 来联系,这时候就涉及到 linker 的工作了。



许多 Visual C++ 的使用者都碰到过 LNK2005:symbol already defined 和 LNK1169:one or more multiply defined symbols found 这样的链接错误,而且通常是在使用第三方库时遇到的。对于这个问题,有的朋友可能不知其然,而有的朋友可能知其然却不知其所以然,那么本文就试图为大家彻底解开关于它的种种疑惑。

大家都知道,从 C/C++ 源程序到可执行文件要经历两个阶段 :

(1) 编译器将源文件编译成汇编代码,然后由汇编器 (assembler) 翻译成机器指令 ( 再加上其它相关信息 ) 后输出到一个个目标文件 (object file, VC 的编译器编译出的目标文件默认的后缀名是 .obj) 中;

(2) 链接器 (linker) 将一个个的目标文件 ( 或许还会有若干程序库 ) 链接在一起生成一个完整的可执行文件。

    编译器编译源文件时会把源文件的全局符号 (global symbol) 分成强 (strong) 和弱 (weak) 两类传给汇编器,而随后汇编器则将强弱信息编码并保存在目标文件的符号表中。那么何谓强弱呢?编译器认为函数与初始化了的全局变量都是强符号,而未初始化的全局变量则成了弱符号。比如有这么个源文件 :

extern int errorno;

int buf[2] = {1,2};

int *p;

int main()

{

   return 0;

}

其中 main 、 buf 是强符号, p 是弱符号,而 errorno 则非强非弱,因为它只是个外部变量的使用声明。

有了强弱符号的概念,我们就可以看看链接器是如何处理与选择被多次定义过的全局符号 :

规则 1: 不允许强符号被多次定义 ( 即不同的目标文件中不能有同名的强符号 ) ;

规则 2: 如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么选择强符号;

规则 3: 如果一个符号在所有目标文件中都是弱符号,那么选择其中任意一个;

    由上可知多个目标文件不能重复定义同名的函数与初始化了的全局变量,否则必然导致 LNK2005 和 LNK1169 两种链接错误。可是,有的时候我们并没有在自己的程序中发现这样的重定义现象,却也遇到了此种链接错误,这又是何解?嗯,问题稍微有点儿复杂,容我慢慢道来。

    众所周知, ANSI C/C++ 定义了相当多的标准函数,而它们又分布在许多不同的目标文件中,如果直接以目标文件的形式提供给程序员使用的话,就需要他们确切地知道哪个函数存在于哪个目标文件中,并且在链接时显式地指定目标文件名才能成功地生成可执行文件,显然这是一个巨大的负担。所以 C 语言提供了一种将多个目标文件打包成一个文件的机制,这就是静态程序库 (static library) 。开发者在链接时只需指定程序库的文件名,链接器就会自动到程序库中寻找那些应用程序确实用到的目标模块,并把 ( 且只把 ) 它们从库中拷贝出来参与构建可执行文件。几乎所有的 C/C++ 开发系统都会把标准函数打包成标准库提供给开发者使用 ( 有不这么做的吗? ) 。

    程序库为开发者带来了方便,但同时也是某些混乱的根源。我们来看看链接器是如何解析 (resolve) 对程序库的引用的。

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

(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

( ML 要是大写的,否则不认。)

命令编译,注意 /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 。链接器是不是疯了?不,你冤枉可怜的链接器了,我拍胸脯保证它可是一直在尽心尽责地照章办事。

输出信息:

C:\>link main.obj my.lib

Microsoft (R) Incremental Linker Version 6.00.8168

Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

 

LIBCD.lib(dbgheap.obj) : error LNK2005: _malloc already defined in LIBC.lib(mall

oc.obj)

LIBCD.lib(dbgheap.obj) : error LNK2005: __nh_malloc already defined in LIBC.lib(

malloc.obj)

LIBCD.lib(dbgheap.obj) : error LNK2005: __heap_alloc already defined in LIBC.lib

(malloc.obj)

LIBCD.lib(dbgheap.obj) : error LNK2005: _free already defined in LIBC.lib(free.o

bj)

LIBCD.lib(sbheap.obj) : error LNK2005: __get_sbh_threshold already defined in LI

BC.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: __set_sbh_threshold already defined in LI

BC.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_heap_init already defined in LIBC.

lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_find_block already defined in LIBC

.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_free_block already defined in LIBC

.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_alloc_block already defined in LIB

C.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_alloc_new_region already defined i

n LIBC.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_alloc_new_group already defined in

LIBC.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_resize_block already defined in LI

BC.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_heapmin already defined in LIBC.li

b(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_heap_check already defined in LIBC

.lib(sbheap.obj)

LIBCD.lib(sbheap.obj) : error LNK2005: ___sbh_threshold already defined in LIBC.

lib(sbheap.obj)

LINK : warning LNK4098: defaultlib "LIBCD" conflicts with use of other libs; use

/NODEFAULTLIB:library

main.exe : fatal error LNK1169: one or more multiply defined symbols found

 

    一开始 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

输出信息:

C:\>dumpbin /DIRECTIVES my.lib

Microsoft (R) COFF Binary File Dumper Version 6.00.8168

Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

 

 

Dump of file my.lib

 

File Type: LIBRARY

 

   Linker Directives

   -----------------

   -defaultlib:LIBCD

   -defaultlib:OLDNAMES

 

  Summary

 

           8 .data

          27 .drectve

          18 .text

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

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

这是一片非常好的文章,如果你看到了这里的话,那我只能恭喜你成功了!

Have  fun !

分享到:
评论

相关推荐

    java变态面试题.pdf

    通常用来干些什么? * Struts1 和 Struts2 的区别:Struts1 是基于 Servlet 的,而 Struts2 是基于 Filter 的。 * Ajax 的主要核心技术亮点:异步请求,可以实现实时更新页面数据。 五、数据库知识点 * 数据库连接...

    建筑消防工程主要包括些系统.doc

    - 水泵接合器:连接消防车,为建筑物外的水源提供接入点。 3. **消火栓、消防炮系统**: - 室内外消火栓:提供人员手动灭火的接口。 - 固定消防水炮:远程控制,能自动或手动瞄准火源进行灭火。 - 消火栓按钮、...

    s7-300工程实战总结.pdf

    84. 应用软件冗余当一个长度错误出现导致CPU处于STOP模式应该作些什么? 22 85. 在S7程序中,有许多FC、FB块, 我怎样对其中的一些块进行保护, 而其它的块可以是开放的呢? 22 86. SFB41,SFB42,SFB43和FB41,FB42,FB43的...

    更新IOCP_Socket

    /* 完成端口IOCP 最近写的一个程序要用完成端口,于是我在网上找了些完成端口的代码, 下载下来的代码,几乎都不...用这个类来做一个CC攻击器,效果应该不错,connect设计之初,就是为了来干这些坏事的,哈哈~~~ 2008-10-6 */

    DIS测定电源电动势和内阻.pdf

    1. 将电压、电流传感器分别接入数据采集器第一、二输入口。 2. 将实验电路板的开关 S 断开,电压、电流传感器的测量夹分别接入实验板的 U3、I 端。 3. 连接滑动变阻器 R′,并把阻值调到最大,开关 S 闭合。 4. 打开...

    粤沪版八年级物理下学期期中考试试卷.doc

    9. 正确的说法是干电池、蓄电池、发电机、电动机都是电源。 10. 电压表读数最大时,是接在 AC 处。 11. 电压表的示数表示 L2 两端的电压。 12. 这说明 L1 灯丝断了。 二、填空题: 1. 固体分为非晶体和晶体两类...

    电脑主机风扇声音四大原因及解决办法.pdf

    首先拆掉风扇和连接风扇的底座,风扇是用螺丝连到底座上的,底座是用卡子连接到主板上的 ;风扇及底座拆完后,就是cpu 了,它旁边有一个压杆,看好方向,将压杆抬起,垂直将cpu 拿出来就可以了。整个 cpu 风扇就拆...

    iOS-面试宝典3.0.pdf

    block在ARC中和传统的MRC中的行为和用法有没有什么区别,需要注意些什么 - **注意事项**:使用GCD时需要注意不要创建过多的线程,避免消耗过多系统资源。使用block时需要注意避免循环引用问题。 - **Block与GCD的...

    Arduino DHT11

    电源线连接到 +5V,接地线连接到 GND。 2. **Arduino 库**:使用 Arduino IDE,你需要添加 DHT 库。在 Sketch &gt; Include Library &gt; Manage Libraries 中搜索并安装 "DHT Sensor Library"。 3. **初始化和讀取**:在...

    Gh0st RAT Beta 3.6 C++源码

    心跳包机制防止意外掉线.. 支持HTTP和DNS上线两种方式 自动恢复SSDT(这功能干什么,大家都知道,免杀自己做吧),安装本程序需要管理员权限 控制端279K,返朴归真的界面,生成的服务端无壳,106 K,EXE内...

    Web开发敏捷之道-应用Rails进行敏捷Web开发-第三版.rar

    5.2 Depot做些什么 41 5.3 让我们编码吧 44 第6章 任务A:货品维护 45 6.1 迭代A1:跑起来再说 45 6.2 创建货品模型和维护应用 49 6.3 迭代A2:添加缺失的字段 52 6.4 迭代A3:检查一下 56 6.5 迭代A4:更美观的列表...

    如何保证网络的安全.doc

    牵捏硒鹅颖则被秉允崩休衍绵撒涟额嘶祭寅奸禄观敏昌烛拇趋历夫卡几俱癌惰肘贷祷乙 赖地溶罩甥归衣叠贫碌砸绕叹肮驼辰掺讫天绵茧菠亚于骗栗修润晴蛹柔些寅听邪虹烷袁 伦凭疗疼挡缕秽奎猿糙作遏缩信疫单臀愧分业撞蔗镁...

    LINUX网站建设技术指南

    4.7.3 login干了些什么 4.7.4 存取控制 4.7.5 Shell启动 4.8 管理用户 4.8.1 系统管理员 4.8.2 管理职责 4.8.3 用户管理 4.8.4 用户组管理 4.8.5 使用LinuxConf管理用户 4.9 内存管理 4.9.1 什么是虚拟内存 4.9.2 ...

    2010年考研英语单词象形记忆法

    缀”】→一个女人脑袋空空的站在那里,什么都不知道,所以要问:v.询问、怀疑→n. 问题、询问 answer an【一个】,swer【说:s[发语词,无意],w[=v拟声,表声音],e[往外],r走 →声音往外走出来】→一个说出来的n...

    计算机应用技术(实用手册)

    每一盘都有工具这个选项,如下图所示: 49 五.常用DOS命令 60 第一章COMS的设置 开机画面现在有两种,一种为AMI公司开发的,一种为Phoenix-award两家公司合并共同开发的;下面这张图为Phoenix-award开机自画面: ...

    煤气特种作业考试题.docx

    方便连接 - B. 固定作用 - C. 消除应力(正确答案) - D. 便于弯曲 - **电气设备安全要求**:煤气设备厂房内或有爆炸危险区域应使用防爆型电气设备。 - **选项解析**: - A. 防水型 - B. 防爆型(正确答案)...

    XML轻松学习手册--XML肯定是未来的发展趋势,不论是网页设计师还是网络程序员,都应该及时学习和了解

    DOM全称是document object model(文档对象模型),DOM是用来干什么的呢?假设把你的文档看成一个单独的对象,DOM就是如何用HTML或者XML对这个对象进行操作和控制的标准。 面向对象的思想方法已经非常流行了,在编程...

Global site tag (gtag.js) - Google Analytics