跳转至
随着知识点越来越多,记忆难免会混乱、遗忘,一个好的知识点总结能有效帮助回忆。

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
  • 万能引用(通用引用):模板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是什么?

类型修饰符,告诉编译期每次必须取地址,防止一些过度的优化(如循环直接把之前的值拿过来用)导致的问题。

Comments