C++——内存管理(new/delete使用详解)

C++内存管理

本章思维导图:
C++——内存管理(new/delete使用详解)_第1张图片注:本章思维导图对应的xmind文件和.png文件已同步导入至资源

1. C/C++内存区域的划分

在C/C++中,内存区域主要划分为:内核区域、栈区、内存映射段、堆区、数据段、代码段等区域,如图:

C++——内存管理(new/delete使用详解)_第2张图片

本篇我们主要讨论栈区、堆区、数据段和代码段这四个区域。

栈区:主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。存放在栈区的变量的生命周期就是所在函数的作用域

堆区:主要存放的是malloc、realloc、calloc、new等动态开辟函数开辟的空间。存放在堆区的变量的生命周期是整个进程

数据段:主要存放的是我们在函数外定义的全局数据,或者static静态数据。存放在数据段的变量的生周期是整个进程

代码段:主要存放的是const只读常量即字面量

下面,我们通过一个具体的例子来弄清楚各类数据到底存放在计算机的那一块空间:

int globalVar = 1;
static int staticGlobalVar = 1;

void Test()

{
	static int staticVar = 1;
	const int localVar = 1;
	
	int num1[10] = {1, 2, 3, 4};
	char char2[] = "abcd";
	char* pChar3 = "abcd";

	int* ptr1 = (int*)malloc(sizeof (int)*4);

	free (ptr1);
	free (ptr3);
}

Question:

/*  
选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
   globalVar在哪里?____   staticGlobalVar在哪里?____   staticVar在哪里?____   const localVar在哪里?____
   num1 在哪里?____
   
   char2在哪里?____   *char2在哪里?___
   pChar3在哪里?____      *pChar3在哪里?____
   ptr1在哪里?____        *ptr1在哪里?____
*/
  • globalVar定义在全局(函数外),是一个全局变量,因此存放在数据段
  • staticGlobalVar也是一个定义在全局的静态变量,因此也存放在数据段(静态区)
    • 需要注意:尽管globalVarstaticGlobalVar都存放在数据段中,但是它们之间也有链接属性的区别。globaVar可以多文件共同使用,而staticGlobalVar只能在本文件使用。
  • staticVar尽管定义在函数内,但是由static修饰,是一个静态变量,因此也存放在数据段
  • const localVar定义在函数内,是一个局部变量,因此存放在栈区
    • 注:千万不要认为localVarconst修饰他就存放在代码段(常量区),const只是修饰局部变量localVar,表示它的值不能被修改。
  • num1是静态开辟的数组,所以存放在栈区
  • char2也是个静态开辟的字符串数组,因此也存放在栈区

  • *char2:这里的char2表示首元素的地址,所以*char2就表示字符a。看到这里,可能有小伙伴就会说*char2存放在常量区,但事实上*char2还是存放在栈区。我们可以从两个方面解释:

    • 常量区的数据不能被修改但是对于*char2,我们可以对其进行修改,也就是普通的堆字符数组进行修改,因此*char2不在常量区,而是在栈区。

    • 实际上,初始化字符数组char2的字符abcd是存放在常量区的字符abcd的拷贝,这份拷贝同样存放在栈区:

      C++——内存管理(new/delete使用详解)_第3张图片

  • pchar3就是定义在函数内的局部变量,因此存放在栈区

  • pchar3指针指向的就是字符串字面量abcd,而字符串字面量const常量,因此存储在代码段

  • ptr1就是定义在函数内的局部变量,因此存放在栈区

  • ptr1指向的是malloc开辟的空间,因此*ptr1存放在堆区

2. new/delete

C++——内存管理(new/delete使用详解)_第4张图片

  • C++使用newdelete动态管理内存
  • newdelete是C++内置的两个运算符,不需要显示的包含头文件来使用。

2.1 new/delete和malloc/free的区别

有小伙伴可能会有疑惑:

既然C语言已经可以用malloc、realloc、free等函数来动态管理内存了,为什么C++还要新创建两个运算符newdelete来替代C语言的方法呢?

一个原因是,C++是面向对象的,C++有C语言没有的class类,类的初始化必须调用构造函数,但是构造函数不能显示调用,例如:

class A
{
public:
	A(int a = 1)
		:_a(a)
	{

	}

private:
	int _a;
};

int main()
{
	A* p = (A*)malloc(sizeof(A));
	p->A();

	return 0;
}

//会报错:类型名称“A”不能出现在类成员访问表达式的右侧

可见,C语言并不能解决C++自定义类型初始化的问题,所以C++才要新创建两个运算符newdelete来解决这一问题。

这次,我们换用C++的new运算符来动态开辟内存,并用delete进行空间的释放:

class A
{
public:
	A(int a = 1)
		:_a(a)
	{

	}
    
    ~A()
    {
	}

private:
	int _a;
};

int main()
{
	A* p = new A;
    delete p;

	return 0;
}

让我们进行调试:

