Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

16.80-20准则

Posted on 2018-04-26 | In More Effective C++

80-20准则说的是大约20%的代码使用了80%的程序资源;大约20%的代码耗用了大约80%的运行时间;大约20%的代码使用了 80%的内存;大约20%的代码执行80%的磁盘访问;80%的维护投入于大约20%的代码上;通过无数台机器、操作系统和应用程序上的实验,这条准则已经被再三地验证过。

80-20准则不只是一条好记的惯用语,它更是一条有关系统性能的指导方针,有着广泛的适用性和坚实的实验基础。不要在具体数字上纠缠不清,不管准确地数字是多少,基本的观点是一样的:软件整体的性能取决于代码组成中的一小部分。

profiler有助于洞察性能,虽然并非每一次都能被你发现,为了保证开发者能够明晰低效的症结位于何处,最好的方法是用尽可能多的数据profile应用程序。

15.了解异常处理的成本

Posted on 2018-04-26 | In More Effective C++

为了处理异常,程序必须做到:

  • 无论执行至何处,都应该明确此时如果抛出异常将释放哪个对象
  • 知晓每一个入口点,以便从try块退出
  • 对于每一个try,继续跟踪catch子句以及其能够捕获的异常类型

如果你确保你的程序根本不使用异常,链接的程序库里也没有异常,那你可以使用不支持异常处理的方法进行编译,从而缩小程序尺寸和提高速度。

一般来说,每一个try块都降低了速度并且增大代码尺寸,因此应该避免使用无用的try块。同样的,异常明细的成本和try块相差不大。

总的来说,真正处理异常的开销仅仅发生于异常出现,(异常本身就是难于出现的)因此我们无需在效率上顾虑太多,遵循之前所提及的各项要求即可。(More Effective C++ 9~14)

14.审慎使用异常明细

Posted on 2018-04-26 | In More Effective C++

(本节内容在Effective C++ 30中亦有涉及)

前言

 
异常明细明确描述了一个函数可能抛出的异常,如果说函数抛出了一个不能存在于异常明细范围里的异常,系统将在运行时检测到该错误,并且自动地调用特殊函数unexpected。
unexpected默认机制是调用terminate,而terminate缺省行为是调用abort.也就是说,一旦违反了异常明细,程序将立刻终止,所有资源(甚至包括局部变量)都未得到释放。


问题实例

 
前言中描述的情况其实极易发生,因为编译器仅仅部分地检测异常的使用是否与异常明细保持一致。举例而言,假设函数A有一个明确的异常明细,并且调用了函数B,函数B抛出了违反函数A异常明细的异常,所以本次函数调用就违反了A的异常明细,程序将立即终止:

1
2
3
4
5
6
extern void f1();//可以抛出任意的异常
void f2() throw(int){//异常明细中只有int
...
f1(); //抛出非int型异常,程序终止
...
}


解决方案

避免在带有类型参数的模板中使用异常

1
2
3
4
template<class T>
bool operator==(const T& lhs, const T& rhs) throw(){
return &lhs == &rhs;
}

这个函数的意思是,如果类型相同的两个对象地址相同,则认为它们相等。看起来好像确实不会抛出异常,但实际上…万一某个类重载了operator &,并且该函数还能抛异常,那我们就等着terminate吧。

事实上,我们几乎不可能为一个模版提供一个有意义的异常明细,因为模版总是采用不同的方法使用类型参数,解决方法只能是模板和异常明细不要混用。


去除异常明细

不触发terminate的另一个方法是:如果在一个具备异常明细的函数内调用其它没有异常明细的函数,那么我们应该去除该函数的异常明细。

实例

允许用户注册一个回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//一个 window 系统回调函数指针
typedef void (*CallBackPtr)(int eventXLocation,int eventYLocation,void *dataToPassBack);
class CallBack{
public:
CallBack(CallBackPtr fPtr, void *dataToPassBack)
: func(fPtr), data(dataToPassBack) {}
void makeCallBack(int eventXLocation,int eventYLocation) const throw();
private:
CallBackPtr func;//需要调用的函数
void *data;//传递给回调函数的数据
};
void CallBack::makeCallBack(int eventXLocation,int eventYLocation) const throw(){
func(eventXLocation, eventYLocation, data);
}

