C++考试大纲
第一部分:C++语言核心与基础
1. C++程序基本框架与结构
基本框架
1
2
3
4
5
6
7
int main(int argc, char* argv[]) { // 程序入口点
// 你的代码
std::cout << "Hello, World!" << std::endl;
return 0; // 返回值给操作系统
}- 原理深度剖析:
#include:在编译的预处理阶段,预处理器会找到指定的头文件,并将其内容原封不动地复制到#include指令所在的位置。这本质是一种文本替换。main函数:是操作系统加载程序后执行的起点。argc和argv由操作系统填充,argc是参数个数,argv是一个指向字符串指针的数组,存储着具体的参数。返回值return 0;被传递给操作系统,表示程序正常退出(非0值通常表示错误)。
- 原理深度剖析:
模块化与头文件保护
模块化:将声明(
.h/.hpp)与定义(.cpp)分离。.cpp文件是独立的编译单元。头文件保护:
1
2
3
4
// ... 头文件内容 ...- 底层原理:防止由于一个头文件被多个源文件包含而导致的重复定义错误。预处理器维护一个已定义宏的集合。当第一次遇到
#ifndef MY_HEADER_H时,宏未定义,条件为真,于是定义宏并包含内容。后续再遇到同一个头文件时,因为宏已定义,条件为假,整个内容都会被跳过。 - 现代替代品:
#pragma once。这是一个编译器指令(非标准,但被广泛支持),功能相同但更简洁。编译器内部会记录每个文件的唯一标识,遇到重复的#pragma once文件则直接跳过。其效率通常比#ifndef更高,因为它不需要解析整个文件内容。
- 底层原理:防止由于一个头文件被多个源文件包含而导致的重复定义错误。预处理器维护一个已定义宏的集合。当第一次遇到
2. C++现代词汇
nullptr(C++11)- 解决的问题:C++中
NULL通常被定义为0。在函数重载时,func(int)和func(char*),调用func(NULL)会歧义地调用func(int),而不是预期的指针版本。 - 原理与底层:
nullptr是std::nullptr_t类型的字面量,它可以隐式转换为任何指针类型,但不能转换为整数类型。这从类型系统层面消除了歧义。底层上,它通常就是一个表示空地址的值(如0),但其类型是关键。
- 解决的问题:C++中
constexpr(C++11)核心思想:将计算从运行时转移到编译时。
底层原理:
- 声明为
constexpr的变量或函数,编译器会在编译期间验证并(如果可能)计算其值。 - 对于
constexpr变量,其值必须由编译器已知的常量表达式初始化。它进入了C++的常量表达式世界。 - 对于
constexpr函数,当其参数是常量表达式时,它会在编译期被计算;否则,它就像一个普通函数一样在运行时被调用。 - C++14/C++17/C++20 极大地放宽了
constexpr函数内部的限制(允许循环、局部变量等)。
- 声明为
示例与优势:
1
2
3
4
5
6
7constexpr int factorial(int n) { // 编译期阶乘
return n <= 1 ? 1 : (n * factorial(n - 1));
}
int main() {
int arr[factorial(5)]; // 数组大小在编译期确定,栈上分配
// 等价于 int arr[120];
}这避免了运行时计算,提升了性能,并允许在需要常量表达式的上下文中(如数组大小、模板参数)使用函数。
noexcept(C++11)- 作用:
- 异常规范:声明函数不会抛出异常。
- 运算符:在编译期检查一个表达式是否声明为不抛异常。
- 底层原理与优化:
- 从语言机制上,如果
noexcept函数抛出了异常,程序会直接调用std::terminate终止,而不是正常的栈展开。 - 关键优化:
std::vector在重新分配内存(realloc)时,需要将旧元素移动或复制到新内存。如果元素的移动构造函数是noexcept的,vector会优先使用更高效的移动操作;否则,为了强异常安全保证,它必须使用可能更慢的复制操作。这是因为移动操作可能会“掏空”源对象,如果中途抛出异常,源对象已损坏,无法恢复。因此,为移动操作标记noexcept是重要的性能优化手段。
- 从语言机制上,如果
- 作用:
类型推导 (
auto&decltype)auto原理:编译器根据初始化器(等号右边的表达式)来推导变量的类型。它遵循模板参数推导的规则。
底层:这纯粹是编译期的行为,不会产生任何运行时开销。生成的代码与您手动写出类型完全一样。
注意:
auto会忽略引用和顶层const,如果需要,需手动加上&或const。1
2
3const int ci = 0;
auto b = ci; // b 是 int (const被忽略)
auto& c = ci; // c 是 const int&
decltype原理:查询一个实体或表达式的声明类型。它严格返回表达式的确切类型,包括引用和
const限定。规则:
decltype(entity):直接返回实体的声明类型。decltype(expression):如果表达式是左值,返回T&;如果是右值,返回T。
应用:常用于模板库编写、函数返回类型推导(C++14后置返回类型)
1
2
3int i = 0;
decltype(i) a; // a 是 int
decltype((i)) b = i; // b 是 int& (因为(i)是一个左值表达式)
结构化绑定 (C++17)
作用:从数组、结构体或元组等聚合类型中一次性解包多个变量。
示例:
1
2
3
4
5
6std::pair<int, std::string> getPair() { return {42, "hello"}; }
auto [id, name] = getPair(); // id是int, name是std::string
struct Point { double x, y; };
Point p{1.0, 2.0};
auto& [x, y] = p; // x和y是double&, 绑定到p的成员底层实现:编译器会生成一个匿名对象。对于
auto [a, b] = expr;,其行为类似于:1
2
3auto __anonymous = expr; // 创建一个临时对象
auto& a = __anonymous.<member0>; // 绑定到第一个成员
auto& b = __anonymous.<member1>; // 绑定到第二个成员- 注意,
a和b是这个匿名对象的成员的别名,而不是直接绑定到expr的成员,除非你使用auto&。
- 注意,
第二部分:数据类型、表达式和基本运算
1. C++类型系统
- 基本类型
- 底层内存表示:
int,char,bool等的大小由实现定义,但C++标准规定了最小尺寸(如int至少16位)。sizeof运算符可以获取其字节数。- 浮点数
float/double通常遵循IEEE 754标准,在内存中以符号位、指数位、尾数位的形式存储。
- 底层内存表示:
- 指针
- 本质:一个存储内存地址的整数变量。其类型决定了编译器如何解释该地址开始的内存数据(“步长”和“视野”)。
- 底层:在32位系统上通常是4字节,64位系统上是8字节。
- **
void***:无类型指针,仅存储地址。必须强制转换后才能解引用。
std::string- 底层实现原理(常见):
- 短字符串优化(SSO):这是现代
std::string实现的关键。对象内部有一个固定大小的缓冲区(例如16字节)。如果字符串很短,直接存储在这个缓冲区里,避免了堆分配。如果字符串过长,则会在堆上分配内存,内部指针指向堆内存。 - 内存布局:通常包含:大小(
size)、容量(capacity)和一个指向数据的指针(或对于短字符串,直接利用这些字节存储数据本身)。SSO使得在栈上创建短字符串或在容器中存放大量短字符串时,性能极高。
- 短字符串优化(SSO):这是现代
- 底层实现原理(常见):
std::byte(C++17)- 目的:表示内存的原始字节,而非字符。它使代码的意图更清晰。
- 本质:一个枚举类,只有按位运算符被重载。不能进行算术运算(如
+,-),强制程序员在操作前将其转换为适当的类型,提高了类型安全。
2. 常量与变量
- 作用域规则
- 全局/局部:由编译器通过符号表管理。局部变量通常在栈帧上,生命周期随栈帧的创建和销毁而开始和结束。全局/静态变量在程序的静态存储区,生命周期贯穿整个程序。
thread_local- 原理:每个线程都拥有该变量的一个独立实例。
- 底层实现:操作系统或运行时库提供了线程局部存储(TLS) 机制。编译器会为每个
thread_local变量生成一个密钥,访问时通过一个内部函数(如pthread_getspecific或Windows的TlsGetValue)来获取当前线程对应的变量地址。这比访问普通全局变量慢,但实现了线程间的数据隔离。
- 统一初始化 (
{}语法)- 形式:
T obj{arg1, arg2, ...}; - 优势:
- 防止窄化转换:
int x{5.0};会编译错误,而int x(5.0);只会警告。 - 避免“最令人烦恼的解析”:
TimeKeeper tk(Timer());可能被解析为函数声明,而TimeKeeper tk{Timer{}};则明确是对象初始化。 - 对所有类型一致:可以初始化数组、容器、聚合类等。
- 防止窄化转换:
- 底层:编译器会尝试匹配构造函数,包括
std::initializer_list构造函数。如果存在std::initializer_list参数的构造函数,且参数能转换,编译器会强烈优先匹配它。
- 形式:
3. 表达式与运算符
表达式求值顺序
重要规则:除了少数运算符(如
&&,||,,,?:),C++标准未规定函数参数和子表达式的求值顺序。1
2f(++i, ++i); // 行为未定义!
std::cout << i << i++; // 行为未定义!底层原因:给予编译器最大的优化自由,根据平台特性生成最高效的代码。
短路求值 (
&&和||)- 原理:对于
a && b,只有当a为真时才对b求值。对于a || b,只有当a为假时才对b求值。 - 底层实现:编译器通过条件跳转指令实现。这不仅是性能优化,更是逻辑上的常用技巧(如
if (p && p->isValid()))。
- 原理:对于
位运算
- 底层:直接对整数的二进制位进行操作。编译器会生成对应的CPU位操作指令(如
AND(按位与),OR(按位或),XOR(按位异或),SHL(左移),SHR(右移))。 - 应用:标志位处理、掩码、底层协议、优化(如用移位代替乘除2的幂)。
- 底层:直接对整数的二进制位进行操作。编译器会生成对应的CPU位操作指令(如
条件运算符 (
?:)- 底层:编译为条件跳转指令,类似于
if-else。
- 底层:编译为条件跳转指令,类似于
指针运算
p + n:计算地址(char*)p + n * sizeof(*p)。类型决定了步长。p1 - p2:计算两个指针之间的元素个数,((char*)p1 - (char*)p2) / sizeof(*p1)。
第三部分:C++的基本语句
1. 分支与循环结构
if与switch的底层实现if-else链:编译器生成一系列的比较和条件跳转指令。switch语句:- 跳转表:当
case标签密集且值较小时,编译器会创建一个静态数组(跳转表),每个索引对应一个目标地址。switch的值直接作为索引进行跳转,时间复杂度O(1),非常高效。 - 二分查找/if-else链:当
case标签稀疏时,编译器可能生成二分查找逻辑或退化为if-else链,时间复杂度O(log n)或O(n)。
- 跳转表:当
范围
for循环语法:
for (declaration : range) { ... }底层实现:编译器将其展开为基于迭代器的普通
for循环。1
2
3
4
5
6for (auto& x : vec) { ... }
// 等价于:
for (auto it = vec.begin(), end = vec.end(); it != end; ++it) {
auto& x = *it;
// ...
}要求:
range必须能调用begin()和end(),返回的迭代器支持!=,++,*操作。
2. 转向语句
break:直接跳出当前一层循环或switch。continue:跳过本次循环剩余部分,直接进入下一轮循环的条件判断。return:结束当前函数,并返回一个值(如果有)。- 底层实现:所有这些语句都编译为无条件跳转指令,跳转到程序代码的特定位置。
3. 异常处理
try/catch底层机制(概念模型)- 这是一个复杂的、与编译器ABI紧密相关的特性。其核心是栈展开。
- 当
throw发生时,异常对象被创建在一个特殊区域。 - 运行时系统接管控制权,它从当前函数开始,沿调用链向上查找匹配的
catch块。这个过程就是栈展开。 - 在栈展开过程中,所有局部对象的析构函数会被自动调用。这是RAII和异常安全的基础。
- 找到匹配的
catch后,栈展开停止,控制权转移到catch块。 - 如果未找到匹配的
catch,调用std::terminate。
实现成本:为了支持栈展开,编译器需要生成额外的“栈映射”数据,记录每个函数调用点上哪些局部对象需要析构,以及
catch块的位置。这增加了二进制文件的大小,并在正常路径(无异常抛出时)可能带来微小的性能开销。因此,异常通常只用于真正的、不可预期的错误情况。异常安全保证
- 基本保证:操作失败时,程序仍处于有效状态,无资源泄漏。所有不变量均保持。(例如,一个容器操作失败后,容器本身仍是可用的)。
- 强保证:操作要么完全成功,要么失败后程序状态回滚到操作调用前的状态。具有提交或回滚的语义。通常通过“拷贝-交换”惯用法实现。
- 无抛出保证:操作承诺永远不会抛出异常。所有内置类型和POD类型的操作都是无抛出的。如前所述,移动操作标记为
noexcept是提供强异常安全的关键。
第四部分:数组、指针与引用
1. 数组的底层原理
- 内存布局与地址计算
- 一维数组:在内存中是连续的线性序列。
arr[i]被编译器翻译为*(arr + i),其中arr是数组首元素的地址( decays to pointer)。 - 二维数组:本质是”数组的数组”。
int arr[M][N]在内存中按行优先顺序排列。arr[i][j]的地址计算为:(char*)arr + i * (N * sizeof(int)) + j * sizeof(int)。编译器在编译时就知道N,所以能直接计算偏移量。
- 一维数组:在内存中是连续的线性序列。
- 数组与指针的微妙关系
- 数组到指针的衰减:在大多数表达式中,数组名会退化为指向其首元素的指针。这是为什么
int arr[10]; int* p = arr;是合法的。 - 关键区别:
sizeof(arr):返回整个数组的字节大小。sizeof(p):返回指针变量的大小。&arr:产生一个指向整个数组的指针,类型是int(*)[10]。&arr + 1会跳过整个数组。&p:产生一个指向指针变量的指针,类型是int**。
- 数组到指针的衰减:在大多数表达式中,数组名会退化为指向其首元素的指针。这是为什么
2. 字符串:C风格 vs. C++风格
C风格字符串(字符数组)
- 本质:以空字符
'\0'结尾的字符数组。 - 函数原理:
strlen:从给定指针开始,线性扫描内存直到遇到'\0',计数。时间复杂度O(n)。strcpy_s/strcat_s:安全版本,要求传入目标缓冲区大小,防止缓冲区溢出。内部在拷贝前后会检查边界。strcmp:按字节比较ASCII值,直到遇到不同的字符或'\0'。strstr:实现子串查找,朴素算法复杂度O(n*m),但标准库实现可能使用KMP或Boyer-Moore等高效算法。
- 本质:以空字符
std::string(重温与深化)SSO的典型实现:一个
std::string对象内部可能包含:1
2
3
4
5
6union {
char* _ptr; // 指向堆内存(长字符串)
char _local[16]; // 本地缓冲区(短字符串)
};
size_t _size; // 当前字符串长度
size_t _capacity; // 已分配容量(对于长字符串)- 通过
_size或_local的某个字节来判断当前是短字符串模式还是长字符串模式。
- 通过
3. 引用:带语法糖的安全指针
- 本质:在底层,引用几乎总是通过指针来实现。编译器会为引用变量分配存储空间(存储所引用的地址)。
- 与指针的关键区别(语言层面):
- 必须初始化:引用在诞生时必须绑定到一个对象,不存在空引用。
- 不可重新绑定:一旦初始化,不能再指向其他对象。
- 语法便利:使用引用不需要解引用操作符
*,编译器自动处理。
- 底层视角:
int& r = x;生成的汇编代码与int* const p = &x;非常相似。r = 5;会被编译为*p = 5;。
4. 数据结构的构建(链表、树、栈)
指针单链表
1
2
3
4
5struct ListNode {
int val;
ListNode* next; // 自引用结构,存储下一个节点的地址
ListNode(int x) : val(x), next(nullptr) {} // 构造函数初始化
};- 内存模型:节点在堆上非连续分配,通过指针”链”在一起。插入/删除操作只需修改指针,时间复杂度O(1)(已知前驱节点),但随机访问需要遍历,O(n)。
二叉树
1
2
3
4
5
6struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};- 递归结构:树的定义是递归的,因此遍历(前序、中序、后序)最自然的实现方式是递归。递归调用会使用调用栈,深度过大时可能导致栈溢出。
栈的实现
基于数组:
1
2
3
4
5
6
7
8
9template<typename T>
class Stack {
private:
std::vector<T> elems; // 动态数组,自动管理内存
public:
void push(const T& e) { elems.push_back(e); }
void pop() { elems.pop_back(); }
T& top() { return elems.back(); }
};- 原理:利用
vector的尾部作为栈顶,push_back和pop_back都是摊销常数时间复杂度。
- 原理:利用
基于链表:
1
2
3
4
5
6
7
8
9
10
11
12template<typename T>
class Stack {
private:
struct Node { T data; Node* next; };
Node* top_ptr;
public:
void push(const T& e) {
Node* new_node = new Node{e, top_ptr}; // 堆分配,在头部插入
top_ptr = new_node;
}
// ... 需要手动管理Node的内存
};- 原理:每个
push都需要在堆上分配新节点,操作本身是O(1),但涉及系统调用,常数因子可能比数组版大。
- 原理:每个
第五部分:函数的有关使用
1. 函数调用机制与栈帧
- 栈帧(活动记录)
- 构成:每次函数调用,编译器都会在调用栈上压入一个栈帧,通常包含:
- 返回地址(调用结束后回到哪里)
- 调用者的栈帧指针
- 函数参数
- 局部变量
- 保存的寄存器
- 原理:通过栈指针(SP) 和帧指针(FP) 寄存器来管理。FP指向当前栈帧的基址,通过固定的偏移量访问参数和局部变量。
- 构成:每次函数调用,编译器都会在调用栈上压入一个栈帧,通常包含:
std::function与类型擦除- 问题:如何存储任意可调用对象(函数指针、Lambda、函数对象)?
- 原理:
std::function使用类型擦除技术。- 包装:它内部有一个模板构造函数,接受任意可调用对象。
- 多态:它创建一个派生自某个基类(如
__base_func)的模板类(如__derived_func<F>),该派生类重写了operator()和clone等虚函数。 - 存储:
std::function对象内部存储一个指向基类的指针(通常是unique_ptr)。调用时,通过这个指针进行动态分发(虚函数调用)。
- 开销:相比直接调用,有一次虚函数调用和可能的一次堆分配(对于小对象,优化实现可能使用小对象优化)。
2. 参数传递的深度解析
传值 (
void func(T t))- 底层:在栈上创建实参的完整副本。调用拷贝构造函数(对于类类型)。成本高。
传常量引用 (
void func(const T& t))- 底层:传递一个指针(引用的实现)到原对象。无拷贝。由于是
const,编译器保证函数内不会修改原对象。
- 底层:传递一个指针(引用的实现)到原对象。无拷贝。由于是
传引用 (
void func(T& t))- 底层:同样传递指针,但允许修改原对象。用于输出参数。
移动语义 + 传值 (
void func(T t)) 的现代用法场景:当函数内部无论如何都需要一个副本时(例如,存储到成员变量中)。
优势:
- 如果传入左值,会调用一次拷贝构造,与以前一样。
- 如果传入右值,会调用移动构造,将资源”偷”过来,成本极低。
示例:
1
2
3
4
5void setData(std::string data) { m_data = std::move(data); } // 注意最后的move!
// 调用:
setData("Hello"); // 传入字符串字面量(右值)-> 移动构造
std::string s = "World";
setData(s); // 传入左值 -> 拷贝构造- 这提供了一个”拷贝或移动”的统一接口,有时比提供
setData(const string&)和setData(string&&)两个重载更简洁。
- 这提供了一个”拷贝或移动”的统一接口,有时比提供
3. 函数高级特性
函数重载
- 原理:C++使用名称修饰 来在编译器层面区分不同重载。编译器将函数名、参数类型、命名空间等信息编码成一个唯一的链接器符号。例如,
void func(int)和void func(double)会被修饰成像_Z4funci和_Z4funcd这样的符号。
- 原理:C++使用名称修饰 来在编译器层面区分不同重载。编译器将函数名、参数类型、命名空间等信息编码成一个唯一的链接器符号。例如,
内联函数
- 原理:编译器将函数体直接展开到调用处,消除函数调用的开销(压栈、跳转、返回)。
- 代价与权衡:以代码膨胀为代价换取性能。适用于短小且频繁调用的函数。
inline关键字:在现代C++中,它主要是一个链接指令,表示允许在多个编译单元中定义相同的函数(通常放在头文件中),链接器会选取一个。
操作符重载
本质:就是特殊的成员函数或非成员函数,函数名是
operator@。实现为成员函数:
a @ b等价于a.operator@(b)。左操作数是this。实现为非成员函数:
a @ b等价于operator@(a, b)。通常需要声明为friend以访问私有成员。示例:
<<重载1
2
3
4// 非成员函数,用于输出自定义类型
std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
return os << obj.data_; // 需要friend或public访问
}
4. Lambda表达式
编译器魔法:生成匿名类
- 原理:对于每个Lambda,编译器会在当前作用域生成一个唯一的、匿名的类类型(闭包类型)。
- 捕获列表的映射:
[=]:所有捕获的变量成为该匿名类的值类型成员。编译器在构造函数中按值拷贝这些变量。[&]:所有捕获的变量成为该匿名类的引用类型成员。编译器存储这些变量的引用。[var]/[&var]:指定特定变量的捕获方式。
- 函数调用运算符:Lambda的函数体成为这个匿名类的
operator()。
mutable的作用- 默认行为:默认情况下,Lambda的
operator()是const的,这意味着你不能修改按值捕获的变量(它们被视为const成员)。 mutable:加上后,operator()变为非const,允许你修改按值捕获的变量(修改的是Lambda对象内部的副本,不影响外部变量)。
- 默认行为:默认情况下,Lambda的
泛型Lambda (C++14)
1
auto lambda = [](auto x, auto y) { return x + y; };
原理:编译器为它生成一个模板化的
operator():1
2
3
4
5class AnonymousLambda {
public:
template<typename T1, typename T2>
auto operator()(T1 x, T2 y) const { return x + y; }
};
第六部分:类与对象
1. 封装与访问控制
public/private/protected- 编译期检查:访问控制是编译期概念。编译器在解析代码时,根据上下文检查对成员的访问是否合法。违反规则会直接报错。运行时没有任何开销。
2. 特殊成员函数与=default/=delete
规则 of Three/Five/Zero:
- Rule of Three:如果你需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,你很可能需要全部三个。
- Rule of Five (C++11):加上移动构造函数和移动赋值运算符。
- Rule of Zero:理想情况是,让编译器生成的默认行为处理资源管理,你将资源管理交给像
vector、string、unique_ptr这样的成员对象。
=default:显式要求编译器生成默认实现。即使你定义了其他构造函数,也可以用
=default来获取编译器生成的默认构造函数。=delete:禁止编译器生成某些函数,或禁止某些不希望的转换。
1
2
3
4
5
6class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝
NonCopyable& operator=(const NonCopyable&) = delete;
};
explicit作用:阻止构造函数的隐式转换。
示例:
1
2
3
4
5
6
7class String {
public:
explicit String(int size); // 禁止从int隐式构造String
};
void f(const String& s);
f(10); // 错误!不能隐式将int转换为String
f(String(10)); // 正确,显式构造
3. 构造函数与析构函数家族
拷贝 vs. 移动
- 拷贝语义:
T(const T&)和T& operator=(const T&)。不改变源对象,创建资源的完整副本(深拷贝)。 - 移动语义:
T(T&&)和T& operator=(T&&)。将源对象的资源”偷”过来,并将源对象置于有效但未定义的状态(通常是空)。必须标记为noexcept,以便被标准库高效使用。
- 拷贝语义:
委托构造函数
1
2
3
4
5
6class Foo {
int a, b, c;
public:
Foo(int x) : a(x), b(0), c(0) {}
Foo(int x, int y) : Foo(x) { b = y; } // 委托给第一个构造函数
};- 原理:在一个构造函数的初始化列表中调用另一个构造函数,避免代码重复。
- 虚析构函数
- 核心原理:当
delete一个指向派生类对象的基类指针时,如果基类析构函数不是virtual,则行为是未定义的(通常只调用基类析构函数,导致派生部分资源泄漏)。 - 多态析构:通过虚函数表,
delete base_ptr;会正确调用派生类的析构函数,然后自动调用基类的析构函数。
- 核心原理:当
4. 成员函数限定符
const成员函数- 底层:在成员函数声明的末尾加
const,实质是给this指针加上了const限定。即T* const this变成了const T* const this。这保证了该函数不会修改对象的任何非mutable成员。
- 底层:在成员函数声明的末尾加
引用限定符 (C++11)
问题:对于
operator=,我们希望区分左值对象和右值对象。解决方案:
1
2
3
4
5class String {
public:
String& operator=(const String&) &; // 只能被左值对象调用
String operator=(const String&) &&; // 只能被右值对象调用
};应用:防止对右值进行无意义的赋值,如
getTempString() = otherString;。
5. 友元与静态成员
友元
- 本质:打破了封装,是编译期的白名单机制。它授予非成员函数或其他类访问本类
private和protected成员的权力。 - 注意:友元关系不可传递,也不继承。
- 本质:打破了封装,是编译期的白名单机制。它授予非成员函数或其他类访问本类
静态成员
内存与生命周期:存储在程序的静态存储区,不依赖于任何对象实例。生命周期从程序开始到结束。
线程安全实现:
1
2
3
4
5
6
7
8
9class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证这是线程安全的
return instance;
}
private:
Singleton() = default;
};- C++11标准规定,局部静态变量的初始化是线程安全的。编译器会插入底层同步代码(如原子操作或锁)来保证只初始化一次。
6. 设计原则与UML
- 关键设计原则:
- 单一职责原则:一个类应该只有一个引起它变化的原因。
- 开放-封闭原则:对扩展开放,对修改封闭。
- 依赖倒置原则:依赖于抽象(接口),而非具体实现。
- UML关联:
- 组合:
A包含B,B的生命周期由A管理(A析构,B也析构)。用实心菱形表示。代码中通常表现为:class A { B b; };。 - 聚合:
A使用B,但B的生命周期独立于A。用空心菱形表示。代码中通常表现为:class A { B* b; };。
- 组合:
第七部分:类的继承与派生、多态性知识
1. 继承的内存布局与访问控制
内存布局模型
单继承:派生类对象包含一个完整的基类子对象,后跟派生类自己的数据成员。
1
2class Base { int b_data; };
class Derived : public Base { int d_data; };- 内存布局:
[Base::b_data | Derived::d_data]
- 内存布局:
- 原理:基类子对象在派生类对象中拥有固定的偏移量(通常为0)。这保证了将
Derived*隐式转换为Base*时,不需要调整指针值——它们指向同一个地址。
访问控制的实际效果
public继承:is-a关系,基类的接口在派生类中保持原样。protected/private继承:is-implemented-in-terms-of关系,本质是组合的语法变体。- 底层实质:访问控制是在编译期由编译器检查的。无论何种继承方式,基类子对象在内存中都存在,只是通过派生类对象访问基类成员的权限不同。
2. 虚函数机制与vtable/vptr
vtable(虚函数表)
本质:每个包含虚函数的类(或从包含虚函数的类派生)都有一个编译期生成的静态函数指针数组,存储在只读数据段。
内容:
- 指向该类各个虚函数实现的指针
- 可选的RTTI信息(用于
typeid和dynamic_cast)
继承链中的vtable构建:
1
2
3
4
5
6
7
8
9
10class Base {
public:
virtual void f1() { ... }
virtual void f2() { ... }
};
class Derived : public Base {
public:
void f1() override { ... } // 重写
virtual void f3() { ... } // 新虚函数
};- Base的vtable:
[&Base::f1, &Base::f2] - Derived的vtable:
[&Derived::f1, &Base::f2, &Derived::f3]
- Base的vtable:
vptr(虚函数表指针)
- 本质:每个对象实例中编译器自动添加的隐藏指针,指向该对象所属类的vtable。
- 生命周期:在构造函数中初始化,在析构函数中调整。
- 内存位置:通常位于对象起始处(保证与C的兼容性和最高访问效率)。
虚函数调用的完整过程
1
2Base* ptr = new Derived;
ptr->f1(); // 多态调用- 通过
ptr找到对象的vptr - 通过vptr找到vtable
- 在vtable的固定偏移处(如索引0)找到函数地址
- 通过该地址调用函数,并传入
this指针
- 通过
- 性能开销:
- 空间开销:每个对象一个vptr(通常4/8字节),每个类一个vtable
- 时间开销:一次指针解引用(vptr) + 一次数组索引(vtable) + 可能的一次缓存未命中
3. 纯虚函数与抽象基类
- 纯虚函数声明:
virtual void func() = 0; - 抽象基类效果:不能实例化,只能作为接口定义。
- vtable中的表示:纯虚函数在vtable中通常填充为一个特殊的纯虚调用句柄,如果被调用会终止程序(或抛出异常)。
4. 重写控制(override/final)
- override:编译期检查,确保函数确实重写了基类的虚函数。防止因签名不匹配导致的意外隐藏。
- final:
- 用于类:禁止进一步派生
- 用于虚函数:禁止在派生类中重写
- 优化机会:标记为
final的虚函数调用,在某些情况下编译器可以进行去虚拟化优化,直接进行静态调用。
5. 多重继承与虚继承
普通多重继承的内存布局
1
2
3class Base1 { int b1_data; };
class Base2 { int b2_data; };
class Derived : public Base1, public Base2 { int d_data; };- 内存布局:
[Base1::b1_data | Base2::b2_data | Derived::d_data]Base1*和Derived*地址相同Base2*需要偏移:(char*)derived + sizeof(Base1)
- 内存布局:
菱形问题与虚继承
1
2
3
4class A { int a_data; };
class B : virtual public A { int b_data; };
class C : virtual public A { int c_data; };
class D : public B, public C { int d_data; };
- 虚继承的实现机制
- 虚基类表(vbtable):类似vtable,存储虚基类相对于当前子对象的偏移量。
- 虚基类指针(vbptr):在包含虚基类的类对象中,指向vbtable。
- 内存布局:虚基类子对象通常放在对象末尾,通过vbptr间接访问。
- 访问成本:通过虚基类指针多一次间接访问,比普通成员访问慢。
6. RTTI成本分析与替代方案
dynamic_cast实现原理- 检查源类型和目标类型是否在同一个继承层次
- 遍历继承树,检查转换是否合法
- 需要时调整指针偏移量
性能成本
- 涉及类型信息查询和继承树遍历
- 在复杂继承层次中可能很昂贵
- 依赖vtable,因此只能用于多态类(有虚函数的类)
替代方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 方案1:使用枚举+static_cast(编译期)
enum class Type { TYPE_BASE, TYPE_DERIVED };
class Base {
protected:
virtual Type getType() const { return Type::TYPE_BASE; }
};
// 方案2:使用自定义类型标识
class Derived : public Base {
protected:
Type getType() const override { return Type::TYPE_DERIVED; }
public:
static bool classof(const Base* b) {
return b->getType() == Type::TYPE_DERIVED;
}
};
第八部分:模板与元编程
1. 模板实例化机制
- 两阶段编译
- 定义期:检查模板本身的语法
- 实例化期:用具体类型替换模板参数,生成具体代码,检查类型相关操作
- 代码膨胀问题:每个不同的模板参数组合都会生成一份独立的代码。可通过提取公共部分到非模板基类来缓解。
2. 变参模板
参数包展开模式
1
2
3
4
5
6template<typename... Args>
void print(Args... args) {
// 递归展开(C++11/14风格)
// 或使用折叠表达式(C++17)
(std::cout << ... << args);
}完美转发参数包
1
2
3
4template<typename... Args>
void forwarder(Args&&... args) {
target(std::forward<Args>(args)...); // 保持值类别
}
3. SFINAE与std::enable_if
SFINAE原理:在模板重载决议中,如果某个模板的立即上下文中的替换失败,编译器会 silently 丢弃该特化,而不是报错。
std::enable_if应用1
2
3
4
5
6template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
process(T value) {
// 只对整数类型有效
return value * 2;
}现代替代品:C++20的Concepts提供了更清晰的语法替代SFINAE。
4. 编译期分支if constexpr
与运行时
if的关键区别:if constexpr在编译期确定分支,未选择的分支不会实例化。1
2
3
4
5
6
7
8template<typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // 对指针类型:解引用
} else {
return t; // 对非指针类型:直接返回
}
}- 如果用
int实例化,return *t;这行代码根本不存在 - 避免了编译错误(对非指针类型解引用)
- 如果用
5. 类型萃取
std::is_same:编译期类型比较
1
2static_assert(std::is_same_v<int, int>); // 通过
static_assert(std::is_same_v<int, float>); // 失败std::decay:模拟按值传递的类型转换
移除引用和cv限定符
数组退化为指针
函数退化为函数指针
1
2
3std::decay_t<const int&> // → int
std::decay_t<int[5]> // → int*
std::decay_t<int(int)> // → int(*)(int)
类型萃取的实现原理
1
2
3
4
5
6
7
8
9template<typename T>
struct is_pointer {
static constexpr bool value = false;
};
template<typename T>
struct is_pointer<T*> {
static constexpr bool value = true; // 偏特化匹配指针
};
第九部分:输入输出流
1. 流缓冲区与格式化
- 流的三层架构
- 格式化层:
ostream/istream,处理数据类型转换和格式化 - 缓冲层:
streambuf,管理字符序列的缓冲 - 设备层:文件、内存、字符串等具体设备
- 格式化层:
- 缓冲区刷新策略
unitbuf:每个操作后刷新nounitbuf:缓冲区满时刷新- 手动刷新:
flush,endl(刷新+换行)
2. 文件I/O高级特性
二进制I/O与内存对齐
1
2
3
4
5
6
7
8
9
10
11struct Data {
int id;
double value;
char name[32];
};
Data data;
// 写入:直接内存拷贝
file.write(reinterpret_cast<const char*>(&data), sizeof(data));
// 读取
file.read(reinterpret_cast<char*>(&data), sizeof(data));- 注意事项:
- 平台字节序问题(大端/小端)
- 结构体填充字节导致的大小不一致
- 版本兼容性问题
- 注意事项:
内存映射文件原理
- 将文件直接映射到进程的虚拟地址空间
- 通过页错误机制实现按需加载
- 优点:避免用户态-内核态数据拷贝,适合大文件随机访问
- 实现:Unix的
mmap,Windows的CreateFileMapping
异步I/O与
std::async1
2
3
4
5
6auto future = std::async(std::launch::async, [file_path]() {
return read_large_file(file_path); // 在独立线程中执行
});
// 主线程继续其他工作
auto result = future.get(); // 等待结果文件锁的实现级别
- 劝告锁:
std::filebuf::lock()(C++17),进程间协作 - 强制锁:操作系统级别强制实施
- 范围锁:锁定文件的特定区域
- 劝告锁:
3. 时间库深度使用
时钟类型区别
system_clock:系统实时时钟,可调整,可转换为日历时间steady_clock:单调时钟,保证始终递增,适合性能测量high_resolution_clock:最高精度的时钟(通常是steady_clock的别名)
高精度计时实现
1
2
3
4
5
6
7
8
9auto start = std::chrono::high_resolution_clock::now();
// 被测代码
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
std::cout << "耗时: " << duration.count() << " 纳秒" << std::endl;时间点算术
1
2
3
4using namespace std::chrono;
auto now = system_clock::now();
auto one_hour_later = now + hours(1); // 时间点 + 时长 = 时间点
auto diff = one_hour_later - now; // 时间点 - 时间点 = 时长
第十部分:标准模板库和高级特性应用
1. 多线程应用编程
线程创建与生命周期管理
1
2std::thread t1([](){ /* 任务 */ });
std::thread t2(func, arg1, arg2);- 底层原理:
std::thread封装了操作系统原生线程(pthread, Windows Thread) - 资源管理:线程对象析构前必须调用
join()(等待结束)或detach()(分离管理) - 线程局部存储:
thread_local变量每个线程有独立副本
- 底层原理:
同步原语深度解析
互斥量 (
std::mutex)1
2
3
4
5std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx); // RAII管理
// 临界区
}- 底层实现:通常使用原子操作+系统调用(futex)实现
- 性能代价:锁竞争导致线程阻塞、上下文切换
条件变量 (
std::condition_variable)1
2
3
4
5
6
7
8
9
10
11
12
13
14std::condition_variable cv;
std::mutex mtx;
bool ready = false;
// 等待方
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// 通知方
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();- 避免虚假唤醒:使用谓词参数的
wait版本 - 底层机制:线程进入等待队列,释放锁,被唤醒后重新获取锁
- 避免虚假唤醒:使用谓词参数的
原子操作 (
std::atomic)1
2std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);- 无锁编程:使用CPU原子指令,避免锁开销
- 内存顺序:
memory_order_relaxed:只保证原子性memory_order_acquire/release:保证同步memory_order_seq_cst:最强一致性(默认)
2. 容器深度分析
序列容器内存布局
- vector:单块连续内存,随机访问O(1),尾部操作摊销O(1)
- 扩容策略:通常2倍增长,
size() + 1可能触发重新分配 - 迭代器失效:插入/删除可能使所有迭代器失效
- 扩容策略:通常2倍增长,
- list:双向链表,插入删除O(1),随机访问O(n)
- 内存开销:每个节点包含两个指针开销
- deque:分块连续内存,头尾操作O(1)
- 实现机制:中央map + 多个固定大小块
- vector:单块连续内存,随机访问O(1),尾部操作摊销O(1)
关联容器实现原理
红黑树特性:
- 自平衡二叉搜索树
- 从根到叶子的最长路径不超过最短路径的2倍
- 插入、删除、查找都是O(log n)
map vs unordered_map:
1
2std::map<int, string> tree_map; // 红黑树,有序
std::unordered_map<int, string> hash_map; // 哈希表,无序但更快
无序容器(哈希表)实现
- 哈希冲突解决:
- 链地址法:每个桶是链表(STL实现)
- 开放地址法:线性探测、二次探测
- 性能参数:
- 负载因子 = 元素数量 / 桶数量
- 重新哈希:负载因子超过阈值时重建哈希表
- 哈希冲突解决:
3. 迭代器类别与概念
迭代器层次体系:
1
2
3输入迭代器 → 前向迭代器 → 双向迭代器 → 随机访问迭代器
↓
输出迭代器- 随机访问迭代器:支持
+,-,[],vector,deque,array - 双向迭代器:支持
++,--,list,set,map - 前向迭代器:仅支持
++,forward_list,unordered_containers
- 随机访问迭代器:支持
4. C++17现代组件
std::optional:类型安全的可空值
1
2
3
4
5
6
7
8std::optional<int> find(int key) {
if (exists(key)) return compute(key);
return std::nullopt; // 无值
}
if (auto result = find(42)) {
use(*result); // 安全解引用
}std::variant:类型安全的联合体
1
2
3
4
5std::variant<int, double, string> v = 3.14;
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) { /* ... */ }
}, v);std::string_view:字符串的非拥有视图
1
2
3void process(std::string_view sv) { // 接受string, char*, 字面量
auto substr = sv.substr(0, 5); // O(1)操作
}- 性能优势:避免不必要的字符串拷贝
- 生命周期风险:不管理内存,必须保证原字符串存活
std::filesystem:现代文件系统操作
1
2
3
4
5
6namespace fs = std::filesystem;
for (auto& entry : fs::directory_iterator(".")) {
if (entry.is_regular_file()) {
std::cout << entry.file_size() << " " << entry.path() << "\n";
}
}
5. 并行STL算法
1 |
|
- 执行策略:
seq:顺序执行par:并行执行par_unseq:并行+向量化
第十一部分:内存管理
1. 动态内存分配机制
new/deletevsmalloc/free1
2
3
4
5
6
7// C++方式:调用构造函数/析构函数
MyClass* obj = new MyClass(args);
delete obj;
// C方式:仅分配原始内存
void* mem = malloc(sizeof(MyClass));
free(mem);operator new重载:可自定义内存分配行为placement new:在已分配内存上构造对象
1
2
3
4
5cpp
void* buffer = malloc(sizeof(MyClass));
MyClass* obj = new (buffer) MyClass(args); // placement new
obj->~MyClass(); // 必须显式调用析构函数
free(buffer);
2. 智能指针实现原理
unique_ptr:独占所有权,零开销1
2std::unique_ptr<MyClass> ptr(new MyClass);
// 编译为裸指针操作,无额外开销shared_ptr控制块机制1
2std::shared_ptr<MyClass> sp1(new MyClass);
auto sp2 = sp1; // 共享所有权内存布局:
1
2
3[sp1] → [控制块] → [对象数据]
[sp2] ↗
控制块包含:引用计数、弱引用计数、删除器、分配器make_shared优化:对象和控制块单次分配
weak_ptr解决循环引用1
2
3
4
5
6class B; class A {
std::shared_ptr<B> b_ptr;
};
class B {
std::weak_ptr<A> a_ptr; // 使用weak_ptr打破循环
};
3. 内存对齐控制
硬件对齐要求:现代CPU要求数据在自然边界对齐
对齐控制:
1
2
3
4
5
6struct alignas(16) AlignedData { // 16字节对齐
int data[4];
};
static_assert(alignof(AlignedData) == 16);
static_assert(sizeof(AlignedData) == 16); // 无填充
4. 移动语义与完美转发
右值引用:
T&&绑定到临时对象引用折叠规则:
T& &→T&T& &&→T&T&& &→T&T&& &&→T&&
完美转发实现:
1
2
3
4template<typename T>
void wrapper(T&& arg) {
target(std::forward<T>(arg)); // 保持值类别
}
5. 内存问题检测工具
Valgrind Memcheck:
1
valgrind --leak-check=full ./my_program
- 检测能力:内存泄漏、使用未初始化内存、越界访问
AddressSanitizer (ASan):
1
g++ -fsanitize=address -g program.cpp
- 原理:编译时插桩,影子内存跟踪每个字节状态
- 优势:比Valgrind快,适合开发期持续检测
第十二部分:工具、测试与系统接口
1. C++与C语言互操作
extern "C"链接规范1
2
3
4
5extern "C" {
int c_function(int); // C风格名称修饰
}- 名称修饰差异:C++支持重载,名称包含类型信息;C名称简单
ABI兼容性:确保数据结构布局、调用约定一致
2. GDB高级调试技巧
核心转储分析:
1
2
3
4gdb ./my_program core.dump
(gdb) bt full # 完整调用栈
(gdb) info registers # 寄存器状态
(gdb) x/10x $sp # 检查栈内存条件断点与观察点:
1
2(gdb) break file.cpp:100 if condition
(gdb) watch variable # 数据断点
3. 单元测试实践
Google Test框架:
1
2
3
4
5
6
7
8TEST(MyTestSuite, SpecificTest) {
EXPECT_EQ(actual, expected);
ASSERT_TRUE(condition); // 失败则终止当前测试
}
TEST_F(TestFixture, UsingFixture) {
// 使用SetUp()初始化的环境
}测试替身:Mock对象模拟依赖组件
4. UML建模与设计模式
UML类图关系:
- 继承:空心三角形箭头
- 组合:实心菱形,整体控制部分生命周期
- 聚合:空心菱形,整体不控制部分生命周期
- 依赖:虚线箭头
常用设计模式实现:
RAII模式:资源获取即初始化
1
2
3
4
5
6
7class FileHandle {
FILE* file;
public:
FileHandle(const char* name) : file(fopen(name, "r")) {}
~FileHandle() { if (file) fclose(file); }
// 禁用拷贝,允许移动
};工厂模式:创建对象而不指定具体类
1
2
3
4
5
6std::unique_ptr<Shape> create_shape(ShapeType type) {
switch (type) {
case CIRCLE: return std::make_unique<Circle>();
case SQUARE: return std::make_unique<Square>();
}
}观察者模式:对象间依赖通知
1
2
3
4
5
6
7
8
9
10
11
12class Observer {
public:
virtual void update(const Event&) = 0;
};
class Subject {
std::vector<Observer*> observers;
public:
void notify(const Event& e) {
for (auto obs : observers) obs->update(e);
}
};