2017模拟面试题库 —— C++相关

来源:互联网 发布:epud阅读器 mac 编辑:程序博客网 时间:2024/06/08 15:03

Q:指针和引用的区别?


A:在x86 32位 Linux系统下,指针占4个字节;

从底层实现上来看:

1. 引用也是一个指针,创建一个指针和创建一个引用的汇编指令是一样的

int a = 8;011A5F6E  mov         dword ptr [a],8  int * p = &a;011A5F75  lea          eax,[a]              # 将变量a地址装入寄存器eax011A5F78  mov         dword ptr [p],eax   # 将寄存器eax中的地址传入指针pint & q = a;011A5F7B  lea          eax,[a]              # 将变量a地址装入寄存器eax011A5F7E  mov         dword ptr [q],eax   # 将寄存器eax中的地址传入指针q

2. 以引用 / 指针改变内存的汇编指令相同

*p = 10;00F25F81  mov         eax,dword ptr [p]   # 将指针p中的地址传入寄存器eax00F25F84  mov         dword ptr [eax],0Ah # 将值传入eax中地址所指向内存q = 10;00F25F8A  mov         eax,dword ptr [q]   # 将指针q中的地址传入寄存器eax00F25F8D  mov         dword ptr [eax],0Ah # 将值传入eax中地址所指向内存

从表现形式上来看:

1. 引用自带解引用

2. 没有空引用,引用必须在定义的时候初始化,且引用一经初始化不能改变    *相当于 int * const p

3. 指针有多级指针 / 引用只有一级引用    *C11支持二级引用(左值、右值引用)


/*************************************************************************************/


Q:C++的函数重载?


A:C/C++ 是编译型语言,源文件经过编译生成目标文件,收集源文件中的函数/变量生成符号表,接着在链接中进行符号解析;

符号表中的函数不允许重复定义,在C中是以 函数名 为区分函数的方式,在C++中则是以 函数名 + 参数列表 区分;(所以C中不能函数重载)

* 函数重载的前提:在同一个作用域下

* 特例:对于const修饰的变量,在编译器眼里是相同的,不构成函数重载

(只有用 const 修饰指针 / 引用 才能构成重载)


/*************************************************************************************/


Q:volitale关键字是做什么用的?


A:

1. 防止编译器对指令顺序进行调整  

/* 防止Cpu调整指令顺序:barrier( ) */

2. 防止多线程对共享变量进行缓存(保证所有线程都能实时获得共享变量的值)

/* 只能保证可见性,不能保证原子性(mutex、信号量、读写锁、原子锁) */


/*************************************************************************************/


Q:内存泄漏 / 资源泄漏


A:

1.调用 malloc/new 未 free/delete

malloc/new 后忘记 free/delete 或在执行 free/delete 之前抛出异常

2.发生浅拷贝对象默认赋值

eg:

