您的位置:首页 > 编程语言 > C语言/C++

C++学习(三)——Effective C++

2016-06-10 07:30 267 查看

1.让自己习惯C++

01.视C++为一个语言联邦

C++高效编程守则视状况而变化,取决于你使用C++的哪一部分

02.尽量以const、enum、inline替换#define

对于单纯常量,最好以const对象或enum替换#define

对于形似函数的宏,最好改用inline函数替换#define

03.尽可能使用const

将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体

编译器强制实施bitwise constness,但你编写程序时应该使用conceptual constness

当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

04.确定对象被使用前已先被初始化

为内置型对象进行手工初始化,因为C++不保证初始化它们

构造函数最好使用member initialization list,而不要在构造函数本体内使用赋值操作。member initialization list中列出的成员变量,其排列次序应该和它们在class中的声明次序相同

为免除跨编译单元的初始化次序问题,请以local static对象替换non-local static对象

2.构造/析构/赋值运算

05.了解C++默默编写并调用哪些函数

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符、析构函数

06.若不想使用编译器自动生成的函数,就该明确拒绝

为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像UNcopyable这样的base class也是一种做法

07.为多态基类声明virtual析构函数

polymorphic base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数

Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不该声明virtual析构函数

08.别让异常逃离析构函数

析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序

如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作

09.绝不在构造和析构过程中调用virtual函数

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class

10.令operator=返回一个reference to *this

令赋值操作符返回一个reference to *this

11.在operator=中处理自我赋值

确保当对象自我赋值时,operator=有良好行为。其中技术包括证同测试、精心周到的语句、copy-and-swap

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

12.复制对象时勿忘其每一个成分

copying函数应该确保复制对象内的所有成员变量及所有base class成分

不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用

3.资源管理

13.以对象管理资源

为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源

两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向NULL

14.在资源管理类中小心copying行为

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为

普遍而常见的RAII class copying行为是:抑制copying、施行引用计数。不过其他行为也都可能被实现

15.在资源管理类中提供对原始资源的访问

APIs往往要求访问原始资源,所以每一个RAII class应该提供一个取得其所管理资源的办法

对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便

16.成对使用new和delete时要采取相同形式

如果你在new表达式中使用[],必须在相应的delete表达式中也使用[];如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]

17.以独立语句将newed对象置入智能指针

以独立语句将newed对象存储于智能指针之内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露

4.设计与声明

18.让接口容易被正确使用,不易被误用

好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质

促进正确性的办法包括接口的一致性,以及与内置类型的行为兼容

阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值、消除客户的资源管理责任

tr1::shared_ptr支持定制型删除器。这可以防范DLL问题,可被用来自动解除互斥锁

19.设计class犹如设计type

class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑或本条款覆盖的所有讨论主题

20.宁以pass-by-reference-to-const替换pass-by-value

尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题

以上规则并不适用与内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当

21.必须返回对象时,别妄想返回其reference

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为在单线程环境中合理返回reference指向一个local static对象提供了一份设计实例

22.将成员变量声明为private

切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性

protected并不比public更具封装性

23.宁以non-member、non-friend替换member函数

宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性

24.若所有参数皆需类型转换,请为此采用non-member函数

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

25.考虑写出一个不抛异常的swap函数——???

5.实现

26.尽可能延后变量定义式的出现时间

尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率

27.尽量少做转型动作

如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计

如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码中

宁可使用C++的转型运算符,也不要使用旧式转型。前者很容易辨识出来,而且也有分门别类的职责

28.避免返回handles指向对象内部成分

避免返回handles指向对象内部。遵守这个条款可增加封装性,帮助const成员的行为像个const,并将发生dangling handles的可能性降到最低

29.为异常安全而努力是值得的

异常安全函数即使发生异常也不会泄露资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型

强烈保证往往能够以copy and swap实现出来,但强烈保证并非对所有函数都可实现或具备现实意义

函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者

30.透彻了解inlining的里里外外

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化

