面经汇总 C++基础

面经汇总 C++基础

Posted by zhaostu4 on November 28, 2019

T:2019/11/28 W:四 17:0:11 [HTML]: @TOC

基本语言

1. 说一下static关键字的作用

  1. 全局静态变量
    • 在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
    • 内存中的位置:静态存储区,在整个程序运行期间一直存在。
    • 初始化:未经初始化的全局静态变量会被自动初始化为0(对于自动对象,如果没有显示初始化,会调用零参数构造函数,如不存在则编译失败);
    • 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
  2. 局部静态变量
    • 在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
    • 内存中的位置:静态存储区
    • 初始化:未经初始化的全局静态变量会被自动初始化为0(对于自动对象,如果没有显示初始化,会调用零参数构造函数,如不存在则编译失败);
    • 作用域:作用域仍为局部作用域,
      • 当定义它的函数或者语句块结束的时候,作用域结束。
      • 但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

    • C语言中局部静态变量不能使用变量进行初始化的, 因为它在编译期间就被编译器初始化了.
    • C++因为引入了对象,必须调用构造函数,所以编译器把对局部静态变量的初始化推迟至第一次调用之前,也就是可以使用变量进行初始化.

  3. 静态函数
    • 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用
    • 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
    • warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
  4. 类的静态成员
    • 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。
    • 因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
  5. 类的静态函数
    • 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
    • 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。*如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);*
    • 不能被virtual修饰,静态成员函数没有this 指针,虚函数的实现是为每一个对象分配一个vptr 指针,而vptr 是通过this 指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function

2. C++C的区别

  • 设计思想上:
    • C++是面向对象(封装, 继承, 多态)的语言,而C是面向过程的结构化编程语言
    • C中的函数编译时不会保留形参列表, 也不能重载; 而C++中的函数在编译时会保留形参列表, 有重载
  • 语法上:
    • C++具有封装继承多态三种特性
    • C++相比C,增加许多类型安全的功能,比如强制类型转换、
    • C++支持范式编程,比如模板类、函数模板等
  • 具体的:
    • 动态管理内存的方法不一样: malloc/freenew/delete
    • struct
      • C中: struct是自定义数据类型; 是变量的集合, 不能添加拥有成员函数; 没有访问权限控制的概念; 结构体名称不能作为参数类型使用, 必须在其前加上struct才能作为参数类型
      • C++中: struct是抽象数据类型, 是一个特殊的类, 可以有成员函数, 默认访问权限和继承权限都是public, 结构体名可以作为参数类型使用
    • C语言没有引用的概念, 更没有左值引用, 右值引用
    • C语言不允许只读数据(const修饰)用作下标定义数组, C++允许
    • C语言的局部静态变量初始化发生于编译时,所以在函数中不能使用变量对局部静态变量进行初始化, 而C++因为增加了对象的概念,而对象需要调用构造函数进行初始化,所以编译器将局部静态变量的初始化推迟至该变量使用之前,也就是说可以使用变量来初始化局部静态变量。


3. c++中四种cast转换

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

  • const_cast:
    • 用来移除constvolatile 属性。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用
    • 如果对一个指向常量的指针,通过const_cast移除const属性, 然后进行修改, 编译通过,但是运行时会报段错误
  • static_cast: 静态类型转换(不能移除const/volatile属性)是最常看到的类型转换, 几个功能.
    • 内置类型之间的转换, 精度耗损需要有程序员把握
    • 继承体系中的上下行转换(上行:子类转父类,安全转换; 下行:父类转子类, 不安全转换)
    • 指针类型转换: 空指针转换成目标类型的空指针, 把任何类型转换成void 类型
  • dynamic_cast: 主要用在继承体系中的安全向下转型
    • 它能安全地将指向基类的指针/引用转型为指向子类的指针/引用, 转型失败会返回null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。
    • dynamic_cast 会利用运行时的信息(RTTI)来进行动态类型检查,因此dynamic_cast 存在一定的效率损失。
    • 而且dynamic_cast进行动态类型检查时, 利用了虚表中的信息, 所以只能用于函数虚函数的类对象中.
  • reinterpret_cast 强制类型转换,非常不安全
    • 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。


