当前位置: 首页 > ai >正文

面试问题:c++的内存管理方式,delete的使用,vector的resize和reverse,容量拓展

目录

c++的内存管理方式

内存分区框架

内存管理机制

delete的使用

vector中的一些知识

普通数组和 vector 的区别

vector中的容量拓展机制

1. 申请更大的内存空间

2. 复制/移动旧数据

3. 释放旧内存

4. 更新内部指针和容量

为什么不每次只增加一点点容量?

面试简洁版回答

vector中的resize和reserve

vector中的emplace_back VS push_back

迭代器失效问题

类的成员函数调用顺序的问题


c++的内存管理方式

内存分区框架

C++ 程序运行时,内存一般分为几个区域:

栈(stack)

  • 局部变量、函数参数
  • 生命周期:自动分配、自动释放(函数结束时销毁)
  • 管理方式:由编译器自动完成

堆(heap)

  • 需要程序员手动申请与释放 (new/delete, malloc/free)
  • 生命周期:由开发者控制,易出错(内存泄漏、悬空指针)

全局/静态区(data segment)

  • 存放全局变量、静态变量
  • 生命周期:整个程序运行期间

常量区

  • 存放字符串常量、const 全局常量等
  • 生命周期:整个程序运行期间

代码区(text segment)

存放程序的可执行指令

内存管理机制

C++的内存管理主要分为两种方式:自动内存管理动态内存管理

  • 自动内存管理:由编译器在程序编译时或运行时自动处理,无需程序员干预。

  • 动态内存管理:需要程序员手动分配和释放内存。

栈内存(Stack)

这是自动内存管理的主要形式。

  • 分配方式:当函数被调用时,其局部变量(包括基本类型、对象和指针)的内存在栈上自动分配。

  • 特点:分配和释放速度非常快,因为这是一个“LIFO”(后进先出)结构,类似于栈。

  • 生命周期与变量的作用域绑定,一旦变量超出其作用域(如函数返回),内存会自动被回收。

  • 缺点:大小有限,如果分配过多可能导致栈溢出(Stack Overflow)。

堆内存(Heap)

这是动态内存管理的主要区域。

  • 分配方式:使用 new 运算符手动分配,new 会返回一个指向新分配内存的指针。

  • 特点:内存空间更大,可以动态调整大小,不受作用域限制。

  • 生命周期:从 new 成功分配后开始,直到程序员使用 delete 运算符手动释放为止。

  • 缺点

    • 需要手动管理:如果忘记使用 delete 释放,会导致内存泄漏(Memory Leak)

    • 悬垂指针(Dangling Pointer):内存释放后,指针仍然指向该地址,如果继续使用,可能导致程序崩溃或不可预测的行为。

    • 重复释放:对同一块内存重复使用 delete,可能导致未定义行为。

静态/全局内存

除了栈和堆,静态和全局变量的内存管理如下:

  • 分配方式:在程序启动时分配,直到程序结束时才释放。

  • 生命周期:贯穿整个程序的执行周期。

现代C++的内存管理(智能指针)

智能指针(如 std::unique_ptr 和 std::shared_ptr)是C++11引入的,它们是封装了裸指针的类,可以自动管理动态分配的内存

它们利用了RAII 机制。当智能指针对象超出作用域时,其析构函数会自动调用 delete 释放内存,从而有效避免了内存泄漏

好处:

  • 显著简化了内存管理。
  • 降低了内存泄漏和悬垂指针等问题的风险。

delete的使用

✅ 安全用法(推荐习惯)

1. 用 delete / delete[] 与 new / new[] 配对

int* p = new int;
delete p;          // 正确int* arr = new int[10];
delete[] arr;      // 正确

2. 对 nullptr 调用 delete 安全无害!!!

int* p = nullptr;
delete p;      // 什么也不会发生
delete[] p;    // 也安全

3. delete 后置空,避免悬空指针(这个习惯一定要养成)

delete p;
p = nullptr;   // 避免重复 delete 或误用

