随着知识点越来越多,记忆难免会混乱、遗忘,一个好的知识点总结能有效帮助回忆。
C语言¶
1.数组和指针¶
- 数组指针
- 指针数组
- 函数指针
- const和指针
- sizeof和指针和数组
- strlen和字符数组
void*
指针:- 隐式转换抹去实例类型信息,强制转换变为其他类型。
- 由于无类型信息,不可解引用(取内容),地址无法自增。
- NULL,是宏,是强制转换的
(void*)0
指针(C++改为了0,避免了重载识别为int类型)
2.库函数的模拟实现¶
- memcpy(void,const void,size):提供拷贝任何数组形式的内存的。
- const作用是防止源字符串被修改
- 判空,转换成char,循环赋值。
- memmove(void,const void,size):相比cpy,新增了内存重叠的判定。
- 先判空(assert),用ret指向dst(dst后面要自增发生变化)。
- 防止指向同字符串不同位置(dst地址<=src或者强制转换到
char*
后,dst>=src+len)。 - 如果不重叠,则从低地址开始复制(强制转换到char,dst自增,src自增)
- 如果重叠,就从高地址开始倒过来赋值(类似于合并数组)
- 返回ret。
- strstr(const char,const char):是否是子串,是的话返回开始的首地址,否则返回NULL
- str2是否为NULL?是的话直接返回str1地址
- 双重while循环遍历,第二重循环遍历子串为空(遍历完了)时直接返回首地址。
- strcpy/strlen/strcmp 出现相对少,上面三个要求轻松手撕
3.数据存储¶
- 基本类型大小,循环越界与死循环问题。
- 整型存储规则,原(符号位正负?)反(负数规则)补(区分0)。IEEE浮点数(考的少)
- 大小端及如何判断(考的很多):存在原因?代码判断(强转或联合体)?端序转换(位)?
- 类型提升和截断:截断因素(类型和大端小端),补位规则(补0和符号)
4.编译链接¶
- 预编译--宏(考察很多,轻松实现宏函数)
- 预编译、编译、汇编、链接的过程
C++¶
1.⭐⭐⭐面向对象基本概念¶
封装概念¶
- 数据,方法以及对外提供接口。
- private,protected,public级别。同类对象无权限隔离,可以访问private。
继承概念¶
- (继承中)隐藏特点?隐藏后调用基类?
- 公共继承、私有继承:权限问题?转换会发生什么?。
⭐⭐⭐多态¶
概念
静态多态¶
- 重载定义?特点? 函数签名(别忘了命名空间了)?
- 模版泛型?特化,偏特化。为什么放.h?\
- (好玩的)lambda模版参数包展开c++17(结合ECS拷贝组件使用)
- 调用虚函数的静态绑定?
虚函数virtual¶
- 继承构造出多态的两个条件?this的隐式多态? 虚函数重写条件?两个例外(协变和析构)?
- 析构为什么需要虚函数?什么是指针的切割?
- 接口继承,实现重写?override和final的作用?
- 虚表指针vptr:数量,大小,产生时机,类型
- 虚表vtbl:和类对应的关系,数量,位置,存储内容(RTTI,函数指针),虚函数存放(重写和未重写?)?数组最后?
-
纯虚函数?抽象类?有啥不同?= 0
-
构造和析构中调用虚函数调用情况:存储角度、实用角度,调用后的情况
- 虚函数的静态绑定机制
- 虚函数能否内联:编译时(理论,实践),运行时(加final?)。
- 派生类为什么不能调用基类?
虚继承¶
这是一个和虚函数无关概念,这玩意儿为啥要问啊,又复杂又没有意义,难评。 - 问题提出:内存浪费、二义性 - 实现:虚基类指针vbptr(每个子类一个,总在虚表指针之后),虚基表(通过虚表偏移地址找到) - 内存:多份父类->仅存在一份 - 虚基表结构:存储偏移值,固定两条偏移量 - 第一条:存储虚基表到该类首地址的偏移(根据vptr,0或-4) - 第二条:到基类的偏移量 - 虚继承的虚表指针机制:虚继承的派生类,如果定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。
- 虚继承菱形继承内存分析:
- 爷爷类A(vptr,A内存)
- 父类B(B-vptr、B-vbptr、B/ 0 / A-vptr内存 )、C
- 子类D都有虚函数的情况下
- B-vptr,B-vbptr,B内存
- C-vptr,B-vbptr,B内存
- D内存
- 0x0000000 4字节
- A-vptr,A内存。
最多3个vptr,2个vbptr
注意:爷爷内存中间会以4个字节的0x00000000相隔。
- 虚继承虚表分析:
- 如果有虚函数,所有基类都有单独的虚函数和虚表,
- 最派生D:不需要虚指针和虚表了,直接优化到第一个派生类虚表里去。
- 普通菱形继承:
- 子类内存(B-vptr,A,B/ C-vptr,A,C / D)
- 虚表结构(多继承:如果D有定义新的虚函数,加在第一个父类虚表后面)
2.⭐⭐⭐ 智能指针¶
【C++】详谈C++智能指针的前世今生-CSDN博客这篇文章讲的不错
- 深拷贝、浅拷贝、野指针、悬空指针、RAII设计思想
- 创造的原因:RAII->auto_ptr(多次析构、copy+NULL)->boost(scoped_ptr那三个)->C11(正式引入,标准废除autoptr)->C14(控制块)->C17(移除auto_ptr)
- 智能指针共性:RAII、
operator*
和operator->
- unique:
- 删除拷贝赋值、删除拷贝构造。
-
sharedptr:
- 两个指针:一个资源,一个控制块
- 控制块内容:强引用计数、弱引用计数、其他数据(删除器、分配器等)
- 工厂函数make_shared和(new)构建:过程、不适合的情况有哪些?
- new过程:new Widget构建对象,再运行std::sharedptr构造函数出控制块,再连接。
- make函数原理:完美转发到所要创建的对象的构造函数,从new产生的原始指针构造出对象,直接构造出一块。生成的对象更小更快
- make异常安全:new第一次单独构造时可能会出现内存泄漏。
- 仿照make_pair,一个是构建资源后拷贝,一个是直接分配内存块在里面new 那块资源。(资源是否分散两次构建的问题)
- make_shared不能使用定制删除器,比如--数组new【】、lock
- Initialize_list不用make,因为可能无法完美转发参数,而是InitializeList类型
- sharedptr不适合:1.自定义内存管理。2.存在weak且生命周期过长。
- 线程安全:原子自增自减(加锁)。同时修改等问题(外部加锁)
- 循环引用问题(小tip,为什么不能弱循环引用)
- 拷贝赋值怎么做的?自检、旧控制块释放(包含自减)、swap、新控制块计数增加。
- 释放过程怎么做的:自减,为0删除指针、计数
- 构造、拷贝构造、拷贝赋值,引用块自增。
- 析构,引用块自减。
- 弱引用计数有啥用?延长生命周期。share拷贝weak不会+,weak拷贝weak、shared才会加+。
- 定制删除器:存在del的参数,底层是函数包装器function,支持仿函数、lambda、函数指针操作。
- boost本来有智能指针数组的,不过c11没加,所以提供了这个
- 方便实现delete【】。
-
(超进阶)代码实现(尽量能讲STL源码):线程安全保障,
- shared_ptr循环引用解决:weak,手动释放指针打破,鸵鸟策略。
- weak_ptr单独使用:另一引用被销毁时,weak_ptr自动失效,避免野指针。
- shared_ptr线程安全问题:
- (进阶)unique_ptr实现,shared_ptr 实现
- (补充)一个引擎如何将智能指针改成自己的指针:别名功能,符号重构
- 智能指针内存泄漏场景
3.⭐⭐⭐ new/delete、malloc/free,STL二级分配¶
malloc和free是库函数,需要头文件支持
- malloc:可用内存块(结构体:可用标识+size+前一块大小+空闲链表指针)与空闲链表。遍历链表找不到则请求延时,整理内存片段合并。内存分配成果返回==void*
,失败返回NULL
- 小内存brk(小于等于128kb),大内存直接mmap映射独立内存页。剩下的部分切成新空闲块加入链表。
- free:通过当前指针参数-内存块结构体大小获取内存块指针,设置为可用并释放size大小的内存。
new和delete是C++关键字,是操作运算符,支持operator重载实现:
- new运算符:
1.operator new
:(可单独拿出来使用,不)自动计算内存,调用malloc,失败返回bad_alloc()异常,成功则严格的返回指针==void*
2.调用构造函数:获取内存指针,通过placement new
机制调用构造函数
- nothrow new:不返回badalloc异常,而是nullptr
- delete的过程:调用析构,operator delete
调用free()
删除对应的指针
- new[]
:会多分配8的内存,记录数组长度n
- delete[]
:根据记录的n值,逐个调用析构。误用delete会造成内存泄漏
- 什么时候使用delete this?主动释放资源,完成某个特定任务后销毁自身,异步回调
- 注意:必须在堆上分配、必须是最后操作、避免析构调用、确保该对象不会再被访问
- 成员函数delete this的后果?
STL的两级分配器:小于128B内存池(链表管理)技术,大于128Bmalloc()。 C++的内存池技术:优化内存碎片问题,针对小对象,申请一定数量内存块(通常8B),构成链表。申请后改内存块从空闲链表去除。
4.⭐⭐⭐ STL数据结构¶
六大特性¶
- 迭代器:解引用,前进后退,判等
- 容器:下面的都是
- 算法:copy,sort等
- 仿函数
- 适配器
- 空间配置器(分配器:小内存池批量分配、内存复用,POD免除构造、局部性优化
迭代器¶
- 各容器迭代器的差别:前后向,单向,双向,随机访问
- 取值,递增递减,判相等。
空间配置器¶
- 内存分配与释放封装底层内存操作(如malloc/new)
- SGI-STL二级分配器内存池优化:减少内存碎片针对频繁的小块内存请求
- 一级空间配置器(大块内存),内存不足分配失败bad_alloc
- 二级基于内存池(<128b),维护 16 个自由链表(free lists),分别管理 8B、16B、24B……128B 的内存块。用户申请内存时,将大小对齐至 8B 倍数,并从对应链表中分配
- 对象构造与析构分离通过
construct()
和destroy()
方法在已分配内存上构造对象或析构对象(如使用placement new
)
顺序容器¶
Vector¶
- 随机迭代器:T*改造
- push_back
- emplaca_back:万能引用,完美转发,容器数据获取,迭代器判断,扩容判断,内部断言判断,编译期条件分支选择(无异常且默认构造,扩展区域内存保护并使用自定义构造),分配内存,传入指针和对象进行构造,迭代器失效,设置模版类型的返回值,迭代器移动,返回返回值。
- vector扩容reserve:重新分配内存,顺序拷贝
list、forward_list¶
- 迭代器为Node指针,需要重载:
deque¶
array¶
关联式容器(键值)¶
map和set-- 红黑树¶
- AVL树:LL、RR、LR、RL。
- 红黑树性质:
- 根节点必为黑色
- 叶子节点一定是黑色的NULL
- 任一路径不能有两个连续的红色(所以一条路径最多是另一条路径的两倍)
- 任何节点出发,下至路径节点的黑色节点数目相同
- 插入情况
- 情况1:空树,插入红节点直接变黑节点
- 情况2:父节点为黑节点,直接插入,为红节点
- 情况3:新节点父节点为红色,并且叔叔节点为红色,祖父必为红。那么红节点上移,父亲和叔叔变黑,祖父变黑。这时候祖父的父亲和祖父可能会形成连续红节点,进入情况4。
- 情况4:LR,父节点是红色,叔叔节点是黑色。若新的节点在父节点右边且父节点为祖父节点左边,则左旋,进入情况5。(反之右旋)
- 情况5:LL,父节点是红色,叔叔节点是黑色。新节点在父节点左边且父节点为祖父节点左边,则父节点变黑色祖父节点变红,以祖父节点进行右旋。
unordered_map和set 哈希表¶
容器适配器¶
stack 、queue¶
priority_queue¶
5.⭐⭐左值右值,移动构造,万能引用,完美转发¶
- 左值:类型--引用,右值引用。形参永远是左值
- 右值:值类别--(无地址)纯右值,(无持久地址)将亡值(区分一下右值引用)
- 右值类型有:
- 除字符串外的字面量:42,3.14,字符‘A’,true
- 运算表达式的结果:a+b,(a > b), a&b
- 返回非引用类型时生成的临时对象:int func() { return 10; },int result = func();
- Lambda表达式本身
- 后缀自增,自减:int j = i++; 本身返回的操作前的值是左值。
- 取地址操作符的结果:int* ptr = &a;
- 显式转换(如
static_cast<double>(x)
)生成临时值。 - 访问对象成员时,若对象本身是纯右值,则结果也是纯右值。:int coord = getPoint().x; // getPoint()是纯右值,.x的结果是纯右值
- move不移动(而是转换),完美转发不完美
- std::move:本身不移动(也不保证转换对象能移动),源码中,move接受通用引用。只有静态强制转换为右值引用类型返回,但实际上是将亡值。实际内存操作在移动拷贝和移动赋值中实现
- move一定会转换成右值引用吗?并不,如果对应函数没有相关移动构造,如果有相关拷贝,会提供拷贝构造(比如想把const string& 传到string&&)
- 尽量别把需要移动构造的类型设置为const
- move一定会转换成右值引用吗?并不,如果对应函数没有相关移动构造,如果有相关拷贝,会提供拷贝构造(比如想把const string& 传到string&&)
- 万能引用(通用引用):模板T&&和auto&&。即便是一个const,也会令其失效
- T&&就一定是了?也不一定,比如push_back(T&&),其需要先定义vector,此时T已经被定性为具体类型,变成push_back(Widget&&)
- 万能引用重载问题:精确匹配:正常函数(特化)>精确匹配:模版实例化(万能引用)大于类型提升。
- 根源:万能引用能转换比想象的多的参数,引发函数错误。
- 解决方法:放弃重载,传递const T&即左值,传值,tag_dipatch(类型判定),enableif(约束模版,判断const、volatile,base或者移除引用)
- 引用折叠:模版、auto、typedef的情况,但即便折叠成右值引用TYPE&&在表达式内也是左值,需要forward协助转发。因为具名的右值引用将被视作左值,注意区分move的右值引用。
- 移动不一定完全高效,需要假定移动操作不存在,成本高,未被使用,除非实现了移动语义。
- 完美转发失败:
- 模板类型推导失败或者推导出错误类型,完美转发会失败
- 导致完美转发失败的实参种类有花括号初始化,作为空指针的
0
或者NULL
,仅有声明的整型static const
数据成员,模板和重载函数的名字,位域。
6.⭐⭐类型转换¶
string、char,char{}类型万物互转
隐式转换条件:值类型-低精度转高精度,有符号无符号混合转无符号,赋值表达式,传参和返回值
非基本类型:NULL转任意,任意转void,void转任意
指针/引用: 1.派生转基类(不改变const或volatile属性)2.非常量转为常量
3.类的隐式转换:单参数构造函数创建,Initialize_list多参数构造函数创建。类型转换运算符自定义。
显式转换条件:
- C语言强制转换:掩盖错误
- const:不能去除变量的常量性,只能去除指针或引用的。移除const和volatile
- 使用mutable也许会更安全
- static:静态转换(编译期确定),基本的转换工作。不然一般编译期会给警告精度损失。
- 适用范围:基本数据类型、向上转型、自定义转型、void 互转,枚举和整型。
- 可能会调整地址,比如多继承向上转型。
- 错误用法:
- 不相干类型(无继承或自定义转换规则),指针和引用无法转换。
- 向下转型
- 无法移除const和volatile。
- 禁止值类型和指针之间的互转
- dynamic:
- 根据RTTI,进行向上,向下(更加的安全),横向(指针偏移)的转换。也可以把类转void。
- 必须有虚函数。
- 转换错误情况:
- 基类无虚函数,目标类型非指针或引用,直接编译时报错
- 若实际指针类型转换不兼容,返回nullptr,需显示检查指针有效性
- 若实际引用类型转换不兼容,抛出 std::bad_cast
异常,异常捕获吧
- 转换失败原因:1.无继承 2.多态缺失(基类无虚函数)3.构造/析构阶段
- reinterpret:底层二进制的直接转换:
- 一般用在类型指针和引用互转、整数间互转。
- 不能移除const*
模版指针任意转换:使用一个T* As()
函数,可以实现指针的任意转换。
7.⭐函数调用过程¶
堆栈,
8.拷贝构造、移动构造、拷贝赋值、移动赋值代码实现¶
9.⭐Lambda和仿函数¶
- 底层实现?仿函数是什么?
- 值引用、引用,auto&&推导
-
捕获、闭包概念
-
类型:引用捕获,按值捕获,
- 避免默认捕获模式(引用)导致悬空问题
- 坑:捕获只能应用于_lambda_被创建时所在作用域里的non-
static
局部变量(包括形参)- 成员变量不能捕获,因为暗含this指针。捕获的是this,不过可以按this->的方式去调用成员变量。
- 可以做一个成员变量的拷贝再进行按值捕获
- C14可以做初始化捕获和移动捕获:
- C++14 泛型【auto&&】lambda:
10.C11 function<>和std::bind和元组std::tuple¶
- function函数包装器:传入返回值和形参
- bind::目标函数,变量,占位符
11.⭐⭐内联函数和宏¶
都可以实现替换的目的 - 引用库#include - 宏定义 # define X Y - 条件编译 inline函数失效 - 代码体积过大(十行) - 递归函数 - 复杂控制流 - 虚函数 - 含指针或引用调用 - 动态库(地址无法确定) 为什么尽量以const、enum、inline替代宏:记号表追踪问题、作用域、取地址问题,函数检查和难以预料的行为。
12.C++ string⭐⭐¶
- 手写string
- 早期g++:COW写时拷贝,读时拷贝,线程安全,优点(读得快)。堆结构:引用计数+字符串内存
- vs实现string:union的16位小内存先存字符串值,超过16则转换为开辟堆内存。
- size_t存长度,size_t存堆容量,还有个指针做其他的
11.⭐⭐static和全局变量和extern'C'和全局静态。¶
- 全局(+static)静态和全局变量区别:作用域为单个文件
- 函数默认为extern,加了static就不能被外部访问了。头文件不要用
- static不需要初始化,默认为0值。使用时才会分配到内存。
- extern‘C’:使代码按照C语言方式编译,主要是为了区分C++的重载底层函数命名。
- 可以结合动态链接,和C#(Unity)进行相互调用。
- 全局静态变量
- cpp定义全局变量,.h文件extern一下,其他想用只需要引用这个有文件
- 定义一个静态变量,提供get和set函数
- 提供一个类的静态成员变量,直接通过类获得。
12.⭐⭐内存分配和二进制问题¶
- 结构体的大小,union的大小,类的大小,空类大小。
- enum和enumClass(C11)
- 内存对齐的意义
- 无符号整数溢出导致的死循环
- 栈溢出的原因:递归、还有什么?
13.⭐const¶
- const作用于函数:
- 常量指针和指针常量
- 修改方式
- constexpr常量表达式
14.⭐⭐指针和引用区别¶
指针传递:本质是值传递,传递的是地址值的副本,可以多级。 引用:本质为指针常量, - 必须初始化绑定 - 不能更改绑定,对外没有内存地址 - 不能多级。 - 返回值?和值传递的区别?(对象生命周期问题) - 传参相比值传递如何?
15.内存大小、内存对齐、sizeof原理¶
- int类型、long、指针
- 对齐方式
- sizeof原理:编译期计算。
15.⭐内存泄漏的定位¶
16.C++普通函数与成员函数区别¶
普通函数: - 作用域为全局或命名空间,类外定义,访问全局变量或传入参数。 - 无封装性。不依赖对象,独立存在于代码区。无法继承,不支持多态(但可以重载) 成员函数: - 作用域为类,类内定义,类外需要作用域解析符(::)。可访问所有类成员,调用隐含this指针。 - 支持封装,通过对象(.)或对象指针(->)调用(除了静态成员函数)。支持继承多态与重载。 - 代码区仅一份副本,默认内联。
17.auto和decltype¶
auto原理:即泛型推导
auto失效场景:
- 代理对象:如vector<bool>::reference
的位特化实现
- 初始化列表歧义
- 依赖上下文类型 T::iterator
- 类型退化:数组指针,函数指针
- cosnt和volatie丢失
- 避免返回局部引用->显示返回类型
18.⭐STL sort实现¶
- 默认快速排序
- 数据量过小:插入排序
- 避免递归深度过深,堆排序
- 能对哪些容器进行排序?
19. emplace_back设计思想¶
20.友元函数,友元类¶
21.Constexpr 编译期常量¶
- 替代#define和const,具备类型安全和作用域
- 强制编译期计算,避免运行时开销,传参到函数直接获得答案替换成常量。(支持模版元编程简化逻辑,允许编译期构造)
- C11单行,C14局部变量与循环,C17编译条件分支,C20允许虚函数和动态类型分配。
- 默认的隐式inline,可以但没必要一起写,关注编译期。inline关注运行时。
23.volatile是什么?¶
类型修饰符,告诉编译期每次必须取地址,防止一些过度的优化(如循环直接把之前的值拿过来用)导致的问题。