c++八股文:c++编译与内存管理

1. c++内存管理

c++在运行程序时,内存被分为几个不同的区域,每个区域负责不同的任务。

c++程序使用的分区一般包括:栈区、堆区、全局/静态变量存储区、常量存储区、代码区。
区域名存放的数据生命周期
栈区局部变量、函数参数、函数返回值栈上的变量⽣命周期与其所在函数的执⾏周期相同
堆区动态分配的内存new、malloc⽣命周期由程序员显式控制
全局/静态存储区全局变量和静态变量整个程序运行期间
常量区常量,不允许修改程序运行结束自动释放
代码区代码
  • 栈区:
    • 由编译器自动存放和释放,栈空间在进程生存周期一直都存在,当进程退出时,操作系统才会对栈空间进行回收。
    • 存放局部变量 、函数参数、函数返回值;
    • 函数的调用和返回通过栈来管理;
局部变量:
在函数或一个代码块内部声明的变量,称为局部变量。
它们只能被函数内部或者代码块内部的语句使用。
  • 堆区:

    • 堆⽤于存储动态分配的内存的区域,由程序员⼿动分配和释放;
    • 使⽤ new 和 delete 或 malloc 和 free 来进⾏堆内存的分配和释放;
  • 全局/静态存储区:

    • ⽣命周期是整个程序运⾏期间;
    • 在程序启动时分配,程序结束时释放;
    • 全局区存储全局变量和静态变量;
全局变量:
在所有函数外部定义的变量(通常是在程序的头部),称为全局变量。
全局变量的值在程序的整个生命周期内都是有效的。

静态变量:
在函数体内声明一个静态局部变量( Static Local Variable )。
它在函数运行结束后不会消失,并且只有声明它的函数中能够使用它。
声明一个静态局部变量的方法是在声明局部变量前加上 static。

  • 常量区:

    • 存放的是常量,不允许修改,程序运行结束自动释放。
  • 代码区:

    • 存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

示例:

#include <iostream>
using namespace std;
/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/
int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var;  // gp_var 在全局区(.bss 段)

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = "abc";         // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
    char *p_var1 = "123456";    // p_var1 在栈区;"123456"为字符串常量,存储在常量区
    static int s_var = 0;       // s_var 为静态变量,存在静态存储区(.data 段)
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}

作者:LeetCode
链接:https://leetcode.cn/leetbook/read/cmian-shi-tu-po/vv6a76/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2. 堆与栈

栈和堆都是⽤于存储程序数据的内存区域。栈是⼀种有限的内存区域,⽤于存储局部变量、函数调⽤信息等。
堆是⼀种动态分配的内存区域,⽤于存储程序运⾏时动态分配的数据。
栈上的变量⽣命周期与其所在函数的执⾏周期相同,⽽堆上的变量⽣命周期由程序员显式控制,可以(使⽤ new 或 malloc )和释放(使⽤ delete 或 free )。
栈上的内存分配和释放是⾃动的,速度较快。⽽堆上的内存分配和释放需要⼿动操作,速度相对较慢。

  • 申请方式:栈中存放的变量在编译时由编译器为其在栈上分配了空间,释放时也由于函数调用的返回,栈的空间会自动进行回收。堆中存放的变量由程序运行时决定的,会有操作系统或者内存管理模块来进行分配的。

3.变量定义与生命周期

c/c++变量有两个非常重要的属性生命周期和作用域,这两个属性分别从时间和空间两个维度描述一个变量。
  • 作用域:作用域即一个变量可以被引用的范围,常见的作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。

    • 全局变量:具有全局作用域。只需要在一个源文件里定义,就可以作用于所有源文件。其他不包含全局变量定义的源文件,需要用extern关键字在次声明全局变量 。
    • 静态全局变量:具有文件作用域。它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域。这样即使两个文件里定义了相同名字的全局静态变量,它们也不是相同的变量。
    • 局部变量:具有局部作用域。仅作用在函数内部,对于函数外部的程序来说不可见。
    • 局部静态变量:具有局部作用域,它只被初始化一次,只对定义自己的函数体始终可见, 只有定义该变量的函数内部可以使用访问和修改该变量。
  • 生命周期:变量可以被引用的时间段(变量的存在时间)

    • 全局变量:整个程序运行期间都会一直存在,都可以随时访问,当程序结束时,对应的变量则会自动销毁,内存会被系统回收。
    • 局部变量:生命周期仅限于函数被调用阶段,调用结束,该变量会自动销毁。
    • 局部静态变量:整个程序运行期间一直存在,只能被初始化一次。
  • 注意:

    • 全局变量定义在不要在头文件中定义:如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,编译时会因为重复定义而报错,因此不能再头文件中定义全局变量。一般情况下我们将变量的定义放在 .cpp 文件中,一般在 .h 文件使用extern 对变量进行声明。

4.内存对齐

  • 什么是内存对齐:内存对⻬是指数据在内存中的存储起始地址是某个值的倍数。
    • ⼤多数计算机硬件要求基本数据类型的变量在内存中的地址是它们⼤⼩的倍数。例如,⼀个 32 位整数通常需要在内存中对⻬到 4 字节边界。
    • 内存对⻬可以提⾼访问内存的速度。当数据按照硬件要求的对⻬⽅式存储时,CPU可以更⾼效地访问内存,减少因为不对⻬⽽引起的性能损失。
    • 许多计算机体系结构使⽤缓存⾏(cache line)来从内存中加载数据到缓存中。如果数据是对⻬的,那么⼀个缓存⾏可以装载更多的数据,提⾼缓存的命中率。
    • 有些计算机架构要求原⼦性操作(⽐如原⼦性读写)必须在特定的内存地址上执⾏。如果数据不对⻬,可能导致⽆法执⾏原⼦性操作,进⽽引发竞态条件。

