您现在的位置是:首页 >技术杂谈 >C&C++内存分布网站首页技术杂谈

C&C++内存分布

恒539 2025-03-31 00:01:03
简介C&C++内存分布

        终于走过了 C++ 的类和对象,我们本篇迎来的是不那么痛苦的内存管理。

目录

C/C++的内存分布

C/C++的内存管理

如何开辟

如何销毁

new 和 delete底层

实现原理

定位 new 表达式

使用


C/C++的内存分布

        在我们使用编程语言时,我们会处理各种各样的数据。这些数据分别是:局部数据、静态数据、全局数据、常量数据以及动态申请的数据。因此,在语言角度,我们可以将内存分为以下几块:栈,堆,静态区(又名为数据段),代码段(又被称作常量区)。这几块是我们经常接触的,除此以外,还有内核空间和内存映射段(可以看下面的图),不过对于这两个我了解不多。

        对于栈和堆,我们需要知道,栈和堆位于内存两端,栈是向下增长的,堆则是向上增长。具体地说,栈内存从高地址向低地址扩展,而堆则从低地址向高地址扩展,对于这些,我们只要知道就行了,暂且不去深究。
内存图

C/C++的内存管理

        我们所说的内存管理主要是动态内存管理,即我们主动在堆中申请一块内存空间,使用完毕后再自己释放。在 C 语言中,动态开辟内存通过一系列函数(malloc / calloc / realloc / free)完成,C++兼容 C 语言,在编写C++程序中,我们也可以使用这些函数进行内存管理,不过,C++有自己的方式进行内存管理:new 和 delete 。我们先看下面的代码:

int main()
{
	// 动态申请一个 int 类型大小的空间
	int* pi = new int;

	// 申请的时候初始化
	int* pi_1 = new int(10);

	// 申请10个空间
	int* pi_2 = new int[10];

	// 申请的时候初始化
	int* pi_3 = new int[10] { 0, 1, 2, 3 };
	
	int n;
    cin >> n;
    int* pi_4 = new int[n];
    // 释放空间
    delete pi;
    delete pi_1;
    delete[] pi_2;
    delete[] pi_3;
    delete[] pi_4;
	return 0;
}

        这就是C++的内存管理:通过 new 开辟内存空间,通过 delete 释放内存空间。

如何开辟

        当只开辟一个空间时,格式为:new 类型名()。当我们想要对开辟的空间进行初始化时,就在括号中写上初始值,不想初始化的话,就不带括号。当想开辟多个空间时,通过"[]“指定数量,此时如果想要进行初始化,就要使用”{}"进行。

        对于基本类型来说,当初始化多个空间时,初始化的规则和初始化数组的初始化相同:给予的初始值的数量小于空间数量时,剩下的空间置为 0。

        至于自定义类型,初始化的时候就不是赋值了,而是传参来调用构造函数。如果不传参,那就调用默认构造函数。具体的可以看下面的代码:

