C/C++面试八股题(五)

目录:

1.(内存)堆和栈的区别是什么?

2.什么是堆,栈,内存泄漏和内存溢出?

3.什么是内存池?

4.请你说说什么是内存碎片?

5.在函数中申请堆内存需要注意什么?

内容:

1.(内存)堆和栈的区别是什么?

栈简介:

  • 栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。参考如下代码:
int main() {
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
}


其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到底,所以后定义的变量地址低于先定义的变量,比如上面代码中变量 s 的地址小于变量 b 的地址,p2 地址小于 s 的地址。栈中存储的数据的生命周期随着函数的执行完成而结束。

堆简介:

  • 堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。参考如下代码:
int main() {
    // C 中用 malloc() 函数申请
    char* p1 = (char *)malloc(10);
    cout<<(int*)p1<<endl;    //输出:00000000003BA0C0

    // 用 free() 函数释放
    free(p1);
   
    // C++ 中用 new 运算符申请
    char* p2 = new char[10];
    cout << (int*)p2 << endl;    //输出:00000000003BA0C0
  
    // 用 delete 运算符释放
    delete[] p2;
}


关于堆上内存空间的分配过程,首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。

内存分配方式:

  • 栈:栈是由操作系统自动管理的内存区域,按照“先进后出”的规则进行管理。当函数被调用时,它的局部变量和参数会被压入栈中,函数结束后,相关的内存空间会被自动释放。
  • :堆是由程序员手动管理的内存区域,内存分配和释放是通过mallocfree(C语言)或newdelete(C++语言)来控制的。堆内存没有自动回收机制,需要程序员显式释放,否则可能导致内存泄漏。

生命周期:

  • :生命周期较短。栈中的变量在函数调用时创建,函数返回时销毁。通常仅限于函数的执行期间。
  • :生命周期由程序员控制。堆中的对象可以在程序运行时动态创建,直到调用freedelete释放内存。

存储方式:

  • :栈内存是线性结构,存储的是局部变量和函数调用信息。每个线程都有自己的栈空间。
  • :堆内存是一个较大的自由区域,存储动态分配的对象和数组等。堆内存中的对象之间没有顺序,程序员负责管理。

分配速度:

  • :栈内存的分配和释放速度非常快,因为它是顺序操作(栈顶入栈或出栈),无需复杂的管理。
  • :堆内存的分配和释放速度较慢,因为堆内存管理需要处理碎片化、内存查找等问题。

内存大小:

  • :栈的内存空间较小,通常有限制。栈的大小由操作系统或编译器设置。栈空间的不足会导致“栈溢出”。
  • :堆的内存空间较大,通常只有物理内存的限制。堆空间的大小通常不受操作系统限制,但分配和管理的开销较大。

内存碎片化:

  • :栈操作遵循"先进后出"的原则,不会有内存块从栈中弹出,因此不会产生碎片。
  • :堆是通过动态分配内存的方式进行分配和释放,频繁的申请和释放内存可能会引发内存碎片问题。

总结:

  • :自动管理、快速分配、内存有限、生命周期短、容易栈溢出。
  • :手动管理、分配较慢、内存空间大、生命周期长、容易造成内存泄漏。

2.什么是堆,栈,内存泄漏和内存溢出?

内存泄漏:

内存泄漏是指程序在运行过程中,动态分配了内存(通常在堆上),由于缺乏正确的内存释放操作,导致这些内存空间无法被回收,造成内存的浪费。

原因:

  • 程序员忘记释放已经分配的内存。内存指针丢失,无法访问已分配的内存。内存引用计数错误(例如在某些语言中,程序员可能没有正确管理内存引用计数)。

后果

  • 内存泄漏会导致程序消耗越来越多的内存,直到系统内存耗尽,程序可能崩溃或者系统变慢。在长时间运行的程序(如服务器)中,内存泄漏特别严重,因为它会不断积累。

解决方法

  • 使用智能指针。在C/C++中使用free、delete等手动释放内存。

例子 :

#include <iostream>
using namespace std;

void memoryLeak() {
    int* ptr = new int[100];  // 动态分配内存
    // 忘记释放内存
}

int main() {
    while (true) {
        memoryLeak();  // 每次调用都会分配新的内存,但没有释放
    }
    return 0;
}

在上面的代码中,每次调用memoryLeak函数时,都会动态分配一个int类型的数组(100个整数)。然而,delete[] ptr; 语句没有被调用来释放内存,这会导致程序每次调用时都占用新的内存空间,导致内存泄漏。

解决方法:

#include <iostream>
using namespace std;

void memoryLeak() {
    int* ptr = new int[100];  // 动态分配内存
    // 执行一些操作...
    delete[] ptr;  // 释放内存
}

