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 - 栈指针寄存器:该指针永远指向系统栈最上面一个栈帧的栈顶。
- 放到data(初始化)或bss(为初始化)
- 成员变量:变成类型共享的了
- 成员方法:方法不再产生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是在多个进程间是共享的,这可以提高空间利用率
堆和栈的区别
- 堆内存远大于栈内存大小
- 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
- 智能指针的具体内容!
什么时候会出现访问越界
- 数组元素越界
- vector容器、string对象、array对象访问越界
- char*没有添加’\0’
- 类型强转,大类型(派生类)指针指向小内存(基类对象),解引用则越界了
智能指针交叉引用的问题
- 定义用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 二级空间配置器的内存池实现