Item17:理解特殊成员函数的生成机制
特殊成员函数的定义与分类特殊成员函数指 C++ 编译器会自动生成的成员函数,不同标准版本涵盖范围不同: C++98:包含 4 种:默认构造函数、析构函数、复制构造函数、复制赋值运算符 C++11 及以上:新增 2 种:移动构造函数、移动赋值运算符 编译器自动生成的特殊成员函数,访问层级为public、inline、非虚(除非基类析构函数为虚,派生类析构函数才会为虚) 大三律(Rule of Three)若声明了复制构造函数、复制赋值运算符、析构函数中的任意一个,就需要同时声明这三个。 需要改写复制操作,通常意味着类涉及裸资源管理(堆内存、文件句柄等): 一种复制操作中做的资源管理,大概率也需要在另一种复制操作中执行; 析构函数需参与资源管理(通常是释放资源)。 推论:若用户声明了析构函数,说明类需手动管理裸资源,而编译器默认的 “逐成员复制” 仅拷贝资源标识(如指针地址),而非资源本身,会导致内存泄漏、重复释放等资源管理混乱问题。 C++11 的补充规则 析构函数影响:只要用户声明了析构函数,编译器不会生成移动操作(移动构造 / 移动赋值); 复制操作的废弃行...
Item24:区分万能引用和右值引用
T&&有两种身份,并非只有右值引用 我们默认T&&是右值引用,但事实是,T&&在源码中可能有两种完全不同的含义; 第一种:真正的右值引用—— 只能绑定到右值(临时对象、std::move后的对象),核心作用是识别可移动的对象; 第二种:通用引用(也叫转发引用)—— 本质是 “二重性引用”,源码上写的是T&&,但可以表现为左值引用或右值引用,能绑定几乎任何东西(左值 / 右值、const/non-const、volatile 等); 通用引用是一种 “抽象”(而非底层真相),底层是 “引用折叠”,但这种抽象足够实用,能帮我们快速区分两种引用。 通用引用的判断:必须同时满足两个条件1:存在类型推导 要么是函数模板形参,T(或模板参数)需要在函数调用时自动推导(排除调用者显式指定类型的边缘情况); 要么是auto&& 声明的对象,auto的类型需要在初始化时自动推导; 没有类型推导,就不可能是通用引用。 2:声明形式必须是纯 T&& 引用的声明必须严格是 “T&...
Item9:优先选用别名声明,而非typedef
具有两种声明别名的方法: 123typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS; using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>; 别名声明在处理涉及函数指针的类型时比较容易理解: 12345//FP是一个指向函数的指针的同义词,它指向的函数带有int和const std::string&形参,不返回任何东西typedef void (*FP)(int, const std::string&); //typedef//含义同上using FP = void (*)(int, const std::string&); //别名声明 别名声明可以模板化(被称为别名模板),而typedef不行: 使用别名声明: 1234template<typename T> usin...
Item8:优先选用nullptr,而非0或NULL
当C++在只能使用指针的语境中发现了0会把勉强解释为空指针,但是C++的基本观点还是0和NULL的类型是int,而非指针。 在C++98中,这样的观点可能在指针类型和整型之间进行重载时可能会发生意外: 1234567void f(int); // 整型版本void f(bool); // 布尔版本void f(void*); // 指针版本f(0); // 调用 f(int),而不是 f(void*)!f(NULL); // 可能通不过编译,但一般会调用 f(int)。从来不会调用 f(void*)f(nullptr); // 正确调用 f(void*) nullptr的优点它不具备整型类型。实际类型是std::nullptr_t,并且可以隐式转换到所有的裸指针类型。 提升代码的可读性: 1234567891011// 使用 0/NULL 的歧义auto result = findRecord();if (result == 0) { // result 是指针还是...
Item26:避免重载通用引用
通用引用太 “贪婪”,会抢走所有重载匹配 通用引用是模板 T&&,对任何类型实参都能精确匹配; 普通重载(如 int、const std::string&)需要类型转换 / 提升时,永远赢不过通用引用。 例子: 12void logAndAdd(int idx); // 普通重载template<typename T> void logAndAdd(T&& name); // 通用引用 传 short 时: 本想走 int 重载(需要类型提升) 结果通用引用精确匹配,直接抢走调用,导致后续无法转成 string,编译报错。 完美转发构造函数会 “劫持” 拷贝构造写一个带通用引用构造函数的类: 123456class Person {public: template<typename T> Person(T&& n); // 通用引用构造 Person(int idx); Person(const Person& r...
Item25:对右值引用使用move,对通用引用使用forward
右值引用用 std::move,通用引用用 std::forward 右值引用(T&&,仅绑定右值):只能绑定 “可以被移动” 的对象(比如临时对象、被 std::move 转换的左值),你能确定这个对象的所有权可以被转移。 通用引用(T&&,模板中):也叫转发引用,可能绑定左值(如局部变量)或右值(如临时对象),无法提前确定是否能移动。 基于这两个特性,正确用法是: 右值引用:无条件用std::move转换为右值(因为它绑定的对象一定可以移动)。示例(Widget 的移动构造函数): 12345678910class Widget {public: Widget(Widget&& rhs) // rhs是右值引用 : name(std::move(rhs.name)), // 无条件移动 p(std::move(rhs.p)) {}private: std::string name; std::shared_ptr<int> p;}; ...
Item28:理解引用折叠
引用折叠是 C++11 引入的底层编译期规则,是通用引用(转发引用)、std::forward能正常工作的核心 ——C++ 语法明确禁止显式声明 “引用的引用”(比如int& &),但编译器在特定场景下会隐式生成引用的引用,此时靠 “引用折叠规则” 将其化简为单个引用,这也是通用引用能同时绑定左值 / 右值的根本原因。 通用引用的类型推导编码规则对于通用引用模板: 12template<typename T>void func(T&& param); // param是通用引用 模板参数T会根据传入实参的 “值类别”(左值 / 右值)做编码推导: 传入左值 → T推导为左值引用(比如传入Widget左值,T= Widget&); 传入右值 → T推导为非引用类型(比如传入Widget右值,T= Widget)。 这种 “不对称编码” 是理解引用折叠的关键,比如: 12345Widget w; // 左值Widget widgetFactory(); // 返回右值的函数func(w)...
Item27:熟悉通用引用重载的替代方法
前提Item26 已证明:通用引用 + 普通重载 = 必出问题(劫持调用、编译报错、破坏拷贝 / 移动构造、继承失效)。 本条款给出5 种合法替代方案,从简单到高级,覆盖所有场景。 5 种通用引用重载的替代方案1. 放弃重载,直接改函数名 做法:把重载函数改成不同名字(如logAndAddName/logAndAddNameIdx) 优点:最简单,无任何匹配风险 缺点:构造函数不能用(名字固定为类名),失去重载的语法便利 2. 退而求其次:传 const T& 做法:放弃通用引用,用 C++98 的常量左值引用 优点:代码简单、匹配稳定、无坑 缺点:效率低,无法利用移动语义、做不到完美转发 3. 按值传递(pass by value) 做法:形参直接传值,配合std::move初始化成员 1explicit Person(std::string n) : name(std::move(n)) {} 优点:无匹配冲突,代码简洁,兼顾拷贝 / 移动 缺点:比完美转发略低效(多一次移动 / 拷贝) ...
Item29:认识移动操作的缺点
移动语义是 C++11 最核心的特性之一:它能在满足特定条件时,用低开销的移动操作替代高开销的复制操作,甚至能让 C++98 的代码重新编译后直接获得性能提升。但也正因如此,移动语义的效果和适用范围被普遍夸大,本条款的目的是让开发者理性看待移动语义,避免过度乐观。 移动语义无优势 / 失效的场景移动语义无法带来性能提升、甚至直接退化为复制操作的四类典型场景: 类型本身不支持移动操作:对于 C++98 遗留的未适配类型、不符合编译器默认生成移动操作条件的自定义类型(如声明了复制操作、移动操作或析构函数,或成员 / 基类禁用了移动),移动写法会直接退化为复制,无法带来任何性能提升。 移动操作并不比复制更快:并非所有支持移动的类型都有低开销的实现。比如 std::array,数据直接存储在对象本身而非堆内存,移动需要线性时间逐个处理元素,开销与复制处于同一量级;再比如开启了小字符串优化(SSO)的短 std::string,数据存储在对象内部缓冲区,移动开销与复制几乎无差异。 移动操作被上下文强制禁用:标准库部分容器操作要求强异常安全保证,若移动操作未声明noex...
Item35:优先考虑基于任务的编程而非基于线程的编程
两种异步执行方式的直观对比基于线程(thread-based):直接创建 std::thread12int doAsyncWork();std::thread t(doAsyncWork); // 直接创建线程执行函数 无直接获取返回值的方式; 若函数抛出异常,程序会直接调用std::terminate终止。 基于任务(task-based):使用 std::async1auto fut = std::async(doAsyncWork); // 提交任务,返回future对象 代码更简洁; future的get()函数可获取返回值,还能捕获函数抛出的异常(避免程序终止); 线程管理的责任交给标准库,而非开发者。 理解线程的三层含义 线程类型 定义 硬件线程 CPU 核心提供的真实执行单元,是计算的物理载体。 软件线程(OS 线程) 操作系统管理的线程,运行在硬件线程上,数量可多于硬件线程(阻塞时 OS 调度其他线程)。 std::thread C++ 中 “软件线程” 的句柄,可能为空(默认构造、移动、join、detach 后无对应软件线程)。 ...
