C++不允许改变引用的指向,如果类里面有一个引用成员,或内含const成员,当在进行类对象赋值的时候,编译器会拒绝。如果某个基类将拷贝构造函数声明为private,编译器将拒绝为其derived class生成一个拷贝操作符。
条款6:若不想使用编译器自动生成的函数,就该明确拒绝
如果要阻止一个类进行拷贝和赋值,一种方法,自己定义他们并设为private, 这可以阻止编译器生成公共的这些函数,防止在类外进行拷贝或赋值。但是成员函数和友元函数还可以调用,所以可以使用以下方法:
1 将其声明为private,但是不进行定义,这样成员函数与友元函数内进行拷贝或赋值,链接器会发生错误,这将编译器错误移到链接器,在C++ iostream程序库中为了组织copying行为,使用的就是这个方法,将拷贝构造函数,赋值函数声明为私有,并且只声明,不进行定义。如果进行了这 些操作,则链接器发生错误返回。
2 如果将链接期的错误移至编译期,则可以构造一个专门为了阻止copying而设计的基类,且基类的析够函数不一定是virtual,不包含任何数据。然后使该类继承它,不一定要共有继承,可以私有继承。如下是两种方式:
//构造一个不能进行拷贝和赋值的类
//#include <iostream>
//using namespace std;
class HomeForSale
{
public:
HomeForSale() {}
private:
//以下两个函数只声明不定义,编译通过,如果不进行拷贝和赋值,
//则一起正常,否则链接器错误
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
//定义一个不能拷贝的类,允许继承对象构造和析构,不允许拷贝和赋值
class Uncopyable
{
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
//继承类继承它,则编译器会拒绝为其生成拷贝构造函数与赋值函数
class HomeForSale1 : private Uncopyable
{
public:
HomeForSale1() {}
};
int main()
{
HomeForSale1 h;
return 0;
}
条款07:为多态基类声明virtual析够函数
C++标准库的容器:vector, list, set, map都不含有任何虚函数,所以不应该继承任何一个容器,否则会有内存泄漏。当一个类的目前不是是作为基类使用,或不是为了具备多态,就不应该将析构函数声明为虚的。
如果一个类的目的是用作基类,并且含有虚函数,应该为其定义虚析够函数,这样当一个基类指针通过new指向继承对象时,当删除这个基类时,会调用继承类的 析构函数,防止内存泄漏。也可以将其声明为虚基类,析构函数声明为pure virtual析构函数,但是为了编译器正常调用,需要在类外部提供一份纯虚析够函数的定义。
条款08:别让异常逃离析够函数
析够函数绝对不要吐出异常。如果一个被析够函数调用的函数可能抛出异常,析够函数应该能捕捉异常,然后吞下它们(不传播)或结束程序。如以下,
//不要让异常逃离析构函数
class DBConnection
{
public:
static DBConnection create();
void close();
};
//定义一个类管理DBConnection对象,如果close()抛出异常,应该捕捉它,
//不应该让它逃离,捕捉异常后,记录调用失败,然后 1 程序结束 2 吞下异常,
class DBConn
{
public:
~DBConn()
{
try
{
db.close();
}
catch(...) //捕获所有异常
{
//记录异常
std::abort(); //可以调用该函数直接结束程序,或不结束。
}
}
private:
DBConnection db;
};
//一个较好的是重新设置DBConn接口,提供一个接口使客户可以有机会处理出现的问题
class DBConn
{
public:
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed)
{
try
{
db.close();
}
catch(...)
{
//纪录调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
上述的重写设计DBconn的方法,是可以给客户一个机会自己处理可能出现的问题,所以需要提供一个close接口,使客户显示调用来关闭数据库,但如果 他们没有调用close()而依赖析构函数的调用,则如果错误发生,则又成为析构函数处理这个异常了。一个原则就是如果某个操作可能在失败的时候抛出异 常,而又存在某种需要处理该异常,这个异常必须来自析构函数以外的某个函数。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,class应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09:绝不要在构造和析够函数过程中调用virtual函数
如以下继承体系:
//如有以下继承体系,希望每创建一个交易对象,都会有一笔日志记录
class Transaction
{
public:
Transaction()
{
logTransaction();
};
virtual void logTransaction() const = 0;
};
//继承类,需要实现logTransaction()
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
//继承类,需要实现logTransaction()
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
原意是希望借助构造函数在每次构造一个继承类对象时,就进行一笔日志记录,这样的行为就像我正在构造继承类对象的基类部分,自己还没有构造完成,然后又下 降到继承类去调用继承类的一个函数,而现在还没有继承类,所以不可能调用继承类的,对象只是基类对象,所以只会调用基类的log处理,并不能实现所期盼的 行为。
base class构造期间virtual函数绝不会下降到derived class。所以对象的行为就像隶属基类一样。即在基类构造期间,virtual函数不是虚函数。在继承类对象的基类对象构造期间,对象的类型是基类,而 不是继承类,如果这时调用virtual或使用运行其类型信息(dynamic_cast和typeid),也会把对象视为基类类型。析构函数也如此,一 旦继承类的析构函数开始执行,继承类成员变量便呈现未定义值,进入基类析够函数后对象就成为一个base class对象,虚函数与dynamic_casts也只能将该对象视为基类对象。
如果要确保每次一有Transaction继承体系上对象被创建,就会有适当的logTraction被调用的方法,实现上面的需求,一种方法是可以在基类内将logTransaction更改为非虚,然后要求继承类构造函数传递必要的信息给基类构造函数:
//解决方案:logTransaction改为非虚,然后继承类构造函数传递必要的信息给基类构造函数
class Transaction
{
public:
explicit Transaction(const std::string& logInfo)
{
logTransaction(logInfo);//将继承类信息传递过来
}
void logTransaction(const std::string& logInfo) const; //非虚
};
class BuyTransaction : public Transaction
{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters))
{}
private:
//静态的放置“初期未成熟的buytransaction对象内尚未初始化的成员变量”
static std::string createLogString(parameters);
};
上面的方法就是,如果无法使用虚函数从基类向下调用,则可以藉由“令derived classes将必要的构造函数信息向上传递止base class构造函数”来加以弥补。
请记住:在构造和析构期间不要调用virtual函数,因为这类调用不会下降至继承类(比起当前执行构造函数和析构函数那层)
条款10:令operator=返回一个reference to *this
为了实现连锁赋值,(x=y=z=15),赋值操作符必须返回一个reference指向操作符的左侧实参(为啥呀。。)。
这是一个协议,也适用于赋值相关运算(+=等), 并不一定到遵守,但是如果不这么写也可以通过编译,。但标准库类型string, vector,complex,tr1::shared_ptr等共同遵守。
条款11:令operator=中处理自我赋值
处理自我赋值有3中解决方案:1 证同测试,2 合理安排语句顺序保证代码的异常安全 3 copy and swap, 如以下例子
//在operator=中处理自我赋值
class Bitmap {};
//保存一个指针指向一块动态分配的bitmap
class Widget
{
public:
//问题的发生:
Widget& operator=(const Widget& rhs)
{
delete pb;
//如果rhs与this指向同一块内存则错误
pb = new Bitmap(*rhs.pb);
return *this;
}
//解决方法1:证同测试,
Widget& operator= (const Widget& rhs)
{
if (this == &rhs)
return *this;
delete pb;
//这里可能出现异常问题,如果new发生异常(不论内存不足还是copy构造函数异常)
//widget会持有一个指针指向一块被删除的bitmap.
pb = new Bitmap(*rhs.pb);
return *this;
}
//解决方法2:精心安排语句顺序来保证“异常安全”,防止解法1的问题
Widget& operator=(const Widget& rhs)
{
//记住原先指针,再构造一个副本,然后再删除,即删除在构造之后
Bitmap* porg = pb;
pb = new Bitmap(*rhs.pb);
delete porg;
return *this;
}
//解法3:2的一个替代方案,即copy and swap技术
void swap(Widget& rhs)
{
//交换数据
}
Widget& operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp); //交换*this与temp的数据
return *this;
}
//3的另一个变型解法,依赖以下事实(1) 某类的拷贝赋值操作可能被声明为"by value"方式接受实参
//(2)以by value方式传递东西会造成另一份副本
Widget* operator=(Widget rhs) //这里利用by value构造一个副本
{
swap(rhs); //这里是将*this与副本数据互换,
return *this;
}
private:
Bitmap* pb; //指向一个从堆上分配的对象
};
在最后一个变型解法中实际上是将在函数内调用赋值构造函数的时机转移到了参数构造阶段来构造一个副本,利用编译器的特性,会比较高校,但是作者认为这个做法牺牲了清晰性。我也觉得2,3解法就够好了,3的变型可有可无。
条款12:复制对象时勿忘其每一个成分
当自己编写一个拷贝函数,要确保1 复制所有的local成员变量,2 调用所有base calsses内的适当的copy函数。
不要在赋值函数里调用拷贝构造函数,因为拷贝构造函数的作用是构造对象,所以在赋值函数中调用赋值构造函数相当于重新构造一个已经存在的对象,这是荒谬的。
不要在拷贝构造函数里调用赋值函数,同样因为拷贝函数用来构造对象,而赋值函数作用于已初始化的对象身上,而在拷贝构造函数时对象尚未构造完成,所以就像在一个尚未初始化的对象身上做”只对已初始化对象才有意义“的事一样。
如果拷贝构造函数与拷贝赋值函数有相似的代码,可以建立一个新的成员函数,通常是private且命名为init,然后令二者调用该函数,这就可以消除重复的代码了。