读书笔记 Effective C++: 02 构造析构赋值运算
条款05:了解C++默认编写并调用的哪些函数
编译器会为class创建:
1. default构造函数(前提是:没有定义任何构造函数);
如果已经声明了一个构造函数,编译器就不会再创建default构造函数了;
2. 析构函数
3. copy构造函数;
对于指针,只拷贝地址,并不会重建内容,所以要注意double free;
下面是一段错误的代码:
class TestDoubleFree
{
public:
explicit TestDoubleFree(char c)
: pTest(new char(c))
{
}
~TestDoubleFree( )
{
if(pTest != NULL) {
delete pTest;
pTest = NULL;
}
}
private:
char* pTest;
};
int main( )
{
TestDoubleFree t1('x'); //这句本身并没有错
TestDoubleFree t2(t1); //但是这句错了,会引起double free
}
这是一段错误的代码,运行时会发生double free。main函数中,如果单独使用TestDoubleFree t1('x'),这并不会有问题。
问题在于TestDoubleFree t2(t1),copy构造函数会让t1.pTest和t2.pTest同时指向一块内存单元,那么t1,t2并析构的时候,就会对该内存单元发生double free。
解决这个问题:
1. 可以继承boost::noncopyable,这样就不允许使用TestDoubleFree t2(t1)语句。(例子见条款06)
2. 可以用boost::share_ptr替代裸指针;
#include <boost/shared_ptr.hpp>
class TestDoubleFree
{
public:
explicit TestDoubleFree(char c)
: pTest(new char(c))
{
}
~TestDoubleFree( )
{
}
private:
boost::shared_ptr<char> pTest;
};
int main( )
{
TestDoubleFree t1('x');
TestDoubleFree t2(t1);
}
4. copy assignment操作符;
copy构造函数一定会生成,但是copy assignment赋值符并不一定能默认生成。
如果class中含有reference成员变量,const成员变量,那么赋值运算无法完成;
#include <cassert>
class TestNonCopyAssignment
{
public:
TestNonCopyAssignment(char c, int i)
: ch(c), cInt(i)
{
}
void setChar(char c)
{
ch = c;
}
void testAssert(char c, int i)
{
assert(ch==c);
assert(cInt==i);
}
private:
char& ch;
const int cInt;
};
int main( )
{
TestNonCopyAssignment t1('x', 123);
TestNonCopyAssignment t2(t1);
t1.setChar('z');
t1.testAssert('z', 123);
t2.testAssert('z', 123);
t2 = t1; //赋值符号错误
}
由于TestNonCopyAssignment类含有reference和const,operator=被禁用,如果强行赋值,编译器会报错:
错误: 使用了被删除的函数‘TestDoubleFree& TestDoubleFree::operator=(const TestDoubleFree&)’
错误: ‘TestDoubleFree& TestDoubleFree::operator=(const TestDoubleFree&)’ is implicitly deleted because the default definition would be ill-formed:
错误: non-static reference member ‘char& TestDoubleFree::pTest’, can’t use default assignment operator
错误: non-static const member ‘const int TestDoubleFree::count’, can’t use default assignment operator
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
方法1:
在private中声明而不定义:copy构造函数,copy assignment操作符;
缺点:friend函数或者成员函数调用,这个错误不是出现再编译期,而是link期;
方法2:
继承boost::noncopyable类;
缺点:多重继承可能会阻止empty base class optimization
优点:醒目地标识该类不能被copy
实现条款05的例子:
#include <boost/noncopyable.hpp>
class TestDoubleFree : private boost::noncopyable
{
public:
explicit TestDoubleFree(char c)
: pTest(new char(c))
{
}
~TestDoubleFree( )
{
if(pTest != NULL) {
delete pTest;
pTest = NULL;
}
}
private:
char* pTest;
};
int main( )
{
TestDoubleFree t1('x');
TestDoubleFree t2(t1); //由于继承boost::noncopyable, 所以编译不会通过
}
条款07:为多态基类声明virtual析构函数
1. 具有polymorphic(多态)的base class,或者带有任何virtual函数的class,应该声明virtual析构函数;
避免局部析构;
2. 如果class设计的目的就不是作base class使用,或者不是为了具备多态,就不应该声明virtual析构函数。
节省存储空间;
3. 有时候为了得到一个抽象类,以避免该类被实例化,就把析构函数定义为pure virtual,但是必须为这个pure virtual析构函数提供一份定义,要不然link会出错的。
#include <iostream>
class Base
{
public:
virtual void print( )
{
std::cout << "Base Classes." << std::endl;
}
virtual ~Base( )=0;
};
// 析构函数先声明成pure virtual再定义
// 1. 如果不在外面定义,link会出错的
// 2. 这是一个小技巧,能阻止Base被实例化
Base::~Base( )
{
}
class DerivedA : public Base
{
};
class DerivedB : public Base
{
public:
void print( )
{
std::cout << "Derived Classes." << std::endl;
}
};
int main( )
{
Base* p = new DerivedA( );
p->print( );
delete p;
p = NULL;
p = new DerivedB( );
p->print( );
//由于p是Base*,所以如果Base的析构函数不是virtual的,就可能会导致Base的那部分析构了而Derived的那部分没能被析构
delete p;
p = NULL;
}
条款08:别让异常逃离析构函数
1. 析构函数绝对不要吐出异常。
如果某个语句在运行期间可能抛出异常,class应该提供普通函数执行该操作,而且该函数需要在析构函数之前被用户手动调用,而析构函数中包含这个普通函数只是起到双重保险的作用。
class DBConn
{
public:
//提供给客户使用,异常由客户处理
//这里谈论的是db.close()吐出异常的处理,不是db.close()关闭失败的处理,关闭失败是db.close()程序本身的错误
void close(){
db.close();
closed = true;
}
~DBConn(){
//如果客户没有处理,这里将代为处理,只是起到双保险的作用;但是原则上,是建议客户程序员处理。
if(!closed){
try{
db.close();
}catch(...){
// 默认的处理一般是直接结束程序,以避免错误传播
}
}
}
private:
DB db;
bool closed;
};
条款09:绝不在构造和析构过程中调用virtual函数
如果class中含有virtual函数,在构造函数中调用该函数,而此时派生类还没能创建,调用virtual函数不能正确指向派生类的函数。
析构函数也一样,派生类先析构,基类的析构函数调用virtual函数,此时vpt指向的函数已经析构了。
#include <iostream>
class Base
{
public:
Base( )
{
// 此时Derived Class还没有构建出来,所以调用的是Base Classes的print函数
print("Constructor");
}
virtual ~Base( )
{
// 此时Derived Class已经析构,所以调用的是Base Classes的print函数
print("Destructor");
}
virtual void print(std::string str)
{
std::cout << "Base Classes: " << str << std::endl;
}
};
class Derived : public Base
{
public:
Derived( )
{
print("Constructor");
}
~Derived( )
{
print("Destructor");
}
void print(std::string str)
{
std::cout << "Derived Classes: " << str << std::endl;
}
};
int main( )
{
Derived dd;
}
运行结果:
Base Classes: Constructor
Derived Classes: Constructor
Derived Classes: Destructor
Base Classes: Destructor
条款10:令operator=返回一个reference to *this
class Widget{
public:
// 也适用于+=,-=, *=
Widget& operator=(const Widget&){
......
return *this;
}
};
这样做的好处是可以连续赋值;
如:x = y = z = 15;
会被正确的解析为:x = (y = (z = 15));
条款11:在operator=中处理“自我赋值”
版本1:
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs)return *this;
delete pb;
try{
pb = new Bitmap(*rhs.pb);
}catch(...){
}
return *this;
}
版本1的缺点是不具备异常安全性:如果new失败了(内存不足或者Bitmap的copy构造函数出现异常),而旧的pb又已经delete了,这个对象就成了一颗地雷(一踩就崩;即使不踩,析构它也可能会因为delete pb而崩;就算使用pb之前判断pb!=NULL,程序也会进入不稳定的状态)。
版本2:
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs)return *this; //可加可不加,需要权衡。加了,如果自我赋值少,影响效率;不加,如果自我赋值多,由于后面要重建,也会影响效率。
Bitmap* pOrig = pb;
try{
pb = new Bitmap(*rhs.pb);
delete pOrig;
}catch(...){
throw "赋值失败了";
}
return *this;
}
版本2可以解决异常安全性,如果new失败了,此时pb依然指向原来的对象,这样程序还是可以继续运行的,只是进入赋值失败的异常分支,而不是像版本1那样只能退出了。
版本3:
class Widget{
void swap(Widget& rhs);
Widget& Widget::operator=(const Widget& rhs){
Widget tmp(rhs);
swap(tmp);
return *this;
}
};
使用copy and swap技术,能有效处理异常安全性。
版本4:
把版本3的operator=改为value传递
Widget& Widget::operator=(Widget rhs){
swap(rhs);
return *this;
}
版本4是版本3的简化版,而且可能会让编译器生成更高效的代码,但是牺牲了清晰性。
完整代码:
#include <iostream>
#include <exception>
class BitMap
{
public:
explicit BitMap(int ibm)
: ibm(ibm)
{
}
BitMap(const BitMap& bm)
: ibm(bm.ibm)
{
throw std::string("Test Exception");
}
int get( )
{
return ibm;
}
private:
int ibm;
};
class Widget
{
public:
explicit Widget(int ibm)
: pb(new BitMap(ibm))
{
}
~ Widget( )
{
delete pb;
}
Widget(const Widget& wg)
: pb(new BitMap(*wg.pb))
{
}
Widget& operator+=(const Widget& wg)
{
Widget tmp(wg);
std::swap(tmp.pb, pb);
return *this;
}
Widget& operator-=(Widget wg)
{
std::swap(wg.pb, pb);
return *this;
}
Widget& operator*=(const Widget& wg)
{
if(this == &wg) return *this;
BitMap* pOrig = pb;
pb = new BitMap(*wg.pb);
delete pOrig;
return *this;
}
// 错误的代码
Widget& operator/=(const Widget& wg)
{
if(this == &wg) return *this;
delete pb;
pb = new BitMap(*wg.pb);
return *this;
}
void print( )
{
std::cout << pb->get( ) << std::endl;
}
private:
BitMap* pb;
};
int main( )
{
Widget wg1(1);
Widget wg2(2);
wg1.print( );
wg2.print( );
try {
wg1 += wg2;
} catch(std::string& str) {
//赋值失败,但是原来的值没有修改
std::cout << str << std::endl;
wg1.print( );
wg2.print( );
}
try {
wg1 -= wg2;
} catch(std::string& str) {
//赋值失败,但是原来的值没有修改
std::cout << str << std::endl;
wg1.print( );
wg2.print( );
}
try {
wg1 *= wg2;
} catch(std::string& str) {
//赋值失败,但是原来的值没有修改
std::cout << str << std::endl;
wg1.print( );
wg2.print( );
}
try {
wg1 /= wg2;
} catch(std::string& str) {
//赋值失败,原来的值却成了个地雷
//不仅状态不稳定,而且会造成内存泄漏
std::cout << str << std::endl;
wg1.print( );
wg2.print( );
}
}
条款12:复制对象时勿忘其每一个成分
1. 不要copy构造函数和copy assignment函数应该确保copy“对象中的所有成员变量”,以及base classes成分
2. 不要用copy构造函数去实现copy assignment函数,也不要用copy assignment函数去实现copy构造函数;
copy构造函数是,创建并初始化一个新的对象,copy assignment函数是,在已初始化的对象上做处理;
如果两者有相同的代码,可以抽出来定义private函数;
class PriorityCustomer : public Customer{
public:
PriorityCustomer(const PriorityCustomer& rhs) : Custormer(rhs), priority(rhs.priority){
}
PriorityCustomer& operator=(const PriorityCustomer& rhs){
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
private:
int priority;
};
例:
#include <iostream>
#include <sstream>
#include <cassert>
class Base
{
public:
Base(int a, int b, int c)
: a(a), b(b), c(c)
{
}
protected:
int toInt( )
{
int tmp;
std::stringstream ss;
ss << a << b << c;
ss >> tmp;
return tmp;
}
private:
int a;
int b;
int c;
};
class Derived : public Base
{
public:
Derived( )
: Base(1, 1, 1), d(1), e(1)
{
}
Derived(int a, int b, int c, int d, int e)
: Base(a, b, c), d(d), e(e)
{
}
Derived(const Derived& derived)
: Base(derived), d(derived.d), e(derived.e)
{
}
Derived& operator=(const Derived& derived)
{
Base::operator=(derived);
d = derived.d;
e = derived.e;
return *this;
}
int toInt( )
{
int tmp;
std::stringstream ss;
ss << Base::toInt( ) << d << e;
ss >> tmp;
return tmp;
}
private:
int d;
int e;
};
int main( )
{
Derived dd(1, 2, 3, 4, 5);
assert(dd.toInt( ) ==12345);
Derived ee(dd);
assert(ee.toInt( ) ==12345);
Derived ff;
assert(ff.toInt( ) ==11111);
ff = ee;
assert(ff.toInt( ) ==12345);
}