文章目录
- 一、内存分布
- 二、c语言内存管理复习
- 三、c++内存管理
- 四、operator new 和 operator delete
- 五、定位new表达式
- 六、malloc/free 与 new/delete的异同
前言
c语言内存管理复习、c++内存管理、operator new 和 operator delete、定位new表达式、malloc/free 与 new/delete的异同
一、内存分布
1.内存区域
栈区、堆区、静态区(数据段)、常量区(代码段)
2.内存区域存储的数据
(1).栈区:函数栈帧在栈区,局部变量存储在栈区
(2).堆区:动态开辟的内存就是堆区的内存
malloc、calloc、realloc开辟的空间
(3).静态区(数据段):全局变量,静态全局变量,静态局部变量
(4).常量区(代码段):常量字符串,常量值,代码编译好的指令
3.题目分析
(1):C 解析:globalvar是一个全局变量,全局变量存储在静态区(数据段)
(2):C 解析:staticGlobalvar是一个静态全局变量,静态全局变量存储在静态区
(数据段)
静态全局变量和全局变量的异同点:
静态全局变量和全局变量都存储在静态区
静态全局变量和全局变量的生命周期都是整个程序
静态全局变量拥有内部链接属性
全局变量拥有外部链接属性
(3):C 解析:staticVar是一个静态局部变量,静态局部变量存储在静态区
静态局部变量的生命周期是整个程序
静态局部变量只有执行到静态局部变量这一行代码时才会创建
静态局部变量只会创建一次
(4):A 解析:localvar是一个局部变量,局部变量存储在栈区
(5):A 解析:numl数组名,表示整个数组/数组首元素地址
numl表示整个数组:局部数组创建在栈区
numl表示数组首元素地址:此时numl也在栈区
(6):A 解析:char2是一个局部数组,存储在栈区
char2使用常量字符串初始化数组
那么数组就会根据常量字符串的大小在栈区开辟空间
然后再将常量字符串拷贝过来
(7):A 解析:*char2表示数组首元素,而char2数组是在栈区上的
数组在栈区开辟空间存储数据,所以char2的元素也在栈区
(8):A 解析:pchar3是一个局部指针变量,指针指向的内容存储在常量区
const修饰的是pchar3指向的内容不可以修改,而非pchar3本身
注意:在c++中const修饰的变量称作常变量
const修饰的变量也存储在栈区
(9):D 解析:pchar3是一个局部指针变量,pchar3存储在栈区
pchar3指向的内容存储在常量区(代码段)
所以*pchar3是指常量字符串的第一个字符,存储在常量区(代码段)
(10):A 解析:ptr1是一个局部指针变量存储在栈区
(11):B 解析:ptr1是一个局部指针变量存储在栈区
但是ptr1指向的空间是malloc动态开辟的空间
malloc开辟的空间存储在堆区
所以*ptr1表示堆区的第一个元素,所以*ptr1存储在堆区
4.注意:在c++中const修饰的变量称作常变量
const修饰局部变量存储在栈区
const修饰的全局变量存储在静态区
const修饰的static局部变量存储在静态区
二、C语言内存管理复习
1.C语言内存管理所需要的是函数
包括malloc,calloc,realloc,free
2.malloc
malloc只负责开辟内存
3.calloc
calloc负责开辟内存和初始化为0
4.realloc
(1). realloc传NULL可以开辟内存
(2). realloc传其他指针可以扩容
(3). realloc扩容分为原地扩容和异地扩容
假设已经开辟内存16个字节,需要扩容到40个字节
a.realloc原地扩容
如果这16个字节后面有24个未使用的字节,那么就采用原地扩容
返回原地的起始地址
b.ralloc异地扩容
如果这16个字节后面没有24个字节,那么就是用异地扩容
异地扩容过程:
_1.开辟一块新的40个字节的空间
_2.将原来16个字节的数据拷贝到新的空间
_3.释放原来的16个字节的空间
_4.返回新的内存空间的起始地址
5.
该情况下无需释放p1
原因:realloc分为原地扩容和异地扩容
如果是原地扩容返回的是原地的起始地址,那么此时p2 == p1
释放p2就足够了,同一块空间不可以释放两次
如果是异地扩容那么原地就在realloc中释放了,返回的是异地的起始地址
释放p2就够了
6.无论c/c++,同一块内存空间不可以释放两次
同一块内存空间不可以分开释放、
三、c++内存管理
1.c++内存管理是通过new和delete操作符来操作的
c/c++此处区别:
C语言是通过malloc/calloc/realloc/free函数来进行内存管理,注意是通过函数
c++是通过new/delete操作符来进行内存管理,注意是使用操作符
2.c++内存管理new和delete操作符使用方式
(1). new内置类型
a.new用来开辟内存的
new会自动识别类型不需要强制类型转换
b.new可以开辟一个对象也可以开辟多个对象
开辟一个对象语法:new 类型;
开辟多个对象语法:new 类型[对象个数];
c.new可以在开辟内存的时候初始化
语法:new 类型(初始化值)
语法:new 类型[对象个数]{初始化值,如果是不完全初始化其余使用0初始化}
(2).delete内置类型
a.delete是用来释放对象的
b.delete释放new单个对象
语法: delete 指针;
c.delete释放new多个对象
语法:delete[] 指针;
一定要配套使用
(3).new/delete在内置类型上和malloc/free区别不大
new/delete在自定义类型上和malloc/free区别很大
(4).new自定义类型
a.new自定义类型
第一步:根据自定义类型大小开辟空间
第二步:自动调用自定义类型的构造函数初始化
b.new单个自定义类型对象
_1:语法:new 自定义类型;
过程:new开辟自定义类型大小的空间
new自动调用默认构造函数
如果此时没有默认构造函数,编译器会报错
_2:语法:new 自定义类型(参数);
过程:new开辟自定义类型大小的空间
new调用带参构造函数
c.new多个自定义类型的对象
_1:语法:new 自定义类型[对象个数size];
过程:new开辟size个对象大小的空间
new自动调用size次默认构造函数初始化size个对象
_2:语法:new 自定义类型[对象个数size]{自定义类型(参数),自定义类型(参数)};
如果是不完全初始化,其余的对象会自动调用默认构造函数
过程:new开辟size个对象大小的空间
使用带参构造函数初始化的对象自动调用带参构造函数
其余对象自动调用默认构造函数进行初始化
(5).delete自定义类型对象
a.delete自定义类型对象
过程:delete调用自定义类型的析构函数,
清理释放自定义类型对象申请的动态内存
delete释放动态开辟的自定义类型对象本身
b.delete单个自定义类型对象
语法:delete 指针;
过程:delete首先调用自定义类型的析构函数,清理释放对象申请的动态内存
delete再释放动态开辟的自定义类型对象本身
c.delete多个自定义类型对象
语法:delete[] 指针;
过程:delete[]首先调用size次自定义类型的析构函数,
清理释放size个对象申请的内存空间
delete[]再清理释放开辟的size个对象的空间
(6).new和delete一定要配对使用
a.new一个对象使用delete 指针;
b.new多个对象使用delete[] 指针;
(7).new、delete与malloc、free的区别
a.new 与 malloc
_1.内置类型中,new可以开辟空间,可以初始化空间
malloc只可以开辟空间
_2.自定义类型中,new不仅仅可以开辟空间
并且自动调用默认构造函数、带参构造函数
malloc只可以开辟空间,因为类的成员变量大部分都是私有private
malloc开辟了空间初始化都没办法初始化
b.delete 与 free
_1.内置类型中,delete可以释放动态开辟的空间
free可以释放动态开辟的空间
_2.自定义类型中,delete不仅仅会释放动态开辟的空间
并且自动调用析构函数
free只能释放动态开辟的空间,对象被释放后,对象申请 的动态内存资源都找不到了
(8).new自定义类型对象的运作过程
a.首先调用operator函数取开辟自定义类型对象的空间
b.开辟好空间后,再调用自定义类型对象的默认、带参构造函数
c.编译器看到new操作符之后会自动转换为调用operator new函数
以及对应的构造函数
(9).delete自定义类型对象的运作过程
a.首先调用该自定义类型对象的析构函数,清理释放对象申请的动态内存
b.清理完毕对象申请的资源之后,
调用operator delete函数清理释放动态开辟的对象
c.编译器看到delete操作符之后会自动转换为调用析构函数
以及operator delete函数
(10).new内置类型运作过程
a.编译器看到new操作符之后就自动转换为operator new函数
(11).delete内置类型运作过程
a.编译器看到delete操作符之后会自动转换为operator delete函数
3.new失败返回与malloc失败返回
(1).new失败后会抛异常,malloc失败后会返回NULL
(2).异常
a.c语言出现错误时返回的是错误码,通过错误码进行报错,但是错误码那么多
总不能出现一个错误码就去查询一下
b.于是c++提出异常,面向对象的语言处理错误的方式基本上都是抛异常
c.c++程序中出现错误会抛异常,抛异常 == 抛一个类型的对象出来
通常该类型由错误信息和错误码、id组成
d.c++库中的内容抛异常抛的是一个exception类型的对象
exception类包括what()成员函数,错误信息,错误码,id
通过访问what()成员函数就可以访问到错误信息,错误码,id
e.当程序中抛出异常之后,必须在调用链中捕获异常
如果没有捕获异常,那么程序会异常退出
如果捕获异常之后,输出异常,并继续执行程序
(从捕获异常之后的下一行代码执行)
(3).库中的内容失败会自动抛异常,抛exception类型对象的异常
自己的内容如果需要抛异常需要自己写抛异常 + 抛什么类型对象的异常
(4).抛异常的语法
throw 类型对象
(5).捕获异常异常语法
try
{
会出现异常的代码
}
catch(抛出异常的类型对象的const引用)
{
输出异常;
}
(6).调用链
main -> func1() -> func2() -> func3();
如果func3()抛异常
那么只需要在func3()/func2()/func1()/main()中任意一层捕获异常就可以
func3() -> func2() -> func1() -> main
如果直到main函数还没有捕获异常,那么程序会异常退出
无论哪一层捕获异常,抛出异常之后就会自动跳转到捕获异常的那一层
捕获完异常之后,从捕获完异常的下一行代码继续执行
4 .阶段总结
(1).malloc无论内置类型、自定义类型只会开辟空间,不会初始化
free无论内置类型、自定义类型只会释放空间
(2).new内置类型会开空间也可以初始化
new自定义类型会开空间并且自动调用构造函数初始化
(3).delete内置类型,会释放内置类型的空间
delete自定义类型,会显调用自定义类型的析构函数
再释放自定义类型对象的空间
(4).malloc返回的是void*接收时需要强制类型转换
new会自动识别类型,接收时不需要强制类型转换
(5).malloc失败后是返回NULL,new失败后是抛异常(抛exception类型的对象)
四、operator new 和 operator delete
1.operator new 和 operator delete 是两个库函数,和运算符重载没有关系
2.当new内置类型对象时,编译器看到new会自动转换为调用operator new函数
当delete内置类型对象时,编译器看到delete会自动转换为operator delete函数
3.当new一个自定义类型对象时
编译器看到new会自动转换为调用operator new函数开辟空间
再自动调用自定义类型的构造函数初始化自定义类型对象
当delete一个自定义类型对象时
编译器看到delete会哦自动转换为调用自定义类型的析构函数
再自动调用operator delete 函数
4.operator new函数的底层是malloc函数
为什么不直接还用malloc函数?
因为malloc失败后返回的是NULL,而c++规定new失败后要抛异常,
所以就将malloc函数包装一下
形成operator new函数,是的失败后可以抛异常
5.operator delete函数底层是free函数
为什么不直接还用free函数?
为了和operator new进行配对
6.new内置类型[];
当new多个内置类型的对象时,编译器会自动转换为调用operator new[]函数
new自定义类型[];
当new多个自定义类型的对象时,编译器会自动转换为调用operator new[]函数
再调用size次构造函数
operator new[] -> operator new -> malloc
operator new[]纯属为了配对
7.delete[] 内置类型的指针;
当delete[]释放多个内置类型的对象时,编译器会自动转换为operator delete[]函数
delete[] 自定义类型的指针;
当delete[]释放多个自定义类型的对象时,会先调用size次析构函数,
再调用operator delete[]函数
operator delete[] -> operator delete -> free
operator delete[]函数纯属为了配对
8.前提:类中自动产生的析构函数对于内置类型的成员变量不做处理
对于自定义类型的成员变量会调用该类型的析构函数
当前类中的成员变量都是内置类型
条件1:当显示定义析构函数时
在new多个自定义类型的对象时,开辟的空间会自动增加4个字节
在头部的4个字节会被用来存储对象的个数,返回指针时
返回的是剩余字节的起始地址
存储对象的个数是为了使得delete[]时可以知道要调用几次析构函数
delete[] 指针;会自动将指针向前偏移4个字节,拿到要析构函数的次数
并且释放时,也是从向前偏移4个字节之后的地址开始释放
因为这四个字节也是从这次内存开辟中开辟出来的
条件2:当没有显示定义析构函数时
编译器自动生成的析构函数对内置类型什么也不会做
当new多个自定义类型的对象时,开辟空间不会自动增加4个字节
因为此时编译器自动生产的析构函数什么都不会做,那么编译器就会优化
此时该自定义类型的对象delete[]时不需要调用析构函数,所以就不会
添加4个字节去存储析构的次数
此时delete[]也不会向前偏移4个字节去取析构次数
此时delete[]都不会取调用析构函数
如果new多个自定义类型的对象使用delete释放而不是delete[]释放会发生什么?
(1)如果自定义类型的成员变量都是内置类型,且没有显示定义析构函数,
那只会调用一次析构函数其他的无影响;
原因:
a.编译器自动生成的西沟函数对于内置类型不会做处理,所以此时编译器会自动优化,
认为自定义类型的对象不需要调用析构函数,所以开辟空间时也不会多开辟4个字节,
delete此时要释放的位置就是正确位置,但是此时只会调用一次析构函数
(2)如果自定义类型的成员变量都是内置类型,且显示定义了析构函数,那么就会报错
原因:
a.delete默认这次释放的是一个对象,只会调用一次析构函数,析构函数调用次数不准确
b.delete默认这次释放的是一个对象,就不会向前偏移4个字节去找析构的次数
释放的位置也不会向前偏移4个字节,释放的位置不对
连续开辟的内存不可以只释放一部分
(3)如果自定义类型的成员变量有自定义类型,且没有显示定义析构函数,那么就会报错
原因:
a.编译器自动生成的析构函数对于自定义类型的成员变量会调用该自定义类型的析构函数
b.所以编译器就会认为开辟的对象是需要析构的,所以就会多开4个字节存储析构的次数
而此时使用delete释放,只会调用一次析构函数,并且也不会自动向前偏移4个字节,
释放的位置也不正确,不可以只释放一部分
(4)如果自定义类型的成员变量有自定义类型,且显示定义了析构函数,那么就会报错
a.此时定义了析构函数,那么编译器就会认为开辟的对象需要调用析构函数
那么在开辟对象时就会多开辟4个字节,存储需要析构的次数
b.而此时使用delete进行释放,那么delete只会调用一次析构函数
并且delete不会自动向前偏移4个字节,此时释放的位置也不正确,不可以只释放一部分
五、定位new表达式
1.构造函数是不可以通过对象显示调用的,析构函数可以通过对象显示调用
2.定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
3.语法: new(指针)类型; 调用默认构造函数
new(指针)类型(参数); 调用带参构造函数
4.定位new表达式使用场景
(1)在内存池中可以使用定位new表达式
(2)内存池
a.new和malloc都是向系统申请空间
b.系统需要做的工作有很多,向系统申请的资源是需要排队的
c.如果此时需要频繁的向系统申请内存,效率低
d.此时就可以直接向系统申请自己所需要的足够大的空间,自己来管理这一块内存
自己进行内存的开辟和释放,提高了效率
e.向系统一次性申请的一大块空间就称作内存池
(3)因为new和malloc都是向系统申请内存,所以在内存池中申请内存时不可以通过new
和malloc进行申请内存,所以此时我们就需要自己手动去申请内存
那么申请的内存只是一块内存,无法通过指针->显示调用构造函数,
那么为了初始化这一块空间,也就是为了调用构造函数
就需要使用定位new表达式来调用构造函数
六、malloc/free 与 new/delete的异同
1.malloc与new都是在堆区上开辟空间,free与delete、都是在堆上释放空间
2.malloc与free是两个函数,new和delete是两个操作符
3.malloc无论内置类型/自定义类型,都只会开辟空间,不会初始化
new对于内置类型会开辟空间也可以初始化
new对于自定义类型会开辟空间并且自动调用自定义类型的构造函数初始化
4.free无论内置类型/自定义类型只会释放空间
delete对于内置类型只会释放空间
delete对于自定义类型会显调用该自定义类型的析构函数,再释放自定义类型
5.malloc失败后会返回NULL,new失败后会抛异常
6.malloc返回的是void*需要强制类型转换
new会自动返回所需要的类型不需要强制类型转换
7.malloc需要手动计算所需要的字节数,new只需要跟类型
8.new delete new [] delete[] 一定要配对使用