您现在的位置是:首页 >技术杂谈 >C&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 并不是函数,而是运算符。但是这两个运算符是依靠两个全局函数 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 表达式指在已分配的内存空间中调用构造函数进行初始化。我们可以看下面的代码:
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;
}
读者可以对代码自行调试。
我们从代码中可以看到,我们在使用时可以对一块空间进行多次定位,这时,指针所指向的内存空间会不断进行初始化。