第二章:构造/析构/赋值运算
了解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指向操作符的左侧实参。