不要只因为function templates出现在头文件,就将它们声明为inline

31.将文件间的编译依存关系降至最低

支持编译依存性最小化的一般构想是:相依与声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和interface classes

程序头文件应该以完全且仅有声明式的形式存在。这种做法不论是否涉及templates都适用

6.继承与面向对象设计

32.确定你的public继承塑模出is-a关系

public继承,意味着is-a。适用于base-class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象

33.避免遮掩继承而来的名称

derived class内的名称会遮掩base class内的名称。在public继承下从来没有人希望如此

为了让遮掩的名称再见天日,可使用using声明或forwarding functions

34.区分接口继承和实现继承

接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口

pure virtual函数只指定接口继承

impure virtual函数指定接口继承及缺省实现继承

non-virtual函数指定接口继承以及强制性实现继承

35.考虑virtual函数以外的其他选择

virtual函数的替代方案包括NVI方法及Strategy模式的多种形式。NVI本身是一个特殊形式的TemplateMethod模式

将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员

tr1::function对象的行为就像一般函数指针。这样的对象可接纳与给定的target signature兼容的所有查了callable entities

36.绝不重新定义继承而来的non-virtual函数

37.绝不重新定义继承而来的缺省参数值

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数是动态绑定

38.通过复合塑模出has-a或根据某物实现出has-a

composition的意义和public继承完全不同

在应用域,复合意味着has-a;在实现域,复合意味着is-implemented-in-terms-of

39.明智而审慎地使用private继承

private继承意味is-implemented-in-terms-of。它通常比composition的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的虚函数时,这么设计是合理的

和composition不同,private继承可以造成empty base最优化。这对致力于对象尺寸最小化的程序库开发者而言,可能很重要

40.明智而审慎地使用多重继承

多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要

virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况

多重继承的确有正当用途。其中一个情节涉及public继承某个interface class和private继承某个协助实现的class的两相结合

7.模板与泛型编程

41.了解隐式接口和编译器多态

42.了解typename的双重意义

43.学习处理模板化基类内的名称

44.将于参数无关的代码抽离templates

45.运用成员函数模板接受所有兼容类型

46.需要类型转换时请为模板定义非成员函数

47.请使用traits classes表现类型信息

48.认识template元编程

8.定制new和delete

49.了解new-delete的行为

set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用

Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常

50.了解new和delete的合理替换时机

有许多理由需要写个自动的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息

51.编写new和delete时需固守常规

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 byte申请。class专属版本则还应该处理比正确大小更大的(错误)申请

operator delete应该在收到NULL指针时不做任何事情。class专属版本则还应该处理比正确大小更大的(错误)申请

52.写了placement new也要写placement delete

当你写一个placement operator new,请确定也出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄露

当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本

9.杂项讨论

53.不要轻忽编译器的警告

严肃对待编译器发出的警告信息。努力在你的编译器的最高警告级别下争取无任何警告的荣誉

不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同,一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失

54.让自己熟悉包括TR1在内的标准程序库

C++标准库的主要机能由STL、iostreams、locales组成。并包含C99标准库

TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式以及另外的10个组件的支持

TR1自身只是一份规范。为获得TR1提供的好处,你需要一份实物。一个好的实物来源是boost

55.让自己熟悉boots

boost是一个社群,也是一个网站。致力于免费源码开放、同行评审的C++程序库开发。boost在C++标准化过程中扮演深具影响力的角色

- boost提供许多TR1组件实现品,以及其他许多程序库

01.视C++为一个语言联邦

C++支持的编程范式:

面向过程(procedural-oriented)

面向对象(object-oriented)

基于对象(object-based)

函数形式(functional)

泛型(generic)

元编程(metaprogramming)

将C++看成包含四种自语言的复合语言

C

C++

Templates

STL

02.尽量以const、enum、inline替换#define

使用const string 代替 const char * const

只有整数类型(ints,chars,bools)在类中被定义为static const时,可以在定义时赋值,double会有告警(Apple LLVM 7.3.0 clang-703.0.31),自定义类型不可以

