【C++深入浅出】C/C++内存管理(教你如何new到对象)


一. 前言

        前面我们学习了有关C++类和对象的知识,学会了如何构建一个完整的类,这些类都是存储在栈空间上的。在C语言中,我们不仅可以在栈上定义变量,也可以对上的空间进行管理,在接下来的几期中,我们的目标就是学会C++中是如何进行内存管理的

        没有对象的兄弟们都看过来啦,接下来的内容就是教你如何new一个对象出来,学习完本章节内容,保你们人人都有对象,好好看好好学

        话不多说,开整!!!

二. C/C++的内存分布

        在正式学习之前,我们先来看一下如下的示例代码:

#include<stdlib.h>
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);
}

问:你能不能指出以上每个变量在内存中存储的位置?如果这个变量是个指针,那指针又是指向内存中的哪块区域?请画图分析

        我们知道,在C/C++中,内存被分为栈区、堆区、静态区、字符常量区不同的区域,每个区域存储的内容互有差别,具体可以回顾往期:

C语言地址空间icon-default.png?t=N7T8http://t.csdn.cn/P5rkL        由此我们很容易可以看出,像globalVar这些全局变量是放在静态区(数据段)中,像num1这种非静态局部变量是存放在栈区的,像"abcd"这些字符串常量是存放在字符常量区(代码区)的,而像ptr1这些则是指向由malloc函数在堆区上所申请的空间。画图分析如下:

三. C语言内存管理方式

        在C语言中,我们通常使用mallocrealloccalloc来进行动态内存管理。它们的函数原型和基本功能如下所示:

函数原型 功能说明
void* malloc(unsigned int num_bytes)

上动态申请一段num_bytes字节的空间,并返回这段空间的首地址,空间内的值不进行初始化,是随机值

void *calloc(size_t n, size_t size) 上动态申请n个size字节的空间,和malloc不同的是,空间内的值会被初始化为0
void realloc(void *ptr, size_t new_Size) 和上面两个函数不同,realloc主要是对已申请的内存空间进行扩容。ptr为指向原来空间的指针,new_size为扩容后内存空间的大小

        我们来看个小栗子:

void Test()
{
	int* p1 = (int*)malloc(sizeof(int)); //申请一个整形大小的空间,不进行初始化
	free(p1); //释放空间
	
	int* p2 = (int*)calloc(4, sizeof(int)); //申请4个整形大小的空间,初始化为0
	int* p3 = (int*)realloc(p2, sizeof(int) * 10); //对p2指向的空间进行扩容,扩容后的空间大小为10个整形的空间
	// 这里需要free(p2)吗?
	free(p3);
}

上面的p2还需要进行free()释放吗?答案是不用的。因为我们在之后对p2进行了扩容操作,而扩容分为原地扩容异地扩容

假如进行的是原地扩容,那么p2和p3指向的都是同一段空间,这时对p3进行释放就相当于对p2进行释放,如果此时我们又对p2进行了释放,则相当于对同一段空间进行多次释放,程序会崩溃

还有一种可能就是原空间所在分区后面剩余的空间不够了,此时需要进行异地扩容,那么系统就会在新的区域开辟一段空间,然后再将旧空间的数据拷贝下来,接着会自动释放旧空间,最后返回新空间的地址。

由此可见,对于p2,我们无需对其进行free()释放。

四. C++内存管理方式

        C++全面兼容C语言,故在C语言中进行动态内存管理的方式依然可以继续沿用。但在一些特殊场合使用C语言的方式无法达到目的;而且使用起来比较麻烦,需要进行各种函数调用、传参,因此C++提出了新的内存管理方式:通过newdelete操作符进行动态内存管理。

4.1 new/delete内置类型

        使用new和delete操作内置类型,基本上和malloc与free没什么区别,使用方法如下:

注意:new和deletenew[]和delete[]需要匹配进行使用,前者用于申请和释放单个元素空间,后者用于申请和释放连续的空间,不要混用,否则可能引发问题。

