ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Effective C++与现代C++实践:从RAII到智能指针的编程哲学

Effective C++与现代C++实践:从RAII到智能指针的编程哲学 1. 从“能用”到“好用”为什么《Effective C》依然是C程序员的必修课最近在带新人做项目review代码时又看到了那些熟悉的“坑”一个类里塞满了public成员资源管理全靠手动new/delete拷贝构造函数和赋值运算符重载直接用了编译器默认生成的版本结果在多线程环境下数据竞争得一塌糊涂。每次看到这些我都会想起十几年前自己刚学C时踩过的那些雷然后默默地把Scott Meyers的《Effective C》电子版链接发过去。你可能觉得奇怪一本最初基于C98/03标准写的书在C11/14/17甚至20都普及的今天还有必要看吗我的答案是不仅有必要而且更加重要。这本书讲的从来不是某个特定版本的语法糖而是一种编程哲学——如何用C的语言特性写出健壮、高效、易于维护的代码。它教你的是“道”而非“术”。那些关于对象生命周期、资源管理、接口设计、继承与多态的准则在智能指针、移动语义大行其道的今天其核心思想依然熠熠生辉。很多新手觉得C难不是难在语法复杂而是难在不知道“好”的代码应该长什么样以及为什么应该长那样。《Effective C》就是那把帮你建立“好代码”直觉的钥匙。这本书的55条准则Item每一条都源于大量工程实践中的教训总结。它不是教科书不会教你for循环怎么写它是军规告诉你战场上哪里容易踩地雷以及如何绕过去。接下来我会结合我这些年在游戏服务器、高频交易系统等对性能和稳定性要求极高的场景下的实战经验为你拆解其中最为核心、也最容易在实践中被忽视的几条准则并融入现代CC11/14/17的最佳实践让你不仅能读懂书更能用上书。2. 资源管理从“手动挡”到“智能驾驶”的核心转变资源泄露是C程序最常见的Bug之一也是让很多初学者对C望而却步的原因。传统上我们遵循“谁申请谁释放”的原则但这在异常安全、代码路径复杂的情况下极易出错。《Effective C》的条款13到17系统阐述了资源管理其核心思想可以概括为使用对象来管理资源即RAIIResource Acquisition Is Initialization。2.1 RAII将资源绑定到对象生命周期RAII不是什么新概念但理解其精髓的人并不多。它的核心是在构造函数中获取资源在析构函数中释放资源。这样只要对象本身的生命周期被正确管理例如在栈上创建离开作用域自动销毁或者作为成员变量随其所属对象一起销毁资源就会得到自动、正确的释放。举个例子我们来看一个手动管理文件句柄的“反面教材”void processFile(const std::string filename) { std::fstream file(filename, std::ios::in); if (!file.is_open()) { // 处理打开失败 return; } // ... 一系列复杂的文件操作 ... if (someErrorCondition) { // 发生错误提前返回 return; // 文件没有显式关闭依赖析构函数如果后面还有清理代码呢 } // ... 更多操作 ... file.close(); // 理想情况下的手动关闭 }这段代码的问题在于在someErrorCondition成立时函数提前返回file.close()没有被执行。虽然std::fstream的析构函数会关闭文件但这依赖于我们熟知这个细节并且假设后续没有其他会抛出异常或导致提前返回的操作。在更复杂的资源如动态内存、互斥锁、数据库连接管理中这种依赖是不可靠的。正确的做法是依赖RAII。对于文件std::fstream本身已经是RAII对象我们只需要确保其生命周期即可。更通用的模式是使用智能指针来管理动态内存这是现代C对《Effective C》资源管理思想最直接的实践。2.2 智能指针现代C的“资源管家”书中提到了auto_ptr现已废弃而现代C提供了std::unique_ptr,std::shared_ptr,std::weak_ptr这一套组合拳。std::unique_ptr对应条款13、14独占所有权的智能指针。它实现了“资源获取即初始化”和“析构即释放”。当你需要一个对象独占某块内存或任何资源时就用它。#include memory void useUniquePtr() { // 创建一个独占的Widget对象 std::unique_ptrWidget pw(new Widget()); // 使用pw... // 离开作用域时pw自动销毁并删除其管理的Widget对象 // 无需手动delete }关键点std::unique_ptr禁止拷贝拷贝构造函数被删除但允许移动移动构造函数和移动赋值运算符。这强制实现了独占语义避免了多个指针指向同一资源导致的重复释放问题。这也是对条款14“在资源管理类中小心copying行为”的现代化实现——通过delete禁用拷贝通过移动语义转移资源所有权。std::shared_ptr对应条款15、16共享所有权的智能指针。通过引用计数管理资源生命周期。当最后一个shared_ptr被销毁时资源才会被释放。void useSharedPtr() { std::shared_ptrWidget sp1 std::make_sharedWidget(); { std::shared_ptrWidget sp2 sp1; // 引用计数1 // sp1和sp2共享同一个Widget对象 } // sp2离开作用域引用计数-1 // 引用计数不为0Widget对象依然存在 } // sp1离开作用域引用计数变为0Widget对象被销毁重要技巧优先使用std::make_shared来创建shared_ptr。它通常只需一次内存分配将对象本身和控制块分配在连续内存中比先new再构造shared_ptr更高效并且是异常安全的避免了new成功但构造shared_ptr前发生异常导致的内存泄漏。std::weak_ptr条款20的延伸弱引用指针不增加引用计数。它用于解决shared_ptr的循环引用问题。weak_ptr必须通过lock()方法尝试提升为shared_ptr来访问资源如果资源已被释放则返回空的shared_ptr。class Observer; class Subject { std::vectorstd::weak_ptrObserver observers_; // 使用weak_ptr避免Observer和Subject相互持有shared_ptr导致循环引用 };实战心得在项目初期就确立资源管理规范。我的经验法则是默认使用std::unique_ptr除非明确需要共享所有权才使用std::shared_ptr并时刻警惕循环引用。对于文件、网络连接、锁等非内存资源可以封装成自定义的RAII类或者使用标准库如std::lock_guard和可靠的第三方库。2.3 成对使用的资源与RAII封装条款15提到了“在资源管理类中提供对原始资源的访问”。对于像互斥锁lock/unlock这类必须成对使用的APIRAII封装尤其重要。C11的mutex库已经提供了完美的范例#include mutex std::mutex g_mutex; void unsafe_function() { g_mutex.lock(); // ... 临界区操作 ... if (someError) { return; // 糟糕锁没有释放 } // ... 更多操作 ... g_mutex.unlock(); // 可能还是会被异常跳过 } void safe_function() { std::lock_guardstd::mutex lock(g_mutex); // 构造时加锁 // ... 临界区操作 ... if (someError) { return; // 没问题lock_guard析构时会自动解锁 } // ... 更多操作 ... } // lock_guard离开作用域自动解锁std::lock_guard就是一个典型的RAII资源管理类。它不提供直接操作原始mutex的接口如手动unlock而是通过生命周期自动管理彻底杜绝了忘记解锁的可能性。这就是“使用对象管理资源”力量的体现。3. 设计与声明构建稳固的抽象边界写C不仅仅是实现功能更是设计接口和类型系统。《Effective C》的条款18-35花了大量篇幅讨论如何设计良好的类、函数和模板。这里我挑几个最容易在实践中出问题的点。3.1 让接口容易被正确使用不易被误用这是条款18的核心。一个糟糕的接口会引诱甚至强迫用户犯错。例如一个表示日期的类class BadDate { public: BadDate(int month, int day, int year); // 参数顺序容易搞混 // ... }; BadDate d(30, 3, 2023); // 糟糕把日和月写反了编译还能通过。改进方法之一是使用“外覆类型”wrapper types来区分逻辑类型struct Day { explicit Day(int d) : val(d) {} int val; }; struct Month { explicit Month(int m) : val(m) {} int val; }; struct Year { explicit Year(int y) : val(y) {} int val; }; class GoodDate { public: GoodDate(const Month m, const Day d, const Year y); // ... }; GoodDate d(Month(3), Day(30), Year(2023)); // 顺序错了类型不匹配编译报错 GoodDate d(30, 3, 2023); // 错误不能将int转换为Month等类型。通过引入Month、Day、Year这些具有明确语义的类型并在构造函数前加上explicit防止隐式转换我们利用编译器在编译期就帮我们排除了参数顺序错误这种低级Bug。这比写一堆运行时检查的代码要可靠得多。3.2 成员变量封装与访问控制条款22说“将成员变量声明为private”。这不仅仅是出于数据隐藏的OOP教条更有其深刻的实践意义语法一致性如果客户通过函数访问成员那么他们就不需要记住哪些是变量、哪些是函数因为访问函数可以伪装成变量即getter/setter。更精确的访问控制你可以轻松实现只读、只写、读写等不同权限而public变量只有“读写”一种权限。封装这是最重要的。如果所有成员变量都是private你可以随时改变其内部实现例如将int类型改为long或者将直接存储改为计算得出而无需修改客户代码。只要接口成员函数保持不变客户就感知不到变化。一个常见的误区提供了getter和setter就等于封装好了。并非如此。如果你为每个private成员变量都机械地提供public的getXxx和setXxx那在可维护性上和你直接把变量声明为public区别不大只是语法上绕了个弯。真正的封装意味着你限制对数据的访问只提供必要的、安全的操作接口。很多时候setter是不必要的甚至是有害的。3.3 继承与面向对象设计条款32-40深入探讨了继承。这里我想强调一个关键点区分接口继承和实现继承。纯虚函数virtual func() 0;只继承接口。派生类必须提供自己的实现。普通虚函数virtual func();继承接口和一份默认实现。派生类可以选择覆盖它也可以使用基类的默认实现。非虚函数void func();继承接口和一份强制性实现。派生类不应该重新定义它条款36。容易踩的坑为纯虚函数提供默认实现。这听起来矛盾但技术上可行class Airplane { public: virtual void fly() 0; // 纯虚函数接口 // ... }; void Airplane::fly() { // 纯虚函数也可以有实现 // 默认的飞行实现 } class ModelA : public Airplane { public: virtual void fly() override { Airplane::fly(); // 调用默认实现 } };这样做的好处是派生类可以通过显式调用基类实现来复用代码避免了在派生类中重复编写相同代码。但这也带来了混淆fly()到底是接口还是实现更好的做法可能是将默认实现作为一个独立的protected非虚函数如defaultFly()然后在需要默认行为的派生类fly()中调用它。这样意图更清晰。关于多重继承条款40慎用它带来了名字冲突、菱形继承等复杂性。如果一定要用优先考虑“接口类”所有函数都是纯虚函数无成员变量的多重继承这类似于Java或C#的接口。对于实现的多重继承通常有更好的替代方案如组合composition或通过std::variant、策略模式等实现。4. 实现魔鬼藏在细节里即使设计好了接口实现过程中的细微差别也可能导致性能天差地别或难以察觉的Bug。这部分对应书中条款26-31。4.1 延后变量定义并在能初始化时立刻初始化条款26建议“尽可能延后变量定义式的出现”。这样做有两个好处避免不必要的构造和析构开销。如果一个变量在代码块早期定义但可能在某个条件分支后才使用那么在条件不满足的分支里这个变量就被白白构造和析构了。提高代码清晰度。变量在即将被使用的地方定义读者更容易理解它的用途。更重要的是定义时立刻初始化条款4。对于内置类型避免“先定义再赋值”// 不好 std::string name; name getUserName(); // 更好 std::string name getUserName(); // 或 std::string name(getUserName());对于自定义类型直接初始化通常比先默认构造再赋值更高效因为它可能避免了一次不必要的默认构造过程。4.2 关于inline、const、mutable和volatileinline条款30这是一个对编译器的请求而非命令。编译器可以忽略它。将函数定义在类体内友元函数除外会隐式地将其声明为inline。inline的优点是消除函数调用开销缺点是可能增加目标代码体积代码膨胀从而可能降低指令缓存命中率。通常小巧、频繁调用的函数适合inline如getter/setter。记住inline函数和模板通常应该放在头文件中。const条款2-37这是C中最强大的工具之一。尽可能使用const。它可以施加于任何作用域的对象、函数参数、函数返回值、成员函数。const成员函数条款3承诺不修改对象的逻辑状态mutable成员除外。这使该函数可以在const对象上调用并且是接口设计中的重要部分告诉用户这个函数是“只读”的。const可以帮助编译器进行优化。const和non-const成员函数避免重复条款3当const和non-const成员函数有相同的实现时可以用non-const版本调用const版本并通过const_cast去除const性需谨慎而不是反过来或重复代码。mutable用于修饰类的成员变量表示即使在const成员函数中该变量也可以被修改。通常用于缓存、互斥量等与对象逻辑状态无关的“物理状态”。class CachedData { private: mutable std::mutex cacheMutex_; // const函数也需要加锁 mutable Data cachedData_; mutable bool cacheValid_ false; public: Data getData() const { std::lock_guardstd::mutex lock(cacheMutex_); if (!cacheValid_) { // 计算并填充 cachedData_... cacheValid_ true; } return cachedData_; } };volatile告诉编译器该变量可能被程序本身以外的代理如硬件、其他线程修改因此禁止编译器对该变量的读写进行激进的优化如缓存到寄存器、指令重排。在嵌入式或底层系统编程中常见但在标准多线程编程中volatile不能替代原子操作或内存屏障线程间的数据同步应使用std::atomic或std::mutex等机制。4.3 处理异常异常安全条款29是编写健壮C代码的关键。它有三个基本保证级别基本保证如果异常被抛出程序内的任何事物仍然保持在有效状态下。没有资源泄漏所有对象处于可析构状态。强烈保证如果异常被抛出程序状态不改变。调用要么完全成功要么完全失败就像没调用过一样。这通常通过“copy and swap” idiom实现。不抛掷保证承诺绝不抛出异常。作用于内置类型如int指针上的所有操作都提供了不抛掷保证。一个关键技巧copy and swap。这是实现强烈异常安全性的常用手法。class WidgetImpl; // 实现细节的类 class Widget { public: Widget(const Widget rhs); Widget operator(const Widget rhs) { Widget temp(rhs); // 第一步为rhs数据制作一份副本可能抛出异常但*this尚未改变 swap(temp); // 第二步将副本与*this的数据交换。swap通常不抛异常仅交换指针。 return *this; // 第三步返回。temp离开作用域销毁原*this的数据。 } void swap(Widget other) noexcept { using std::swap; swap(pImpl, other.pImpl); // 仅交换指针高效且安全 } private: WidgetImpl* pImpl; };这个实现提供了强烈的异常安全性。如果在temp(rhs)拷贝构造过程中发生异常*this的原始状态完全不受影响。swap操作通常只交换指针等简单类型可以做到noexcept。最后旧的资源随着temp的析构而释放。5. 继承与面向对象设计多态与虚函数这是C的核心也是容易产生误解的地方。条款34-40对此有深入讨论我结合现代实践补充几点。5.1 绝不重新定义继承而来的非虚函数条款36明确指出这一点。非虚函数是静态绑定的编译期根据指针或引用的类型决定调用哪个版本而虚函数是动态绑定的运行期根据对象的实际类型决定。class Base { public: void nonVirtual() { std::cout Base::nonVirtual\n; } virtual void virtualFunc() { std::cout Base::virtualFunc\n; } }; class Derived : public Base { public: void nonVirtual() { std::cout Derived::nonVirtual\n; } // 隐藏了Base::nonVirtual不好 virtual void virtualFunc() override { std::cout Derived::virtualFunc\n; } }; int main() { Derived d; Base* pb d; Derived* pd d; pb-nonVirtual(); // 输出 Base::nonVirtual (静态绑定看指针类型) pd-nonVirtual(); // 输出 Derived::nonVirtual pb-virtualFunc(); // 输出 Derived::virtualFunc (动态绑定看对象类型) pd-virtualFunc(); // 输出 Derived::virtualFunc }重新定义非虚函数会导致令人困惑的行为通过基类指针调用的是基类版本通过派生类指针调用的是派生类版本。这违反了“公有继承”意味着“是一个is-a”关系的原则。如果派生类需要改变基类非虚函数的行为那么这个函数在基类中就应该被设计为虚函数。5.2 绝不重新定义继承而来的缺省参数值条款38解释了原因缺省参数值是静态绑定的而虚函数是动态绑定的。class Shape { public: virtual void draw(int borderWidth 1) const { std::cout Shape with border borderWidth std::endl; } }; class Rectangle : public Shape { public: virtual void draw(int borderWidth 2) const override { // 重新定义了缺省参数 std::cout Rectangle with border borderWidth std::endl; } }; int main() { Shape* ps new Rectangle; ps-draw(); // 调用 Rectangle::draw但缺省参数用的是 Shape::draw 的 1 // 输出Rectangle with border 1 delete ps; }输出结果违背直觉。为了避免这个问题避免在虚函数中使用缺省参数。如果必须使用考虑使用“非虚接口NVI” idiom条款35将缺省参数放在非虚的公共接口函数中该函数再调用一个私有的虚函数做实际工作。5.3 通过复合composition塑造“有一个”或“根据某物实现出”的关系条款38和39强调了“复合”的重要性。继承表示“是一个is-a”关系而复合表示“有一个has-a”或“根据某物实现出is-implemented-in-terms-of”关系。“有一个”Person对象有一个Address、一个Job。这很直观用成员变量即可。“根据某物实现出”你希望Set类根据std::list实现出来但Set并不是一种listSet不是list所以不应该用公有继承。你可以私有继承list但更推荐的做法是复合即将list作为成员变量。// 使用复合实现 Set templatetypename T class Set { public: bool insert(const T item) { if (std::find(data_.begin(), data_.end(), item) data_.end()) { data_.push_back(item); return true; } return false; } bool contains(const T item) const { return std::find(data_.begin(), data_.end(), item) ! data_.end(); } // ... 其他Set操作基于 std::list 实现 private: std::listT data_; // 复合Set 有一个 list };这种方式比私有继承更灵活耦合度更低。你可以在Set内部随意更换容器类型比如换成std::vector而对外接口不变。私有继承通常只在需要访问基类的protected成员或需要重写基类的虚函数时使用即“根据某物实现出”且需要利用多态性这种情况相对少见。6. 模板与泛型编程类型安全的抽象模板是C实现泛型编程的利器但也容易写出难以理解和编译的代码。条款41-55提供了模板使用的黄金准则。6.1 了解隐式接口和编译期多态对于面向对象编程我们关心的是显式接口函数签名和运行期多态虚函数。对于模板编程我们关心的是隐式接口模板类型参数必须支持的操作和编译期多态通过模板具现化和函数重载实现。templatetypename T void process(T w) { if (w.size() 10 w ! someGlobalWidget) { T temp(w); temp.normalize(); temp.swap(w); } }对于模板函数process类型T的隐式接口是它必须提供一个返回类型可与int比较的size()成员函数支持operator!与某个全局对象比较有一个拷贝构造函数以及normalize()和swap(T)成员函数。这些约束不是在基类中声明的而是通过模板体中的表达式隐式定义的。这带来了极大的灵活性但也要求模板作者对类型的要求有清晰的文档说明。C20的Concepts特性正是为了将这种隐式接口显式化、规范化。6.2 了解typename的双重含义在模板声明中typename和class通常可以互换。但在模板内部typename有一个特殊用途标识嵌套从属类型名称。templatetypename C void print2nd(const C container) { if (container.size() 2) { // 假设C有一个value_type的嵌套类型定义 // C::const_iterator iter container.begin(); // 可能编译错误 typename C::const_iterator iter container.begin(); // 正确 iter; // 假设C::value_type是容器元素的类型 // C::value_type value *iter; // 可能编译错误 typename C::value_type value *iter; // 正确 std::cout value std::endl; } }C::const_iterator和C::value_type是“嵌套从属类型名称”因为它们嵌套在模板参数C内部并且依赖于C的具体类型。编译器在解析模板时默认假设这样的名称不是一个类型可能是静态数据成员。我们必须用typename关键字明确告诉编译器“这是一个类型”。这个规则有个例外在基类列表和成员初始化列表中不能使用typename。6.3 将与参数无关的代码抽离模板模板会导致代码膨胀因为每个不同的模板参数组合都会生成一份独立的代码。条款44建议将模板中与模板参数无关的部分抽离出来以减少重复。因非类型模板参数造成的膨胀例如一个为固定尺寸矩阵提供支持的模板。templatetypename T, std::size_t n class SquareMatrix { // 处理 n x n 矩阵 public: void invert(); // 求逆 }; SquareMatrixdouble, 5 sm1; SquareMatrixdouble, 10 sm2; // 会实例化两份 invert 代码尽管算法可能完全相同改进让矩阵尺寸成为一个函数参数或成员变量而不是模板参数。templatetypename T class SquareMatrixBase { // 尺寸无关的基类 protected: void invert(std::size_t matrixSize); // 尺寸作为参数 // ... }; templatetypename T, std::size_t n class SquareMatrix : private SquareMatrixBaseT { private: using SquareMatrixBaseT::invert; // 避免名称隐藏 public: void invert() { this-invert(n); } // 调用基类版本传入尺寸 };这样invert的算法部分只在SquareMatrixBaseT中有一份SquareMatrix只是薄薄的一层包装。因类型参数造成的膨胀int和long在多数平台上有相同的底层表示但vectorint和vectorlong会产生两份几乎相同的代码。某些情况下可以让它们共享同一份底层实现例如通过使用void*指针但会丧失类型安全。这需要权衡通常编译器优化已经做得不错除非有确切的性能分析证据否则不必过度优化。7. 定制new和delete理解内存管理的底层对于大多数应用使用标准operator new和operator delete以及容器、智能指针就足够了。但在某些特定领域如游戏开发、高频交易定制内存管理可以带来显著的性能提升。条款49-52讨论了如何定制new和delete。7.1 理解new-handler的行为当operator new无法满足内存申请时它会先调用一个错误处理函数——new-handler。你可以通过std::set_new_handler()来设置它。一个设计良好的new-handler应该做以下几件事之一让更多内存可用例如程序启动时分配一大块内存作为备用池此时释放一些出来。安装另一个new-handler如果当前handler无法解决问题可以安装一个能力更强的。卸除new-handler将new-handler设置为nullptr这样operator new在失败时会直接抛出std::bad_alloc异常。抛出std::bad_alloc或派生异常。不返回调用abort()或exit()终止程序。为特定类定制new-handler可以重载其静态成员函数set_new_handler和operator new。这样当为该类对象分配内存失败时会调用该类自己的new-handler而不是全局的。7.2 为什么需要定制new和delete检测运用错误自定义的new可以在分配的内存前后放置签名signaturedelete时检查签名是否被破坏用以检测缓冲区溢出或重复释放。提升性能通用分配器为了处理任意大小、任意生命周期的内存请求通常比较复杂且慢。针对特定类型、特定大小的对象可以实现更高效、碎片更少的分配器。例如一个固定大小的对象池memory pool。收集使用统计在分配和释放时记录信息用于分析内存使用模式定位内存泄漏或优化分配策略。弥补默认分配器的非最佳对齐某些硬件平台对特定类型的数据有严格的对齐要求默认new可能无法保证。将相关对象成簇集中例如将同一个数据结构中的节点分配在相邻内存提高缓存局部性。获得非传统行为例如在共享内存中分配或者被垃圾收集器管理的内存中分配。7.3 实现一个简单的对象池对象池是定制内存管理的经典案例。它为特定类的对象预分配一大块内存池分配时从池中取一个空闲对象释放时将其放回池中。这避免了频繁向系统申请/释放小块内存减少了碎片提高了效率。class MyClassPool { public: static void* operator new(std::size_t size) { if (size ! sizeof(MyClass)) { return ::operator new(size); // 大小不对回退到全局new } // 从空闲链表中获取一个节点 if (freeList_ nullptr) { // 空闲链表为空分配一大块新内存例如一次分配10个对象 expandPool(); } void* p freeList_; freeList_ *(static_castvoid**(freeList_)); // 将下一空闲节点设为链表头 return p; } static void operator delete(void* p) noexcept { if (p nullptr) return; // 将释放的节点插回空闲链表头部 *(static_castvoid**(p)) freeList_; freeList_ p; } private: static void expandPool() { std::size_t chunkSize 10 * sizeof(MyClass); void* newBlock ::operator new(chunkSize); // 向系统申请一大块内存 // 将这块内存切成多个MyClass大小的节点并串成空闲链表 char* p static_castchar*(newBlock); for (std::size_t i 0; i 10; i) { void* node p i * sizeof(MyClass); *(static_castvoid**(node)) freeList_; freeList_ node; } } static void* freeList_; // 空闲链表头指针 }; void* MyClassPool::freeList_ nullptr;这是一个高度简化的示例实际应用中需要考虑线程安全、对齐、与new[]/delete[]的兼容性等问题。但它的核心思想展示了定制内存管理如何工作接管特定类型的分配请求用更高效、专用的策略来管理内存。8. 杂项编程范式的融合与取舍最后一部分条款53-55和一些贯穿全书的思想提醒我们C是一个支持多种范式的语言要懂得取舍。8.1 明智地使用extern CC支持与C语言和其他语言交互。extern C用于指示编译器按照C语言的命名和调用约定来链接函数。这在编写供C语言调用的C库或使用C语言编写的库时必不可少。// 在C头文件中这样声明一个C函数 #ifdef __cplusplus extern C { #endif void c_function(int arg); // C语言风格的函数 #ifdef __cplusplus } #endif这样在C代码中链接c_function时编译器不会进行名称修饰name mangling链接器就能找到C语言编译的目标文件中的正确符号。8.2 让自己熟悉标准库尤其是TR1和C11/14/17这是条款54。如今C11/14/17/20已经带来了翻天覆地的变化。熟悉标准库能极大提升开发效率和代码质量。除了容器和算法你至少应该了解智能指针memory如前所述是资源管理的基石。正则表达式regex文本处理的利器。线程支持库thread,mutex,atomic,future现代多线程编程的基础。随机数库random比C的rand()更强大、更可控。时间库chrono精确、类型安全的时间操作。元组tuple、可变参数模板实现泛型组件的重要工具。移动语义和右值引用这是现代C性能优化的关键它使得资源转移而非拷贝成为可能直接实现了条款29中“copy and swap”的优化版本。Lambda表达式让函数对象就地定义与STL算法完美结合。类型推导auto和decltype简化代码特别是在模板和复杂类型声明中。范围for循环更简洁的容器遍历语法。8.3 熟悉Boost库条款55建议熟悉Boost。Boost是一个经过同行评审、高质量、可移植的C库集合。许多Boost组件后来被纳入C标准库如智能指针、线程、正则表达式、随机数、函数对象绑定等。即使你现在使用较老的编译器也可以通过Boost获得这些现代特性。此外Boost还包含大量尚未进入标准但极其有用的库如asio网络编程、spirit解析器生成、graph图算法等。将Boost视为标准库的延伸是学习先进C技术和思想的好地方。最后的个人体会学习《Effective C》不是一蹴而就的。我第一次读的时候很多条款只觉得“有道理”但理解不深。直到在项目中踩了坑回头再看才恍然大悟“原来它早就警告过我了”。最好的学习方式是把这本书放在手边在编码和review时经常问自己“我现在的做法符合书中的哪条准则有没有更好的写法” 久而久之这些准则就会内化成你的编程习惯让你写出的C代码自然而然地更加健壮、高效和优雅。C很复杂但遵循这些经过时间检验的最佳实践能让你在复杂的道路上走得更加稳健。
返回列表