C++

  1. C++ 中析构函数可以为纯虚函数吗?

    可以为纯虚函数,如果基类将析构函数设置为纯虚函数,那么基类就会成为抽象类,是不能创建对象的。且如果基类不提供纯虚析构函数的定义的话,使用基类指针指向派生类对象会出现无法解析的外部符号命令错误,所以基类同时也必须提供纯虚析构函数的定义。

  2. 实现#define max(a, b, c)

    #define max(a, b, c) (a > b ? (a > c ? a : c) : (b > c ? b : c))
    
  3. C++ 中引用和指针的区别?

    • 引用是别名,在函数内操作一个通过传引用方式传入的变量,相当于直接操作函数外原本的变量。指针是一个变量,但是不同于一般变量,指针存放的是内存地址。传指针方式传入函数内的指针,只是拷贝外部指针的副本,可以在函数内修改它们共同指向的内存区域,但是却无法在函数内修改函数外指针本身。
    • 声明方式不同,一个通过&,一个通过*
    • 引用一旦绑定不可更改,指针可以更改指向地址。
    • 引用是和绑定变量共享内存的,并不另外为引用开辟一块内存。指针是一个独立的变量,需要占据内存,且可改变它存储的内容。
    • 引用不可以绑定到null值,指针可以直接指向null值。
    • 不可以创建引用数组,可以创建指针数组。int & a[];
  4. 栈和堆的区别?

    • 栈是由系统自动分配的,每次调用函数都需要分配一个栈帧用来存放参数、局部变量、函数返回地址等;堆则是由程序员手动申请和释放,如果不手动释放,则该内存则会一直保留到进程退出为止。当然栈也可以通过alloca函数去动态申请栈内存,但是也是由操作系统去释放,不需要程序员手动释放。
    • 栈的地址由高往低,堆的地址由低往高生长。
    • 空间大小不同,每个进程拥有的栈大小远远小于堆大小。
    • 分配效率不同,栈由操作系统自动分配,会在硬件层面提供支持,比如分配专门的寄存器存放栈的地址等,压栈出栈都有专门的指令。堆内存的申请则是由库函数或运算符来完成,实现机制较为复杂,操作系统有一个记录空闲内存地址的链表,当申请堆内存时,会遍历该链表寻找第一个大于申请空间的内存,然后删除该节点,分配堆空间给用户。由于找到的堆内存块不一定刚好等于申请空间大小,所以多余的内存空间还是会重新放到链表里,频繁的内存申请容易产生内存碎片。显然栈的效率要比堆高。
  5. const和#define区别?(编译阶段、安全性、内存占用)

    • 编译器处理不同,define宏是由预处理器负责,在预处理阶段进行简单的字符替换,并没有类型检查。而const由编译器负责,在编译期和运行期起到作用,有类型信息,可以避免一些类型错误。
    • 存储方式不同,define宏仅仅是在预处理阶段展开,不会分配内存。而const定义变量是分配内存的。
    • const可以节省一些空间。define定义的宏常量展开多少次,在内存里就有多少备份。const定义的只读变量在程序运行过程中只有一份。const定义常量从汇编角度看,只是给出了对应的内存地址,而不是像define给出的是立即数,所以const定义的常量在程序运行过程中只有一份拷贝,因为是全局的只读变量,存在静态区,而define定义的常量在内存中有若干拷贝。
  6. const和static区别?

    • 如果是类内声明了一个const变量和static变量。则const成员变量和static成员变量都不可以在声明的时候初始化,const变量和普通成员变量一样,每个对象都有一份,只不过const变量对于每个对象的声明周期来说是常量,且不同对象是可以有不同的值的,所以必须提供构造函数,并在初始化列表中对const成员变量进行初始化。而static变量则是与对象无关的,即使没有对象,通过类也可以访问static变量,类似全局函数,不过作用域在所在文件内。static变量必须在类外进行初始化。
    • 如果类内声明了const成员函数,和static成员函数,static函数的作用是类作用域内的全局函数,不能访问非静态成员及this指针,且不能声明为virtual。const成员函数主要是防止在函数体内修改成员变量。
  7. c语言中static 函数和普通函数的区别?

    点击阅读

  8. C++内存管理?

    点击阅读

  9. std::unordered_map/std::map的区别?

    • unordered_map内部采用哈希表实现,所有元素是无序的,map内部采用自平衡BST类似红黑树实现,除内置类型外,自定义类型在插入时需要重载比较<运算符帮助每次插入元素时进行比较,所以map中的元素是有序的。
    • unorder_map查询、插入和删除时间平均是O(1),最坏的情况是O(n),map查询log(n),插入和删除是log(n)+恢复平衡的花销。
  10. 介绍一下vector,及如何扩容

    vector是STL中实现的一个容器,类似数组,会开辟一块线性连续的内存,与array不同,其对于空间的运用更灵活(array静态空间,一旦数据满了可能得程序员手动去执行:配置新空间->数据移动->释放旧空间)。vector一旦就空间装满,就会自动扩充空间(不论多大,当然不太可能每新增一个元素就去扩展空间)。

    为了降低空间配置的速度成本,vector实际配置的大小比用户分配的大小要更大一些,以备扩充使用,即容量的概念。vector会以startfinish两个迭代器来分别指向连续内存中目前已被使用的范围,以end_of_storage指向整块连续空间(含备用空间)的尾端。

    当我们以push_back将新元素插入尾端时,该函数首先检测是否还有备用空间,如果有直接在备用空间上构造元素,并调整finish,如果没有空间,就扩充空间(分配空间->移动数据->释放原空间)。动态增加大小并不是在原空间之后接着新空间,因为无法保证原空间之后还有足够空闲的空间可供使用,而是以原大小两倍在另外地址开辟一块新空间,然后拷贝原内容。对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了,务必小心。

    参考《STL源码剖析》

    image.png
  11. 什么情况下出现野指针

    实际上,只要是指针指向的内存不是你想要的地址,都属于野指针。不一定是非法的。比如,声明一个指针未初始化,它默认可能并不是nullptr,所以即使判断是不是nullptr也没用;或者是一个指针指向的对象的生命周期结束了,但是指针本身生命周期还未结束,则继续使用的时候,指针指向的地址虽然未变化,但是地址所指的对象已经不在了。等等。

    int *ptr = new int;
    //... use ptr to do something
    delete ptr;
    // ptr = nullptr;
    
    // 在释放了 ptr 指向的内存之后,没有给ptr指向新的内存,又继续使用了 ptr,所以最好释放之后指向 nullptr
    *ptr = 3;
    
    // 指向了 nullptr 之后,在使用 ptr 时可以增加一个判断,提升程序鲁棒性
    if (ptr) *ptr = 3;
    
  12. new和malloc区别

    • 二者都是在堆上动态开辟内存,返回指向新内存的指针。new返回指定类型指针,而malloc返回void *,需要自己强制转化

    • new是操作符,malloc是库函数

    • 释放内存方式不一样deletefree
    • malloc只是开辟一块内存,并不做任何初始化,new会调用相应的构造函数完成初始化
  13. static_cast、dynamic_cast、const_cast、reinterpret_cast、bad_cast

    • static_cast:用于非多态类型的转换、不执行运行时类型检查、通常用于数值类型转换、在类结构层次中,子类转父类(向上)安全,父类转子类不安全(子类可能存在不在父类的字段或方法)
    • dynamic_cast:用于多态类型转换、执行运行时类型检查、只适用于指针或引用、对不明确的指针将转换失败(返回nullptr),但不引发异常、可以在整个类结构层次移动指针,包括向上和向下
    • const_cast:用于删除constvolatile__unaligned特性(如将const int转化为int
    • reinterpret_cast:用于位的简单重新解释、滥用reinterpret_cast可能很容易带来风险,除非所需转化本身是低级别的,否则应使用其他强制转化运算符、允许将任何指针转化为任何其他指针类型、也允许将任何整数类型转换为任何指针类型以及反向转换、不能丢掉constvolatile__unaligned特性、一个实际用途是在哈希函数中,通过让两个不同的值几乎不宜相同的索引结尾的方式将值映射到索引
    • bad_cast:由于强制转化为应用类型失败,dynamic_cast运算符引发bad_cast异常:
       try {
          A& ref = dynamic_cast<A&>(var);
       }
       catch (bad_cast e) {
          cout << b.what() << endl;
       }
      
  14. C++源文件编译过程

    1. 预处理阶段:预处理器根据字符#开头的命令,修改原始的程序。比如#include直接告诉预处理器读取引入的头文件内容,并直接插入到程序文本中;#define定义的弘,预处理器会在使用的地方直接替换为定义的值。(预处理后的文件通常以.i作为文件拓展名)
    2. 编译阶段:编译器将文本文件(预处理后的.i文件)翻译成文本文件.s。它包含的是翻译后的汇编语言。
    3. 汇编阶段:汇编器将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中,其是一个二进制文件。
    4. 链接阶段:假设程序调用了其它库,则会去连接其它库的目标文件,将其包含到我们的程序里。比如调用了C库函数里的printf,该函数存在于一个名为printf.o的单独的预编译好了的目标文件,需要将其合并过来。链接器负责处理这种合并,最终生成一个可执行文件,可以被加载到内存,由系统执行。
  15. 动态链接库和静态链接库的区别,各自的优缺点

    1. 静态库:编译器提供一种机制,将所有相关的目标模块打包成一个单独的文件,称为静态库,它可以作为链接器的输入,当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。在linux系统中,静态库文件以一种存档的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。 假设标准C函数都在一个单独的可重定位目标文件中,如libc.o中,那么在使用到了C库函数时,链接时需要链接到我们的可执行文件中:gcc main.c /usr/libc.o,虽然对于程序员很便利,每次只需链接libc.o即可,但是如果库函数很多,而你程序只使用少量库函数,则需要将其全部链接,造成对磁盘空间的很大的浪费,糟糕的是,程序运行时会将这些函数副本放在内存中,也是对内存极度的浪费,还有一个缺点就是,一旦其中某个库函数发生了变动,库开发人员需要对整个源文件重新编译,非常耗时。 或者假设为每一个库函数单独创建一个独立的可重定位目标文件,然而,这样之就需要程序员显示链接合适的目标模块到它们的可执行文件中,这是非常耗时且容易出错的,gcc main.c /usr/bin/printf.o /usr/bin/scanf.o ...。 静态库的提出就是为了解决这些缺点的,将相关的函数编译成独立的目标模块,然后将它们封装成一个单独的静态库文件。程序员只需链接该静态库,即可使用里面的库函数。链接时,链接器只复制被程序引用的目标模块,这就减少了可执行文件在磁盘和内存中的大小,另一方面,程序员只需要包含较少的库文件名称即可。如gcc main.c /usr/lib/libc.a
    2. 共享库:静态库的一个缺点就是当静态库经常需要更新时,每次更新都需要重新将你的程序和新的静态库进行重新链接。另一个问题就是,几乎每个C程序都会使用标准库函数里的函数,如printfscnaf这种,在运行时,这些函数会被复制到每个进程的文本段中。如果一个操作系统上运行成百上千个进程,每个进程都对这些库函数复制到其文本段中的话,这时对稀缺的内存系统资源极大的浪费。共享库这时致力于解决静态库缺陷的一个产物,共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,由动态链接器的程序来完成。共享库也成为共享目标,在linux系统中通常以.so后缀表示。 假设里的你的程序使用了libvector.so里的东西,则链接时需要指定需要动态链接的库gcc -o prog main.c ./libvector.so,创建了一个可执行目标文件prog,运行时可以和libvector.so链接。在生产可执行目标文件prog时,并没有任何libvector.so的代码和数据节真的被复制到可执行文件prog中,反之,链接器复制了一些重定位和符号表信息,使得在运行时可以解析对libvector.so中代码和数据的引用。 共享库的一个主要目的就是允许多个进程共享内存中相同的库代码,因而节约宝贵的内存资源。那么多个进程如何共享程序的一个副本呢?现代系统以这样的一种方式编译共享库,使得他们可以加载到内存任何位置而无需链接器修改。创建共享库时需要指定-fpic表示创建位置无关代码,被编译为位置无关的代码的共享库可以加载到任何地方,也可以在运行时被多个进程共享。
  16. i++ 和 ++i 的区别 直观的区别就是i++会在使用i的值之后才对进行加 1,++i对在使用i值之前就进行加1操作.

      int a[];
      int i = 3, j = 3;
      int m = a[i++], n = a[++j];
      // m = a[3], n = a[4],  i = 4, j = 4
    

    从汇编角度去看的话, i++++i多一步操作就是用额外的一个寄存器保存i原先的值。但是,如果只是单纯的写i++++i,实际很多编译器都将优化的,二者是一样的。

results matching ""

    No results matching ""