编译器与连接器
我从没见过(不过应该有)任何一本C++教材有讲过何谓编译器(Compiler)及连接器(Linker)(倒是在很老的C教材中见过),现在都通过一个类似VC这样的编程环境隐藏了大量东西,将这些封装起来。在此,对它们的理解是非常重要的,本系列后面将大量运用到这两个词汇,其决定了能否理解如声明、定义、外部变量、头文件等非常重要的关键。
前面已经说明了电脑编程就是一个“翻译”过程,要把用户的程序翻译成CPU指令,其实也就是机器代码。所谓的机器代码就是用CPU指令书写的程序,被称作低级语言。而程序员的工作就是编写出机器代码。由于机器代码完全是一些数字组成(CPU感知的一切都是数字,即使是指令,也只是1代表加法、2代表减法这一类的数字和工作的映射),人要记住1是代表加法、2是代表减法将比较困难,并且还要记住第3块内存中放的是圆周率,而第4块内存中放的是有效位数。所以发明了汇编语言,用一些符号表示加法而不再用1了,如用ADD表示加法等。
由于使用了汇编语言,人更容易记住了,但是电脑无法理解(其只知道1是加颂隐法,不知道ADD是加法,因为电脑只能看见数字),所以必须有个东西将汇编代码翻译成机器代码,也就是所谓的编译器。即编译器是将一种语言翻译成另一种语言的程序。即使使用了汇编语言,但由于其几乎只是将CPU指令中的数字映射成符号以帮助记忆而已,还是使用的空迹电脑的思考方式进行思考的,不够接近人类的思考习惯,故而出现了纷繁复杂的各种电脑编程语言,如:PASCAL、BASIC、C等,其被称作高级语言,因为比较接近人的思考模式(尤其C++的类的概念的推出),而汇编语言则被称作低级语言(C曾被称作高级的低级语言),因为它们不是很符合人类的思考模式,人类书野亏厅写起来比较困难。由于CPU同样不认识这些PASCAL、BASIC等语言定义的符号,所以也同样必须有一个编译器把这些语言编写的代码转成机器代码。对于这里将要讲到的C++语言,则是C++语言编译器(以后的编译器均指C++语言编译器)。
因此,这里所谓的编译器就是将我们书写的C++源代码转换成机器代码。由于编译器执行一个转换过程,所以其可以对我们编写的代码进行一些优化,也就是说其相当于是一个CPU指令程序员,将我们提供的程序翻译成机器代码,不过它的工作要简单一些了,因为从人类的思考方式转成电脑的思考方式这一过程已经由程序员完成了,而编译器只是进行翻译罢了(最多进行一些优化)。
还有一种编译器被称作翻译器(Translator),其和编译器的区别就是其是动态的而编译器是静态的。如前面的BASIC的编译器在早期版本就被称为翻译器,因为其是在运行时期即时进行翻译工作的,而不像编译器一次性将所有代码翻成机器代码。对于这里的“动态”、“静态”和“运行时期”等名词,不用刻意去理解它,随着后续文章的阅读就会了解了。
编译器把编译后(即翻译好的)的代码以一定格式(对于VC,就是COFF通用对象文件格式,扩展名为.obj)存放在文件中,然后再由连接器将编译好的机器代码按一定格式在Windows操作系统下就是Portable Executable File Format--PE文件格式)存储在文件中,以便以后操作系统执行程序时能按照那个格式找到应该执行的第一条指令或其他东西,如资源等。至于为什么中间还要加一个连接器以及其它细节,在后续文章中将会进一步说明。
❷ 汇编编译器和连接器是如何协同工作的
NO,I 't now .《lntel汇编程序设计》的作业题可以找的啊......
..................................................................................................................................................................................................................
❸ Visual Unit 设定编译器和连接器
预先安装好VC,比如我安装的是Microsoft visual studio 2005.
再来,Visual Unit 4.0 安装,完成后,重新打开进入演示版。
在窗口的主界面,点击菜单栏“工程”-》“打开示例工程”,选择一个自己想打开的项目。
点击菜单栏“工具”-》“设定”。里面便有“编译器”和“编辑器”。
点击“编译器”,“名称”项,选择你安装VC对应的Visual C++ 版本,比如"Visual C++ 2005". 编译器和链接器,同样道理,选择之前你安装VC的根目录下,对应的编译器和链接器。比如:
“D:\Program files\Microsoft Visual Studio 2005\VC\bin\cl.exe”
“D:\Program files\Microsoft Visual Studio 2005\VC\bin\link.exe”。
这样便可以了。
如果项目执行测试,还是有类似编译器或者链接器的问题,请继续看以下注意事项,检查下工程属性是否一致:
待工程打开加载后,再次点击菜单栏“工程”-》“工程属性”,
在“常规”界面,选择你需要的测试代码编译器,比如我装的VC2005,那么我就选择Visual C++ 2005.
点击第五个tab "链接"。点击“环境变量”。在变量名下拉列表中,点击您安装VC的版本对应的dir,比如“vc2005_dir”. 在下面的“值”一栏,点击“浏览”找到VC对应的根目录。比如“D:/Program files/Microsoft Visual Studio 2005”。“OK”确认。再点击当前小窗口的“保存”。关闭小窗口。
退回到“工程属性”窗口,检查“库文件搜索目录”,是否是自己设定的vc2005_dir为头的目录,比如:“$(vc2005_dir)/vc/lib”.
其它地方比如“头文件”里的目录设置,亦是如此。不过只要你设置了一处的环境变量,其它地方也就会自动调用你设置的路径下的编译器了。
希望对你有帮助。:)
❹ 对单片机编程要用什么软件(编写单片机程序用什么软件)
keil最流行
单片机开发中除必要的硬件外,同样离不开软件,我们写的汇编语昌咐言源程序要变为CPU可以执行的机器码有两种方法,一种是手工汇编,另一种是机器汇编,目前已极少使用手工汇编的方法了。机器汇编是通过汇编软件将源程序变为机器码,用于MCS-51单片机的汇编软件有早期的A51,随着单片机开发技术的不断发展,从普遍使用汇编语言到逐渐使用高级语言开发,单片机的开发软件也在不断发展,Keil软件是目前最流行开发MCS-51系列单片机的软件,这从近年来各仿真机厂商纷纷宣布全面支持Keil即可看出。Keil提供了包括C编译器、宏汇编、连接器、库管理和一个功能强大的仿真调试器等在内的完整开发方案,通过一个集成开发环境(uVision)将这些部份组合在一起。运行Keil软件需要Pentium或以上的CPU,16MB或更多RAM、20M以上空闲的硬盘空间、WIN98、NT、WIN2000、WINXP等操作系统。掌握这一软件的使用对于使用51系列单片机的爱好者来说是十分必要的,如果你使用C语言编程,那么Keil几乎就是你的不二之选(目前在国内你只能买到该软件、而你买的仿真机也很可能只支持该软件),即使不使用C语言而仅用汇耐灶纯编语言编程,其方便易用的集成环境辩汪、强大的软件仿真调试工具也会令你事半功倍。
❺ 编译器的发展史
编译器
编译器,是将便于人编写,阅读,维护的高级计算机语言翻译为计算机能识别,运行的低级机器语言的程序。编译器将源程序(Source program)作为输入,翻译产生使用目标语言(Target language)的等价程序。源程序一般为高级语言(High-level language),如Pascal,C++等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。
一个现代编译器的主要工作流程如下:
源程序(source code)→预处理器(preprocessor)→编译器(compiler)→汇编程序(assembler)→目标程序(object code)→连接器(链接器,Linker)→可执行程序(executables)
目录 [隐藏]
1 工作原理
2 编译器种类
3 预处理器(preprocessor)
4 编译器前端(frontend)
5 编译器后端(backend)
6 编译语言与解释语言对比
7 历史
8 参见
工作原理
翻译是从源代码(通常为高级语言)到能直接被计算机或虚拟机执行的目标代码(通常为低级语言或机器言)。然而,也存在从低级语言到高级语言的编译器,这类编译器中用来从由高级语言生成的低级语言代码重新生成高级语言代码的又被叫做反编译器。也有从一种高级语言生成另一种高级语言的编译器,或者生成一种需要进一步处理的的中间代码的编译器(又叫级联)。
典型的编译器输出是由包含入口点的名字和地址以及外部调用(到不在这个目标文件中的函数调用)的机器代码所组成的目标文件。一组目标文件,不必是同一编译器产生,但使用的编译器必需采用同样的输出格式,可以链接在一起并生成可以由用户直接执行的可执行程序。
编译器种类
编译器可以生成用来在与编译器本身所在的计算机和操作系统(平台)相同的环境下运行的目标代码,这种编译器又叫做“本地”编译器。另外,编译器也可以生成用来在其它平台上运行的目标代码,这种编译器又叫做交叉编译器。交叉编译器在生成新的硬件平台时非常有用。“源码到源码编译器”是指用一种高级语言作为输入,输出也是高级语言的编译器。例如: 自动并行化编译器经常采用一种高级语言作为输入,转换其中的代码,并用并行代码注释对它进行注释(如OpenMP)或者用语言构造进行注释(如FORTRAN的DOALL指令)。
预处理器(preprocessor)
作用是通过代入预定义等程序段将源程序补充完整。
编译器前端(frontend)
前端主要负责解析(parse)输入的源程序,由词法分析器和语法分析器协同工作。词法分析器负责把源程序中的‘单词’(Token)找出来,语法分析器把这些分散的单词按预先定义好的语法组装成有意义的表达式,语句 ,函数等等。 例如“a = b + c;”前端词法分析器看到的是“a, =, b , +, c;”,语法分析器按定义的语法,先把他们组装成表达式“b + c”,再组装成“a = b + c”的语句。 前端还负责语义(semantic checking)的检查,例如检测参与运算的变量是否是同一类型的,简单的错误处理。最终的结果常常是一个抽象的语法树(abstract syntax tree,或 AST),这样后端可以在此基础上进一步优化,处理。
编译器后端(backend)
编译器后端主要负责分析,优化中间代码(Intermediate representation)以及生成机器代码(Code Generation)。
一般说来所有的编译器分析,优化,变型都可以分成两大类: 函数内(intraproceral)还是函数之间(interproceral)进行。很明显,函数间的分析,优化更准确,但需要更长的时间来完成。
编译器分析(compiler analysis)的对象是前端生成并传递过来的中间代码,现代的优化型编译器(optimizing compiler)常常用好几种层次的中间代码来表示程序,高层的中间代码(high level IR)接近输入的源程序的格式,与输入语言相关(language dependent),包含更多的全局性的信息,和源程序的结构;中层的中间代码(middle level IR)与输入语言无关,低层的中间代码(Low level IR)与机器语言类似。 不同的分析,优化发生在最适合的那一层中间代码上。
常见的编译分析有函数调用树(call tree),控制流程图(Control flow graph),以及在此基础上的 变量定义-使用,使用-定义链(define-use/use-define or u-d/d-u chain),变量别名分析(alias analysis),指针分析(pointer analysis),数据依赖分析(data dependence analysis)等等。
上述的程序分析结果是编译器优化(compiler optimization)和程序变形(compiler transformation)的前提条件。常见的优化和变新有:函数内嵌(inlining),无用代码删除(Dead code elimination),标准化循环结构(loop normalization),循环体展开(loop unrolling),循环体合并,分裂(loop fusion,loop fission),数组填充(array padding),等等。 优化和变形的目的是减少代码的长度,提高内存(memory),缓存(cache)的使用率,减少读写磁盘,访问网络数据的频率。更高级的优化甚至可以把序列化的代码(serial code)变成并行运算,多线程的代码(parallelized,multi-threaded code)。
机器代码的生成是优化变型后的中间代码转换成机器指令的过程。现代编译器主要采用生成汇编代码(assembly code)的策略,而不直接生成二进制的目标代码(binary object code)。即使在代码生成阶段,高级编译器仍然要做很多分析,优化,变形的工作。例如如何分配寄存器(register allocatioin),如何选择合适的机器指令(instruction selection),如何合并几句代码成一句等等。
编译语言与解释语言对比
许多人将高级程序语言分为两类: 编译型语言 和 解释型语言 。然而,实际上,这些语言中的大多数既可用编译型实现也可用解释型实现,分类实际上反映的是那种语言常见的实现方式。(但是,某些解释型语言,很难用编译型实现。比如那些允许 在线代码更改 的解释型语言。)
历史
上世纪50年代,IBM的John Backus带领一个研究小组对FORTRAN语言及其编译器进行开发。但由于当时人们对编译理论了解不多,开发工作变得既复杂又艰苦。与此同时,Noam Chomsky开始了他对自然语言结构的研究。他的发现最终使得编译器的结构异常简单,甚至还带有了一些自动化。Chomsky的研究导致了根据语言文法的难易程度以及识别它们所需要的算法来对语言分类。正如现在所称的Chomsky架构(Chomsky Hierarchy),它包括了文法的四个层次:0型文法、1型文法、2型文法和3型文法,且其中的每一个都是其前者的特殊情况。2型文法(或上下文无关文法)被证明是程序设计语言中最有用的,而且今天它已代表着程序设计语言结构的标准方式。分析问题(parsing problem,用于上下文无关文法识别的有效算法)的研究是在60年代和70年代,它相当完善的解决了这个问题。现在它已是编译原理中的一个标准部分。
有限状态自动机(Finite Automaton)和正则表达式(Regular Expression)同上下文无关文法紧密相关,它们与Chomsky的3型文法相对应。对它们的研究与Chomsky的研究几乎同时开始,并且引出了表示程序设计语言的单词的符号方式。
人们接着又深化了生成有效目标代码的方法,这就是最初的编译器,它们被一直使用至今。人们通常将其称为优化技术(Optimization Technique),但因其从未真正地得到过被优化了的目标代码而仅仅改进了它的有效性,因此实际上应称作代码改进技术(Code Improvement Technique)。
当分析问题变得好懂起来时,人们就在开发程序上花费了很大的功夫来研究这一部分的编译器自动构造。这些程序最初被称为编译器的编译器(Compiler-compiler),但更确切地应称为分析程序生成器(Parser Generator),这是因为它们仅仅能够自动处理编译的一部分。这些程序中最着名的是Yacc(Yet Another Compiler-compiler),它是由Steve Johnson在1975年为Unix系统编写的。类似的,有限状态自动机的研究也发展了一种称为扫描程序生成器(Scanner Generator)的工具,Lex(与Yacc同时,由Mike Lesk为Unix系统开发)是这其中的佼佼者。
在70年代后期和80年代早期,大量的项目都贯注于编译器其它部分的生成自动化,这其中就包括了代码生成。这些尝试并未取得多少成功,这大概是因为操作太复杂而人们又对其不甚了解。
编译器设计最近的发展包括:首先,编译器包括了更加复杂算法的应用程序它用于推断或简化程序中的信息;这又与更为复杂的程序设计语言的发展结合在一起。其中典型的有用于函数语言编译的Hindley-Milner类型检查的统一算法。其次,编译器已越来越成为基于窗口的交互开发环境(Interactive Development Environment,IDE)的一部分,它包括了编辑器、连接程序、调试程序以及项目管理程序。这样的IDE标准并没有多少,但是对标准的窗口环境进行开发已成为方向。另一方面,尽管近年来在编译原理领域进行了大量的研究,但是基本的编译器设计原理在近20年中都没有多大的改变,它现在正迅速地成为计算机科学课程中的中心环节。
在九十年代,作为GNU项目或其它开放源代码项目的一部分,许多免费编译器和编译器开发工具被开发出来。这些工具可用来编译所有的计算机程序语言。它们中的一些项目被认为是高质量的,而且对现代编译理论感性趣的人可以很容易的得到它们的免费源代码。
大约在1999年,SGI公布了他们的一个工业化的并行化优化编译器Pro64的源代码,后被全世界多个编译器研究小组用来做研究平台,并命名为Open64。Open64的设计结构好,分析优化全面,是编译器高级研究的理想平台。
编译器是一种特殊的程序,它可以把以特定编程语言写成的程序变为机器可以运行的机器码。我们把一个程序写好,这时我们利用的环境是文本编辑器。这时我程序把程序称为源程序。在此以后程序员可以运行相应的编译器,通过指定需要编译的文件的名称就可以把相应的源文件(通过一个复杂的过程)转化为机器码了。
编译器工作方法
首先编译器进行语法分析,也就是要把那些字符串分离出来。然后进行语义分析,就是把各个由语法分析分析出的语法单元的意义搞清楚。最后生成的是目标文件,我们也称为obj文件。再经过链接器的链接就可以生成最后的可执行代码了。有些时候我们需要把多个文件产生的目标文件进行链接,产生最后的代码。我们把一过程称为交叉链接。
❻ 谁能帮我解释一下C交叉编译器,汇编器,连接器 和ucos系统的关系
没有必然的联系,只是ucos采用C语言扰岩编制,故而需要C编译器。如果用汇编编制,那需要的就是汇编器。理论上,ucos还可以用其它编程语言来编制,那么就需要相应语言的编译器镇李滑。不御腊过就ucos本身而言,跟编译器是没有关系的。
❼ 电脑里自带汇编编译器和连接器吗
好像没有,需要自己下载
dos下用masm5.0 link.exe
网上很多的
❽ C++从零开始——何谓类
前篇说明了结构只不过是定义了内存布局而已,提到类型定义符前还可以书写class,即类型的自定义类型(简称类),它和结构根本没有区别(仅有一点小小的区别,下篇说明),而之所以还要提供一个class,实际是由于C++是从C扩展而成,其中的class是C++自
己提出的一个很重要的概念,只是为了与C语言兼容而保留了struct这个关键字。不过通过前面括号中所说的小小区别也足以看出C++的设计者为结构和类定义的不同语义,下篇说明。
暂时可以先认为类较结构的长足进步就是多了成员函数这个概念(虽然结构也可以有成员函数),在了解成员函数之前,先来看一种语义需求。
操作与资源
程序主要是由操作和被操作的资源组成,操作的执行者就是CPU,这很正常,但有时候的确存在一些需要,需要表现是某个资源操作了另一个资源(暂时称作操作者),比如游戏中,经常出现的就是要映射怪物攻击了玩家。之所以需要操作者,一般是因为这个操作也需要修改操作者或利用操作者记录的一些信息来完成操作,比如怪物的攻击力来决定玩家被攻击后的状态。这种语义就表现为操作者具有某些功能。为了实现上面的语义差枯散,如原来所说进行映射,先映射怪物和玩家分别为结构,如下:
struct Monster { float Life; float Attack; float Defend; };
struct Player { float Life; float Attack; float Defend; };
上面的攻击操作就可以映射为void MonsterAttackPlayer( Monster mon, Player pla );。注意这里期望通过函数名来表现操作者,但和前篇说的将过河方案起名为sln一样,属于一种本末倒置,因为这个语义应该由类型来表现,而不是函数名。为此,C++提供了成员函数的概念。
成员函数
与之前一样,在类型定义符败槐中书写函数的声明语句将定义出成员函数,如下:
struct ABC { long a; void AB( long ); };
上面就定义了一个映射元素--第一个变量ABC::a,类型为long ABC::;以及声明了一个映射元素--第二个函数ABC::AB,类型为void ( ABC:: )( long )。类型修饰符ABC::在此修饰了函数ABC::AB,表示其为函数类型的偏移类型,即是一相对值。但由于是函数,意义和变量不同,即其依旧映射的是内存中的地址(代码的地址),但由于是偏移类型,也就是相对的,即是不完整的,因此不能对它应用函数操作符,如:ABC::AB( 10 );。这里将错误,因为ABC::AB是相对的,其相对的东西不是如成员变量那样是个内存地址,而是一个结构指针类型的参数,参数名一定为this,这是强行定义的,后面说明。
注意由于其名字为ABC::AB,而上面仅仅是对其进行了声明,要定义它,仍和之前的函数定义一样,如下:
void ABC::AB( long d ) { this-a = d; }
应注意上面函数的名字为ABC::AB,但和前篇说的成员变量一样,不能直接书写long ABC::a;,也就不能直接如上书写函数的定义语句(至少函数名为ABC::AB就不虚氏符合标识符规则),而必须要通过类型定义符“{}”先定义自定义类型,然后再书写,这会在后面说明声明时详细阐述。
注意上面使用了this这个关键字,其类型为ABC*,由编译器自动生成,即上面的函数定义实际等同于void ABC::AB( ABC *this, long d ) { this-a = d; }。而之所以要省略this参数的声明而由编译器来代劳是为了在代码上体现出前面提到的语义(即成员的意义),这也是为什么称ABC::AB是函数类型的偏移类型,它是相对于这个this参数而言的,如何相对。
如下:
ABC a, b, c; a.ABC::AB( 10 ); b.ABC::AB( 12 ); c.AB( 14 );
上面利用成员操作符调用ABC::AB,注意执行后,a.a、b.a和c.a的值分别为10、12和14,即三次调用ABC::AB,但通过成员操作符而导致三次的this参数的值并不相同,并进而得以修改三个ABC变量的成员变量a。注意上面书写a.ABC::AB( 10 );,和成员变量一样,由于左右类型必须对应,因此也可a.AB( 10 );。还应注意上面在定义ABC::AB时,在函数体内书写this-a = d;,同上,由于类型必须对应的关系,即this必须是相应自定义类型的指针,所以也可省略this-的书写,进而有void ABC::AB( long d ) { a = d; }。
注意这里成员操作符的作用,其不再如成员变量时返回相应成员变量类型的数字,而是返回一函数类型的数字,但不同的就是这个函数类型是无法用语法表示出来的,即C++并没有提供任何关键字或类型修饰符来表现这个返回的类型(VC内部提供了__thiscall这个类型修饰符进行表示,不过写代码时依旧不能使用,只是编译器内部使用)。也就是说,当成员操作符右侧接的是函数类型的偏移类型的数字时,返回一个函数类型的数字(表示其可被施以函数操作符),函数的类型为偏移类型中给出的类型,但这个类型无法表现。即a.AB将返回一个数字,这个数字是函数类型,在VC内部其类型为void ( __thiscall ABC:: )( long ),但这个类型在C++中是非法的。
C++并没有提供类似__thiscall这样的关键字以修饰类型,因为这个类型是要求编译器遇到函数操作符和成员操作符时,如a.AB( 10 );,要将成员操作符左侧的地址作为函数调用的第一个参数传进去,然后再传函数操作符中给出的其余各参数。即这个类型是针对同时出现函数操作符和成员操作符这一特定情况,给编译器提供一些信息以生成正确的代码,而不用于修饰数字(修饰数字就要求能应付所有情况)。即类型是用于修饰数字的,而这个类型不能修饰数字,因此C++并未提供类似__thiscall的关键字。和之前一样,由于ABC::AB映射的是一个地址,而不是一个偏移值,因此可以ABC::AB;但不能ABC::a;,因为后者是偏移值。根据类型匹配,很容易就知道也可有:
void ( ABC::*p )( long ) = ABC::AB;或void ( ABC::*p )( long ) = ABC::AB;
进而就有:void ( ABC::**pP )( long ) = p; ( c.**pP )( 10.0f );。之所以加括号是因为函数操作符的优先级较“*”高。再回想前篇说过指针类型的转换只是类型变化,数值不变(下篇说明数值变化的情况),因此可以有如下代码,这段代码毫无意义,在此仅为加深对成员函数的理解。
struct ABC { long a; void AB( long ); };
void ABC::AB( long d )
{
this-a = d;
}
struct AB
{
short a, b;
void ABCD( short tem1, short tem2 );
void ABC( long tem );
};
void AB::ABCD( short tem1, short tem2 )
{
a = tem1; b = tem2;
}
void AB::ABC( long tem )
{
a = short( tem / 10 );
b = short( tem - tem / 10 );
}
void main()
{
ABC a, b, c; AB d;
( c.*( void ( ABC::* )( long ) )AB::ABC )( 43 );
( b.*( void ( ABC::* )( long ) )AB::ABCD )( 0XABCDEF12 );
( d.*( void ( AB::* )( short, short ) )ABC::AB )( 0XABCD, 0XEF12 );
}
上面执行后,c.a为0X00270004,b.a为0X0000EF12,d.a为0XABCD,d.b为0XFFFF。对于c的函数调用,由于AB::ABC映射的地址被直接转换类型进而直接被使用,因此程序将跳到AB::ABC处的a = short( tem / 10 );开始执行,而参数tem映射的是传递参数的内存的首地址,并进而用long类型解释而得到tem为43,然后执行。注意b = short( tem - tem / 10 );实际是this-b = short( tem - tem / 10 );,而this的值为c对应的地址,但在这里被认为是AB*类型(因为在函数AB::ABC的函数体内),所以才能this-b正常(ABC结构中没有b这个成员变量),而b的偏移为2,所以上句执行完后将结果39存放到c的地址加2所对应的内存,并且以short类型解释而得到的16位的二进制数存放。对于a = short( tem / 10 );也做同样事情,故最后得c.a的值为0X0027004(十进制39转成十六进制为0X27)。
同样,对于b的调用,程序将跳到AB::ABCD,但生成的b的调用代码时,将参数0XABCDEF12按照参数类型为long的格式记录在传递参数的内存中,然后跳到AB::ABCD。但编译AB::ABCD时又按照参数为两个short类型来映射参数tem1和tem2对应的地址,因此容易想到tem1的值将为0XEF12,tem2的值为0XABCD,但实际并非如此。参数如何传递由之前说的函数调用规则决定,函数调用的具体实现细节在《C++从零开始(十五)》中说明,这里只需了解到成员函数映射的仍然是地址,而它的类型决定了如何使用它,后面说明。
声明的含义前面已经解释过声明是什么意思,在此由于成员函数的定义规则这种新的定义语法,必须重新考虑声明的意思。注意一点,前面将一个函数的定义放到main函数定义的前面就可以不用再声明那个函数了;同样如果定义了某个变量,就不用再声明那个变量了。这也就是说定义语句具有声明的功能,但上面成员函数的定义语句却不具有声明的功能,下面来了解声明的真正意思。
声明是要求编译器产生映射元素的语句。所谓的映射元素,就是前面介绍过的变量及函数,都只有3栏(或3个字段):类型栏、名字栏和地址栏(成员变量类型的这一栏就放偏移值)。即编译器每当看到声明语句,就生成一个映射元素,并且将对应的地址栏空着,然后留下一些信息以告诉连接器--此.obj文件(编译器编译源文件后生成的文件,对于VC是.obj文件)需要一些符号,将这些符号找到后再修改并完善此.obj文件,最后连接。
回想之前说过的符号的意思,它就是一字符串,用于编译器和连接器之间的通信。注意符号没有类型,因为连接器只是负责查找符号并完善(因为有些映射元素的地址栏还是空的)中间文件(对于VC就是.obj文件),不进行语法分析,也就没有什么类型。
定义是要求编译器填充前面声明没有书写的地址栏。也就是说某变量对应的地址,只有在其定义时才知道。因此实际的在栈上分配内存等工作都是由变量的定义完成的,所以才有声明的变量并不分配内存。但应注意一个重点,定义是生成映射元素需要的地址,因此定义也就说明了它生成的是哪个映射元素的地址,而如果此时编译器的映射表(即之前说的编译器内部用于记录映射元素的变量表、函数表等)中没有那个映射元素,即还没有相应元素的声明出现过,那么编译器将报错。
但前面只写一个变量或函数定义语句,它照样正常并没有报错啊?实际很简单,只需要将声明和定义看成是一种语句,只不过是向编译器提供的信息不同罢了。如:void ABC( float );和void ABC( float ){},编译器对它们相同看待。前者给出了函数的类型及类型名,因此编译器就只填写映射元素中的名字和类型两栏。由于其后只接了个“;”,没有给出此函数映射的代码,因此编译器无法填写地址栏。而后者,给出了函数名、所属类型以及映射的代码(空的复合语句),因此编译器得到了所有要填写的信息进而将三栏的信息都填上了,结果就表现出定义语句完成了声明的功能。
对于变量,如long a;。同上,这里给出了类型和名字,因此编译器填写了类型和名字两栏。但变量对应的是栈上的某块内存的首地址,这个首地址无法从代码上表现出来(前面函数就通过在函数声明的后面写复合语句来表现相应函数对应的代码所在的地址),而必须由编译器内部通过计算获得,因此才硬性规定上面那样的书写算作变量的定义,而要变量的声明就需要在前面加extern。即上面那样将导致编译器进行内部计算进而得出相应的地址而填写了映射元素的所有信息。
#p#副标题#e#
上面难免显得故弄玄虚,那都是因为自定义类型的出现。考虑成员变量的定义,如:
struct ABC { long a, b; double c; };
上面给出了类型--long ABC::、long ABC::和double ABC::;给出了名字--ABC::a、ABC::b和ABC::c;给出了地址(即偏移)--0、4和8,因为是结构型自定义类型,故由此语句就可以得出各成员变量的偏移。上面得出三个信息,即可以填写映射元素的所有信 struct ABC { void AB( float ); };
上面给出了类型--void ( ABC:: )( float );给出了名字--ABC::AB。不过由于没有给出地址,因此无法填写映射元素的所有信息,故上面是成员函数ABC::AB的声明。按照前面说法,只要给出地址就可以了,而无需去管它是定义还是声明,因此也就可以这样:
struct ABC { void AB( float ){} };
上面给出类型和名字的同时,给出了地址,因此将可以完全填写映射元素的所有信息,是定义。上面的用法有其特殊性,后面说明。注意,如果这时再在后面写ABC::AB的定义语句,即如下,将错误:
struct ABC { void AB( float ){} };
void ABC::AB( float ) {}
上面将报错,原因很简单,因为后者只是定义,它只提供了ABC::AB对应的地址这一个信息,但映射元素中的地址栏已经填写了,故编译器将说重复定义。再单独看成员函数的定义,它给出了类型void ( ABC:: )( float ),给出了名字ABC::AB,也给出了地址,但为什么说它只给出了地址这一信息?首先,名字ABC::AB是不符合标识符规则的,而类型修饰符ABC::必须通过类型定义符“{}”才能够加上去,这在前面已多次说明。因此上面给出的信息是:给出了一个地址,这个地址是类型为void ( ABC:: )( float ),名字为ABC::AB的映射元素的地址。结果编译器就查找这样的映射元素,如果有,则填写相应的地址栏,否则报错,即只写一个void ABC::AB( float ){}是错误的,在其前面必须先通过类型定义符“{}”声明相应的映射元素。这也就是前面说的定义仅仅填地址栏,并不生成映射元素。
声明的作用
定义的作用很明显了,有意义的映射(名字对地址)就是它来做,但声明有什么用?它只是生成类型对名字,为什么非得要类型对名字?它只是告诉编译器不要发出错误说变量或函数未定义?任何东西都有其存在的意义,先看下面这段代码。
extern"C" long ABC( long a, long b );
void main(){ long c = ABC( 10, 20 ); }
假设上面代码在a.cpp中书写,编译生成文件a.obj,没有问题。但按照之前的说明,连接时将错误,因为找不到符号_ABC。因为名字_ABC对应的地址栏还空着。接着在VC中为a.cpp所在工程添加一个新的源文件b.cpp,如下书写代码。
extern"C" float ABC( float a ){ return a; }
编译并连接,现在没任何问题了,但相信你已经看出问题了--函数ABC的声明和定义的类型不匹配,却连接成功了?
注意上面关于连接的说明,连接时没有类型,只管符号。上面用extern"C"使得a.obj要求_ABC的符号,而b.cpp提供_ABC的符号,剩余的就只是连接器将b.obj中_ABC对应的地址放到a.obj以完善a.obj,最后连接a.obj和b.obj。
那么上面什么结果,由于需要考虑函数的实现细节,这在《C++从零开始(十五)》中再说明,而这里只要注意到一件事:编译器即使没有地址也依旧可以生成代码以实现函数操作符的功能--函数调用。之所以能这样就是因为声明时一定必须同时给出类型和名字,因为类型告诉编译器,当某个操作符涉及到某个映射元素时,如何生成代码来实现这个操作符的功能。也就是说,两个char类型的数字乘法和两个long类型的数字乘法编译生成的代码不同;对long ABC( long );的函数调用代码和void ABC( float )的不同。即,操作符作用的数字类型的不同将导致编译器生成的代码不同。
那么上面为什么要将ABC的定义放到b.cpp中?因为各源文件之间的编译是独立的,如果放在a.cpp,编译器就会发现已经有这么个映射元素,但类型却不匹配,将报错。而放到b.cpp中,使得由连接器来完善a.obj,到时将没有类型的存在,只管符号。下面继续。
struct ABC { long a, b; void AB( long tem1, long tem2 ); void ABCD(); };
void main(){ ABC a; a.AB( 10, 20 ); }
由上面的说法,这里虽然没有给出ABC::AB的定义,但仍能编译成功,没有任何问题。仍假设上面代码在a.cpp中,然后添加b.cpp,在其中书写下面的代码。
struct ABC { float b, a; void AB( long tem1, long tem2 ); long ABCD( float ); };
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
这里定义了函数ABC::AB,注意如之前所说,由于这里的函数定义仅仅只是定义,所以必须在其前面书写类型定义符“{}”以让编译器生成映射元素。但更应该注意这里将成员变量的位置换了,这样b就映射的是0而a映射的是4了,并且还将a、b的类型换成了float,更和a.cpp中的定义大相径庭。但没有任何问题,编译连接成功,a.AB( 10,20 );执行后a.a为0X41A00000,a.b为0X41200000,而*( float* )a.a为20,*( flaot* )a.b为10。
为什么?因为编译器只在当前编译的那个源文件中遵循类型匹配,而编译另一个源文件时,编译其他源文件所生成的映射元素全部无效。因此声明将类型和名字绑定起来,而名字就代表了其所关联的类型的地址类型的数字,而后继代码中所有操作这个数字的操作符的编译生成都将受这个数字的类型的影响。即声明是告诉编译器如何生成代码的,其不仅仅只是个语法上说明变量或函数的语句,它是不可或缺的。
还应注意上面两个文件中的ABC::ABCD成员函数的声明不同,而且整个工程中(即a.cpp和b.cpp中)都没有ABC::ABCD的定义,却仍能编译连接成功,因为声明并不是告诉编译器已经有什么东西了,而是如何生成代码。
头文件上面已经说明,如果有个自定义类型ABC,在a.cpp、b.cpp和c.cpp中都要使用它,则必须在a.cpp、b.cpp和c.cpp中,各自使用ABC之前用类型定义符“{}”重新定义一遍这个自定义类型。如果不小心如上面那样在a.cpp和b.cpp中写的定义不一样,则将产生很难查找的错误。为此,C++提供了一个预编译指令来帮忙。
预编译指令就是在编译之前执行的指令,它由预编译器来解释执行。预编译器是另一个程序,一般情况,编译器厂商都将其合并进了C++编译器而只提供一个程序。在此说明预编译指令中的包含指令--#include,其格式为#include 文件名。应注意预编译指令都必须单独占一行,而文件名就是一个用双引号或尖括号括起来的文件名,如:#include "abc.c"、#include "C:abc.dsw"或#include C:abc.exe。它的作用很简单,就是将引号或尖括号中书写的文件名对应的文件以ANSI格式或MBCS格式(关于这两个格式可参考《C++从零开始(五)》)解释,并将内容原封不动地替换到#include所在的位置,比如下面是文件abc的内容。
struct ABC { long a, b; void AB( long tem1, long tem2 ); };
则前面的a.cpp可改为:
#include "abc"
void main() { ABC a; a.AB( 10, 20 ); }
而b.cpp可改为:
#include "abc"
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
这时,就不会出现类似上面那样在b.cpp中将自定义类型ABC的定义写错了而导致错误的结果(a.a为0X41A00000,a.b为0X41200000),进而a.AB( 10, 20 );执行后,a.a为10,a.b为20。
注意这里使用的是双引号来括住文件名的,它表示当括住的只是一个文件名或相对路径而没有给出全路径时,如上面的abc,则先搜索此时被编译的源文件所在的目录,然后搜索编译器自定的包含目录(如:C:Program FilesMicrosoft Visual Studio .NET 2003Vc7include等),里面一般都放着编译器自带的SDK的头文件(关于SDK,将在《C++从零开始(十八)》中说明),如果仍没有找到,则报错(注意,一般编译器都提供了一些选项以使得除了上述的目录外,还可以再搜索指定的目录,不同的编译器设定方式不同,在此不表)。
如果是用尖括号括起来,则表示先搜索编译器自定的包含目录,再源文件所在目录。为什么要不同?只是为了防止自己起的文件名正好和编译器的包含目录下的文件重名而发生冲突,因为一旦找到文件,将不再搜索后继目录。
所以,一般的C++代码中,如果要用到某个自定义类型,都将那个自定义类型的定义分别装在两个文件中,对于上面结构ABC,则应该生成两个文件,分别为ABC.h和ABC.pp,其中的ABC.h被称作头文件,而ABC.cpp则称作源文件。头文件里放的是声明,而源
文件中放的是定义,则ABC.h的内容就和前面的abc一样,而ABC.cpp的内容就和b.cpp一样。然后每当工程中某个源文件里要使用结构ABC时,就在那个源文件的开头包含ABC.h,这样就相当于将结构ABC的所有相关声明都带进了那个文件的编译,比如前面的a.cpp就通过在开头包含abc以声明了结构ABC。
为什么还要生成一个ABC.cpp?如果将ABC::AB的定义语句也放到ABC.h中,则a.cpp要使用ABC,c.cpp也要使用ABC,所以a.cpp包含ABC.h,由于里面的ABC::AB的定义,生成一个符号?AB@ABC@@QAEXJJ@Z(对于VC);同样c.cpp的编译也
❾ 简述一下编译器和链接器的作用
1、编译器:
编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。
2、链接器:
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。
然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。