整数类型的类static const也可以在外部定义;外部定义后编译器会为其分配地址

宏会产生副作用

#define MAX(a,b) ((a) > (b) ? (a) : (b))
int a = 5, b = 0
MAX(++a, b)     //a被累加二次
MAX(++a, b+10)  //a被累加一次


预处理器依然尤其作用,例如#include #ifdef #ifndef # ##

03.尽可能使用const

迭代器的作用,像一个T*指针

const std::vector<int>::iterator iter  ->  T* const
std::vector<int>::const_iterator iter iter  -> const T*


const成员函数的意义:

使class接口容易被理解。得知哪个函数可以改动对象的内容而哪些函数不行,非常重要

使操作const对象成为可能。非const成员函数,无法操作const对象

如果两个成员函数只是常量性不同,是可以被重载的

const char& operator [] ( std:size_t position ) const
char & operator [] ( std::size_t position )


如果函数的返回类型是一个内置类型,那么改动函数的返回值从来就不合法。如果想让返回值成为左值,需要返回引用

即使类为const,类的const 成员函数如果返回了一个指向对象内部值的引用,类的值依然可能被更改

class T
{
//[]返回p的引用,但p中的值仍可能被更改
char& operator [] ( std:size_t position ) const { return p[position] }
private:
char *p;
}


在const和non-const成员函数中避免重复

让non-const版本调用const版本

调用const版本时,将常量性移除(使用const_cast)

如果只是单纯的调用,会递归调用自己,必须把*this转换为const(使用static_const)

const char& operator [] ( std:size_t position ) const
{
...
...
...
return p[position]
}

char & operator [] ( std::size_t position )
{
const_cast<char &>(static_cast<const T&>(*this)[position]);
}


04.确定对象被使用前已先被初始化

赋值和初始化

对象的成员变量的初始化动作,发生在进入构造函数本体之前,内置类型除外

使用初始化列表,通常效率较高

class A
{
public:
A(string &a, string &b)
{
_a = a;   //赋值而非初始化
_b = b;   //调用了a和b的default constructor,再调用copy assignment
}
private:
string _a;
string _b;
}


class A
{
public:
A(string &a, string &b):_a(a),_b(b){} //只调用了a和b的copy constructor
private:
string _a;
string _b;
}


初始化列表

总是使用初始化列表,因为有些成员变量(const或reference)必须要在此初始化

总是在初值中列出所有成员变量,以免还得记住哪些成员变量无需赋初值

初始化列表中,最好总是以其声明次序为次序,防止依赖性

如果有多个constructor,为避免重复的初始化列表,可以将赋值效率高的成员变量,放到一个函数中初始化

定义与不同编译单元内的non-local static对象的初始化次序,并无明确定义

将每个non-local static对象搬到自己的专属函数内

任何一种non-const static对象,不论它是local还是non-local,都不是线程安全的。在程序单线程启动阶段,手动调用所有专属函数,保证初始化顺序