显然,func可能抛出异常,违反了第二种解决方案。


处理系统本身抛出的异常

 
有人说未雨绸缪强过临阵磨枪,但实际上对于unexpected而言,直接处理unexpected异常比防止它们被抛出更有效果。
虽然阻止抛出unexpected异常是不现实的,但是C++允许以其它不同的异常类型替换unexpected异常。比如我们可以将所有unexpected异常都被替换为UnexpectedException对象:

1
2
3
4
5
class UnexpectedException {};
void convertUnexpected() {
throw UnexpectedException();
}
set_unexpected(convertUnexpected);//将unexpected替换掉

当然还有一个更安全的方法:
1
2
3
4
void convertUnexpected(){
throw;
}
set_unexpected(convertUnexpected);//unexpected被抛出时只是重新抛出当前异常


总结

 
异常明细是一个应被审慎使用的特性。在编写之前,确保你已经考虑了所有可能发生的情况。

13.以引用方式捕获异常

Posted on 2018-04-26 | In More Effective C++

前言

 
异常有三种方式传递到catch子句:通过指针(by pointer),通过传值(by value)或通过引用(by reference)。


by pointer

 
用指针来传递异常理论上来说是效率最高的,因为只有它不用copy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class exception { ... };
void someFunction(){
static exception ex; // 异常对象
...
throw &ex; // 抛出一个指针,指向 ex
...
}
void doSomething(){
try {
someFunction(); // 抛出一个 exception*
}
catch (exception *ex) { // 捕获 exception*;
... // 没有对象被拷贝
}
}

逻辑上看起来没错,但老是有人忘了下述约定:定义异常对象时,必须确保程序控制权离开抛出指针的函数后对象仍然继续生存,否则catch的对象已被析构。具体来说,当我们用指针抛出异常,必须确保该异常是一个static对象或者位于heap中。
但这两种情况又引入了新的问题:你不知道该在何时删除它们。中的对象不删除必然会导致资源泄漏,而全局变量无法删除,否则程序行为将不可预测。
c++不鼓励通过指针传递异常,四大标准异常:bad_alloc、bad_cast(dynamic_cast失败)、bad_typeid(dynamic_cast操作空指针)、bad_exception(unexpected异常)均不是指向对象的指针,因此你必须通过值或引用来捕获。


by value

 
值捕获有两大缺陷:

  1. 两次拷贝
  2. slicing problem(由于拷贝静态类型引起)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class exception {
public:
virtual const char * what() throw();
};
class runtime_error:public exception { ... };
class Validation_error:public runtime_error {
public:
virtual const char * what() throw();
...
};
void someFunction(){
...
if (a validation 测试失败) {
throw Validation_error();
}
...
}
void doSomething(){
try {
someFunction();
}
catch (exception ex) {
cerr << ex.what(); // 调用 exception::what(),而不是 Validation_error::what()
...
}
}

传值模式在何时都只会调用基类函数,完全丧失了多态性质。


by reference

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void someFunction(){
...
if (a validation 测试失败) {
throw Validation_error();
}
...
}
void doSomething(){
try {
someFunction();
}
catch (exception& ex) {//引用捕获
cerr << ex.what(); //Validation_error::what(),
...
}
}


总结

 
如果你通过引用捕获异常(catch by reference),有优点如下:

  1. 无需担心对象已被析构或者不确定对象何时需要删除(by pointer缺点)
  2. 无需拷贝2次且丧失多态性质(by value缺点)

12.了解异常的抛出

Posted on 2018-04-25 | In More Effective C++

异常的抛出与函数传递参数

 
从语法角度看,传递参数和捕获异常其简直一摸一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget { ... }; 
//函数调用
void f1(Widget w);
void f2(Widget& w);
void f3(const Widget& w);
void f4(Widget *pw);
void f5(const Widget *pw);
//异常捕获
catch (Widget w) ...
catch (Widget& w) ...
catch (const Widget& w) ...
catch (Widget *pw) ...
catch (const Widget *pw) ...

