C++面经

1. 面向对象、多态、继承

this指针?

  • 一个类型定义了很多对象
  • 各个对象有各自的成员变量
  • 但共享一套成员方法
  • 用this指针来确保,方法被调用时,操作的成员变量是哪个对象的

继承的优点

  • 类和类之间的关系 – 继承和组合
  • 代码复用 – 基类代码给到派生类
  • 基类给派生类纯虚函数接口 – 多态

继承多态

  • 静多态:编译时 – 函数重载、模版
  • 动多态:运行时 – 虚函数 – 指针/引用指向派生类对象

构造/析构函数可不可以是虚函数

  • 构造函数不可以:
    • 构造时对象还不存在,没有虚函数指针
    • 基类构造在子类构造之前
  • 析构函数可以
    • 一般基类析构函数都是虚函数
    • 可以确保正确释放子类

构造函数和析构函数能不能抛出异常

主要是可能导致 – 资源泄漏

  • 构造函数不可以
    • 没有完全创建完成则不会调用析构,进而无法正确释放资源
  • 析构函数也不可以
    • 没有完全释放释放资源

拷贝构造函数为什么传引用?

如果传值会导致无限递归,产生形参会调用自己

如何实现一个不可被继承的类?

将构造函数设为private – 因为构造顺序:基类 —》派生类

什么是纯虚函数,虚函数表

  • virtual void func()=0;
  • 有纯虚函数的类叫抽象类,抽象类不可以实例化,但可以创建指针和引用
  • 一般定义在基类里,本质是定义接口,从而利用动态多态
    • 用基类指针/引用调用子类同名覆盖函数接口
  • 虚函数表在编译阶段产生,运行时放在 .rodata段
  • 基类指针/引用调用虚函数 == 基类指针/引用访问对象的头4个字字节——虚函数指针vfptr,再去虚函数表vftable中找对应的函数进行调用,实现动态绑定

手写单例模式

class Singleton{
private:
    Singleton();
    Singleton(const Singleton& other);
public:
    static Singleton* m_instance; // 声明
    static Singleton* getInstance(){
        // 理论上这里要上锁
        if(m_instance == nullptr){
            m_instance = new Singleton();
        }
        return m_instance;
    }
};
Singleton* Singleton::m_instance = nullptr; // 定义

2. 关键字

何时用new[]申请,可以用delete释放

  • delete做两件事:析构 + 释放内存
  • 自定义类型、并且提供了析构函数,一定是new[] + delete[]

static关键字的作用

  • 全局变量、函数用static变成当前文件可见
    • 符号表中符号的作用域从global变成local了
    • 在一起定义的两个全局变量,在内存的中位置是相邻的
  • 局部变量
    • 放到data(初始化)或bss(为初始化)
      • bss:block started by symbol – 未初始化的全局变量
      • data:已初始化的全局变量
    • 局部变量本身不产生符号,在栈上用ebp减偏移量来访问的,static修饰后,在符号表中产生local符号
      • ebp - 基地址指针寄存器:该指针永远指向系统栈最上面一个栈帧的底部
      • esp - 栈指针寄存器:该指针永远指向系统栈最上面一个栈帧的栈顶。
  • 成员变量:变成类型共享的了
  • 成员方法:方法不再产生this指针

new/malloc

  • malloc按字节开辟内存,new底层用malloc开辟内存,但还可提供初始化
  • malloc开辟失败返回空指针,new开辟失败返回bad_alloc异常
  • malloc是C的库函数,new是运算符重载
  • 对于数组和单个对象,malloc都是一样的
    • 而new:new int()、new int[20]()

class/struct

  • 定义时默认,class是private,struct默认public
  • 继承时默认,class是private,struct默认public
  • struct空结构体是0,struct空类是1
  • c++11 struct初始化,struct data{int a; int b}; data d = {10, 20}; class不可
  • template< class T >,struct不能

const 和 static 区别

  • const定义常量 – 全局变量、局部变量、函数形参、类成员变量
    • 编译方式:编译时,把常量出现的位置都用常量的值替换
// 常量
const int a = 10;
int *p = (int *)&a;
*p = 20;
cout << *p << " " << a << endl; // 打印结果:20 10
// 常变量
int b = 10;
const int a = b;
int *p = (int *)&a;
*p = 20;
cout << *p << " " << a << endl; // 打印结果:20 20
  • const定义常成员方法
    • func() const; – this 指针变成了const
    • 普通成员和const成员都可以使用这个函数
  • const不能修饰非成员函数
  • static能修饰全局变量、局部变量、成员/非成员函数、成员变量
    • 但是不能修饰函数形参

