C++重点知识梳理(下)
目录
C++继承与多态要点
继承
多态
C++模板
模板参数:
函数模板与类模板:
模板特化:
分离编译:
C++异常
C++11
列表初始化:
类型推导相关:
范围for:
空值指针:
override以及final:
delete或default:
新增容器:
智能指针:
右值引用:
lambda:
C++继承与多态要点
继承
概念:面向对象编程中代码复用的重要手段,它允许程序员在保有原来类特性的基础上进行功能增加、参数拓展。因此产生的类称之为衍生类,这种层次结构,体现了由简单到复杂的设计过程。
作用:实现多态、代码复用等。
继承的方式:public、protected、private。其中不种继承方式会影响继承参数函数的使用。
继承权限:struct默认的继承权限为public(为兼容C),class默认为private。
class中函数的三种关系:基类与子类是不同的作用域。
-
重写:子类中,函数与
🐔(基)类中的virtual函数完全相同,用于实现多态。 -
重载:同一作用域,同名,返回值类型可不同,参数列表必须不同。
-
隐藏:子类中,同名函数(参数一样)。
父类与子类的赋值兼容规则(public继承):
-
子类对象的指针或引用可以赋值给基类的指针或引用。
-
父类对象的指针或引用赋值给子类对象的指针或用于时,需要强转,非常不安全,可能会造成越界访问,需使用dynamic_cast来进行显式类型转换。
子类的构造以及析构顺序:父类构造 -> 子类构造 -> 子类析构 -> 父类析构。
多继承中的菱形继承:
-
为了解决二义性、数据冗余的问题,需要在构成菱形继承的开端,使用虚继承(virtual public className)。
-
虚继承中,会使用虚基类表来记录虚基类中成员的偏移量。
-
不要使用菱形继承,过于复杂,若可能尽量使用组合。
多态
概念:同一物体在不同场景的多种形态。
静态与动态多态:
-
静态多态:编译时确定,静态联编。
-
形式:函数重载、模板。
-
编译器通过传入参数的不同,根据实际调用(实例化调用)不同参数的函数。
-
-
动态多态:运行时,根据指针或引用指向不同类的对象,来调用具体的函数。
-
触发条件:
-
基类需有虚函数(virtual修饰的函数),并且在派生类中完成对其的重写。
-
通过基类的指针或引用访问子类对象,来调用虚函数。
-
-
多态的原理:
-
表现:当类中包含虚函数时,创建的对象会多出4个字节(头4个字节)来存储虚函数表的指针。
-
虚函数表的构建:
-
基类:在基类中若存在虚函数,则会按照虚函数的声明顺序,依次将虚函数地址填入虚函数表中。
-
子类:
-
在创建子类对象时,会先将基类的虚函数表拷贝一份,到子类虚函数表中。
-
若子类重写了某个虚函数,会将子类中重写的虚函数的地址,替换掉基类中相同虚函数在虚标中的地址,就是覆盖。
-
若子类中,增加了新的虚函数,则会将这些新增的虚函数按照声明顺序放到虚函数表的最后。
-
-
注:
-
在对象中存的是虚函数表的指针,这个指针指向的才是虚函数表(虚函数指针数组)。
-
普通函数在编译期间就已经确定了地址,而虚函数在编译期间只是确定了其在虚函数表中的偏移,它的地址是在运行时动态绑定的。
-
在调用过程中,通过对象内的vptr找到对应的虚函数表,再根据虚函数在表中偏移量找到实际地址,再行调用。
-
每个包含虚函数的类都包含一个独属于这个类的虚函数表,这个类的对象共用这个虚函数表。(基类和子类都有自己的)
重写-协变:在协变中,其返回值的类型必须是指针或引用,且子类返回的对象与基类返回的对象需构成继承关系。
多态调用与普通调用:
-
多态调用时,函数是由父类虚函数的函数定义与基类指针指向的子类或父类的函数实现组合而来,即多态调用时绝不重新定义函数。
-
普通调用时(子类指针或引用直接调用),则直接以子类函数的定义与实现为准。
析构函数多态:继承时基类的析构函数需加上virtual,因为析构函数同样构成重写,即使它们的函数名并不相同,因为在编译时期,它们的名字会被编译器特殊处理,替换为destructor,所以会构成重写。
override、final:
-
override在修饰虚函数时,会在编译时检查该重写的虚函数的语法是否正确,若不正确则会报错。
-
final在修饰类时,表示该类无法被继承,即使继承也无法实例出对象;在修饰虚函数时,表示该函数无法被重写。
抽象类:
-
概念:包含纯虚函数的类就是抽象类(接口类)。
-
纯虚函数:在虚函数后加上
= 0(如,virtual int comp(int a) = 0;),不需要实现,因为继承它的子类必须重写这个虚函数。 -
抽象类不能实例出对象,但可以定义抽象类的指针。
-
不重写纯虚函数的子类仍是抽象类。
inline函数:
-
inline函数可以是虚函数,但是意义不大。
-
inline注重的优化运行时速度,而virtual注重的是多态。由于virtual动态绑定的特性,inline通常会被编译器忽略,除非是显示的调用。
-
若设计inline虚函数,不仅可读性变低,编译器也不一定会按inline将函数内敛,实属没必要。
静态成员函数和构造函数不能是虚函数,第一是语法上的错误,static不能与virtual不能一起用,构造函数不能是虚函数;第二是对象是通过this指针来访问虚函数表的,而static函数无this指针且是直接访问static函数的地址的,且虚函数表是在对象完全构造后才建立的。
虚函数表的存储:
-
编译期间生成,其通常存放与静态存储区,如.data数据段、.rodata只读数据段或.text代码段。
一个类对象可能存在多个虚函数表(根据编译器的实现而定):
-
GCC/Clang中,多继承时,对象会包括多个vptr来指向不同的虚函数表。
-
MSCV中,可能会采取“基类指针调整”策略,通过主vptr访问派生类虚函数表,其它基类的虚函数通过偏移量访问(具体实现取决于编译器)。
多态的缺点:
-
由于其实时绑定的特性,会比直接调用函数多1-2次内存访问和1次间接跳转,增加性能损耗。
-
虚函数表会带来额外的空间占用。
-
继承层次不可太高,否则结构过于复杂,耦合度过高。
相对于多态的优点,在实现时除非带来过大损耗,仍是可以使用多态。
C++模板
-
C++泛型编程:通过编写与类型无关的程序,来实现代码复用的手段。
模板参数:
-
类型模板参数:在模板参数列表中,在class或typename之后的参数类型名称。
template<class T1, typename T2, ......,typename Tn> //class与typename在这没有区别
函数或类
-
非类型模板参数:与类型模板参数不同,它可以在编译器传值,是编译时常量,会在模板实例化时被替换为具体值。
-
允许的类型有:整型、枚举类型、指向静态资源的指针或引用、浮点数 (C++20)。允许给予默认值。
-
在C++17之后,允许使用auto推导。
-
template <int a>
template <typename T, int b= 64>
函数模板与类模板:
-
概念:函数模板代表了一系列函数,这个模板与类型无关,在编译期间,编译器会根据传入的参数,来实例出相应的函数。
template<typename T1, typename T2, ......,typename Tn>
返回类型 函数名(参数列表){函数体}
-
隐式实例化:在编译器编译器会根据传入的参数,来推导模板的类型,然后生成代码,生成代码的时候,不会进行隐式类型转换。
-
显式实例化:也可以显式实例化,在编写代码的时候,直接指定<>中的类型,若参数类型不匹配,则尝试进行隐式类型转换,若转换成功则调用函数,失败则编译报错。
template<size_t T>
返回类型 函数名(参数列表){函数体}
-
模板实例化运行步骤:编译时才会对函数模板进行实例化,未实例化前只会对语法进行简单检查。
-
会先检查普通函数中是否存在匹配的函数,存在就调用;否则↓。
-
然后检查是否存在匹配的函数模板,若存在,则在编译时期根据传入的参数,来推导类型实例化函数模板,之后调用;若不存在适合的函数模板↓。
-
报错。
-
-
类模板:与函数模板类似。
模板特化:
-
概念:对于模板,它带来了编写代码的便捷性,但对特殊的类型可能会推导出错误的结果,因此模板需要有一些特殊的实现,即对模板的部分类型进行特化处理。
-
全特化:在template的<>内不留参数,在类或函数后加上<>以及相应的类型。
template<>
函数名<int, double>(int a,double b){}
template<>
类名<int, int*>
{}
-
偏特化:在template<>中留有参数,在类或函数后加上<>以及参数类型。函数模板不支持偏特化,可以通过重载来实现。
template <参数列表> // 保留部分模板参数
class/struct 模板名<特化参数> {// 特化实现
};template <typename T>
class Handler {
public:void process(T val) { /* 通用处理 */ }
};// 偏特化:处理指针类型
template <typename T>
class Handler<T*> {
public:void process(T* ptr) {if (ptr) std::cout << *ptr; // 指针有效性检查}
};template <typename T1, typename T2>
class PairPrinter {
public:void print(T1 a, T2 b) { std::cout << a << "-" << b; }
};// 偏特化:两个类型相同
template <typename T>
class PairPrinter<T, T> {
public:void print(T a, T b) { std::cout << "Same: " << a << "," << b; }
};
-
优先级:全特化 > 偏特化 > 通用模板。编译器根据实参类型自动匹配最特化的版本。
分离编译:
-
概念:将项目分为多个编译单元(.h和.cpp),核心目标是减少重复编译、提高构建速度,并增强代码的可维护性。C++20引入模块来对编译时间进行优化。
-
模板必须在头文件中实现,模板实例化发生在编译期,每个编译单元需独立生成代码,因此必须看到完整定义。
C++异常
-
程序终止的方式:
return、exit()、_Exit()、abort()、未被捕获的异常、std::terminate、信号终止、断言、堆栈溢出等等。 -
传统处理错误的方式:暴力终止程序、返回错误码、C标准库中的setjmp和longjmp等等。
-
异常概念:异常是一种运行时错误机制,C++提供了一种异常处理方式,try将可能存在异常通过throw抛出,再由catch来捕捉,通过这种方式避免程序崩溃或发生UB。
-
实现方式:
-
try:将可能抛出异常的代码放在try中,再由try之后的catch来捕获抛出的异常。
-
throw:负责抛出异常。
-
catch:按照类型捕获异常。
-
-
异常的规则:
-
异常并不是将异常对象直接抛出,而是将其副本抛出。
-
异常是按照类型捕获的,所以一般不会进行类型转化。
-
距离抛出异常最近位置的catch会优先捕获到异常。
-
在工程实现时,一般是通过自定义类型将异常抛出和捕获的,具体错误抛出的具体类型的异常对象,然后catch通过其基类的引用来讲其捕获。
-
异常的重新抛出:在捕获到异常的catch中,不会处理这个捕获到的异常,而是需要通过捕获这个动作来完成其它事,然后将异常继续抛出,让外部的catch来处理这个异常。
-
-
栈展开:
-
概念:当异常被抛出后,C++会在运行时从抛出异常的起始点,沿函数调用栈反向回溯,依次销毁函数调用链中的局部对象,一直到找到匹配的catch块或终止程序。
-
每个函数退出时,都会调用局部对象的析构函数,但是要保证这些析构函数是异常安全的(不会再抛出新的异常),因为析构函数中抛出的异常没在内部被捕获,C++就会调用
std::terminate终止程序。 -
栈展开会涉及到运行时栈扫描、析构函数调用等等可能会带来性能损耗,若对性能有性能有要求,使用错误码或std::optional等替代方案会更好。
-
-
在C++中,异常处理不当可能引发多种程序安全性问题,包括资源泄漏、状态不一致、拒绝服务(DoS)攻击以及安全漏洞等等。
-
自定义异常类:
-
通过继承
std::exception或其子类(如std::runtime_error),结合上下文信息与资源管理,能显著提升程序的健壮性与可维护性。 -
可以在自定义异常类中携带上下文信息,就是添加变量来记录错误码、文件名、行号等等的信息。
-
自定义异常类的析构函数不可抛出异常,最好标记为noexcept。
-
如果是需要用动态内存管理(如使用std::string等等的容器),需要正确实现拷贝、移动构造以及赋值重载。
-
还可以重写
what()函数来返回错误信息,支持多线程下的安全访问。
-
C++11
列表初始化:
-
C++98:仅支持内置类型(数组、结构体)和类的成员列表初始化,不支持容器,而且初始化方式多样(直接初始化、拷贝初始化等等),缺乏统一语法。
-
C++11:对所有类型都支持列表初始化,可以省略=,对于自定义类型,其本质是类型转换,中间产生的临时对象会被优化成直接构造。
-
容器支持:通过
std::initializer_list构造函数直接初始化容器。 -
std::initializer_list类:底层是一个数组,有两个指针,分别指向开头和结尾,然后容器只需支持一个std::initializer_list的构造函数,即可通过std::initializer_list来完成构造初始化,实际是将std::initializer_list中的数据拷贝到容器,会被直接优化成构造。例如↓。
-
template<class T>
class vector
{
public:typedef T* iterator;vector(initializer_list l) { for (auto e : l) push_back(e);}
}
-
std::initializer_list禁止可能导致数据丢失的窄化转换,如int a{3.14},会编译报错。
类型推导相关:
-
auto:编译器通过初始化表达式来推导类型。在有些时候编译器为了更加符合初始化规则,会适当修改推导的结果类型。
-
auto会忽略顶层const而保留底层const。需要时,需明确指出,即const auto。
-
auto推导的数组/函数退化为指针。
-
不能单独用于推导函数返回类型。
-
auto不会自动推导出引用类型。
-
-
decltype:用于获取表达式的精确类型。
-
decltype可以通过函数来推导出类型,如
decltype(func()),但是不会去调用函数。 -
decltype与auto不同,它可以推导出const以及引用类型,它也会保留顶层const。
-
decltype推导的解引用类型会推导成引用类型(
decltype(T*)会推出T&),在推导括号表达式时也会退出引用类型(decltype((T))推出T&)。即左值表达式会推出T&,右值表达式会推出T。 -
可以配合auto来推导函数的返回值:
auto func() -> decltype(x + y){},在后续C++标准中这个组合更加灵活。
-
范围for:
-
C++11中的范围for以更加简便的方式来实现对可遍历容器的自动遍历。它通过隐藏的begin()以及end()迭代器来实现自动遍历。提升代码可读性。
语法
for (declaration : expression) { //declaration:循环变量声明// 循环体 //expression:可遍历的序列
}string str = "cfkzyq";
for(char c: srt){std::cout << c; //cfkzyq
}int arr[] = {1, 2, 3, 4, 5};
for(auto& c: arr){c += 1;std::cout << c; //23456
}
底层实现
auto &&__range = expression; // 获取表达式引用
auto __begin = __range.begin(); // 起始迭代器
auto __end = __range.end(); // 结束迭代器
while (__begin != __end) {declaration = *__begin; // 提取当前元素// 循环体++__begin;
}
空值指针:
-
C++11引入了nullptr,是类型安全的空指针常量,不会被强转成int。之前的NULL被定义为void*(0),而void无法隐式转化为其它类型指针,void*(0)会指向0x00000000。C++将其定义为void(0)来解决这个问题,因此引入了nullptr来解决指针问。nullptr的类型为
std::nullptr_t,typedef decltype(nullptr) nullptr_t;其底层实现更加复杂。std::nullptr_t可以隐式转换为void*,而void*不能隐式转换为std::nullptr_t。
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif
#endif
override以及final:
-
override: 显式标记虚函数重写,编译器在会检查这个被标记的函数是否重写了基类的虚函数(函数名称、参数列表、返回类型、const限定符等需完全匹配),若不符合或找不到基类虚函数则编译报错。 -
final:被final标记则不可再被重写或继承。被标记final的函数可以避免动态绑定,且可能会被编译器内敛,优化性能。
delete或default:
-
delete、default:使用= delete显式禁止默认成员函数生成。使用= default显式声明成员函数,让编译器自己实现。
新增容器:
-
序列式容器:
-
array:封装了原生数组的顺序表,提供STL接口。 -
std::forward_list:单向循环链表。
-
-
关联式容器:
容器名称 存储元素类型 键唯一性 键值对结构 底层结构 适用场景 std::unordered_set 唯一键 唯一 键(Key) 哈希表 快速查找、去重(如单词计数、缓存键) std::unordered_map 键值对(Key-Value) 键唯一 std::pair<const Key, Value> 哈希表 快速键值映射(如配置字典、缓存) std::unordered_multiset 可重复键 允许重复 键(Key) 哈希表 允许重复元素的集合(如日志记录、多值查询) std::unordered_multimap 可重复键值对 允许重复键 std::pair<const Key, Value> 哈希表 允许重复键的映射(如多属性索引、历史记录)
智能指针:
-
概念:RAII本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏。智能指针是C++中用于动态管理资源的类模板,不仅满足了RAII的设计思想,还要方便资源的访问,所以重载了
operator*/operator->/operator[]等运算符。 -
auto_ptr:是C++98设计出来的指针,它在拷贝时,通过把被拷贝资源的管理权转移给拷贝对象来转移资源的管理权,但是会把被拷贝的对象悬空,造成访问错误。
-
unique_ptr:原理是一份资源只能被一个unique_ptr管理,不允许拷贝,同时也不能多个对象共享资源。
-
shared_ptr:原理是通过引用计数的方式来实现多个对象之间的资源共享,不过可能会出现循环引用的问题,可以使用weak_ptr来解决。weak_ptr支持
expired来检查指向资源是否过期,以及use_count也可获取shared_ptr的引⽤计数。
template<class T>
struct ListNode{std::shared_ptr<ListNode> next;std::weak_ptr<ListNode> prev;......
}
右值引用:
-
右值引用:它用于绑定到临时对象,来支持完美转发和移动语义。
-
左值与右值:
-
左值:⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
-
右值:是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
-
-
与左值引用的区别:
| 特性 | 左值引用 (&) | 右值引用 (&&) |
| 绑定对象 | 左值(变量、持久对象) | 右值(临时对象、字面量) |
| 修改对象 | 可以 | 可以(通常用于“窃取”资源) |
| 典型用途 | 避免拷贝(const &) | 移动语义、完美转发 |
| 示例 | int& lref = x; | int&& rref = 42; |
-
右值对象不能直接引用左值对象,否则会编译报错,可以通过std::move强制将左值转换为右值引用,从而实现移动语义。
-
std::move:可以将左值强制转换为右值引用,从而启用移动语义,避免不必要的深拷贝开销,提升性能。移动操作通常标记为
noexcept,即不会抛出异常。 -
完美转发:在Function(T&&t)函数模板中,传入左值对象则实例化后为左值引用的函数,传入右值则是右值引用版本的函数。
-
完美转发forward本质是一个函数模板,它本质是通过引用折叠来实现。
-
// 处理左值引用(T为左值引用类型时调用)
template <typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {return static_cast<T&&>(t); // 转换为左值引用或右值引用
}// 处理右值引用(T为非引用类型时调用)
template <typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept {static_assert(!std::is_lvalue_reference<T>::value, "Cannot forward an rvalue as an lvalue");return static_cast<T&&>(t); // 转换为右值引用
}
lambda:
-
概念:lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
语法
[capture-list] (parameters)-> return type { function boby }
-
lambda捕捉列表:lambda表达式存在一个捕捉列表,规则如下。
| 捕获模式 | 语法 | 行为 | 适用场景 |
| 值捕获 | [x] 或 [=] | 复制变量值,与外部变量独立 | 需避免悬空引用的小对象 |
| 引用捕获 | [&x] 或 [&] | 捕获引用,访问实时值 | 对象生命周期长于 lambda |
| 混合捕获 | [x, &y, z=expr] | 结合值、引用和初始化捕获 | 复杂场景,如移动语义、常量表达式 |
| 捕获 this | [this] 或 [=] | 访问类成员 | 成员函数中的 lambda |
| 初始化捕获 | [x=expr] | 捕获时初始化变量 | 移动语义、常量表达式 |
-
lambda不存在类型,所以在定义lambda对象时使用auto来声明。
-
原理:通过反汇编可以观察调用lambda的底层是调用仿函数的
operator(),那么捕捉列表也是lambda的变量也是lambda的实参。 -
与仿函数的区别:
| 特性 | Lambda 表达式 | 仿函数(Functor) |
| 语法简洁性 | ✅ 匿名内联,代码紧凑 | ❌ 需显式定义类,冗余 |
| 捕获外部变量 | ✅ 灵活(值/引用/初始化捕获) | ❌ 需通过成员变量显式管理 |
| 性能 | ⚠️ 编译器优化后接近仿函数 | ✅ 无闭包开销,性能稳定 |
| 灵活性 | ❌ 不支持继承、多态 | ✅ 支持虚函数、模板、策略模式 |
| 调试与维护 | ❌ 匿名导致可读性下降 | ✅ 显式命名,易于维护 |
| 适用场景 | 简单逻辑、临时使用、STL 算法 | 复杂状态、多态、高性能计算、长期维护 |
