C++-内存

内存

内存分区模型

内存大致分为四个区域
在代码运行前就存在
代码区:存放CPU指向的机器指令
全局区 :存放全局变量和静态变量以及常量

在运行之后才有
堆区
栈区

代码区:存放函数体的二进制代码 由操作系统进行管理

存放了CPU的执行的机器指令(就是把你在编译器内写的代码翻译成一个机器可以执行的二进制命令再存放在代码区 应该就是.out文件)并不是在编译器中写的代码
具体的过程看
从cpp到exe
应该是写的所有的代码翻译成的机器码都在代码区中

代码区特点:

共享性:代码区的内容是对于外界是共享的 共享的目的是对于频繁被执行的程序,只需在内存中有一份代码即可 减少重复存储
只读性:防止外界修改指令

全局区(静态/全局存储区):存放全局变量和静态变量以及常量

该区的数据在程序结束后由操作系统自动释放
也包含了常量区

那什么是常量?

字符常量:
1,’A’,”Test Const“字符常量
例如 char x = ‘a’;会先将’a’创建在文字常量区 (.rodata) 然后当遇到需要赋值这个’a’文字常量时 将文字常量区的’a’拷贝到x变量的内存地址中
符号常量:
const float PI = 3.14;
#define PI 3.14

全局区分为三个部分 .data区和.bss区 .rodata

.data区

全局初始化了的非0全局变量(包括静态全局变量 静态局部)

.bss区

全局未初始化或者为0的全局变量

那么如果本来我没有初始化全局变量a 他现在在bss区 然后我给他赋值了 他在哪呢 其实他还在bss区 不会发生改变了
请添加图片描述

.rodata区(常量区)

存储常量

    const char* b = "b";//在常量区分配了一个内存给“b” 
    const char* a = "a";
    const char* aa = "aa";
    cout << &b << endl;//000000AE97F4F5D8
    cout << &a<<endl; //000000AE97F4F5F8
    cout << &aa<<endl; //000000AE97F4F5F8

可以看到相同的字符串在常量区只会分配一个内存

静态区

静态存储区博客

栈区:由编译器自动分配释放 存放函数的参数值 局部变量等(从下往上 内存地址不断减小)

先进后出
函数的参数值,局部变量的值,函数的地址
栈是操作系统所维护的一块特殊内存 线性表结构

大小

一般在32位操作系统中栈默认内存是1M 64位是2M

分配方式

采用先进后出的方式 从而避免了碎片问题
存在一个规则:先进后出 也就是说 如果在创建类对象的时候 先创建a1 再创建a2 在销毁的时候 会先执行a2的析构函数 再执行a1的析构函数

栈溢出是什么鬼?

官方解释:程序向栈中某个变量写入的字节数超过了这个变量本来申请的字节数 导致栈中与其相邻的变量值发生了改变

栈溢出的原因有哪些呢

局部数组过大
递归调用层次太多 例如递归时执行压栈操作 压栈次数太多
指针或数组越界

栈的一些理解
栈中的数组

栈的地址是不断递减的 但是如果一个数组a在栈中建立 他的a[0]->a[n]地址还是递增的
请添加图片描述

当调用一个函数时 栈中的变化
int func(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    cout<<func(1,3);
    return 0;
}

在函数调用时
先进栈的是main函数里的第一条指令的地址(func函数在代码段中的地址)(当然如果func函数调用前创建了变量 变量也会先进入栈中)
然后是函数的参数(先是b形参入栈 再是a 从右往左)
再将函数内的局部变量(变量c)入栈
在函数调用结束 局部变量(变量c)先出栈 然后参数再出栈(先a后b) 最后是函数地址func的地址出栈 原因就是先进后出原则

堆区:由程序员分配和释放 也可程序结束时由操作系统回收(从上往下 内存地址不断增大)

先进后出
链式结构
malloc和new分配

大小

一般在32位操作系统中堆内存可以达到4G
堆的大小受限于计算机系统中有效的虚拟内存

分配方式

类似于内存池
首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序

malloc

