Effective C++学习 part 1

Effective C++

Introduction

构造函数 & 赋值函数

copy构造和copy赋值并不相同,如果是在定义时赋值,实际上调用的是构造函数而不是赋值函数,如果并非新定义的对象,才会调用赋值函数。

Pass-by-value

Pass-by-value 意味“调用copy构造函数”。以 by value 传递用户自定义类型通常是个坏主意,Pass-by-reference-to-const 往往是比较好的选择;详见条款20。

不明确的行为将导致不可预期的结果

Client

所谓 client(客体、客户)是指某人或某物,他(或它)使用你写的代码(通常是一些接口)。函数的客户是指其使用者,也就是程序中调用函数(或取其地址)的那一部分,也可以说是编写并维护那些代码的人。Class 或 template 的客户则是指程序中使用 class 或template 的那一部分,也可以说是编写并维护那些代码的人。说到“客户”时通常我指的是程序员,因为程序员可能被迷惑、被误导、或因糟糕的接口而恼怒,他们所写的代码却不会有这种情绪。

Accustoming Yourself to C++

条款01:视C++为一个语言联邦

C++ = C with Classes

今天的C++已经是个多重范型编程语言( multiparadigm programminglanguage),一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言。这些能力和弹性使C++成为一个无可匹敌的工具。最简单的方法是将 C++ 视为一个由相关语言组成的联邦而非单一语言。在其某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易,可以将 C++ 分为四个次语言:

  • C
  • Object-Oriented C++
  • Template C++
  • STL

条款02:尽量以 const, enum, inline 替换 #define

  • 对于单纯常量,最好以 const 对象或 enums 替换 #defines。
  • 对于形似函数的宏(macros),最好改用 inline 函数替换 #defines。

条款03:尽可能使用const

const 对象

const 可以被用在 classes 外部修饰 global 或 namespace (见条款2)作用域中的常量,或修饰文件、函数、或区块作用域(block scope)中被声明为static的对象。你也可以用它修饰 classes 内部的 static 和 non-static 成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不)是 const:

在这里插入图片描述

const 成员函数

const 在对函数声明进行限制时的位置既可以在最前端也可以在最后端,如下所示:

char& operator[] (std::size_t position)				//返回对象和调用对象都不能有const限制
const char& operator[] (std::size_t position) 		//返回对象做const限制,调用对象不能有const限制
char& operator[] (std::size_t position) const		//调用对象做const限制,返回对象不能有const限制
const char& operator[] (std::size_t position) const //返回对象与调用对象同时做const限制

对参数的限制比较简单,同普通对象一致。

Tips:

​ operator[] 函数的返回类型应该是 reference to type 而不是 type,内置类型,因为这样才能写入。

bitwise constness & logical constness

bitwise constness:成员函数不应该改变成员对象哪怕一个bit,这种只要侦察对成员对象的赋值就可以保证

logical constness:允许修改部分成员对象,但客户端应该侦察不到。

可以利用 mutable 修饰那些允许在 const 成员函数中发生变化的对象成员,可以实现 logical constness

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

对于“ bitwise-constness 非我所欲 ”的问题,mutable是个解决办法,但它不能解决所有的 const 相关难题。举个例子,假设某个 class 内的 operator[] 不单只是返回一个 reference 指向某字符,也执行边界检验(boundschecking)、志记访问信息(logged access info)、甚至可能进行数据完善性检验。把所有这些同时放进 const 和 non-const operator[] 中,导致臃肿的代码。两个版本的operator[] ,其中重复了一-些代码,例如函数调用、两次return语句等等。

常量性转除(cast away constness)可以解决这样的问题,也就是 const_cast,如下函数展示了这样的流程:

在这里插入图片描述

可以看到,其中 static_cast负责将 TextBlock 类型的对象转型成 const TextBlock 类型的,而const_cast 则负责将返回值的 const 的限制去掉。

Ttips:

​ const_cast 是去掉 cosnt 限制,而不是像字面描述地加上 const 限制,为对象添加 const 限制只能通过 static_cast 完成。

