Item 18 对于独占资源使⽤std::unique_ptr
前言原始指针的一些问题:
它的声明不能指⽰所指到底是单个对象还是数组。
它的声明没有告诉你⽤完后是否应该销毁它,即指针是否拥有所指之物。
如果你决定你应该销毁对象所指,没⼈告诉你该⽤ delete 还是其他析构机制(⽐如将指针传给专⻔的销毁函数)。
如果你发现该⽤ delete。 原因 1 说了不知道是 delete 单个对象还是 delete 数组。如果⽤错了结果是未定义的。
假设你确定了指针所指,知道销毁机制,也很难确定你在所有执⾏路径上都执⾏了销毁操作(包括异常产⽣后的路径)。少⼀条路径就会产⽣资源泄漏,销毁多次还会导致未定义⾏为。
⼀般来说没有办法告诉你指针是否变成了悬空指针,即内存中不再存在指针所指之物。悬空指针会在对象销毁后仍然指向它们。
条款18:对于独占资源使⽤std::unique_ptr当你需要⼀个智能指针时,std::unique_ptr 通常是最合适的。可以合理假设,默认情况下,std::unique_ptr 等同于原始指针,而且对于⼤多数操作(包括取消引⽤),他们执⾏的指令完全相同。这意味着你甚⾄可以在内存和时间都⽐较紧张的情况下使⽤它。如果原始指针够小够 ...
Item 17 理解特殊成员函数的⽣成
条款17:理解特殊成员函数的生成在C++术语中,特殊成员函数是指 C++ ⾃⼰⽣成的函数。C++98 有四个:默认构造函数函数,析构函数,拷⻉构造函数,拷⻉赋值运算符。这些函数仅在需要的时候才⽣成,⽐如某个代码使⽤它们但是它们没有在类中声明。默认构造函数仅在类完全没有构造函数的时候才⽣成。⽣成的特殊成员函数是隐式public且inline,除⾮该类是继承⾃某个具有虚函数的类,否则⽣成的析构函数是⾮虚的。
但是时代改变了,C++⽣成特殊成员的规则也改变了。要留意这些新规则,因为⽤ C++ ⾼效编程⽅⾯很少有像它们⼀样重要的东西需要知道。
C++11 特殊成员函数俱乐部迎来了两位新会员:移动构造函数和移动赋值运算符。它们的签名是:
1234567class Widget {public: ... Widget(Widget&& rhs); Widget& operator=(Widget&& rhs); ...};
掌控它们⽣成和⾏为的规则类似于拷⻉系列。移动操作仅在需要的时候⽣成,如果⽣成了,就会对⾮ ...
Item 15 尽可能使用 constexpr
条款15:尽可能的使⽤constexprconstexpr 当⽤于对象上⾯,它本质上就是 const 的加强形式,但是当它⽤于函数上,意思就⼤不相同了。
从概念上来说,constexpr 表明⼀个值不仅仅是常量,还是编译期可知的。这个表述并不全⾯,因为当 constexpr 被⽤于函数的时候,事情就有⼀些细微差别了。
我现在只想说,你不能假设 constexpr 函数是 const,也不能保证它们的(译注:返回)值是在编译期可知的。最有意思的是,这些是特性。关于 constexpr 函数返回的结果不需要是 const,也不需要编译期可知这⼀点是良好的⾏为。
编译期可知的值 “享有特权”,它们可能被存放到只读存储空间中。对于那些嵌⼊式系统的开发者,这个特性是相当重要的。更⼴泛的应⽤是 “其值编译期可知” 的常量整数会出现在需要“整型常量表达式的 context 中,这类 context 包括数组⼤小,整数模板参数(包括 std::array 对象的⻓度),枚举量,对⻬修饰符(译注: alignas(val) ),等等。如果你想在这些 context 中使⽤变量,你⼀定会希望将它们声明为 ...
Item 16 让const成员函数线程安全
条款16:让const成员函数线程安全在数学领域中⼯作,我们就会发现⽤⼀个类表⽰多项式是很⽅便的。在这个类中,使⽤⼀个函数来计算多项式的根是很有⽤的。也就是多项式的值为零的时候。这样的⼀个函数它不会更改多项式。所以,它⾃然被声明为 const 函数。
12345class Polynomial {public: using RootsType = std::vector<double>; // 数据结构保存多项式为零的值 RootsType roots() const;};
计算多项式的根是很复杂的,因此如果不需要的话,我们就不做。如果必须做,我们肯定不会只做⼀次。所以,如果必须计算它们,就缓存多项式的根,然后实现 roots 来返回缓存的值。下⾯是最基本的实现:
1234567891011121314class Polynomial {public: using RootsType = std::vector<double>; RootsType roots() const { i ...
Item 14 如果函数不抛出异常请使⽤noexcept
条款 14:如果函数不抛出异常请使⽤noexcept在 C++98 中,你不得不写出函数可能抛出的异常类型,若果函数实现有所改变,异常说明也可能需要修改。 改变异常说明会影响客户端代码,因为调⽤者可能依赖原版本的异常说明。编译器不会为函数实现,异常说明和客⼾端代码中提供⼀致性保障。⼤多数程序员最终都认为不值得为C++98的异常说明如此⿇烦。
在C++11标准化过程中,⼤家⼀致认为异常说明真正有⽤的信息是⼀个函数是否会抛出异常。⾮⿊即⽩,⼀个函数可能抛异常,或者不会。这种 “可能-绝不” 的⼆元论构成了 C++11 异常说的基础,从根本上改变了 C++98 的异常说明。在 C++11 中,⽆条件的 noexcept 保证函数不会抛出任何异常。
关于⼀个函数是否已经声明为 noexcept 是接口设计的事。函数的异常抛出⾏为是客⼾端代码最关⼼的。调⽤者可以查看函数是否声明为 noexcept,这个可以影响到调⽤代码的异常安全性和效率。
就其本⾝而⾔,函数是否为 noexcept 和成员函数是否 const ⼀样重要。如果知道这个函数不会抛异常就加上 noexcept 是简单天真的接口说明。 ...
Item 13 优先考虑const_iterator而⾮iterator
条款13:优先考虑 const_iterator 而⾮ iterator“STL const_iterator 等价于指向常量的指针。它们都指向不能被修改的值。标准实践是能加上 const 就加上,这也指⽰我们对待 const_iterator 应该如出⼀辙。”
上面的说法对于 C++98 和 C++11 都是正确的,但是在 C++98 中,标准库对于 const_iterator 的支持不是很完整。
首先不容易创建它们,其次就算有了它,它的使用也是受限的。
假如你想在 std::vector<int> 中查找第⼀次出现 1983 (这是 C++ 代替 C with classes 的那⼀年)的位置,然后插⼊1998(这是第⼀个 ISO C++ 标准被接纳的那⼀年)。如果 vector 中没有 1983,那么就在 vector 尾部插⼊。在 C++98 中使⽤ iterator 可以很容易做到:
1234std::vector<int> values;...std::vector<int>::iterator it = std::find(val ...
深入理解虚拟内存
前言程序中编写业务逻辑代码的时候,往往需要引用这些创建出来的数据结构,并通过这些引用对相关数据结构进行业务处理。当程序运行起来之后就变成了进程,而这些业务数据结构的引用在进程的视角里全都都是虚拟内存地址,因为进程无论是在用户态还是在内核态能够看到的都是虚拟内存空间,物理内存空间被操作系统所屏蔽进程是看不到的。进程通过虚拟内存地址访问这些数据结构的时候,虚拟内存地址会在内存管理子系统中被转换成物理内存地址,通过物理内存地址就可以访问到真正存储这些数据结构的物理内存了。随后就可以对这块物理内存进行各种业务操作,从而完成业务逻辑。
本篇主要介绍:
前言
1. 到底什么是虚拟内存地址
2. 为什么要使用虚拟地址访问内存
3. 进程虚拟内存空间
4. Linux 进程虚拟内存空间
4.1 32 位机器上进程虚拟内存空间分布
4.2 64 位机器上进程虚拟内存空间分布
5. 进程虚拟内存空间的管理
5.1 内核如何划分用户态和内核态虚拟内存空间
5.2 内核如何布局进程虚拟内存空间
5.3 内核如何管理虚拟内存区域
5.4 定义虚拟内存区域的访问权限和行为规范
5.5 关联内存映射中的映射关 ...
Item 11 优先考虑delete
条款11:delete 和私有声明当写的代码不想被其他人调用的时候,通常不会声明这个函数,但是有时 C++ 会自动声明一些函数,如果你想防⽌客⼾调⽤这些函数,事情就不那么简单了。
上述场景⻅于特殊的成员函数,即当有必要时C++⾃动⽣成的那些函数。Item17 详细讨论了这些函数,但是现在,我们只关⼼拷⻉构造函数和拷⻉赋值运算符重载。
在 C++98 中防止调用这些函数的方法是将他们声明为私有的成员函数。举个例子:
在 C++ 标准库 iostream 继承链的顶部是模板类 basic_ios。所有 istream 和 ostream 类都继承此类(直接或者间接)。拷⻉ istream 和 ostream 是不合适的,因为要进⾏哪些操作是模棱两可的。⽐如⼀个 istream 对象,代表⼀个输⼊值的流,流中有⼀些已经被读取,有⼀些可能⻢上要被读取。如果⼀个 istream 被拷⻉,需要像拷⻉将要被读取的值那样也拷⻉已经被读取的值吗?解决这个问题最好的⽅法是不定义这个操作。直接禁⽌拷⻉流。
要使 istream 和 ostream 类不可拷⻉,basic_ios 在C++98中是这样声明 ...
Item 12 使⽤ override 声明重载函数
条款12:使⽤ override 声明重载函数在C++⾯向对象的世界里,涉及的概念有类,继承,虚函数。这个世界最基本的概念是派⽣类的虚函数重写基类同名函数。鉴于 “重写” 听起来像 “重载”,尽管两者完全不相关,下⾯就通过⼀个派⽣类和基类来说明什么是虚函数重写
12345678910111213class Base {public: virtual void doWork(); // 基类虚函数 …};class Derived: public Base {public: virtual void doWork(); // 重写Base::doWork(这里"virtual"是可以省略的) …};std::unique_ptr<Base> upb = std::make_unique<Derived>();// 创建基类指针指向派⽣类对象关于std::make_unique请参⻅Item1...upb->doWork(); // 通过基类指针调⽤doWork实际上是派⽣类的 ...
Item 10 Item10:优先考虑限域枚举而非未限域枚举
条款10:限域枚举和非限域枚举通常来说,在花括号中声明⼀个名字会限制它的作⽤域在花括号之内。但这对于 C++98 ⻛格的 enum 中声明的枚举名是不成⽴的。这些在 enum 作⽤域中声明的枚举名所在的作⽤域也包括 enum 本⾝,也就是说这些枚举名和 enum 所在的作⽤域中声明的相同名字没有什么不同
1234enum Color { black, white, red }; // black, white, red 和 // Color⼀样都在相同作⽤域auto white = false; // 错误! white早已在这个作⽤ // 域中存在
事实上这些枚举名泄漏进和它们所被定义的 enum 域⼀样的作⽤域。有⼀个官⽅的术语:未限域枚举(unscoped enum)在 C++11 中它们有⼀个相似物,限域枚举(scoped enum),它不会导致枚举名泄漏:
12345678enum ...