【C++八股题整理】内存布局、堆和栈、内存泄露、函数调用栈

2024-09-02 17:36

本文主要是介绍【C++八股题整理】内存布局、堆和栈、内存泄露、函数调用栈,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++八股题整理

  • 内存布局
    • C++中的内存分配情况
    • 堆和栈的内存有什么区别?
    • 堆内存分配慢如何优化?内存池
    • 内存溢出和内存泄漏是什么?如何避免?
    • 内存碎片是什么?怎么解决?
    • 为什么栈的访问效率比堆高?
    • 函数调用时栈的变化?
    • 函数的参数列表为什么从右往左入栈?

内存布局

C++中的内存分配情况

在这里插入图片描述

区域存储内容分配方式生命周期
(Stack)局部变量、局部常量、函数的参数和返回地址自动分配和释放,由编译器管理。连续的内存块,分配释放速度快函数调用时入栈,返回时释放
(Heap)动态分配的对象和数组newdelete,或mallocfree动态分配和释放。不连续的内存块,易出现碎片,速度慢程序员手动分配和释放
全局/静态存储区全局变量、静态变量程序启动时分配,程序结束时释放与程序的整个生命周期一致
常量区字符串字面量、const修饰的全局和静态变量、虚函数表程序启动时分配与程序的整个生命周期一致
代码区程序的二进制代码由操作系统在程序加载时分配与程序的整个生命周期一致
#include <iostream>int globalVar = 10;  // 全局变量,存储在全局/静态存储区
static int staticGlobalVar = 20;  // 静态全局变量,存储在全局/静态存储区class MyClass {
public:int memberVar;  // 成员变量,存储在对象实例所分配的内存中(栈或堆)static int staticMemberVar;  // 静态成员变量,存储在全局/静态存储区// 构造函数,存储在代码区MyClass(int val) : memberVar(val) {}// 成员函数,存储在代码区void show() {std::cout << "Member Var: " << memberVar << std::endl;}};const int constGlobalVar = 40;  // 全局常量,存储在常量区int main() {int localVar = 50;  // 局部变量,存储在栈中const int constLocalVar = 60;  // 局部常量,存储在栈中(通常编译器优化后会放入寄存器)// 静态创建对象MyClass obj(localVar);  // 对象本身和成员变量存储在栈中// 动态创建对象MyClass* heapObj = new MyClass(localVar);  // 指针heapObj在栈上,对象本身和成员变量存储在堆中// 释放动态分配的对象delete heapObj;  // 释放堆中的内存return 0;
}

堆和栈的内存有什么区别?

特性栈内存堆内存
管理方式自动管理,系统分配和释放手动管理,程序员分配和释放
内存布局连续的内存块,不会产生内存碎片非连续的内存块,会产生内存碎片
分配地址由高向低由低向高
分配速度慢,因为要寻找合适大小的内存块
空间大小较小,通常几MB到几十MB较大,通常可达GB级别
生命周期随函数调用开始和结束程序员控制,直到显式释放
存储内容局部变量、函数调用信息动态分配的对象和数据结构

堆内存分配慢如何优化?内存池

内存池通过一次性预先分配一大块内存,并将其划分为多个固定大小的小块,当需要分配内存时,从这些小块中快速分配,而不是每次调用 malloc;释放内存时,直接将小块返回内存池,而不调用 free。这样大幅减少了频繁的堆内存分配和释放操作的系统开销,降低了内存碎片的风险,显著提升了在高频率、小内存块分配场景下的性能。

