当前位置: 首页 > news >正文

C++ 动态内存管理

什么是内存

一个C/C++编译的程序占用内存分为以下几个部分:

栈区(stack):由 编译器自动分配和释放,存放的是 运行时函数分配的局部变量,函数参数,返回数据,返回地址等参数,其操作类似于数据结构中的栈。

堆区(heap):一般 由程序员手动分配,如果程序员没有释放,程序结束时可能由os回收,其分配类似于链表。

全局区(静态区static):存放全局变量,静态数据,常量。程序结束后由系统释放,全局区分为已初始化全局区(data)和未初始化全局区(bss)。

常量区(文字常量区):存放常量字符串,程序结束后由系统释放

代码区:存放函数体(类成员函数和全局区)的二进制代码


三种内存分配方式:

一.从静态存储区分配内存
从静态存储区分配的内存在 程序编译的时候就已经被分配完毕了,这块内存在程序的整个运行期间都会存在(例如全局变量,static变量)

二.在栈上创建内存空间
在执行函数时, 函数内局部变量的存储单元可以在栈上创建,函数执行结束的时候,这些内存单元会自动被释放,栈内存分配运算内置于处理器的指令集,效率高,但是 分配的内存容量有限

三.在堆上分配内存(动态内存分配)🌟
在堆上分配内存亦被称为动态分配内存,程序在运行的时候使用malloc或者new申请 任意大小的内存,程序员自己负责在何时使用free和delete进行动态分配的内存的释放。 动态内存的生命周期是由程序员决定的,而且动态内存的申请和释放的使用过程非常灵活,but!如果在堆上分配了空间,则必须对堆上分配的内存进行回收,因为系统是无权对堆上的内存进行管理的,若只是申请了动态内存却不对内存进行释放,程序将会出现内存泄漏,且 频繁的分配和释放不同大小的堆空间将产生内存碎片。


内存分配的简易示意图:

在这里插入图片描述
静态全局数据区:存放全局数据和静态数据
代码段:存放可执行代码和只读常量
堆区:存放动态开辟的变量
栈区:非静态局部变量、函数参数,返回值等

堆和栈的区别

管理方式不同:栈由编译器自动申请和释放空间,堆需要程序员手动申请和释放空间。

空间大小不同:栈的空间有限,32位平台下,VC6下默认为1M,而堆最大可以达到4G。

能否产生碎片: 栈和数据结构中的栈原理相同,在弹出一个元素之前,上一个元素已经弹出了,所以不会产生碎片,但如果不停的进行动态内存的申请和释放则会积累很多内存碎片。

生长方向的不同:堆的生长方向是向上的,也就是向着内存地址增加的方向,而栈刚好是相反的,栈是向着内存减小的方向生长的(因为栈的空间十分有限,所以栈是从上限往栈的下限生长的)

分配的方式不同:堆都是动态分配的,没有静态能进行分配的堆。而栈有静态分配和动态分配两种分配方式。静态分配是编译器完成的,比如局部变量的分配,动态分配在C++中由new函数进行分配。请注意:栈的动态分配和堆的是不同的,栈的动态分配由编译器进行释放,无需使用delete进行释放。

分配的效率不同:栈的效率比堆要高很多,因为栈是机器系统提供的数据结构,计算机在底层提供了栈的支持,分配专门的寄存器来存放栈的地址,压栈和出栈都有相应的指令,因此栈在分配的效率上是一定比堆上快的。而堆是由库函数提供的,机制很复杂,库函数会按照一定的算法进行搜索内存,因此比较慢。


静态全局变量(static),全局变量,静态局部变量(static),局部变量的区别:

静态全局变量和全局变量的区别:
1.静态全局变量和全局变量 都属于常量区
2.静态全局区 只在本文件中有效,别的文件如果向调用该变量,是调用不了的,且 全局变量在别的文件中还可以调用
3.如果别的文件中定义了一个该全局变量相同的的变量名,是会出错的。

静态局部变量和局部变量的区别:
1. 静态局部变量是属于常量区的,而函数内部的 局部变量属于栈区
2. 静态局部变量在该函数调用结束的时候,不会销毁,而是随着整个程序的结束而结束,静态局部变量不能被此函数外的函数调用。局部变量也是随着该函数的结束而结束的。
3.如果定义这两个变量的时候没有赋初始值,那么 静态局部变量会自动的定义为0,而局部变量就是一个随机的值(一般的编译器会强制要求局部变量必须进行赋初值 而静态局部变量是否赋初值则不做强制性要求)。
4.静态局部变量在编译期间 只赋值一次,以后每次调用函数时,不再赋值,而是直接调用上次的函数调用结束时的值。而局部变量在调用期间, 每调用一次,便会对这个局部变量进行重新赋值