它们确实有相同点,但是也存在巨大差异。

  • 相同点在于:传递过程都是可以传值、传递引用、传递指针。
  • 不同点在于:调用函数时,程序控制权终将返回调用处,但当你抛出异常时,控制权将永远回不到抛出异常的地方。

异常与拷贝

 
以如下函数举例:

1
2
3
4
5
6
istream operator>>(istream& s, Widget& w);
void passAndThrowWidget(){
Widget localWidget;
cin >> localWidget;//传递参数
throw localWidget;//抛出异常
}

当我们传递参数时,其实是把w绑定到localwidget,任何施加于w的操作都施加于它。但throw则不同,无论通过传值还是引用,异常都会被拷贝,原因很简单,localwidget一旦离开了生存空间后析构函数就会被调用。如果我们把localwidget本身传递出去,catch只能接受到一个析构了的widget,因此异常抛出的对象必须被复制。

static异常

即使对象是static,不会被析构,抛出异常的时候也会进行复制:

1
2
3
4
5
void passAndThrowWidget(){
static Widget localWidget;
cin >> localWidget;
throw localWidget; // 拷贝操作
}

这表明哪怕是使用引用捕获异常,我们永远也无法通过catch来修改localwidget本身,仅仅只能修改对象的拷贝。这也证明了抛出异常与参数传递的第二个差异:抛出异常速度比参数传递要慢。

静态类型

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型所对应类的拷贝构造函数,而不是对象的动态类型对应类的拷贝构造函数。

1
2
3
4
5
6
7
8
class Widget { ... };
class SpecialWidget: public Widget { ... };
void passAndThrowWidget(){
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget;
throw rw;//抛出一个类型为Widget的异常
}


catch中的异常

 
异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。

1
2
3
4
5
6
7
8
catch (Widget& w){// 捕获 Widget 异常
... // 处理异常
throw; // 重新抛出异常,让它继续传递
}
catch (Widget& w){// 捕获 Widget 异常
... // 处理异常
throw w; // 传递被捕获异常的拷贝
}

这两个catch块中,第一个catch块重新抛出的是当前捕获的异常,第二个抛出的是当前捕获的异常的一个拷贝。
第一个块中抛出的类型不需要静态动态类型,因为它没有进行拷贝操作。
第二个块中重新抛出的是新异常,其类型必然是widget.
一般来说,推荐使用throw,这样不会改变异常的类型,也不用生成额外的异常临时变量了。(临时变量详见More Effective C++ 19)


异常的捕获方式

 
异常一般有传值、传引用、传指向const的引用三种:

1
2
3
catch (Widget w) ... // 通过传值捕获异常
catch (Widget& w) ... // 通过传递引用捕获异常
catch (const Widget& w) ... //通过传递指向const的引用捕获异常

通过比对我们又发现了一大差异,我们已经知道抛出的拷贝其实是一个临时变量,而在传递参数时临时变量无法被普通引用绑定,只能通过const reference。

不同捕获方式之间的差异

传值

当我们声明这样的一个catch子句时:

1
catch (Widget w) ...//传值捕获

该表达式会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,另一个是把临时对象拷贝进w中。

引用

当我们声明使用引用捕获时:

1
2
catch (Widget& w) ... // 通过引用捕获
catch (const Widget& w) ... //也通过引用捕获

只会建立一个拷贝,该拷贝是一个临时对象,并且被引用绑定,但我们使用引用传递参数时则不会发生拷贝。

指针

对于指针而言,传递参数与传递异常差不多,都是一个指针的拷贝被传递,只不过要记得用指针传递异常时异常必须是全局的或者在堆中,否则catch子句只能通过指针去往一个已经被析构的地方。


异常与类型转换

隐式转换

异常传递中不允许出现隐式转换,但传递参数时可以。