malloc申请的空间并不在内存中 而是申请的虚拟地址空间 得到的也只是虚拟空间的地址
堆空间也并不是内存空间 而是程序向操作系统申请的一大块虚拟地址空间
程序运行所提供的物理内存是操作系统完成的

具体能申请多大的内存 具体的数值会受到操作系统版本、程序本身的大小、用到的动态/共享库数量、大小、程序栈数量、大小等的影响,甚至每次运行的结果都可能存在差异,因为有些操作系统使用了一种叫做随机地址分布的技术,使得进程的堆空间变小

new

new是一个运算符
执行两件事情 一个是调用构造函数 一个是给指针分配内存 也就是调用operator new函数
new返回的是该数据类型的指针

int * p = new int(10) //创建一个变量存放10  p指针的地址是在栈中 存储10的这个地址在堆中   p存储的地址在堆中
int * arr = new int[10] //这里的10是代表数组有10元素
delete p //手动释放
delete[] //arr手动释放数组

他做了什么事情

/*A *a = new A*/
void *p = A::operator new(sizeof(A));
A* a1 = static_cast<A*>(p);
a1->A::A();
return a1;

实际上new有三种情况

new operator(常用的new)

平时最经常用的new就是这个
T *ptr = new T(),给指针分配内存,调用构造函数

class A
{
public:
    A(int i) :a(i){}
private:
    int a;
};

int main()
{
    A* example = new A(1);
}

new operator实际上执行了以下三个步骤:
调用operator new分配内存:operator new (sizeof(A))
调用构造函数生成类对象:A::A()
调用placement new 返回相应指针

operator new

void * operator new(size)是一个函数
系统默认的全局重载就只是调用了malloc 并返回相应指针

new operator会调用operator new
operator new不调用构造函数,而仅仅分配内存

有两个版本,前者抛出异常,后者当失败时不抛出异常,而是直接返回:

(1) void* operator new (std::size_t size);//分配size字节的存储空间,如果成功的话返回一个非空指针,将对象类型进行内存对齐,指向分配空间第一个字节。如果失败的话,会抛出bad_alloc异常
(2) void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;//和第一种一样,差别在于,如果失败的话,不抛出异常,而是返回一个null指针
(3) void* operator new (std::size_t size, void* ptr) noexcept;//只是返回ptr指针,并不分配内存空间。这里的ptr应该指向先前已经分配好的空间,这里的new调用对象的构造函数,在ptr指向的内存空间构造对象或对象数组。ptr指向的内存只要不释放,可以重复使用,所以这种用法一般在对象池或内存池实现中使用也就是placement new版本

operator new我们可以实现全局重载和类重载

class Node
{
    
public:
/*在这里我们重载operator new函数*/
    void* operator new(size_t t) 
    {
    	cout << "调用了operator new函数"<<endl; 
    	void * temp = malloc(t);
    	return temp; 
    }
    int a;//4
    char b;//1
    Node() :a(1), b('b')
    {
        cout << "调用了构造函数"<<endl;
    }
};
int main()
{
    Node* node = new Node();
}

请添加图片描述

placement new

placement new仅在一个已经分配好的内存指针上调用构造函数
placement new构造起来的对象或其数组,要显示的调用他们的析构函数来销毁,千万不要使用delete
它做的唯一一件事情就是调用对象的构造函数

new的使用方法
//可以在new后面直接赋值
  int *p = new int(3);

int q = *new int;//但是这个试了一下 好像并不是在堆区 好像会被回收(忘记了 没懂这是啥)

student *stlist = new student[3]{{"abc", 90}, {"bac", 78}, {"ccd", 93}};
堆空间数组申请方式
int**a = new int*[3]{0};
for(int i = 0;i<3;i++)
{
	a[i] = new int[4]{0};
}
/*删除*/
for(int i =0;i<3;i++)
{
	delete a[i];
}
delete[]a;

堆栈的区别

寻址方式:
堆寻址从低到高 栈从高到低
分配方式:
堆由程序员自己分配释放 栈由操作系统自动分配释放
内存碎片:
堆频繁分配释放会产生碎片程序越来越慢 栈先进后出的特性不会产生碎片
分配效率:
堆分配效率低 栈分配效率高
造成这样效率不同的原因是:
栈是操作系统提供的数据结构 有专门的寄存器 堆是c++提供的 需要各自分配内存的算法 也就是堆每次分配都需要计算