class FileSystem
{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;

class Directory
{
public:
Directory(para);
};
Directory::Directory(para)
{
std::size_t disks = tfs.numDisks();   //如果此时tfs没有被初始化,则会有麻烦
}


class FileSystem
{
public:
std::size_t numDisks() const;
};

FileSystem& tfs()
{
static FileSystem fs;
return fs;
}

class Directory
{
public:
Directory(para);
};

Directory::Directory(para)
{
std::size_t disks = tfs().numDisks();   //如果此时tfs没有被初始化,则会初始化
}

Directory& tempDir()
{
static Directory td;
return td;
}


05.了解C++默默编写并调用哪些函数

编译器自动生成的函数

default constructor

copy constructor

copy assignment operator

destructor

有时编译器会拒绝生成copy constructor或copy assignment operator,并报错

如果class内包含reference、const成员变量,因为他们都不允许被重新赋值

某个base class将copy constructor或copy assignment operator,编译器无法调用

06.若不想使用编译器自动生成的函数,就该明确拒绝

将copy constructor和copy assignment operator声明为private

只是声明为private也不安全,因为friend和member函数一样可能会调用它们

声明而不定义,这样谁都无法调用

当friend或member函数调用时,会产生链接错误

继承一个专门为阻止copy动作而设计的base class

此技术可能会导致多重继承,因为往往还需要继承其他的base class

可以使用boost中的noncopyable

在编译时报错

class Uncopyable
{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};

class XXX:private(public) Uncopyable
{
...
};


07.为多态基类声明virtual析构函数

不应继承string、所有STL容器等类,因为这些类没有virtual析构函数,可能会造成资源泄露

08.别让异常逃离析构函数

class DBConn
{
void close()               //给客户提供一个调用close的函数,以便处理异常
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed)           //如果客户没有调用close,在析构函数也会close
{
try
{
db.close();
}
catch(...)        //在析构函数里捕捉任何异常
{
do something
}
}
}
}


09.绝不在构造和析构过程中调用virtual函数

编译器会先调用基类的构造函数,再调用derived class的构造函数。调用基类的构造函数时,derived class还未初始化,其virtual函数如果访问了成员变量则会出错

析构的方向与构造相反,当调用到基类的析构函数时,derived class的析构函数已经被调用过了,derived class中的数据已经被清除了,所以也无法调用其virtual函数

如果想在构造函数中,调用derived class的某些东西,可以令derived class将必要的构造信息向上传递至base class构造函数

10.令operator=返回一个reference to *this

如果按by-value返回的话,不能作为左值,无法实现连锁赋值

所有像+=、-=这样的赋值运算符重载,都应该照此实现

11.在operator=中处理自我赋值

自我赋值的场景

a[i] = a[j];    //i与j相等
*p = *q;        //p与q指向同一个对象


自我赋值的危害

class A{...};
class B
{
public:
B& operator = (const B& rhs);
private:
A *p;
}

B& B::operator=(const B& rhs)
{
delete p;           //如果rhs==this,此时rhs和this中的p,都被delete了
p = new A(*rhs.p);  //此时rhs.p已经失效,但此处仍然用其构造一个新对象
return *this;
}


解决方案

证同测试

B& B::operator=(const B& rhs)

{

if( this == &rhs ) return *this;

delete p;

p = new A(*rhs.p);

return *this;

}


精心构造的语句

B& B::operator=(const B& rhs)

{

A *pOrig = p;         //记住原先的p

p = new A(*rhs.p);

delete pOrig;         //删除原先的p

return *this;

}


copy-and-swap

B& B::operator=(const B& rhs)

{

A temp(rhs);   //将rhs制作一份副本

swap(temp);    //交换

return *this;

}


12.复制对象时勿忘其每一个成分

令copy assignment调用copy constructor

copy assignment是对已经存在的对象赋值,copy constructor是构造新对象

在copy assignment中调用copy constructor,相当于构造一个已经存在的对象,当然不可以

令copy constructor调用copy assignment

用一个还未初始化好的对象给另一个对象赋值,毫无意义

13.以对象管理资源

auto_ptr

若通过copy构造函数和copy assignment操作符赋值auto_ptr,它会变成NULL

受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它

STL容器不能使用auto_ptr

tr1::shared_ptr

RCSP:reference-counting smart pointer

无法打破环状引用(两个失效的对象彼此互指)

允许指定删除器

可被用于STL容器

auto_ptr和tr1::shared_ptr在其析构函数内调用delete而不是delete[]

14.在资源管理类中小心copying行为

复制方案

禁止复制

引用计数(tr1::shared_ptr)

复制资源

转移底部资源的拥有权(auto_ptr)

15.在资源管理类中提供对原始资源的访问

auto_ptr和tr1::shared_ptr中对原始资源的访问

get()

重载operator->和operator*

16.成对使用new和delete时要采取相同形式

17.以独立语句将newed对象置入智能指针