4. C/C++ 中指针和引用的区别

  • 初始化:
    • 引用在定义的时候必须进行初始化,并且不能够改变
    • 指针在定义的时候不一定要初始化,并且指向的空间可变
  • 访问逻辑不同:
    • 通过指针访问对象, 用户需要使用间接访问
    • 通过引用访问对象, 用户只需使用直接访问, 编译器负责将其处理为间接访问
  • 运算结果不同:
    • 自增运算结果不同
    • sizeof运算的结果不同
    • 下标运算:
      • 指针通过下标运算结果是指针所指值为基地址加上偏移, 且基地址可变.
      • 引用通过下标运算结果是引用的是数组才能有这个操作.
    • 函数参数:
      • 传指针的实质是传值,传递的值是指针内储存的变量地址;
      • 传引用的实质是传地址,传递的是变量的地址。
    • 多级: 有多级指针,但是没有多级引用,只能有一级引用。


5. 给定三角形ABC和一点P(x,y,z),判断点P是否在ABC内,给出思路并手写代码

  • 根据面积法,如果P在三角形ABC内,那么三角形ABP的面积+三角形BCP的面积+三角形ACP的面积应该等于三角形ABC的面积。算法如下:
    • 三角形求面积的原理为: 向量的外积表示由此两个向量构成的四边形的面积 S = a x b
        #include <iostream>
        #include <math.h>
        using namespace std;
        #define ABS_FLOAT_0 0.0001
        struct point_float
        {
        float x;
        float y;
        };
        /** @brief 计算三角形面积 **/
        float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2)
        {
        point_float AB,   BC;
        AB.x = pt1.x - pt0.x;
        AB.y = pt1.y - pt0.y;
        BC.x = pt2.x - pt1.x;
        BC.y = pt2.y - pt1.y;
        return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
        }
        /** @brief 判断给定一点是否在三角形内或边上 **/
        bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D)
        {
        float SABC, SADB, SBDC, SADC;
        SABC = GetTriangleSquar(A, B, C);
        SADB = GetTriangleSquar(A, D, B);
        SBDC = GetTriangleSquar(B, D, C);
        SADC = GetTriangleSquar(A, D, C);
        float SumSuqar = SADB + SBDC + SADC;
        if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0))
        {
            return true;
        }
        else
        {
            return false;
        }
        }
      

6. c++中的smart pointer四个智能指针

  • 为什么要使用智慧指针
    • 智慧指针的作用是管理指针,因为在堆上申请的内存空间,是需要手动释放的,如果未释放就会造成内存泄漏,而智慧指针的使用可以很大程度的避免这个问题.
    • 因为使用智慧指针管理内存的本质是: 栈对象管理堆内存, 而栈对象是超出作用域或者程序意外终止,都会自动调用析构函数,而智慧指针的析构函数会自动释放资源,从而避免内存泄漏.
    • RAII
  • auto_ptr: 采用所有权模式, 但是其允许所有权剥离,所以有内存崩溃的风险, 在c++11中已经放弃了
  • unique_ptr:采用独占式拥有,保证同一时间只有一个智慧指针可以指向该对象. unique_ptr不允许所有权剥离,除非本身是临时变量或者使用move指令.
  • shared_ptr:采用共享式拥有,多个共享指针可以指向相同的对象,该对象和其相关的资源会在最后一个共享指针被销毁时释放. 注意shared_ptr有交叉引用相互锁死的问题,即两个对象相互持有对方的共享指针, 造成用不释放的问题
  • weak_ptr: 是一种不控制对象生命周期的智慧指针,他是共享指针的附属品, 主要是为了避免shared_ptr的死锁问题,具体表现为weak_ptr只能从shared_ptr或另一个weak_ptr构造,持有它不会造成shared_ptr的引用计数增加,以及并不能通过weak_ptr直接访问推向.

7. 怎么判断一个数是2的倍数, 怎么求一个数中有几个1,说一下思路并手写代码

  • 判断一个数是不是2的倍数,可以判断该数的二进制末位是不是0;
    • a%2==0 或者 a & 1 ==0
  • 求一个数中1的个数,可以直接除10取余