分四个区的意义

在不同的区域内 数据的生命周期会不同 使编程更为灵活

自由存储区

自由存储是C++中通过new和delete动态分配和释放对象的抽象概念
堆是操作系统所维护的一块特殊内存
new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)

注意

一个空类对象分配的地址空间是1
如果该对象中有一个指针 就是4

例子

//main.cpp 
int a = 0; //已经初始化的全局变量 全局初始化区.data段 
char *p1; //为初始化的全局变量 系统默认初始化为NULL 全局未初始化区.bss段 
main() 
{ 
int b; //局部变量 栈 
char s[] = "abc"; //局部变量 并不是new的对象 栈 
char *p2; //局部变量 只是一个指针 这个指针是存储在栈中 栈 
char *p3 = "123456"; //123456是常量 在常量区,p3在栈上。 
static int c =0;//常量 全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
//分配得来的10和20字节的区域就在堆区。 
strcpy(p1, "123456"); //123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
}

VirtualAlloc

VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存

寄存器

ESP:栈指针寄存器(extended stack pointer)

其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
就是指着栈顶

EBP 基址指针寄存器(extended base pointer)

其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
指着栈底

系统中真正的内存分配

请添加图片描述
系统中并不是你要分配多少内存就在系统中占用多少内存
如图
block size那块蓝色的区域是真实分配的大小 上面和下面都有填充0xfdfdfdfd用来分割客户可以使用的内存区和不可使用的内存区 同时,当这块内存被归还时,编辑器也可以通过下gap的值区判断当前内存块是否被越界使用了。 这也是为什么只能分配比用户申请更大的内存给用户 也就是用户申请10字节的 你必须给12字节的(为了对齐) 原因在于你如果给8字节的 虽然我们总的分配的内存更大 因为有cookie啊 或者debug什么的 但是 如果你少给了 例如申请10字节 你给8字节 那么多的两个字节数据就被写在了0xfdfdfdfd这里的前两个fd被覆盖了 那么系统检测到只有两个fd就会直接崩溃

最上面和最下面都有一个cookie占4个字节 这个cookie会记录这块内存总共用了多少 这cookie作用是在释放的时候知道要释放到哪个Header链表末尾去(Header应该就是一个内存池)
debug header那块区域是调试信息存储的地方 这也是debug版本的程序为什么会比release版本大很多的原因
并且总大小要为16的倍数

小内存分配(内存池)

就会用上内存池 主要也是为了减少cookie等空间白白消耗
在进入程序之前 系统就会分配一个堆空间 这个堆空间用于管理的动态分配 他会创建一个长度为16的类型为HEADER的链表 这个链表的每一个结点都会管理一个1MB的空间 我的感觉就是 如果这个空间被申请出去了 那么就为1 没有就为0
free和delete不需要提供大小参数的原因在于他只需要将这个链表相应结点置为1就行了 不需要知道要释放多大内存 释放的时候通过cookie就知道释放的这个内存是多大 要放到哪个header链表中去
请添加图片描述

请添加图片描述
这个就是Header链表的样子 16下面挂着的就是 最开始如果16下面没有挂着内存块 系统就会分配一块很大的内存然后分割成很多个16字节的内存块挂在16下面 如果有人要申请8-16字节的内存 就从这拿

内存池同样会产生碎片

主要原因是 例如如果我们在8字节的内存块都用完而16字节的内存块还有的时候 我们再申请8字节内存 并不会找系统要一块很大的内存再重新分割 而是找一个最适合他的 例如16字节的 分割8字节给用户 然后剩下8字节就重新挂到8字节下面 不能挂回16字节下面 如果挂回去 下次要申请16字节的时候 就会出问题 那么时间长了 就会小的字节链表下面挂了很多被分割的内存块 然后大的字节链表字节块都被分割了 但是实际上用户又把这些内存块归还了
请添加图片描述
像这样

具体看这篇博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值