C++面试重点再梳理

智能指针

请讲一下智能指针原理,并实现一个简单的智能指针

  • 智能指针其实不是一个指针。它是一个用来帮助我们管理指针的类,维护其生命周期的类。有了它,妈妈再也不用担心我的内存泄露啦!

  • 需要解决的问题:

    • 怎么释放内存?
    • 什么时候释放内存?
  • 释放内存方法一:同归于尽! auto_ptr

  • 释放内存方法二:引用计数!

    • 引用计数:对一个指针所指向的内存,目前有多少个对象在使用它
    • 当引用计数为0的时候,删除对象
    • 多个智能指针共享同v 一个引用计数类
    • 在进行赋值等操作时,动态地维护引用计数
  • 实现,有两个问题需要解决

    1. 如何对指针引用的内存进行计数
    2. 如何改进SmartPointer类使得它能够动态地维护引用计数

Counter类实现

class Counter {
    friend class SmartPointPro;
public:
    Counter(){
        ptr = NULL;
        cnt = 0;
    }
    Counter(Object* p){
        ptr = p;
        cnt = 1;
    }
    ~Counter(){
        delete ptr;
    }

private:
    Object* ptr;
    int cnt;
};

SmartPointPro类 未考虑线程安全

  • 如何动态维护引用计数?引用计数改变发生在如下时刻:
    1. 调用构造函数时: SmartPointer p(new Object());
    2. 赋值构造函数时: SmartPointer p(const SmartPointer &p);
    3. 赋值时:SmartPointer p1(new Object()); SmartPointer p2 = p1;
class SmartPointPro {
public:
    SmartPointerPro(Object* p){
        ptr_counter = new Counter(p);
    }
    SmartPointerPro(const SmartPointerPro &sp){
        ptr_counter = sp.ptr_counter;
        ++ptr_counter->cnt;
    }
    SmartPointerPro& operator=(const SmartPointerPro &sp){
        ++sp.ptr_counter->cnt;
        --ptr_counter.cnt;
        if(ptr_counter.cnt == 0)
            delete ptr_counter;
        ptr_counter = sp.ptr_counter;
    }
    ~SmartPointerPro(){
        --ptr_counter->cnt;
        if(ptr_counter.cnt == 0)
            delete ptr_counter;
    }
private:
    Counter *ptr_counter;
};