例如以下代码演示:
在这里插入图片描述

总结:🎯
全局变量:可以被本程序所有对象或函数引用.
局部变量:只能被函数内部引用,而无法被其他的对象或函数引用.
全局静态变量:是在全局变量声明前加上一个static关键字,使该变量只能在这个源文件可用.
局部静态变量:通常放在函数内部,只能在函数内部被调用,只进行一次初始化,每次执行函数时保持上一次执行的值.

来自https://blog.csdn.net/cherrydreamsover/article/details/81627855


为什么需要动态内存

到目前为止,我们实现的每一个程序在完成它的任务的时候使用的内存空间都是固定不变的,这个固定不变的内存空间在程序编写的时候就可以知道和确定(一般为变量的形式)。这些程序都不能在程序运行期间动态增加或减少内存空间,也就是说,只要程序里定义了变量,系统就会给这个变量分配固定的内存空间,但是当需要的内存取决于用户输入。在这些情况下,程序需要动态分配内存。

但是C++必须支持动态的管理内存,比如说我们不可能要求用户输入的文本必须为固定字符。在很多时候,需要存储的数据量到底有多大在事先往往是一个未知的数,要想处理好这类的情况,我们就需要在C++程序中使用动态内存(程序能识别需要的内存大小,动态增加或减少内存)。

例如: 数组的长度是预定好的,在整个程序中固定不变,在未出现动态内存和动态数组之前,C++是不允许定义元素个数不确定的数组的

int n;
int a[n];//这种定义,是C++所不允许的

像上述代码是不可能编译成功的


但是在实际的编程中,往往会出现所需的内存空间大小取决于实际要处理的数据的多少,而实际处理的数据数量在编程时无法确定的情况,如果总是定义一个尽可能大的数组,又会造成空间浪费。况且,这个"尽可能大"的具体范围也无法确定

所以为了解决上述等问题,C++提供了一种 动态分配内存 的机制,使的 程序可以在运行的期间,根据实际的需要,要求操作系统临时分配一片内存空间用于存放数据。此种内存分配是在程序运行中进行的,而非在程序编译是就确定的,因此被称为 “动态内存分配”

动态内存分配 支持程序员创建和使用种种能够根据具体需要扩大和缩小的数据结构,它们只受限于计算机的硬件内存总量系统的特殊要求


静态内存

使用静态内存的量: 变量(包括指针变量),固定长度的数组,某给定类(自定义类)的对象。这些量我们可以在程序代码里通过它们的名字或者地址来进行访问和使用

使用静态内存的弊端:
不得不在编写程序时为有关变量分配一块尽可能大的内存(以防止不够存储数据),一旦程序运行,不管变量占用内存的情况如何,这个变量都将占用那么多的内存,没有任何办法能改变这个静态内存的大小


动态内存

动态内存是由一些 没有名字,只有地址的内存块构成,动态内存块是在程序运行期间动态分配的。动态内存块来自一个C++库管理的大池子(“内存池”)

申请动态内存(new):

从内存池申请一些内存需要用到 “new语句” ,他将 根据你提供的数据类型,分配一块大小适当的内存,不必担心内存块的尺寸问题,编译器能记住每一种数据类型的单位长度并迅速的计算出需要分配出多少字节

new做了两件事:
1.调用operator new ,然后 operator new再调用malloc函数申请动态内存空间。
2.调用构造函数初始化。

Array *p1=new Array;->call operator new()->call malloc()

动态变量申请:

基本格式:

Type* pointer = new Type;
//...
delete pointer;

此表达式用于分配内存以包含某个类型的类型的单个元素。

例如声明一个指针变量 “i” ,并动态分配 int类型大小的内存块

int *i=new int;

🔥其中,int是任意类型名,i是类型为int*的指针,这样的语句会动态分配一片大小为sizeof(int)字节的内存空间,并且将该内存空间的起始地址赋值给 " i ",例如:

int *p;
p=new int; 🔥此行动态分配了4个字节大小(int)的内存空间,而p指向这片空间,于是我们通过P就可以读写该内存空间。
*p=5;

在这里插入图片描述
.
.
.

动态数组申请:

前边我们用new给基本类型和对象在运行时分配了动态内存,但是这些基本类型的尺寸是在编译时就已经被确定的,这是因为我们为之申请的内存的数据类型在程序里有明确的定义和明确的单位长度。但是有些时候,我们必须等到程序运行时才能确定到底需要申请多少内存,甚至还需要根据程序的运行情况追加申请更多的内存:
例如一个在程序运行时才决定元素多少的数组

此时我们便需要了解new运算符的第二种用法,动态分配一个任意大小的数组.