3. C/C++内存分布和管理

用户空间私有,内核空间共享

  • userspace:

    • reserve(保留区)、text(代码段)、rodata(常量),只能读
    • 数据区:data、bss、heap、stack、命令行参数、环境变量
  • kernelspace

    • ZONE_DMA
    • ZONE_NORMAL(text、rodata、data、bss、heap、stack)
    • ZONE_HIGHMEM

data、bss、rodata的区别:

在ELF格式的可执行文件中,全局内存包括三种:bss、data 和 rodata。

bss是指那些 没有初始化 的和 初始化为 0 的全局变量

data指那些 初始化过(非零)的 非const的全局变量

rodata表示常量

常量不一定就放在rodata。有的立即数直接编码在指令里,存放在代码段(.text)中

对于字符串常量,编译器会自动去掉重复的字符串,保证一个字符串在一个可执行文件(EXE/SO)中只存在一份拷贝

rodata是在多个进程间是共享的,这可以提高空间利用率

进程内存模型 img

堆和栈的区别

  • 堆内存远大于栈内存大小
  • malloc/new在堆上,需要手动创建/释放
  • 函数运行在栈上分配栈帧、函数的局部变量在栈上分配,自动释放
  • 堆:低地址-》高地址,栈:高地址-》低地址

局部变量存在哪里?

  • 栈上 – ebp指针偏移访问
  • 不产生符号,是指令text的一部分
    • int a = 10; ==> mov dword ptr[ebp-4] 0Ah

shared_ptr的引用计数

https://blog.csdn.net/QIANGWEIYUAN/article/details/88973735#shared_ptr_111

如何防止内存泄露

  • 内存泄漏 – 分配的堆内存没有释放,也再没有机会释放了
    • 堆内存没有名字
  • 解决方法 – 使用智能指针
    • unique_ptr、shared_ptr、weak_ptr、auto_ptr
  • 智能指针的具体内容!

什么时候会出现访问越界

  1. 数组元素越界
  2. vector容器、string对象、array对象访问越界
  3. char*没有添加’\0’
  4. 类型强转,大类型(派生类)指针指向小内存(基类对象),解引用则越界了

智能指针交叉引用的问题

  • 定义用shared_ptr,引用用weak_ptr
  • 当用weak_ptr访问对象成员时,先调用weak_ptr的lock提升方法
    • 提升成功,会成为shared_ptr,再对对象成员进行调用
    • 提升失败表示已经析构了

4. STL

  • 容器:
    • 顺序容器:vector、deque、list、forward_list
    • 关联容器:
      • 有序:map、set、multimap、multiset
      • 无序:unordered_map、unordered_set、…multi…
  • 容器适配器:queue、stack、priority_queue
  • 近容器:
    • string、bitset、array、valarray、pair、tuple
  • 迭代器
  • 泛型算法
  • 函数对象

迭代器失效

  • 不允许一边读一边修改
    • 通过迭代器插入一个元素时,迭代器都失效了
    • 通过迭代器删除一个元素时,后面的迭代器都失效了
  • 因此要及时更新迭代器

map/set实现原理

  • set只存储key / map是映射表,存储[key,value]
  • 底层都是红黑树
  • 可以理解成红黑树数据结构的适配器

空间配置器

  • 给容器使用的
  • 把对象的内存开辟和对象构造分开
  • 把对象的析构和内存释放分开

vector和list区别

  • vector – 内存可以二倍扩容的动态数组 – 随机访问
    • queue和priority_queue是在vector基础上实现的
  • list – 循环的双向链表 – 增加删除

map和multi_map

  • 映射表[key - value]
  • 底层实现是“红黑树” – BST
    • 红黑树5个性质
    • 红黑树插入3种情况 – 最多旋转两2次
    • 删除4种情况 – 最多旋转3次
  • map不允许key重复,multi_map允许

deque的底层实现


5. 语言特性

宏和内联的区别

  • 宏是预编译 – 字符串替换
  • 内联是编译阶段 - 在函数调用点直接把函数展开调用
    • 节省了函数的调用开销
    • 编译器决定到底展开还是不展开
  • 宏可以定义名字(常量、代码块、函数块),内联只能修饰函数
  • 宏没有办法debug,内联可以

内联和普通函数的区别

  • 函数调用开销
  • 反汇编角度