异常传递中允许发生的转换

第一种是派生类与基类间的转换。(is-a)
另一种就是允许从一个类型化指针转变为无类型指针,所以参数为const void*的catch语句能捕获任何类型的指针类型异常。

1
catch(const void*) ...


catch语句匹配

 
传递函数与传递异常的差别还在于catch语句匹配不按最合适的,而是总是按照先后顺序,因此可能会发生派生类异常永远总是处理不到的情形:

1
2
3
4
5
6
7
8
9
try {
...
}
catch (logic_error& ex) {//base
...
}
catch (invalid_argument& ex) {//derived
...//永远不会被执行
}

虚函数采用的是最优匹配法,而异常处理采用的是最先适合法。但如果一个派生类的catch在一个基类的catch后面,通常编译器会发出警告。


总结

 
一个对象传递给函数,一个对象调用虚函,把一个对象做为异常抛出,它们之间有三个主要区别:

  • 异常在传递时总被拷贝,传值时被拷贝了两次
  • 异常的类型转换很少(2种)
  • 异常采用最先匹配法

11.禁止异常信息传递到析构函数之外

Posted on 2018-04-25 | In More Effective C++

(本节内容可参照Effective C++ 9共同阅读)

前言

 
析构函数只有2种情况会被调用:

  1. 正常情况,对象离开作用域或被显式delete
  2. 对象被异常处理机制——异常传播过程中的stack-unwinding机制析构

在这两种情况中,调用析构函数时异常可能处于激活状态也可能没有被激活(无法在析构函数内进行区分),我们只能假定异常处于激活状态。因为在一个异常被激活的同时,如果析构函数再次抛出异常,C++会立刻终止程序运行,甚至连局部变量都尚未释放。


问题实例

有一个session类用来跟踪在线计算机的sessions,session是计算机从登陆到注销时一直存在的某种文件。每个session对象关注的是它建立与释放的日期与时间:

1
2
3
4
5
6
7
8
9
class Session {
public:
Session();
~Session();
...
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
};

函数logCreation与logDestruction被分别用于记录对象的建立与释放。我们因此可以这样编写 Session 的析构函数:
1
2
3
Session::~Session(){
logDestruction(this);
}

如果logDestruction抛出异常,会发生什么?


实例剖析

 
析构函数抛出的异常并没有被捕获,因此它会被传递到调用位置。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么terminate函数将被自动调用,彻底终止程序。为了保证logDestruction抛出的异常被传递到析构函数之外,我们必须手动地加以try-catch:

1
2
3
4
5
6
7
8
Session::~Session(){
try {
logDestruction(this);
}
catch (...) {
cerr << "Unable to log destruction of Session object ";
}
}

看起来异常确实被捕获了,但如果operator << 抛出了一个异常….程序又会被直接终止运行。
因此,最终解决方案是释放Session时忽略掉所有它抛出的异常:
1
2
3
4
5
6
Session::~Session(){
try {
logDestruction(this);
}
catch (...) { }
}

catch语句看起来像什么都没做,但他从实际上保证了异常不会被传递到析构函数之外,因此terminate不会被调用。


非完全析构

 
除却保证程序不会因为多个异常同时激活导致termi,异常不得离开析构函数的第二个原因就是保证完全析构。如果一个异常被析构函数抛出但却没有在函数内部被捕获,那么析构函数就不会完全运行(停在抛出异常的那个地方)。如果析构函数不完全运行,就会发生非完全析构。

仍以session为例,假设在建立session时启动一个database transaction,析构session时结束它:

1
2
3
4
5
6
7
8
Session::Session() { 
logCreation(this);
startTransaction();
}
Session::~Session(){
logDestruction(this);
endTransaction();
}

显然,如果logDestruction抛出一个异常,transaction就没有被终止。


总结

 
禁止异常传递到析构函数之外有两个原因:

  1. 阻止terminate被调用
  2. 阻止非完全析构

10.在构造函数中防止资源泄漏

Posted on 2018-04-25 | In More Effective C++

