Item20:当shared_ptr可能悬空时使用weak_ptr
std::weak_ptr 是 std::shared_ptr 的辅助智能指针,不独立存在,特点如下: 不参与资源所有权共享,不影响指向对象的引用计数 能检测自身是否悬空(过期),即指向的对象是否已被销毁 无法直接解引用、无法直接判空,需借助 std::shared_ptr 实现安全访问 基础用法创建从 std::shared_ptr 直接创建,两者指向同一对象,且不会增加对象的引用计数: 123auto spw = std::make_shared<Widget>(); // 对象引用计数(RC)=1std::weak_ptr<Widget> wpw(spw); // RC仍为1,wpw与spw指向同一对象spw = nullptr; // RC=0,对象销毁,wpw悬空(过期) 安全访问对象(解决竞态条件)单独调用 expired() 判断是否过期存在竞态风险(检查后到访问前,对象可能被其他线程销毁),需通过原子操作同时完成 “检查 + 访问”,有两种方式: 方式 操作 效果 ...
Item11:优先选用删除函数,而非private未定义函数
主要考虑复制构造函数和复制赋值运算符:C++ 会自动生成这类特殊成员函数,但部分场景(如iostream的basic_ios类)中拷贝操作存在逻辑歧义(如流的读取状态、资源句柄拷贝混乱),因此需要主动禁止这些函数被调用。 C++98 禁止函数调用的方案:私有声明 + 未定义为阻止复制构造 / 赋值运算符被使用,C++98 采取 “声明为 private + 不定义” 的方式: 12345678template <class charT, class traits = char_traits<charT>>class basic_ios : public ios_base {public: …private: basic_ios(const basic_ios& ); // not defined basic_ios& operator=(const basic_ios&); // not defined}; 作用逻辑:private修饰阻止客户端代码调用;故意不...
Item12:使用override重写声明函数
面向对象编程的基本理念是:在派生类中虚函数实现,会改写基类中对应虚函数的实现。 虚函数重写的约束条件要想重写一个函数,必须满足下列要求: 基类中的函数必须是虚函数。 基类和派生类中的函数名字必须完全相同(析构函数例外)。 基类和派生类中的函数形参类型必须完全相同。 基类和派生类中的函数常量性必须完全相同。 基类和派生类中的函数返回值和异常规格必须兼容。 基类和派生类中的函数引用限定符必须完全相同。(C++11新增) 引用限定符:限定类的非静态成员函数只能被左值对象或右值对象调用。带&的成员函数仅允许左值对象调用;带&&的成员函数仅允许右值对象调用。 如果基类中的虚函数带有引用饰词,则派生类要对该函数进行改写版本必须也带有完全相同的引用饰词。否则相当于重新定义了一个新的函数。 编译器可能不报警但是存在错误的例子: 123456789101112131415class Base {public: virtual void mf1() const; virtual void mf2(int x); virtual void m...
Item22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中
Pimpl 的价值Pimpl 的目的是减少编译依赖、加快编译速度: 传统写法中,Widget的头文件依赖std::string、std::vector、Gadget等头文件,一旦这些依赖变更,所有包含Widget.h的代码都要重新编译; Pimpl 把这些依赖移到.cpp文件中,头文件仅前向声明Impl结构体,用指针指向它,彻底切断 “头文件 - 实现依赖” 的关联。 C++98 用原始指针实现 Pimpl,现代 C++ 优先用std::unique_ptr(因为 pImpl 是 “独占所有权”),但直接用unique_ptr会踩坑。 用std::unique_ptr做 Pimpl 会编译报错1. 报错现象头文件仅前向声明Impl,用std::unique_ptr<Impl> pImpl,且未手动声明析构函数时,用户创建Widget对象会编译报错(提示 “sizeof/delete 应用到不完整类型”): 123456789101112// widget.h(有问题的写法)class Widget {public: Widget() : ...
Item13:优先选用const_iterator,而非iterator
STL 的const_iterator等价于 “指向常量的指针(pointer-to-const)”,指向的值不可修改,符合 “能加const就加” 的通用编程准则 —— 只要无需修改迭代器指向的值,就应优先使用const_iterator;但该迭代器的实用性随 C++ 版本迭代大幅提升,C++11 是关键转折点,同时通用代码中需优先使用非成员函数版本的begin/end/cbegin等。 各 C++ 版本对 const_iterator 的支持差异1. C++98:支持残缺,实用性差const_iterator理论上符合规范,但实际使用存在两大痛点: 创建麻烦:非const容器无法直接获取const_iterator,需强制类型转换(或绑定到const引用)才能得到; 使用受限:insert/erase等容器操作仅接受iterator,且const_iterator无法可移植转换为iterator(即使reinterpret_cast也不行); 最终导致开发者被迫放弃使用,违背 “能const则const” 的准则。 123456789101112typedef s...
Item15:尽可能使用constexpr
constexpr作用于对象时,是加强版的const;但作用于函数时含义截然不同 —— 它并非简单限定 “返回 const 值” 或 “结果编译期可知”,而是根据调用场景动态适配。 constexpr对象:必然具备const只读属性,且必须由编译期可知的值完成初始化(技术上是翻译期,包含编译 + 链接,编程层面可视为编译期)。 constexpr函数:不保证返回值是 const,也不保证结果一定编译期可知—— 结果是否编译期确定,完全取决于传入的实参: 若实参是编译期已知的常量 → 结果在编译期计算,可用于 “整型常量表达式” 语境; 若实参是运行期才能确定的值 → 函数退化为普通函数,在运行期计算结果。 编译期可知的常量整型值,可用于 C++ 要求 “整型常量表达式” 的核心语境:数组尺寸、整型模板实参(如std::array的长度)、枚举量的值、对齐修饰符(alignas)等。普通const对象无法满足这些场景(仅保证只读,不保证编译期初始化),因此: 所有constexpr对象都是const对象,但并非所有const对象都是constexpr对象。 cons...
Item16:让const成员函数线程安全
const 成员函数承诺 “不修改对象核心逻辑状态”,但常因mutable修饰的辅助变量(如多项式根的缓存rootVals、缓存有效标记rootsAreValid),在执行时修改这些变量; 多线程同时调用该 const 函数时,会出现 “不同线程读写同一块内存” 的数据竞争,导致程序行为未定义(即使逻辑上是 “读操作”)。 线程安全的解决方案1. 基础方案:使用std::mutex 适用场景:需要同步多个变量 / 多个操作(如缓存值 + 缓存有效标记),保证操作的原子性; 实现要点: 将std::mutex声明为mutable(因 const 成员函数中 mutex 被视为 const 对象,而锁 / 解锁操作会修改 mutex 状态); 用std::lock_guard自动加锁 / 解锁,避免手动解锁遗漏; 副作用:std::mutex不可拷贝 / 移动,导致包含它的类也不可拷贝 / 移动; 示例:多项式根缓存的线程安全实现(加锁后保证缓存计算 / 赋值的原子性)。 2. 轻量方案:使用std::atom...
Item7:在创建对象时注意区分()和{}
初始化的基本方式指定初始化的方式包括使用小括号、使用等号,或是使用大括号: 123int x(0);int y = 0;int z{0}; 很多情况下使用一个等号和一对大括号也是可以的:(处理方式等同于只有大括号) 1int z = {0}; 对于用户定义的类型,等号的初始化方式实际并没有发生赋值: 123Widget w1; // 调用的是默认构造函数Widget w2 = w1; // 井非赋值,调用的是复制构造函数w1 = w2; // 并非赋值.调用的是复制赋值运算符 C++11引入了统一初始化:使用大括号进行初始化。 使用大括号来制定容器的初始内容: 1std: :vector<int> v{ 1, 3, 5 }; // v 的初始内容为1、3、5 为非静态成员指定默认初始化值(也可以使用“=”,但是()不行): 12345678class Widget { ... private: int x{ 0 }; // 可行,x 的默认值为 0...
Item14:只要函数不抛出异常就加上noexcept声明
C++11 的noexcept是替代 C++98 废弃异常说明的核心特性,作为函数接口的关键组成部分,仅当确定函数永远不会抛出异常时声明;其核心价值是让编译器生成更高效的目标代码,同时为调用者 / STL 提供明确的异常安全承诺,且不可为了加noexcept扭曲函数实现,大多数函数因 “异常中立” 无需声明。 noexcept的核心价值性能层面:编译器最大化优化代码 逻辑:noexcept承诺 “函数异常传播时可直接终止程序”,编译器无需保证调用栈完整展开、局部对象按构造反序析构,因此可剔除冗余代码,生成更精简高效的机器码; 对比差异:noexcept函数的优化度远高于 C++98 的throw()或无异常声明的函数(后两者需预留栈展开的冗余代码); 1234567891011121314// noexcept函数:编译器无冗余准备,极致优化void moveWidget(Widget&& w) noexcept { // 无栈展开、析构顺序记录的冗余代码,机器码更精简}// throw()函数:需预留栈展开准备,优化有限(...
Item23:理解move和forward
两者的本质是编译期类型转换,运行时无任何操作 std::move不移动任何数据,std::forward不转发任何数据; 它们都是函数模板,仅在编译期对参数做值类别转换(左值→右值,或条件性转换),运行时不生成任何可执行代码(一字节都没有); 两者的核心作用都是改变参数的值类别(左值 / 右值),而非操作数据本身。 std::move:无条件将参数转为右值(但不保证能移动)1. 本质拆解接收一个通用引用参数,通过static_cast无条件转换为非引用类型的右值引用(用std::remove_reference确保不会因为 T 是左值引用而变成左值),最终返回一个右值。 简单说:std::move(x)的唯一作用是把x标记为右值,告诉编译器 “这个对象可以被移动”—— 但这只是 “提议”,不是 “强制”。 2. const 对象用 std::move 会 “伪移动”(实际是拷贝)123456789// 错误示范:给想移动的对象加了constclass Annotation {public: explicit Annotation(const std::...
