C++内存管理

内存区域划分

C++内存管理_第1张图片

☀️具体区域和存储的数据:

  1. 栈区:非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 堆区:用于程序运行时动态内存分配,堆是可以上增长的。
  3. 数据段:全局数据/静态数据(static)。
  4. 代码段:可执行的代码/只读常量(const)。
  5. 内存映射段:(目前没学)是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。

☀️补充:const修饰的哪一部分不能被修改?

被const修饰的变量,不可以被修改,然而对于const修饰指针的不同位置,到底是哪部分不可以被修改?

int a[] = {1,2,3};
const int* p1 = a;
int* const p2 = a;

两个指针p1和p2都指向数组a,数组名a即数组首元素地址,const分别写在不同位置:

对于p1:const修饰的是*p1,即修饰的是解引用该地址后得到的东西,这个东西就是数组中的所有元素。因此,数组中的元素不可以被改变。

对于p2:const修饰的是p2,即修饰的是这一串地址。因此这一个整型空间内储存的地址编号不可以被改变,指针永远指向数组所占用的那一块空间。(至于空间的元素可以被改变)

☀️练习:判断变量具体存储在哪个区域

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
 static int staticVar = 1;
 int localVar = 1;
 int num1[10] = { 1, 2, 3, 4 };
 char char2[] = "abcd";
 const char* pChar3 = "abcd";
 int* ptr1 = (int*)malloc(sizeof(int) * 4);
 int* ptr2 = (int*)calloc(4, sizeof(int));
 int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
 free(ptr1);
 free(ptr3);
}
  1. 选择题:
    选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
    globalVar在哪里?答:(全局变量)C
    staticGlobalVar在哪里?答:(静态变量)C
    staticVar在哪里?答:(静态变量)C
    localVar在哪里?答:(局部变量)A
    num1 在哪里?答:(局部变量,数组名是首元素地址)A

    char2在哪里?答:(局部变量,数组名是首元素地址)A
    *char2在哪里?答:(解引用得到首元素,存放在栈区)A
    pChar3在哪里?答:(指针名,首元素地址)A
    *pChar3在哪里?答:(*pChar3被const修饰)D
    ptr1在哪里?答:(指针,指向动态空间,但指针本身储存于栈区)A
    *ptr1在哪里?答:( *ptr1是动态空间上储存的数据)B

  2. 填空题:
    sizeof(num1) = 答:4×10=40(num1是数组名,算数组本身大小)
    sizeof(char2) = 答:5×1=5(字符串以“\0”结尾,要算上“\0”)
    strlen(char2) = 答:4(strlen计算时以“\0”为结束标志,不计“\0”)
    sizeof(pChar3) = 答:4或8(pChar3是地址,占4或8字节)
    strlen(pChar3) = 答:4
    sizeof(ptr1) = 答:4或8(ptr1是地址,占4或8字节)

  3. sizeof 和 strlen 区别?
    sizeof(数组名)=数组大小(以字节为单位)
    sizeof(字符串)=类型大小×元素个数+1(字符串以“\0”结尾,要计算在内)
    sizeof(指针)=4或8(指针储存于一个整形空间)

    strlen(数组名)=数组大小
    strlen(字符串)=字符串本身大小
    strlen(指针)=指针指向的字符串的长度

    虽然数组名即指针名,但被放在sizeof和strlen的括号中时,计算的是整个数组的大小;当strlen括号中放入一个指针时,计算的是指针指向字符串的长度。

C语言动态内存管理方式

malloc/calloc/realloc开辟,free释放。

☀️面试题:malloc/calloc/realloc的区别:

malloc:指针指向新开辟空间的起始位置:
size以字节为单位。
在这里插入图片描述

calloc:指针指向新开辟空间的起始位置,将所有位置初始化为0:
开辟num个空间,每个开辟的空间的大小是size,总字节数为num×size。
在这里插入图片描述

realloc:在原有空间的基础上扩大或缩小,指针仍指向空间的起始位置:
prt指向原始的空间,size是扩大或缩小后空间的字节数
在这里插入图片描述

