一、C++中的动态内存分配
在C++中,使用运算符new分配的内存,称为动态内存,C语言使用malloc函数来分配动态内存。
动态内存由运算符new 和delete控制,而不是由作用域和链接性规则控制,因此可以在一个函数中分配动态内存,在另一个函数中将其释放。
与自动内存的栈不同,动态内存不是先进后出,其分配和释放的顺序仅取决于new和delete在何时,以何种方式被使用。
通常编译器使用3块独立的内存:
一块用于静态变量,也就是常说的静态存储区;
一块用于自动变量,也就是常说的栈;
一块用于动态内存,也就是常说的堆;
关于数据的存储,可以参考文章:C++学习:数据的存储、作用域、链接-CSDN博客
虽然存储方案概念不适用于动态内存,但是适用于 用来跟踪动态内存的 自动和静态指针变量。比如下面的语句
float* p_free = new float[20];
由new分配80个字节(float占4个字节)的内存将一直保留在内存中,直到使用delete运算符将其释放。但是当包含该声明的语句块执行完毕时,p_free指针消失了,如果希望另一个函数能够使用这80个字节中的内容,则必须将其地址传递或者放回给该函数。另一方面,如果p_free的链接性申明为外部的,则文件中位于该申明后面的所有函数都可以使用它,通过在另一个文件中使用下面的声明,就可以使用该指针
extern float* p_free;
二、使用new运算符初始化
语法:
Type* pointer = new Type;
delete pointer;
为int和double类型分配存储空间并初始化;
所谓初始化就是在类型后面加上初始值,并用括号括起来。
int* pi = new int (6);//*pi初始化为6
double* pd = new double (99.99);// *pd初始化为99.99
程序实例1:变量申请
#include<stdio.h>
int main()
{
int* p = new int; //在堆空间申请一个int类型的变量,指针p指向该变量
*p = 5;
*p = *p + 10;
printf("p = %p\n", p);
printf("p = %d\n", *p);
delete p; //释放堆空间
return 0;
}
输出结果:
p = 00CF4CA8
p = 15
数组申请
语法:
Type* pointer = new Type[N];
delete[] pointer;
程序实例2:数组申请
#include<stdio.h>
int main()
{
int* p = new int[10]; //指向一片堆空间,数组空间不小于40个字节,而不一定是40个字节
for (int i = 0; i < 10; i++)
{
p[i] = i + 1;
printf("p[%d] = %d\n",i,p[i]);
}
delete[] p; //释放堆空间
return 0;
}
输出结果
p[0] = 1
p[1] = 2
p[2] = 3
p[3] = 4
p[4] = 5
p[5] = 6
p[6] = 7
p[7] = 8
p[8] = 9
p[9] = 10
三、new与new[]分别对应的函数
new和new[]是运算符,他们分别对应下列函数
//new对应的函数
void* operator new(std::size_t);
//new[]对应的函数
void* operator new[] (std::size_t);
//delete对应的函数
void* operator delete(void*);
//delete[]对应的函数
void* operator delete[] (void*);
四、new关键字与malloc函数的区别
new关键字是C++的一部分;
malloc是由C库提供的函数;
new以具体类型为单位进行内存分配;
malloc以字节为单位进行内存分配;
new在申请单个类型变量时可进行初始化;
malloc不具备内存初始化的特性;
程序实例3:new关键字的初始化
#include<stdio.h>
int main()
{
int* pi = new int(1); //申请一段内存,int类型,并初始化为1
float* pf = new float(2.0f); //申请一段内存,float类型,并初始化为2.0
char* pc = new char('c'); //申请一段内存,char类型,并初始化为'c'
printf("pi = %d\n",*pi);
printf("pf = %f\n", *pf);
printf("pc = %c\n", *pc);
return 0;
}
输出结果
pi = 1
pf = 2.000000
pc = c
五、定位new运算符
通常new负责在堆中找到一个足以能够满足要求的内存块,但是这个内存块在什么位置,只能看天意了。new运算符还有另一个变体,被称为定位new运算符,它能让你在指定的位置使用内存。程序员可以使用这种特性来设置内存管理,处理需要通过特性地址进行访问的硬件或在特定位置创造对象。
要使用定位new,首先要包含new头文件,下面的代码演示了new运算符的4种用法
#include <iostream>
#include <new>
struct chaff
{
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
using namespace std;
chaff *p1, *p2;
int *p3, *p4;
//常规new运算符的使用
p1 = new chaff; //在堆中申请一片内存,用来存chaff类型的结构体
p3 = new int[20]; //在堆中申请一片内存,用来存int[20]数组
//定位new运算符
p2 = new (buffer1) chaff; //把chaff类型的结构体放在buffer1中
p4 = new (buffer2) int[20]; //把int[20]类型的数组放在buffer2中
return 0;
}
熟悉定位new运算符之后,来看看一个示例程序,使用常规new运算符和定位new运算符创建动态分配的数组,来说明常规new与定位new之间的差别。
#include <iostream>
#include <new>
const int BUF = 512;
const int N = 5;
char buffer[BUF];
int main()
{
using namespace std;
double *pd1, *pd2;
int i;
cout << "Calling new and placement new:\n";
pd1 = new double[N]; //在堆中申请一片内存
pd2 = new (buffer) double[N]; //使用数组buffer的内存
for(i=0; i<N; i++)
{
pd2[i] = pd1[i] = 1000 + 20.0*i;
}
cout << "print the memory address of pd1 and buffer:\n";
cout <<"heap:" << pd1 <<" static: " << (void*)buffer << endl; // 打印pd1的地址和buffer的地址
cout << "print the element memory address of pd1 and pd2:\n";
for(i=0; i<N; i++)
{
cout << pd1[i] << " at " << &pd1[i] << " ; " << pd2[i] << " at " << &pd2[i] << endl;
}
cout << "\nCalling new and placement new a second time:\n";
double *pd3, *pd4;
pd3 = new double[N];//在堆中找到一片新的内存
pd4 = new (buffer) double[N]; //覆盖数组中旧的数据
for(i=0; i<N; i++)
{
pd4[i] = pd3[i] = 1000 + 40.0*i;
}
cout << "print the element memory address of pd3 and pd4:\n";
for(i=0; i<N; i++)
{
cout << pd3[i] << " at " << &pd3[i] << " ; " << pd4[i] << " at " << &pd4[i] << endl;
}
cout << "\nCalling new and placement new a third time:\n ";
delete [] pd1; //释放pd1的内存
pd1 = new double[N]; //在堆中重新申请一片内存
pd2 = new (buffer + N * sizeof (double) ) double[N]; //使用数组buffer的内存,在不同的位置
for(i=0; i<N; i++)
{
pd2[i] = pd1[i] = 1000 + 60.0*i;
}
cout << "print the element memory address of pd1 and pd2:\n";
for(i=0; i<N; i++)
{
cout << pd1[i] << " at " << &pd1[i] << " ; " << pd2[i] << " at " << &pd2[i] << endl;
}
delete [] pd1;
delete [] pd3;
return 0;
}
程序结果
Calling new and placement new:
print the memory address of pd1 and buffer:
heap:0x941ba0 static: 0x408040
print the element memory address of pd1 and pd2:
1000 at 0x941ba0 ; 1000 at 0x408040
1020 at 0x941ba8 ; 1020 at 0x408048
1040 at 0x941bb0 ; 1040 at 0x408050
1060 at 0x941bb8 ; 1060 at 0x408058
1080 at 0x941bc0 ; 1080 at 0x408060
Calling new and placement new a second time:
print the element memory address of pd3 and pd4:
1000 at 0x941980 ; 1000 at 0x408040
1040 at 0x941988 ; 1040 at 0x408048
1080 at 0x941990 ; 1080 at 0x408050
1120 at 0x941998 ; 1120 at 0x408058
1160 at 0x9419a0 ; 1160 at 0x408060
Calling new and placement new a third time:
print the element memory address of pd1 and pd2:
1000 at 0x941ba0 ; 1000 at 0x408068
1060 at 0x941ba8 ; 1060 at 0x408070
1120 at 0x941bb0 ; 1120 at 0x408078
1180 at 0x941bb8 ; 1180 at 0x408080
1240 at 0x941bc0 ; 1240 at 0x408088
结果分析:
首先,定位new运算符确实将数组p2放在了数组buffer中,p2和buffer的地址都是0x408040;
常规new将p1放在很远的地方0x941ba0,位于动态管理的堆中;
需要指出的第二点,第二个常规new运算符查找一个新的内存块,起始地址是0x941980;但是第二个定位new运算符分配与以前相同的内存块,起始地址还是0x408040,定位new运算符使用传递给他的地址,它不用识别哪些内存单元已经被使用,也不查找未使用的内存块。将这些事情交给程序员。比如在第三次使用定位new运算符的时候,提供了一个从数组buffer开头算起的偏移量,因此将分配新内存,起始地址从0x408068开始。
程序中我们只使用delete[]运算法释放了p1和p3的内存,因为它们在堆中,使用完就要释放;
而p2和p4没有被释放,因为p2和p4是放在buffer数组中的,而buffer数组所在的内存是静态内存,delete管不着。