2.构造/析构/赋值运算
几乎你写的每个类都会有一或多个构造函数、一个析构函数、一个拷贝赋值操作符。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及整个类。所以确保它们行为正确时生死攸关的大事。
条款05:了解C++默默编写并调用哪些函数
如果你自己没声明,编译器就会为类声明(编译器版本的)一个拷贝构造函数,一个拷贝赋值操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会成为你声明一个默认构造函数。所有这些函数都是public且inline。
惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数(要不然它们该在哪里被调用呢??)。
注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数。
至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
如一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。
编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。(即深拷贝和浅拷贝问题)
请记住:
- 编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符,以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。
由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。通过这个小“伎俩”可以阻止在外部调用它。代码如下:
class HomeForsale{
public:
........
........
private:
HomeForsale(const HomeForsale&);
HomeForsale& operator=(const HomeForsale&);
}
当通过外部拷贝HomeForsale对象时,编辑器会出错,因为声明为了私有。但是类中的成员函数和友元函数还是可以调用private函数。解决方法可能是在一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。代码如下:
class Uncopyable{ //允许继承的对象构造和析构
protected:
Uncopyable() {};
~Uncopyable() {};
private:
Uncopyable(const Uncopyable&); //但是阻止copying
Uncopyable& operator=(const Uncopyable&);
}
//只要继承该类
class HomeForSale:private Uncopytable //不再声名拷贝构造函数和拷贝运算符
{
........
}
任何尝试拷贝HomeForSale对象,编辑器都会报错,因为会尝试调用基类的拷贝构造函数或赋值运算符,而这些被声明为私有。
请记住:
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像noncopyable这样的基类也是一种做法。
条款07:为多态基类声明virtual析构函数
当基类的指针指向派生类的对象的时候,当我们使用完,对其调用delete的时候,其结果将是未有定义——基类成分通常会被销毁,而派生类的充分可能还留在堆里。这可是形成资源泄漏、败坏之数据结构、在调试器上消费许多时间。
消除以上问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会如你想要的那般。
如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。。
许多人的心得是:只有当class内含有至少一个virtual函数才为它声明virtual析构函数。
STL容器都不带virtual析构函数,所以最好别派生它们。
请记住:
- 带有多态性质的基类应该声明一个virtual析构函数。如果一个类带有任何virtual函数,它就应该拥有一个virtual析构函数。
- 一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。
如果可能导致异常:
- 如果抛出异常,就结束程序。(强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去。)
- 捕获异常,但什么也不做。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
请记住:
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09:决不让构造和析构过程中调用virtual函数
你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。
如下代码:
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;
......
};
当以下这行现在执行,会发生什么:
BuyTransaction b;
Transaction构造函数的最后一行调用virtual函数logTransaction,这时被调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本!!!
在derived class对象的base class构造期间,对象的类型是base class 而不是derived class。不止virtual函数会被编辑器解析成base class,若使用运行期类型信息(RIIT,例如:dynamic_cast和typeid),也会把对象视为base class类型。上面的例子:这个对象内的“BuyTransaction 专属成分”尚未被初始化,视为不存在。对象在derived class构造函数开始执行前不会成为一个derived class对象。
派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?
析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?
请记住:
- 在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。
条款10:令operator= 返回一个reference to *this
对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:
int x, y, z;
x = y = z = 15;
为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
即:
Widget & operator = (const Widget &rhs)
{
...
return *this;
}
所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。
请记住:
- 令赋值操作符返回一个reference to *this。
条款11:在operator =中处理“自我赋值”
条款11在我看c++primer plus中就已经重点提及到,里面的operator =都处理了"自我赋值"
先举几个自我赋值的例子:
例:Widget w;
w = w;
a[i] = a[j]; //i == j or i != j
*px = *py;// px,py指向同个地址;
以上情况都是对“值”的赋值,但我们涉及对“指针”和“引用”进行赋值操作的时候,才是我们真正要考虑的问题了。
看下面的例子:
Widget& Widget::operator=(const Widget& rhs)
{
delete pb; //这里对pb指向内存对象进行delete,试想 *this == rhs?情况会如何
pb = new Bitmap(*rhs.pb); //如果*this == rhs,那么这里还能new吗?“大事不妙”。
return *this;
}
也许以下代码能解决以上问题:(C++primer plus中就已经重点提及到的版本)
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this; //解决了自我赋值的问题。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码。”,以上代码同样存在异常安全问题。(即如果new Bitmap产生异常)
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个复本
delete pOrig; //删除原先的pb
return *this; //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
}
请记住:
- 确保当对象自我赋值时operator =有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成员
还记得条款5中提到编译器在必要时会为我们提供拷贝构造函数和拷贝赋值函数,它们也许工作的不错,但有时候我们需要自己编写自己的拷贝构造函数和拷贝赋值函数。如果这样,我们应确保对“每一个”成员进行拷贝(复制)。
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。
在派生类的构造函数,拷贝构造函数和拷贝赋值操作符中应当显示调用基类相对应的函数,否则编译器可能又“自作聪明了”。
当你编写一个copying函数,请确保:
(1)复制所有local成员变量;
(2)调用所有基类内的适当copying函数。
代码如下:
class Customer
{
public:
...
Customer(const Customer& rhs):m_name(rhs.m_name){}
Customer& operator=(const Customer& rhs)
{
name=rhs.name;
return *this;
}
...
private:
std::string m_name;
}
错误的代码:
class PriorityCustomer:pulic Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs):m_priority(rhs.m_priority)
{
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
m_priority=rhs.m_priority;
return *this;
}
...
private:
int m_priority;
}
代码看起来没有问题,但是,却忽略了baseclass的成分 !
正确如下:
class PriorityCustomer:pulic Customer
{
public:
...
PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),m_priority(rhs.m_priority)
{
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
Customer::operator=(rhs); //调用父类的
m_priority=rhs.m_priority;
return *this;
}
...
private:
int m_priority;
}
调用所有基类内的适当copying函数,Customer::operator=(rhs);
请记住:
- Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成员”;
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
参照博客:https://blog.csdn.net/zyq522376829/article/details/48179163