#include <iostream>class MemoryAllocator {
private:int poolsize;            // 内存池的总大小int blocksize;           // 每个块的大小int numblocks;           // 内存池中块的数量char* pool;              // 指向内存池的指针bool* used;              // 用于标记每个块是否被使用的布尔数组public:// 构造函数:初始化内存池MemoryAllocator(int ps, int bs) : poolsize(ps), blocksize(bs), numblocks(ps / bs) {pool = new char[poolsize];          // 分配内存池used = new bool[numblocks];         // 分配标记数组for (int i = 0; i < numblocks; i++) {used[i] = false;                // 初始化标记数组,所有块都是未使用的}}// 析构函数:释放分配的内存~MemoryAllocator() {delete[] pool;   // 释放内存池delete[] used;   // 释放标记数组}// 分配内存块void* Alloc() {for (int i = 0; i < numblocks; i++) {if (!used[i]) {                  // 查找第一个未使用的块used[i] = true;             // 标记为已使用return &pool[blocksize * i]; // 返回块的指针}}return nullptr;  // 如果没有可用块,返回nullptr}// 释放内存块void free(void* ptr) {// 检查指针是否在内存池的有效范围内if (ptr >= pool && ptr < pool + poolsize) {// 计算块的索引int index = (static_cast<char*>(ptr) - pool) / blocksize;if (used[index]) used[index] = false; // 标记为未使用}}
};int main() {MemoryAllocator pool(100, 4);  // 创建一个内存池,总大小100字节,每块4字节// 分配两个内存块void* ptr1 = pool.Alloc();std::cout << "Allocated at " << ptr1 << std::endl;void* ptr2 = pool.Alloc();std::cout << "Allocated at " << ptr2 << std::endl;// 释放第二个内存块pool.free(ptr2);std::cout << "Freed at " << ptr2 << std::endl;return 0;
}

内存溢出和内存泄漏是什么?如何避免?

  • 内存泄漏:程序在堆上动态分配内存后,未能正确释放不再需要的内存,导致这部分内存无法被重用,从而使得系统中的可用内存逐渐减少。内存泄漏最终会导致内存溢出。
  • 内存溢出(out of memory,OOM):程序试图分配的内存超过了系统或应用程序所能提供的最大可用内存,导致程序运行失败或异常。内存溢出可能发生在堆或栈上。
    • 堆溢出:通常是因为内存泄漏
      int main() {while (true) { int* ptr = new int[1000000]; } // 不断分配内存,最终会导致堆溢出return 0;
      }
      
    • 栈溢出:通常是因为过深的递归调用
      void recursiveFunction() {int largeArray[10000]; // 大量消耗栈空间recursiveFunction();   // 无限递归调用,最终会导致栈溢出
      }
      int main() {recursiveFunction();return 0;
      }
      

可以使用智能指针来自动管理内存,避免手动管理的复杂性。

内存碎片是什么?怎么解决?

  • 内存碎片:在动态内存管理中,由于频繁的分配和释放内存,导致内存中出现许多大小不一的、无法被利用的空闲块的现象
    • 内部碎片:当分配的内存块比实际需要的内存要大时,未使用的部分称为内部碎片。内部碎片主要出现在固定大小的内存分配策略中
    • 外部碎片:内存中存在足够多的空闲空间总量,但由于它们不连续,无法为大块的内存请求提供服务
  • 解决方法
    • 内存紧缩:通过移动分散的内存块来将合并外部碎片,形成连续的内存块。紧缩通常伴随暂停程序运行,因此在实时系统中不太适用
    • 优化分配策略:最佳适应、最先适应、最后适应、首次适应等
    • 内存池:因为块的大小固定且地址连续,因此能避免外部碎片

为什么栈的访问效率比堆高?

  • 缓存局部性:栈内存分配是线性增长的,符合CPU缓存的局部性原则,访问效率高;而堆内存分配则可能在内存中是离散的,导致缓存命中率低,访问速度相对较慢
  • 线程安全性:每个线程都有自己独立的栈,所以栈上的操作通常是线程安全的,不需要同步机制;堆是全局共享的资源,多个线程访问时可能需要同步机制(如锁),这会进一步降低堆内存分配和访问的效率

函数调用时栈的变化?

