第二章:构造/析构/赋值运算
了解C++默默编写并调用哪些函数
在C++处理过一个空类之后,它就不是个空类了。如果你没有生硬,编译器就会自动声明一个复制构造函数,一个复制分配操作符和一个析构函数;并且,如果没有构造函数,编译器也会自动声明一个默认构造函数。所有的这些函数都是public且inline。
如果写下了
class Empty{};
编译器自动帮你:
class Empty{
public:
Empty(){...}
Empty(const Empty& rhs){...}
~Empty(){...}
Empty& operator=(const Empty& rhs){...}
};
同时,在没有定义赋值运算符的类中,改变引用对象的值是错误的。
template<class T>
class NamedObject{
public:
//以下构造函数如今不再接受一个const名称,因为nameValue
//如今是个reference-to-non-const string
Namedobject(std::string& name,const T& value);
...
private:
std::string& nameValue;
const T objectValue;
}
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog,2);
NamedObject<int> s(oldDog.36);
p = s;
上述的对象赋值操作是错误的;因为类中的string类型是引用类型,而“p = s”操作试图改变一个引用类型的值,这在C++中是不允许的,因此C++的响应是拒绝编译那一行赋值动作;如果打算在一个拥有引用成员的类内支持赋值操作,必须自行定义赋值操作符。
若不想使用编译器自动生成的函数,就该明确拒绝
房地产中介会说,每一笔资产都是独一无二的,没有两笔完全相像。我们也认为怎么能够复制独一无二的东西呢?我们很乐意看到HomeForSale的对象拷贝动作以失败收场。
class HomeForSale{...};
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);
h1 = h2;
但实际上,上述操作是可以实现的。尽管在类中没有定义赋值操作符和拷贝构造函数,在编译时,编译器也会自动生成这类的函数,但在我们实际应用中是禁止该类操作进行的。
其中的一个解决办法就是将赋值构造函数和赋值操作符在private中声明,即:
class HomeForSale{
public:
...
private:
...
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
}
【注】此时成员函数和友元函数能够访问private内的两个函数,我们应该尽量避免,实在避免不了就轮到连接器发出抱怨了。
将连接期错误移至编译期是可能的,只要将拷贝构造函数和赋值操作符声明为private就可以了,但是是在一个专门为了阻止拷贝动作而设计的基类中。
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
}
class HomeForSale:private Uncopyable{
...
};
为多态基类声明virtual析构函数
基类中若不声明析构函数为virtual,则在派生类程序执行完毕后,无法执行派生类的析构函数。
欲实现virtual函数,对象必须携带某种信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常由vptr(虚函数表指针)指出。vptr指向一个由函数指针指向的数组,称为vtbl(虚函数表):每一个带有virtual函数的class函数都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用函数取决于该对象的vptr所指的那个vtbl。
【注】不能一股脑儿的将所有析构函数定义为virtual,也不能完全不声明为virtual。
别让异常逃离析构函数
class Widget{
public:
...
~Widget(){...} //假设这个可能吐出一个异常
};
void dosomething(){
std::vector<Widget> v;
...
} //这里v会被自动销毁
当vector被销毁时,他应该销毁存储其中的所有Widget。假设总共有十个Widget,在销毁第一个时抛出了一个异常,在销毁第二个时也抛出了异常,此时对于C++来说异常过于多,则程序不是结束执行就是导致不明确行为;故C++不喜欢析构函数吐出异常。
【注】析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。
绝不在构造和析构过程中调用virtual函数
假设有一个class继承体系,用来塑模股市交易如买进、卖出的订单等等。这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录,如下:
class Traansaction{ //所有交易的基类
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;
...
}
现在执行:
BuyTransaction b;
此时会有BuyTransaction构造函数被调用,同时基类构造函数先于它被调用;并且,基类中的logTransaction被调用,此时的logTransaction版本属于基类而不属于BuyTransaction。这就说明,在基类构造期间,virtual函数不是virtual函数。
对此,有以下解决办法:
- 在class Transaction内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息为基类构造函数,而后那个派生类构造函数就可安全调用non-virtual logTransaction.
class Transaction{ public: explicit Transaction(const std::string& logInfo); void logTransaction(const std::string& logInfo) const; //如今是个non-virtual函数 ... }; Transaction::Transaction(const std::string& logInfo){ ... logTransaction(logInfo); //如今是个non-virtual函数 } class BuyTransaction:public Transaction{ public: //将log信息传给基类构造函数 BuyTransaction(parameters):Transaction(createLogString(parameters)){ ... } ... private: static std::string createLogString(parameters); }
令operator=返回一个reference to *this
即重载赋值操作符时候,返回一个reference指向操作符的左侧实参。
在operator=中处理“自我赋值”
“自我赋值”发生在对象被赋值给自己时:
class Widget {......};
Widget w;
...
w = w; //赋值给自己
此外,以下动作均为赋值操作:
- a[i] = a[j]; (若i=j)
- *px = *py;(若px和py指向同一个东西)
一般而言,如果某段代码操作指针或引用被用来“指向多个相同类型的对象”,就需考虑这些对象是否为同一个。实际上,只要两个对象来自同一个继承体系,他们甚至不需声明为相同类型就可能造成“别名”。如下:
class Base {...};
class Derived: public Base {...};
void doSomething (const Base &rb,Derived* pd);//rb和*pd可能是同一个对象
“在停止使用资源之前意外释放了它”的陷阱
class Bitmap {...};
class Widget {
..
private:
Bitmap* pb; //指针,指向一个从heap分配而得到的对象
}
以下是对应的operator=实现代码:
Widget& Widget::operator=(const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
此时看起来是没什么问题的,编译也能够通过;但是*this和rhs可能是同一个对象,那么在返回this之前,就已经把该指针删除,引起上述的陷阱。所以,传统的做法就是使用“证同测试”,即:
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
复制对象时勿忘其每一个成分
考虑用一个class来表现顾客,其中重载赋值操作符,并记录下来:
void logCall(const std::string& funcName);
class customer{
public:
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
}
Customer::Customer(const Customer& rhs):name(rhs.name){ //赋值rhs的数据
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs){
logCall("Customer copy assignment operator");
name = rhs.name; //赋值rhs的数据
return *this;
}
这里的每一件事情看起来都很好,而实际上每件事也确实很好,直到另外一个成员变量的加入:
class Date {...}; //日期
class Customer{
public:
... //同前
private:
std::string name;
Date lastTransaction;
}
以上既有copying函数执行的是局部拷贝:它们的确复制了顾客的name,但没有复制新添加的lastTransaction。如果需要为class添加一个成员变量,必须同时修改copying函数。