8. 数组和指针的区别

  • 参考:数组和指针的区别与联系(详细)
  • 概念:
    • 数组:数组用于储存多个相同类型数据的集合。
    • 指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。
  • 区别:
    • 赋值: 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
    • 储存方式:
      • 数组:数组在内存中开辟一块连续的内存空间。可以根据下标进行访问, 它不在静态区,就在栈上
      • 指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
    • sizeof:
      • sizeof(数组名): 数组所占存储空间的内存
      • sizeof(指针名):在32位平台 都是4(无论指针的类型), 在64位平台都是8(无论指针的类型)。
    • 数组传参时,会退化为指针(首元素的地址), 而不会进行copy, 如果是二维数组,需要指定最后一个的维度

指针 数组
保存数据的地址 保存数据
间接访问数据,首先获得指针的内容,然后将其作为地址,再从该地址获取数据 直接访问数据
通常用于动态数据结构 通常用于固定数目且数据类型相同的元素
通过Malloc分配内存,free释放内存 隐式的分配和删除
通常指向匿名数据,操作匿名函数 自身即为数据名

9. 野指针是什么?

  • 野指针就是一个指向已删除的对象 或者 为申请访问受限内存区域的指针(指针未初始化)

10. 智慧指针内存泄漏的情况

  • 当两个对象使用shared_ptr相互引用,就会使计数器失效,从而导致内存泄漏,解决方案是使用weak_ptr打破循环引用

11. 为什么析构函数必须是虚函数? 为什么C++默认的析构函数不是虚函数

  • 将可能被继承的基类的析构函数设置为虚函数,可以保证当我们使用基类指针指向派生类对象时,依旧可以正常的delete, 不至于引起内存泄漏.
  • 在类中定义虚函数会增加额外的开销,包括虚函数表和虚表指针,对于非基类而言,构造函数如果设置为虚函数会浪费内存.

12. 说一下fork函数

  • Fork函数: 创建一个和当前进程一样的进程
  • Fork调用
    • 成功调用fork后会创建一个和当前进程几乎一模一样的新进程,两个进程都会继续运行.
    • 在子进程中fork返回值为0, 在父进程中fork返回值为子进程pid. 如果出现错误会返回负值.
    • 现代系统通常采用写时复制, 如果子父进程的数据未发生修改不会立即复制
  • Fork常见用法
    • Fork创建一个进程之后,然后使用exec载入一个二进制映像,替换掉当前进程的映像. 这种情况下,派生的子进程,会执行一个新的二进制可执行文件的映像.


13. C++ 中的析构函数的作用

  • 析构函数名与类名相同,只是在函数名前增加了取反符号~以区别于构造函数,其不带任何参数, 也没有返回值. 也不允许重载.
  • 析构函数与构造函数的相反, 当对象生命周期结束的时候,如对象所在函数被调用完毕时,析构函数负责结束对象的生命周期. 注意如果类对象中分配了堆内存一定要在析构函数中进行释放.
  • 和拷贝构造函数类似,如果用户未定义析构函数, 编译器并不是一定会自动合成析构函数, 只有在成员变量或则基类拥有析构函数的情况下它才会自动合成析构函数.
  • 如果成员变量或则基类拥有析构函数, 则编译器一定会合成析构函数, 负责调用成员变量或则基类的析构函数, 此时如果用户提供了析构函数,则编译器会在用户析构函数之后添加上述代码.
  • 类析构的顺序为: 派生类析构函数, 对象成员析构函数, 基类析构函数.

14. 静态函数与虚函数的区别

  • 静态函数在编译的时候已经确定运行时机了, 而虚函数则时运行时动态绑定的, 因为虚函数使用虚函数表机制进行映射,它只能是运行时确定的. 注意只有通过指针调用虚函数,虚函数表映射机制才会生效, 通过对象进行调用是不会生效的

15. 重载和覆盖以及隐藏

  • C++中重载、重写(覆盖)和隐藏的区别
  • 重载: 同一可访问区域内声明了多个具有不同参数列表的同名函数, 根据参数列表确定那个函数被调用, 重载不关心函数返回类型.
  • 覆盖/重写: 是指派生类中重新定义了基类中的virtual函数. 其函数名,参数列表,返回值类型,所有都与基类中被重写的函数一致。派生类对象通过派生类指针或则基类指针调用时都会调用派生类的重写函数。
  • 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,只要函数名相同,基类函数都会被隐藏. 不管参数列表是否相同。

  • 重载和重写的区别: 1) 范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。 2) 参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。 3) virtual区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
  • 隐藏和重写,重载的区别: 1) 与重载范围不同:隐藏函数和被隐藏函数在不同类中。 2) 参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;