参考资料 参考资料
比如main函数调用add(int a , int b)函数:

  1. 从右往左压入add的参数值
  2. 保存call指令的下一条指针的地址
  3. 压入EBP(此时EBP指向main的栈底),EBP=ESP(让EBP指向add的栈底),保存main的栈
  4. 开辟add函数的栈空间
  5. add函数中的指令
  6. add函数保存计算值
  7. ESP=EBP,add函数开始退栈
  8. EBP出栈,就是还原main函数的栈底指针
  9. 清除add函数栈空间,继续执行main

这一过程中开辟出的空间称为栈帧,它包括以下内容:

  1. 被调函数的参数:通常从右向左顺序入栈(根据调用约定),即最后一个参数最先入栈。
  2. 被调函数的返回地址:用于在被调函数执行完毕后,返回到调用函数的下一条指令。
  3. 调用者函数的栈帧指针(ebp):保存调用者的栈底地址,以便在被调函数执行完毕后恢复调用者的栈帧。
  4. 被调函数的局部变量:在栈上为被调函数的局部变量分配的空间。

函数的参数列表为什么从右往左入栈?

主要是为了支持函数的变长参数 示例
但这只是一种约定,有些语言和平台并不这样

这篇关于【C++八股题整理】内存布局、堆和栈、内存泄露、函数调用栈的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1130617

相关文章

C++中RAII资源获取即初始化

《C++中RAII资源获取即初始化》RAII通过构造/析构自动管理资源生命周期,确保安全释放,本文就来介绍一下C++中的RAII技术及其应用,具有一定的参考价值,感兴趣的可以了解一下... 目录一、核心原理与机制二、标准库中的RAII实现三、自定义RAII类设计原则四、常见应用场景1. 内存管理2. 文件操

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

MyBatis Plus 中 update_time 字段自动填充失效的原因分析及解决方案(最新整理)

《MyBatisPlus中update_time字段自动填充失效的原因分析及解决方案(最新整理)》在使用MyBatisPlus时,通常我们会在数据库表中设置create_time和update... 目录前言一、问题现象二、原因分析三、总结:常见原因与解决方法对照表四、推荐写法前言在使用 MyBATis

C++作用域和标识符查找规则详解

《C++作用域和标识符查找规则详解》在C++中,作用域(Scope)和标识符查找(IdentifierLookup)是理解代码行为的重要概念,本文将详细介绍这些规则,并通过实例来说明它们的工作原理,需... 目录作用域标识符查找规则1. 普通查找(Ordinary Lookup)2. 限定查找(Qualif

Redis过期删除机制与内存淘汰策略的解析指南

《Redis过期删除机制与内存淘汰策略的解析指南》在使用Redis构建缓存系统时,很多开发者只设置了EXPIRE但却忽略了背后Redis的过期删除机制与内存淘汰策略,下面小编就来和大家详细介绍一下... 目录1、简述2、Redis http://www.chinasem.cn的过期删除策略(Key Expir

MySQL复杂SQL之多表联查/子查询详细介绍(最新整理)

《MySQL复杂SQL之多表联查/子查询详细介绍(最新整理)》掌握多表联查(INNERJOIN,LEFTJOIN,RIGHTJOIN,FULLJOIN)和子查询(标量、列、行、表子查询、相关/非相关、... 目录第一部分:多表联查 (JOIN Operations)1. 连接的类型 (JOIN Types)

C/C++ chrono简单使用场景示例详解

《C/C++chrono简单使用场景示例详解》:本文主要介绍C/C++chrono简单使用场景示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友... 目录chrono使用场景举例1 输出格式化字符串chrono使用场景China编程举例1 输出格式化字符串示

C++/类与对象/默认成员函数@构造函数的用法

《C++/类与对象/默认成员函数@构造函数的用法》:本文主要介绍C++/类与对象/默认成员函数@构造函数的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录名词概念默认成员函数构造函数概念函数特征显示构造函数隐式构造函数总结名词概念默认构造函数:不用传参就可以