C++动态管理内存的方式

通过new和delete操作符。

☀️1.new/delete操作内置类型

void Test()
{
  // 动态申请一个int类型的空间
  int* ptr4 = new int;
  
  // 动态申请一个int类型的空间并初始化为10
  int* ptr5 = new int(10);
  
  // 动态申请10个int类型的空间
  int* ptr6 = new int[3];
  delete ptr4;
  delete ptr5;
  delete[] ptr6;
}

语法:

T是某个内置类型

  1. 申请一个T类型空间:指针=new T;释放:delete 指针;
  2. 申请一个T类型空间并初始化为n(n是常量):指针=new T(n);释放:delete 指针;
  3. 申请N个T类型的空间:指针=new T[N];释放:delete[ ] 指针;

特性:

  1. 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ]。
  2. 动态存储内置类型本质上和malloc与free一样,malloc相当于new或new[ ],free相当于delete或delete[ ]。
  3. 不同的是,malloc和free是函数,new和delete是操作符

☀️2.new/delete操作自定义类型。

主要针对的是类

class A
{
public:
 A(int a = 0)
 : _a(a)
 {
 cout << "A():" << this << endl;
 }
 ~A()
 {
 cout << "~A():" << this << endl;
 }
private:
 int _a;
};
nt main()
{
 // new可以初始化,malloc不行
 A* p1 = (A*)malloc(sizeof(A));
 A* p2 = new A(1);
 free(p1);
 delete p2;

//申请一段连续空间时,new后面要跟[N],delete后面要跟[]
 A* p3 = (A*)malloc(sizeof(A)*10);
 A* p4 = new A[10];
 free(p3);
 delete[] p4;
 return 0;
 }
  1. new的过程分为两部分:①开辟空间 ②调用构造函数初始化
  2. delete的过程分为两部分:①调用析构函数 ②释放的空间
    (注意,二者顺序不可以颠倒,有时一个类内部的成员变量会指向动态开辟的空间,如果先释放掉整个类占用的空间,则会丢失掉那个指针,造成内存泄漏)
  3. new[N]的过程分为两部分:①一次性开辟N份空间 ②调用N次构造函数,将每一个空间都初始化
  4. delete[ ]的过程分为两部分:①调用N次析构函数 ②一次性释放掉这N个类所占用的空间
  5. new(new A[N])/delete(delete[ ])对于自定义类型除了开空间还会调用构造函数和析构函数,malloc和free只能开空间,无法同时初始化。

例:栈类Stack,栈的内部还有一块动态开辟的空间:

涉及到两层动态开辟:1.先开辟一个Stack大小的空间,系统计算Stack有多大 2.然后调用Stack的构造函数,函数内部n还需要动态开辟指针_a指向的空间,该空间大小为sizeof(int)*capacity。

涉及到两层delete:1.先调用析构函数,函数内的delete是释放_a指向的空间 2.然后释放掉类本身占用的空间。

class Stack
{
public:
	Stack(int capacity = 4)
	{
	cout<<"Stack(int capacity = 4)"<<endl;
	_a = new int[capacity];
	_top = 0;
	_capacity=capacity;
	}
	~Stack()
	{
	cout<<"~Stack()"<<endl;
	delete[] _a;
	_a=nullptr;
	_top=0;
	_capacity=0;
	}
	
private:
int* _a = nullptr;
int _top;
int _capacity;
};

☀️疑问1:当连续申请或释放空间时,为何new的[N]内标明空间的个数N,而delete的[ ]内什么都不需要?

答:当内部开空间时,实际上会多开辟四个字节,存放n这个整型数据,用这个数据告诉编译器你在delete时要释放多大的空间,也因此,delete时不需要我们传个数。
在这里插入图片描述

☀️疑问2:当连续申请或释放空间,new和delete格式不匹配时,会发生什么?

比如,用new A[10]申请了10份空间,但用delete释放,没加[ ]:
C++内存管理_第2张图片