5.内存泄露

  • 什么是内存泄露:内存泄漏并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。
  • 内存分类:
    • 堆内存泄漏:堆是动态分配的,一旦用户申请了内存分配而为及时释放,那么该部分内存在整个程序运行周期内都是被占用的,其他程序无法再使用这部分内存。我们在调用过程中使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete 释放内存,否则这块内存就会造成内存泄漏。
    • 系统资源泄露:主要指程序使⽤系统分配的资源⽐如 Bitmap,handle ,SOCKET 等没有使⽤相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运⾏不稳定。
    • 没有将基类的析构函数定义为虚函数:
      当基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没有正确是释放,因此造成内存泄露。
  • 什么操作导致内存泄漏:指针指向改变,未释放动态分配内存。
  • 如何防止内存泄漏:将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使⽤智能指针。
  • 智能指针有了解哪些:智能指针是为了解决动态分配内存导致内存泄露和多次释放同⼀内存所提出的,C11标准中放在<memory>头⽂件。包括:共享指针,独占指针,弱指针。
  • 构造函数,析构函数要设为虚函数吗,为什么?
    (1)析构函数
    析构函数需要。当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类析构函数,导致派⽣类资源⽆法释放,造成内存泄漏。
    (2)构造函数
    构造函数不需要,没有意义。虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝⽽不知道对象的确切类型。 要创建⼀个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。

6.智能指针

智能指针⽤于管理动态内存的对象,其主要⽬的是在避免内存泄漏和⽅便资源管理。
  • std::unique_ptr 独占智能指针
  • std::shared_ptr 共享智能指针
  • std::weak_ptr 弱引⽤智能指

7.new 和 malloc 有什么区别

newmalloc
类型安全性是C++的运算符,可以为对象分配内存并调⽤相应的构造函数。是C语⾔库函数,只分配指定⼤⼩的内存块,不会调⽤构造函数。
返回值返回的是具体类型的指针,⽽且不需要进⾏类型转换。返回的是 void* ,需要进⾏类型转换,因为它不知道所分配内存的⽤途。
内存分配失败在内存分配失败时会抛出 std::bad_alloc 异常。在内存分配失败时返回 NULL 。
内存块大小可以⽤于动态分配数组,并知道数组⼤⼩。只是分配指定⼤⼩的内存块,不了解所分配内存块的具体⽤途。
释放内存方式会调⽤对象的析构函数,然后释放内存。只是简单地释放内存块,不会调⽤对象的析构函数。

8.delete和free的区别

deletefree
类型安全性会调⽤对象的析构函数,确保资源被正确释放。不了解对象的构造和析构,只是简单地释放内存块。
内存块释放后的⾏为释放的内存块的指针值会被设置为 nullptr ,以避免野指针。不会修改指针的值,可能导致野指针问题。
数组的释放可以正确释放通过 new[] 分配的数组。不了解数组的⼤⼩,不适⽤于释放通过 malloc 分配的数组

9.什么野指针,怎么产生的,如何避免野指针

野指针是指指向已被释放的或⽆效的内存地址的指针。使⽤野指针可能导致程序崩溃、数据损坏或其他不可预测的⾏为。

怎么产生的野指针真么避免野指针
指针没有初始化指针变量一定要初始化,可以初始化为nullptr
释放后没有置空指针在释放内存后将指针置为 nullptr
返回局部变量的指针避免返回局部变量的指针
函数参数指针被释放避免函数参数指针被释放
使⽤智能指针(如 std::unique_ptr 和 std::shared_ptr )
注意函数参数的⽣命周期, 避免在函数内释放调⽤⽅传递的指针,或者通过引⽤传递指针

10.野指针和指针悬浮的区别

野指针是指向已经被释放或者⽆效的内存地址的指针。通常由于指针指向的内存被释放,但指针本身没有被置为nullptr 或者重新分配有效的内存,导致指针仍然包含之前的内存地址。使⽤野指针进⾏访问会导致未定义⾏为,可能引发程序崩溃、数据损坏等问题。
悬浮指针是指向已经被销毁的对象的引⽤。当函数返回⼀个局部变量的引⽤,⽽调⽤者使⽤该引⽤时,就可能产⽣悬浮引⽤。访问悬浮引⽤会导致未定义⾏为,因为引⽤指向的对象已经被销毁,数据不再有效。

  • 区别:
野指针悬浮指针
关联对象类型涉及指针类型设计引用类型
问题表现可能导致访问已释放或⽆效内存,引发崩溃或数据损坏可能导致访问已销毁的对象,引发未定义⾏为
产⽣原因野指针通常由于不正确管理指针⽣命周期引起悬浮指针通常由于在函数中返回局部变量的引⽤引起
如何避免悬浮指针避免在函数中返回局部变量的引⽤

11.字符串操作函数

  • strcpy(): char * strcpy(char *dest, const char *src)
    把从src地址开始且含有’\0’结束符的字符串复制到以dest开始的地址空间,返回值的类型为char*

  • strlen():size_t strlen (const char *s)
    计算给定字符串的⻓度。

  • strcat():char * strcat(char *dest, const char *src)
    作⽤是把src所指字符串添加到dest结尾处。

  • strcmp():int strcmp(const char *s1, const char *s2)
    ⽐较两个字符串设这两个字符串为s1,s2,
    若s1 == s2,则返回零
    若s1 < s2,则返回负数
    若s1 > s2,则返回正数

参考

Leetcodec++面试突破

代码随想录——最强八股文

  • 28
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值