C++ 指针进阶:动态分配内存
C++ 动态实例化(new 和 malloc)
malloc / free
工作原理
malloc
是 stdlib.h
库中的函数,声明为 void *__cdecl malloc(size_t _Size);
-
原理:
malloc
函数沿空闲链表(位于内存 堆空间 中)申请一块满足需求的内存块,将所需大小的内存块分配给用户剩下的返回到链表上;并返回指向该内存区的首地址的指针,意该指针的类型为
void *
,因此我们需要强制转换指针类型; -
参数:
_Size
为要申请的空间大小,即需要显式填入申请内存的大小,如n * sizeof(int)
; -
返回值:
malloc
分配内存失败时返回NULL
指针,可以通过返回值判断是否分配成功; -
malloc
并不会初始化所申请的空间;
free
也是 stdlib.h
库中的函数,声明为 void __cdecl free(void *_Memory);
-
free
函数会将用户释放的内存块连接到空闲链上; -
参数:指针
_Memory
应指向由malloc()
分配的内存块,其他方式声明的内存不能用free()
;
具体使用
动态创建一维数组
size_t element_cnt = 10;
int *arr = (int *)malloc(element_cnt * sizeof(int));
free(arr)
动态创建二维数组
size_t m = 10, n = 10;
int **arr = (int **)malloc(m * sizeof(int *));
for (int i = 0; i < m; i ++)
arr[i] = (int *)malloc(n * sizeof(int));
需要注意,这样获得的二维数组,不能保证其空间是连续的。
calloc
calloc
定义在 stdlib.h
库中,声明为 void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
calloc
与 malloc
的主要区别有:
-
原理:
calloc
函数会将申请的空间逐一初始化为0
;由于自动初始化的原因,
calloc
的运行效率要低于malloc
; -
参数:
calloc
多了一个参数NumOfElements
,无需人为计算空间大小;
realloc
realloc
定义在 stdlib.h
库中,声明为 void *__cdecl realloc(void *_Memory,size_t _NewSize);
,用于对动态内容进行扩容
-
参数:
_Memory
为 指向原来空间的指针;_NewSize
为 扩容后空间大小 -
返回值:
如果
_Memory
后有足够的连续空间,则扩大_Memory
指向的地址,并返回_Memory
;如果空间不够,则按照
_NewSize
分配空间,拷贝原有数据到新分配的内存空间,并释放_Memory
所指的内存区(自动释放,不需要free
),同时返回新分配的内存区域的首地址; -
分配失败时返回空指针
NULL
; -
若
_NewSize
小于原大小,原数据末尾可能会丢失;
new / delete
工作原理
new
和 delete
是 C++ 中的关键字,若要使用,需要编译器支持。
-
返回值:
内存分配成功时,
new
返回对象类型的指针,类型严格与对象匹配;new
内存分配失败时,会抛出bac_alloc
异常,如果不捕捉异常,那么程序就会异常退出; -
new
无需显式填入申请的内存大小,new
会根据new
的类型分配内存; -
new
分配的内存空间在自由存储区; -
new
和delete
支持重载; -
不能对一块内存释放两次或以上;
对空指针
nullptr
使用delete
操作是合法的;
具体应用
动态实例化
// 动态创建变量
int *p = new int(1234);
/* ... */
delete p;
new
动态创建对象时会经历三个步骤:
- 调用
operator new
函数分配一块足够的内存空间(通常底层默认使用malloc
实现)以存储特定类型的对象; - 编译器运行相应的构造函数以构造函数,并为其传入初值;
- 返回一个指向该对象的指针;
delete
释放对象内存时会经历两个步骤:
- 调用对象的析构函数;
- 编译器调用
operator delete
函数释放内存空间(通常底层默认使用free
实现);
// 开辟新的对象
class A {
int a;
public:
A(int a_) : a(a_) {}
};
int main() {
A *p = new A(1234);
/* ... */
delete p;
}
{}
运算符可以用来初始化没有构造函数的结构。初次以外,使用 {}
运算符可以使得变量的初始化形式变得同意。
struct ThreeInt {
int a;
int b;
int c;
};
int main() {
ThreeInt* p = new ThreeInt{1, 2, 3};
/* ... */
delete p;
}
动态创建数组
创建和释放数组需要使用 new[]
和 delete[]
,new[]
运算符会返回数组的首地址。
size_t element_cnt = 5;
int *p = new int[element_cnt];
/* ... */
delete[] p;
动态创建二维数组
动态创建二维数组有以下三种方式:
-
声明一个长度为
N * M
的一维数组,通过下标r * M + c
访问二维数组中小标为(r, c)
的元素:int *arr = new int[N * M];
这种方法可以保证二维数组的物理空间是 连续的。
-
通过变量存储 数组的数组 的首地址——指向一个一维数组的指针的地址。这个变量即 二重指针:
int **arr = new int*[M]; for (int i = 0; i < M; i ++) a[i] = new int[N];
需要注意,这样获得的二维数组,不能保证其空间是连续的。
对于这样获得的内存的释放,需要进行一个逆向操作:想释放每一个一维数组,再释放存储一维数组首地址的地址:
for (int i = 0; i < M; i ++) delete[] arr[i]; delete[] arr;
-
第三种方法用到 指向数组的指针:
int (*arr)[N] = new int[M][N]; /* ... */ delete[] arr;
这种方式得到的也是连续的内存,但与第一种方式相比,可以直接使用
arr[n]
的形式得到数组第n + 1
行的首地址,使用arr[r][c]
的形式访问到下标为(r, c)
的元素。由于指向数组的指针也是一种确定的数据类型,因此除数组的第一维外,其他维度的长度均须为一个能在编译器确定的常量。
malloc 和 new 的主要区别
特征 | new / delete | malloc / free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配失败 | 抛出异常 bac_alloc |
返回 NULL |
分配内存大小 | 编译器根据类型计算得出 | 显式指定字节数 |
处理数组 | new[] |
人为计算数组大小后进行内存分配 |
已分配内存的扩张 | 不支持 | realloc |
分配时内存不足 | 可以指定处理函数或重新指定分配器 | 无法通过用户代码处理 |
是否可以重载 | 可以 | 不可以 |
构造和析构函数 | 调用 | 不调用 |
热门相关:全天下都知道太子爱她 全天下都知道太子爱她 觅仙道 龙印战神 厨道仙途