我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

C++进阶:从内存管理到并发编程的现代C++核心技术与工程实践

C++进阶:从内存管理到并发编程的现代C++核心技术与工程实践 1. 从“会用”到“精通”的思维转变很多朋友学C可能都是从学校课程或者某个在线教程开始的跟着敲代码知道int、for、class怎么用项目也能跑起来。这算是“会用”了。但当你真正踏入工业级项目面对动辄几十万行代码、需要高性能、高并发的场景时你会发现之前学的那些语法糖远远不够。这时候需要的是一种“精通”的思维——不仅仅是知道语言有什么特性更要深刻理解这些特性背后的设计哲学、实现机制、性能开销以及它们之间的精妙配合。上篇我们梳理了基础到进阶的语法和面向对象下篇我们就来啃这些硬骨头聊聊如何让你的C代码从“能跑”变得“跑得快”、“跑得稳”、“跑得优雅”。这不仅仅是技术的堆砌更是一种工程素养和设计能力的体现。2. 内存管理的艺术超越new/delete手动管理内存是C给予程序员最大自由同时也带来最大责任的地方。现代CC11及以后提供了强大的工具来帮助我们更安全、更高效地管理资源其核心思想是RAII。2.1 智能指针你的内存管家new和delete必须成对出现但在复杂的逻辑流如异常、多重返回中保证这一点极其困难。智能指针通过将动态分配的内存生命周期与对象本身绑定来自动化管理。std::unique_ptr独占所有权这是最常用、开销最小的智能指针。它独占所指向的对象不能被复制只能被移动。这完美模拟了裸指针的独占语义同时避免了内存泄漏。#include memory #include iostream class Widget { public: Widget() { std::cout Widget constructed\n; } ~Widget() { std::cout Widget destroyed\n; } void doSomething() { std::cout Working...\n; } }; void process() { // 创建一个独占指针管理一个Widget对象 std::unique_ptrWidget upw std::make_uniqueWidget(); upw-doSomething(); // 函数结束upw超出作用域自动调用delete无需手动操作 // 即使doSomething抛出异常析构也会被调用资源安全释放。 } // unique_ptr 的移动语义 std::unique_ptrWidget createWidget() { auto p std::make_uniqueWidget(); // ... 一些操作 return p; // 正确通过移动返回 } int main() { process(); auto myWidget createWidget(); // 所有权从函数内转移到myWidget // main结束myWidget析构资源释放 return 0; }注意优先使用std::make_unique而非直接new。make_unique在构造对象和创建智能指针时提供了更强的异常安全性。例如在函数调用foo(std::unique_ptrT(new T), some_function())中如果new T成功但some_function()抛出异常那么已分配的T对象就会泄漏。而foo(std::make_uniqueT(), some_function())则避免了这个问题。std::shared_ptr共享所有权当多个实体需要“共享”同一个对象且无法确定谁最后使用时就需要共享指针。它通过引用计数来跟踪有多少个shared_ptr指向同一对象计数归零时销毁对象。#include memory #include vector class Resource { // 某种昂贵资源 }; void shareResource() { std::shared_ptrResource sp1 std::make_sharedResource(); { std::shared_ptrResource sp2 sp1; // 复制引用计数1现在为2 std::cout Inside block, use count: sp2.use_count() std::endl; } // sp2 析构引用计数-1变为1 std::cout Outside block, use count: sp1.use_count() std::endl; } // sp1 析构引用计数变为0Resource被销毁std::weak_ptr打破循环引用的观察者shared_ptr最大的陷阱是循环引用A持有B的shared_ptrB也持有A的shared_ptr导致引用计数永远无法归零内存泄漏。weak_ptr是为解决此问题而生它指向一个由shared_ptr管理的对象但不增加其引用计数。你需要通过lock()方法尝试获取一个临时的shared_ptr来访问对象。#include memory #include iostream class Node { public: std::shared_ptrNode next; std::weak_ptrNode prev; // 使用weak_ptr指向前一个节点打破循环 ~Node() { std::cout Node destroyed\n; } }; int main() { auto node1 std::make_sharedNode(); auto node2 std::make_sharedNode(); node1-next node2; node2-prev node1; // 这里是weak_ptr赋值不会增加node1的引用计数 // 此时node1引用计数为1被main的变量持有node2引用计数为2被main和node1-next持有 // 当main结束时node1先析构其nextnode2引用计数减为1。 // 接着node2析构引用计数归零被销毁。完美避免了循环引用。 return 0; }实操心得智能指针选用指南默认使用unique_ptr表达独占所有权性能零开销与裸指针相同是大多数场景的首选。需要共享时用shared_ptr明确对象需要被多个所有者共享且生命周期不确定。持有shared_ptr的“非拥有性”引用时用weak_ptr典型场景如缓存、观察者模式、打破循环引用。避免在接口中传递裸指针或引用指向智能指针管理的资源除非你非常清楚该接口不会涉及所有权的转移或生命周期的延长。接口应明确接受shared_ptrT、const shared_ptrT或weak_ptrT来表明意图。2.2 移动语义与右值引用性能优化的利器C11引入的移动语义旨在解决不必要的深拷贝带来的性能损耗。其核心是右值引用T。理解左值、右值、将亡值左值有名字、有地址、可以取地址的表达式。如变量、函数返回的左值引用。右值临时对象、字面量除了字符串字面量它是左值、返回非引用类型的函数调用。如42xy的结果std::move(x)的返回值。将亡值是右值的一个子集指生命周期即将结束的对象。它是移动语义操作的主要目标。std::move强制转换为右值std::move本身不移动任何东西它只是无条件地将其参数转换为右值引用相当于一个static_castT。移动的操作发生在该右值被用于构造或赋值时调用移动构造函数或移动赋值运算符。class Buffer { char* data; size_t size; public: // 移动构造函数 Buffer(Buffer other) noexcept // noexcept 很重要标准库容器在扩容时会优先使用移动 : data(other.data), size(other.size) { other.data nullptr; // 至关重要置空源对象使其处于有效但可析构状态 other.size 0; } // 移动赋值运算符 Buffer operator(Buffer other) noexcept { if (this ! other) { delete[] data; // 释放当前资源 data other.data; size other.size; other.data nullptr; other.size 0; } return *this; } // ... 析构函数、拷贝构造/赋值等 }; void takeOwnership(Buffer buf) { // buf 在这里是一个左值有名字但其类型是右值引用 // 我们可以移动它到另一个地方 Buffer localBuf std::move(buf); // 再次使用std::move因为buf是左值 } int main() { Buffer buf1(1024); Buffer buf2 std::move(buf1); // 调用移动构造函数buf1的资源被“窃取” // 此时buf1处于有效但空的状态可以安全析构或赋予新值 takeOwnership(std::move(buf2)); // 将buf2作为右值传入 return 0; }完美转发std::forward在模板编程中我们有时需要将一个函数的参数原封不动地保持其左值/右值属性传递给另一个函数。这就是完美转发的用武之地它通常与通用引用T 在模板推导或auto推导中形成配合使用。templatetypename T, typename... Args std::unique_ptrT make_unique(Args... args) { // Args... 是通用引用 return std::unique_ptrT(new T(std::forwardArgs(args)...)); // 完美转发参数 }踩坑记录std::move和std::forward是编译期动作不产生运行时开销。但滥用std::move很危险特别是对局部变量移动后如果再使用它行为是未定义的。一个常见错误是在返回局部变量时使用std::move这反而会阻止编译器的返回值优化。3. 模板元编程与编译期计算C的模板不仅是实现泛型编程的工具更是一个功能强大的编译期计算子系统。通过模板我们可以在编译期完成类型计算、值计算甚至执行复杂的算法。3.1 类型萃取与SFINAE类型萃取是模板元编程的基础用于在编译期获取和操作类型信息。标准库在type_traits中提供了大量类型特征查询和转换的模板。#include type_traits #include iostream templatetypename T void printTypeInfo() { std::cout std::boolalpha; std::cout Is integral: std::is_integralT::value std::endl; std::cout Is pointer: std::is_pointerT::value std::endl; std::cout With const removed: typeid(typename std::remove_constT::type).name() std::endl; } int main() { printTypeInfoconst int(); // 输出 int 的信息 printTypeInfodouble*(); }SFINAE是“替换失败并非错误”的缩写。它是C模板重载决议的核心规则之一在模板参数推导和替换过程中如果某个候选模板导致无效代码如类型表达式无意义编译器不会报错而是简单地将其从重载集中剔除。#include iostream #include type_traits // 版本1针对有serialize成员函数的类型 templatetypename T auto serialize(const T obj) - decltype(obj.serialize(), std::string()) { std::cout Calling member serialize.\n; return obj.serialize(); } // 版本2针对其他类型如基本类型的通用版本 templatetypename T std::string serialize(const T obj) { std::cout Calling generic serialize.\n; return std::to_string(obj); } class MyClass { public: std::string serialize() const { return MyClass data; } }; int main() { MyClass obj; std::cout serialize(obj) std::endl; // 调用版本1 std::cout serialize(42) std::endl; // 调用版本2 // 对于serialize(42)编译器尝试匹配版本1。 // decltype(obj.serialize(), std::string()) 中的 obj.serialize() 对 int 是无效表达式。 // 根据SFINAE这个版本被忽略不报错。然后匹配版本2成功。 }C17引入了std::void_t它利用SFINAE来检测类型成员是否存在是编写类型特征检查的利器。C20的概念则提供了更清晰、更强大的方式来约束模板参数可以看作是SFINAE的语法糖和升级版。3.2 可变参数模板可变参数模板允许模板接受任意数量、任意类型的参数是实现如std::tuple、std::function、std::make_shared等强大工具的基础。#include iostream // 递归终止函数 void print() { std::cout End of recursion.\n; } // 可变参数模板函数 templatetypename T, typename... Args void print(T first, Args... args) { std::cout first ; print(args...); // 递归展开参数包 } // 使用折叠表达式 (C17) 更优雅 templatetypename... Args void printFold(Args... args) { (std::cout ... args) std::endl; // 二元左折叠 } int main() { print(1, 2.5, hello, a); // 输出: 1 2.5 hello a End of recursion. printFold(1, 2.5, hello, a); // 输出: 12.5helloa }实操心得模板元编程的应用场景性能关键路径的编译期计算如数学库中的常数、查找表生成。静态多态与策略模式通过模板实现编译期选择的策略零运行时开销。类型安全的通用容器和算法标准库的容器和算法是最好例子。反射的模拟通过特化和类型萃取可以在一定程度上模拟运行时类型信息。领域特定语言如Boost.Spirit用于解析器的编译期构造。但模板元编程代码可读性差、编译错误信息晦涩、编译时间长。务必在明确收益如显著的性能提升或更强的类型安全时才使用并做好文档和测试。4. 并发编程驾驭多线程现代CPU是多核的并发编程是释放其性能潜力的关键。C11在标准库中引入了线程支持使得编写跨平台并发程序成为可能。4.1 线程基础与数据竞争创建线程很简单但安全地协调线程是难点。#include iostream #include thread #include vector void hello(int id) { std::cout Hello from thread id (ID: std::this_thread::get_id() )\n; } int main() { std::vectorstd::thread workers; for (int i 0; i 5; i) { workers.emplace_back(hello, i); // 创建并启动线程 } for (auto t : workers) { t.join(); // 等待所有线程结束 } std::cout Main thread done.\n; return 0; }数据竞争是并发编程的头号敌人。当多个线程在没有同步的情况下访问同一内存位置且至少有一个是写操作时就会发生数据竞争导致未定义行为。#include thread #include iostream int counter 0; // 共享数据 void increment() { for (int i 0; i 100000; i) { counter; // 数据竞争 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout Counter value: counter std::endl; // 几乎肯定不是200000 return 0; }4.2 互斥锁与锁的管理最基本的同步原语是互斥锁std::mutex。但直接使用lock()和unlock()容易因异常或提前返回导致死锁。#include mutex std::mutex mtx; int shared_data 0; void unsafe_increment() { mtx.lock(); // 如果这里抛出异常锁永远不会被释放 shared_data; mtx.unlock(); }RAII锁管理器是解决方案。std::lock_guard和std::unique_lock在构造时加锁析构时自动解锁即使发生异常也能保证锁被释放。void safe_increment() { std::lock_guardstd::mutex lock(mtx); // 构造时锁定mtx shared_data; // lock 析构时自动解锁mtx } // unique_lock 更灵活可以延迟加锁、转移所有权等 void flexible_operation() { std::unique_lockstd::mutex lock(mtx, std::defer_lock); // 暂不加锁 // ... 做一些不需要锁的操作 lock.lock(); // 手动加锁 // ... 操作共享数据 lock.unlock(); // 可以手动解锁 // ... 其他操作 // 析构时如果锁仍持有会自动解锁 }死锁通常发生在需要同时获取多个锁时。标准库提供了std::lock函数它可以一次性锁定多个互斥量且保证不会死锁。std::mutex mtx1, mtx2; void process_with_two_locks() { // 错误的做法容易死锁 // std::lock_guardstd::mutex lock1(mtx1); // std::lock_guardstd::mutex lock2(mtx2); // 正确的做法 std::unique_lockstd::mutex lock1(mtx1, std::defer_lock); std::unique_lockstd::mutex lock2(mtx2, std::defer_lock); std::lock(lock1, lock2); // 一次性锁定两个无死锁风险 // ... 安全地操作受两个锁保护的资源 }4.3 条件变量与线程间通信互斥锁用于互斥访问而std::condition_variable用于线程间的等待/通知机制常用于生产者-消费者模式。#include iostream #include thread #include mutex #include condition_variable #include queue std::queueint data_queue; std::mutex mtx; std::condition_variable cv; bool finished false; void producer() { for (int i 0; i 10; i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时 { std::lock_guardstd::mutex lock(mtx); data_queue.push(i); std::cout Produced: i std::endl; } cv.notify_one(); // 通知一个等待的消费者 } { std::lock_guardstd::mutex lock(mtx); finished true; } cv.notify_all(); // 通知所有消费者结束 } void consumer(int id) { while (true) { std::unique_lockstd::mutex lock(mtx); // 等待条件队列非空或生产结束 cv.wait(lock, [] { return !data_queue.empty() || finished; }); if (finished data_queue.empty()) { break; // 生产结束且队列已空退出循环 } // 条件满足处理数据 int value data_queue.front(); data_queue.pop(); lock.unlock(); // 尽早释放锁让其他消费者可以运行 std::cout Consumer id consumed: value std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟消费耗时 } std::cout Consumer id exiting.\n; } int main() { std::thread prod(producer); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod.join(); cons1.join(); cons2.join(); return 0; }重要提示条件变量的使用有一个经典模式cv.wait(lock, predicate)。wait会在阻塞线程前自动释放锁并在被唤醒后重新获取锁。predicate一个返回bool的lambda或函数是必须的用于防止虚假唤醒即线程被唤醒不是因为notify而是由于系统原因。在检查条件和开始等待之间必须持有锁以防止竞态条件。4.4 原子操作与内存模型对于简单的计数器或标志位使用互斥锁开销过大。C提供了std::atomic模板用于定义原子类型对其的读写操作是不可分割的无需额外的锁。#include atomic #include thread #include iostream std::atomicint atomic_counter{0}; // 初始化 void atomic_increment() { for (int i 0; i 100000; i) { atomic_counter.fetch_add(1, std::memory_order_relaxed); // 原子加1 } } int main() { std::thread t1(atomic_increment); std::thread t2(atomic_increment); t1.join(); t2.join(); std::cout Atomic counter value: atomic_counter.load() std::endl; // 一定是200000 return 0; }内存序是原子操作中更深入的话题。它定义了原子操作周围非原子内存访问的可见性顺序。std::memory_order_relaxed只保证原子操作本身的原子性不提供同步和顺序保证性能最好。std::memory_order_seq_cst顺序一致性默认提供最强的保证但性能开销也最大。std::memory_order_acquire和std::memory_order_release通常配对使用用于实现高效的锁和同步原语。除非你在进行极低层次的并发数据结构设计否则使用默认的memory_order_seq_cst通常是安全且简单的选择。5. 现代C惯用法与最佳实践掌握了工具还要知道如何用好。下面是一些在现代C项目中广泛认可的最佳实践。5.1 资源管理RAII无处不在RAII不仅是用于内存。任何需要成对出现的操作都可以用RAII封装文件句柄、网络连接、数据库事务、锁等等。class FileHandle { FILE* fp; public: explicit FileHandle(const char* filename, const char* mode) : fp(fopen(filename, mode)) { if (!fp) throw std::runtime_error(Failed to open file); } ~FileHandle() { if (fp) fclose(fp); } // 禁用拷贝 FileHandle(const FileHandle) delete; FileHandle operator(const FileHandle) delete; // 允许移动 FileHandle(FileHandle other) noexcept : fp(other.fp) { other.fp nullptr; } FileHandle operator(FileHandle other) noexcept { if (this ! other) { if (fp) fclose(fp); fp other.fp; other.fp nullptr; } return *this; } // 使用接口 void write(const std::string data) { if (fwrite(data.c_str(), 1, data.size(), fp) ! data.size()) { throw std::runtime_error(Write failed); } } };5.2 使用const和noexcept正确表达意图const尽可能使用const。它让代码意图更清晰编译器能进行更多优化并防止意外修改。const成员函数承诺不修改对象状态除非成员被mutable修饰。const参数函数内部不会修改该参数。const返回值返回的对象不能被修改除非强制转换。noexcept如果一个函数保证不会抛出异常就标记它为noexcept。这有两方面好处编译器可能生成更高效的代码。标准库组件如std::vector在重新分配内存时会优先使用移动构造函数如果它是noexcept的否则会使用拷贝构造函数以保证强异常安全。class MyString { char* data; public: // 移动构造函数标记为noexcept使标准容器能高效移动 MyString(MyString other) noexcept : data(other.data) { other.data nullptr; } // 一个不会失败的交换操作 void swap(MyString other) noexcept { using std::swap; swap(data, other.data); } // 一个不修改对象状态的查询函数 size_t length() const noexcept { // 假设我们有一个存储长度的成员变量len // return len; return 0; // 示例 } };5.3 拥抱STL算法与Lambda表达式很多新手喜欢手写循环。但在大多数情况下使用STL算法配合Lambda表达式代码更简洁、更不易出错而且通常性能更好因为算法实现可能使用了特定优化。#include vector #include algorithm #include iostream int main() { std::vectorint vec {5, 2, 8, 1, 9, 3}; // 传统循环 for (size_t i 0; i vec.size(); i) { if (vec[i] 5) { std::cout vec[i] ; } } std::cout std::endl; // 使用STL算法和Lambda std::for_each(vec.begin(), vec.end(), [](int n) { if (n 5) std::cout n ; }); std::cout std::endl; // 更函数式的写法copy_if std::vectorint large_numbers; std::copy_if(vec.begin(), vec.end(), std::back_inserter(large_numbers), [](int n) { return n 5; }); for (int n : large_numbers) std::cout n ; std::cout std::endl; // 排序 std::sort(vec.begin(), vec.end()); // 查找 auto it std::find(vec.begin(), vec.end(), 8); // 累加 int sum std::accumulate(vec.begin(), vec.end(), 0); return 0; }Lambda表达式[capture](params) - ret { body }是现代C的利器。捕获列表[]按值捕获所有外部变量、[]按引用捕获所有、[var]按值捕获特定变量、[var]按引用捕获特定变量需要根据生命周期仔细选择避免悬空引用。5.4 错误处理异常与错误码的权衡C同时支持异常和错误码两种错误处理机制。异常适用于不可恢复的、罕见的错误如内存耗尽、文件不存在、无效输入。异常的优势是错误处理代码与非错误代码分离调用链可以自动向上传播。但异常有运行时开销并且要求代码是异常安全的RAII是达成此目标的关键。错误码适用于可预期的、频繁发生的错误如网络超时、解析失败。它没有运行时开销但需要每个调用层手动检查并传递错误码容易遗漏。最佳实践建议在构造函数和操作符重载中失败时优先使用异常因为很难通过返回值报告错误。在性能极其关键的路径上或者与C语言接口交互时使用错误码。对于析构函数绝对不要抛出异常。如果析构函数中调用的操作可能失败请吞下异常或记录日志。使用noexcept明确标识不会抛出异常的函数。自定义异常类型应从std::exception派生以便被通用处理器捕获。class MyException : public std::runtime_error { public: explicit MyException(const std::string msg) : std::runtime_error(msg) {} }; void riskyOperation(int param) { if (param 0) { throw MyException(Parameter cannot be negative); } // ... 正常操作 } void process() { try { riskyOperation(-1); } catch (const MyException e) { std::cerr Caught my exception: e.what() std::endl; // 尝试恢复或清理 } catch (const std::exception e) { std::cerr Caught standard exception: e.what() std::endl; } catch (...) { std::cerr Caught unknown exception std::endl; throw; // 重新抛出 } }6. 性能分析与调试技巧写出正确的代码是第一步写出高效的代码是进阶要求。6.1 性能分析工具Profiler使用像gprof、Valgrind的callgrind、perfLinux或Visual Studio Profiler等工具找到代码的热点消耗CPU最多的函数。基准测试使用Google Benchmark库进行微基准测试比较不同算法或实现的性能。内存检查使用Valgrind的memcheck检测内存泄漏、越界访问。在Linux下这是必备工具。6.2 常见的性能陷阱与优化避免不必要的拷贝使用引用传递大对象使用移动语义返回局部对象。注意std::vector的增长策略push_back可能导致多次重新分配和拷贝。如果知道大致大小使用reserve()预分配空间。选择合适的数据结构std::list在中间插入快但内存不连续缓存不友好遍历慢。std::vector在尾部插入快缓存友好但中间插入慢。std::map红黑树保证有序std::unordered_map哈希表平均查找O(1)。警惕虚函数的开销虚函数调用比普通函数调用多一次间接寻址。在极端性能敏感的循环中可以考虑用CRTP等静态多态替代。理解缓存局部性顺序访问内存比随机访问快得多。设计数据结构时让一起使用的数据在内存中尽量靠近。6.3 调试心智与工具核心工具熟练使用GDB或LLDB进行断点、单步、查看变量、调用栈回溯。日志在关键路径添加有意义的日志使用不同的日志级别INFO, DEBUG, WARN, ERROR。断言使用assert宏或自定义断言在调试版本中检查不变量。它在发布版本中通常被禁用。** sanitizers**现代编译器GCC/Clang提供的地址消毒剂AddressSanitizer、内存消毒剂MemorySanitizer、未定义行为消毒剂UBSan等能在运行时检测大量常见错误比Valgrind更快。# 使用clang编译并开启AddressSanitizer clang -g -O1 -fsanitizeaddress -fno-omit-frame-pointer your_program.cpp -o your_program7. 迈向大型项目构建、测试与设计模式个人项目与大型工业级项目的主要区别在于可维护性、可测试性和可扩展性。7.1 构建系统CMake现代C项目几乎都用CMake作为构建系统生成器。它跨平台能很好地管理依赖、编译选项和安装规则。一个简单的CMakeLists.txt示例cmake_minimum_required(VERSION 3.10) project(MyAwesomeProject VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 添加可执行文件 add_executable(my_app main.cpp src/utility.cpp include/utility.h) # 添加头文件目录 target_include_directories(my_app PUBLIC include) # 添加编译选项 target_compile_options(my_app PRIVATE -Wall -Wextra -Werror) # 查找并链接库例如Threads find_package(Threads REQUIRED) target_link_libraries(my_app PRIVATE Threads::Threads) # 如果项目有库 add_library(my_lib STATIC src/mylib.cpp) target_include_directories(my_lib PUBLIC include) target_link_libraries(my_app PRIVATE my_lib)7.2 单元测试Google Test没有测试的代码是不可靠的。Google Test是C领域最流行的单元测试框架之一。// mylib.h #pragma once int add(int a, int b); // mylib.cpp #include mylib.h int add(int a, int b) { return a b; } // test_mylib.cpp #include gtest/gtest.h #include mylib.h TEST(AddTest, PositiveNumbers) { EXPECT_EQ(add(2, 3), 5); } TEST(AddTest, NegativeNumbers) { EXPECT_EQ(add(-1, -1), -2); } TEST(AddTest, MixedNumbers) { EXPECT_EQ(add(5, -3), 2); } int main(int argc, char **argv) { ::testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }7.3 常用设计模式在C中的实现设计模式是解决特定问题的经典方案模板。C的特性如模板、RAII使得某些模式的实现非常优雅。单例模式确保一个类只有一个实例并提供全局访问点。现代C中使用Meyers Singleton局部静态变量是线程安全的C11起。class Singleton { public: static Singleton getInstance() { static Singleton instance; // C11保证此初始化是线程安全的 return instance; } // 删除拷贝构造和赋值 Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; void doSomething() { /* ... */ } private: Singleton() default; // 私有构造函数 ~Singleton() default; };工厂模式用于创建对象而不指定具体类。结合智能指针和std::function可以很灵活。class Product { public: virtual ~Product() default; virtual void use() 0; }; class ConcreteProductA : public Product { void use() override { std::cout Using A\n; } }; class ConcreteProductB : public Product { void use() override { std::cout Using B\n; } }; using ProductPtr std::unique_ptrProduct; class Factory { public: static ProductPtr createProduct(const std::string type) { if (type A) return std::make_uniqueConcreteProductA(); if (type B) return std::make_uniqueConcreteProductB(); return nullptr; } };观察者模式定义对象间的一对多依赖当一个对象状态改变时所有依赖它的对象都得到通知并自动更新。可以用std::function和信号槽库如Boost.Signals2高效实现。C的学习是一个持续的过程语言标准也在不断演进C20的协程、概念、范围库带来了新的范式。但万变不离其宗扎实理解内存模型、对象生命周期、模板机制和并发基础就能以不变应万变。最好的学习方式永远是阅读优秀代码如标准库实现、Boost库、动手写项目、在调试和优化中踩坑并总结。希望这篇“万字总结”能成为你C进阶之路上一份有用的参考地图。
返回列表