Effective C++学习笔记
Effective C++学习笔记
诡隐沦1.视c++为一个语言联邦
C
面向对象的C++
Template C++ 泛型编程
STL
2.尽量以const,enum,inline替换 #define
宁可以编译器换预处理器
1.#define错误
#define ASPECT_RATIO 1.111
错误:你所使用的名称可能并未进入记号表
解决:const double AspectRatio = 1.111//大写通常用于宏
2.特殊情况
1.定义常量指针( constant pointers)。
由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。
const char* const authorName = "scott Meyers"; |
2.class专属常量
将常量的作用域限制于class内
class GamePlayer{ |
2.实现宏错误
|
3.小结
有了consts、enums和 inlines,我们对预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但你应该明确地给予它更长更频繁的假期。
请记住:
对于单纯常量,最好以const对象或enums替换#defines。
对于形似函数的宏(macros),最好改用inline函数替换#defines。
3.尽可能使用const
1.const规则
1.指针
const语法虽然变化多端,但并不莫测高深。如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
2.const修饰函数参数
表示参数不可变,若参数为引用,可以增加效率(引用传递而不用值拷贝)
3.const 修饰函数返回值
避免返回值被修改
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
例如函数
const char * GetString(void); |
4.const修饰类的成员变量(class)
表示成员变量不能被修改,同时只能在初始化列表中赋值
5.const修饰类的成员函数(class)
不能修饰全局函数,因为全局函数没有this指针
该函数不能修改成员变量,方法:在变量前加 mutable ,可以更改
不能调用非const成员函数,因为任何非const成员函数会有修改成员变量的企图
6.const修饰类对象(class)
对象的任何成员都不能被修改
const类对象只能调用const成员函数
7.类中的所有函数都可以声明为const函数吗。哪些函数不能?(class)
构造函数不能
因为const修饰的成员函数不能修改成员变量。但是构造函数恰恰需要修改类的成员变量
static静态成员函数不能
static静态成员是属于类的,而不属于某个具体的对象,所有的对象共用static成员。this指针是某个具体对象的地址,因此static成员函数没有this指针。而函数中的const其实就是用来修饰this指针的,表示this指向的内容不可变,static静态成员却没有this指针,所以const不能用来修饰static成员函数
2.STL迭代器
iterator和const_iterator
std: : vector<int> vec;... |
3.const成员函数
1.它们使 class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。
2.它们使“操作const对象”成为可能。这对编写高效代码是个关键,因为如条款20所言,改善C+程序效率的一个根本办法是以pass byreference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。
4.小结
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”( conceptual constness)。
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。(P55)
4.确定对象被使用前已确定先被初始化
1,构造函数初始化
ABEntry: :ABEntry (const std::string& name,const std::string& address, |
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。
在 ABEntry构造函数内,theName,theAddress和 thePhones 都不是被初始化,而是被赋值。
初始化的发生时间更早,发生于这些成员的default 构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。
但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作:
ABEntry : :ABEntry(const std::string& name,const std::string& address, |
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本(本例第一版本)首先调用default构造函数为theName, theAddress和thePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列( member initialization list)的做法避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的theName以nane为初值进行copy构造,theAddress 以 address为初值进行copy构造,thePhones 以 phones为初值进行copy构造。
2.小结
- 为内置型对象进行手工初始化,因为C+t不保证初始化它们。
- 构造函数最好使用成员初值列( member initialization list),而不要在构造函数本体内使用赋值操作( assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-localstatic对象。
5.了解C++默默编写并调用了哪些函数
class Empty { }; |
6.若不想使用编译器自动生成的函数,就应该明确拒绝
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。
private: Uncopyable (const Uncopyable&) ; //但阻止 copying Uncopyable& operator=(const Uncopyable&) ;
7.为多态基类声明virtual析构函数
1.情况
这是一个引来灾难的秘诀,因为CH+明白指出,当derived class对象经由一个baseclass指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived 成分没被销毁。如果getTimeKeeper返回指针指向一个AtomicClock 对象,其内的 AtomicClock 成分(也就是声明于Atomicclock class内的成员变量〉很可能没被销毁,而atomicClock的析构函数也未能执行起来。然而其 base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。这可是形成资源泄漏、败坏之数据结构、在调试器上浪费许多时间的绝佳途径喔。
2.方法
消除这个问题的做法很简单:给base class一个virtual析构函数.此后删除derivedclass对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class成分
3.建议
有不同的实现码。任何 class只要带有 virtual函数都几乎确定应该也有一个virtual析构函数。
如果class不含virtual函数,通常表示它并不意图被用做一个base class。
当class不企图被当作base class,令其析构函数为virtual往往是个馒主意。
4.抽象类
class AwOV { |
这个class有一个pure virtual函数,所以它是个抽象class,又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。
然而这里有个窍门:你必须为这个pure virtual析构函数提供一份定义: AwOV : : ~AWOV() { }
5.小结
polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性( polymorphically),就不该声明virtual析构函数。
8.别让异常逃离析构函数
小结
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9.绝不在构造和析构过程中调用virtial函数
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层))。
|
10.令operator = 返回一个reference to *this
int x, y, z; x = y = z = 3; class widget { public: widget& operator+= (const widget& rhs) { … return *this; }
11.在**operator = 中处理“自我赋值”**
widget& widget : :operator= (const widget& rhs){ |
小结
确保当对象自我赋值时operator=有良好行为。
其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12.复制对象时勿忘其每一个成分
任何时候只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是 private(见条款22),所以你无法直接访问它们,你应该让 derived class 的 copying函数调用相应的base class函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer (rhs) , |
- 复制所有local 成员变量
- 调用所有base classes 内的适当的copying函数。
13.以对象管理资源
1.情况
Investment* createInvestment(); |
…中可能return,continue,抛出异常,使得delete语句不执行
2.解决
标准程序库提供的auto_ptr 正是针对这种形势而设计的特制产品。
auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。
void f( ){ |
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。
如果真是那样,对象会被删除一次以上,而那会使你的程序搭上驶向“未定义行为”的快速列车上。
为了预防这个问题,auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!
trl: :shared ptr和 auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
14.在资源管理类中小心coping行为
Coping函数(包括copy构造函数和 copy assignmen 操作符)有可能被编译器自动创建出来,因此除非编译器所生版本做了你想要做的事(条款5提过其缺省行为),否则你得自己编写它们。
小结:
复制RAII 对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
普遍而常见的RAI class copying 行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。
15.在资源管理类中提供对原始资源的访问
资源管理类(resource-managing classes)很棒。它们是你对抗资源泄漏的堡垒。排除此等泄漏是良好设计系统的根本性质。在一个完美世界中你将倚赖这样的classes来处理和资源之间的所有互动,而不是玷污双手直接处理原始资源( rawresources)。但这个世界并不完美。许多APIs直接指涉资源,所以除非你发誓(这其实是一种少有实际价值的举动)永不录用这样的APIs,否则只得绕过资源管理对象(resource-managing objects)直接访问原始资源( raw resources)。
APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供-个“取得其所管理之资源”的办法。
对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
16.成对使用new和delete时要采取相同形式
规则:如果你调用new时使用[ ],你必须在对应调用delete时也使用[ ]。如果你调用new时没有使用[ ],那么也不该在对应调用delete时使用[ ]。
std::string*stringPtr1 = new std::string; |
17.以独立语句将newed对象置入智能指针
待续—————————