16. strcpystrncpy以及strlen

  • strcpy是字符串拷贝函数,
    • char* strcpy(char* dest, const char* src);
    • srcdest所指内存区域不可以重叠且dest必须预分配足够的空间。
    • src逐字节的拷贝到dest, 直到遇到\0结束,因为没有指定长度,可能会导致拷贝越界, 造成缓冲区溢出漏洞,安全版本是strncpy函数
  • char *strncpy(char *dest, const char *src, int n)
    • src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中,并返回被复制后的dest
    • 如果src的长度小于n个字节,则用NULL填充dest直到满n个字节。
    • strncpy要手工增加结束符
  • strlen 函数计算字符串的长度的函数, 返回从开始到\0之间的字符个数, 这个函数也有末尾没有\0的风险

17. 理解的虚函数和多态

  • 多态的实现主要分为动态多态和静态多态, 静态多态就是重载, 在编译的时候就已经确定了, 动态多态是用虚函数机制实现的, 在运行期间动态绑定. 例如如果父类中定义了虚函数,而派生类重写了此函数,此时通过一个指向派生类对象的父类指针调用此函数式, 子类中重写的函数将被调用.
  • 虚函数的实现: 在存在虚函数的类中会存在一个指向虚函数表的指针, 虚函数表中存放了虚函数的地址,当子类继承父类时也会继承其虚函数表, 当子类重写父类的虚函数时, 会将其继承的虚函数表中的地址替换为重写的函数地址. 使用虚函数,会增加访问内存的开销, 降低效率.

18. ++ii++的区别

  • ++i先自增1, 再返回. 函数定义式无形参, 实现过程中不会有临时变量生成,
  • i++先返回i,再增加1. 函数定义式有一形参, 实现过程中由于需要返回自增前的量,所以需要一个临时变量.

19. ++ii++的实现

// ++i
int & int::operator++(){
	*this += 1;
	return *this;
}
// i++
int & int::operator++(int){
	int t = *this;
	++ *this;
	return t;
}

20. 在main()之前执行前运行

  • GCC编译器
      // 在main之前
      __attribute((constructor)) void before_main(){
          printf("befor\n");
      }
      // 在main之后
      __attribute((deconstructor)) void after_main(){
          printf("befor\n");
      }
    

21. 修改一个字符使得代码输出20hello

  • 原代码如下:
      for(int i=0; i< 20; i--){
          cout <<"hello" <<endl;
      }
    
  • 修改如下:
      for(int i=0; i+20; i--){
          cout <<"hello" <<endl;
      }
    
  • !!!! 强制类型转换 !!!
  • 常值转bool的基本规则为 !=0,
  • 指针转bool的基本围着为!=nullptr

22. 说一下shared_ptr的实现

  • 内存管理函数: 构造函数, 拷贝构造函数, 赋值操作, 析构函数, 取计数器
  • 基本功能函数: 取原始指针, 成员访问操作, 取对象操作, 加n操作, 相减操作
      // 自己写的
      template<typename T>
      class shared_ptr{
      private:
          T *ptr;
          long *use_count;
      public:
          shared_ptr(T* p);
          shared_ptr(const shared_ptr<T> & orig);
          ~shared_ptr();
          shared_ptr<T>& operator=(const shared_ptr<T> &orig);
          T operator*();
          T* operator->();
          T* operator+(int i);
          unsigned int operator-(shared_ptr<T> &t1, shared_ptr<T> &t2);
          long getcount();
      }
      template<typename T>
      shared_ptr<T>::shared_ptr(T& *p){
          ptr = p;
          try{
              use_count = new long(1);
          }catch(...){
              delete ptr;
              ptr = nullptr;
              p=nullptr;
              use_count = nullptr;
          }
      }
      template<typename T>
      shared_ptr<T>::shared_ptr(const shared_ptr<T> &orig){
          use_count = orig->use_count;
          ptr = orig->ptr;
          ++(*use_count);
      }
      template<typename T>
      shared_ptr<T>& shared_ptr<T>::operator=(const shared_ptr<T> &orig){
          if(--(*use_count)==0){
              delete ptr;
              ptr = nullptr
              delete use_count;
              use_count = nullptr;
          }
          ++(*orig->use_count);
          ptr = orig->ptr;
          use_count = orig->use_count;
          return *this;
      }
    
      template<typename T>
      shared_ptr<T>::~shared_ptr(){
          if(--(*use_count)==0){
              delete ptr;
              ptr = nullptr;
              delete use_count;
              use_count = nullptr;
          }
      }
      template<typename T>
      T shared_ptr<T>::operator*(){ return *ptr;}
    
      template<typename T>
      T* shared_ptr<T>::operator->(){ return ptr;}
    
      template<typename T>
      T* shared_ptr<T>::operator+(int i){	return ptr + i;}
    
      template<typename T>
      unsigned int shared_ptr<T>::operator-(shared_ptr<T> &t1, shared_ptr<T> &t2){ return t1->ptr - t2->ptr;}
    
      template<typename T>
      long shared_ptr<T>::getcount(){return *use_count;}
    
    

