C++内存管理(可能对面试有用)

  • 在32位模式下,一个指针或地址占用4个字节的内存,共有32位,理论上能够访问的虚拟内存空间大小为 2 32 = 0 X 100000000 2^{32} = 0X100000000 232=0X100000000 Bytes,即4GB,有效虚拟地址范围是 0 ~ 0XFFFFFFFF。程序能够使用的最大内存为 4GB,跟物理内存没有关系。如果程序需要的内存大于物理内存,或者内存中剩余的空间不足以容纳当前程序,那么操作系统会将内存中暂时用不到的一部分数据写入到磁盘,等需要的时候再读取回来,而我们的程序只管使用 4GB 的内存,不用关心硬件资源够不够。如果物理内存大于 4GB,例如目前很多PC机都配备了8GB的内存,那么程序也无能为力,它只能够使用其中的 4GB。

  • 在64位编译模式:能够访问的虚拟内存空间大小为 2 64 2^{64} 264。这是一个很大的值,不但物理内存不可能达到这么大,CPU的寻址能力也没有这么大,实现64位长的虚拟地址只会增加系统的复杂度和地址转换的成本,带不来任何好处,所以 Windows 和 Linux 都对虚拟地址进行了限制,仅使用虚拟地址的低48位(6个字节),总的虚拟地址空间大小为 2 48 = 256 T B 2^{48} = 256TB 248=256TB

  • 32位的操作系统只能运行32位的程序(也即以32位模式编译的程序),64位操作系统可以同时运行32位的程序(为了向前兼容,保留已有的大量的32位应用程序)和64位的程序(也即以64位模式编译的程序)。64位的CPU运行64位的程序才能发挥它的最大性能,运行32位的程序会白白浪费一部分资源。

  • **Windows:**在默认情况下会将高地址的 2GB 空间分配给内核(也可以配置为1GB)。Linux: 默认情况下会将高地址的 1GB 空间分配给内核。分配给内核的这段空间成为内核空间。为了安全,只能借助系统API来访问你自己,这个API函数俗称为 System Call。应用程序只能使用剩下的地址空间,称为用户空间(User Space)。

  • 如果内核用有自己独立的地址空间让内核处于一个独立的进程中,这样每次进行系统调用都需要切换进程。切换进程的消耗是巨大的,不仅需要寄存器进栈出栈,还会使CPU中的数据缓存失效、MMU中的页表缓存失效,这将导致内存的访问在一段时间内相当低效。而让内核和用户程序共享地址空间,发生系统调用时进行的是模式切换,模式切换仅仅需要寄存器进栈出栈,不会导致缓存失效;现代CPU也都提供了快速进出内核模式的指令,与进程切换比起来,效率大大提高了。MMU:内存管理单元,负责虚拟地址映射为物理地址。虚拟地址:现代操作系统都使用分页机制来管理内存,这使得每个程序都拥有自己的地址空间。每当程序使用虚拟地址进行读写时,都必须转换为实际的物理地址,才能真正在内存条上定位数据。

  • 从⾼地址到低地址,⼀个程序由命令⾏参数和环境变量、栈、⽂件映射区、堆、BSS段、数据段、代码段组成。 命令⾏参数是指从命令⾏执⾏程序的时候,给程序的参数;栈从⾼地址向低地址增⻓,是⼀块连续的空间, 存储局部变量、函数参数值;⽂件映射区位于堆和栈之间;堆区动态申请内存⽤,堆从低地址向⾼地址增⻓;BSS 段存放程序中未初始化的 全局变量和静态变量的⼀块内存区域; 数据段存放程序中已初始化的 全局变量和静态变量 的⼀块内存区域;代码段存放程序执⾏代码的⼀块内存区域。只读,代码段的头部还会包含⼀些只读的常数变量。

  • 在C/C++中内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。静态内存分配:编译时分配。包括:全局、静态全局、静态局部三种变量。动态内存分配:运行时分配,包括:栈(stack): 局部变量,堆(heap): c语言中用到的变量被动态的分配在内存中。(malloc或calloc、realloc、free函数)

    • 在这里插入图片描述

    • **栈区(stack):**指那些由编译器在需要的时候分配,不需要时自动清除的变量所在的储存区,如函数执行时,函数的形参以及函数内的局部变量分配在栈区,函数运行结束后,形参和局部变量去栈(自动释放)。栈内存分配运算内置与处理器的指令集中,效率高但是分配的内存空间有限

    • 堆区(heap):指哪些由程序员手动分配释放的储存区,如果程序员不释放这块内存,内存将一直被占用,直到程序运行结束由系统自动收回,c语言中使用malloc,free申请和释放空间。

    • **全局数据区(global data):**全局变量和静态变量的储存是放在一块的,其中初始化的全局变量和静态变量在一个区域,这块空间当程序运行结束后由系统释放。⽣命周期是整个程序运⾏期间

    • **常量储存区(const):**常量字符串就是储存在这里的,如“ABC”字符串就储存在常量区,储存在常量区的只读不可写。const修饰的全局变量也储存在常量区,const修饰的局部变量依然在栈上

    • **程序代码区:**存放源程序的二进制代码。

  • 栈:先进后出原则,一段连续的内存,需要同时记录栈顶和栈底,才能对当前的栈定位。内存有限,一般是1M-8M,超过这个值就会栈溢出。栈在函数调用时,栈中存放的是函数中各个参数(局部变量)。栈底下是函数调用后的下一条指令。一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

    • 管理方式由系统自动分配空间,同时系统自动释放空间。例如,声明在函数中一个局部变量“int b“。系统自动在栈中为b开辟空间,当对应的生存周期结束后栈空间自动释放。需要程序员手动申请并且手动释放,并指明大小。在C语言中malloc函数申请,释放free函数,在C++中new和delete实现。
      分配方式有2种分配方式——静态分配和动态分配。静态由编译器完成,例如局部变量;动态由alloca函数实现,并且编译器会进行释放。都是动态分配的,没有静态分配的堆。
      空间大小一般情况下是1-8M大小的内存,超过就会栈溢出。获得空间根据系统的有效虚拟内存有关,比较灵活,比较大。
      回收内存是否产生碎片问题空间连续的,所以不会产生碎片。链式存储,会产生碎片。
      数据扩展方式向低地址扩展的数据结构,是一块连续的内存的区域。向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。
      分配效率由系统自动分配,速度较快。但程序员是无法控制的。由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。
  • 内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。可以使⽤Valgrind, mtrace进⾏内存泄漏检查。指针指向改变,未释放动态分配内存

    • 堆内存泄漏 (Heap leak) :对内存指的是程序运⾏中根据需要分配通过malloc,realloc new等从堆中分配的⼀块内存,在是完成后必须通过调⽤对应的 free或者 delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使⽤,就会产⽣ Heap Leak。
    • 系统资源泄露(Resource Leak):主要指程序使⽤系统分配的资源⽐如 Bitmap,handle ,SOCKET 等没有使⽤相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运⾏不稳定。
    • 没有将基类的析构函数定义为虚函数:当基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没有正确是释放,因此造成内存泄露。
    • 防⽌内存泄露:将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使⽤智能指针
  • 构造函数,析构函数要设为虚函数吗

    • 析构函数需要。当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类析构函数,导致派⽣类资源⽆法释放,造成内存泄漏
    • 构造函数不需要,没有意义。虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝⽽不知道对象的确切类型。 要创建⼀个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。
  • 智能指针是为了解决动态分配内存导致内存泄露和多次释放同⼀内存所提出的,C11标准中放在< memory>头⽂件。包括:共享指针,独占指针,弱指针。智能指针⽤于管理动态内存的对象,其主要⽬的是在避免内存泄漏和⽅便资源管理。

    • std::unique_ptr 独占智能指针,提供对动态分配的单⼀对象所有权的独占管理。通过独占所有权,确保只有⼀个。std::unique_ptr 可以拥有指定的内存资源。移动语义和右值引⽤允许 std::unique_ptr 在所有权转移时⾼效地进⾏转移。

    • std::shared_ptr (共享智能指针):允许多个智能指针共享同⼀块内存资源。内部使⽤引⽤计数来跟踪对象被共享的次数,当计数为零时,资源被释放。提供更灵活的内存共享,但可能存在循环引⽤的问题。

    • std::weak_ptr (弱引⽤智能指针):⽤于解决 std::shared_ptr 可能导致的循环引⽤问题。std::weak_ptr 可以从 std::shared_ptr 创建,但不会增加引⽤计数,不会影响资源的释放。通过 std::weak_ptr::lock() 可以获取⼀个 std::shared_ptr 来访问资源。

      • #include <memory>
        std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
        std::weak_ptr<int> weakPtr = sharedPtr;
        
  • new malloc 有什么区别

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

    • deletefree
      类型安全调⽤对象的析构函数,确保资源被正确释放。不了解对象的构造和析构,只是简单地释放内存块。
      内存块释放后释放的内存块的指针值会被设置为 nullptr ,以避免野指针。不会修改指针的值,可能导致野指针问题。
      数组的释放可以正确释放通过 new[] 分配的数组。不了解数组的⼤⼩,不适⽤于释放通过 malloc 分配的数组。
    • 野指针是指指向已被释放的或⽆效的内存地址的指针。使⽤ᰀ指针可能导致程序崩溃、数据损坏或其他不可预测的

      ⾏为。通常由以下⼏种情况产⽣

      • 释放后没有置空指针

      • int* ptr = new int;
        delete ptr;
        // 此时 ptr 成为ᰀ指针,因为它仍然指向已经被释放的内存
        ptr = nullptr; // 避免ᰀ指针,应该将指针置为 nullptr 或赋予新的有效地址
        
      • 返回局部变量的指针

      • int* createInt() {
         int x = 10;
         return &x; // x 是局部变ᰁ,函数结束后 x 被销毁,返回的指针成为ᰀ指针
        }
        // 在使⽤返回值时可能引发未定义⾏为,避免返回局部变量的指针
        int* createInt() {
         int* x = new int;
         *x = 10;
         return x;
        }
        
      • 函数参数指针被释放

      • void foo(int* ptr) {
         // 操作 ptr
         delete ptr;  //  注意函数参数的⽣命周期, 避免在函数内释放调⽤⽅传递的指针,或者通过引⽤传递指针。
        }
        int main() {
         int* ptr = new int;
         foo(ptr);
         // 在 foo 函数中 ptr 被释放,但在 main 函数中仍然可⽤,成为ᰀ指针
         // 避免:在 foo 函数中不要释放调⽤⽅传递的指针
        }
        
      • 使⽤智能指针(如 std::unique_ptr std::shared_ptr :避免显式 delete,指针会在超出作⽤域时⾃动释放

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

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

  • 内存对⻬是指数据在内存中的存储起始地址是某个值的倍数。在C语⾔中,结构体是⼀种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是⼀些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其⾃然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第⼀个成员的地址和整个结构体的地址相同。

    • 为了使CPU能够对变量进⾏快速的访问,变量的起始地址应该具有某些特性,即所谓的“对⻬”,⽐如4字节的 int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对⻬”跟数据在内存中的位置有关。如果⼀个变量的内存地址正好位于它⻓度的整数倍,他就被称做⾃然对⻬。
    • ⽐如在32位cpu下,假设⼀个整型变量的地址为0x00000004(为4的倍数),那它就是⾃然对⻬的,⽽如果其地址为0x00000002(⾮4的倍数)则是⾮对⻬的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照⼀定的规则在空间上排列,⽽不是顺序的⼀个接⼀个的排放,这就是对⻬
    • 需要字节对⻬的根本原因在于CPU访问数据的效率问题。假设上⾯整型变量的地址不是⾃然对⻬,⽐如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第⼀次取从0x00000002-0x00000003的⼀个short,第⼆次取从0x00000004-0x00000005的⼀个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第⼀次为char,第⼆次为short,第三次为char,然后组合得到整型数据。⽽如果变ᰁ在⾃然对⻬位置上,则只要⼀次就可以取出数据。⼀些系统对对⻬要求⾮常严格,⽐如sparc系统,如果取未对⻬的数据会发⽣错误,⽽在x86上就不会出现错误,只是效率下降
    • 各个硬件平台对存储空间的处理上有很⼤的不同。⼀些平台对某些特定类型的数据只能从某些特定地址开始存取。⽐如有些平台每次读都是从偶地址开始,如果⼀个int型(假设为32位系统)如果存放在偶地址开始的地⽅,那么⼀个读周期就可以读出这32bit,⽽如果存放在奇地址开始的地⽅,就需要2个读周期,并对两次读出的结果的⾼低字节进⾏拼凑才能得到该32bit数据。显然在读取效率上下降很多。
      • ⼤多数计算机硬件要求基本数据类型的变量在内存中的地址是它们⼤⼩的倍数。例如,⼀个 32 位整数通常需要在内存中对⻬到 4 字节边界。
      • 内存对⻬可以提⾼访问内存的速度。当数据按照硬件要求的对⻬⽅式存储时,CPU可以更⾼效地访问内存,减少因为不对⻬⽽引起的性能损失。
      • 许多计算机体系结构使⽤缓存⾏(cache line)来从内存中加载数据到缓存中。如果数据是对⻬的,那么⼀个缓存⾏可以装载更多的数据,提⾼缓存的命中率
      • 有些计算机架构要求原⼦性操作(⽐如原⼦性读写)必须在特定的内存地址上执⾏。如果数据不对⻬,可能导致⽆法执⾏原⼦性操作,进⽽引发竞态条件
  • 函数原型: void * malloc (size_ t size) ;

    • 开辟一块size大小的连续堆内存。size表示堆上所开辟内存的大小(字节数)。
    • 函数返回值是一个指针,指向刚刚开辟的内存的首地址。如果开辟内存失败, 返回一个空指针,即返回值为NULL。
    • 当内存不再使用时,应使用free ()函数将内存块释放,使用时 必须包含头文件<stdlib.h>或<malloc.h>
  • 函数原型: void * calloc(size_ t n, size t size);

    • 在内存的动态存储区中分配n个长度为size的连续空间,
    • 函数返回一个指向分配起始地址的指针;
    • 如果分配不成功,返回NULL。
    • 当内存不再使用时,应使用free ()函数将内存块释放。使用时必须包含头文件<stdlib.h>或<malloc.h>
  • **函数原型:**void * realloc(void * mem_ address, size_ t newsize) ;

    • 为已有内存的变量重新分配新的内存大小(可大、可小) ;
    • 先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_ address返回;如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address 所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。
    • 如果分配不成功,返回NULL。
    • 当内存不再使用时,应使用free ()函数将内存块释放。使用时必须包含头文件<stdlib.h>或<malloc.h>
  • 函数原型: void free (void *ptr) ; //ptr为要释放的内存指针。free():释放指针变量在堆区上的内存空间,不能释放栈上的内存空间,free要与malloc(calloc、realloc)成对使用。

    • 如果malloc(calloc、realloc) 比 free 多, 会造成内存泄漏;
    • 如果malloc(calloc、realloc) 比 free 少,会造成二次删除, 破坏内存,导致程序崩溃。
  • 分析运⾏下⾯的Test函数会有什么样的结果

    • void GetMemory1(char* p)
      {
       p = (char*)malloc(100);
      }
      void Test1(void)
      {
       char* str = NULL;
       GetMemory1(str);
       strcpy(str, "hello world");
       printf(str);
      }
      
    • 程序崩溃。 因为GetMemory1并不能传递动态内存,Test1函数中的 str⼀直都是NULL。strcpy(str, “hello world”)将使程序崩溃

    • char *GetMemory2(void)
      {
       char p[] = "hello world";
       return p;
      }
      void Test2(void)
      {
       char *str = NULL;
       str = GetMemory2();
       printf(str);
      }
      
    • 可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,使其原现的内容已经被清除,新内容不可知。

    • void GetMemory3(char** p, int num)
      {
       *p = (char*)malloc(num);
      }
      void Test3(void)
      {
       char* str = NULL;
       GetMemory3(&str, 100);
       strcpy(str, "hello");
       printf(str);
      }
      
    • 能够输出hello, 内存泄露。GetMemory3申请的内存没有释放

    • void Test4(void)
      {
       char *str = (char*)malloc(100);
       strcpy(str, "hello");
       free(str);
       if(str != NULL) {
       strcpy(str, "world");
       cout << str << endl;
       }
      }
      
    • 篡改动态内存区的内容,后果难以预料。⾮常危险。因为 free(str);之后,str成为野指针,if(str != NULL)语句不起作⽤。

    • 实现内存拷⻉函数:char* strcpy(char* strDest, const char* strSrc);

    • char* strcpy(char *dst,const char *src) {// [1] 源字符串参数⽤const修饰,防⽌修改源字符串。
       assert(dst != NULL && src != NULL); // [2] 
       char *ret = dst; // [3]  忘记保存原始的strdst值。
       while ((*dst++=*src++)!='\0'); // [4]
       return ret;
      }
      
    • 检查指针的有效性时使⽤ assert(!dst && !src);char *转换为 bool 即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增⼤和维护成本升⾼。

    • 检查指针的有效性时使⽤ assert(dst != 0 && src != 0);直接使⽤常ᰁ(如本例中的0)会减少程序的可维护性。⽽使⽤NULL代替0,如果出现拼写错误,编译器就会检查出来。

    • 假如考虑dst和src内存重叠的情况,strcpy该怎么实现

    • char s[10]="hello";
      strcpy(s, s+1);
      // 应返回 ello
      strcpy(s+1, s);
      // 应返回 hhello 但实际会报错
      // 因为dst与src重叠了,把'\0'覆盖了
      
    • 所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有⼀种情况: src<=dst<=src+strlen(src)。C函数 memcpy ⾃带内存重叠检测功能,下⾯给出 memcpy 的实现my_memcpy

    • char * strcpy(char *dst,const char *src)
      {
       assert(dst != NULL && src != NULL);
       char *ret = dst;
       my_memcpy(dst, src, strlen(src)+1);
       return ret;
      }
      /* my_memcpy的实现如下 */
      char *my_memcpy(char *dst, const char* src, int cnt)
      {
           assert(dst != NULL && src != NULL);
           char *ret = dst;
           /*内存重叠,从⾼地址开始复制*/
           if (dst >= src && dst <= src+cnt-1)
           {
               dst = dst+cnt-1;
               src = src+cnt-1;
               while (cnt--)
               {
               	*dst-- = *src--;
               }
           }
           else //正常情况,从低地址开始复制
           {
               while (cnt--)
               {
               	*dst++ = *src++;
               }
           }
           return ret;
      }
      
  • 已知String的原型如下,请编写以下四个函数

    • class String
      {
      public:
           String(const char *str = NULL);
           String(const String &other);
           ~ String(void);
           String & operate =(const String &other);
      private:
       	char *m_data;
      };
      
    • 对构造函数赋值运算符实现的理解。实际考察类内含有指针的构造函数赋值运算符函数写法。

    • // 构造函数
      String::String(const char *str)
      {
           if(str==NULL)
           {
               m_data = new char[1]; //对空字符串⾃动申请存放结束标志'\0'
               *m_data = '\0';
           } 
           else
           {
               int length = strlen(str);
               m_data = new char[length + 1];
               strcpy(m_data, str);
           }
      }
      // 析构函数
      String::~String(void)
      {
       	delete [] m_data; // 或delete m_data;
      }
      //拷⻉构造函数
      String::String(const String &other)
      {
           int length = strlen(other.m_data);
           m_data = new char[length + 1];
           strcpy(m_data, other.m_data);
      }
      //赋值函数
      String &String::operate =(const String &other)
      { 
           if(this == &other)
           {
           	return *this; // 检查⾃赋值
           } 
           delete []m_data; // 释放原有的内存资源
           int length = strlen(other.m_data);
           m_data = new char[length + 1]; //对m_data加NULL判断
           strcpy(m_data, other.m_data); 
           return *this; //返回本对象的引⽤
      }
      
  • 编写完成一个C/C++程序后,想要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件(注意后缀名):

    • 预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
    • 编译阶段。编译器(ccl)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令。
    • 汇编阶段。汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本文件中打开hello.o文件,看到的将是一堆乱码。
    • **链接阶段。**链接器(ld)负责处理合并目标代码,生成一个可执行目标文件,可以被加载到内存中,由系统执行。
  • C与C++的内存分配⽅式

    • 从静态存储区域分配,内存在程序编译的时候就已经分配好,这块内存在程序的整个运⾏期间都存在,如全局变量,static变量。
    • 在栈上创建,在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。
    • 从堆上分配(动态内存分配),程序在运⾏的时候⽤malloc或new申请任意多少的内存,程序员负责在何时⽤free或delete释放内存。动态内存的⽣存期⾃⼰决定,使⽤⾮常灵活。
  • 如果是带有⾃定义析构函数的类类型,⽤ new [] 来创建类对象数组,⽽⽤ delete来释放会发⽣什么?

    • class A {};
      A* pAa = new A[3];
      delete pAa;
      
    • 那么 delete pAa; 做了两件事: 1. 调⽤⼀次 pAa 指向的对象的析构函数; 2. 调⽤ operator delete(pAa);释放内存

    • 这⾥只对数组的第⼀个类对象调⽤了析构函数,后⾯的两个对象均没调⽤析构函数,如果类对象中申请了⼤量的内存需要在析构函数中释放,⽽你却在销毁数组对象时少调⽤了析构函数,这会造成内存泄漏。直接释放pAa指向的内存空间,这个总是会造成严重的 段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地⽅减去 4 个字节的地⽅。你应该传⼊参数设为那个地址!

  • 内存顺序模型是为了解决多线程程序中的内存一致性和可见性问题而引入的。在多线程环境下,不同线程可能同时访问和修改共享的内存,这会引发一系列并发性问题,如竞态条件、数据竞争等。内存顺序模型的目的是通过定义不同操作之间的执行顺序和可见性规则,来保证多线程程序的正确性和可预测性。主要原因如下:

    • 多线程并发问题: 在多线程程序中,线程之间可能并发地读取和写入共享内存,导致数据不一致和不可预测的行为。
    • 编译器和处理器优化: 编译器和处理器可能会对代码进行优化,例如重排指令以提高性能。这些优化可能会导致操作的执行顺序与代码中的顺序不一致,从而引发问题。
    • 硬件内存模型: 不同的计算机体系结构有不同的硬件内存模型,即不同的读写操作在不同的条件下可能表现出不同的行为。
    • 数据依赖性: 在某些情况下,某个操作的结果可能会影响后续操作的执行。内存顺序模型可以帮助定义这种数据依赖性。
  • 有人可能会说,我可以用锁来保证顺序,为什么还要设计内存顺序模型呢?虽然锁(比如互斥锁)在多线程编程中是一种常见的同步机制,用于保护共享资源,但锁并不能解决所有的并发性问题,而且在某些情况下使用锁可能会引入性能问题。内存顺序模型的设计是为了在不同线程之间定义操作的执行顺序和可见性规则,以解决锁无法解决的一些问题,同时在一些情况下也可以提高性能

    • 细粒度同步: 锁通常是用于保护共享资源的,但有时候我们需要更细粒度的同步,比如在不涉及共享资源的情况下也需要保证操作的顺序和可见性。
    • 原子操作: 内存顺序模型通过定义原子操作的执行顺序和可见性,可以在不使用锁的情况下确保操作的正确执行。这在一些场景下可以避免锁带来的性能开销。
    • 锁的开销: 锁在某些情况下可能引入较大的性能开销,特别是在高并发环境中。内存顺序模型提供了一种更轻量级的同步机制,可以在一些情况下取代锁,提高性能
    • 编译器和处理器优化: 编译器和处理器对代码进行优化时可能会引入指令重排,这可能会导致锁保护下的共享资源出现问题。内存顺序模型通过规定操作的执行顺序,可以避免这种问题。
    • 原子操作的组合: 内存顺序模型的原子操作可以灵活地组合,以实现更复杂的同步和顺序要求,而不必仅仅依赖于锁。
    • 总之,内存顺序模型和锁在多线程编程中都有其适用的场景。锁通常用于保护共享资源的访问,而内存顺序模型则用于定义操作的执行顺序和可见性,以确保多线程程序的正确性。在多线程编程中,根据具体需求和性能要求,可以选择合适的同步机制。如果想理解内存顺序,首先要理解两个东西:同一线程中,谁先执行,谁后执行;不同线程中,切换内存的时是否会及时的把依赖数据带过去,对另一个线程可见
  • C++ 标准库中定义了六种内存顺序模型,用于控制多线程程序中不同操作之间的执行顺序和可见性。这些内存顺序模型通过枚举值表示,从“宽松”到“严格”的次序分别是:

    • std::memory_order_relaxed 这是最轻量级的内存顺序模型。它不会引入任何额外的同步开销,只保证操作在时间上的顺序是正确的。即使没有明确的同步操作,也不会改变其他线程看到的操作结果。
    • std::memory_order_consume 在 C++11 中引入,但在 C++20 中被弃用。它主要用于处理数据依赖关系,但在实际中难以实现,已经不推荐使用。
    • std::memory_order_acquire 在执行当前操作之前,确保所有前面的读操作都完成。它提供了一种读操作同步的保证,确保读操作的结果在后续操作中是可见的。
    • std::memory_order_release 在执行当前操作之后,确保所有后面的写操作都不会重排到当前操作之前。它提供了一种写操作同步的保证,确保写操作的结果对其他线程是可见的。
    • std::memory_order_acq_relmemory_order_acquirememory_order_release 的组合。它同时提供了读和写操作的同步保证,适用于需要同时保证读写操作同步的情况。
    • std::memory_order_seq_cst 是最严格的内存顺序模型,提供了全局的、顺序一致的保证。它确保所有操作按照一个全局的顺序执行,不会引入重排,也保证了最高级别的可见性
    • 编译器和处理器对代码进行优化时可能会引入指令重排,这可能会导致锁保护下的共享资源出现问题,内存顺序和原子操作的引入,是为了无锁的并发编程,提高并发编程的效率
  • ⽆副作⽤编程:存在⼀个函数,传⼀个参数x进去,⾥⾯进⾏⼀系列的运算,返回⼀个y。中间的所有过程都是在栈中进⾏修改

  • 有副作⽤编程:⽐如在⼀个函数运⾏的过程中对全局变量进⾏了修改或在屏幕上输出了⼀些东⻄。此函数还有可能是类的成员⽅法,在此⽅法中如果对成员变量进⾏了修改,类的状态就会发⽣改变

  • 在多线程情况下的有副作⽤编程:在线程1运⾏的时候对成员变量进⾏了修改,此时如果再继续运⾏线程2,此时线程2拥有的就不是这个类的初始状态,运⾏出来的结果会收到线程1的影响。解决办法:将成员⽅法设为const,此时就可以放⼼进⾏调⽤

  • 信号量(semaphore)是操作系统用来解决并发中的互斥和同步问题的一种方法。信号量是一个与队列有关的整型变量,你可以把它想象成一个数后面拖着一条排队的队列。信号:简单来说就是消息,是由用户、系统或者进程发送给目标进程的信息,用来通知目标进程某个状态的改变或系统异常,对应的是异步的场景。可以当事件来⽤,只有有信号和⽆信号两种状态,⼀次只能被⼀个线程所持有。

    • 初始创建信号量,并且⼀开始将其置位成⽆信号状态 std::binary_semaphore sem(0)
    • 线程使⽤acquire()⽅法等待被唤醒
    • 主线程中使⽤release()⽅法,将信号量变成有信号状态
  • counting_semaphore:⼀次可以被很多线程所持有,线程的数量由⾃⼰指定

    • 创建信号量,指定⼀次可以进⼊的线程的最⼤数量,并在最开始将其置位成⽆信号状态:std::biinary_semaphore<8> sem(0);
    • 主线程中创建10个线程,并且这些线程全部调⽤acquire()⽅法等待被唤醒。但是主线程使⽤release(6)⽅法就只能随机启⽤6个线程。
  • future库:⽤于任务链(即任务A的执⾏必须依赖于任务B的返回值)(⽣产者消费者问题

    • ⼦线程作为消费者,参数是⼀个future,⽤这个future等待⼀个int型的产品:std::future& fut,⼦线程中使⽤get()⽅法等待⼀个未来的future,返回⼀个result
    • 主线程作为⽣产者,做出⼀个承诺:std::promise prom,⽤此承诺中的get_future()⽅法获取⼀个future
    • 主线程中将⼦线程创建出来,并将刚刚获取到的future作为参数传⼊,主线程做⼀些列的⽣产⼯作,最后⽣产完后使⽤承诺中的set_value()⽅法,参数为刚刚⽣产出的产品
    • 此时产品就会被传到⼦线程中,⼦线程就可以使⽤此产品做⼀系列动作 ,最后使⽤join()⽅法等待⼦线程停⽌,但是join只适⽤于等待没有返回值的线程的情况
    • 如果线程有返回值
      • 使⽤async⽅法可以进⾏异步执⾏,参数⼀: 可以选择是⻢上执⾏还是等⼀会执⾏(即当消费者线程调⽤get()⽅法时才开始执⾏)。参数⼆: 执⾏的内容(可以放⼀个函数对象或lambda表达式)。
      • ⽣产者使⽤async⽅法做⽣产⼯作并返回⼀个future
      • 消费者使⽤future中的get()⽅法可以获取产品
  • 常⻅的字符串函数实现

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

    • char* strcpy(char *strDest,const char *strSrc)
      {
          assert((strDest!=nullptr) && (strSrc != nullptr));
          char* address = strDest;
          while((*strDest++ = *strSrc++)!='\0');
          return address;
      }
      
    • strlen():计算给定字符串的⻓度。

    • int strlen(const char* str)
      {
          assert(str!=nullptr);
          int len;
          while((*str)!='\0')
          {
              len++;
          }
          return len;
      }
      
    • strcat():作⽤是把src所指字符串添加到dest结尾处。

    • char* strcat(char* dest,const char* src)
      {
          assert(dest && src);
          char* ret=dest;
          while(*dest)
          {
              dest++;
          }
          while(*dest++ = *src++);
          return ret;
      }
      
    • strcmp():⽐较两个字符串设这两个字符串为str1,str2,若str1 == str2,则返回零;若str1 < str2,则返回负数;若str1 > str2,则返回正数

    • int strcmp(const char* str1,const char* str2)
      {
          assert(str1&&str2);
          while(*str1&&*str2&&(*str1==*str2))
          {
              str1++;
              str2++;
          }
          return *str1-*str2;
      }
      
  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羞儿

写作是兴趣,打赏看心情

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值