概念:面向对象编程中代码复用的关键方式,它允许开发者在保持原有类特性的基础上扩展功能、增加参数。因此产生的新类称为派生类,这种层次结构体现了从简单到复杂的设计理念。
作用:实现多态和代码复用等。
public、protected、private。不同的继承方式会影响继承成员函数的使用权限。
struct 默认的继承权限为 public(为了兼容 C),而 class 的默认继承权限是 private。
在类中,基类与派生类的关系表现为不同的作用域。
在子类中,函数需与基类中的 virtual 函数完全一致,用于实现多态。
在同一作用域内,同名但返回类型可以不同,参数列表必须有所不同。
在子类中,存在同名函数(参数相同)的情况。
父类与子类的赋值兼容规则(public 继承):子类对象的指针或引用可赋予基类的指针或引用。而将基类对象的指针或引用赋给子类时,需要强制转换,这非常不安全,可能会导致越界访问,应使用 dynamic_cast 进行类型转换。
子类的构造与析构顺序:先父类构造 -> 子类构造 -> 子类析构 -> 父类析构。
概念:同一实体在不同情境下的多种表现形式。
静态与动态多态:
基类需有虚函数(使用 virtual 关键字修饰),并且在派生类中完成重写。通过基类的指针或引用来访问子类对象,从而调用虚函数。
表现:当类包含虚函数时,创建的对象会额外占用 4 字节(前 4 字节)来存储指向虚函数表的指针。
虚函数表的构建:
对象中存储的是指向虚函数表的指针,这个指针实际上指向一个数组。普通函数在编译期间确定了具体地址,而虚函数仅确定其在虚函数表中的偏移量,实际地址是在运行时动态绑定的。
调用过程中,通过对象内的 vptr 找到对应的虚函数表,根据虚函数在表中的偏移找到具体地址,然后进行调用。每个包含虚函数的类都有一个专属于该类的虚函数表,所有该类的对象共享这个表(基类和子类分别有自己的表)。
在协变中,返回值类型必须是指针或引用,并且子类返回的对象需与基类返回的对象构成继承关系。
多态调用时,函数由父类虚函数的定义和基类指针指向的具体对象(子类或父类)实现组成。而普通调用时,则直接使用子类函数的定义与实现。
在继承时,基类的析构函数应加上 virtual 关键字,因为即使它们的函数名不同,编译器也会对其进行特殊处理以确保正确重写。
使用 override 修饰虚函数时,编译器会在编译期间检查重写的虚函数语法是否正确。若不正确,则会报错。final 关键字用于类或虚函数,表明该类不可被继承,或该函数不可被重写。
概念:包含纯虚函数的类即为抽象类(接口类)。
纯虚函数:在虚函数声明后加上 标记(例如,= 0),无需实现。因为继承该类的子类必须重写这些虚函数。virtual int comp(int a) = 0;
抽象类不能实例化对象,但可以定义指向抽象类的指针。未重写纯虚函数的子类仍然是抽象类。
inline 函数可以是虚函数,但这在实际应用中意义不大。因为 inline 旨在优化运行时速度,而 virtual 则侧重于实现多态性。由于 virtual 的动态绑定特性,通常编译器会忽略 inline 关键字,除非显式调用。
若设计内联虚函数,不仅可读性会降低,编译器也不一定会按照inline将函数内联,实属不必要。
静态成员函数和构造函数不能是虚函数。首先,这是语法上的错误,static与virtual不能同时使用,构造函数也不能是虚函数;其次,对象通过this指针访问虚函数表,而static函数没有this指针且直接访问static函数的地址,而且虚函数表是在对象完全构建后才建立的。
虚函数表的存储:编译期间生成,通常存放在静态存储区,如.data数据段、.rodata只读数据段或.text代码段。一个类对象可能存在多个虚函数表(取决于编译器实现):
GCC/Clang中,多继承时,对象包含多个vptr来指向不同的虚函数表。
MSVC中,可能采用“基类指针调整”策略,通过主vptr访问派生类的虚函数表,其他基类的虚函数则通过偏移量访问(具体实现取决于编译器)。
多态的缺点:由于其实时绑定的特点,会比直接调用函数增加1-2次内存访问和1次间接跳转,导致性能损耗。虚函数表也会占用额外的空间。继承层次不应太高,否则结构过于复杂,耦合度高。
相对于多态的优点,在实现时不带来过大损耗的情况下,仍可使用多态。
C++模板:C++泛型编程:通过编写与类型无关的程序来实现代码复用。
模板参数:
类型模板参数:在模板参数列表中,在class或typename之后的参数类型名称。
template<class T1, typename T2, ......,typename Tn> //class与typename在这没有区别
函数或类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 <参数列表> // 保留部分模板参数
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捕获,以此避免程序崩溃或发生未定义行为。
实现方式:try:将可能抛出异常的代码置于try块中,之后通过try后的catch捕获抛出的异常。
throw:负责抛出异常。
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;
其底层实现更加复杂。
#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支持
expireduse_counttemplate<class T>
struct ListNode{
std::shared_ptr<ListNode> next;
std::weak_ptr<ListNode> prev;
......
}
用于绑定临时对象,支持完美转发和移动语义。
左值:
表示数据的表达式(如变量名或解引的指针),通常具有持久状态并存储在内存中。可以获取其地址,左值可以在赋值符号左侧或右侧出现。当定义为const修饰时,不可对其赋值但可取址。
右值:
表示数据的表达式,要么是字面常量、要么是在求值过程中创建的临时对象等,只能出现在赋值操作符右侧,不能取地址。
| 特性 | 左值引用 (&) | 右值引用 (&&) |
|---|---|---|
| 绑定对象 | 左值(变量、持久对象) | 右值(临时对象、字面量) |
| 修改对象 | 可以 | 可以(通常用于“窃取”资源) |
| 典型用途 | 避免拷贝(const &) | 移动语义、完美转发 |
| 示例 | int& lref = x; | int&& rref = 42; |
右值对象不能直接引用左值对象,否则会导致编译错误。可通过std::move强制将左值转换为右值引用,从而启用移动语义。
可以将左值强制转换成右值引用,以启动移动语义,避免不必要的深拷贝开销,提高性能。移动操作通常标记为
noexcept在Function(T&& t)函数模板中,传入左值对象时实例化为左值引用的版本,传入右值则为右值引用版本。本质是通过引用折叠实现的。
// 处理左值引用(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表达式本质上是一个匿名函数对象,与普通函数不同的是它可以定义在函数内部。
语法
[capture-list] (parameters)-> return type { function boby }
lambda捕捉列表:
存在一个捕捉列表,规则如下。
| 捕获模式 | 语法 | 行为 | 适用场景 |
|---|---|---|---|
| 值捕获 | [x] 或 [=] | 复制变量值,与外部变量独立 | 需避免悬空引用的小对象 |
| 引用捕获 | [&x] 或 [&] | 捕获引用,访问实时值 | 对象生命周期长于 lambda |
| 混合捕获 | [x, &y, z=expr] | 结合值、引用和初始化捕获 | 复杂场景,如移动语义、常量表达式 |
| 捕获 this | [this] 或 [=] | 访问类成员 | 成员函数中的 lambda |
| 初始化捕获 | [x=expr] | 捕获时初始化变量 | 移动语义、常量表达式 |
lambda无固定类型,因此定义时使用auto声明。
通过反汇编可以观察到调用lambda的底层是调用仿函数的
operator()| 特性 | Lambda 表达式 | 仿函数(Functor) |
|---|---|---|
| 语法简洁性 | 匿名内联,代码紧凑 | 需显式定义类,较为冗余 |
| 捕获外部变量 | 灵活(值/引用/初始化捕获) | 需通过成员变量管理 |
| 性能 | 编译器优化后接近仿函数 | 无闭包开销,性能稳定 |
| 灵活性 | 不支持继承、多态 | 支持虚函数、模板、策略模式 |
| 调试与维护 | 匿名导致可读性下降 | 显式命名,易于维护 |
| 适用场景 | 简单逻辑、临时使用、STL 算法 | 复杂状态、多态、高性能计算、长期维护 |
扫码加好友,拉您进群



收藏
