C++面试重点再梳理
2018-01-25 本文已影响76人
杭河苇
智能指针
请讲一下智能指针原理,并实现一个简单的智能指针
-
智能指针其实不是一个指针。它是一个用来帮助我们管理指针的类,维护其生命周期的类。有了它,妈妈再也不用担心我的内存泄露啦!
-
需要解决的问题:
- 怎么释放内存?
- 什么时候释放内存?
-
释放内存方法一:同归于尽! auto_ptr
-
释放内存方法二:引用计数!
- 引用计数:对一个指针所指向的内存,目前有多少个对象在使用它
- 当引用计数为0的时候,删除对象
- 多个智能指针共享同v 一个引用计数类
- 在进行赋值等操作时,动态地维护引用计数
-
实现,有两个问题需要解决
- 如何对指针引用的内存进行计数
- 如何改进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类 未考虑线程安全
- 如何动态维护引用计数?引用计数改变发生在如下时刻:
- 调用构造函数时: SmartPointer p(new Object());
- 赋值构造函数时: SmartPointer p(const SmartPointer &p);
- 赋值时: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的对其系数和以下几个关系有关
- 元素大小
- 元素顺序
- #pargma pack参数
对齐规则
- struct中成员在内存中按顺序排列,在没有#pargma pack(n)的情况下,各个成员的对齐系数为自己的长度
- 在有#pargma pack(n)的情况下,各个成员的对齐系数为min(自己的长度,n)
- struct整体的对齐系数为对齐系数中最大的
- 依次排列时要满足地址对对齐系数取模为0
C++虚函数相关问题
C++中为什么要使用虚函数
- Base类和派生类都有相同的函数接口,但是考虑到不同的派生类的特性,可以用虚函数来使用不同的实现
- 虚函数实现动态绑定,提高程序的灵活性
- 实现动态绑定的两个条件
- 相应成员函数为虚函数
- 使用基类对象的引用或指针进行调用
请说明C++虚函数的底层实现机制
出现频率: 5星 题目难度: 4星 使用范围: 所有公司
-
虚函数表: 一个类的虚函数的地址表,所有对象都是通过它来找到合适的虚函数. 他就是这个类的"四十二章经"
-
虚函数表指针: 每个类的对象实例都拥有一个指针指向这张虚函数表(一般在对象实例的最前面),它帮助对象找到这张表的地址,然后就可以遍历其中的函数指针,调用相应的函数
-
注意:虚函数表一个类有一个,而不是一个对象有一个
-
子类的虚函数表是子类的,从父类拷贝一份过来,并进行修改. 和父类并不是共享
-
那么发生继承覆盖时发生了什么呢?
- 单继承没有覆盖: 拷贝一份父类的虚函数表,然后添上自己的函数
- 单继承有覆盖: 拷贝一份父类的虚函数表,有重载的将其替换
- 多继承无覆盖: 子类的虚函数被放在了第一个父类中
- 多继承有覆盖: 覆盖的部分就修改,三个父类拷贝过来的虚函数表都进行了覆盖
-
普通函数不进入虚函数表
-
C++本身是没有类地址的,虚函数表放在代码段或者数据段的
-
如果没有在子类中重载,调用的就还是父类的虚函数实现.
国内的面试题和虚函数有关的题
题目1:为什么需要虚析构函数?
- 若析构函数非虚,用基类指针去new一个派生类对象时,调用delete时,只会析构派生类对象的基类部分
- 如果子类有资源需要释放,则析构函数一定要写成虚函数
题目2:析构函数一定是虚函数吗?
- 不一定
- 在没有资源需要释放,或者final类型的类也不需要
题目3:访问虚函数和普通函数哪个快?
- 显然是普通函数
- 普通函数在编译阶段就确定了,访问时可以直接调到对应的地址执行.
- 虚函数需要在虚函数表中查找,有一定的耗时
- 此外,由于有虚函数表的存在,因此会有一定的内存占用
题目4:内联函数/构造函数/静态成员函数可以是虚函数吗
- 否 否 否
- 内联函数是编译时展开,虚函数是运行时绑定,存在冲突,不能是虚函数
- 构造函数,子类在构造过程中,先构造父类,此时还没有生成子类成员,此时无法动态绑定
- 静态成员函数不存在this指针
题目5: 构造函数中可以调用虚函数吗?
- 不可以. 调用构造函数后如果有多层继承关系,实际上会从最顶层的基类从上往下构造,如果此时调用了一个子类的虚函数,其尚未被构造出来,这样的函数行为就是完全不可预测的。
C++虚继承相关问题
- 解决菱形继承带来的二义性的问题
- 中间层继承基类方式为虚继承, virtual public Base
- 实现原理和编译器高度相关,VC++实现思路如下:
- 在mid1和mid2对象中添加虚基类指针,指向基类对象Base,这样Mid1和Mid2中就会指向共同的基类成员,从而消除了二义性
- 在开发中应该尽量避免
C++虚继承中的sizeof
- 遇到这样的题,只能说明面试官在耍流氓,因为都是和编译器高度相关的,没有唯一答案
- 但是我们要知道类的大小取决于哪些因素:
- 类成员的大小
- 虚函数表指针大小(是否和父类共享)
- 虚基类指针
先看下面的一段代码
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)]