23. 字面值常量和左右值

  • 简述四行代码的区别
      const char* arr = "123"; // "123" 为字符串类型字面值, 其储存于常量区, 其值不可修改, arr表示一个指向字符串类型的const指针, 如果试图通过此指针修改字符串的指,编译器会组织这一行为,导致编译失败
      char * brr = "123"; // "123" 为字符串类型字面值, 其储存于常量区, 其值不可修改, brr表示一个指向字符串类型的指针, 这里潜在的逻辑错误, 当我们试图通过brr修改所指向的字符串时, 编译器并不会阻止这一行为, 可编译通过, 但是运行时候,可能发生 DEADLYSIGNAL(致命错误)
      const char crr[] = "123" // 声明了一个以"123"为初值的常量数组, 通常情况下应该是存在于栈区, 但是使用了const修饰,编译器可能会将其放在常量区
      char drr[] = "123"; // 声明了一个以"123"为初值的字符串数组, 应该是储存于栈区, 可通过drr对数组进行修改
    


24. C++怎么定义常量,以及常量所在的位置

  • 对于局部变量, 常量储存于栈区,
  • 对于全局变量, 常量储存于静态储存区
  • 对于字面值, 常量存放在常量储存区.

  • 这个才正确: const int * p 常量指针,指向常量的指针; int* const p 指针常量,指针本身是常量;

  • 关于Top-Level constlow-level const的分别

25. const 修饰成员函数的目的

  • const 修饰成员函数表示设计者承诺调用此函数不会对修改对象任何内容,
  • const成员函数内部获取到的this带有const属性,因此不能对对象成员进行修改,也不能调用非const函数
  • const成员函数本身即可以由const对象调用, 也可以由非const对象调用
  • 事实上如果确认不会对对象做更改,就应该给函数加上const限定, 这样无论const对象还是普通对象都可以调用该函数.
  • 注意, 尽管const函数成员函数的设计者承诺不修改任何对象成员,但是事实上他是可以通过const_cast移除this指针const属性,进而调用非const函数或者修改对象成
  • mutable

26. 同时定义两个同名同参数的函数, 区别仅在于是否带const, 会出问题么?

  • 不会出问题,相当于函数重载.
  • 通过const指针或对象将调用const修饰的函数
  • 通过非const指针或对象将调用无const修饰的函数


27. 说一下隐式变换

  • 首先, 对内置类型, 低精度变量会给高精度变量赋值时会发生隐式类型转换,
  • 再则, 继承体系中从派生类对象到基类对象的转换
  • 其次, 对于只存在单参数构造函数除了一个参数之外, 其他所有参数都带缺省值的类, 函数调用可以直接传递该参数, 编译器会自动调用其构造函数生成临时变量.
  • 对于刚说的这种对象的隐式构造, 可以通过增加explicit关键之阻止.

28. C++函数栈空间的最大值

  • Windows默认为1M, 可以调整,
  • Linux默认为10M


29. 说一下extern "C"

  • C++调用C函数需要extern C, 因为C语言没有函数重载
    • 由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中, 而不仅仅是函数名;
    • C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

