条款十六:成对使用new和delete时要采取相同形式
string * stringArr=new string[100];
delete stringArr;
看起来好像没有问题,使用了new也使用了delete,但是程序有部分行为未定义,stringArr中有99个不太可能被正当删除。
当你使用new,有两件事发生:第一:内存被分配出来。第二针对此内存调用构造函数(一个或多个)。
当你使用delete,也有两件事发生:第一,针对此内存调用析构函数(一个或多个)。第二,释放内存。
delete最大的问题在于即将被删除的内存中究竟有多少个对象?这决定调用多少次析构。
其实很好解决,
string * stringArr=new string[100];
string * stringArr2=new string;
delete[] stringArr;
delete stringArr2;
delete加[],系统就会知道它是一个数组,释放多个对象,未加就只释放一个对象。
如果对stringArr2使用delete[],会发生什么事呢?delete会读取若干内存并将它解释为数组,然后开始多次调用析构。
如果对stringArr没有使用delete[]形式,可能会有太少的析构函数被调用。
总结:
- 如果在你使用new时使用[],那么delete也应该加[],若在使用new时没有加[],那么delete中也不加。
条款十七:以独立语句将newed对象置入智能指针
假设我们有个函数揭示优先级、另一个函数用在某动态分配的Widget上进行优先级的处理
int priority();
void processWidget(shared_ptr<Widget> pw,priority);
由于谨记以对象管理资源的条款,该处使用了智能指针,现在开始考虑调用processWidget
processWidget(new Widget,priority());
该调用方式不会通过编译。shared_ptr构造函数需要一个原始指针,但该构造函数是一个
explicit构造函数,不支持隐士转换
processWidget(shared_ptr<Widget> (new Widget),priority);
这样就可以通过编译,但上述方式尽管使用了智能指针管理资源,但仍然有可能造成资源泄露。
编译器产出一个processWidget调用码之前,首先要核算各个实参。第二个实参是对priority函数的调用,第一个shared_ptr由两部分组成
- 调用new Widget函数
- 调用shared_ptr构造函数
于是在调用processWidget函数之前,会做三件事情
- 调用priority函数
- 调用new Widget函数
- 调用shared_ptr构造函数
C++编译器以什么顺序完成这些事呢?首先new应该在shared_ptr构造函数之前,因为其是构造函数的参数。但priority函数调用时间可能是第一、第二或第三,假设以如下顺序执行
- 调用new Widget函数
- 调用priority函数
- 调用shared_ptr构造函数
假设priority函数抛出异常,那么new Widget返回的指针就会遗失,因为它尚未置入智能指针内,于是就发生了资源泄露。
避免这类问题的方法也很简单:使用分离语句,
shared_ptr<Widget>pw (new Widget);
processWidget(pw,priority);
这样就没有了重新排列的自由。
总结;
- 以独立语句将对象存储与智能指针中。如果不这样做,很可能有难以察觉的资源泄露。
条款十八:让接口容易被正确使用,不易被误用
理想上,如果客户使用某个接口却没有得到他所预期的行为,这个代码不应该通过编译。
假设我们设计一个用来表现日期的类
class Date
{
public:
Date(int month,int day,int year);
};
乍一看这个接口同情合理,但其客户容易至少犯下两个错误。第一,以错误的次序传参
Date(30,3,1995);//应该是 3,30
第二,他们可能传递一个无效的月份或天数
Date(2,30,1995); //2月没有30
许多错误可以因为导入新类型而被预防,类型系统可以有效地预防,让我们导入外覆类型来区别天、月和年,然后再Date中使用:
struct Day
{
explicit Day(int d):val(d){}
int val
};
struct Month
{
explicit Month(int m):val(m){}
int val
};
struct Year
{
explicit Year(int y):val(y){}
int val
};
class Date
{
public:
Date(const Month& m,const Day&d,const Year& y);
};
Date(30,3,1995); //错误,不正确的类型
Date(Day(30),Month(3),year(1995));//错误,不正确的类型
Date(Month(3),Day(30),year(1995));//正确
令上述结构体成为成熟且经充分锻炼的class要比简单的struct好。明智而谨慎的导入新类型对预防结构被误用有奇效。
一旦正确的类型就定位,限制其值是通情达理的。例如一年有12个月,所以month应该反映这一事实。办法之一是使用enum类型,但enum类型不具有类型安全性,例如enum可以被拿来当作一个ints使用。比较安全的方法是预先定义所有有效地月:
class Month
{
public:
static Month Jan(){return Month(1);}
static Month Feb(){return Month(2);}
...
static Month Dec(){return Month(12);}
private:
explicit Month(int m);//阻止生成新的月份
...
};
Date d(Month::Mar(),Day(30),Year(1995));
如果一函数替换对象,表现某个特定月份让你觉得诡异,可能是你忘记了非局部静态对象初始化次序可能有问题(条款4)。
预防客户错误的另一个方法是,显示类型内什么事可以做,什么事不能做。常见的是加上const。
除非有好的理由,否则应该尽量让你的类型行为与内置类型一致。客户知道int这样的类型有什么行为,你应该让你的类型在合理范围内也有相同表现。如对a*b复制不合理等,如果有怀疑,就拿出int做范本。
任何借口如果要求客户必须记得做那些事,就是有着不正确使用的倾向,因为客户可能会忘记。利用Investment的工厂函数。
Investment* createTnvestment();
为了避免资源泄露,该函数返回的指针必须被删除,那就有了两个错误的机会:没有删除,或者删除了多次。
较佳的方式是返回值直接使用智能指针,强迫用户使用智能指针。
shared_ptr<Investment> createTnvestment();
假设类设计这希望从上述类中取得的指针传递给一个名为getRidOfInvestment函数,而不是直接在获得指针上使用delete,那就又可能发生错误,该错误叫做企图使用错误的资源析构机制(拿delete替换getRidOfInvestment)。
createTnvestment类的设计者可以针对此问题先发制人:返回一个将getRidOfInvestment绑定为删除器的shared_ptr.shared_ptr提供的构造函数可以接受两个实参,一个是被管理的指针,另一个是引用次数为0时调用的删除器。这启发我们创建一个null shared_ptr,并以getRidOfInvestment作为删除器
shared_ptr<Investment> Pinv(0,getRidOfInvestment);//此式无法通过编译
shared_ptr坚持第一个参数必须是个指针,而0不是指针。但它可以被转为指针(可以直接传入nullptr)
shared_ptr<Investment> Pinv(static_cast<Investment*>(0),getRidOfInvestment);
这样就可以,那么createInvestment可以这样实现
shared_ptr<Investment> createTnvestment()
{
shared_ptr<Investment> Rev(static_cast<Investment*>(0),getRidOfInvestment);
Rev=...;
return Rev;
}
当然,如果被Pinv管理的指针可以在建立Pinv之前被确立,那么将原始指针传入为最佳。
shared_ptr有个很好的特性就是它会自动使用每个指针专属的删除器,消除 cross-DLL problem.这个问题发生于对象在动态链接库(DDL)中被new创建,却在另一个DDL中被销毁。shared_ptr不会有这个问题,因为它缺省的删除器是来自shared_ptr所产生的那个DLL的delete。
如果Stock派生自Investment,而createInvestment实现如下
shared_ptr<Investment> createTnvestment()
{
return shared_ptr<Investment> (new Stock);
}
返回的智能指针可以传给其他任意DLLs。这个指向“Stock的shared_ptr会最终记录当Stock引用次数为0该调用的那个DLLs的delete”。
总结
- 好的接口很容易被正确使用,不容易被误用。我们应该努力达到这点。
- 促进正确使用的方法包括接口一致性、以及内置类型的行为兼容。
- 阻止误用的方法包括建立新类型、限制类型上操作、束缚对象值、以及消除客户的资源管理责任。
- shared_ptr支持定制删除器,这可防范DLL问题,可被用来自动解除互斥锁(条款14)等。