Effective C++ 读书笔记(二)
2 构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
编译器可以暗自为class创建 default构造函数,copy 构造函数,copy assignment操作符,以及析构函数。
编译器产生的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
class Empty{};
好像你写下如下代码:
class Empty{
public:
Empty(){..}
Empty(const Emtpy& rhs){…}
~Empty(){…}
Empty& operator=(const Empty& rhs){…}
};
template<typename T>
class NameObject{
public:
NameObject(const char* name, const T& value);
NameObject(const std::string& name, const T& value);
…
private:
std::string nameValue;
T objectValue;
};
因为已经为NameObject声明了一个构造函数,编译器于是不再为它创建default构造函数。但NameObject没有声明copy构造函数和copy assignment操作符,所以编译器会为它创建那些函数。
template<typename T>
class NameObject{
public:
NameObject(const std::string& name, const T& value);
…
private:
std::string& nameValue;
const T objectValue;
};
std::string newDog(“Persephone”), oldDog(“Satch”);
NameObject<int> p(newDog, 2);
NameObject<int> s(oldDog, 36);
p = s; //c++ 拒绝编译。
若要在一个“内含ref成员”的class内支持赋值操作,必须自己定义copy assignment操作符。同理,“内含const成员”。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
如果你不声明copy构造函数或copy assignment操作符,编译器可能为你产出一份,于是你的class支持copying。但所有编译器产出的函数都是public,为阻止这些函数被创建出来,可以将copy构造函数或copy assigement操作符声明为private。将成员函数声明为private而且故意不实现它们这一会使为大家所接受。
class HomeForSale{
public:
…
private:
…
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
将连接期错误移至编译期是可能的,只要将copy构造函数和copy assignment操作符声明为private就可办到,但不是在HomeForSale自身,而是在一个专门为了阻止copying动作而设计的base class内。
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator = (const Uncopyable&);
};
class HomeForSale: private Uncopyable{
…
};
只要任何人——甚至是member函数或friend函数——尝试拷贝HomeForSale对象,编译器便试着生成一个copy构造函数和一个copy assignment操作符,这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,最终会被拒绝。
条款07:为多态基类声明virtual析构函数
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果没有定义——实际执行时通常发生的是对象的derived成分没有被销毁。消除这个问题的做法是给base class一个virtual析构函数。
如果class不含virtual函数,通常表示它并不意图被用做一个base class。例如:标准string不含任何virtual函数,但有时程序员错误地把它当做base class:
class SpecialString: public std::string{
…
};
SpecialString *pss = new SpecialString(“Impending Doom”);
std::string* ps;
…
ps = pss;
…
delete ps; //SpecialString资源会泄漏,因为SpecialString析构函数没被调用。
有时令class带一个pure virtual析构函数,可能颇为便利。
class AWOV{
public: virtual ~AWOV() = 0;
};
这个class有一个pure virtual函数,所以它是个抽象class,又由于它有个virtual析构函数,所以不担心析构函数的问题。编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以必须为这个函数提供一份定义。
AWOV::~AWOV() { }
n 带多态性质的 base class应该申明一个virtual析构函数,如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
n classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不应该声明virtual析构函数。
条款08:别让异常逃离析构函数
C++不禁止析构函数吐出异常,但它不鼓励这样做。但如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,怎么办??
class DBConnection{
public:
…
static DBConnection create();
void close(); //关闭联机,失败抛异常
};
class DBConn{
public:
~DBConn() //确保数据库连接总是会关闭
{
db.close();
}
private:
DBConnction db;
};
两个办法:
n 若抛出异常就结束程序。
~DBConn()
{
try { db.close(); }
catch( … ){
制作运转记录,记下对close的调用失败
std::abort();
}
}
n 吞下异常。
~DBConn()
{
try { db.close(); }
catch( … ){
制作运转记录,记下对close的调用失败
}
}
更好的策略是把close的责任从DBConn析构函数移到 DBConn客户手上。
class DBConn{
public:
void close() //供客户使用的新函数
{
db.close();
closed = true;
}
~DBConn() //确保数据库连接总是会关闭
{
if (!closed){
try { db.close(); }
catch( … ){
制作运转记录,记下对close的调用失败;
//若关闭动作失败,记录下来结束程序或吞下异常
…
}
}
}
private:
DBConnction db;
};
条款09:绝不在构造和析构过程中调用virtual函数。
这是C++与Java和C#不相同的一个地方。
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;
…
};
Transaction::Transaction(){
…
logTransaction();
};
class BuyTransaction:public Transaction{
public: virtual void logTransaction () const;
};
class SellTransaction:public Transaction{
public: virtual void logTransaction () const;
};
Transaction构造函数最后一行调用virtual函数logTransaction,这正是引发惊奇的起点。这时候调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本——即使当前即将建立的对象类型是BuyTransaction。
在derived class对象的base class构造期间,对象类型是base class而不是derived class, 因为对象内的“BuyTransaction专属成分”尚未被初始化。相同的道理适用于析构函数。
下面的代码与上面的结果是一样的,都调用基类的logTransaction。
class Transaction{
public:
Transaction(){init();}
virtual void logTransaction() const = 0;
void init();
…
};
Transaction::init(){
…
logTransaction();
};
解决方法:
class Transaction{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const ;
…
};
Transaction::Transaction(const std::string& logInfo){
…
logTransaction(logInfo);
};
class BuyTransaction:public Transaction{
public: BuyTransaction (parameters): Transaction(createLogString(parameters)){};
};
条款10:令operator返回一个ref to *this
int x, y, z;
x = y = z = 15;
为了实现连锁赋值,赋值操作符必须返回一个ref。
class Widget{
public:
Widget& operator=(const Widget& rhs){
…
return *this;
}
};
同理:
class Widget{
public:
Widget& operator+=(const Widget& rhs){
…
return *this;
}
};
条款11:在operator=中处理“自我赋值”
自我赋值发生在对象被赋值给自己时:
class Widget{};
Widget w;
w = w;
另外的赋值不总是一眼看出来:如a[i] = a[j], i == j。或*px = *py,但px和py指向同一个东西。
自我赋值是危险的:
class Bitmap{};
Class Widget{
…
private:
Bitmap* pb;
};
Widget& Widget::operator = (const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
若是自我赋值,delete不只销毁了当前对象的bitmap,也销毁了rhs的bitmap。
Widget& Widget::operator = (const Widget& rhs)
{
if (this == &rhs)return *this; // 证同测试,自我赋值,什么也不做。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但这个版本存在异常麻烦,若new Bitmap导致异常,Widget最终会持有一个指向一块被删除的Bitmap。
Widget& Widget::operator = (const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
若很关心效率可以把证同测试放回函数起始处。
在operator=函数中手工排列语句确保异常安全和自我赋值安全的一个替代方案是,使用copy和swap技术。
Class Widget{
…
void swap(Widget& rhs);
private:
Bitmap* pb;
};
Widget& Widget::operator = (const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
条款12:复制对象时勿忘其每一个成分
1, copying函数应该确保复制对象内的所有成员变量及所有base成分
class Date{};
class Customer{
public:
…
private:
std::string name;
Date lastTransaction;
};
class PriortyCustormer: public Customer{
public:
PriortyCustormer(const PriortyCustormer& rhs);
PriortyCustormer& opernator = (const PriortyCustormer& rhs);
private:
int priority;
};
PriortyCustormer:: PriortyCustormer(const PriortyCustormer& rhs) : customer(rhs), priority (rhs.priority) {}
PriortyCustormer& PriortyCustormer::operator=(const PriortyCustormer& rhs)
{
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
2, 不要尝试以某个copying函数实现另一个 copying函数,应该将共同机能放进第三个函数中,并由两个 copying函数共同调用。