30. new/deletemalloc/free的区别

  • 首先: new/deletec++关键字, 而malloc/freec语言的库函数
  • 其次:
    • malloc/free需要指明申请的空间的大小, 且并不负责构造和析构
    • new/delete是在malloc/free上增加了一层封装, 他们根据类的确定所需要的内存大小并调用malloc分配空间, 还负责调用构造和析构函数
    • new 返回的指针有类型
    • new 失败了会抛出bad_malloc异常, 而malloc只会返回一个null指针

31. RTTI

  • 运行时类型检查, 在C++层面主要体现在dynamic_casttypeid, 虚函数表中存放了一个type_info的指针.
  • 对于虚函数的类型, typeiddynamic_cast都会查询type_info

32. 虚函数表具体怎么实现的运行时多态

  • 对于存在虚函数的类对象中总是保存着一个指向虚函数表的指针, 子类若重写父类的虚函数, 虚函数表中该函数的地址会被替换.
  • 当通过对象指针访问虚函数的时候, 实际上是一个间接调用过程.
  • 首先通过虚函数指针访问虚函数表,然后从虚函数表中得到得到调用函数的入口地址,
  • 然后再通过此地址调用函数

33. C语言是怎么进行函数调用的?

  • 参考: C语言函数调用栈(一)
  • 每个运行中的函数都对应于一个栈帧.
  • 函数调用时候的入栈顺序为:
    • 实参倒序(N-1)入栈,
    • 主调函数返回地址入栈,
    • 主调函数栈基指针入栈
    • 设置当前栈顶为被调函数栈底
    • 被调函数局部变量顺序(1-N)入栈
  • 在被调函数内栈基(向上)分别为返回地址, 1-N的实参

  • 函数的返回值用于初始化在调用函数时创建的临时对象: 1) 返回值为非引用类型:会将函数的返回值复制给临时对象。 2) 返回值为引用类型:没有复制返回值,返回的是对象本身。(但是不能返回局部变量的引用, 可以是主函数以引用方式传递的对象,或则是堆对象的引用-堆对象的引用可能会造成内存溢出)

  • 关于返回值:
    • 如果 返回值 <= 4字节,则返回值通过寄存器eax带回。
    • 如果 4< 返回值 <=8字节,则返回值通过两个寄存器eaxedx带回。
    • 如果 返回值 > 8字节,则返回值通过产生的临时量带回。

34. C语言参数入栈顺序

  • 倒序入栈,参考上一个问题

35. C++如何处理返回值

  • 函数的返回值用于初始化在调用函数时创建的临时对象: 1) 返回值为非引用类型:会将函数的返回值复制给临时对象。 2) 返回值为引用类型:没有复制返回值,返回的是对象本身。(但是不能返回局部变量的引用, 可以是主函数以引用方式传递的对象,或则是堆对象的引用-堆对象的引用可能会造成内存溢出)

  • 关于返回值:

    • 如果 返回值 <= 4字节,则返回值通过寄存器eax带回。
    • 如果 4< 返回值 <=8字节,则返回值通过两个寄存器eaxedx带回。
    • 如果 返回值 > 8字节,则返回值通过产生的临时量带回。
int& abc(int a, int b, int c, int& result){
	result = a + b + c;
	return result;
}
// 这种形式也可改写为:
int& abc(int a, int b, int c, int *result){
	*result = a + b + c;
	return *result;
}
// 但是,如下的形式是不可以的:
int& abc(int a, int b, int c){
	return a + b + c;
}


36. C++中拷贝赋值函数的形参能否使用值传递

  • 不能. 编译器会阻止这种使用.
  • 如果这种情况在调用拷贝构造函数的时候,首先会将实参传递给形参,这个过程需要调用拷贝构造函数, 从而构成了循环,最终无法完成拷贝, 而且会栈满终止.

37. mallocnew的区别

  • mallocC语言关键字, 需要指定申请的内存大小,分配之后需要进行强转为指定类型
  • newC++关键字, 不需要指定内存大小, 返回指针也不用强转

38. 说一下select

  • select在使用前,先将需要监控的描述符对应的bit位1,然后将其传给select,
  • 当有任何一个事件发生时,select将会返回所有的描述符,
  • 需要在应用程序遍历检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大