❌ 常见误区

1. 忘记区分 delete 和 delete[]

int* arr = new int[10];
delete arr;      // ❌ 未定义行为
delete[] arr;    // ✅ 正确

2. 对非 new 内存使用 delete

int x;
int* p = &x;
delete p;   // ❌ 未定义行为

3. 混用 malloc/free 和 new/delete

int* p = (int*)malloc(sizeof(int));
delete p;     // ❌ UB,应该 free(p)int* q = new int;
free(q);      // ❌ UB,应该 delete q

4. 重复释放(double delete)

int* p = new int;
delete p;
delete p;    // ❌ 未定义行为(悬空指针)

vector中的一些知识

普通数组和 vector 的区别

内存管理

  • 普通数组: 内存是在栈上或堆上静态或动态分配的。如果是动态分配(new),需要手动管理内存。例如,int arr[10]; 的内存在栈上自动分配和释放;int* arr = new int[10]; 则需要在堆上手动分配和用 delete[] 手动释放。
     
  • std::vector: 自动管理内存。它会在堆上动态分配内存,但其内部机制会自动处理内存的增长、收缩和释放。你无需关心 delete 操作,这有效避免了内存泄漏的风险。

大小和容量

  • 普通数组: 大小是固定的,在声明时就确定了,且不能改变。
     
  • std::vector: 大小是可变的。你可以随时向其中添加或删除元素。当现有容量不足时,vector 会自动分配一块更大的内存,并将旧数据迁移过去。你可以通过 size() 获取元素数量,通过 capacity() 获取当前分配的内存容量。

功能和操作

  • 普通数组: 只是一个内存块,提供的操作非常有限。你只能通过索引访问元素。

     
  • std::vector: 是一个强大的类模板,提供丰富的成员函数来操作数据。例如:
    • 增删改查: push_back()、pop_back()、insert()、erase() 等。
    • 迭代器: 支持迭代器,可以方便地与标准库算法(如 std::sort、std::find)配合使用。

vector中的容量拓展机制

当向 vector 中添加元素(比如通过 push_back())导致其当前元素数量(size)超过了当前分配的内存容量(capacity)时,vector 就会触发容量拓展。

这个过程主要包括以下几个步骤:

1. 申请更大的内存空间

vector 首先会向堆内存申请一块新的、更大的连续内存空间。通常,新容量是旧容量的 1.5 倍或 2 倍(具体倍数取决于不同的编译器实现,但通常是翻倍策略)。例如,如果旧容量是 10,新容量可能会是 20。

2. 复制/移动旧数据

接下来,vector 会将旧内存空间中的所有元素,逐一复制(copy)或移动(move)到新分配的内存空间中。

  • 复制(Copy):在 C++11 之前,或对于没有移动构造函数的对象,会调用对象的复制构造函数来完成数据的迁移。

  • 移动(Move):从 C++11 开始,如果对象支持移动语义,vector 会优先使用移动构造函数。移动操作通常只涉及指针的重新赋值,因此比复制操作更高效。

3. 释放旧内存

数据迁移完成后,vector 会释放旧的内存空间。

4. 更新内部指针和容量

最后,vector 会更新其内部指向内存的指针,并更新其 capacity 值为新申请的空间大小。

为什么不每次只增加一点点容量?

        你可能会想,为什么 vector 不每次只增加一个元素的空间?这是因为内存的重新分配和数据复制/移动操作都是有性能开销的。如果每次都只增加一点点,那么频繁的 push_back() 就会导致频繁的内存重新分配,使得性能变得非常差。

        通过翻倍策略(或其他指数增长策略),vector 可以在均摊时间复杂度上达到O(1) 的性能。这意味着,虽然偶尔的扩容操作会很慢,但在长序列的添加操作中,平均下来,每次添加一个元素的时间开销是恒定的。这是一种以空间换取时间的典型设计。

面试简洁版回答

