(一)在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域 1、代码区C++程序运行的时候,将内存大方向划分为4个区域,不同区域存放的数据,赋予不同的生命周期可以灵活编程
存放函数体的二进制码,由操作系统进行管理
存放 CPU 执行的机器指令代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令 2、全局区
该区域的数据在程序结束后由操作系统释放,不是程序员
存放全局变量、静态变量(static关键字修饰的)以及常量(字符串常量、全局常量) (二) 执行该程序后分为两个区域 1、 栈区
由编译器自动分配释放, 存放函数的参数值、局部变量等
不要返回局部变量的地址栈区的数据由编译器开辟和释放 2、堆区
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new在堆区开辟、释放内存
在堆区开辟数据
new返回的是该数据类型的指针int* p=new int(10) 释放堆区的数据
delete p 在堆区开辟、释放数组
int* arr=new int[10] ;(new返回的是连续空间的首地址)delete[] arr; 指针本质也是局部变量,放在栈区;指针保存的数据是放在堆区 3、堆和栈的区别
(1) 堆是由低地址向高地址扩展,是不连续的内存区域(系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址);栈是由高地址向低地址扩展,是一块连续的内存的区域(栈顶的地址和栈的最大容量是系统预先规定好的)
(2) 堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存
(3) 堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片
(4) 堆的分配效率较低,而栈的分配效率较高(栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。)
(1)与堆相比,栈不会导致内存碎片,分配效率高。
所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址和局部变量都采用栈的方式存放。如果少量数据需要频繁的操作,那么在程序中动态申请少量栈内存,会获得很好的性能提升。
(2)堆可以申请的内存大很多。
与堆相比,栈的使用不是那么灵活,如果分配大量的内存空间,推荐使用堆内存。
(一)C++编译过程 1、 第一步:编译把源文件中的源代码翻译成机器语言,保存到目标文件中。如果编译通过,就会把 cpp 转换成 obj 文件。
预处理把文本形式的源代码翻译成机器语言,并形成目标文件
做些代码文本替换工作。编译器执行预处理指令(以#开头,例如#include),这个过程会得到不包含#指令的.i文件。这个过程会拷贝#include 包含的文件代码,进行#define 宏定义的替换 , 处理条件编译指令 (#ifndef #ifdef #endif)等。 编译
通过预编译输出的.i文件中,只有常量:数字、字符串、变量的定义,以及c语言的关键字:main、if、else、for、while等。这阶段要做的工作主要是,通过语法分析和词法分析,确定所有指令是否符合规则,之后翻译成汇编代码。这个过程将.i文件转化位.s文件。 汇编
把汇编语言代码翻译成目标机器指令,生成目标文件(.o文件、.obj文件)。此过程会依赖机器的硬件和操作系统环境 2、第二步:链接
链接程序的主要工作就是将有关的目标文件连接起来。这个过程将.o文件转化成可执行的文件。 (二)C++链接: 动态链接和静态链接的区别 1、静态链接
特点:在生成可执行文件的时候(链接阶段),把所有需要的函数的二进制代码都包含到可执行文件中去。因此,链接器需要知道参与链接的目标文件需要哪些函数,同时也要知道每个目标文件都能提供什么函数,这样链接器才能知道是不是每个目标文件所需要的函数都能正确地链接。如果某个目标文件需要的函数在参与链接的目标文件中找不到的话,链接器就报错了。目标文件中有两个重要的接口来提供这些信息:一个是符号表,另外一个是重定位表。优点:在程序发布的时候就不需要的依赖库,也就是不再需要带着库一块发布,程序可以独立执行。缺点:程序体积会相对大一些。如果静态库有更新的话,所有可执行文件都得重新链接才能用上新的静态库。 2、动态链接
特点:在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。优点: 多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝。缺点: 由于是运行时加载,可能会影响程序的前期执行性能。 (三)C++静态库与动态库
1、 静态库(.a、.lib):静态库、动态库的区别来自链接阶段如何处理库,链接成可执行程序。
静态链接:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
静态库对函数库的链接是放在编译时期完成的。程序在运行时与函数库再无瓜葛,移植方便。浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新) 2、 动态库(.so、.dll):
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
动态库把对一些库函数的链接载入推迟到程序运行的时期。可以实现进程之间的资源共享。(因此动态库也称为共享库)将一些程序升级变得简单。适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
隐式加载
隐式加载,也称载入时加载,是程序载入内存时加载所需的dll文件,且该dll随主进程始终占用内存。在编码时需要使用#pragma comment(lib,“myDll.lib”)获得所需函数的入口。注意该.lib与静态链接库的.lib文件不同,静态链接库的.lib中包含了所需函数的代码,动态链接库的.lib仅指示函数在dll文件中的入口。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长。
显式加载
显式加载,也称运行时加载,是在程序运行过程中加载,不需要该dll时则将其释放。在需要时使用LoadLibrary加载,不需要时使用FreeLibrary释放。如果在LoadLibrary时该dll已经在内存,则只需将其引用计数加1,如果其引用计数减为0则移出内存。使用动态链接库的程序在发行时需要提供dll文件。在编译时,如果使用隐式链接则需要提供.lib文件,生成可执行文件后则不再需要该.lib。如果使用显式链接,在编译时不需提供.lib文件。显式加载将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。 (四) C++防止头文件被重复引入的方法 1、 使用宏定义避免重复引入
#ifndef,#define,#endif是C/C++语言中的宏定义,通过宏定义避免文件多次编译。#ifndef 是通过定义独一无二的宏来避免重复引入的,这意味着每次引入头文件都要进行识别,所以效率不高。但考虑到 C 和 C++ 都支持宏定义,所以项目中使用 #ifndef 规避可能出现的“头文件重复引入”问题,不会影响项目的可移植性。
2、 使用#pragma once避免重复引入#pragma once是一个比较常用的C/C++杂注,只要在头文件的最开始加入这条杂注,就能够保证头文件只被编译一次。和 ifndef 相比,#pragma once 不涉及宏定义,当编译器遇到它时就会立刻知道当前文件只引入一次,所以效率很高。但值得一提的是,并不是每个版本的编译器都能识别 #pragma once 指令,一些较老版本的编译器就不支持该指令(执行时会发出警告,但编译会继续进行),即 #pragma once 指令的兼容性不是很好。
三、 C++ 深拷贝和浅拷贝 1、浅拷贝浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。在释放堆空间时,如果是浅拷贝可能会出重复释放堆区内存的问题 2、深拷贝
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
3、 赋值和深/浅拷贝的区别——针对引用类型赋值: 只是复制了新对象的引用,不会开辟新的内存空间。并不会产生一个独立的对象单独存在,只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。
浅拷贝:创建新对象,其内容是原对象的引用。重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
四、 一个类对象如何进行存储在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。对于子类,最开始的内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。之后是子类自己的成员变量数据。对于子类的子类,也是同样的原理。但是无论继承了多少个子类,对象中始终只有一个虚函数表指针。
(1)对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。
(2)对于单一继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据。
(3)虚函数表中,先存放父类的虚函数,再存放子类的虚函数 。
(4)如果重载了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖。
(5)对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。
(6)对于单一虚继承,会保存两个虚表指针。子类的内存中,首先是自己的虚函数表,然后是子类的数据成员,然后是0x0,之后就是父类的虚函数表,之后是父类的数据成员。如果子类重载了父类的虚函数,那么则将子类内存中父类虚函数表的相应函数替换。
(7) 对于菱形虚继承,会先保存第一个直接基类的拷贝,然后保存第二个直接基类的拷贝,然后保存子类的数据成员,最后才保存间接基类的虚表指针和数据成员。子类的虚函数保存在第一个直接基类的虚表中。