class example
{
public:
	example()
	{
		cout << "example():  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	example(int a)
		: _a(a)
	{
		cout << "example(int a):  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	example(int a, int b)
		: _a(a)
		, _b(b)
	{
		cout << "example(int a, int b):  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
private:
	int _a = 0;
	int _b = 0;
};
int main()
{
	// 无参创建
	example* pe = new example;
	// 传参创建
	example* pe_1 = new example(5);
	example* pe_2 = new example(5, 25);

	delete pe;
	delete pe_1;
	delete pe_2;
	
	return 0;
}

在这里插入图片描述
        就像代码显示的那样,对于基本类型和自定义类型,开辟空间时的区别就是:基本类型传值是进行初始化,自定义类型传值是为构造函数传递参数,调用构造函数进行初始化

        当创建多个空间时,会多次调用构造函数进行初始化,此时如果不传参,调用的仍然是默认构造函数。就像下面那样:

int main()
{
	example* pe = new example[3];
	cout << "===================================" << endl;
	example* pe_1 = new example[4]{ 1, 2 , 3 }; // 为每个对象传递单个参数
	cout << "===================================" << endl;
	example* pe_2 = new example[6]{ {1, 2}, {3, 4}, {5, 6}, { 7 }, 8 }; // 为每个对象传递
    													//多个参数, { 7 } 和 8 是传递单个参数
	return 0;
}

在这里插入图片描述

如何销毁

        销毁非常简单,如果是单个空间,就是:delete 内存首地址,如果是多个空间,那就是:delete[] 内存首地址。对于基本类型,delete 是释放空间,对于自定义类型,delete 会先调用自定义类型的析构函数,然后再释放空间。

class example
{
public:
	~example()
	{
		cout << "~example()" << endl;
	}
};

int main()
{
	example* pe = new example[3];
	example* pe_1 = new example[4];
	example* pe_2 = new example[6];

	delete[] pe;
	cout << "===================================" << endl;
	delete[] & pe_1[0];
	cout << "===================================" << endl;
	delete[] pe_2;
	return 0;
}

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

new 和 delete底层

        首先,new 和 delete 并不是函数,而是运算符。但是这两个运算符是依靠两个全局函数 operator new() 和 operator delete() 实现的(并不是运算符重载)。在 operator new() 中会使用 malloc 函数申请空间,而在 operator delete() 中最终也是使用 free() 进行空间的释放。在operator new() 中如果空间申请失败则会抛出异常,所以我们使用 new 开辟内存空间时并不需要检查是否开辟成功,而是需要捕获异常。对于异常,我们之后再说。

        如果是开辟和释放多个空间,实现的函数也会不同,分别是 operator new[] (这个函数会调用 operator new)和 operator delete[] (调用 operator delete)。

实现原理

        对于内置类型而言,new/delete 和 malloc/free 并没有本质区别,不同的是,malloc 开辟失败会返回 NULL,而 new 开辟失败会抛出异常。并且在涉及多空间时,要使用 new[] 和 delete[]。

        对于自定义类型而言,使用 malloc/free 仅仅只会开辟/释放一定大小的空间,但是使用 new和delete 的话,也是使用 malloc 开辟空间,但是开辟空间之后会调用构造函数进行初始化;通过 free 释放空间前会调用析构函数进行资源释放。

定位 new 表达式

        定位 new 表达式指在已分配的内存空间中调用构造函数进行初始化。我们可以看下面的代码:

class example
{
public:
	example()
	{
		cout << "example():  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	example(int a)
		: _a(a)
	{
		cout << "example(int a):  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	example(int a, int b)
		: _a(a)
		, _b(b)
	{
		cout << "example(int a, int b):  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
private:
	int _a = 0;
	int _b = 0;
};
int main()
{
	// 开辟一块空间
	example* p1 = (example*)malloc(sizeof(example));
	new(p1)example(25, 16);
	new(p1)example(13);
	new(p1)example();
	p1->~example();
	free(p1);

	return 0;
}

        代码中 main() 函数体中的代码演示的就是定位 new 表达式。

使用

        如上所示,定位 new 的使用格式非常明显:

new (place_address) type或者new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

        而且使用定位 new 时,指针所指向的空间必须是动态申请的内存空间。此外,当指针指向的空间并不是单个自定义类型大小时(动态开辟数组),我们无法使用定位 new 将所有元素进行初始化,只能一个一个进行初始化:

class example
{
public:
	example()
	{
		cout << "example():  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	example(int a)
		: _a(a)
	{
		cout << "example(int a):  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	example(int a, int b)
		: _a(a)
		, _b(b)
	{
		cout << "example(int a, int b):  ";
		cout << "_a = " << _a << " ; " << "_b = " << _b << endl;
	}
	~example()
	{
		cout << "~example()" << endl;
	}
private:
	int _a = 0;
	int _b = 0;
};
int main()
{
	// 开辟一块空间
	example* p1 = (example*)malloc(sizeof(example));
	new(p1)example(25, 16);
	new(p1)example(13); // 多次定位new初始化
	new(p1)example();
	p1->~example();
	free(p1);
	cout << "===================================================" << endl;
	example* p2 = (example*)malloc(5 * sizeof(example)); // 多个空间
	new(p2)example(25, 16); // 首元素初始化
	new(p2 + 1)example(15, 16); // 第二个元素初始化
	new(p2)example(13);
	new(p2)example();
	p2->~example();
	(p2 + 1) -> ~example();
	free(p2);

	return 0;
}

        读者可以对代码自行调试。

        我们从代码中可以看到,我们在使用时可以对一块空间进行多次定位,这时,指针所指向的内存空间会不断进行初始化。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。