👉 vector 的扩容机制是按几何倍数(通常 1.5 倍或 2 倍)增长容量:当大小超过容量时,会重新分配一块更大的连续内存,将旧数据拷贝/移动过去,然后释放旧内存。这样保证了 push_back 的均摊复杂度是 O(1),但扩容会触发一次 O(n) 的整体搬迁。实际开发中,可以用 reserve 预分配来减少扩容开销。

vector中的resize和reserve

capacity():当前分配的容量

reserve(n):预分配容量,避免频繁扩容

resize(n):改变大小,多余元素被销毁,新增元素默认初始化

功能和目的

resize():用于改变 vector 中元素的数量(size)。

  • 如果新大小大于当前大小,vector 会插入新元素,并用默认值或指定的值进行初始化。
  • 如果新大小小于当前大小,vector 会删除多余的元素。

reserve():用于改变 vector 的容量(capacity),而不是元素的数量。它只是预留内存空间,但并不会改变 size 的值,也不会创建任何新的对象

对 size() 和 capacity() 的影响
 

resize(n):

  • size() 变为 n。
  • capacity() 可能会增加,但永远不会减少。如果 n > capacity(),vector 会扩容以满足新大小的需求。

reserve(n):

  • size() 保持不变。
  • capacity() 会变为至少 n。如果 n > capacity(),vector 会重新分配内存,使得 capacity() 变为 n 或一个更大的值。如果 n <= capacity(),则什么也不会发生。

resize 和 reserve 的核心区别在于:

resize 是改变实际存储的元素数量,

而 reserve 是改变底层分配的内存大小。

你可以用一个简单的比喻来帮助记忆:

resize 就像调整一个房间里的人数,人多了就请进来,人少了就请出去。

reserve 就像提前租一个更大的房间,但房间里的人数暂时不变。

使用场景

resize():当你需要精确地确定 vector 中元素的个数,并希望这些元素被初始化时,使用 resize()。例如,你需要一个包含 10 个零的 vector,你可以使用 vector<int> v; v.resize(10, 0);。

reserve():当你预先知道将要向 vector 中添加大量元素,但你还不确定具体的元素值时,使用 reserve()。这可以避免多次不必要的内存重新分配,从而提高性能。例如,如果你需要从文件中读取 1000 行数据,可以先 v.reserve(1000);,然后循环 push_back()。

vector中的emplace_back VS push_back

在 C++11 标准之前,我们只能使用 push_back() 来向 vector 末尾添加元素。C++11的emplace 相关的成员函数(如emplace_back()提供了一种更高效的方式来向vector 末尾添加元素)

push_back():先创建后复制/移动

push_back() 的工作流程是这样的:

  • 在外部构造一个临时对象
  • 将这个临时对象通过拷贝构造或移动构造的方式,添加到 vector 的末尾。
  • 简单来说,push_back() 需要一个已经存在的对象作为参数。

emplace_back() 的工作流程更直接:

  • 它接收的参数是构造新元素所需的参数。
  • 它会直接在 vector 内部的内存空间上构造这个新元素,避免了不必要的临时对象的创建和随后的拷贝/移动操作

核心区别:创建方式

  • push_back():拷贝/移动一个对象到 vector。
  • emplace_back():就地构造一个对象在 vector 中。

使用建议:

        对于大多数情况,尤其是当你的元素类型是复杂的对象时,emplace_back() 的性能通常优于 push_back()。因为它减少了一次临时对象的创建和销毁,以及一次拷贝或移动的开销。

        对于基本数据类型(如 int 或 double),push_back() 和 emplace_back() 的性能几乎没有区别,因为拷贝这些简单类型非常快。

因此,作为 C++ 程序员的最佳实践,在添加新元素时,优先考虑使用 emplace_back()

迭代器失效问题