int f1();
void f2(std::tr1::shared_ptr<A>(new A), f1());


上面的代码,f2的参数包含三个表达式:

new A

std::tr1::shared_ptr()

f1()

new A一定发生在std::tr1::shared_ptr()之前

f1()调用的顺序不一定。如果按照下面的顺序调用

new A

f1()

std::tr1::shared_ptr()

如果new A之后,f1()中发生异常,则会发生资源泄露

/* 用独立语句把newed对象放入智能指针内 */
std::tr1::shared_ptr<A>p(new A)
f2(p, f1())


18.让接口容易被正确使用,不易被误用

19.设计class犹如设计type

当设计一个class时,下面的问题必须要考虑周全

新type的对象应该如何被创建和销毁?

影响到构造、析构、内存的分配与释放

对象初始化和对象的赋值该有什么样的区别?

影响到构造和赋值运算符的重载

新type的对象如果被pass-by-value,意味着什么?

拷贝构造函数

什么是新type的合法值?

新type需要配合某个继承图系?

如果继承自基类,则需要收到基类的设计约束

如果允许其他类继承本类,则需要声明virtual析构函数

新type需要什么样的转换?

构造函数需不需要用explicit修饰?

需不需要定义类型转换函数?

什么样的操作符或函数对新type而言是合理的?

class有哪些member函数?有哪些non-member函数?

什么样的标准函数应该驳回?

构造、析构、拷贝构造、赋值运算符重载等标准接口,是否需要用private隐藏?

谁该取用新type的成员?

什么是新type的“未声明接口”?

新type有多么一般化?

定义成类还是类模板?

真的需要一个新type么?

是否可以派生自某个类,并添加一些机能?

是否可以定义一个或多个non-member函数或templates?

20.宁以pass-by-reference-to-const替换pass-by-value

21.必须返回对象时,别妄想返回其reference

22.将成员变量声明为private

23.宁以non-member、non-friend替换member函数

将相关的non-member函数,声明在单独的头文件内,并且跟class处于同一个namespace

//webbrowser.h
namespace WebBrowserStuff
{
class WebBrowser {...};
}

//webbrowserbookmarks.h
namespace WebBrowserStuff
{
... //一些相关函数的定义
}

//webbrowsercookies.h
namespace WebBrowserStuff
{
... //一些相关函数的定义
}


24.若所有参数皆需类型转换,请为此采用non-member函数

只有当参数被列于参数列表内时,这个参数才能参与隐式类型转换

member函数的反面是non-member函数,而不是friend函数

任何时候,如果可以避免friend函数就该避免

25.考虑写出一个不抛异常的swap函数——???

26.尽可能延后变量定义式的出现时间

//A:定义于循环外,1个构造,1个析构,n个赋值
Widget w;
for(int i=0; i<n; i++)
{
w = ...
}

//B:定义与循环内,n个构造,n个析构
for(int i=0; i<n; i++)
{
Widget w(...);
}


除非赋值成本比构造+析构低,或代码对效率高度敏感,否则应该用做法B,因为A的变量作用域更大

27.尽量少做转型动作

旧式转型(将expression转为T)

(T) expression

T(expression)

C++的转型运算符

const_cast

移除常量性

dynamic_cast

安全向下类型转换

reinterpret_cast

执行低级转型

非常不安全

实际实现取决于编译器,不可移植

static_cast

强迫隐式转换

int–>double

non-const–>const

void*指针–>typed指针

关于dynamic_cast

执行速度相当慢。一种实现是,基于class名称的字符串比较。如果是深度继承或多重继承,则要经过n个strcmp调用

绝对避免连串的dynamic_cast,速度慢,可扩展性差

//1.连续调用dynamic_cast,速度慢
//2.如果新增加派生类,需要增加代码
if(...)
derived class 1* p = dynamic_cast<derived class 1*>(iter->get())
else if(...)
derived class 2* p = dynamic_cast<derived class 2*>(iter->get())
else if(...)
derived class 3* p = dynamic_cast<derived class 3*>(iter->get())