更值得了解的是,反向做法 —— 令 const 版本调用 non-const 版本以避免重复—并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态(logical state) ,non-const 成员函数却没有这般承诺。如果在 const 函数内调用 non-const 函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。这就是为什么“const 成员函数调用 non-const 成员函数”是一种错误行为:因为对象有可能因此被改动。实际上若要令这样的代码通过编译,你必须使用一个const_cast 将 *this 身上的 const 性质解放掉,这是乌云罩顶的清晰前兆。反向调用(也就是我们先前使用的那个)才是安全的: non-const 成员函数本来就可以对其对象做任何动作,所以在其中调用一个const 成员函数并不会带来风险。这就是为什么本例以 static_cas t作用于*this的原因:这里并不存在const相关危险

最后:

  • 将某些东西声明为 const 可帮助编译器侦测出错误用法。const 可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施 bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
  • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。

条款04:确定对象被使用前已先被初始化

读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些“半随机” bits,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多令人不愉快的调试过程。

首先,我们给出一个类:

在这里插入图片描述

然后观察下面两个构造函数:

在这里插入图片描述
在这里插入图片描述

两个构造函数的最终结果相同,但第二个通常效率较高。基于赋值的那个版本(第一个)首先调用 default 构造函数为 theName, theAddress 和 thePhones 设初值,然后立刻再对它们赋予新值。default 构造函数的一切作为因此浪费了。**成员初值列(member initialization list)**的做法(第二个)避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。

对于类型 ABEntry 中的成员对象,假如成员对象的类型是非内置类型,那么这种写法是很有必要的,而对于内置类型,他们的初始化和赋值的效率的近似的,可以不用这样写。


通常如果你使用C part of C++(见条款1)而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入 non-C parts of C++,规则有些变化。这就很好地解释了为什么 array(来自C part of C++)不保证其内容被初始化,而 vector(来自STL part of C++)却有此保证。

不同编译单元(translation unit)定义之 non-local static 对象的初始化顺序

static:

  • 全局 static

  • 局部 static

  • static 变量的初始化顺序:编译时初始化 > 加载时初始化 > 运行时初始化

    参考:https://blog.csdn.net/qq_34139994/article/details/105157313

translation unit:

​ 所谓编译单元( translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。

假如在多个文件中都分别存在 non-loacl static 对象,且这些对象之间的初始化相互有关联,且都是加载时初始化,那么可能出现相互调用的对象并没用完成初始化。因此必须将这种初始化推迟到运行时初始化。

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class 中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。

Constructors,Destructors, and Assignment Operators

条款05:了解C++默默编写并调用哪些函数

什么时候 empty class(空类)不再是个 empty class 呢?当C++处理过它之后。是的,如果你自己没声明,编译器就会为它声明(编译器版本的)一个 copy 构造函数、一个copy assignment 操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是 public 且 inline(见条款30)。因此,如果你写下:

class Empty()

这就好像你写下:

class Empty {public:
	Empty(){ ... } // default构造函数
	Empty (const Empty& rhs) { ... }  // copy构造函数
	~Empty( ) { ... } //析构函数,是否该是virtual见稍后说明.
	Empty& operator=(const Empty& rhs) { ... } // copy assignment 操作符
};

但是当内含的成员变量是比较特殊的类别约束时,比如 reference,const,那么编译器将拒绝为这样的类生成 copy assignment 操作符的重载函数。

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

如同标题一样,如果在某些类型的使用时不希望一些特定的函数被调用,这里说的函数是指编译器会自动添加的函数,这些函数都会被写作 public 。但即使是写作 private 或者 protect 类别,依然会被一些函数调用。在这里,书中介绍的方式是像如下这种方式去声明且不对函数进行定义:

class A {
public:
    ...
private:
	A (const A&);	//只有声明
	A& operator= (const A&);
};

有了上述 class 定义,当客户企图拷贝 HomeForSale 对象,编译器会阻挠他。如果你不慎在 member 函数或 friend 函数之内那么做,轮到连接器发出抱怨。

将连接期错误移至编译期是可能的(而且那是好事,毕竟愈早侦测出错误愈好),只要将 copy 构造函数和 copy assignment 操作符声明为 private 就可以办到。但不是在HomeForsale 自身,而是在一个专门为了阻止 copying 动作而设计的 base class 内。这个base class非常简单:

class Uncopyable {
protected:
	Uncopyable() { }  //允许derived对象构造和析构
	~Uncopyable () {}
private:
	Uncopyable(const Uncopyable&);  //但阻止 copying
	Uncopyable& operator= (const Uncopyable&) ;
};

对应的类别只要继承这样的类,就可以禁用 copy 函数以及 copy assigment 操作符。

new c++ grammar

当然在新的 C++ 语法中也有别的方式可以禁止对 default 函数的使用,语法如下:

class A{
public:
	A(){}
	~A(){}
private: //这里最好声明为 private 函数,当然 public 也可以
	A(const A&) = delete;//拷贝构造函数
	A& operator=(const A&) = delete;//赋值运算符
}

在对应的函数声明后添加 ``=delete`这样的函数在编译时就会被编译器所阻止。

条款07:为多态基类声明virtual析构函数

假如我们拥有一个 base class 和一堆该 base class 的 derived class,那在使用中会有以 base class 的指针指向这些 derived class 对象的情况,这种使用中这被称为向上转型,如下所示:

class TimeKeeper {
public:
	TimeKeeper () ;
    ~TimeKeeper ();...
};

class Atomicclock: public TimeKeeper { ... };l//原子钟
class waterClock: public TimeKeeper { ... };//水钟
class wristwatch: public TimeKeeper { ... };//腕表

TimeKeeper*getTimeKeeper (); //返回一个指针,指向一个TimeKeeper派生类的动态分配对象

getTimeKeeper 返回一个 TimeKeeper 的指针,但他可能动态地指向一个 derived class,如果这个函数所返回的指针在使用结束时被 delete 进行注销,那么他会使用哪个类的析构函数呢?假如父类中有一个 non-virtual 的析构函数,那么这个 delete 就会调用这个析构函数,这会引来灾难,因为 C++ 明确指出,当 derived class 对象经由一个 base class 指针被删除,而该base class带着一个 non-virtual 析构函数,其结果未有定义——实际执行时通常发生的是对象的 derived 成分没被销毁。如果 getTimeKeeper 返回指针指向一个AtomicClock 对象,其内的 AtomicClock 成分(也就是声明于Atomicclock class 内的成员变量〉很可能没被销毁,而AtomicClock的析构函数也未能执行起来。然而其 base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。这可是形成资源泄漏、败坏之数据结构、在调试器上浪费许多时间的绝佳途径喔。

消除这个问题的做法很简单;给base class 一个virtual析构函数。此后删除 derived class 对象就会如你想要的那般。

如果 class 不含 virtual 函数,通常表示它并不意图被用做一个 base class。当 class 不企图被当作 base class,令其析构函数为 virtual 往往是个馊主意:带有 virtual 函数的类会比普通类携带更多信息,占据空间

即使 class 完全不带 virtual 函数,被“non-virtual 析构函数问题”给咬伤还是有可能的。举个例子,标准 string 不含任何 virtual 函数,但有时候程序员会错误地把它当做base class:

class Specialstring: public std::string { //馊主意 std::string有个 non-virtual析构函数
    ...
};
Specialstring* pss = new Specialstring ( "Impending Doom");
std::string* ps;
ps = pss; // SpecialString* => std: :string*
...
delete ps;  //未有定义!现实中*ps的 Specialstring资源会泄漏,因为Specialstring析构函数没被调用。

在这种情况中,Specialstring 的析构函数会被忽略,进而造成资源没能销毁。

  • polymorphic(带多态性质的)base classes应该声明一个 virtual析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性( polymorphically),就不该声明virtual析构函数。
    “ 给 base classes 一个 virtual 析构函数 ”,这个规则只适用于 polymorphic(带多态性质的)base classes身上。这种 base classes 的设计目的是为了用来“通过 base class 接口处理 derived class 对象”。。

条款08:别让异常逃离析构函数

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

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

    这个普通函数能保证反复调用。

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

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。

*条款10:令 operator= 返回一个 reference to this

注意,这只是个协议,并无强制性。如果不遵循它,代码一样可通过编译。然而这份协议被所有内置类型(见条款54)共同遵守。trl : :shared_ptr 或即将提供的类型(见条款54)共同遵守。因此除非你有一个标新立异的好理由,不然还是随众吧。

条款11:在 operator= 中处理“自我赋值”

“自我赋值”发生在对象被赋值给自己时:

a[i] = a[j]  //这里当 i = j时,就是自我赋值

如果遵循条款13和条款14的忠告,你会运用对象来管理资源,而且你可以确定所谓“资源管理对象”在copy发生时有正确的举措。这种情况下你的赋值操作符或许是“自我赋值安全的”(self-assignment-safe),不需要额外操心。然而如果你尝试自行管理资源(如果你打算写一个用于资源管理的 class 就得这样做),可能会掉进“在停止使用资源之前意外释放了它”的陷阱。假设你建立一个 class 用来保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap { ... };
class widget {
    ...
private:
	Bitmap* pb;	//指针,指向-一个从heap 分配而得的对象
};

下面是operator= 实现代码,表面上看起来合理,但自我赋值出现时并不安全(它也不具备异常安全性,但我们稍后才讨论这个主题)。

widget&
widget ::operator=(const widget& rhs)	//一份不安全的operator=实现版本.
{
	delete pb;						//停止使用当前的bitmap,
	pb = new Bitmap (*rhs.pb);		//使用rhs's bitmap 的副本(复件)。
	return *this;					//见条款10。
}

这里的赋值会出现问题,因为这样的赋值函数在开始复制之前,已经把左值的内部指针 delete。欲阻止这种错误,传统做法是藉由operator=最前面的一个“证同测试(identitytest)” 达到 “自我赋值” 的检验目的:

Widget& widget : :operator= (const widget& rhs)
{
	if (this -= &rhs) return *this;  //证同测试((identity test):如果是自我赋值,就不做任何事。
	delete pb;
	pb = new Bitmap ( *rhs.pb);
    return *this;
}

这样做行得通,前一版operator=不仅不具备“自我赋值安全性”,也不具备“异常安全性”,这个新版本仍然存在异常方面的麻烦。更明确也说,如果"new Bitmap”导致异常(不论是因为分配时内存不足或因为 Bitmap 的 copy 构造函数抛出异常),widget最终会持有一个指针指向一块被删除的Bitmapo这样的指针有害。你无法安全地删除它们,甚至无法安全地读取它们。

关于“异常安全性”,下面这个版本的代码就可以导出:

widget& widget::operator= (const widget& rhs)
{
	Bitmap* porig = pb;		//记住原先的pb
	pb = new Bitmap (*rhs.pb) ;	//令pb指向 *pb 的一个复件(副本)
	delete porig;		//删除原先的pb
	return *this;
}

现在,如果"new Bitmap"抛出异常,pb(及其栖身的那个widget)保持原状。即使没有证同测试(identity test),这段代码还是能够处理自我赋值,因为我们对原 bitmap 做了一份复件、删除原 bitmap、然后指向新制造的那个复件。它或许不是处理“自我赋值”的最高效办法,但它行得通。

如果你很关心效率,可以把“证同测试(identity test)“再次放回函数起始处。然而这样做之前先问问自己,你估计“自我赋值”的发生频率有多高?因为这项测试也需要成本。它会使代码变大一些(包括原始码和目标码)并导入一个新的控制流(control flow)分支,而两者都会降低执行速度。Prefetching、caching 和 pipelining 等指令的效率都会因此降低。

在 operator= 函数内手工排列语句(确保代码不但“异常安全”而且“自我赋值安全”)的一个替代方案是,使用所谓的 copy and swap 技术。这个技术和“异常安全性”有密切关系,所以由条款29详细说明。然而由于它是一个常见而够好的 operator= 撰写办法,所以值得看看其实现手法像什么样子:

class widget {
	...
	void swap (Widget& rhs); //交换*this和rhs 的数据;详见条款29
	...
};
widget& widget : :operator= (const widget& rhs)
{
	widget temp (rhs);	//为rhs数据制作一份复件(副本)
	swap(temp);			//将*this数据和上述复件的数据交换。
	return *this;
}

amazing!!!

条款12:复制对象时勿志其每一个成分

设计良好之面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),那便是带着适切名称的 copy构造函数和 copy assignment 操作符,我称它们为 copying 函数。条款5观察到编译器会在必要时候为我们的 classes 创建 copying 函数,并说明这些“编译器生成版”的行为:将被拷对象的所有成员变量都做份拷贝。

当一个类型中添加了新的成员变量,那么相应的他的 copying 函数也应该发生改动,虽然不改动并不会收到编译器的提示(假如这并不是编译器提供给你),对于 derived class 来说,可以利用 base class 的 copying 函数简化这一流程,具体来说:

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs) ,
	priority(rhs.priority) //调用base class 的copy构造函数
{
	logCall ("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer: : operator=(const PriorityCustomer& rhs)
{
	logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs); //对base class成分进行赋值动作
	priority = rhs.priority;
	return *this;
}
  • Copying 函数应该确保复制“对象内的所有成员变量”及“所有 base class 成分”。
  • 不要尝试以某个 copying 函数实现另一个 copying 函数。应该将共同机能放进第三个函数中,并由两个 copying 函数共同调用。

Resource Management

条款13:以对象管理资源

对于向计算机内存中申请的空间资源,应该注意在不使用时进行资源释放,比较传统的思路是使用工厂模式的设计模式,这样可以动态的控制对象的资源释放,在新的 C++ 语法中,有其他的方法可以替代实现——“智能指针”。auto_ptr 是“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。无论是由工厂设计模式还是类指针对象,都传递了一个思想“以对象管理资源”,其中包含几个关键的点

  • 获得资源后立刻放进管理对象(managing object)内。

    实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机(ResourceAcquisition Is Initialization;RAll),即无论以什么样的方式获取资源,都在获取后立即放入管理对象中。

  • 管理对象(managing object)运用析构函数确保资源被释放。

    不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。如果资源释放动作可能导致抛出异常,事情变得有点棘手,但条款8已经能够解决这个问题,所以这里我们也就不多操心了。

如果以这样的思路去控制对象资源的释放,只剩下一个问题,就是对管理对象的管理,这类对象在复制与拷贝时应该格外注意,因为同样的资源不应该被释放两次。

std::auto_ptr<Investment>pInv1 (createInvestment());  //pInv1指向createInvestment返回物.

std::auto_ptr<Investment> pInv2 (pInv1);	//现在pInv2指向对象,pInv1被设为null.
pInv1 = pInv2; //现在pInv1指向对象,pInv2被设为null.

如果使用常用的拷贝构造函数或者赋值函数,都会使原先的对象变为null,这并不符合许多 STL 库中的容器的定义。

auto_ptr 的替代方案是“引用计数型智慧指针”(reference-counting smart pointer;RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收(garbage collection),不同的是 RCSPs无法打破环状引用(cycles of references,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。

std::shared_ptr<Investment>pInv1 (createInvestment());  //pInv1指向createInvestment返回物.

std::shared_ptr<Investment> pInv2 (pInv1);	//现在pInv2和pInv1指向同一对象.
pInv1 = pInv2; 	//同上.

由于shared_ptr 的复制行为“一如预期”,它们可被用于STL容器以及其他“auto_ptr之非正统复制行为并不适用”的语境上。

auto_ptr 和 shared_ptr 两者都在其析构函数内做 delete 而不是 delete[] 动作(条款16对两者的不同有些描述)。那意味在动态分配而得的 array 身上使用 auto_ptr 或 shared ptr 是个馊主意。尽管如此,可叹的是,那么做仍能通过编译。在C++中,并没有特别针对“C++动态分配数组”而设计的类似 auto_ptr 或 shared _ptr 那样的东西,那是因为 vector 和 strin g几乎总是可以取代动态分配而得的数组。

最后,createInvestment() 返回的**“未加工指针”(rawpointer)**简直是对资源泄漏的一个死亡邀约,因为调用者极易在这个指针身上忘记调用delete。(即使他们使用 auto_ptr 或 shared ptr 来执行delete,他们首先必须记得将 createInvestment 的返回值存储于智能指针对象内。)为与此问题搏斗,首先需要对createInvestment进行接口修改,那是条款18面对的事。

条款14:在资源管理类中小心 coping 行为

条款13导入这样的观念:“资源取得时机便是初始化时机”(Resource AcquisitionIs Initialization; RAII),并以此作为“资源管理类”的脊柱,也描述了auto_ptr 和 shared_ptr 如何将这个观念表现在 heap-based 资源上。然而并非所有资源都是 heap-based,对那种资源而言,像auto_ptr和 shared_ptr 这样的智能指针往往不适合作为资源掌管者(resource handlers)。既然如此,有可能偶而你会发现,你需要建立自己的资源管理类。

如下就是一个管理 Mutex 变量的 Lock 类:

class Lock {
public:
	explicit Lock(Mutex* pm):mutexPtr(pm){ lock(mutexPtr);}//获得资源
    ~Lock() {unlock(mutexPtr);}//释放资源
private:
	Mutex *mutexPtr;
};

Lock 在创立时对目标互斥量进行控制,在析构时解除对应的控制。

但是如果该对象被复制,将可能出现不可预期的情况。因此需要通过别的手段来控制复制行为:

  • 禁止复制

  • 对底层资源使用**“引用计数法"**,例如 shared_ptr 或者 auto_ptr,值得注意的是,这类对象在析构时对底层资源的操作通常是delete,但是如上图所示的例子中,可能需要的是对底层资源(互斥量)的释放,因此在定义智能指针需要传入适当的删除器:

    class Lock{	
    public:
    	explicit Lock(Mutex* pm) //以某个Mutex初始化shared ptr,并以unlock函数为删除器
    	:mutexPtr(pm,unlock)
    	{
    		lock(mutexPtr.get());
    	}
    private:
    	std::shared ptr<Mutex> mutexPtr;//使用shared ptr替换raw pointer
    };
    
  • 复制底层资源:深度复制可以解决这种问题

  • 转移底部资源的控制权:类似 unique_ptr 的行为

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

  • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个**“取得其所管理之资源(get())”**的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

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

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

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

现在有如下的函数:

int priority() ;
void processWidget(std::tr1::shared_ptr<widget> pw,int priority);

由于谨记“以对象管理资源”(条款13)的智慧铭言,processwidget 决定对其动态分配得来的 widget 运用智能指针(这里采用std:: shared_ptr)。

现在考虑调用processwidget:

processwidget (new widget, priority() );

这种调用方式不能通过编译,虽然 shared_ptr 需要的是一个原始指针(raw pointer),但是这应该是一个显示调用(explict)的转换函数,无法进行进行隐式转换,应该通过如下的方式调用:

processWidget(std::shared_ptr<widget>(new Widget), priority());

虽然这种方式看上去已经不存在什么问题了,但实际上这种方式依然有可能造成资源泄露。具体原因可以表述如下,在 processWidget 函数被调用之前,编译器会首先核实其实参,那么对于此函数来说,由于实参是函数,因此这里牵扯到函数的执行顺序

于是在调用processwidget之前,编译器必须创建代码,做以下三件事:

  • 调用 priority 函数
  • 执行 new widget
  • 调用std::shared_ptr构造函数

C++ 编译器以什么样的次序完成这些事情呢?弹性很大。这和其他语言如 Java 和 C# 不同,那两种语言总是以特定次序完成函数参数的核算。可以确定的是"new widget”一定执行于std::shared _ptr构造函数被调用之前,但是关于 priority 函数的调用次序,可能是一个未知数,因此,最好使用如下的调用方式:

Widget* new_widget = new Widget();
processWidget(std::shared_ptr<widget>(new_widget), priority());
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值