简介:本资源是面向C++初学者与进阶学习者的系统化课程资料包,聚焦语法基础、面向对象编程及STL实战应用,特别适合高校学生、转行程序员及准备技术面试的开发者夯实核心能力。压缩包为ZIP格式,共64.14MB,内含三个循序渐进的完整实验项目(《通讯录管理系统》《职工管理系统》《基于STL的演讲比赛流程管理系统》),配套三套基础课程文档手册,并整合《InterviewGuide第四版By阿秀》《LeetCode刷题手册》《背包问题九讲》《算法图解》等高质量PDF学习资料,覆盖面试高频考点、经典算法解析与工程实践路径。目前已有1700人学习下载,内容组织由简入繁,项目均具备完整功能模块、清晰目录结构与可运行逻辑,配合手册实现“学—练—用”闭环,有效支撑从语法理解到项目落地的能力跃迁。
1. C++ 黑马程序员学习资料:一张覆盖语法、内存、STL 与项目实战的完整能力地图
如果你正在系统学 C++,或者刚刷完几道 LeetCode 感觉“会写但不敢改、能编译但一调试就崩”,这份 C++ 黑马程序员学习资料大概率就是你缺的那张「能力坐标图」。它不是零散的视频合集,而是一套按认知逻辑分层推进的闭环体系:从int a = 5;的字面量语义开始,到std::shared_ptr<std::vector<std::unique_ptr<Shape>>>这类嵌套智能指针的生命周期管理;从手写单链表理解指针偏移,到用 RAII 封装线程池并接入真实 HTTP 请求模块;最后落地到一个带 Qt 界面、SQLite 本地存储、多线程日志回滚的「轻量级笔记同步工具」项目。整套资料默认面向有 C 语言基础(或 Python/Java 转型)的学习者,不回避const_cast的危险区,也不跳过std::atomic_flag的内存序细节——它把 C++ 分成四根主梁:语法骨架、内存神经、STL 血脉、项目肌理。复习时你会发现,90% 的面试卡点(比如虚函数表布局、move 语义触发条件、allocator 自定义陷阱)和工程翻车现场(野指针访问、迭代器失效、静态初始化顺序问题),都能在这张图里找到对应坐标和修复路径。
2. 语法骨架:从变量声明到模板元编程的七层递进结构
C++ 语法不是线性罗列的规则集,而是一个由底层约束向上生长的树状结构。黑马资料把语法拆成七个可验证层级,每层都配「反例代码 + 编译错误定位 + 修正逻辑」三件套,避免死记硬背。
2.1 第一层:声明与定义的物理边界(为什么extern int x;不分配内存?)
这是所有内存错误的起点。资料用objdump -t导出符号表,对比int x = 10;(BSS 或 DATA 段)、extern int x;(UND 符号)、const int y = 20;(可能进入 .rodata)的实际内存位置。重点讲清:
static在全局作用域 vs 局部作用域的语义分裂(链接属性 vs 生命周期)constexpr不是const的升级版,而是编译期求值的强制契约(constexpr int f() { return rand(); }必报错)inline函数在 C++17 后的 ODR(One Definition Rule)豁免机制,以及头文件中定义inline变量的必要性
提示:所有
extern "C"的使用场景必须绑定具体 ABI 约束,资料中用nm libmath.so | grep sin验证 C 链接符号不带 name mangling,而g++编译的 C++ 符号Z3sinf则被完整修饰。
2.2 第二层:类型系统与转换的显式契约(static_cast为何不能转void*到函数指针?)
C++ 类型安全不是靠编译器“猜”,而是靠开发者用 cast 显式声明意图。资料用 GDB 观察内存布局差异:
static_cast<int*>(void_ptr)合法(地址数值平移)static_cast<void(*)()>(func_ptr)非法(函数指针与数据指针在 x86_64 下宽度相同但 ABI 语义隔离)reinterpret_cast是唯一能跨语义域的转换,但必须配合alignas(16)确保内存对齐
附带实测代码:
#include <iostream> #include <cstdint> int main() { char data[16] alignas(16) = {0}; // ✅ 合法:数据指针间转换 int* p1 = static_cast<int*>(static_cast<void*>(data)); // ❌ 编译失败:函数指针需 reinterpret_cast // void (*fp)() = static_cast<void(*)()>(data); // error // ✅ 合法:但运行时行为未定义(除非 data 真存机器码) void (*fp)() = reinterpret_cast<void(*)()>(data); std::cout << "p1 addr: " << (uintptr_t)p1 << "\n"; return 0; }关键参数说明:alignas(16)强制 16 字节对齐,避免某些 CPU 架构下非对齐访问崩溃;uintptr_t是唯一能无损承载指针值的整数类型,比long更可靠。
2.3 第三层:类与对象的内存契约(sizeof(EmptyClass)为何是 1?)
空类大小非零,本质是编译器为每个对象分配唯一地址的物理需求。资料用offsetof宏验证基类子对象偏移,并对比虚继承与普通继承的 vptr 插入位置:
| 继承方式 | sizeof(Derived) | vptr 数量 | 成员偏移(offsetof(Derived, member)) |
|---|---|---|---|
| 普通单继承 | 16 | 1 | base_member: 0,derived_member: 8 |
| 虚继承 | 24 | 2 | base_member: 16(vbase offset 后) |
| 多重继承 | 24 | 2 | base1_member: 0,base2_member: 16 |
实测代码验证虚基类偏移:
#include <iostream> #include <cstddef> struct Base { int b; }; struct Derived1 : virtual Base { int d1; }; struct Derived2 : virtual Base { int d2; }; struct Multi : Derived1, Derived2 { int m; }; int main() { std::cout << "sizeof(Multi): " << sizeof(Multi) << "\n"; // 32 on x86_64 std::cout << "offsetof(Multi, d1): " << offsetof(Multi, d1) << "\n"; // 16 std::cout << "offsetof(Multi, d2): " << offsetof(Multi, d2) << "\n"; // 24 std::cout << "offsetof(Multi, m): " << offsetof(Multi, m) << "\n"; // 32 return 0; }注意:offsetof只对 POD 类型有效,virtual成员函数会使类变为 non-POD,此时offsetof行为未定义。
2.4 第四层:运算符重载的隐式陷阱(operator==为何要声明为const?)
重载不是语法糖,而是编译器生成调用指令的严格协议。资料强调三个铁律:
operator==必须接受const左右操作数,否则const Obj a, b; if (a == b)编译失败operator[]返回引用才能支持obj[i] = val和val = obj[i]两种用法operator->必须返回指针或重载了operator->的对象(实现链式调用)
易漏点章节见 2.7。
2.5 第五层:异常与 noexcept 的资源守恒(noexcept函数抛异常会怎样?)
noexcept不是注释,而是 ABI 级别的承诺。资料用std::terminate的调用栈证明:当noexcept函数意外抛出异常,程序直接终止,不执行任何栈展开(stack unwinding),导致 RAII 对象析构函数不被调用——这是资源泄漏的终极黑匣子。
实测对比:
#include <iostream> #include <stdexcept> struct Guard { Guard() { std::cout << "Guard ctor\n"; } ~Guard() { std::cout << "Guard dtor\n"; } }; void may_throw() { throw std::runtime_error("boom"); } void noexcept_func() noexcept { Guard g; // ctor 执行 may_throw(); // 抛异常 → terminate → dtor 不执行! } int main() { try { noexcept_func(); } catch (...) { std::cout << "caught\n"; } return 0; }输出只有Guard ctor,无Guard dtor—— 这就是noexcept的真实代价。
2.6 第六层:模板实例化的编译期爆炸(为什么vector<vector<vector<int>>>编译慢?)
模板不是宏,每次实例化都生成独立符号。资料用g++ -ftemplate-backtrace-limit=0展开嵌套实例化栈,并指出:
std::vector<T>实例化时,会隐式实例化std::allocator<T>、std::iterator_traits<...>等十余个辅助模板template<typename T> void foo(T t)的函数模板,若传入std::string,则实例化foo<std::string>,但不会实例化foo<const char*>(除非显式调用)
优化实践:用using Vec3i = std::vector<std::vector<std::vector<int>>>;减少重复解析。
2.7 易错点:语法层最常被忽略的五个“理所当然”
这些点在调试时往往被归因为“环境问题”或“编译器 bug”,实则是标准规定的硬约束:
易漏点:
for (auto& x : container)中x的类型推导规则
→ 为什么std::vector<bool>的auto&会编译失败?
→ 因为std::vector<bool>是特化模板,operator[]返回std::vector<bool>::reference(代理类),非真实引用,auto&无法绑定临时对象。
→ 怎么补:改用auto x(值拷贝)或显式写std::vector<bool>::reference x。易漏点:
std::initializer_list的生命周期陷阱
→auto il = {1,2,3};创建的initializer_list持有临时数组,其生命周期仅限当前表达式;return {1,2,3};返回的是悬垂指针。
→ 怎么补:用std::vector<int>{1,2,3}替代,或确保initializer_list仅用于函数形参(编译器延长临时对象生命周期)。易漏点:
constexpr if的分支必须语法正确
→if constexpr (cond) { invalid_code; } else { valid_code; }中,即使cond为 false,invalid_code仍需通过语法检查(只是不参与实例化)。
→ 怎么补:把invalid_code放进requires约束或decltypeSFINAE 中。易漏点:
mutable成员在const成员函数中可修改,但不可用于this指针本身
→mutable std::unique_ptr<int> ptr;在const void func() const中可ptr.reset(new int(42)),但this = nullptr仍非法。
→ 怎么补:mutable仅解引用权限,不改变对象地址所有权。易漏点:
#include的头文件搜索顺序影响 ODR
→ 若A.h和B.h都定义class Logger,且main.cpp先#include "A.h"后#include "B.h",则Logger被多次定义,违反 ODR。
→ 怎么补:强制使用 include guards(#pragma once或#ifndef LOGGER_H),并在 CMake 中启用-Wodr检测。
3. 内存神经:从栈帧布局到智能指针的全链路控制
C++ 的内存模型是它的灵魂,也是崩溃的温床。黑马资料不讲抽象概念,而是用pstack、valgrind --tool=memcheck、gdb三件套,把内存操作还原成寄存器指令流。
3.1 栈帧结构与 RBP 链:call指令后发生了什么?
资料用gdb单步main(),观察%rsp、%rbp变化,并导出汇编:
main: push %rbp # 保存旧 rbp mov %rsp,%rbp # 新栈帧基址 sub $0x10,%rsp # 分配 16 字节局部空间 movl $0x1,(%rbp) # int a = 1 → 存入 [rbp-4] leave # mov %rbp,%rsp; pop %rbp ret关键结论:
- 局部变量地址 =
%rbp - offset,offset由变量大小和对齐要求决定 alloca()动态分配栈空间,但不更新%rbp,需手动管理指针__attribute__((naked))函数禁用 prologue/epilogue,必须手写ret
3.2 堆内存的双刃剑:new/delete与malloc/free的 ABI 差异
new不是malloc的封装,而是调用operator new(可重载)再调用构造函数。资料用LD_PRELOAD拦截malloc,发现new int[10]调用operator new[](size_t),而非malloc:
# 编译时加 -fsanitize=address 检测越界 g++ -fsanitize=address -g test.cpp -o test ./test # ASan 输出:heap-buffer-overflow at 0x60200000001c对比实验:
| 操作 | 是否调用构造函数 | 是否可重载 | 是否触发std::set_new_handler |
|---|---|---|---|
new int(5) | ✅ | ✅ | ✅ |
malloc(sizeof(int)) | ❌ | ❌ | ❌ |
new(std::nothrow) int(5) | ✅ | ✅ | ❌(失败返回 nullptr) |
3.3 RAII 的物理实现:std::unique_ptr如何保证析构?
unique_ptr本质是T*加一个删除器(deleter)函数对象。资料反汇编unique_ptr<int> p(new int(42));,发现:
- 构造时:
p.ptr_ = new int(42),p.deleter_ = default_delete<int> - 析构时:
p.deleter_(p.ptr_),即delete p.ptr_ - 移动后:源
p.ptr_被置为nullptr,避免二次 delete
实测双重释放:
#include <memory> #include <iostream> int main() { auto p = std::make_unique<int>(42); int* raw = p.release(); // 交出所有权,p.ptr_ = nullptr delete raw; // 手动释放 // delete raw; // 再次 delete → UB,ASan 可捕获 return 0; }注意:
release()后unique_ptr不再管理内存,reset()才会触发原指针的 deleter。
3.4shared_ptr的线程安全边界:引用计数原子性 ≠ 对象线程安全
这是最高频的误解。资料用std::thread并发修改shared_ptr,证明:
shared_ptr的引用计数增减是原子的(内部用std::atomic<int>)- 但
shared_ptr指向的对象T本身无任何线程保护
并发代码:
#include <memory> #include <thread> #include <vector> struct Counter { int value = 0; }; std::shared_ptr<Counter> global_ptr = std::make_shared<Counter>(); void worker() { for (int i = 0; i < 10000; ++i) { global_ptr->value++; // ❌ data race! } } int main() { std::vector<std::thread> ts; for (int i = 0; i < 4; ++i) { ts.emplace_back(worker); } for (auto& t : ts) t.join(); std::cout << global_ptr->value << "\n"; // 非 40000,因 data race return 0; }修复方案:用std::atomic<int>替代int value,或加std::mutex。
3.5 自定义分配器:为什么std::vector的reserve()不调用allocate()?
reserve()预分配内存,但std::vector使用std::allocator<T>,其allocate()仅在push_back需扩容时调用。资料用自定义分配器打印调用栈:
template<typename T> struct TraceAlloc { using value_type = T; T* allocate(size_t n) { std::cout << "allocate " << n << " elements\n"; return static_cast<T*>(::operator new(n * sizeof(T))); } void deallocate(T* p, size_t) { ::operator delete(p); } }; int main() { std::vector<int, TraceAlloc<int>> v; v.reserve(100); // 不触发 allocate! v.push_back(1); // 触发 allocate(1) return 0; }原因:reserve()仅调整 capacity,不改变 size,std::vector的内存策略是 lazy allocation。
3.6 易错点:内存层三大“玄学崩溃”的根因与定位法
这些崩溃在日志里常显示segmentation fault,但根源各不相同:
易漏点:
std::string的短字符串优化(SSO)导致c_str()悬垂
→std::string s = "hello"; const char* p = s.c_str(); s += " world";后p指向已释放内存。
→ 为什么:SSO 字符串在栈上存储,+=触发堆分配,原栈内存失效。
→ 怎么补:std::string::data()在 C++17 后保证 null-terminated,或用std::string_view替代裸指针。易漏点:
std::vector迭代器失效的静默陷阱
→for (auto it = v.begin(); it != v.end(); ++it) { if (*it == 0) v.erase(it); }导致it失效后继续++it→ UB。
→ 怎么补:erase()返回新有效迭代器,应it = v.erase(it);或用remove-erase惯用法。易漏点:
std::function捕获this的生命周期错位
→class A { void foo() { timer_.async_wait([this]{ callback(); }); } };中,若A对象销毁早于async_wait执行,则this悬垂。
→ 怎么补:用shared_from_this()确保对象存活,或传递weak_ptr并在 lambda 中lock()。
4. STL 血脉:容器、算法与迭代器的协同契约
STL 不是工具箱,而是一套基于概念(Concepts)的协议体系。黑马资料用 C++20 Concepts 重写经典算法,揭示std::sort为何要求随机访问迭代器。
4.1 迭代器分类的本质:五种概念的物理约束
资料将迭代器分为五类,每类对应不同 CPU 指令能力:
| 迭代器类型 | 支持操作 | 典型容器 | 汇编级约束 |
|---|---|---|---|
| InputIterator | ++it,*it | std::istream_iterator | 单向读取,不可回退 |
| ForwardIterator | ++it,*it, 可多次遍历 | std::forward_list | 地址连续,但无随机跳转指令 |
| BidirectionalIterator | ++it,--it,*it | std::list | 支持dec %rax指令 |
| RandomAccessIterator | it + n,it[n],< | std::vector | 支持lea (%rax,%rdx,4),%rcx |
| ContiguousIterator | &*(it + n) == &*it + n | std::vector | 内存真正连续,可memcpy |
std::sort要求RandomAccessIterator,因为其实现依赖it + n计算中位数,std::list::sort则用归并(仅需双向迭代器)。
4.2std::map与std::unordered_map的时空权衡
资料用perf stat -e cache-misses,instructions对比插入 10 万键值对:
| 容器 | 时间复杂度 | 实际耗时(ms) | Cache Misses | 适用场景 |
|---|---|---|---|---|
std::map(红黑树) | O(log n) | 12.3 | 1.2M | 需有序遍历、范围查询 |
std::unordered_map(哈希) | 平均 O(1) | 8.7 | 3.5M | 高频单 key 查找,key 可哈希 |
关键参数:max_load_factor(0.75)控制哈希桶密度,rehash()触发扩容(重建哈希表),此时所有迭代器失效。
4.3 算法的“就地”真相:std::rotate如何用三次反转实现?
std::rotate(first, middle, last)将[first,middle)移至[middle,last)之后。资料用std::reverse模拟:
#include <algorithm> #include <vector> #include <iostream> void my_rotate(std::vector<int>& v, size_t mid) { // [0,mid) [mid,end) std::reverse(v.begin(), v.begin() + mid); // [mid-1..0) [mid..end) std::reverse(v.begin() + mid, v.end()); // [mid-1..0) [end-1..mid) std::reverse(v.begin(), v.end()); // [0..mid-1) [mid..end-1) → 正确 } int main() { std::vector<int> v = {1,2,3,4,5,6}; my_rotate(v, 2); // [3,4,5,6,1,2] for (int x : v) std::cout << x << " "; return 0; }原理:三次反转等价于循环移位,时间 O(n),空间 O(1),无额外内存分配。
4.4std::variant与std::any的类型擦除成本
std::variant是编译期确定类型的 union,std::any是运行期类型擦除。资料用sizeof和alignof对比:
| 类型 | sizeof(x86_64) | alignof | 是否支持std::visit | 运行时开销 |
|---|---|---|---|---|
std::variant<int, double> | 16 | 8 | ✅ | 无(tag 存于高位) |
std::any | 24 | 8 | ❌(需any_cast) | std::type_info查询 |
std::variant的index()返回当前类型索引(0-based),std::any的type()返回std::type_info&,需strcmp匹配。
4.5std::optional的零开销抽象:如何避免bool成员的内存浪费?
std::optional<T>的大小 =max(sizeof(T), sizeof(bool)),但对齐按T。资料用offsetof验证:
#include <optional> #include <iostream> #include <cstddef> struct Small { char c; }; // sizeof=1, alignof=1 struct Big { double d; }; // sizeof=8, alignof=8 int main() { std::cout << "sizeof(optional<Small>): " << sizeof(std::optional<Small>) << "\n"; // 1 std::cout << "sizeof(optional<Big>): " << sizeof(std::optional<Big>) << "\n"; // 16(8+8 padding) return 0; }std::optional用T的尾部字节存储bool has_value_,无额外内存。
4.6 易错点:STL 中最隐蔽的“假线程安全”与“真陷阱”
STL 容器的线程安全有严格限定,资料用std::shared_mutex演示正确用法:
易漏点:
std::vector的size()与empty()非原子
→if (!v.empty()) { auto x = v.back(); }在多线程下仍可能崩溃(empty()返回 true 后v被另一线程清空)。
→ 怎么补:用std::shared_mutex读锁保护整个临界区,或改用std::deque(front()/back()无迭代器失效风险)。易漏点:
std::string的+=操作符非线程安全
→s1 += s2可能触发重新分配,此时s1的内部缓冲区被替换,其他线程若正访问s1.c_str()则 UB。
→ 怎么补:对std::string加互斥锁,或用std::string_view作只读视图。易漏点:
std::map::insert()的异常安全保证
→insert({k,v})若v的拷贝构造抛异常,map保持原状(强异常安全),但insert_or_assign(k,v)中v的赋值若抛异常,map状态未定义。
→ 怎么补:优先用try_emplace(k, args...),它只在 key 不存在时构造v,且保证强异常安全。
5. 项目肌理:从 Qt 界面到 SQLite 封装的工业级落地链
理论终需项目淬炼。黑马资料的最终项目「轻量笔记同步工具」不是玩具,而是按工业标准拆解的模块:UI 层用 Qt Widgets(非 QML),数据层用 SQLite 封装,网络层用std::thread+libcurl,日志层用环形缓冲区。
5.1 Qt 事件循环与 C++ 对象生命周期的耦合陷阱
Qt 的QObject有 parent-child 树,deleteLater()延迟到事件循环空闲时执行。资料用QTimer::singleShot(0, ...)模拟:
#include <QApplication> #include <QWidget> #include <QTimer> #include <QDebug> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget* w = new QWidget; w->setAttribute(Qt::WA_DeleteOnClose); // ❌ 危险:w 被 deleteLater(),但 lambda 捕获 this 指针 QTimer::singleShot(0, w, [w]() { qDebug() << "w is still alive?"; // 可能 crash! w->show(); }); // ✅ 安全:用 weak_ptr 或检查 QObject::isDeleted() QTimer::singleShot(0, w, [w]() { if (!w->isDeleted()) w->show(); }); return app.exec(); }关键:QObject的deleteLater()不立即释放内存,但对象状态变为isDeleted(),此时调用成员函数 UB。
5.2 SQLite 封装的 RAII 实践:sqlite3_stmt的自动管理
原始 C API 需手动sqlite3_prepare_v2/sqlite3_finalize。资料用 RAII 封装:
class Statement { sqlite3_stmt* stmt_; public: Statement(sqlite3* db, const char* sql) : stmt_(nullptr) { int rc = sqlite3_prepare_v2(db, sql, -1, &stmt_, nullptr); if (rc != SQLITE_OK) throw std::runtime_error("prepare failed"); } ~Statement() { if (stmt_) sqlite3_finalize(stmt_); } void bind(int idx, int val) { sqlite3_bind_int(stmt_, idx, val); } bool step() { int rc = sqlite3_step(stmt_); return rc == SQLITE_ROW || rc == SQLITE_DONE; } }; // 使用 sqlite3* db; Statement stmt(db, "INSERT INTO notes(title, content) VALUES(?, ?)"); stmt.bind(1, 123); stmt.bind(2, "hello"); stmt.step(); // 自动 finalize优势:step()后stmt_自动释放,无需sqlite3_reset()。
5.3 多线程日志的无锁环形缓冲区设计
避免std::mutex锁竞争,资料实现LockFreeRingBuffer:
#include <atomic> #include <array> template<typename T, size_t N> class LockFreeRingBuffer { std::array<T, N> buffer_; std::atomic<size_t> head_{0}, tail_{0}; public: bool push(const T& item) { size_t t = tail_.load(std::memory_order_acquire); size_t next_t = (t + 1) % N; if (next_t == head_.load(std::memory_order_acquire)) return false; // full buffer_[t] = item; tail_.store(next_t, std::memory_order_release); return true; } bool pop(T& item) { size_t h = head_.load(std::memory_order_acquire); if (h == tail_.load(std::memory_order_acquire)) return false; // empty item = buffer_[h]; head_.store((h + 1) % N, std::memory_order_release); return true; } };内存序说明:acquire保证后续读取看到之前写入,release保证之前写入对其他线程可见。
5.4libcurl同步请求的异常安全封装
原始curl_easy_perform()返回CURLE_*错误码。资料用 RAII 封装句柄,并确保异常时自动 cleanup:
class CurlHandle { CURL* handle_; public: CurlHandle() : handle_(curl_easy_init()) { if (!handle_) throw std::runtime_error("curl init failed"); } ~CurlHandle() { if (handle_) curl_easy_cleanup(handle_); } void setopt(CURLoption option, auto&& value) { curl_easy_setopt(handle_, option, value); } void perform() { CURLcode res = curl_easy_perform(handle_); if (res != CURLE_OK) { throw std::runtime_error(std::string("curl error: ") + curl_easy_strerror(res)); } } }; // 使用 CurlHandle curl; curl.setopt(CURLOPT_URL, "https://api.example.com/note"); curl.setopt(CURLOPT_WRITEFUNCTION, +[](void* ptr, size_t size, size_t nmemb, void* userp) { std::string* s = static_cast<std::string*>(userp); s->append(static_cast<char*>(ptr), size * nmemb); return size * nmemb; }); curl.setopt(CURLOPT_WRITEDATA, &response); curl.perform(); // 异常时自动 cleanup5.5 项目集成测试:用gtest验证 SQLite 数据一致性
资料提供完整测试用例,验证事务回滚:
#include <gtest/gtest.h> #include "database.h" TEST(DatabaseTest, TransactionRollback) { Database db(":memory:"); // in-memory DB db.exec("CREATE TABLE notes(id INTEGER PRIMARY KEY, title TEXT)"); { auto tx = db.beginTransaction(); db.exec("INSERT INTO notes(title) VALUES('test1')"); db.exec("INSERT INTO notes(title) VALUES('test2')"); // 不 commit,作用域结束自动 rollback } auto count = db.query<int>("SELECT COUNT(*) FROM notes").value_or(0); EXPECT_EQ(count, 0); // 确保回滚成功 }beginTransaction()返回 RAII 对象,析构时自动COMMIT或ROLLBACK(根据是否调用commit())。
5.6 易错点:项目层最易被忽视的“部署即崩溃”三宗罪
这些错误在开发机完美运行,上线后必现:
易漏点:Qt 资源文件(
.qrc)未随可执行文件部署
→QFile(":/icons/save.png")在开发时存在,但打包后若未windeployqt或macdeployqt,资源路径失效。
→ 怎么补:用QResource::registerResource()动态加载.rcc文件,或构建时强制复制资源目录。易漏点:SQLite 数据库路径硬编码为相对路径
→Database db("notes.db")在 IDE 中工作,但 Linux systemd 服务启动时
本文还有配套的精品资源,点击获取