避免调用dynamic_cast

使用容器并在其中存储直接指向derived class对象的指针

如果要处理多中类型,需要多个容器

在base class内提供virtual函数,供基类指针调用

28.避免返回handles指向对象内部成分

成员变量的封装性最多只等于返回其reference的函数的访问级别

如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据

dangling

悬挂指针或悬挂引用

29.为异常安全而努力是值得的

什么是异常安全?当异常被抛出时:

不泄露任何资源

不允许数据破坏

异常安全的级别

基本承诺

如果异常被抛出,程序内的任何事物仍然保持在有效状态下

将所有资源都用对象管理

强烈保证

如果异常被抛出,程序状态不变

copy and swap——如果之前的代码将iostream流的读取记号改变,那么用此方法可以恢复

不抛掷保证

承诺绝不抛出异常

任何使用动态内存的东西,包括所有STL容器,如果分配内存失败,会抛出bad_alloc异常

如果系统内有一个函数不具备异常安全性,整个系统就不具备异常安全性

30.透彻了解inlining的里里外外

inline只是对编译器的申请,编译器可以视情况选择实现还是忽略

大多数编译器,如果无法实现inline,会给出一个告警

如果要取一个inline函数的地址,则会生成此函数的本体

编译器通常不会对通过函数指针而进行的调用实施inlining

许多调试环境中,禁止inlining

31.将文件间的编译依存关系降至最低

handle class

在头文件声明class,class具体的方法通过另一个类的指针实现

interface class

定义抽象类,用户用抽象类的指针或引用编程

抽象类的派生类负责实现

32.确定你的public继承塑模出is-a关系

鸟与企鹅

鸟会飞

企鹅是鸟

企鹅不会飞

如果想让企鹅继承鸟,要么取消鸟的“飞”方法,要么特殊实现企鹅的飞方法,否则企鹅无法继承鸟

矩形与正方形

矩形有单独改变长、宽的方法

正方形是矩形的一种

不可能单独改变正方形的长或宽

33.避免遮掩继承而来的名称

using声明

class base
{
virtual void f1();
virtual void f1(int);
}
class derived : public base
{
using base::f1;
virtual void f1();
}

class derived d;
d.f1();       //derived::f1
d.f1(int i);  //base::f1


forwarding function

class base
{
virtual void f1();
virtual void f1(int);
}
class derived : public base
{
virtual void f1()
{
base::f1();
}
}


34.区分接口继承和实现继承

切断纯虚函数的接口与实现,即可以在派生类中保留缺省的实现,又能保证派生类一定有自己的实现

class Base
{
public:
virtual void func() = 0;
};
//在基类中实现纯虚函数,可以作为该方法的缺省实现
void Base::func()
{
TRACE();
}

class Derived : public Base
{
public:
virtual void func();
};
//派生类必须实现func方法,否则无法编译通过
//派生类可以调用基类的缺省实现
void Derived::func()
{
...
Base::func();
...
}


35.考虑virtual函数以外的其他选择

Non Virtual Interface (Template Method 模式)

将通用的、琐碎的代码放在基类中,将可替换的部分放在派生类实现

class A
{
public:
void func()
{
...
func2();
...
}
private:
virtual void func2()
{
...
}
}


古典Strategy模式

将算法当做对象,随时替换

Strategy模式(函数指针)

用函数指针代替算法

同一个类型的不同对象,可以有不同的算法

可以在运行时变更

非成员函数无法访问non-public成员,会破坏封装性

tr1::function Strategy模式——???

36.绝不重新定义继承而来的non-virtual函数

37.绝不重新定义继承而来的缺省参数值

默认参数静态绑定,是为了运行时效率

虚函数有默认参数,可扩展性不强,如果基类要改变默认参数,所有派生类的代码都要改

38.通过复合塑模出has-a或根据某物实现出has-a

39.明智而审慎地使用private继承

private继承

