面向对象编程:
程序的生命周期:编写源代码,编译,链接,装载,执行。
源代码编写分为三部分:头文件.h,源代码文件.cpp,主程序源代码文件main.cpp。头文件.h和源代码文件.cpp的文件名必须相同。
头文件中:定义了抽象数据类型,也就是类的接口。类的接口包含类的成员函数和成员变量。
成员函数包括一些构造函数,析构函数,功能函数。功能函数:指对类的成员变量进行操做,该类可以执行的操做=增删改查,再加上排序。
源代码文件.cpp:接口的实现。是指如何实现成员函数的细节。
主程序源代码文件main.cpp:程序执行主体,要对类进行什么操作。包含头文件。
类的基本思想:数据抽象和封装。数据抽象指接口和实现分离的状态,封装指对该状态的实现。目的就是隐藏实现的细节。在编译结束后,只将头文件也就是类的接口可以给别人看,但是类的具体实现不给别人看。也就实现了隐藏。
那主要在这三个文件中书写什么呢?下面有一张图。
头文件书写格式:这样做是为了防止重复编译,不这样做就有可能出错。
#ifndef CIRCLE_H
#define CIRCLE_H
//你的代码写在这里
#endif
.cpp源文件书写:开头必须#include一下实现的头文件,以及要用到的头文件。
main.cpp源文件:开头必须#include一下实现的头文件,以及要用到的头文件。
还有一种.hpp头文件:将头文件内容和实现内容都放在一个文件中。
编译阶段:包括预处理,汇编和优化,二进制目标代码。
声明和定义的区别:定义才会分配存储空间,确定该变量占多少存储空间。
头文件无法编译,只有.cpp文件进行编译,且每个.cpp源文件都是作为独立的编译单元。包括源文件.cpp和main.cpp文件。
预处理阶段:读取源程序字符流,对其中的伪指令(以# 开头的指令)和特殊符号进行处理。得到.i文件。
1) 宏定义指令,如# define Name TokenString,# undef等。
对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
2) 条件编译指令,如# ifdef,# ifndef,# else,# elif,# endif等。
这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。防止重定义。
3) 头文件包含指令,如# include "FileName" 或者# include < FileName> 等。等价于将.h中的代码添加到.cpp文件中从系统目录和环境变量目录中寻找。包含到源程序中的头文件可以是系统提供的,这些头文件一般被放在/ usr/ include目录下。在程序中# include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与源程序放在同一目录下,此时在# include中要用双引号("")。
汇编和优化:得到.s文件。汇编文件。ASC码文件。
经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。
对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。
后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。
目标代码生成:指把汇编语言代码翻译成目标机器指令的过程。得到.obj文件。二进制文件。一个目标文件不仅要提供数据和二进制代码,还要提供两个表:未解决符号表和导出符号表,来告诉链接器自己需要什么和自己能提供些什么。地址重定向表:提供了本编译单元所有对自身地址的引用记录。
多个.cpp文件会生成多个.obj文件。符号和段是目标文件的基本组成部分。其中符号表示的是程序中的内存地址或数据内存。
段包括代码段.text,初始化数据段.data,未初始化数据段.bss,以及一些特殊段。每个段由地址和操作两部分组成。每个段的起始地址设置为0,等待链接调整。每个段有自己的地址范围。也就是段的长度。在将目标文件中的段拼接到程序内存映射中时,段长是唯一重要的参数。内存映射中的堆与栈内容在运行时确定,而只需指定堆与栈的长度。
链接:将编译的每个独立的源代码文件生成的段拼接到一个二进制可执行文件中。合成最终的程序内存映射节。
该二进制文件包含了多个相同类型的段。如下图:
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。为了访问变量或者函数,必须知道它们的地址准确的说是程序内存中的函数地址和数据内存中的全局变量地址。在拼接段完成前,函数与变量的地址无法确定。当引用同一文件中定义的函数或者全局变量时不会出现该问题。因为它们都在同一个段内,它们的相对偏移地址在链接之前就已经确定。在拼接完成后,相对内存地址就已经确定了。为了支持二进制级别的程序复用所以才有链接阶段的原因。
链接阶段包含重定位和解析引用阶段。链接过程的最终结果是二进制可执行文件。
重定位阶段:将分散在单独目标文件中不同类型的节拼接到程序内存映射节中。也就是将段中从0开始的地址范围转换成最终内存映射中更具体的地址范围。
虚拟内存机制使得每个程序都拥有相同、简单的程序地址范围为0到2的N次方。但是程序执行时的实际物理地址由操作系统运行时决定的。当重定位完成后,绝大部分的程序内存映射也完成了。
解析引用阶段:为不同的代码之间建立关联是一个很难的问题使得程序成为一个整体。
1.源文件1编译的目标文件映射到程序内存映射中,源文件1中变量,函数的映射地址都是一个已知量。
2.源文件2编译的目标文件映射到程序内存中,源文件2会调用源文件1中的函数和变量,但却不知道其实际程序的内存地址。编译器会假定这些符号未来会在进程内存映射中存在。但是,直到完整内存映射之前,这两项引用会一直被当成未解析引用。
可执行文件:总会包含几个合成的段(.text,.data,.bss)。在其包含的所有符号中,main函数是整个程序执行的起点。但是这却不是程序启动后真正首先执行的代码。实际上用于启动程序的代码是在链接阶段才添加到程序内存映射中的起始处。
启动代码有两种不同形式:crt0,CRT1;整体结构如下:
可执行程序与动态库的唯一区别:后者没有启动代码。
程序装载:当系统识别出二进制格式后,内核装载器模块会开始运行。1.首先定位二进制文件中的PI_INTERP段,用于动态加载阶段。首先解释动态编译与静态编译的区别。静态编译是指可执行文件不包含任何动态链接依赖。所有可执行文件需要的外部库都被静态链接到程序中。得到的程序完全可移植的。缺点:容量大很多。
2.然后读取程序的二进制文件段的头,确定每个段的地址和字节长度。然后确定可执行文件段和程序内存映射关联的结构。
程序中每一部分只有在运行时真正需要时才会加载。真正从可执行文件复制段的操作是在程序启动后才执行的。
而extern关键字告诉编译器n已经在别的编译单元里定义了,在这个单元里不用定义。由于编译单元之间是互不相关的,所以编译器就不知道n究竟在哪里,所以在函数FunB中就没有办法生成n的地址。
再总结一遍编译的详细原理:
1.先说一下计算机的存储结构:
存储器的访问速度与其存储容量成反比。系统并不总是访问所有的存储器。只需为立即需要执行的程序预留最快的存储器。而让那些并非立即执行的代码或数据使用较慢的存储设备。于是用到缓存策略。
缓存存储器的级别:远程存储设备,本地磁盘,主存,三级缓存,二级缓存,一级缓存,CPU寄存器。容量越小速度越快。
内存储器包括寄存器、高速缓冲存储器(Cache)和主存储器。寄存器在CPU芯片的内部,高速缓冲存储器也制作在CPU芯片内,而主存储器由插在主板内存插槽中的若干内存条组成。内存的质量好坏与容量大小会影响计算机的运行速度。内存有RAM,ROM在内存条中,其他在可能CPU内,都算作内存。
虚拟内存:内存不够用,从硬盘中划出一部分空间当做内存来用。用物理内存与虚拟内存区分。
针对多个程序运行的情况:
1.所以程序分配的虚拟内存配额是固定的,均等的和显示的,即程序内存映射空间。均为2的N次方,N为32或64字节。
2.程序分配的物理内存数量可能各不相同。比声明的进程地址空间小很多。
3.运行时的物理内存会被划分成数个小的分段(页),每一个页都可以用来同步执行程序。
正在运行的程序的完整内存布局会被保存在低速存储器中,只有当前即将被执行的一部分会被加载到物理内存的页中。CPU通过虚拟内存地址与物理内存地址的映射,访问部分程序。
内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。
进程的内存划分方案:
首先程序编写结束后,经过预处理后。得到经过精简的字符符号程序,也就是说还是文本文件。经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。
进行编译成汇编语言:主要表明每条指令存放地址,操纵码,操做树。
再经过优化后编译成二进制语言:由地址,操作数,操纵码完成。
C++内存管理用户区,最顶层为系统区:内存分成5个区,他们分别是栈、自由存储区(共享存储区)、堆、全局/静态存储区(全局和静态在同一个区域)和文字常量区。从高地址到低地址。
关于内存分配的问题:
全局/静态存储区域:内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
静态变量在编译期间就可以确定他们的值,静态变量即使不提供初始值也会被零初始化。此外,类内静态变量同样如此,全局变量也是如此。
1>全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern关键字再次声明这个全局变量。
2>静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
在栈上创建:在执行函数也就是运行程序时,才会给局部变量分配内存空间。
编译器在编译的过程中,遇到函数调用时,会加入几条汇编指令。这些汇编指令的作用是:
1 分配一段栈空间,用于存放被调函数的参数和局部变量。
2 call被用函数。
3 当被调函数返回时释放掉这段分配栈空间。
在堆上:和栈一样。
四、堆和栈的区别
管理方式不同:栈是由编译器自动申请和释放空间,堆是需要程序员手动申请和释放;
空间大小不同:栈的空间是有限的,在32位平台下,VC6下默认为1M,堆最大可以到4G;
能否产生碎片:栈和数据结构中的栈原理相同,在弹出一个元素之前,上一个已经弹出了,不会产生碎片,如果不停地调用malloc、free对造成内存碎片很多;
生长方向不同:堆生长方向是向上的,也就是向着内存地址增加的方向,栈刚好相反,向着内存减小的方向生长。
分配方式不同:堆都是动态分配的,没有静态分配的堆。栈有静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
分配效率不同:栈的效率比堆高很多。栈是机器系统提供的数据结构,计算机在底层提供栈的支持,分配专门的寄存器来存放栈的地址,压栈出栈都有相应的指令,因此比较快。堆是由库函数提供的,机制很复杂,库函数会按照一定的算法进行搜索内存,因此比较慢。
C++类占用空间计算:
1、一个类占用的空间主要是属性占用空间,而成员函数一般不占用空间,但是虚函数占用空间,需要说明的是,无论多少个虚函数,只要占用4个字节即可,也就是索引指向一个虚拟表的首位置。另外需要说明的是占用空间都考虑了对齐,所以不足4个的按照满4个的算。
2、类的继承,子类占用空间是父类基础上增加本类空间即可。所以说可以认为,子类就是直接拷贝了父类的内容,然后结合自身的内容。而且存储空间也是这个顺序,即先父类分配空间,然后才是子类空间。
3、静态成员变量不占用类空间,应该是确实没有放入这个类的里面,而且没有指针指向它,只能通过类::来访问,也就是说静态成员是随着类的存在而存在,而 不依赖于对象,它的存在意义主要还是区分,否则如何确定其意义,这还是体现了相关的都方一起的思想,比全局变量或者常量更方便使用和理解。
4、需要说明的是,虚函数对应的虚拟表在空间的其他位置,和对象是没有联系的,但是虚拟表地址是和类统一的,也就是说一旦确定,无论在哪个对象中,其指针 值是一样的,即虚拟表位置是一定的。指针放在对象的最前面,首先是指向虚函数的虚拟表指针,然后才是其他成员变量空间。