effective c++_【阅读笔记】Effective C++()
全文参考自Effective C++, Scott Meyers
程序来自本人
https://github.com/hxz1998/ccl
1. 让自己习惯C++
C++高效编程守则视状况而变化,取决于使用C++的哪一部分。
C++四大块:
- C
- Object-Oriented C++:面向对象
- Template C++:泛型编程,模板元编程
- STL:容器,迭代器,算法,函数对象
2. 尽量以const,enum,inline替换 #define
- 对于单纯常量,最好使用
const
或者enums
来替换#define
- 对于形似函数的宏(macros),最好使用
inline
函数来替换#define
有一种方法,可以获得宏带来的效率,以及一般函数带来的可预料行为以及类型安全性(Type Safety),例如:
template<typename T>inline T callWithMax(const T &a, const T &b) { return a > b ? a : b;}
int main() { int a = callWithMax<int>(1, 2); cout <endl;}
3. 尽可能使用 const
- 声明为
const
可以让编译器帮助检查错误。const
可以施加于任何作用域内的对象、函数参数、函数返回类型、成员函数。- 编译器强制实施bitwise constness,但编写程序时,应该使用“概念上的常量性”conceptual constness。
- 当
const
和non-const
成员函数有实质等价的实现时,要用non-const
版本去调用const
版本,这样可减少代码重复。
const
如果出现在 *
左边,那么表示被指物是常量;如果在 *
右边,那么表示指针是常量;如果出现在两边,那么表示指针和被指物都是常量。例如:
int main() { int a = 0, b = 1; int const *p1 = &a; int *const p2 = &a;
*p1 = 2; // 不行!因为 p1 指向的内容是常量 *p2 = 2; // 可以,p2 自身是常量,p2 只能指向a,但是 a 中的内容可以变 p1 = &b; // 可以,p1 指向另一个内容,并声称这个内容不可变 p2 = &b; // 不可以,p2 自身是常量,不能指向其他东西了}
试着习惯这样的写法:
void f1(const int *i); // 指向一个不能修改内容的 ivoid f2(int const *i); // 一猫猫一样
这俩写法效果是一样的,都是指向一个内容不可变的数据(指针本身可以再修改指向的对象)。
对于第三点,一个很好的例子如下:
class Text {private: std::string text;public: const char &operator[](std::size_t pos) const { return text[pos]; }
char &operator[](std::size_t pos) { return const_cast<char &> ( // 使用 const_cast 去掉 const 声明 static_cast<const Text &> // 使用 static_cast 把 *this 转换成 const 对象 (*this)[pos]); // 使用 (*this)[] 方法返回(这个时候是 const 结果,经过 const_cast 去掉 const 声明 }};
4. 确定对象被使用前已被初始化
- 为内置对象进行手工初始化,因为C++并不能保证完全初始化好它们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。
- 初始列列出的成员变量,其排列顺序要和它们在类声明中的一致。
- 为免除“跨编译单元初始化次序”问题,使用 local static 对象来代替 non-local static 对象。
如果成员变量是 const
的或者 references
的,那么它们一定需要有初值,不能被赋值。例如:
class X { const int val; int &re_val;public: X(int val_, int &re_val_) : val(val_), re_val(re_val_) {}};
基类总是比派生类要先初始化好,例如:
class X { const int val; int &re_val;public: X(int val_, int &re_val_) : val(val_), re_val(re_val_) { cout <"X initialization..." <endl; }};
class Y : public X {public: Y(int val, int &re_val) : X(val, re_val) { cout <"Y initialization..." <endl; };};
int main() { int re_v = 1; Y y(1, re_v); // >: X initialization... // Y initialization...}
由于定义于不同编译单元内的 non-local static
对象的初始化顺序并未明确定义,因此会出现这样情况:
- 定义在
File1.hh
中一个静态全局变量tfs
- 在
File2.cc
中使用tfs
那么如果 File1.hh
中的 tfs
还没初始化好呢,File2.cc
中就想使用了,那么就会出现大问题!例如下面这个例子:
// File1.hh
#include using namespace std;class FileSystem {public: size_t numDisks() const { return 0; }};extern FileSystem tfs;
// File2.cc
#include #include "File1.hh"using namespace std;class Directory { size_t disks;public: Directory() { disks = tfs.numDisks(); } size_t getDisks() const { return disks; }};int main() { Directory directory; cout <}
这个时候编译器就会报错:
CMakeFiles\local_static.dir/objects.a(File2.cc.obj):File2.cc:(.rdata$.refptr.tfs[.refptr.tfs]+0x0): undefined reference to `tfs'
很明显,在 local_static
目录中没有找到该引用,因此报错了,那么该怎样做呢?
使用方法(类似于工厂方法)来获取这个值,而不是依赖编译器初始化
例如:
// File1.hh
class FileSystem {public: size_t numDisks() const { return 0; }};FileSystem &getFS() { static FileSystem tfs; return tfs;}
// File2.ccclass Directory { size_t disks;public: Directory() { disks = getFS().numDisks(); } size_t getDisks() const { return disks; }};int main() { Directory directory; cout <}
这样一来,就不用担心了☺。
不过,这样还是有另外一个问题,例如多线程环境下还是有不确定情况,处理这种麻烦情况的做法之一是:在单线程启动阶段,手动调用一遍所有的 reference-returning
方法。这样可以消除与初始化有关的“竞速形式(race conditions)”
5. 了解C++默默编写并调用哪些函数?
编译器可以暗自为
class
创建default
构造函数、copy
构造函数,copy assignment
操作符,以及析构函数。
首先,开门见山地说,C++默认编写了默认构造函数、默认析构函数、拷贝构造函数,以及拷贝赋值函数,而且它们默认都是 inline
的。当然,这些函数的默认创建在一定时期是失效的,例如:
- 默认构造函数:当提供了一个构造函数后,编译器不再为类提供默认构造函数,而且默认。
- 默认析构函数:当提供了一个析构函数后,编译器就不再提供默认析构函数,默认析构函数是
non-virtual
的。 - 拷贝构造函数:只要没提供,而且满足可拷贝构造的条件,那么就提供,否则不提供。
- 拷贝赋值函数:只要没提供,而且满足可拷贝复制的条件,那么就提供,否则不提供。
上面说了两个条件,那么具体是什么条件呢?
5.1 可拷贝构造&可拷贝赋值?
先来看一下满足这俩条件的例子:
template<typename T>class NamedObject {private: T objectValue; string name;public: NamedObject(string n, T val) : name(n), objectValue(val) {} NamedObject(const NamedObject &rhs) { objectValue = rhs.objectValue; name = rhs.name + " copy "; }friend ostream &operator<const NamedObject &rhs) { os <" " < return os; } NamedObject &operator=(const NamedObject &rhs) { objectValue = rhs.objectValue; name = rhs.name + " = ";return *this; }};int main() {string newDog = "newDog";string oldDog = "oldDog";NamedObject<int> od(oldDog, 1);NamedObject<int> nd(newDog, 2);cout <" : " <endl; // >: 2 newDog : 1 oldDog nd = od; cout <// >: 1 oldDog =}
那么此时,即便自己不提供拷贝构造以及拷贝赋值构造操作符,编译器也会对成员变量进行递归的拷贝赋值过来。但是在遇到成员变量是 const
或者 reference
类型时,编译器就两手一摊,无能为力了(具体可参考Effective C++, 3th, P37)。
例如下面的例子:
template<typename T>class NamedObject {private: const T objectValue; string& name;public: // 其他函数都一样 NamedObject(const NamedObject &rhs) { objectValue = rhs.objectValue; name = rhs.name + " copy "; } NamedObject &operator=(const NamedObject &rhs) { objectValue = rhs.objectValue; // 不能对一个 const 对象赋值! name = rhs.name + " = ";return *this; }};int main() {string newDog = "newDog";string oldDog = "oldDog";NamedObject<int> od(oldDog, 1);NamedObject<int> nd(newDog, 2); nd = od; // >: error: use of deleted function 'NamedObject& NamedObject::operator=(const NamedObject&)'}
当然,这只是编译器不再提供了而已,用户自己还是可以设计如何去复制拷贝以及构造拷贝的,这完全取决于自己怎么处理成员变量。
除此之外,如果基类把拷贝构造函数设置成了 private
那么在派生类中也是没办法操作的。
6. 若不想使用编译器自动生成的函数,那该明确拒绝
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为
private
并且不予实现。或者使用继承Uncopyable
这样的基类。
如果不想让一个类支持拷贝构造或者赋值构造,那么我们可以将函数声明但不实现,例如这样子:
class Uncopyable {private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&);};
当然,对于每一个想实现这个功能的类都能去单独这样声明,不过,还可以使用继承方法去实现,例如:
class Uncopyable {protected: Uncopyable() = default;private: Uncopyable(const Uncopyable &); Uncopyable &operator=(const Uncopyable &);};class SubClass : public Uncopyable { // 默认不允许拷贝构造和赋值运算符};int main() { SubClass s1, s2; s1 = s2; // error!}
7. 为多态基类声明virtual析构函数
- 带多态性质的基类应该声明一个
virtual
析构函数。- 如果类带有任何
virtual
函数,那么它就应该拥有一个virtual
析构函数。- 如果类的设计目的不是用来做基类的,那么就不应该声明
virtual
析构函数。
当使用基类指针指向派生类对象时,没有问题,但是要是想把这个基类指针给删掉,这时候问题就来了,例如下面这个例子:
class BaseClass {private: char *name;public: BaseClass(int size) { name = new char[size]; for (int i = 0; i 'a'; } // 基类的析构函数 ~BaseClass() { delete name; }};
class DeriveClass : public BaseClass {private: char *count;public: DeriveClass(int size) : BaseClass(size) { count = new char[size]; for (int i = 0; i 'b'; } // 派生类的析构函数 ~DeriveClass() { delete count; }};int main() { // 多态用法,基类指针指向派生类对象,没毛病 BaseClass *obj = new DeriveClass(16); // 删除基类指针,出现了问题! delete obj; return 0;}
上面的程序乍一看看不出个毛病来,现在对 BaseClass *obj = new DeriveClass(16);
设置断点,进行单步调试,可以观察到构造函数过程是:
new DeriveClass(16) |BaseClass(16) |DeriveClass(16) | end
这个顺序完全正确,先构造基类再构造派生类嘛,执行完后,内存状态是这样的:
可以得知操作系统给这两个对象中的成员分配内存到了 name : 0x1061980
和 count : 0x10619c0
。
那么执行 delete obj;
时,顺序是这样的:
delete obj |~BaseClass() | end
从上面可以看出来,竟然只执行了基类的析构函数,而没有执行派生类的析构函数,那么这时的内存表示是怎么样的?见下图:
由此可见,在不经意间,就造成了内存泄漏问题,那么该如何解决这个问题呢?
很简单,只需要把基类的析构函数声明为 virtual
就可以了,这样强制去执行子类的析构函数。
不过,这样还是有两种结果,例如下面是一种结果:
class BaseClass { // 其他都一样 virtual ~BaseClass() { delete name; }};class DeriveClass : public BaseClass { // 其他都一样 ~DeriveClass() override { delete count; }};
这个时候,是先执行的派生类析构函数,再执行基类析构函数。
另一种结果是:
class BaseClass { virtual ~BaseClass() { delete name; }};class DeriveClass : public BaseClass { // 删掉了自己的析构函数};
这个情况下,才是先执行基类析构函数,再执行派生类析构函数。
8. 别让异常逃离析构函数
- 析构函数绝对不要抛出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。
- 如果接口使用者需要对某个操作函数运行期间抛出的异常做出反应,那么
class
应该提供一个普通函数(而不是在析构函数中)执行操作。
即便C++允许析构函数抛出异常,但是最好不要这样做。当然,吞掉异常也是有争议的,比如“草率地结束程序”可能会带来更严重的问题,或者“不明确的行为带来的风险”可能会带来不安全的问题等等,具体问题具体分析是比较好的。
但是,通常可以提供一个让用户在析构函数前控制异常的机会,例如使用“双重保险”来尽最大化确保问题得到解决。
9. 绝不在构造函数和析构函数过程中调用virtual函数
在构造和析构期间不要调用
virtual
函数,因为这类调用从不下降至派生类。
不管怎样,都不应该在构造函数和析构函数内部去调用一个 virtual
函数,因为这样的操作是不可预估的,带来意想不到的结果。为什么这样?因为在基类中,构造函数执行阶段或者析构函数执行阶段只能看到基类的内容,所以在派生类中实现的程序,是不可用的。下面这句话直白且有效的指出了问题的所在:
在基类(base-class)构造期间,
virtual
函数不是virtual
函数。
也正是因为这样一个“对象在 derived class
构造函数开始执行前,不会成为一个 derived class
对象”的规则,所以最好在构造期间对 virtual
函数视而不见。
那么如何科学有效地去解决这个问题?当然是在基类中把需要在构造函数内执行地函数设置成非 virtual
函数。
总之就是,在基类构造和析构期间调用的 virtual
函数不可下降至派生类。
10. 令 operator= 返回一个 reference to *this
令赋值(assignment)操作符(
=
)返回一个 reference to*this
。
为什么这样做呢?是因为可以实现类似于这样的程序:
a = b = c = 10;
因此,我们在编写类的 operator=
操作符时,可以写成:
class BaseClass {public: BaseClass &operator=(const BaseClass &rhs) { // 随便干点什么 return *this; // 关键在于这里 }};
当然啦,也可以不做返回,不过既然这是一个好的实践,那么没有确切的理由不去做,最好就去做。
11. 在 operator= 中处理“自我赋值”
- 确保当对象自我赋值时,
operator=
有可预估的行为。其中需要注意的包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序以及拷贝交换。- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
简而言之,就是需要考虑操作符两边是否是同一个对象,因为如果是同一个对象,会出现类似下面的问题:
class BaseClass {private: char *data;public: BaseClass &operator=(const BaseClass &rhs) { delete data; data = rhs.data; return *this; }};int main() { BaseClass baseClass; baseClass = baseClass;}
自己给自己赋值,没毛病,但是在运算符函数的 delete data
却带来了问题,因为它删除掉了自己的内存空间,却在下面那行 data = rhs.data
又想用了,而这时系统已经收回了这块空间,这样一来操作系统肯定是不干的,所以程序就报错了。
那么该如何解决呢?这样:
class BaseClass {private: char *data;public: BaseClass &operator=(const BaseClass &rhs) { // 多一个检查是否是自己的操作就可以了,也称证同测试 if (&rhs == this) return *this; delete data; data = rhs.data; return *this; }};
12. 复制对象时勿忘其每一个成分
- 拷贝函数应该确保复制了“对象内的所有成员变量”以及“所有的
base class
成员”。- 不要尝试以某个拷贝函数去实现另一个拷贝函数,应该将两者共同的部分抽取到一个新的函数中去完成,然后由两个拷贝函数共用。
一般而言,如果自己不声明拷贝构造函数和拷贝赋值操作符,那么编译器会帮自己生成的,但是!重点来了!如果选择了自己去声明定义,那么麻烦事就来了(因为即便可能出错编译器也不会告诉你)。
尤其是一个类派生自基类的时候,就需要小心谨慎地去处理基类的对象,然而有些是 private
的,因此复制起来比较麻烦,这个时候可以使用这样的方式来解决问题:
- 对于拷贝构造函数,在初始化列表中显式地去调用基类的拷贝构造函数,然后在子类的拷贝构造函数内部处理好自己的问题。
- 对于赋值拷贝操作符,在合适的位置显式调用基类的
operator=()
函数。
具体例子见下面:
class BaseClass {private: string name;public: BaseClass() = default; BaseClass(int sz, char c) { name = string(sz, c); } BaseClass(const BaseClass &rhs) : name(rhs.name) {} BaseClass &operator=(const BaseClass &rhs) { if (this == &rhs) return *this; this->name = rhs.name; return *this; } friend ostream &operator<const BaseClass &rhs) { os < return os; }};class DeriveClass : public BaseClass {private: int age;public: DeriveClass(int a, int sz, char c) : BaseClass(sz, c), age(a) {} // 必须要调用基类的拷贝构造函数,否则不会拷贝构造完全 DeriveClass(const DeriveClass &rhs) : age(rhs.age), BaseClass(rhs) {} DeriveClass &operator=(const DeriveClass &rhs) { if (&rhs == this) return *this; age = rhs.age; // 如果不调用下面这句,将会出现没有拷贝基类 name 值的问题! BaseClass::operator=(rhs); return *this; } friend ostream &operator<const DeriveClass &rhs) { os <"\t" < return os; }};int main() { DeriveClass d1(18, 3, '1'); DeriveClass d2(20, 5, '2'); DeriveClass d3(d1); d1 = d2; cout <endl <endl < /** * 正常输出: * 22222 20 * 22222 20 * 111 18 */ /** * 如果按照前两点建议,那么出现这样的情况概不负责; * 111 20 * 22222 20 * 18 */}
总而言之,一旦选择了自己去完成拷贝构造函数和复制拷贝操作符,那么就别怪编译器不厚道了,需要自己去谨慎操作。
软考之后终于可以静下心来看看书了?(开)?(心)
effective c++_【阅读笔记】Effective C++()相关推荐
- 《Effective Java》阅读笔记(二)
最近在看<Effective Java>这本书,顺便就记录一些笔记,记录一下书中的一些知识点以及对知识点的总结.一般情况会记录所有的知识点,但是知识点太过简单或者无归纳点总结的就不做详细记 ...
- 《Effective Java》阅读笔记
第2章 第1条:用静态工厂方法代替构造器 [1] 辅助理解静态工厂的文章,关于 Java 的静态工厂方法,看这一篇就够了! [2] 单例模式:Hi,我们再来聊一聊Java的单例吧 第2条:遇到多个构造 ...
- 阅读笔记-Effective Capacity Analysis of STAR-RIS-Assisted NOMA Networks
STAR-RIS:即同时折射和反射可重构智能表面(simulta-neously transmitting and reflecting reconfigurable intelligent sur- ...
- 《Effective Java》阅读笔记7 避免使用终结方法
1.序 本条的意思是,让你尽量不要在你的类中覆盖finalize方法,然后在在里面写一些释放你的类中资源的语句. 1.1为什么要避免覆盖并使用finalize方法? (1)finalize方法不能保证 ...
- 大道至简_阅读笔记02
接下来是三四五章的阅读: 给我印象最深的就是第五章所说的在项目开发的过程中,难免遇到不少的问题,甚至是失败,但这并不代表什么,可能是我们在某些环节有一点漏洞,只要我们将其打上补丁,什么问题都能解决,但 ...
- verilog设置24进制计数器_阅读笔记:《Verilog HDL入门》第3章 Verilog语言要素
3.1标识符 1.Verilog中的Identifier是由任意字母.数字.下划线和$符号组成的,第一个字符必须是字母或者下划线.区分大小写. 2.Escaped Identifier是为了解决简单标 ...
- 软件构架实践_阅读笔记01(1-3)
之前的学期,我们学习了软件工程概论和软件需求分析,而下个学期即将学习软件体系架构.如课程安排的一样,如大众的观点一致:需求在架构之前.即传统的思想:在知道了系统的需求,就可以为此系统构建构架.而紧接着 ...
- 迁移学习_迁移学习简明手册(王晋东)_阅读笔记5-6
5.迁移学习的基本方法 基于样本迁移: 根据一定的权重生成规则,增加源域中跟目标域样本相似度高的样本的权重. 增加狗类别样本的权重 基于特征迁移: 通常假设源域和目标域间有一些交叉的特征,通过特征变换 ...
- 《这些道理没有人告诉过你》_阅读笔记
1/选择决定结果 在某种程度上,择校决定着一个孩子是否能学有所成,择业决定着一个人的事业是否 成功,择偶决定着一个人的生活是否美满.而学有所成.事业成功.生活美满,恰恰是 芸芸众生所追求的三大人生目标 ...
- 迁移学习_迁移学习简明手册(王晋东)_阅读笔记7-8
7.第二类方法:特征选择 特征选择法的基本假设是: 源域和目标域中均含有一部分公共的特征,在这部分公共的特征上,源领域和目标领域的数据分布是一致的.因此,此类方法的目标就是,通过机器学习方法,选择出这 ...
最新文章
- 一文详解为什么Serverless比其他软件开发方法更具优势
- 如何在SharePoint Server中整合其他应用系统?
- 成功解决Please use the NLTK Downloader to obtain the resource:
- Mybatis学习--Mapper.xml映射文件
- 【Redis】4.Redis数据存储listsetsorted_set
- 信息学奥赛一本通 1025:保留12位小数的浮点数 | OpenJudge NOI 1.1 05
- python--split方法
- python去除列表中的重复元素,简单易理解,超详细解答,步骤分析
- tmb100 刷linux,天猫魔盒TMB100C短接刷机教程
- python写词法分析器_python实现词法分析器
- 软件测试的测试代码,软件测试(示例代码)
- 数论 —— 逆元与同余式定理
- 岛屿数量问题(C实现)
- 互联网医疗的千姿百态:火热、亏损、巨头亲赖
- 初始3D打印机(Hori 3D Z600)
- 【CTF WriteUp】2023数字中国创新大赛网络数据安全赛道决赛WP(1)
- 阿里云函数计算(fc)使用体验
- 最新ThinkPHP微信独立精彩互换抢红包系统源码开源版
- 海康NCG联网网关设备通过国标接入到EasyCVR视频图像智能分析平台注册失败问题排查
- google检索技巧-从菜鸟到黑客
热门文章
- iphone屏幕录制_无需第三方APP,苹果iPhone手机屏幕录制的方法
- lightroom 闪退_UP加速器闪退怎么办 UP加速器闪退解决方法
- MATLAB入门级知识
- 牛式 Prime Cryptarithm
- 谁拿了最多奖学金pascal程序
- AtCoder AGC024F Simple Subsequence Problem (字符串、DP)
- HDU 6741 MUV LUV UNLIMITED (博弈论)
- python文本关键词匹配_NLP利剑篇之模式匹配
- 关于Rabbitmq的routingkey的作用
- 【Unity3D与23种设计模式】模板方法模式(Template Method)