39. fork,wait,exec函数

  • 父进程首先使用fork拷贝出来一个父进程的副本, 此时只拷贝了父进程的页表,两个进程采用写时复制策略, 当有进程写的时候才调用拷贝机制分配内存. fork一次调用将会在父进程子进程中分别返回一直, 在父进程中返回子进程pid, 在子进程中返回0
  • exec函数族可以用于加载函数镜像替换掉当前进程, 从而完全改变该进程的功能. exec执行成功后并不会返回到该函数的调用处,这和函数无返回值调用有本质的区别, 但是当执行失败后,函数返回-1,并继续执行当前进程的后续指令.
  • 调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1

40. 请你回答一下静态函数和虚函数的区别

  • 静态函数在编译的时候就已经确定运行时机,而虚函数在运行的时候动态绑定。
  • 虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
  • 另外
    • 静态成员函数是类中特殊的成员函数,
    • 它被储存于静态储存区, 被该类的所有对象之间共享, 并不属于某个具体的类, 可以直接通过类名访问, 也可以通过对象访问
    • 它没有this指针,因此它只能访问类中的静态成员函数或者静态成员变量, 而不能直接访问类中的非静态成员变量或非静态成员函数

  • 参考: C++中静态成员函数
  • 静态成员函数总结:
    • 静态成员函数是类中的特殊的成员函数
    • 静态成员函数没有隐藏的this指针:当调用一个对象的非静态成员函数时,系统会将该对象的起始地址赋值给成员函数的this指针.但是,静态成员函数不属于某个对象,为该类的所有对象共享,所以静态成员函数没有this指针.
    • 静态成员函数可以通过类名直接访问
    • 静态成员函数可以通过对象访问
    • 静态成员函数只能直接访问静态成员变量(函数),而不能直接访问普通成员变量(函数)

容器和算法

1. mapset有什么区别,分别又是怎么实现的?

  • mapset都是STL中的关联容器,其底层实现都是红黑树(RB-Tree)。由于 mapset所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的mapset的操作行为,都只是转调RB-tree的操作行为。

  • mapset区别在于:

    • map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;set只是关键字的简单集合,它的每个元素只包含一个关键字。
    • set的迭代器是const的,不允许修改元素的值;而map虽然不允许修改关键字(Key),但是允许修改value。 其原因是mapset都是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了mapset的结构,导致iterator失效。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
    • map支持下标操作,set不支持下标操作。 map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find

2. 请你来介绍一下STLallocator

  • STL的分配器用于封装STL容器在内存管理上的底层细节。
  • C++中,其内存配置和释放包括两个关键之: newdelete
    • new运算分两个阶段:1) 调用::operator new配置内存;2) 调用对象构造函数初始化对象
    • delete运算分两个阶段:1) 调用对象析构函数;2) 调用::operator delete释放内存
  • STL allocator将以上阶段分作四个函数分别负责: allocate函数负责分配内存空间, deallocate函数负责内存释放, construct负责对象构造, destroy负责对象析构.
  • 为了提升内存管理效率, 减少申请小内存造成的内存碎片化问题, SGI STL采用两级分配至, 当分配空间的大小超过128B的时候,会使用第一级空间配置器, 当分配空间大小小于128B时,采用第二级空间配置器.
    • 一级空间配置器直接使用malloc, realloc, free函数进行内存空间分配和释放.
    • 二级空间配置器使用内存池技术管理内存, 使用16个链表维护8-128byte的16级别的小内存块.

总结: - 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。 - 如果需要的内存大小小于128bytesallocate根据size找到最适合的自由链表。 - a) 如果链表不为空,返回第一个node,链表头改为第二个node。 - b) 如果链表为空,使用blockAlloc请求分配node。 - x) 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。 - y) 如果内存池只有一个node的空间,直接返回给用户。 - z) 若果如果连一个node都没有,再次向操作系统请求分配内存。 - 分配成功,再次进行b过程。 - 分配失败,循环各个自由链表,寻找空间。 - I. 找到空间,再次进行过程b。 - II. 找不到空间,抛出异常。 - 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。 - 否则按照其大小找到合适的自由链表,并将其插入。

3. STL迭代器删除元素

  • 这个主要考察的是迭代器失效的问题。
    • 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器
    • 对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
    • 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

4. STLMAP数据存放形式

  • 红黑树。unordered map底层结构是哈希表