编译器不会进行向上类型转换

基类的所有protected和public成员,在派生类中都变为private属性

private继承,意味着只有继承实现部分,不继承接口部分

private继承在设计层面上没有意义,只在实现层面上有意义

只在必要时使用private继承

使用基类的protected成员

改写基类的virtual函数

EBO empty base optimization

大多数编译器sizeof空类,值为1

//Empty不占用内存空间,不会影响内存对齐

class A : private Empty

{

int x;

}


无论何时,只要可以,尽量选择复合

class A
{
class ATimer : public Timer
{
virtual void onTick() const;
};
ATimer timer;
}


40.明智而审慎地使用多重继承

多重继承 multiple inheritance

单一继承 single inheritance

钻石型多重继承——base class的数据,是一份还是多份?

C++支持两种方案

第一种方案,也是缺省方案,是执行复制

第二种方案,是virtual base class,只有一份

virtual base class

需要在空间和速度上付出代价

非必要不要使用virtual bases

尽可能避免在virtual bases中放置数据

41.了解隐式接口和编译器多态

42.了解typename的双重意义

43.学习处理模板化基类内的名称

44.将于参数无关的代码抽离templates

45.运用成员函数模板接受所有兼容类型

46.需要类型转换时请为模板定义非成员函数

47.请使用traits classes表现类型信息

48.认识template元编程

49.了解new-delete的行为

一个设计良好的new-handler函数必须做以下事情

让更多内存可被调用

安装另一个new-handler

卸除new-handler

抛出bad_alloc

不返回,通常调用abort()或exit()

class NewHandlerHolder
{
public:
explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
~NewHandlerHolder(){ std::set_new_handler(handler) }
private:
std::new_handler hander;
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};

template<typename T>
class NewHandlerSupport
{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void *operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};

template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}

template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

class Widget : public NewHandlerSupport<Widget>
{
...
}


Nothrow new

只是new的时候不抛异常,构造函数还是可能会抛异常

所以其实此功能很鸡肋,没有使用的必要

Widget *p = new(std::nothrow) Widget;


50.了解new和delete的合理替换时机

重载new和delete的理由

用来检测运用上的错误

为了收集统计数据

为了增加分配和归还的速度

为了降低缺省内存管理器带来的空间额外开销

为了弥补缺省分配器中的非最佳内存对齐

为了将相关对象成簇集中

为了获得非传统的行为

51.编写new和delete时需固守常规

C++要求所有new返回的指针,必须内存对齐

new-handler必须继续分配内存,否则new内的循环永远不会结束

如果一个类重载了new和delete,但不想被其派生类继承,在实现时要判断大小

if(size != sizeof(Base))
return ::operator new(size);   //只在基类使用,派生类用系统的new


C++保证删除空指针永远安全

关于new[ ] ,唯一要做的事情就是分配一块未加工的内存,因为不知道是否是派生类调用的,所以每个元素的大小不确定,元素个数也不确定

base class的析构函数要设定为虚函数,否则可能内存泄露

52.写了placement new也要写placement delete

Widget *p = new Widget;


上面的代码,有两个函数被调用,一个是new,一个是Widget的构造函数

如果new成功,但构造函数抛出异常,此时程序员是无法归还内存的,所以此时系统会调用delete

系统会调用参数个数和类型跟new系统的某个delete版本

只有系统自动调用时,才使用上述规则。手动对一个指针调用delete,只会调用正常的delete

如果定义了placement new,则必须同时提供一个正常的delete,和一个placement delete

缺省情况下C++提供以下形式的new,如果声明了placement new,则会覆盖下列所有形式。所以定义placement new时,必须保证缺省的版本好用

void *operator new(std::size_t) throw(std::bad_alloc);
void *operator new(std::size_t, void*) throw();
void *operator new(std::size_t, const std::nothrow_t &) throw();


53.不要轻忽编译器的警告

54.让自己熟悉包括TR1在内的标准程序库

55.让自己熟悉boots

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: