【C++ 面试 - 内存管理】每日 3 题(八)
✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/fYaBd
📚专栏简介:在这个专栏中,我将会分享 C++ 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
22. delete p 和 delete[] p 的区别
在 C++ 中,delete
和 delete[]
都是用于释放通过 new
和 new[]
分配的内存,但它们的使用场景是不同的。
delete p
用于释放单个对象的内存。这意味着在使用 new
分配内存后,如果你只用一个指针指向这个对象,就应该使用 delete p
来释放内存。例如:
int* p = new int;
// 使用 p 指针操作对象
delete p; // 释放内存
delete[] p
用于释放数组对象的内存。这意味着在使用 new[]
分配内存后,如果你用一个指针指向一个数组的起始地址,就应该使用 delete[] p
来释放内存。例如:
int* p = new int[10];
// 使用 p 指针操作数组对象
delete[] p; // 释放内存
使用 delete
释放数组对象的内存或者使用 delete[]
释放单个对象的内存,会导致未定义的行为(Undefined Behavior),可能会导致内存泄漏或程序崩溃。
因此,当我们使用 new
和 new[]
分别分配单个对象和数组对象时,需要确保使用与其对应的 delete
或 delete[]
来正确释放内存,以避免内存泄漏和其他潜在问题。
23. new 和 delete 的实现原理,delete 是如何知道释放内存的大 小的?
1、 new 简单类型直接调用 operator new 分配内存;而对于复杂结构,先调用 operator new 分配内存,然后在分配的内存上调用构造函数。
对于简单类型,new[] 计算好大小后调用 operator new;
对于复杂数据结构,new[] 先调用 operator new[] 分配内存,然后在 p 的前四个字节写入数组大小 n,然后调用 n 次构造函数,针对复杂类型,new[] 会额外存储数组大小;
① new 表达式调用一个名为 operator new (operator new[]) 函数,分配一块足够大的、原始的、未命名的内存空间;
② 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
③ 对象被分配了空间并构造完成,返回一个指向该对象的指针。
2、 delete 简单数据类型默认只是调用 free 函数;复杂数据类型先调用析构函数再调用 operator delete。
针对简单类型,delete 和 delete[] 等同。假设指针 p 指向 new[] 分配的内存。因为要 4 字节存储数组大小,实际分配的内存地址为 [p-4],系统记录的也是这个地址。delete[] 实际释放的就是 p-4 指向的内存。而 delete 会直接释放 p 指向的内存,这个内存根本没有被系统记录,所以会崩溃。
3、 需要在 new[] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
24. 什么是内存泄露,如何检测与避免?
(1)内存泄露
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的 (内存块的大小可以在程序运行期决定) 内存块,使用完后必须显式释放的内存。应用程序般使用 malloc、realloc、new 等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用 free 或 delete 释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
(2)避免内存泄露的几种方式
-
使用智能指针:智能指针是一种能够自动管理内存的对象,可以在不再需要对象时自动释放内存。C++ 标准库提供了智能指针类型,如 std::unique_ptr 和 std::shared_ptr,可以使用它们来管理动态分配的内存。
-
析构函数为虚函数:一定要将基类的析构函数声明为虚函数。
-
申请和释放操作成对出现:有 new 就有 delete,有 malloc 就有 free,保证它们一定成对出现。对象数组的释放一定要用 delete[]。
-
重载 new 和 delete:手动管理内存时,应该重载 new 和 delete 运算符,以便在分配和释放内存时使用适当的函数。这可以避免使用不当的指针操作,从而减少内存泄漏的风险。
-
使用 RAII(资源获取即初始化)的思想:RAII 是一种编程技术,它通过将资源的生命周期与对象的生命周期关联起来,确保资源在使用后被正确释放。在 C++ 中,可以使用类来封装动态分配的资源,并在类对象的生命周期结束时自动释放资源。
-
预先设定好内存池:预先设定好内存池可以减少频繁的内存分配和释放操作,从而减少内存碎片和提高性能。在程序开始时,可以预先分配一定数量的内存块,并在需要时从内存池中分配内存。这样可以避免频繁地调用 new 和 delete 运算符,从而减少错误的可能性。
(3)检测方法
1. 计数法
使用 new 或者 malloc 时,让该数 +1,delete 或 free 时,该数 -1,程序执行完打印这个计数,如果不为 0 则表示存在内存泄露。
2. gcc 启用 asan 标志检查
GCC 中的 “启用 ASAN 标志检查” 是一种内存泄漏检测工具,它是 GCC 编译器的一个特性,称为 AddressSanitizer(ASan)。ASan 是一个动态分析工具,它可以检测程序中可能的内存泄漏和其他内存错误。
当你在编译程序时,添加 ASan 标志可以启用内存泄漏检测。当程序运行时,ASan 会跟踪分配和释放的内存,并检查是否存在未被释放的内存块。如果发现未被释放的内存块,ASan 会报告内存泄漏。
使用 ASan 进行内存泄漏检测的一般步骤如下:
-
安装 GCC 编译器并启用 ASan 标志。
-
编写代码,使用智能指针或 RAII 技术管理内存。
-
编译程序时添加 ASan 标志。
-
运行程序并检查任何内存泄漏报告。
3. 标记清除法(Mark-Sweep Garbage Collection)
标记清除法是一种内存泄漏检测方法,用于检测程序中可能存在的内存泄漏问题。标记清除法主要包括两个步骤:标记和清除。
在程序运行过程中,标记清除法会跟踪分配和释放的内存块,并使用标记来标识可能存在问题的内存区域。这些标记通常是由程序中的特定对象或数据结构来管理的。当程序运行到某个特定点时,标记清除法会遍历所有已分配的内存块,并检查每个内存块的状态。如果某个内存块已经被释放,但仍然被引用或使用,那么这个内存块就被认为是内存泄漏。
清除步骤则是将所有标记清除,以防止后续的引用或使用。在清除过程中,标记清除法会再次遍历所有已分配的内存块,并确保每个内存块的状态都被正确地清除。
标记清除法的优点在于它可以检测到内存泄漏问题,并且可以在程序运行时实时检测和报告问题。然而,它也存在一些缺点,例如可能会影响程序的性能和稳定性,尤其是在大规模程序中。此外,标记清除法需要正确地管理内存和跟踪分配和释放的操作,因此需要编写复杂的代码来正确地实现它。
4. Linux 下可以使用 Valgrind 工具
首先看一段 C 程序示例,比如:
#include <stdlib.h>
int main()
{
int *array = malloc(sizeof(int));
return 0;
}
编译程序:gcc -g -o main main.c
,比哪一需要加上 -g
选项打开调试,使用 IDE 的可以用 Debug 模式编译。
使用 Valgrind 检测内存使用情况:
valgrind --tool=memcheck --leak-check=full ./main
结果:
==31416== Memcheck, a memory error detector
==31416== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31416== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31416== Command: ./main_c
==31416==
==31416==
==31416== HEAP SUMMARY:
==31416== in use at exit: 4 bytes in 1 blocks
==31416== total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==31416==
==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416== at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416== by 0x400537: main (main.c:5)
==31416==
==31416== LEAK SUMMARY:
==31416== definitely lost: 4 bytes in 1 blocks
==31416== indirectly lost: 0 bytes in 0 blocks
==31416== possibly lost: 0 bytes in 0 blocks
==31416== still reachable: 0 bytes in 0 blocks
==31416== suppressed: 0 bytes in 0 blocks
==31416==
==31416== For counts of detected and suppressed errors, rerun with: -v
==31416== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
先看看输出信息中的 HEAP SUMMARY
,它表示程序在堆上分配内存的情况,其中的 1 allocs
表示程序分配了 1 次内存,0 frees
表示程序释放了 0 次内存,4 bytes allocated
表示分配了 4 个字节的内存。
另外,Valgrind 也会报告程序是在哪个位置发生内存泄漏。例如,从下面的信息可以看到,程序发生了一次内存泄漏,位置是 main.c
文件的第 5 行:
==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416== at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416== by 0x400537: main (main.c:5)
5. Win dows 下可以使用 CRT库