new[N]做了两件事:
1.调用operator new分配空间。
2.调用N次构造函数分别初始化每个对象。

基本格式:

Type* pointer=new Type[N];

🔥其中,Type是任意类型名,pointer是类型为Type*的指针,N代表"元素个数",元素个数可以是任何值为正整数的表达式,表达式里
可以包含变量,函数调用等参数.上述的语法动态分配出  Nxsizeof(Type)个字节的内存空间,这片动态内存空间的起始地址被赋值给pointer

delete[] pointer;
pointer=NULL;

表示用于分配某个类的类型的元素的块(数组),其中N是表示这些元素的量的整数值。

例如:

int *foo;
int i=5;
foo=new int[i]; //一共有 0-4 五个元素
foo[0]=20;
foo[4]=30; //所以此动态数组下标值最大的元素为[4] 

在这种情况下,系统为int类型的五个元素动态分配空间,并返回指向序列的第一个元素的指针,该指针被分配给foo这个(指针),因此 foo现在指向一个有效的内存块,其中包含5个int类型元素的空间。
在这里插入图片描述
由于这里foo是一个指针,因此foo指向的第一个元素可以使用表达式foo[0]或表达式*foo(两者都是等价的)来访问。可以使用foo[1]或 *(foo+1)访问第二个元素,以此类推…

如果 有足够的内存能满足申请,new语句将返回新分配地址块的起始地址(首地址)
如果 没有足够的内存,那么new语句将抛出std::bad_alloc异常


释放动态内存(delete):

在用完动态内存块之后,应该 用delete语句把它释放,还给内存池。另外作为一种附加的保险措施,在 释放了内存块以后,还应该把与之关联的指针设置为NULL(防止野指针的出现)。

动态变量释放:

delete 做了两件事:
1.调用析构函数清理对象
2.调用operator delete释放空间

例如:

delete i;

在这里插入图片描述
.
.

动态数组释放(delete []):

delete[]干了两件事:
1.调用N次析构函数清理对象。
2.调用opera投入 delete释放空间。

int *p=new int[20];
p[0]=1;
p[2]=3;

delete[] p;//释放动态数组

因为用来保存数组地址的变量只是一个简单的指针,所以我们需要明确的告诉编译器他应该删除一个数组。
删除动态数组的具体做法是在delete保留字的后面加上一对方括号,方括号后方紧跟数组的名字:delete[]p;


释放动态分配关联的指针(NULL) :

NULL指针
有一个特殊的地址值: NULL指针,当把一个指针向量设置为NULL的时候,它的含义是那个指针将不再指向任何东西

例如:

i=NULL;

在这里插入图片描述
例如:

int *x;
x=NULL; //这时候x什么地址都不指向

我们无法通过一个被设置为NULL的指针去访问任何数据,试图对一个NULL指针进行解引用将在运行的时候被检测到错误到并导致程序终止运行!

总结:🎯
自定义变量时,new和delete会自动调用默认成员函数, new 时调用构造函数,delete时调用析构函数


为对象分配动态内存空间

为对象分配内存和为各种基本数据类型(int,char,flaot…)分配内存在做法上完全一样,也是用new向内存池申请内存,用delete释放分配的内存。

为对象分配动态内存后 千万别忘记把调用的方法声明成虚方法。

重新使用某个指针之前一定要将这个指针delete释放,如果不这样做,这个指针将得到一个新内存块地址,而程序永远也无法释放原先的内存块,因为它的地址已经被覆盖掉了。

delete语句只释放给定指针变量正指向的内存块,不影响这个指针。在执行delete语句之后,那个指针指向的内存块被释放了,但这个指针变量还在

为自定义类的对象分配动态内存的实例:

class Company
{
public:
	Company(string theName);
	virtual void printInfo();
protected:
	string name;
};

class TechCompany :public Company
{
public:
	TechCompany(string theName, string product);
	virtual void printInfo();
private:
	string product;
};

Company::Company(string theName)
{
		name = theName;
}

void Company::printInfo()
{	
	cout<<"这个公司的名字叫:"<<name<<"\n\n";
}

TechCompany::TechCompany(string theName, string product) :Company(theName)
{
	this->product = product;
}

void TechCompany::printInfo()
{	
	cout << name << "公司大量生产了" << product << "这款产品!\n";
}

int main()
{
	Company* company = new Company("APPLE");
	company->printInfo();

	delete company;
	company = NULL;

	company = new TechCompany("APPLE", "IOS");
	company->printInfo();

	delete company;
	company = NULL;

}

运行结果:
在这里插入图片描述


为数组分配动态内存

