c++ 新特性 尾置返回值
std::optional
结构化绑定
STL RAII 是什么? resource acquire is initialization 资源获取即初始化
将资源和对象的生命周期绑定。
hash map 怎么实现
标准库用 vector 保存链表的头指针
什么时候 rehash ?
超过最大负载因子
vector 扩容机制 两倍或者 1.5 倍。
均摊分析
容器删除和迭代器
顺序容器 (vector deque)
erase 迭代器不仅会使该迭代器失效,还会使后面的迭代器都失效。
但是 erase 会返回下一个有效的迭代器。
关联容器 (map, set, multimap, multiset)
erase 迭代器只是让该迭代器失效。
erase 返回 void。
使用 earse(it++) 的方法删除迭代器。
迭代器的类型 前向
unordered_set & unordered_map
forward_list
双向
随机访问
输入迭代器 InputIterator 支持逐个遍历和读取
输出迭代器 OutputIterator 支持逐个遍历和写入
迭代器失效 以 vector 为例
插入位置之后的迭代器失效。如果插入使得需要扩容时,所有迭代器失效。
删除位置之后的迭代器失效。
rehash 之后 unordered_map 的迭代器失效
List 和 deque 的区别 list 是一个双向环形链表
deque 是一个双向开口的连续线性空间
deque 和 vector
deque 允许常数时间对头部和尾部插入或者移除
deque 没有容量概念 。动态地以分段连续空间组合而成。没有所谓的空间保留功能?
deque 支持随机访问
空间配置器 allocator deallocator
两级配置器
第一级直接用 malloc, free 和 relloc
第二级若区块小于 128 bytes 使用内存池
free_list 是一个以 8 为容量公差的长度为 16 的链表,最后一个节点区块为 128 bytes。
不足时调用 refill 申请 [1, 20] 块,并且将多的块放入 freelist
内存池一个 njob 空间都不够的时候,用 malloc 向 OS 申请内存
申请不到,在后续的 freelist 里找
还是找不到,转到一级适配器,借助 oom 机制申请内存。
deallocate 先判断大小,若大于 128b 调用一级配置器,否则调用二级配置器。
std::deque 的实现 问题:vector 头部操作的效率特别差
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class deque { ... protected : typedef pointer* map_pointer; map_pointer map; size_type map_size; public : ... iterator begin () ; iterator end () ; ... } template <class T ,...>struct __deque_iterator { size_t buffer_size () ; ... T* cur; T* first; T* last; map_pointer node; }
基础语法 noexcept 将函数标记为不会抛出异常,使用noexcept关键字标记的函数在它抛出异常时,编译器会直接调用名为”std::terminate”的方法,来中断 程序的执行。
析构函数通常会被默认加上 noexcept
移动的时候加上 noexcept
大多数容器调整大小用的不会抛出异常的移动构造 ,否则调用拷贝构造
因为在资源的移动过程中如果抛出了异常,那么那些正在被处理的原始对象数据 可能因为异常而丢失
拷贝的时候原始数据是安全的
指针和引用的区别 指针是变量,存一个地址。引用是一个别名。
指针在传参的时候是值传递,引用是引用传递。
引用必须初始化,指针可以为空,也可以随便指向一个地址。
引用不可以再改变。引用不能为空。
递归的时候用引用可以降低开销。
define const typedef inline
define 只在预处理阶段起作用,没有类型检查。展开后占用的是代码段空间。
const 有类型。
typedef 有作用域限制,有类型检查
inline 是函数?再编译器替换。有类型检查。
explicit 隐式类型转换 构造函数前加上 explicit 可以防止构造函数的参数在传递的时候进行隐式类型转换。
支队一个实参的构造函数有用,因为需要多个实参的构造函数不能用于隐式类型转换,也就不用指定为 explicit 了。
堆和栈的区别
大小、位置不同
栈空间比较小,向低地址增长。申请的地址是固定的。
堆空间比较大,向高地址增长。申请的位置可以变化。
申请和管理方式不同
栈是系统自动分配的。自动回收。
堆要自己手动申请。由内存泄漏风险。
申请效率不同
栈由系统分配,快且没有碎片。
堆由程序员分配,慢且会有碎片。
取栈里的对象要快一些 ,因为
寄存器里有栈地址
获取堆的内容要先读指针的内容,再读地址的内容。
new / delete 与 malloc / free 的异同
前者是 C++ 的关键字,调用 new 运算符,后者是 C/c++ 标准库函数。
前者自动算大小
前者会返回类型,是类型安全的。
前者会调用构造函数/析构函数
前者可以重载
new 会调用 operator new 申请空间,然后调用构造函数。
重载operator new
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Foo {public : void * operator new (std::size_t size, void * ptr) { std::cout << "placement new" << std::endl; return ptr; } } int main () { Foo* m = new Foo; Foo* m2 = new (m) Foo; std::cout << sizeof (m) << std::endl; delete m; return 0 ; }
可以用再内存池,不用重新申请空间,而是返回一个已经分配好空间的首地址。
重载operator delete
一般不会重载 operator delete,原因是重载后的 operator delete 不能手动调用 。
这种重载的意义是和重载operator new
配套 。只有operator new
报异常了,就会调用对应的operator delete
。若没有对应的operator delete
,则无法释放内存。
不同类型的new
plain new
placement new
void* operator new(size_t,void*); // 不会分配内存,也就不会失败了
void operator delete(void*,void*);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #### delete p、 delete [] p、 allocator 都有什么作用? * delete [] 时,数组中的元素按照逆序进行销毁。 * delete p会调用一次析构函数,而delete [] p会**调用每个成员的析构函数**。 * delete [] 时候会**向前找4 个字节获取长度**,这4 个字节是未定义的,所以调用了**不固定次数**的析构函数 * allocator 将**内存分配和对象构造分开**,allocator 申请一部分内存,不进行初始化对象,只有需要的时候才会进行初始化操作。 #### malloc 和 free 是怎么实现的? 用系统调用 brk, mmap, munmap 这些系统调用实现。 * brk 是堆顶指针向高地址移动 * mmap 是在进程的虚拟空间中(文件映射区)找一快空闲的虚拟内存。 * 在第一次访问的时候,发生**缺页中断**,操作系统负责分配物理内存,然后简历虚拟内存和物理内存之间的映射关系。 * malloc**大于128 k的内存**,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0 ), * brk 分配的内存要等到高地址内存释放后才能释放,mmap可以单独释放。当高地址空间的空闲内存高于 128 k 执行内存紧缩。 * 操作系统有一个记录**空闲地址的链表**,当操作系统收到程序的申请就会遍历链表找到第一个大于申请空间的节点,然后删除这个节点。 brk 找K线链表的策略: * 最优匹配:找到 >= M 的最小的节点 * 最差匹配:找到 >= M 的最大的节点 * 首次匹配 * 下次匹配 除了空闲链表的其他空闲内存方式: * 分离分散链表:每一种大小的空间简历独立的链表 * **伙伴系统**:空闲空间递归一分为二直到满足。伙伴系统的伙伴只有1 位不同,比较好找。 #### malloc realloc calloc * realloc 用于扩容 ```c++ void * malloc (unsigned int num_size);int *p = malloc (20 *sizeof (int )); void * calloc (size_t n,size_t size) ;int *p = calloc (20 , sizeof (int )); void realloc (void *p, size_t new_size) ;
顶层const 底层const 顶层 const 修饰的变量本身 是一个常量
底层 const 指的是 const 修饰的变量指向的对象 是一个常量
final 禁止继承
禁止重写,C++中还允许将方法标记为fianal,这意味着无法再子类中重写该方法。这时final关键字至于方法参数列表后面,如下
野指针和悬空指针
野指针:没有被初始化的指针 ==》 初始化
悬空指针:指针最初指向的内存被释放了 ==》 释放后立即置空
重载重写和隐藏
重载 overload
重写 override
派生类覆盖基类的同名函数
相同的参数个数、参数类型和返回值类型
隐藏
派生类的函数屏蔽了基类的同名函数(可以用::访问被隐藏的函数)
参数相同,但是基类函数不是虚函数
参数不同,无论基类函数是不是虚函数都会被隐藏
构造函数的类别
默认构造函数
初始化构造函数
拷贝构造函数
委托构造函数
被委托的构造函数在委托构造函数的初始化列表里被调用,而不是在委托构造函数的函数体里被调用。
转换构造函数
类成员初始化?构造函数顺序?初始化列表为什么快?
派生类构造函数的执行顺序
虚基类
基类
类类型成员的构造函数
自己的构造函数
前者是构造函数里赋值,后者是纯粹的初始化操作。赋值操作有时候会产生临时对象。
什么时候必须成员列表初始化?作用是什么? 其实就是什么时候不能用赋值初始化。
引用成员
常量成员
基类带参数的构造函数
类成员的带参数的构造函数
列表初始化实际上:
编译器在构造函数内安插初始化操作。
初始化顺序和声明顺序相关。
浅拷贝和深拷贝
浅拷贝:只拷贝一个指针,不开辟新的地址
深拷贝:拷贝指针值,并且开辟出新的空间
大端和小端
volatile mutable explicit
异常处理 try throw catch
x = x ^ y;
y = x ^ y;
x = x ^ y;
1 2 3 4 5 6 7 8 9 10 #### strcpy 和 memcpy 的区别 ```c++ #include <cstring> char *strcpy (char *dest, const char *src) ;void *memcpy (void *str1, const void *str2, size_t n) ;
strcpy 复制字符擦混
memcpy 复制任何内容
strcpy不用指定长度 ‘\0’
memcpy 要指定长度
编译器的默认函数
默认缺省构造函数
默认拷贝构造函数
默认析构函数
默认赋值运算符
…. 默认移动构造 默认移动赋值?
迭代器
输入迭代器
输出迭代器
前向迭代器
双向迭代器
随机访问迭代器
高级特性 虚继承 虚继承可以解决菱形继承的问题。不用复制多份基类。
bptr 虚继承的子类指向父类的指针/偏移量,可能会和 vptr 合并。
链继承 C : B : A
1 2 3 4 5 6 7 8 9 10 C VTable(不完整) struct C +------------+object | RTTI for C | 0 - struct B +-------> +------------+ 0 - struct A | | C::f0 () | 0 - vptr_A -------------------------+ +------------+ 8 - int ax | B::f1 () | 12 - int bx +------------+ 16 - int cx | C::f2 () | sizeof (C): 24 align: 8 +------------+
多继承
C : A, B
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 C Vtable (7 entities) +--------------------+ struct C | offset_to_top (0 ) |object +--------------------+ 0 - struct A (primary base) | RTTI for C | 0 - vptr_A -----------------------------> +--------------------+ 8 - int ax | C::f0 () | 16 - struct B +--------------------+ 16 - vptr_B ----------------------+ | C::f1 () | 24 - int bx | +--------------------+ 28 - int cx | | offset_to_top (-16 ) | sizeof (C) : 32 align: 8 | +--------------------+ | | RTTI for C | +------> +--------------------+ | Thunk C::f1() | +--------------------+
虚继承
虚基类只存一次!子类存到虚基类的虚函数表的指针
不使用虚继承,基类存多份
使用菱形继承,基类只存一份
B:A; C:A, D: B, C
虚基类偏移量 / 虚基类指针 ? (和编译器有关! 可以是存在线性地址里,通过偏移量确定(g++),也可以开辟新的虚基表 指针,指向虚基类的地址(vs))
虚基类由最后的子类实现
所以在最后的位置
虚基类中被子类重写的函数需要指向 vcall_offset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 D VTable +---------------------+ | vbase_offset(32) | +---------------------+ struct D | offset_to_top(0) | object +---------------------+ 0 - struct B (primary base) | RTTI for D | 0 - vptr_B ----------------------> +---------------------+ 8 - int bx | D::f0() | 16 - struct C +---------------------+ 16 - vptr_C ------------------+ | vbase_offset(16) | 24 - int cx | +---------------------+ 28 - int dx | | offset_to_top(-16) | 32 - struct A (virtual base) | +---------------------+ 32 - vptr_A --------------+ | | RTTI for D | 40 - int ax | +---> +---------------------+ sizeof(D): 48 align: 8 | | D::f0() | | +---------------------+ | | vcall_offset(0) |x--------+ | +---------------------+ | | | vcall_offset(-32) |o----+ | | +---------------------+ | | | | offset_to_top(-32) | | | | +---------------------+ | | | | RTTI for D | | | +--------> +---------------------+ | | | Thunk D::f0() |o----+ | +---------------------+ | | A::bar() |x--------+ +---------------------+
虚基类位于派生类存储空间的末尾。
虚函数指针和虚函数表的创建时机: 虚函数表是在编译的过程创建
虚函数指针在运行时创建
构造函数、析构函数、虚函数能不能是内联函数?
构造函数为什么不能是虚函数?析构函数为什么是虚函数?
构造函数
存储上,没有实例化就没有vtable。调用构造函数的时候不能确定真实的类型。所以 ctor 不能是虚函数。
构造函数只在初始化时运行一次,不是动态行为,没必要多态。
构造函数第一件事就是初始化 vptr。
析构函数
是为了防止内存泄漏。
如果析构函数不是虚函数,就不能正确识别对象类型从而正确调用析构函数。如果不把析构函数弄成虚函数,基类指针指向派生类的时候就不会发生动态绑定。
多个构造函数、析构函数顺序
构造函数
基类构造函数,多个基类按照派生表中的顺序
成员类构造函数,按照声明顺序
派生类构造函数
析构函数
派生类的虚构函数
成员类的析构函数
基类的析构函数
构造函数内部执行顺序
基类/虚基类构造
vptr 初始化
扩展成员初始化列表
执行程序员代码
哪些函数不能是虚函数?
构造 函数
静态 函数
友元函数
普通 函数
内联 函数
模板类要写在一个文件里面 因为编译的时候模板不会生成真正的代码。实例化模板只能找到声明,链接器找不到链接程序会报错。
c++ 内存管理 类空间有什么
非静态成员
虚函数表指针
padding
空类 size 为1
C++内存分区 栈
堆
全局数据
常量
代码段
异常处理
try throw catch
COREDUMP http://sunyongfeng.com/201609/programmer/tools/coredump
1 2 3 4 5 6 7 8 9 10 11 12 ulimit -c // 查看当前core 大小限制ulimit -c unlimited // 解除限制cat /etc/security/limits.conf // 查看限制cat /proc/sys/kernel/core_pattern // 查看 core pattern // %t 时间戳 // %e 程序名 // %s 信号 // %p 进程号 // GDB 调试 coredump gdb a.out core-a.out
bt 查看调用栈
f n 查看某个栈帧
info
info frame
info registers
info args
info locals
info threads 查看线程
编译连接
预处理 g++ -E main.cpp -o main.i
删除注释
引入头文件 #pragma once once
宏展开
编译 g++ -S main.i -o main.s
汇编 二进制可重定位文件 main.o 每个都有 text data bss heap 内核段,需要合并(链接)
为什么合并?1. 浪费空间 2. 空间局部性不好
汇编编程机器码
链接 可执行文件
合并所有的 obj 文件的段 ,调整段的偏移和段长度 ,合并符号表
地址与空间分配
符号解析与重定位
.bss
节在目标文件和可执行文件中不占用文件的空间,但是它在装载时占用地址空间
TODO
静态链接和动态链接
静态链接
符号解析
重定位
作用:为了生成位置无关代码。这样共享库就可以放在任意的位置了。
相对重定位条目
绝对重定位条目
动态链接
为了解决静态库的问题
一个库只有一个文件
在内存中共享库的 .text 节可以被共享
需要一个动态链接器
动态编译和静态编译
静态编译和动态编译是两种不同的编译方式,用于生成可执行文件。让我为您详细解释一下:
静态编译 :
在静态编译时,编译器将程序与其所有依赖项(包括库)链接在一起 ,形成一个单独的可执行文件。
这个可执行文件包含了所有代码和数据,因此它是一个完全独立的二进制文件。
静态编译的优点是可执行文件不依赖于外部动态链接库 ,因此在运行时不需要加载其他库文件。
缺点是可执行文件体积较大,且编译速度较慢。
动态编译 :
在动态编译时,只创建程序的框架,而不将所有依赖项包含在可执行文件中。
动态编译的可执行文件需要附带一个动态链接库 ,在执行时,需要调用其对应动态链接库中的命令。
优点是缩小了可执行文件本身的体积,加快了编译速度 ,节省了系统资源。
缺点是需要安装对应的运行库,否则无法运行动态编译的可执行文件。
并发编程相关 C++ 的锁
智能指针相关 1. enable_shared_from_this 允许一个类继承自它,以便获得指向 this
的 shared_ptr
用处:异步回调,事件处理,观察者模式
实现方法
weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*
和->
,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
使用weak_ptr的成员函数use_count()
可以观测资源的引用计数,另一个成员函数expired()
的功能等价于use_count()==0
,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()
从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true
的时候,lock()
函数将返回一个存储空指针的shared_ptr。
调试相关 GDB 使用
内存泄漏
COREDUMP 调试 终止时产生 Coredump 文件,默认为 core 可以 echo “pattern” core_pattern 更改命名规则。
死锁?发个 kill -3 pid 或者 kill -s SIGQUIT pid 产生 core
然后
1 2 3 4 5 6 7 gdb -c ./a.out ./coregdb bt 或者 gdb info stack 或者 pstack pid 看进程信息
多线程调试 C++ 并发编程 unique_lock vs lock_guard vs scope_lock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lock_guard (mutex_type& m, adopt_lock_t tag);unique_lock (mutex_type& m, defer_lock_t t); unique_lock (mutex_type& m, adopt_lock t); unique_lock (mutex_type& m, try_to_lock t); std::lock (...) std::scope_lock (mutex...)
异步 Promise future packaged_task async
C++协程 关键字 co_await 调用一个 awaiter 对象
co_yield 挂起一个协程
co_return 协程返回
写一个脚本,自动执行以下步骤:
运行以上脚本
执行 git pull
执行 git commit -a -m {message}
执行 git push