问题实例

 
假设我们正在开发一个多媒体电话簿,它不仅可以存储姓名地址号码外,还能存储照片和声音。该类具体形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Image {
public:
Image(const string& imageDataFileName);
...
};
class AudioClip {
public:
AudioClip(const string& audioDataFileName);
...
};
class PhoneNumber { ... }; //存储号码
class BookEntry { //条目
public:
BookEntry(const string& name,const string& address = "",
const string& imageFileName = "",const string& audioClipFileName = "");
~BookEntry();
void addPhoneNumber(const PhoneNumber& number);
...
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
Image *theImage;
AudioClip *theAudioClip;
};

因为条目必须存在姓名,所以不存在默认构造函数(More Effective C++ 3)。
对于条目的构造和析构函数,有具体实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),theImage(nullptr), theAudioClip(nullptr){
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
BookEntry::~BookEntry(){
delete theImage;
delete theAudioClip;
}

这一切看起来运作良好,但如果函数中出现了异常…


问题剖析

 
如果BookEntry的构造函数正在执行中,突然发生异常:

1
2
3
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}

异常的抛出原因,可能是operator new不能够分配足够内存,也可能是audio的构造函数自己抛出了异常。不管如何,这个异常将传递到建立BookEntry对象的地方。
假设建立audio时抛出了异常(且控制权此时在Entry构造函数外),那么已经构建好的image对象该由谁来删除呢?无疑该资源应该由Entry的析构函数释放,但在实际情况中,Entry析构函数并未被调用,原因很简单:C++只能对完全构造的对象进行析构,只有一个对象的构造函数完全运行完毕,这个对象才被定义为完全构造。
也许有人会想到使用try—catch主动处理异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
void testBookEntryClass(){
BookEntry *pb = nullptr;
try {
pb = new BookEntry("Addison-Wesley Publishing Company",
"One Jacob Way, Reading, MA 01867");
...
}
catch (...) {
delete pb;
throw;
}
delete pb;
}

这并没有什么用,因为new操作并没有完成,pb依旧是一个nullptr,delete只不过删了一个空指针而已。

c++禁止为尚未完成构造的对象进行析构,因为允许该操作需要记录构造函数执行了多少步,这会造成额外的开销。


解决方案

 
为了解决资源泄漏这一问题,处理方法是在构造函数中捕获所有异常,捕获后执行清除操作,最后再重新抛出异常让它继续传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),theImage(nullptr), theAudioClip(nullptr){
try {
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch (...) {
delete theImage;
delete theAudioClip;
throw;
}
}

非指针成员在构造函数被调用之前就完成了初始化,属于完全构造,因此会被自动释放。
显然,catch语句块中的清除操作几乎与析构函数一摸一样,为了避免代码重复,我们定义一个私有的helper function,让构造函数与析构函数都调用它,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class BookEntry {
public:
... // 同上
private:
...
void cleanup();
};
void BookEntry::cleanup(){
delete theImage;
delete theAudioClip;
}
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),theImage(nullptr), theAudioClip(nullptr){
try {
...
}
catch (...) {
cleanup();
throw;
}
}
BookEntry::~BookEntry(){
cleanup();
}


异常产生于构造函数调用之前

 
假设image与audio是一个const指针,因此它们必须通过初始化列表来完成初始化(不允许赋值):

1
2
3
4
5
6
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(imageFileName != ""? new Image(imageFileName): nullptr),
theAudioClip(audioClipFileName != ""? new AudioClip(audioClipFileName): nullptr)
{}

倘若常量指针初始化时抛出了异常….这一次情况发生在构造函数被调用之前,所以无法在构造函数中使用try catch语句。


解决方案

新建初始化函数