C++——内存管理(new/delete使用详解)_第5张图片

可以看到:

  • new在给自定义类型开辟空间时,会在给对象开辟完空间后继续调用该对象的默认构造完成初始化

  • delete在释放自定义类型对象之前,会先调用对象的析构函数,再释放对象的空间

除了上面最重要的不同之外,malloc/freenew/delete还有许多不同之处:

  • malloc/free是函数,而new/delete是运算符
  • malloc开辟空间时需要用sizeof计算开辟空间的大小,而new只需要在后面加上开辟对象的类型就行
  • malloc的返回值时void*,实际使用时需要强制类型转换;而new不需要,因为开辟时就指定了对象类型
  • malloc失败时,会返回0,因此需要对结果进行判空;而new不需要,但是需要进行异常捕获

2.2 new/delete的使用

如果一次只开辟、释放单个对象,基本格式为:

  • new type;
  • 例如:int*p = new int; delete p;

如果一次开辟、释放多个对象,基本格式为:

  • new type[nums]
  • 例如:int*p = new int[10]; delete[] p

对于自定义类型,newdelete的使用和内置类型基本一致,但是我们可以在new的同时给默认构造传参(如果可以传的话),这样可以一次创建初始值不同的多个同一类型的多个对象:

例如:

class AB
{
public:
	AB(int a = 1, int b = 1)
		: _a(a)
		, _b(b)
	{

	}
private:
	int _a;
	int _b;
};

class C
{
public:
	C(int c = 1)
		:_c(c)
	{

	}
private:
	int _c;
};

int main()
{
	AB* p1 = new AB;	//生成一个对象:_a = 1, _b =  1
	AB* p2 = new AB{ 2,2 };		//生成一个对象: _a = 2, _b =  2
	C* p3 = new C(2);	//生成一个对象:_c = 2

	C* p4 = new C[3];	//生成三个对象:_c都为1
	C* p5 = new C[3]{ 2, 3, 4 };	//生成三个对象:_c分别为2,3,4
	AB* p6 = new AB[3]{ {2,2}, {3,3}, {4,4} };	//生成三个对象: _a, _b分别为(2,2)、(3,3)、(4,4)

	delete p1;
	delete p2;
	delete p3;

	delete[] p4;
	delete[] p5;
	delete[] p6;

	return 0;
}

特别注意

new/deletenew[]/delete[]必须配套使用,不能随意搭配!!!

2.3 new/delete的底层原理

C++——内存管理(new/delete使用详解)_第6张图片

实际上,执行运算符new开辟空间和delete释放空间时,编译器都会调用operator newoperaotr delete这两个函数

注意:

不要operator newoperaotr delete这两个函数理解为运算符重载

我们可以先来看看operator new的具体实现:

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
     if (_callnewh(size) == 0)
     {
         // report no memory
         // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
         static const std::bad_alloc nomem;
         _RAISE(nomem);
     }
return (p);
}

可以看到,operator new开空间时实际上用的也是C语言的malloc函数,operator new只不过是对malloc的一层封装。那么为什么要对malloc进行封装呢

malloc失败时返回的是整数0,这不符合C++面向对象的要求,因此C++要对malloc进行封装,使内存开辟失败时可以抛出异常

同样**,实际上operator delete也是用C语言的free函数,来实现对空间的释放**

//等效
int* p1 = (int*)malloc(sizeof(int) * 10);
int* p2 = (int*)operator new(sizeof(int) * 10);

//等效
free(p1);
operator delete(p2);

现在,我们对于C++newdelete对于自定义类型空间的开辟和释放就更加明了了:

  • new首先调用operator new来开辟对象的空间,再调用对象的默认构造进行初始化
  • delete首先调用析构函数释放对象成员变量的资源,再调用operator delete释放该对象的空间

2.4 定位new(仅作了解)

在这里插入图片描述

在之后的学习过程中,我们可能会遇到需要显式调用自定义类型构造函数的情况。但是一般情况下,构造函数不支持显式调用,此时定位new就可以帮我们解决这个问题。

  • 定位nwe可以显示调用构造函数
  • 其基本格式为new (place_address) type或者new (place_address) type(initializer-list)
  • place_address就是是一个指向和一个类相同大小空间的指针
  • type就是类类型
  • initializer-list就是构造函数的参数列表

例如:

class A
{
public:
	A(int a = 1)
		: _a(a)
	{

	}

	~A()
	{

	}
private:
	int _a;
};

//构造函数不能显式调用,但是析构函数可以
int main()
{
	A* p = (A*)malloc(sizeof(A));
	new(p)A(10);

	p->~A();

	return 0;
}![请添加图片描述](https://img-blog.csdnimg.cn/49bacd2462d14cf3a6b58056d5206c4a.gif)


下一篇,我们将对C++的模板和泛型编程展开讲解,感兴趣的小伙伴可以关注此专栏。

C++——内存管理(new/delete使用详解)_第7张图片

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