在 vector 中,迭代器失效主要有两类原因:

        1. 导致容量变化的写操作:push_back()、insert()、emplace()、resize() 和 clear() 都可能导致迭代器失效,因为它们可能触发扩容。当 vector 发生扩容后,所有指向其内部元素的普通指针、引用和迭代器都会失效

        2. 删除操作:erase()、pop_back()。erase() 会使所有指向被删除元素之后(包括被删除元素本身)的迭代器失效。pop_back() 会使指向被删除元素的迭代器失效。

良好的编程习惯

1.尽量避免扩容,预先估计规模并 reserve(n):

2. 尽量避免在持有迭代器时做可能触发重分配的操作,比如:避免拿着迭代器遍历时又 push_back/insert/resize。

3. 如果需要保存一些迭代器,在使用迭代器执行了删除操作之后,及时更新迭代器。

类的成员函数调用顺序的问题

构造函数调用顺序:
当创建一个派生类(子类)的对象时,构造函数的调用顺序是:

  1. 先调用父类(基类)的构造函数。 如果存在多层继承,则从最顶层的基类开始,依次调用到直接父类的构造函数。
  2. 然后调用成员对象(如果存在)的构造函数。 按照成员对象在类定义中的声明顺序进行调用。
  3. 最后调用子类自身的构造函数体。
     

简单来说,构造顺序是:基类 -> 成员对象 -> 派生类自身。

析构函数调用顺序:

当销毁一个派生类(子类)的对象时,析构函数的调用顺序与构造函数的调用顺序完全相反:

  1. 先调用子类自身的析构函数体。
     
  2. 然后调用成员对象(如果存在)的析构函数。 按照成员对象在类定义中声明顺序的逆序进行调用。
     
  3. 最后调用父类(基类)的析构函数。 如果存在多层继承,则从直接父类开始,依次调用到最顶层的基类的析构函数。

简单来说,析构顺序是:派生类自身 -> 成员对象 -> 基类。

具体解释可以看:

c++中的构造函数&析构函数调用顺序-CSDN博客

http://www.xdnf.cn/news/19648.html

相关文章:

  • uni-app 布局之 Flex
  • 基于STM32与华为云联动的智能电动车充电桩管理系统
  • QSlider 和 QProgressBar 的区别与实践
  • 【Linux基础】Linux系统启动:深入解析Linux系统启动完整流程
  • 仿真波导中超短脉冲传输中的各种非线性效应所产生的超连续谱
  • AI如何理解PDF中的表格和图片?
  • qt安装FFmpeg后编译遇到error: collect2.exe: error: ld returned 1 exit status错误
  • 链表题类型注解解惑:理解Optional,理解ListNode
  • 数据结构--跳表(Skip List)
  • 【学Python自动化】 7. Python 输入与输出学习笔记
  • kaggle中的2D目标检测训练trick总结
  • 用了企业微信 AI 半年,这 5 个功能让我彻底告别重复劳动
  • 一文带你入门 AT 指令集:从串口通信到模块控制
  • 【智能体开发】怎样提升AI智能体的运行速度?
  • 实验2-代理模式和观察者模式设计
  • C++全局变量未初始的和已初始化的位置放在哪里?
  • C语言————实战项目“扫雷游戏”(完整代码)
  • 【Spring Cloud微服务】9.一站式掌握 Seata:架构设计与 AT、TCC、Saga、XA 模式选型指南
  • MD5加密算法详解与实现
  • 【LeetCode_26】删除有序数组中的重复项
  • 手撕Redis底层2-网络模型深度剖析
  • 云电脑是什么?与普通电脑的区别在哪里?——天翼云电脑体验推荐
  • 全国产FT-M6678核心板
  • SQL JOIN 操作全面解析
  • 哈希表-面试题01.02.判定是否互为字符重排-力扣(LeetCode)
  • 【LeetCode数据结构】栈和队列的应用
  • 在windows平台oracle 23ai 数据库上使用bbed
  • 面阵 vs 线阵相机:怎么选不踩坑?选型公式直接套用
  • SQLShift 实现Oracle 到 OceanBase 的存储过程转换初体验
  • 【Vue2 ✨】 Vue2 入门之旅(六):指令与过滤器