data segment(数据段):存储程序中已初始化的全局变量和静态变量
bss segment(BSS段):存储未初始化的全局变量和静态变量(局部+全局),程序运行main之前时会统一初始化为0
memory mapping segment(文件映射区):存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)
一个由C/C++编译的程序占用的内存分为以下几个部分
分配效率方面:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
栈区stack
堆区heap
数据区:主要包括全局区和静态区,即存放全局变量和静态变量
在以前的 C 语言中,全局变量和静态变量又分为
静态数据成员按定义出现的先后顺序依次初始化,析构时的顺序是初始化的反顺序
代码区
在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句——保护断点,保存返回地址)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
参数是由右往左入栈主要是为了支持可变长参数形式!
printf函数的原型是:printf(const char* format,…)
printf是一个不定参函数,在实际使用中编译器通过format中的%占位符的个数来确定参数的个数。 现在我们假设参数的压栈顺序是从左到右的,这时,函数调用的时候,format最先进栈,之后是各个参数进栈,最后pc进栈,此时,由于format先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到format,而要找到format,必须要知道参数的个数,这样就陷入了一个无法求解的死循环了!! 而如果把参数从右到左压栈,函数调用时,先把若干个参数都压入栈中,再压format,最后压pc,这样一来,栈顶指针加2便找到了format,通过format中的%占位符,取得后面参数的个数,从而正确取得所有参数。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址——恢复断点,继续执行,也就是主函数中的下一条指令,程序由该点继续运行。
限制对象只能建立在堆上:
构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。
限制对象只能建立在栈上:
解决方法:将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。
指针和引用的不同之处:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。
分配 | 释放 | 可否重载 |
---|---|---|
malloc | free | 不可 |
new | delete | 不可 |
operator new | operator delete | 可 |
new[] | delete[] | 不可 |
operator new[] | operator delete[] | 可 |
delete[]时,数组中的元素按逆序的顺序进行销毁
那么如何知道调用多少次析构函数呢?
C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、munmap这些系统调用实现的。
从操作系统角度来看,进程分配内存有2种方式,分别由2个系统调用完成:brk和mmap(不考虑共享内存)。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
当我们使用new进行内存分配时,new是所谓的new operator。该操作符意义无法改变,一直做两件事:
new与malloc区别
new
申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc
在申请空间时,需要确定所申请空间的大小- new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。
- free和malloc不会调用析构函数和构造函数,new和delete会
- malloc申请内存失败返回null,new申请内存失败返回std::bad_alloc
new operator和delete operator(即new和delete)是C++内建操作符,无法修改行为。我们能够改变的是分配内存那个行为operator new
Base *b1= new Base(1,2); //等价于 Base *b1; try { //2,3可调换顺序 //1.先分配内存,返回原始内存,底层调用malloc void *temp=operator new(sizeof(Base)); //2.转型 b1=static_cast<Base*>(temp); //3.只能由编译器进行调用构造函数 b1->Base:Base(1,2) //若想直接调用ctor,可以用new(p)Base(1,2) } catch(std::bad_alloc) { //失败情况 } delete b1; //等价于 b1->~Base(); operator delete(b1); //底层调用free
operator new 源代码三种
//抛异常的 void* operator new (std::size_t size) throw (std::bad_alloc); //不抛异常的 void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw(); //placement new void* operator new (std::size_t size, void* ptr) throw();
可以重载operator new,加上额外参数,但第一参数类型必须总身size_t
这里的重载遵循作用域覆盖原则,即在里向外寻找operator new的重载时,只要找到operator new()函数就不再向外查找,如果参数符合则通过,如果参数不符合则报错,而不管全局是否还有相匹配的函数原型。
operator delete也可以重载,不过它的第一参数是void*,返回值为void。
operator delete的自定义参数重载并不能手动调用,只能老老实实delete p
如果没有给出operator new对应的operator delete,编译器将会警告或出现异常行为
operator new内部包含一个无限循环,跳出循环的办法有4种,分别为:
operator new在无法完成内存分配请求时,会在抛出异常之前调用客户指定的一个出错处理函数new_handler函数。new_handler指向一个没有输入参数也没有返回值的函数
typedef void (*new_handler()); new_handler set_new_handler(new_handler p) { throw(); }
set_new_handler
可以在malloc(需要调用set_new_mode(1)
)或operator new内存分配失败时指定一个入口函数new_handler
这个函数完成自定义处理(继续尝试分配,抛出异常,或终止程序),如果new_handler
返回,那么系统将继续尝试分配内存,如果失败,将继续重复调用它,直到内存分配完毕或new_handler
不再返回(抛出异常,终止)。
void handleBad{} //定义入口函数 _set_new_mode(1); //使new_handler有效 set_new_handler(handleBad); //指定入口函数 函数原型void f();
源代码
inline void *__cdecl operator new(size_t, void *_P) {return (_P); }
它虽然只是返回指针,但它可以实现在ptr所指地址上构建一个对象(通过调用其构造函数),这在内存池技术上有广泛应用。
即上面所说的想直接调用ctor时,用的就是placement new,调用方式
new(p)className();//可以带参数 //等价于1.调用placement new,2.在p上调用className:className()
使用placement new在某内存块里产生对象,那么应该避免对其使用delete operator
struct 结构体名 { 成员列表; }变量列表; typedef struct 结构体名 { 成员列表; }别名; struct { 成员列表; }变量列表;
编译阶段,系统只会为结构体变量分配内存空间,而不会为结构体类型分分配内存单元
为何需要内存对齐
性能原因
CPU的字长,是CPU每次到内存中存取的数据的长度。
在访问未对齐的内存时,处理器需要访问两次,而对齐的内存处理器只需要访问一次。 内存字节对齐机制为了最大限度的减少内存读取次数,CPU读取速度比内存读取速度快至少一个数量级,所以是以空间换时间
平台原因
不是所有的硬件平台都能访问任意地址的任意数据
未指定对齐系数时:
指定对齐系数时
手动指定内存对齐
#pragma pack(n);//n=1,2,4,8,16
每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行
__attribute__((aligned(n)))
放于结构体成员后面可单独改变该成员的m值
各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权,各变量共用一个内存首地址
结构体是各成员各自拥有自己的内存,同时存在的
大小计算准则
union un { char a; int b; short c; double d; int e[5]; }; //大小为24
用处:使用union检查系统大小端模式
类所占内存的大小主要是由成员变量(静态变量除外)决定的,成员函数(虚函数除外)是不计算在内的。
即类内存大小=成员变量+虚表指针(虚函数表)+虚偏移量表指针(虚继承)
类的内存计算与struct的对齐原则类似
子类的大小是本身成员变量的大小加上父类的大小
空类占用内存空间是1
因为c++要求每个实例在内存中都有独一无二的地址。所以编译器隐含地为空类增加一个字节
指针包含两部分信息:所指向的值和类型信息。
void* 指针是一种特殊的指针类型,可用于存放任意对象的地址,但是丢失了类型信息,解引用之前必须正确类型转换
指针类型
int** p_pointer; //指向 一个整形变量指针的指针 int *ptr[3]; //存储三个指向int元素的指针数组 int(*p_arr)[3]; //指向含有3个int元素的数组指针 int(*p_func)(int,int); //指向返回类型为int,有2个int形参的函数的指针
一般用取地址符号&
指针之间的赋值是一种浅拷贝,即指向同一个地址
指针值+1,编译后会让1乘上了单位sizeof(type)
特殊情况
使用解引用符(*)来访问该对象
对于结构体和类,则使用->符号访问内部成员:
void*指针在C与C++的区别:C语言中可以隐式转换,C++不行
void*指针数组在delete[]后只会释放指针内存,不会调用析构函数
对于C++的内存泄漏,总结一句话:就是new出来的内存在不需要的时候没有通过delete合理及时的释放掉!
内存泄漏不是系统无法回收那片内存,而是你自己的应用程序无法使用那片内存。
当你程序结束时,你所有分配的内存自动都被系统回收,不存在泄漏问题。但是在你程序的生命期内,如果你分配的内存都不回收,你将很快没内存使用,这才是平时所指的内存泄漏问题。
常见情况
操作系统本身就有内存管理的职责,一般而言,用malloc、new操作分配的内存,在进程结束后,操作系统是会自己的回收的。
Cache就是缓存,一般用在慢速设备和快速设备之间,目的是方便快速存取。
处理器和内存之间,存在着巨大的速度差异,提前将数据从内存载入到Cache中,在需要的时候,直接从Cache中获取,因为Cache的速度更快,这样就提高了CPU的处理能力。
Cache之所以能提高系统性能,主要在于程序执行具有局部性现象,包括时间局部性和空间局部性。
时间局部性是指,程序即将用到的数据和指令可能就是目前正在使用的数据和指令。利用时间局部性,可以将当前访问的数据和指令存放到Cache中,以便将来使用,比如C++语言中的for、while循环、递归调用等。
空间局部性是指,程序即将用到的数据和指令可能与目前正在使用的数据和指令在地址空间上相邻或者相近。
利用空间局部性,可以在处理器处理当前数据和指令时,把内存中相邻区域的指令/数据读取到Cache中,以备将来使用,比如数组访问、顺序执行的指令等。局部性原理也符合80-20原则,即程序20%的代码占用了处理器百分之八十的执行时间,占用了80%的内存。Cache预取就是根据局部性原理,预测数据和指令使用情况,并提前载入到Cache中,这样,当数据/指令需要被使用时,就能快速从Cache中获取到,而不需要访问内存。