Class Test{private:    int * p;public:    Test()    {        p = (int*)malloc(sizeof(int));    }    ~Test()    {        free(p);    }};int main(){    Test A;    Test B(A);    // 此处发生浅拷贝,当调用默认拷贝构造函数时,A、B中的指针指向同一块内存                  // 当A、B析构时,同一块内存被free两次,程序将崩溃    Test C;      C = A;        // 此处发生浅拷贝,当调用默认赋值函数时,A、C中的指针指向同一块内存                  // C中指针原指向的内存将丢失,发生内存泄漏    return 0;}

3.基类指针指向堆区资源,而基类析构函数非虚,则派生类无法析构

基类指针实际指向的是派生类对象中基类对象的起始地址,若基类中无虚函数,则派生类自己生成的虚表指针在内存布局中会在基类对象上方,导致 delete 时偏移量错误

class Base{private:int * p;public:Base() {p = (int *)malloc(sizeof(int));}~Base() // 此处应为虚函数{cout<<"hello"<<endl;free(p);}};class Test : public Base{public:Test() {}~Test() { cout<<"world"<<endl; }};int main(){Base * pB = new Test(); // 以基类指针指向派生类对象delete pB; // 只调用了Base的析构函数(根据指针类型静态绑定)return 0;}

将基类 Base 的析构函数定义为虚函数后,形成动态绑定;

调用析构函数时查虚表,发现派生类 Test 重写了析构函数,则调用 Test 的析构函数 -> 调用父类 Base 析构函数,对象被成功的析构掉。

4.socket / fd 未 close (fd 进程上限: 2^16 = 65535  , 即 epoll 可操作 fd 上限)

5. 产生僵尸进程,进程内核栈内存泄漏(8k  底端PCB)

6.new Test[100]  ->  delete Test

使用 new 一次生成多个对象,而没有使用 delete [] ,多个对象只会调用一次析构函数

7. 构造函数中抛出异常(new -> bad_alloc),退出时未调用析构函数,申请的内存将无法释放

8. 智能指针的交叉引用

class B;class A{public:shared_ptr<B> _pb;};class B{public:shared_ptr<A> _pa;};int main(){shared_ptr<A> pa(new A); // A引用计数加一(1)shared_ptr<B> pb(new B); // B引用计数加一(1)pa->_pb = pb; // B引用计数加一(2)pb->_pa = pa; // A引用计数加一(2)return 0;}
当函数退出时,调用 B 的析构函数,B 的引用计数 - 1 = 1,B 没有析构掉;

接着调用 A 的析构函数,A 的引用计数 - 1 =  1,A 也没有析构掉,A、B 的内存泄漏。




/*************************************************************************************/


Q:C++多态的原理?


A:C++中的多态分为静态多态和动态多态,也称为编译期多态和运行期多态:

静态多态包括:重载、模板

动态多态包括:虚函数

C++通过虚函数实现动态多态:

在基类的函数前加上 virtual 关键字,在派生类中重写该函数,以指针或引用调用类从成员函数会发生多态,运行时根据对象的实际类型调用相应的函数;

原理:

对于每个定义了虚函数的类和它的派生类,都会产生一个虚函数表,这个虚函数表是类共享的;

每个对象前四个字节都有一个虚表指针 vf_ptr,指向该类型的虚函数表vf_table(虚表存放在只读数据段 .rodata )

动态联编在 基类指针 指向不同的 派生类对象 时发生,若派生类对象重写了虚函数,则虚表对应项将被覆盖;

实际调用中根据对象的 vf_ptr 找到该类的虚表,调用对应的函数

拥有虚函数的类的构造函数过程:

push  ebp    # 将栈底保存mov   ebp, esp    # 将栈顶指针值赋给栈底指针sub    esp, 4ch    #  开辟栈帧ebp <-> esp   0xCCCCCCCC    #  刷新栈帧vftable -> vfptr    # 将虚表地址赋给虚表指针

/*************************************************************************************/


Q:早绑定/晚绑定 、 静态绑定/动态绑定?


A:早绑定又称静态绑定,在程序编译期发生,即编译期就确定将要调用的函数地址(以对象直接调用成员函数);

晚绑定又称动态绑定,在程序运行期发生,即在程序运行中确定要调用的函数地址(前提:以指针或引用调用成员函数,且类中该函数定义为虚函数,调用不在构造函数中——即该对象存在,可取地址);

p->Show();    # 静态绑定 0139B5D8  push        0Ah    # 参数压栈0139B5DA  mov         ecx,dword ptr [p]    0139B5DD  call        Base::Show (013916BDh)    # 调用函数
p->Show();    # 动态绑定009AA858  mov         esi,esp009AA85A  push        0Ah    # 参数压栈009AA85C  mov         eax,dword ptr [p]  # 将对象前四个字节(vfptr)放入eax009AA85F  mov         edx,dword ptr [eax]    # 将 vftable 地址放入edx009AA861  mov         ecx,dword ptr [p]  009AA864  mov         eax,dword ptr [edx]    # 查虚表得到虚函数地址009AA866  call        eax    # 调用相应的虚函数009AA868  cmp         esi,esp  009AA86A  call        __RTC_CheckEsp (09A147Eh)   # 调整栈平衡

/*************************************************************************************/


Q:智能指针


A:一种确定性的通用垃圾收集机制。

智能指针和普通指针的区别在于智能指针加了一层封装,目的是为了方便的管理一个对象的生命周期;

实质是一个对象,行为表现却像一个指针;

* 智能指针更深层的意义在于,值语义到引用语义的转换;

C++工程实践经验谈——陈硕:值语义与数据抽象

http://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html

// TODO

shared_ptr    强智能指针

直接持有资源,可直接修改资源,其中的计数器记录资源有多少个强智能指针引用

weak_ptr   弱智能指针

不共享资源,只有资源的观测权,通过观测引用资源的强智能指针计数来管理资源,它的构造不引起资源引用计数的增加

(创建对象时用强智能指针,其他地方使用只能持有资源的弱智能指针;避免交叉引用导致资源无法释放)

需要特别指出的是,如果shared_ptr所表征的引用关系中出现一个环,那么环上所述对象的引用次数都肯定不可能减为0那么也就不会被删除,为了解决这个问题引入了weak_ptr。

环状引用的本质矛盾是不能通过任何程序设计语言的方式来打破的,为了解决环状引用,第一步首先得打破环,也就是得告诉C++,这个环上哪一个引用是最弱的,是可以被打破的,因此在一个环上只要把原来的某一个shared_ptr改成weak_ptr,实质上这个环就可以被打破了,原有的环状引用带来的无法析构的问题也就随之得到了解决。

------

例:多线程访问共享对象的线程安全

1. 主线程创建对象 Test

2. 子线程调用 Test 的成员方法(Test 可能已经被析构)

解决方法:

1. 以强智能指针创建对象

shared_ptr<Test> pa(new Test());

2. 将弱智能指针传入子线程

weak_ptr<Test> pb = *(weak_ptr<Test> *)lparg;

通过观察强智能指针的引用计数,来判断该对象是否可用。

void * pthread(void * arg){    /* ... ... */    shared_ptr<Test> pc = pb.lock();  // 将弱智能指针提升成强智能指针    if(pc != NULL)  // 判断对象是否存在    {         pc->func();    }    /* ... ... */}


智能指针实现:https://github.com/chen892704/STL-Learning

/*************************************************************************************/

C++多继承与虚继承的内存布局:

https://www.oschina.net/translate/cpp-virtual-inheritance?p=1#comments

继承与多态——要点总结


继承结构中构造函数的调用:

push  ebp    #  保存栈底

mov  ebp,  esp    #  将栈顶设为新的栈底

sub  esp, 4ch    #  开辟新的栈帧

rep  stos ...    #  将新的栈帧刷为0xCCCCCCCCh

vftable  ->  vfptr    #  将当前类的虚表地址赋给创建对象的虚表指针

(每一层构造函数都会将当前类的虚函数表地址写入虚表指针中)


虚函数表内容:包括虚函数指针,以及运行时的信息;


用基类指针指向堆区派生类对象时,指针指向的位置是派生类对象中基类部分的起始位置;

若派生类实现了虚函数而基类没有,则对象中的虚表指针将在派生类对象的前面,导致调用 delete 释放内存的时候,找不到派生类对象的实际起始位置(ptr - 4byte),程序崩溃;

解决的方法是在基类中定义虚函数,则派生类对象中的虚表指针将从基类继承而来,当用基类指针指向派生类对象时,基类指针将指向对象的首部。


成员能否访问 / 访问权限是否正确 / 函数的默认值用哪一个  ->  在编译期确定

包含虚函数的继承结构中,调用哪个类的虚函数  ->  在运行时确定


C++类型强转:

const_cast  去掉对象const属性的类型转换

static_cast  编译器认为较安全的类型转换

reinterpret_cast  类似于C的强转,较底层的类型转换

dynamic_cast  支持RTTI(run-time type identity)的类型转换

/* 

Test * ptr = dynamic_cast<Test *>(p);    

如果指针p指向的对象类型为Test,则返回Test *类型的指针,否则返回NULL

*/