**1、C/C++**内存分布
cpp
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);
}
选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
1.globalVar在哪里?____
2.staticGlobalVar在哪里?____
3.staticVar在哪里?____
4.localVar在哪里?____
5.num1 在哪里?____
6.char2在哪里?____
7.*char2在哪里?___
8.pChar3在哪里?____
9.*pChar3在哪里?____
10.ptr1在哪里?____
11.*ptr1在哪里?____
问题分析:
选项说明:
A. 栈:存储非静态局部变量、函数参数、返回值等
B. 堆:动态内存分配(malloc/calloc/realloc)
C. 数据段(静态区):存储全局数据和静态数据(包括全局变量和static修饰的变量)
D. 代码段(常量区):存储可执行代码和只读常量(如字符串常量)
具体分析:
1.globalVar:全局变量 → 数据段(静态区)→ C
2.staticGlobalVar:静态全局变量 → 数据段(静态区)→ C
3.staticVar:静态局部变量(虽在函数内,但由static修饰)→ 数据段(静态区)→ C
4.localVar:非静态局部变量 → 栈 → A
5.num1:非静态局部数组(在函数内)→ 栈 → A
6.char2:非静态局部字符数组(在函数内)→ 栈 → A
( 注意 :char2[] = "abcd" 在栈上分配数组并初始化,字符串"abcd"本身在常量区,但数组副本在栈上)
7.*char2:解引用char2(即char2数组的第一个元素)→ 存储在栈上(因为char2本身在栈)→ A
8.pChar3:非静态局部指针变量(在函数内)→ 栈 → A
9.*pChar3:解引用pChar3(指向字符串常量"abcd"的首元素,里面存首元素的地址)→ 字符串常量存储在代码段(常量区)→ D
10.ptr1:非静态局部指针变量(在函数内)→ 栈 → A
11.*ptr1:解引用ptr1(指向malloc动态分配的内存)→ 堆 → B
图片示例:
**2、C****语言中动态内存管理方式:**malloc/calloc/realloc/free
cpp
void Test()
{
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
// 这里需要free(p2)吗?
free(p3);
}
这里需要free(p2)吗?
答:不需要,而且绝对不能这样做。
原因分析:
realloc 函数的功能是重新分配之前由 malloc, calloc, 或 realloc 所分配的内存块(p2所指向的内存块)的大小。
当调用** int* p3 = (int*)realloc(p2, sizeof(int)*10); **时,会发生以下两种情况之一:
情况一(原地扩容): 如果 p2 指向的内存块后面有足够的空闲空间,realloc 会尝试在原地扩大这块内存。函数返回的地址值 p3 等于传入的地址值 p2。 此时,p2 和 p3 指向同一个内存块。
情况二(异地迁移): 如果原地没有足够空间,realloc 会在堆的另一块区域分配一个足够大的新内存块(sizeof(int)*10),将旧内存块(p2指向的)中的数据原样拷贝过来,然后自动释放旧内存块。 函数返回的地址值p3 是一个新地址,与 p2 不同。
关键在于,无论哪种情况,realloc 调用后,之前由 p2 指向的旧内存块都已经被系统接管,程序员不再拥有它的所有权,也不应该再尝试去释放它。
在情况一中,这块内存被扩大了,需要通过 p3 来管理。
在情况二中,这块内存已经被 realloc 函数内部自动 free 掉了。
因此,代码中只需要 free(p3) 即可,它释放的就是当前有效的那一块内存。如果在 realloc 后再 free(p2),会导致双重释放的错误,这是一种未定义行为,通常会导致程序崩溃。
牢记:永远只 free 由 malloc, calloc, realloc 返回的最新指针。在一次成功的 realloc 调用后,旧的指针就应该被立即视为无效,不应再使用或释放。
面试题总结
1. malloc/calloc/realloc的区别?
|----------------|-----------------------------------|-----------------------------------------|----------------------------------------------|
| 特性 | malloc | calloc | realloc |
| 功能 | 分配指定大小的内存块 | 分配并初始化指定数量、大小的内存块 | 调整已分配内存块的大小 |
| 函数原型 | void* malloc(size_t size); | void* calloc(size_t num, size_t size); | void* realloc(void* ptr, size_t new_size); |
| 初始化 | 不初始化 内存内容,内容是随机值 | 初始化 内存内容为全零 | 根据情况,新增加的内存区域不会被初始化 |
| 参数 | size:要分配的字节数 | num:元素个数 size:每个元素的字节数 | ptr:要调整的旧内存指针 new_size:新的总字节数 |
| 使用场景 | 需要分配内存且不关心初始值,或准备自行初始化时 | 需要分配数组或结构体并希望初始值全为零时 | 需要扩大或缩小之前动态分配的内存时 |
2. malloc的实现原理?
3、C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1 new/delete操作内置类型
cpp
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int; // 分配空间,值未初始化(是随机值)
delete ptr4; // 释放空间
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10); // 分配空间并初始化为10
delete ptr5; // 释放空间
// 动态申请3个int类型的空间
int* ptr6 = new int[3]; // 这里3表示对象个数,值都未初始化
delete[] ptr6; // 释放连续的数组空间
// 动态申请3个int类型的空间并初始化
int* ptr7 = new int[3] {1,2,3}; // 值初始化为1,2,3
delete[] ptr7; // 释放连续的数组空间
}
1.初始化: new 可以通过 new int(10) 的方式对单个元素进行初始化,而 malloc 无法方便地做到这一点。
2.数组操作: 使用 new[] 来分配数组,使用 delete[] 来释放数组。必须匹配使用,否则行为未定义(尤其是对自定义类型,会导致析构函数调用次数错误)。
3.对于内置类型(如 int),new/delete 和 malloc/free 在功能上几乎没有区别,都是分配和释放内存。但 new 的语法更简单,且支持初始化。
3.2 new和delete****操作自定义类型
cpp
class A
{
public:
A(int a = 0) : _a(a)
{
cout << "A():" << this << endl; // 构造函数
}
~A()
{
cout << "~A():" << this << endl; // 析构函数
}
private:
int _a;
};
int main()
{
// 对比一:自定义类型
A* p1 = (A*)malloc(sizeof(A)); // 只分配内存,不调用构造函数
A* p2 = new A(1); // 1. 分配内存 2. 调用构造函数 A(1)
free(p1); // 只释放内存,不调用析构函数
delete p2; // 1. 调用析构函数 2. 释放内存
// 对比二:内置类型(行为几乎相同)
int* p3 = (int*)malloc(sizeof(int));
int* p4 = new int;
free(p3);
delete p4;
// 对比三:对象数组
A* p5 = (A*)malloc(sizeof(A)*10); // 分配10个A对象的内存,全部未构造
A* p6 = new A[10]; // 分配内存,并调用10次默认构造函数
free(p5); // 直接释放内存,10个对象都未析构
delete[] p6; // 调用10次析构函数,然后释放内存
return 0;
}
注意: 在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数 ,而malloc与
free不会。
4. operator new与operator delete函数(重点)
new/delete 操作符与operator new/operator delete全局函数之间的关系和区别。
1.new 和 delete 是 C++ 语言中的操作符(关键字) ,供程序员直接使用来进行动态内存分配和释放。
2.operator new 和 operator delete 是系统提供的全局函数,是 new 和 delete 操作符在底层实现时所调用的工具。
3.调用关系:
new 操作符的底层行为可以分解为两步:a. 调用 operator new 函数来分配内存。
b. 在分配好的内存上调用对象的构造函数。
delete 操作符的底层行为也可以分解为两步:a. 调用对象的析构函数。
b. 调用 operator delete 函数来释放内存。
4.功能定位:operator new/operator delete 的核心职责只是管理内存的分配和释放(类似于更高级的 malloc 和 free),它们不负责调用构造函数和析构函数。调用构造和析构是 new/delete 操作符自己的逻辑。
**总结:**new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new 函数库代码
cpp
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0) // 底层调用malloc尝试分配size字节的内存
{
if (_callnewh(size) == 0) // 如果malloc失败,检查用户是否设置了"new-handler"函数
{
// report no memory
// 如果用户没有设置new-handler,或者new-handler也无法解决问题,则抛出异常
static const std::bad_alloc nomem;
_RAISE(nomem); // 抛出std::bad_alloc类型异常
}
// 如果用户设置了new-handler(_callnewh返回非0),循环会继续,再次尝试malloc
}
return (p); // 分配成功,返回指向内存的指针
}
operator new底层调用 malloc
operator new 的本质是 malloc 的一个封装。它的主要工作就是调用 malloc(size) 来分配指定大小的内存。
失败处理机制(与malloc的关键区别):
1.malloc 失败时直接返回 NULL 空指针。
2.operator new 失败时,会进入一个循环。 它首先检查用户是否通过 std::set_new_handler 设置了一个new-handler 函数。如果没有设置new-handler或它也无法解决问题,抛出 std::bad_alloc 异常。
operator delete 函数库代码
cpp
void operator delete(void *pUserData)
{
... // 一些调试和线程安全代码
if (pUserData == NULL) // 遵循C++标准:delete空指针是安全的,直接返回
return;
...
_free_dbg( pUserData, _NORMAL_BLOCK ); // 底层调用free的调试版本
...
return;
}
// free的本质
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator delete底层调用 free
1.operator delete 的本质是 free 的一个封装。它的主要工作就是调用 free(pUserData) 来释放内存。
2.operator delete 检查传入的指针是否为空 (NULL),如果是则直接返回。这保证了 delete nullptr; 是安全的操作。
上述两个全局函数的总结:
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果
malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施
就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
5、new和delete****的实现原理
5.1 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间 ,new[]和delete[]申请的是连续空间 ,而且new在申请空间失败时会抛异常,malloc会返回NULL。
5.2 自定义类型
new的原理:
- 调用operator new函数申请空间。
- 在申请的空间上执行构造函数 ,完成对象的构造。
delete的原理:
- 在空间上执行析构函数 ,完成对象中资源的清理工作。
- 调用operator delete函数释放对象的空间。
new T[N]的原理:
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
- 在申请的空间上执行N次构造函数 。
delete[]的原理:
- 在释放的对象空间上执行N次析构函数 ,完成N个对象中资源的清理。
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
6、定位new表达式(placement-new)****(了解)
概念:
定位 new 是一种特殊形式的 new,它不在堆上分配新的内存,而是在已经分配好的原始内存空间上调用构造函数来初始化(创建)一个对象。
使用格式:
1.new (place_address) type
2.new (place_address) type(initializer-list) (带初始化列表,用于有参构造函数)
3.place_address 必须是一个指针,指向预先分配好的内存块。
使用场景:
最主要的用途是配合内存池。内存池为了提高效率,会预先分配大块内存,然后从中切分给小对象使用。这些内存块是"原始"的,没有经过构造函数的初始化。定位 new 就被用来在这些原始内存上构造对象。
注意: 由于对象是手动构造的,其析构函数也必须手动调用。内存的释放也同样需要根据其分配方式(如 malloc 或 operator new)来手动释放。
cpp
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,
// 因为构造函数没有执行
//使用 malloc 分配原始内存
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 使用定位new在p1指向的内存上构造A对象。
// 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A(); // 手动调用析构函数,销毁对象。对象所占的内存还在,但对象的生命周期结束
free(p1); // 释放p1指向的内存块
//使用 operator new 分配原始内存
A* p2 = (A*)operator new(sizeof(A)); // 使用operator new分配内存
new(p2)A(10); // 使用定位new在p2指向的内存上构造A对象
p2->~A(); // 手动调用析构函数
operator delete(p2); // 使用operator delete释放内存
return 0;
}
**注意:**使用 malloc/free 或 operator new/operator delete。必须确保分配和释放的方式匹配(malloc 配 free, operator new 配 operator delete)。在对象周期结束时,需要手动调用析构函数,销毁对象。此时,对象所占的内存还在,但对象的生命周期结束。使用匹配释放的方式才会释放之前对象所占的内存空间。
7、malloc/free和new/delete****的区别(重点)
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方 是(用法,核心特性,原理):
malloc 和free 是函数,new 和delete 是操作符。
malloc 申请的空间不会初始化,new可以初始化。
malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
malloc 的返回值为void*, 在使用时必须强转,new 不需要,因为new 后跟的是空间的类型。
malloc 申请空间失败时,返回的是NULL,因此使用时必须判空,new 不需要,但是new需要捕获异常。
申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成。(最主要不同)