# 假设执行函数前堆栈指针ESP为NN
push   p2    ;参数2入栈, ESP -= 4h , ESP = NN - 0x04h
push   p1    ;参数1入栈, ESP -= 4h , ESP = NN - 0x08h
call test    ;压入返回地址 ESP -= 4h, ESP = NN - 0x0Ch 
# 进入函数内
push   ebp   ;保护先前EBP指针,EBP入栈,ESP-=4, ESP = NN-0x10h
mov    ebp, esp    ;设置EBP指针指向栈顶 NN-0x10h
mov    eax, dword ptr  [ebp+0ch] ;ebp+0x0ch为NN-0x4h,即参数2
mov    ebx, dword ptr  [ebp+08h] ;ebp+0x08h为NN-0x8h,即参数1
sub    esp, 8      ;局部变量所占空间ESP-=8, ESP = NN-0x18h
...
add    esp, 8      ;释放局部变量, ESP+=8, ESP = NN-0x10h
pop    ebp         ;出栈,恢复EBP, ESP+=4, ESP = NN-0x0Ch
ret    8           ;ret返回,弹出返回地址,ESP+=4, ESP=NN-08h, 
# 后面加操作数8为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.

ESP就是一直指向栈顶的指针

而EBP只是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数参数、局部变量等

C++如何调用C语言函数接口

  • C和C++生成符号的方式不同,C和C++之间的API接口无法直接调用
  • C语言的函数声明必须被扩在extern “C” {} 中
#ifdef __cplusplus
extern "C"{
#endif
    int sum(int, int);
#ifdef __cplusplus
}
#endif

C和C++区别

  • 重载:函数重载、运算符重载
  • 动态内存分配: new/delete、malloc/free
  • 新特性:const、inline、带默认参数的函数、C++引用
  • 编程方面:模版 GP、类和对象 OOP、STL标准库
  • 安全方面:异常、智能指针

四种强制类型转换

  • const_cast – 去掉const属性
  • static_cast
  • reinterpret_cast – C风格类型转换
  • dynamic_cast – RTTI类型识别

异常处理

try{
    // 可能会throw的代码
}catch(){
    // 捕获异常并处理,使得代码继续进行而不是崩溃
}
  • 异常的栈展开,从当前栈帧向外,一直到main函数、到系统,直到有一个位置能处理异常,若系统发现异常,直接abort程序
    • 用栈展开可以统一处理某些异常,实现“尽管出现了错误,仍可以正确进行或者至少正确释放相关资源”

早绑定和晚绑定

  • 早绑定(静态绑定、编译时绑定)
    • 用对象调用虚函数 call 编译期已经知道是哪个函数了
  • 晚绑定(动态绑定、运行时绑定)
    • 用指针/引用调用虚函数 call寄存器

指针和引用的区别

表层角度:

  • 引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
  • 引用初始化后不能被改变,指针可以改变所指的对象。
  • 不存在指向空值的引用,但是存在指向空值的指针
  • 引用没有顶层const
  • “sizeof引用**”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小
  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查

底层角度:

  • 引用和指针(代码如下)
    • 创建时产生的汇编代码一摸一样
    • 修改时产生的汇编也是一模一样
int val = 10;
int *ptr = &val; // lea eax,[a]  mov dword ptr[ebp-8],eax
int &ref = val;  // lea eax,[a]  mov dword ptr[ebp-oCh],eax
*ptr = 20; // mov eax, dword ptr[ebp-8]   mov dword[eax],14H
ref = 20;  // mov eax, dword ptr[ebp-0Ch]   mov dword[eax],14H
  • 编译器维护了一张符号表,而且这个表贯串整个编译过程
    • 使用元素本身访问:通过名字在符号表中找到地址,然后访问
    • 使用指针访问:直接通过地址访问
    • 引用在符号表中创建了一个名字不一样但地址一样的表项,故引用访问 – 通过引用名字在符号表中找到地址,然后访问
  • 引用就是给同一块内存又起了一个名字

重载的实现

  • c++生成函数符号 – 依赖于“函数名+参数列表“
  • 编译到调用函数点的时候,根据函数名、参数个数、参数类型来匹配
    • 如果匹配完全成功则调用该函数重载版本 – 静态多态
    • 匹配不成功????[从primer上找一下]

6. 其他

编译链接全过程

  • 编译:预编译、编译、汇编,生成“二进制可重定向obj文件 – *.o ”
  • 链接:1.合并段、符号解析 2.符号重定向, 生成 “可执行文件 – *.out ”

内存池

要求:获取、插入资源 O(1)

  • SGI STL 二级空间配置器的内存池实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值