1. C/C++内存分布
我们先分析以下代码的内存分布情况:
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. 代码段(常量区)
问题与答案:
- globalVar 在哪里?C
- staticGlobalVar 在哪里?C
- staticVar 在哪里?C
- localVar 在哪里?A
- num1 在哪里?A
- char2 在哪里?A
- *char2 在哪里?A
- pChar3 在哪里?A
- *pChar3 在哪里?D
- ptr1 在哪里?A
- *ptr1 在哪里?B
填空题:
- sizeof(num1) = 40
- sizeof(char2) = 5
- strlen(char2) = 4
- sizeof(pChar3) = 4 or 8
- strlen(pChar3) = 4
- sizeof(ptr1) = 4 or 8
sizeof 和 strlen 的区别?
解答:strlen 是标准库函数,用于计算字符串中字符的数量,直到遇到 '\0' 结束符为止。而 sizeof 是一个编译时运算符,并非函数,其作用是获取数据类型或变量在内存中所占的字节数。
【内存区域说明】
- 栈(Stack):又称堆栈,主要用于存放非静态局部变量、函数参数、返回值等,内存由高地址向低地址方向增长。
- 内存映射段:一种高效的 I/O 映射机制,通常用于加载共享动态库。用户也可以通过系统调用创建共享内存实现进程间通信。
- 堆(Heap):程序运行期间用于动态分配内存的空间,通常从低地址向高地址扩展。
- 数据段(静态区):用于存储全局变量和静态变量。
- 代码段(常量区):保存可执行指令以及只读常量(如字符串字面量)。
2. C语言中的动态内存管理方式:malloc/calloc/realloc/free
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?---不需要,可以p2=NULL;
free(p3 );
}
在C语言中,malloc、calloc 和 realloc 都是用于动态内存分配的标准库函数,它们之间存在一些关键差异:
- malloc:分配指定大小的未初始化连续内存块。
- calloc:分配指定数量和单位大小的内存块,并将所有位初始化为0。
- realloc:调整已分配内存块的大小,可以扩大或缩小原有空间。
realloc 扩容的两种常见情况:
- 情况1:若原内存块后方有足够空间,则直接在原地址后追加内存,原数据保持不变。
- 情况2:若无法就地扩展,则系统会在堆中寻找一块新的合适空间,复制原数据并释放旧内存,返回新地址。
3. C++ 内存管理方式
虽然C语言的内存管理方法在C++中仍然可用,但在处理复杂类型时显得不够灵活且容易出错。为此,C++引入了更高级的内存控制机制——使用 new 和 delete 操作符进行动态内存管理。
3.1 new/delete 操作内置类型
void Test()
{
//动态申请一个int类型的空间
int* ptr4 = new int;
//动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
//动态申请10个int类型的空间
int* ptr6 = new int[10]{1,2,3};//申请多个空间,可以在后面初始化。
delete ptr4;
delete ptr5;
delete [] ptr6;
}
注意:对于单个元素的申请与释放,应使用 new 和 delete;而对于连续内存块(数组),则需使用 new[] 和 delete[]。务必成对使用,避免资源泄漏或行为未定义。
3.2 new 和 delete 操作自定义类型
class A
{
public:
A(int a=0) : _a(a)
{
std::cout << "A():" <<this<< std::endl;
}
~A()
{
std::cout << "~A():" <<this<< std::endl;
}
private:
int _a;
};
int main()
{
//new/delete 和malloc/free最大区别就是 new/delete对于【自定义类型】除了开空间
//还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
//内置类型是几乎一样的
int* p3 = (int*)malloc(sizeof(int));
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
重要区别:当使用 new 创建自定义类型的对象时,不仅会分配内存,还会自动调用构造函数;而 delete 则会在释放内存前调用析构函数。相比之下,malloc 和 free 仅进行原始内存分配与释放,不会触发任何构造或析构过程。
4. operator new 与 operator delete 函数
4.1 operator new 与 operator delete 简介
new 和 delete 是 C++ 提供的操作符,用于用户层面的动态内存申请与释放。
而 operator new 和 operator delete 是系统提供的全局函数,new 操作符在底层实际是通过调用 operator new 来完成内存分配的,同理,delete 也是通过调用 operator delete 来释放内存。
/*
* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回,
* 申请失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
/*
* operator delete:该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
* free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
从实现角度看,operator new 内部通常依赖于 malloc 进行实际的内存请求。如果分配成功则直接返回指针;若失败,则尝试执行用户设置的错误处理例程,若无响应则抛出异常(如 std::bad_alloc)。
5. new 和 delete 的实现原理
5.1 内置类型
对于基本数据类型(如 int、double 等),new 主要负责调用 operator new 分配足够的内存空间,然后返回对应类型的指针;delete 则调用 operator delete 释放该内存。
5.2 自定义类型
1. new 的原理
执行步骤如下:
- 调用
operator new分配所需大小的内存空间; - 在分配好的内存上构造对象,即调用类的构造函数。
2. delete 的原理
执行流程为:
- 先调用对象的析构函数,清理资源;
- 再调用
operator delete释放内存。
3. new T[N] 的原理
用于创建对象数组:
- 调用
operator new[](可能内部仍调用operator new)分配足够容纳 N 个 T 类型对象的空间; - 对每个元素依次调用默认构造函数进行初始化。
4. delete[] 的原理
释放对象数组的过程包括:
- 对数组中每一个对象调用析构函数;
- 调用
operator delete[]释放整块内存空间。
6. 定位 new 表达式(placement-new)
定位 new 允许程序员在已分配的内存空间上显式构造对象,语法格式为:new (pointer) Type(args)。
它不分配新内存,仅调用构造函数。常用于内存池、嵌入式系统或需要精细控制对象生命周期的场景。
7. 常见面试题
7.1 malloc/free 与 new/delete 的区别
- 本质不同:malloc/free 是C语言的标准库函数;new/delete 是C++的操作符。
- 初始化支持:malloc 只分配内存,不初始化;new 在分配后还会调用构造函数。
- 类型安全:new 返回指定类型的指针,无需强制转换;malloc 返回 void*,需手动转换。
- 内存失败处理:malloc 失败返回 NULL;new 失败默认抛出异常。
- 操作对象:new/delete 能正确处理自定义类型的构造与析构;malloc/free 仅处理内存,不具备对象语义。
7.2 内存泄漏
内存泄漏指的是程序动态分配了内存但未能正确释放,导致这部分内存无法再次被使用,长时间运行可能耗尽可用堆空间。
常见原因包括:
- 忘记调用 free 或 delete;
- 异常发生时提前退出而未清理资源;
- 指针丢失(指向动态内存的指针被覆盖或超出作用域)。
预防措施:使用智能指针(如 unique_ptr、shared_ptr)、RAII 技术、确保配对使用分配与释放操作。
operator delete 最终是通过调用 free 函数来完成内存空间的释放操作。
5. new 和 delete 的实现机制
5.1 对于内置类型
当申请的是内置类型(如 int、char 等)的内存时,new 与 malloc、delete 与 free 的行为基本相似。主要区别在于:
- new 和 delete 用于申请和释放单个元素的内存空间;而 new[] 和 delete[] 则用于申请和释放连续的内存块。
- 在申请失败的情况下,new 会抛出异常,而 malloc 则返回 NULL 指针。
5.2 针对自定义类型
1. new 的执行流程
- 首先调用 operator new 函数来分配所需的内存空间。
- 在成功分配的空间上,调用对象的构造函数以完成初始化过程。
2. delete 的执行流程
- 先调用对象的析构函数,清理对象内部所持有的资源。
- 然后调用 operator delete 函数,将内存空间归还给系统。
3. new T[N] 的工作原理
- 调用 operator new[] 函数,在其内部实际仍通过 operator new 来完成 N 个对象所需空间的分配。
- 随后在该连续内存区域上依次调用 N 次构造函数,完成每个对象的构造。
4. delete[] 的工作原理
- 在待释放的内存区域上,对每一个对象调用析构函数,共执行 N 次,确保所有资源被正确清理。
- 接着调用 operator delete[] 函数,该函数内部最终会调用 operator delete 来完成实际的内存释放。
6. 定位 new 表达式(placement-new)
定位 new 表达式的作用是在已经分配好的原始内存中,显式地调用构造函数来初始化一个对象。
使用语法格式:
new (place_address) type
或
new (place_address) type(initializer-list)
其中,place_address 必须是一个有效的指针,指向已分配但未初始化的内存区域;initializer-list 是可选的初始化参数列表。
典型应用场景:
该特性通常与内存池技术结合使用。由于内存池分配出的内存块并未自动初始化,因此对于自定义类型的对象,必须使用 placement-new 显式调用构造函数,以保证对象状态的正确性。
class A
{
public:
A(int a=0):_a(a)
{
std::cout << "A()" << this << std::endl;
}
~A()
{
std::cout << "~A()" << this << std::endl;
}
private:
int _a;
};
//定位new/replacement new
int main()
{
//p1现在指向的只不过是与A对象相同大小的一段空间,
//还不能算是一个对象,因为构造函数没有执行。
A* p1 = (A*)malloc(sizeof(A));
new (p1)A;//注意:如果A类的构造函数有参数时,此处还需要传参。
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new (p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
7. 常见面试问题解析
7.1 malloc/free 与 new/delete 的区别
两者共同点:均从堆区动态申请内存,且都需要开发者手动释放,否则会造成内存泄漏。
不同之处如下:
- malloc 和 free 是标准库函数,而 new 和 delete 是 C++ 中的操作符。
- malloc 分配的空间不会进行初始化,而 new 可以自动调用构造函数完成初始化。
- 使用 malloc 时需手动计算字节数并传入,而 new 后只需指定类型,若为多个对象,可通过 [N] 指定数量。
- malloc 返回 void* 类型,使用时必须强制转换为目标类型的指针;new 则直接返回对应类型的指针,无需转换。
- malloc 在分配失败时返回 NULL,因此每次使用都应检查是否为空;new 则通过抛出异常处理失败情况,无需判空但需考虑异常捕获。
- 对于自定义类型对象,malloc/free 仅负责内存的分配与释放,不会调用构造函数或析构函数;而 new 在分配后会自动调用构造函数,delete 在释放前会先调用析构函数清理资源。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
7.2 内存泄漏相关问题
7.2.1 什么是内存泄漏及其危害
定义:
内存泄漏是指程序因设计缺陷或逻辑错误,未能释放不再使用的动态分配内存,导致这部分内存无法被后续使用。
需要注意的是,内存并未物理消失,而是程序失去了对该内存区域的引用或控制权,从而造成资源浪费。
危害:
长时间运行的应用程序一旦发生内存泄漏,后果严重。例如操作系统、后台服务等关键系统组件,随着泄漏积累,可用内存逐渐减少,系统响应速度下降,最终可能导致程序崩溃或系统卡死。
7.2.2 内存泄漏的分类(了解)
在 C/C++ 开发中,主要关注以下两类内存泄漏:
- 堆内存泄漏(Heap Leak):指通过 malloc、calloc、realloc 或 new 等方式从堆上分配的内存,在使用完毕后未通过对应的 free 或 delete 进行释放。这种遗漏会导致该段内存永久不可用,形成堆泄漏。
- 系统资源泄漏:程序使用了操作系统分配的资源(如文件描述符、套接字、管道、互斥锁等),但未通过相应接口(如 close、fclose、ReleaseSemaphore 等)释放。这类泄漏不仅消耗系统资源,还可能引发性能下降甚至系统不稳定。
7.2.3 如何检测内存泄漏(了解)
在 Visual Studio 环境下,可以利用 Windows 提供的 _CrtDumpMemoryLeaks() 函数进行初步检测。该函数能够报告当前是否存在内存泄漏以及大致泄漏的字节数,但缺乏精确的位置信息。
int main()
{
int* p = new int[10];
// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
////////////////////////////////////////////////////////
// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
尽管编码时应格外注意内存管理,但在复杂项目中仍难以完全避免疏漏。简单场景可通过上述方法快速排查,而对于大型工程或存在多处泄漏的情况,通常需要借助专业的第三方内存检测工具(如 Valgrind、Visual Leak Detector 等)进行深入分析与定位。
7.2.4 如何预防内存泄漏
为有效避免内存泄漏,建议采取以下措施:
- 养成良好的编程习惯,确保每次动态分配都有对应的释放操作。
- 优先使用智能指针(如 unique_ptr、shared_ptr)和 RAII 技术,由对象生命周期自动管理资源。
- 使用容器类(如 vector、string)替代原始数组,减少手动内存操作。
- 在关键路径添加日志或断言,辅助调试内存使用情况。
- 定期进行静态代码分析和动态内存检测,及时发现潜在问题。
内存泄漏是开发过程中常见的问题,解决方法主要可分为两类:事前预防和事后排查。
在项目初期,建立良好的设计与编码规范至关重要。例如,申请的内存应确保在使用后及时释放,形成资源管理的良好习惯。
然而,即便开发者严格遵循手动释放原则,在异常发生或控制流复杂的情况下,仍可能出现遗漏。因此,仅依赖人工管理并不足以完全避免问题,需结合更可靠的机制。
采用RAII(Resource Acquisition Is Initialization)思想是一种有效的预防手段。通过构造函数获取资源、析构函数自动释放资源,能够保证资源的正确回收。在此基础上,使用智能指针(如shared_ptr、unique_ptr)进行内存管理,可大幅提升程序的安全性与稳定性。
部分企业还会制定内部开发规范,要求使用自研的私有内存管理库。这类库通常集成了内存分配跟踪功能,并提供可启用的泄漏检测选项,有助于在测试阶段发现潜在问题。
当系统已出现内存异常时,可借助专门的内存泄漏检测工具进行分析。但需要注意的是,许多现有工具存在准确率不高、误报频繁的问题,且部分高性能工具价格昂贵,限制了其广泛应用。
综上所述,应对内存泄漏的核心策略包括两种类型:一是以智能指针为代表的“事前预防型”方案;二是以检测工具为主的“事后查错型”手段。结合两者可在不同阶段有效控制风险。


雷达卡


京公网安备 11010802022788号