int main()
{
	 int count = 0;
	cout << "请输入数组的元素: ";
	cin >> count;
	
	int* x = new int[count]; 🔥 //建立一个数组指针 "x" ,指向使用new出来的动态内存空间的数组 
	🔥//假设有一个普通数组 a[5], 那么 a[0]就相当于 *a,a[1]就相当于*(a+1)... 并且a+1的实际大小是a+sizeof(int)的大小,这是		因为数组的名字就是数组的首地址,那么数组名的地址加上元素的大小就是数组第二个元素的地址...所以此时 int*x指针指向动态数组的时候,			可以用指针x的名字加上[]号索引数组的元素。
	for (int i = 0; i < count; i++)
	{
		x[i]=i;
	}
	for (int i = 0; i <count; i++)
	{
		cout << "x[" << i << "]的值是" << x[i] << endl;
	}

	delete[]x;//释放x指针指向的动态数组
	x = NULL;//释放x这个指针
	return 0;
}

运行结果:
在这里插入图片描述


注意:🎯
1. 静态内存这个术语与C++保留字static没有任何关系,静态内存意思是指内存块的长度在程序编译时被设定为一个固定的值,而这个值在程序运行时是无法改变的。
2.new语句返回的内存块很可能充满"垃圾"数据,所以我们通常先往里面写一些东西覆盖,再对new出来的内存块进行访问,或者再类中直接写一个构造器来初始化。
3.在使用动态内存时,最重要的原则时 每一条new语句都必须要有一条与之匹配的delete语句,没有配对的delete语句或者有多个个与之配对的delete语句都属于编程的漏洞,尤其是前者将会导致 内存泄露!
4.new 会先开辟空间,再调用构造函数 ,delete 会先调用析构函数,再释放空间。

内存泄漏:
内存泄漏(MemoryLeak)是指程序中已动态分配的堆内存由于某种原因程序为释放或无法进行释放,造成系统内存的浪费,累积到一定程度导致程序运行速度减慢甚至系统奔溃等严重的后果。
内存泄漏缺陷具有隐蔽性,累积性的特征,比其他内存非法访问错误更难检测,因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而非过错型缺陷,此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐进行积累,降低系统整体性能,极端的情况下会导致系统崩溃,但是,只要重新启动计算机,这种情况就会消失。

来自:
https://blog.csdn.net/ZAhqc_IT/article/details/75666748
https://blog.csdn.net/cherrydreamsover/article/details/81627855
https://blog.csdn.net/qq_40416052/article/details/82493916
http://c.biancheng.net/view/206.html

相关文章:

  • IOS学习资源
  • C++ 从函数或方法返回动态内存(函数指针与指针函数)
  • PHP实现多web服务器共享SESSION数据-session数据写入mysql数据库
  • C++ 副本构造器
  • C++常识之——C++中堆和栈的区别,自由存储区、全局/静态存储区和常量存储区...
  • C++ 高级强制类型转换
  • thinkphp的一些类库
  • C++ 指针和引用
  • 9月24号忘记
  • C++ 避免内存泄漏
  • [转]SQL Server DBCC用法大全
  • C++ 命名空间和模块化编程
  • FG终于投出去了~
  • 资源仓库
  • open cv建立一个标准的opencv程序
  • AWS实战 - 利用IAM对S3做访问控制
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • gitlab-ci配置详解(一)
  • interface和setter,getter
  • iOS帅气加载动画、通知视图、红包助手、引导页、导航栏、朋友圈、小游戏等效果源码...
  • JS实现简单的MVC模式开发小游戏
  • Koa2 之文件上传下载
  • Mysql数据库的条件查询语句
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • 动态规划入门(以爬楼梯为例)
  • 构建二叉树进行数值数组的去重及优化
  • 记一次用 NodeJs 实现模拟登录的思路
  • 聊聊redis的数据结构的应用
  • 面试遇到的一些题
  • 漂亮刷新控件-iOS
  • 全栈开发——Linux
  • 深度学习在携程攻略社区的应用
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 学习笔记:对象,原型和继承(1)
  • 用 Swift 编写面向协议的视图
  • k8s使用glusterfs实现动态持久化存储
  • Python 之网络式编程
  • 好程序员web前端教程分享CSS不同元素margin的计算 ...
  • 微龛半导体获数千万Pre-A轮融资,投资方为国中创投 ...
  • ​比特币大跌的 2 个原因
  • ​油烟净化器电源安全,保障健康餐饮生活
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • #pragma预处理命令
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • (java)关于Thread的挂起和恢复
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (强烈推荐)移动端音视频从零到上手(上)
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .
  • (转)Linux整合apache和tomcat构建Web服务器
  • (转)母版页和相对路径
  • .bat批处理(二):%0 %1——给批处理脚本传递参数