会程序崩溃。开空间是会在空间前面多开四个字节储存N,此时对应的释放方法是delete[ ],[ ]的作用就是让指针往前移动四个字节,把所有空间都释放掉;如果没有[ ]的话,指针不往前移动,仅仅释放了部分的空间,于是导致程序崩溃。

operator new/delete和抛异常

☀️operator new/delete函数概念

  1. operator new与operator delete函数不是对new和delete的重载,是系统提供的全局函数。
  2. new在底层调用operator new全局函数来申请空间,而operator new是对malloc的一层封装。
  3. delete在底层通过operator delete全局函数来释放空间,而operator delete是对free的一层封装。
  4. 封装有何意义,为何不直接用malloc来申请空间?为了在申请空间失败时,能让C++拥有和C不同的失败处理机制从而符合面向对象特征。

☀️operator new申请空间失败会抛异常

  1. C语言的malloc失败的话是返回空指针(0),而C++作为面向对象的语言,不能接收0,而是采用抛出异常的方式。
  2. 内部原理:先借助malloc开空间,一旦开空间失败了即malloc返回空指针,就进入循环,执行抛异常,换成C++想要的处理方式:
    C++内存管理_第3张图片
  3. 抛异常是在开空间的时候进行的,一点抛出了异常,就不会再去调用构造函数了。
  4. 如果抛出了异常,程序会终止了,因此后面将会学习try catch语句进行异常捕获,就是为了解决程序终止问题。

new和delete的实现原理

☀️1.内置类型

  1. 如果申请的是内置类型的空间,new和malloc,delete和free基本类似。
  2. 不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

☀️2. 自定义类型

(1)new的原理

  1. 调用operator new函数申请空间,operator new实际是对malloc函数的封装。
  2. 在申请的空间上执行构造函数,完成对象的构造。

(2)delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作。
  2. 调用operator delete函数释放对象的空间,operator delete实际是对free函数的封装。

(3)new T[N]的原理

  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数一次性完成N个对象空间的申请。
  2. 在申请的空间上执行N次构造函数。

(4)delete[]的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来一次性释放空间。

☀️常见面试题

malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:

  1. malloc和free是函数,new和delete是操作符。
  2. malloc申请的空间不会初始化,new可以初始化。
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

定位new表达式

定位new表达式一般和内存池配合使用

☀️内存池

  1. 如果遇到需要频繁通过new来申请一小块空间的情况,会十分影响运行效率;而如果能先申请一块比较大的空间,大空间能存放得下许多份小空间,就不需要每次申请小空间了,提高程序效率。
  2. 这个大空间就是内存池,空间具体多大取决于池子的设置,内存池的大小设计要合理。
  3. 内存池仅仅是申请好的空间,有了内存池就不需要operator new或malloc函数和operator delete或free函数了,但还是要自己初始化空间。

☀️定位new表达式格式(显式构造)

为了配合内存池,需要显示进行初始化,然而不可以显式调用构造函数,于是使用定位new表达式就用来在已有空间的基础上初始化。定位new表达式就属于显式初始化。

格式:
new (place_address) type或者new (place_address) type(initializer-list);
place_address必须是一个指针,initializer-list是类型的初始化列表。

☀️显式调用析构函数

为了配合构造函数,也同样需要显式释放,与构造函数不同的是,可以显式调用析构函数。
格式:place_address->~type( );

☀️完整使用定位new的例子:

class A
{
public:
 A(int a = 0)
 : _a(a)
 {
 cout << "A():" << this << endl;
 }
 ~A()
 {
 cout << "~A():" << this << endl;
 }
private:
 int _a;
};
int main()
{
 A* p1 = (A*)malloc(sizeof(A));
 new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
 p1->~A();
 free(p1);//malloc和free对应
 
 A* p2 = (A*)operator new(sizeof(A));
 new(p2)A(10);
 p2->~A();
 operator delete(p2);//operator new和operator delete对应
  return 0;
}

你可能感兴趣的:(c++,开发语言,c语言)