int main() {
    while (true) {
        memoryLeak();  // 正确释放内存
    }
    return 0;
}

内存溢出

内存溢出通常是指程序试图访问或写入超出分配给它的内存空间,导致系统出现异常。内存溢出主要有以下两种常见形式:

  • 栈溢出:栈溢出发生在程序使用了超过栈内存限制的空间。例如,当递归调用太深,或局部变量占用过多的栈空间时,会导致栈溢出。栈溢出的常见错误是“递归调用太深”。 常见原因:递归调用没有正确的终止条件,或者栈上分配了过多的局部变量。后果:栈溢出会导致程序崩溃,通常操作系统会抛出栈溢出的错误。
  • 堆溢出:堆溢出发生在程序试图向堆中分配超过可用内存的空间。当程序请求的堆内存大于系统可提供的内存时,会发生堆溢出。 常见原因:程序请求的内存过多,或者存在内存分配错误。后果:堆溢出会导致程序崩溃,甚至可能引起安全漏洞。

例子:递归过深导致栈溢出

#include <iostream>
using namespace std;

void recursiveFunction() {
    // 每次递归调用都会在栈上分配一些内存
    recursiveFunction();
}

int main() {
    try {
        recursiveFunction();  // 无限递归调用
    } catch (...) {
        cout << "Stack Overflow!" << endl;
    }
    return 0;
}

在上面的代码中,recursiveFunction函数调用自己,而没有终止条件。每次递归调用都会在栈上分配内存,而递归的深度没有限制,最终导致栈空间被用尽,从而发生栈溢出。

解决方法:

为递归函数添加终止条件,确保递归深度有限,避免过多占用栈空间。

#include <iostream>
using namespace std;

void recursiveFunction(int n) {
    if (n <= 0) return;  // 终止条件,防止无限递归
    recursiveFunction(n - 1);
}

int main() {
    recursiveFunction(1000);  // 限制递归深度
    return 0;
}

通过添加终止条件,递归深度不再无限增长,从而避免了栈溢出。

例子:分配过多内存导致堆溢出

#include <iostream>
using namespace std;

int main() {
    try {
        int* ptr = new int[1000000000];  // 尝试分配过大的堆内存
        cout << "Memory allocated successfully!" << endl;
        delete[] ptr;  // 释放内存
    } catch (const std::bad_alloc& e) {
        cout << "Heap Overflow! Memory allocation failed: " << e.what() << endl;
    }
    return 0;
}

在上面的代码中,new int[1000000000]试图分配一个非常大的整数数组,超出了系统的可用内存。由于内存不足,new会抛出一个std::bad_alloc异常,表示内存分配失败。

解决方法:

  • 限制分配的内存大小,确保分配的内存不会超过系统的限制。
  • 使用内存池(例如内存管理库),避免在运行时一次性分配过多内存。

3.什么是内存池?

内存池:

  • 内存池是一种动态内存分配与管理技术,通常情况下,程序员习惯直接使用new,delete,malloc,free等API申请和释放内存,这样导致的后果就是:当程序运行的时间很长的时候,由于所申请的内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。
  • 内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用。当程序员申请内存时,从池中取出一块动态分配,当程序员释放时,将释放的内存放回到池内,再次申请,就可以从池里取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池

内存池的实现方式:

  • 固定大小内存池:所有内存块的大小相同,适用于频繁分配固定大小的对象(例如,分配很多相同大小的数据结构)。
  • 分段内存池:内存池被分成不同大小的区域(例如,分别为16字节、32字节、64字节等),用于分配不同大小的对象。适用于对象大小不一致的情况。

内存池的优点:

性能提升:

  • 减少内存分配的开销:每次从内存池获取内存比直接通过操作系统申请内存要高效得多。操作系统的内存分配通常需要进行复杂的内存管理,尤其在多线程环境下,分配和释放内存需要涉及同步机制,而内存池通过简单的管理策略,避免了这些开销。
  • 降低碎片化:频繁的内存分配和释放会导致内存碎片,而内存池的内存块大小固定或按需求划分,能够有效避免内存碎片化。

内存管理更加可控:

  • 内存池使得内存的分配、释放过程更加明确和集中,程序员可以灵活地管理内存池的生命周期,而不依赖操作系统的内存管理。
  • 对于性能要求高的场景,内存池能够提供更加可预测的内存分配和释放行为。

提高系统稳定性:

  • 在一些长期运行的系统中(如游戏引擎、服务器等),内存池可以有效地避免由于频繁的内存分配和释放而导致的内存泄漏或堆溢出等问题。