4.2 new/delete自定义类型

        new/delete操作符和malloc/free函数最大的区别体现在申请自定义类型中。使用new申请自定义类型的对象会自动调用构造函数,同理,使用delete释放自定义类型对象则会自动调用析构函数。下面我们来验证一下

class A
{
public:
	A(int x)
		:num(x)
	{
		cout << "A(int x)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int num;
};
int main()
{
	cout << "测试new/delete----->" << endl;
	A* ptr1 = new A(3);
	delete ptr1;
	cout << "测试malloc/free---->" << endl;
	A* ptr2 = (A*)malloc(sizeof(A));
	free(ptr2);
	return 0;
}

五. operator new与operator delete函数

        上面我们介绍的new和delete是两个用户进行动态管理内存的操作符。而在系统中,C++还给我们提供了两个全局函数operator newoperator delete,注意,这两个函数不是运算符重载!!!new操作符在底层是调用operator new全局函数来申请空间,delete操作符在底层是通过
operator delete全局函数来释放空间。

        operator new函数在底层实际上也是通过malloc来申请空间的,当空间申请成功时直接返回,如果空间申请失败并且用户没有指定应对措施,就会抛出异常。而operator delete函数底层则是通过free来释放空间。

由此可见,new操作符在空间申请失败时默认会抛出异常,而malloc函数则是返回空指针 。

六. new和delete的实现原理

6.1 对于内置类型

        针对内置类型来说,new和delete实际和malloc和free基本类似。二者只有以下两处不同:

  1. 我们是通过new/delete申请/释放单个元素,通过new[]/delete[]申请/释放连续空间
  2. 使用new和new[]申请空间时,如果申请失败则会抛出异常,而malloc则是返回空指针

6.2 对于自定义类型

        而针对自定义类型来说,new和delete会自动调用构造和析构函数,它们的实现方式如下:

  • new 的原理

        1、底层调用operator new函数申请空间

        2、在申请的空间上执行构造函数,完成对象的构造

  • delete 的原理

         1、先在已申请的空间上执行析构函数,完成对象中资源的清理工作

         2、然后调用operator delete函数释放该对象的空间

  • new T[] 的原理

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

  • delete[] 的原理

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


七. 定位new表达式

        在C++中是不允许通过对象显示调用构造函数的,只允许我们显式地调用析构函数,如下

int main()
{
	A* ptr = (A*)operator new(sizeof(A));  //使用operator new函数开辟空间,注意operator new不会自动调用构造函数
	ptr->A(10); //通过对象显式调用构造函数,不允许
	ptr->~A(); //通过对象显式调用析构函数,允许
	return 0;
}

         可是有时候我们是需要对已分配的内存空间调用构造函数进行初始化的,例如使用内存池中的空间时。由于内存池分配出的内存并没有进行初始化,故如果是自定义类型的对象,则需要显式调用构造函数对内存池的空间进行初始化,此时就需要用到我们的定位new表达式了。

        定位new的使用格式如下:

        new  (place_address)  type (initializer-list)

        其中place_address必须是个指针,而initializer-list就是传递给构造函数的初始化列表,具体使用方法如下:

// 定位new的使用
int main()
{
	//p1现在指向的只是与A对象相同大小的一段空间,并没有调用构造函数初始化对象
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A(10); //使用定位new显式调用构造函数
	p1->~A(); //显式调用析构函数
	free(p1);

	//operator new底层也是用malloc实现的,只是开辟了空间,并没有调用构造函数
	A* p2 = (A*)operator new(sizeof(A)); 
	new(p2)A(10); //使用定位new显式调用构造函数
	p2->~A();
	operator delete(p2);
	return 0;
}

我们可以发现,operator new再加上我们的定位new之后就相当于new操作符。operator new函数先申请空间,然后再使用定位new调用构造函数进行初始化。

八. malloc/free和new/delete的区别

        讲了这么多,我们也知道了如何在C++中new到一个对象了 最后我们就来总结一下C语言和C++动态申请的区别叭,作为找到对象之后的祝福叭

  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在释放空间前会自动调用析构函数完成空间中资源的清理

以上,就是本期的全部内容啦?

制作不易,能否点个赞再走呢?