5. STL有什么基本组成

  • STL主要由六大部分组成:配置器, 容器, 迭代器, 算法, 适配器, 仿函数
  • 他们之间的关系:
    • 配置器为容器提供空间, 它是对空间动态分配,管理和释放的实现
    • 迭代器实现了容器和算法的衔接, 算法通过迭代器获取容器中的内容
    • 仿函数可以协助算法完成各种操作,适配器用来套接适配仿函数

6. STLmap, unordered_map, multimap

  • map, unordermap以及multimap都是关联容器,实现从键值(Key)到实值(Value)的映射, 注意shared_ptr等未重写小于操作符的类型不能作为键值
  • 单映射Map:
    • map 中的所有元素都是 pair, pair 的第一元素为键值,第二元素为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
    • 底层实现:红黑树
    • 适用场景:有序键值对不重复映射
  • 多重映射Multimap:
    • multimap中的所有元素都是pair, pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
    • 底层实现:红黑树
    • 适用场景:有序键值对可重复映射
  • 无序映射unordered_map:
    • unordered_map中的所有元素都是pair, pair 的第一元素被视为键值,第二元素被视为实值。其不允许键值重复, 其不会根据键值的大小自动排序.
    • 底层实现: hash
    • 使用场景: 无排序需求的键值对不重复映射

7. vectorlist的区别,应用,越详细越好

  • vector: 在堆上分配空间, 连续存储的容器, 支持动态调整空间大小
    • 底层实现:数组(array)
    • 容器内存空间增长:
      • vector 增加(插入)新元素时,如果未超过此时的容量(还有剩余空间),那么直接添加到最后(插入指定位置), 然后调整迭代器。
      • 如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
    • 性能:
      • 访问:O(1)
      • 插入:
        • 在最后插入(空间够):很快
        • 在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
        • 在中间插入(空间够):内存拷贝
        • 在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
      • 删除:
        • 在最后删除:很快
        • 在中间删除:内存拷贝
    • 适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
  • List动态链表: 在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
    • 底层:双向链表
    • 性能:
      • 访问:随机访问性能很差,只能快速访问头尾节点。
      • 插入:很快,一般是常数开销
      • 删除:很快,一般是常数开销
    • 适用场景:经常插入删除大量数据
  • 区别:底层, 内存的连续性, 插入和删除的影响, 内存分配时机, 随机访问性能
    • vector底层实现是数组;list是双向 链表。
    • vector支持随机访问,list不支持。
    • vector是连续的内存空间,list不是。
    • vector在中间节点进行插入删除会导致内存拷贝,list不会。
    • vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
    • vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
  • 应用
    • vector拥有连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector
    • list拥有不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list

8. STL中迭代器的作用,有指针为何还要迭代器?

  • 迭代器Iterator
    • (总结)Iterator使用聚合对象, 使得我们在不知道对象内部表示的情况下, 按照一定顺序访问聚合对象的各个元素.
    • Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
    • 由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STLlistvectorstack等容器类及ostream_iterator等扩展iterator
  • 迭代器和指针的区别
    • 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、—等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,—等操作。
    • 迭代器返回的是对象引用而不是对象的值。
  • 迭代器产生原因
    • Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构就可以实现集合的遍历,是算法和容器之间的桥梁.

线程安全性5种级别

  1. 不可变
    • 不变的对象绝对是线程安全的,不需要线程同步,如String、Long、BigInteger
  2. 无条件的线程安全
    • 对象自身做了 足够的内部同步,也不需要外部同步,如 Random 、ConcurrentHashMap、Concurrent集合、atomic
  3. 有条件的线程安全
    • 对象的部分方法可以无条件安全使用,但是有些方法需要外部同步,需要Collections.synchronized;有条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器
  4. 非线程安全 线程兼容
    • 对象本身不提供线程安全机制,但是通过外部同步,可以在并发环境使用, 如ArrayList HashMap
  5. 线程对立
    • 即使外部进行了同步调用,也不能保证线程安全,这种情况非常少,如如System.setOut()、System.runFinalizersOnExit()

设计模式六大原则

  • 设计模式六大原则:开闭原则
  • 说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:
    • 单一职责原则告诉我们实现类要职责单一;
    • 里氏替换原则告诉我们不要破坏继承体系;
    • 依赖倒置原则告诉我们要面向接口编程;
    • 接口隔离原则告诉我们在设计接口的时候要精简 单一;
    • 迪米特法则告诉我们要降低耦合。
    • 而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。