优化内存使用:

  • 内存池的使用可以减少内存分配失败的风险,尤其在资源受限的嵌入式系统中,内存池提供了更好的内存分配效率。

4.请你说说什么是内存碎片?

内存碎片问题造成堆利用率很低的一个主要原因就是内存碎片化。如果有未使用的存储器,但是这块存储器不能用来满足分配的请求,这时候就会产生内存碎片化问题。内存碎片化分为内部碎片和外部碎片。

内碎片:

  • 内部碎片是指一个已分配的块比有效载荷大时发生的。(假设以前分配了10个大小的字节,现在只用了5个字节,则剩下的5个字节就会内碎片)。内部碎片的大小就是已经分配的块的大小和他们的有效载荷之差的和。因此内部碎片取决于以前请求内存的模式和分配器实现(对齐的规则)的模式。

外碎片:

  • 假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是外碎片问题。

解决方法:

  • 段页式管理:采用虚拟内存管理技术,将物理内存划分为不同的页或段,以更灵活地管理和分配内存空间,减少碎片化。
  • 使用内存池:通过分配一定数量的内存块,由内存池来管理分配和回收,减少频繁的内存分配和释放,从而减少碎片化

5.在函数中申请堆内存需要注意什么?

检查申请内存是否成功:

  • 使用动态内存分配函数(如 malloccallocnew)时,需要检查返回值是否为 NULL,以判断内存分配是否成功。
int *ptr = (int *)malloc(sizeof(int) * 10);
if (ptr == NULL) {
    // 内存分配失败,处理错误
    perror("Memory allocation failed");
    exit(1);
}

及时释放内存:

  • 每次在堆上申请的内存必须在不需要时通过 free(C语言)或 delete(C++)进行释放,避免 内存泄漏
  • 确保释放的内存地址是有效的,不能重复释放,也不能释放未分配的地址。
free(ptr);  // 释放内存
ptr = NULL; // 避免悬空指针

避免悬空指针:

  • 悬空指针是指向已释放或无效内存地址的指针。在释放内存后,立即将指针置为 NULL
free(ptr);
ptr = NULL; // 防止悬空指针

处理内存对齐问题:

某些平台或硬件有对齐要求(比如 4 字节或 8 字节对齐),使用标准内存分配函数一般能够保证对齐。如果有特殊需求,可以考虑使用对齐分配函数(如 posix_memalign_aligned_malloc)。

避免内存溢出:

  • 分配的内存要足够存储需要的数据,防止 数组越界 或 缓冲区溢出 问题。

避免重复分配:

  • 多次分配同一指针前,确保已释放旧的内存。如果忘记释放旧内存而直接分配新的,会导致内存泄漏。

减少频繁分配和释放:

  • 如果函数需要频繁分配和释放内存,可能会导致内存碎片化。可以考虑 内存池 或重用分配的内存。

避免对空指针或无效指针操作:

  • 在释放内存或使用指针前,确保指针指向合法的地址。

避免使用未初始化的指针:

  • 在申请内存之前,指针变量应初始化为 NULL,以防误操作。

#牛客创作赏金赛##C/C++##嵌入式##面试##面经#
嵌入式/C++八股 文章被收录于专栏

本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中

全部评论
友友们有什么建议可以指出哦
4 回复 分享
发布于 12-11 16:55 陕西
博主有些说的还是不够简洁,像内存碎片那块我简单理解 内:当分配给进程的内存大于所需大小时,其中剩余空间成为内部碎片 外:未分配的连续内存空间太小分配不能满足时,导致内存无法有效利用就出现外部碎片
3 回复 分享
发布于 12-11 20:07 陕西
博主快更快更
2 回复 分享
发布于 12-13 17:06 陕西
堆栈这块不全诶 数据结构相关堆栈 没有解释
1 回复 分享
发布于 12-13 22:46 陕西
期待继续更新
1 回复 分享
发布于 12-14 11:49 江苏
有一个疑问,既然堆中程序员申请了内存没释放可以由系统释放,那咋还泄露呢
1 回复 分享
发布于 12-14 12:24 江苏
欢迎大家订阅此专栏,订阅点赞收藏送花后,截图si聊我,免费赠送嵌入式学习资料大礼包(含简历模版、c/c++、嵌入式等资料)
1 回复 分享
发布于 12-14 15:59 陕西
已订阅求嵌入式资料
1 回复 分享
发布于 12-16 20:10 陕西

相关推荐

不愿透露姓名的神秘牛友
12-21 00:29
合肥九韶智能 C++软件工程师 14.5k * 13薪 硕士其他
点赞 评论 收藏
分享
评论
17
21
分享
牛客网
牛客企业服务