怎么样获取智能指针所包装的指针呢?

  • method1: 写GetPtr(), GetObject()等函数
  • method2: 重载指针的操作符,->/*

STL中的四种智能指针

  • auto_ptr: 弱版本的智能指针,赋值会有问题
  • shared_ptr: 引用计数版本
  • weak_ptr: 解决循环引用的问题
  • unique_ptr: auto_ptr的改进版本,不允许直接赋值,可以移动

单例模式

//机智的Meyers写法,优雅而线程安全

class Singleton{
private:
    Singleton(){}
    Singleton(const Singleton& s){}
    Singleton& operator=(const Singleton& s){}
public:
    static Singleton* GetInstance() {
        static Singleton instance;
        return &instance;
    }
}

struct对齐问题

  • 笔试、面试中的超高频问题:
struct Q{
    char c;
    int num1;
    double num2;
};
  • 问上述代码中sizeof(Q)为多少? 16
  • 实际上这是考察struct结构地址对齐的问题
  • struct的对其系数和以下几个关系有关
    1. 元素大小
    2. 元素顺序
    3. #pargma pack参数

对齐规则

  • struct中成员在内存中按顺序排列,在没有#pargma pack(n)的情况下,各个成员的对齐系数为自己的长度
  • 在有#pargma pack(n)的情况下,各个成员的对齐系数为min(自己的长度,n)
  • struct整体的对齐系数为对齐系数中最大的
  • 依次排列时要满足地址对对齐系数取模为0

C++虚函数相关问题

C++中为什么要使用虚函数

  • Base类和派生类都有相同的函数接口,但是考虑到不同的派生类的特性,可以用虚函数来使用不同的实现
  • 虚函数实现动态绑定,提高程序的灵活性
  • 实现动态绑定的两个条件
    1. 相应成员函数为虚函数
    2. 使用基类对象的引用指针进行调用

请说明C++虚函数的底层实现机制

出现频率: 5星 题目难度: 4星 使用范围: 所有公司

  • 虚函数表: 一个类的虚函数的地址表,所有对象都是通过它来找到合适的虚函数. 他就是这个类的"四十二章经"

  • 虚函数表指针: 每个类的对象实例都拥有一个指针指向这张虚函数表(一般在对象实例的最前面),它帮助对象找到这张表的地址,然后就可以遍历其中的函数指针,调用相应的函数

  • 注意:虚函数表一个类有一个,而不是一个对象有一个

  • 子类的虚函数表是子类的,从父类拷贝一份过来,并进行修改. 和父类并不是共享

  • 那么发生继承覆盖时发生了什么呢?

    1. 单继承没有覆盖: 拷贝一份父类的虚函数表,然后添上自己的函数
    2. 单继承有覆盖: 拷贝一份父类的虚函数表,有重载的将其替换
    3. 多继承无覆盖: 子类的虚函数被放在了第一个父类中
    4. 多继承有覆盖: 覆盖的部分就修改,三个父类拷贝过来的虚函数表都进行了覆盖
  • 普通函数不进入虚函数表

  • C++本身是没有类地址的,虚函数表放在代码段或者数据段的

  • 如果没有在子类中重载,调用的就还是父类的虚函数实现.

国内的面试题和虚函数有关的题

题目1:为什么需要虚析构函数?

  • 若析构函数非虚,用基类指针去new一个派生类对象时,调用delete时,只会析构派生类对象的基类部分
  • 如果子类有资源需要释放,则析构函数一定要写成虚函数

题目2:析构函数一定是虚函数吗?

  • 不一定
  • 在没有资源需要释放,或者final类型的类也不需要

题目3:访问虚函数和普通函数哪个快?

  • 显然是普通函数
  • 普通函数在编译阶段就确定了,访问时可以直接调到对应的地址执行.
  • 虚函数需要在虚函数表中查找,有一定的耗时
  • 此外,由于有虚函数表的存在,因此会有一定的内存占用

题目4:内联函数/构造函数/静态成员函数可以是虚函数吗

  • 否 否 否
  • 内联函数是编译时展开,虚函数是运行时绑定,存在冲突,不能是虚函数
  • 构造函数,子类在构造过程中,先构造父类,此时还没有生成子类成员,此时无法动态绑定
  • 静态成员函数不存在this指针

题目5: 构造函数中可以调用虚函数吗?

  • 不可以. 调用构造函数后如果有多层继承关系,实际上会从最顶层的基类从上往下构造,如果此时调用了一个子类的虚函数,其尚未被构造出来,这样的函数行为就是完全不可预测的。

C++虚继承相关问题

  • 解决菱形继承带来的二义性的问题
  • 中间层继承基类方式为虚继承, virtual public Base
  • 实现原理和编译器高度相关,VC++实现思路如下:
    • 在mid1和mid2对象中添加虚基类指针,指向基类对象Base,这样Mid1和Mid2中就会指向共同的基类成员,从而消除了二义性
  • 在开发中应该尽量避免

C++虚继承中的sizeof

  • 遇到这样的题,只能说明面试官在耍流氓,因为都是和编译器高度相关的,没有唯一答案
  • 但是我们要知道类的大小取决于哪些因素:
    1. 类成员的大小
    2. 虚函数表指针大小(是否和父类共享)
    3. 虚基类指针

先看下面的一段代码

class A {
public:
    int a;
    virtual void myfunA(){}
};

class B : virtual public A{
public:
    virtual void myfunB(){}
};

class C : virtual public A{
public:
    virtual void myfunC(){}
};

class D: public B, public C{
public:
    virtual void myfunD(){}
};

int main (){
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    cout << sizeof(C) << endl;
    cout << sizeof(D) << endl;
    return 0;
}
  • 在VC++下,答案为: 8, 16, 16, 24

  • 在g++ 32位机器下,答案为: 8, 12, 12, 16

  • 分析VC++

    • 对A, int a(4B) + vptr(4B) = 8 Bytes
    • 对B与C, A的内容(8B) + vptrOfSelf(4B) + 指向Base的虚基类指针(4B) = 16Bytes
    • B + C - A = 16 + 16 - 8 = 24 (? 存疑 !)

C++对象内存模型

  • 单一的一般继承
    [图片上传失败...(image-5efc6-1516850329498)]

  • 多重继承
    [图片上传失败...(image-9653a5-1516850329498)]

  • 钻石形重复继承--没有使用虚继承
    [图片上传失败...(image-322c8a-1516850329498)]

  • 钻石形多重虚拟继承,下面给出gcc的结果
    [图片上传失败...(image-80a507-1516850329498)]

你可能感兴趣的:(C++面试重点再梳理)