我们可以在私有成员函数中,用一些函数返回指针指向初始化过的image与audio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BookEntry {
public:
...
private:
...
Image * initImage(const string& imageFileName);
AudioClip * initAudioClip(const string& audioClipFileName);
};
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,Const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(initImage(imageFileName),theAudioClip(initAudioClip(audioClipFileName))
{}
// theImage首先初始化,初始化失败也不会有内存泄漏,无需考虑异常
Image * BookEntry::initImage(const string& imageFileName){
if (imageFileName != "") return new Image(imageFileName);
else return nullptr;
}
//初始化失败必须要保证Image资源被释放
AudioClip * BookEntry::initAudioClip(const string&audioClipFileName){
try {
if (audioClipFileName != "")
return new AudioClip(audioClipFileName);
else return 0;
}
catch (...) {
delete theImage;
throw;
}
}

该方法的缺点在于原本属于构造函数的代码却分散在各个函数中,维护极为不易,并且对象增加了新成员后代码也需要重新编写。

RAII

我们可以运用RAII思想,把audio与image指向的对象作为资源托管给smart-ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BookEntry {
public:
...
private:
...
const auto_ptr<Image> theImage;
const auto_ptr<AudioClip> theAudioClip;
};
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(imageFileName != ""? new Image(imageFileName): nullptr),
theAudioClip(audioClipFileName != ""? new AudioClip(audioClipFileName): nullptr)
{}

如此一来,audio构造出现异常时,image已经是一个完成构造的完全体,可以被直接清除,同时所有资源均由对象托管,析构函数无需执行任何工作。

9.使用析构函数防止资源泄漏

Posted on 2018-04-25 | In More Effective C++

(本节内容重点描述RAII,详见Effective C++ 14。)

问题实例

 
假设目前我们需要为某宠物店编写软件,工作是读取宠物店建立的档案文件,然后对动物进行适当处理。
合理方案是建立一个pure abstract class
“ALA”(adorable little animal)。然后为小狗小猫建立派生类,一个virtual函数processAdoption负责处理各个不同种类的小动物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ALA{
public:
virtual void processAdoption() = 0;
...
};
class Puppy:public ALA {
public:
virtual void processAdoption();
...
};
class Kitten: public ALA {
public:
virtual void processAdoption();...
};

​
我们通过从文件中读取到的信息建立一个puppy或kitten对象,这很适合工厂模式(虚构造函数):
1
ALA * readALA(istream& s);

程序的核心部分如下:
1
2
3
4
5
6
7
void processAdoptions(istream& dataSource){
while (dataSource) {
ALA *pa = readALA(dataSource);
pa->processAdoption();
delete pa;
}
}

每一次读取完必须删除pa,如果不删除堆对象会导致资源泄漏。


问题剖析

 
倘若processAdoption函数抛出了异常,这将直接导致后续语句被跳过,pa没有被删除,资源泄漏由此产生。
一提到异常,大多数人的第一反应是使用try catch去捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
void processAdoptions(istream& dataSource){
while (dataSource) {
ALA *pa = readALA(dataSource);
try {
pa->processAdoption();
}
catch (...) {
delete pa;
throw;
}
delete pa;
}
}

虽然问题确实解决了,但是我们却使得原有程序冗余且低效,反正无论如何我们都要执行清除操作,何必要写多次呢?


RAII

 
我们可以把总被执行的delete放入process函数内的局部对象的析构函数里,当process返回时局部对象被释放,因此delete操作也完成了。
具体方法是用一个smart-ptr.本次实例使用auto_ptr代替raw指针:

1
2
3
4
5
6
void processAdoptions(istream& dataSource){
while (dataSource) {
auto_ptr<ALA> pa(readALA(dataSource));
pa->processAdoption();
}
}

显然这是一个标准的RAII,用一个对象存储需要被自动释放的资源。


RAII实例

RAII并非只是适用于智能指针或者堆对象,其本意是用对象来管理一切可能存在泄漏的资源的分配与释放。以一个可能发生资源泄漏的GUI程序举例:

1
2
3
4
5
void displayInfo(const Information& info){
WINDOW_HANDLE w(createWindow());
...//在w对应的window中显示信息
destroyWindow(w);
}

如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样。
解决方法是建立一个类,构造函数接受资源,析构函数释放资源:
1
2
3
4
5
6
7
8
9
10
11
class WindowHandle {
public:
WindowHandle(WINDOW_HANDLE handle): w(handle) {}
~WindowHandle() { destroyWindow(w); }
operator WINDOW_HANDLE() { return w; } // see below
private:
WINDOW_HANDLE w;
//防止拷贝
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};

使用了RAII的display函数如下所示:
1
2
3
4
void displayInfo(const Information& info){
WindowHandle w(createWindow());
...//在w对应的window中显式信息;
}


总结

 
资源应当被封装在对象内,遵循该原则可以避免在异常发生时泄漏资源。

8.了解不同意义的new与delete

Posted on 2018-04-25 | In More Effective C++

前言

 
new操作符(new operator)与new操作(operator new)含义完全不同。


具体区别

 
考虑如下代码:

1
string *ps = new string("Memory Management");

这里使用的是new操作符(new operator),它就像sizeof一样是内置的,无法改变其功能与含义。它的功能有以下两个:

  1. 分配足够大的内存以便于容纳对象
  2. 调用构造函数初始化内存中的对象

我们能够改变的是如何为对象分配内存。new操作符使用一个函数来完成内存分配工作,此函数名为operator new.


operator new

 
operator new有声明如下:

1
void * operator new(size_t size);

返回值类型是void*,因为此函数返回了一个raw指针指向未初始化的内存。size代表了分配多少字节。该函数允许重载,但是第一个形参应当保证为size.
类似于malloc,opertaor new只负责分配内存,它对构造函数一无所知。我们可以认为,当执行如下语句时:
1
string *ps = new string("Memory Management");

其行为应当类似于:
1
2
3
void *memory = operator new(sizeof(string));
call string::string("Memory Management") on *memory;//初始化对象
string *ps = static_cast<string*>(memory);

值得注意的是第二部涉及构造函数调用,程序员无法显式在内存中调用构造,因此如果我们想在heap上构建对象,就必须要使用new操作符。


placement new(Effective C++ 53)

 
假定我们已经获取了一块raw内存,我们需要在这些内存中构造对象,我们可以使用一个特殊的operator new,它又被称为placement new,其构造实例如下:

1
2
3
4
5
6
7
8
class Widget {
public:
Widget(int widgetSize);
...
};
Widget * constructWidgetInBuffer(void *buffer,int widgetSize){
return new(buffer) Widget(widgetSize);
}

这个函数返回一个指针,指向一个Widget对象,对象在传递给函数的buffer里分配。
placement new的定义式大致如下所示:
1
2
3
void * operator new(size_t, void *location){
return location;
}


operator new与new operator使用总结

  • 如果你需要在堆上建立一个对象,使用new操作符:它既分配内存又为对象调用构造函数。
  • 如果你只需要分配内存,那你只要用operator new。
  • 如果你希望在内存分配时自定义操作,那无疑需要重载operator new。
  • 如果你需要在指定的raw 内存位置构建对象,那使用placement new。

Deletion and Memory Deallocation

 
为了避免内存泄漏,每一个动态内存分配必须与一个deallocation对应。delete operator 与operator delete的关系类似于new operator与operator new。
如果我们是用placement new在内存中建立对象,那我们应该避免在该内存中使用delete操作符。因为delete操作符调用operator delete,而后者并不知道该去哪里释放内存(Effecive C++ 53)。所以必须显式调用对象的析构函数来解除构造函数的影响:

1
2
3
4
5
6
7
8
9
//在共享内存中分配和释放内存的函数
void * mallocShared(size_t size);
void freeShared(void *memory);
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = constructWidgetInBuffer(sharedMemory, 10);
...
delete pw; //结果不确定! 共享内存来自mallocShared, 而不是 operator new
pw->~Widget();//正确析构
freeShared(pw); //释放内存

(此处内容类似于alloctor)


数组

 
如果我们不是一次建立一个对象,而是试图分配一个数组:

1
string *ps = new string[10];

此时调用的依然是new operator,不过较于之前有两点不同:

  1. 内存分配函数不再是operator new,而是operator new[],它也可以被重载。
  2. new operator调用了n次构造函数。

同理,delete也发生了改变。


总结

 
在试图定制new与delete的行为时,有准则如下:只能改变它们为完成功能所采用的方法,而不能更改它们可以完成什么功能。(You can modify how they do what they do, but what they do is fixed by the language.)

53.placement new与placement delete

Posted on 2018-04-25 | In Effective C++

内存释放

 
一般而言,new表达式形式大致如下:

1
Widget* pw = new Widget;

其中一共调用了两个函数:operator new以及Widget的默认构造函数。
如果operator new被成功调用,但默认构造函数却抛出了异常,我们应该释放分配的内存并让它恢复原状。客户并没有这个能力,因为pw此时尚未被赋值,客户并不知道已分配的内存的地址,该任务需要C++运行期系统完成。
解决方法很简单:调用operator new所对应的那个operator delete。听起来容易,但如果我们曾经声明过带有附加参数的operator new,对应的delete就不好找了。


问题实例

 
假设现有一个operator new接受一个ostream,用来记录信息,同时具备一个正常形式的class专属delete:

1
2
3
4
5
6
7
class Widget{
public:
...
static void* operator new(size_t size,ostream& log) throw(bad_alloc);//非正常形式new
static void operator delete(void* pMemory,size_t size) throw();//正常形式的delete
...
}

这个设计存在问题,但我们在探究原因之前,必须先了解相关术语。


placement new

 
如果一个operator new除了size_t之外还接受其他参数,那它就是一个placement版本。
众多placement版本中最受欢迎的是“接受一个指针指向对象被构造之处”:

1
void* operator new(size_t,void* pMemory) throw();

它是如此地知名以至于我们默认提及placement new即为该版本。


解决方案

 
显然,在实例中,带有ostream参数的placement new并没有与之对应的delete,所以一旦构造函数抛出异常,系统无法做到自恢复。所以我们应该为它补充上placement delete:

1
void operator delete(void*,ostream&) throw();


名称遮蔽

 
成员函数的名称会掩盖其外部作用域的相同名称(Effective C++ 34),如果你的base class中只声明了一个placement new,客户将无法使用正常形式的operator new,delete同理。与此类似的,derived class中的operator news会遮蔽base版本与继承而来的版本。
为了避免名称遮蔽,我们需要了解到C++在global作用域提供以下形式的operator new:

1
2
3
void* operator new(size_t) throw(bad_alloc);
void* operator new(size_t,void*) throw();
void* operator new(size_t,const nothrow_t&) throw();

如果我们在class内声明任何形式的operator new,它们都会遮掩这些标准形式,除非你的本意就是禁止使用标准形式,否则请确保它们可用。确保可用的方法是:建立一个base class,内含所有正常形式的new与delete。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class StandardNewDeleteForms{
public:
//normal
static void* operator new(size_t size) throw(bad_alloc){
return ::operator new(size);
}
static void operator delete(void* pMemory) throw(){
return ::operator delete(pMemory);
}
//placement
static void* operator new(size_t size,void* ptr) throw(){
return ::operator new(size,ptr);
}
static void* operator delete(void* pMemory,void* ptr) throw(){
return ::operator delete(pMemory,ptr);
}
//nothrow
static void* operator new(size_t size,const nothrow_t& nt) throw(){
return ::operator new(size,nt);
}
static void* operator delete(void* pMemory,const nothrow_t& nt) throw(){
return ::operator delete(pMemory);
}
};

当你需要自定义new与delete时,只需要继承该类并使用using机制,即可避免名称遮蔽。


总结

 

  1. placement new和placement delete必须同时存在且匹配
  2. 自定义版本不得遮蔽正常版本
<i class="fa fa-angle-left"></i>1…151617…27<i class="fa fa-angle-right"></i>

xander.liu

266 posts
11 categories
36 tags
RSS
GitHub E-Mail
© 2024 xander.liu
Powered by Hexo
|
Theme — NexT.Pisces v5.1.4