C++ effective 2.构造/析构/赋值运算

来源:互联网 发布:plc编程一个月多少钱 编辑:程序博客网 时间:2024/04/29 18:05
二.构造/析构/赋值运算




条款05:了解C++默默编写并调用哪些函数


如果你自己美声明,编译器就会为类声明(编译器版本的)一个拷贝构造函数,一个拷贝赋值操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会成为你声明一个默认构造函数。所有这些函数都是public且inline。
惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,如调用基类和非静态成员变量的构造函数和析构函数等操作的代码。
注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数(这种情况下这个函数的虚属性;virtualness;主要来自base class)。
至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
如一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。 


copy 构造函数的用法:
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;
};

NamedObject<int> nol("small",2);
NamedObject<int> no2(no1);
这里,编译器对于string的namevalue会调用string的拷贝构造函数,对于int的objectValue拷贝no1.objectValue内的每一个bits来完成初始化。


copy assignment操作符,行为与copy 构造函数基本一致。
对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为缺省的copy assignment操作符会使指针指向同块内存,是个潜在危险;引用不可改变;常量不可改变。如下:

template<typename T>
class Nameobject{
public:
NameObject(const std::string & name, const T& value);//不再接受const的实参,因为name是个reference-to-non-const string。先前的
                                                                                       //char*构造函数不能定义,因为成员变量namevalue是个string &,必须有string可供指涉。
...
private:
std::string& nameValue;//如今是个reference
const T objectValue;//是个const
};
std::string newDog("Pers");
std::string oldDog("dew");
NamedObject<int> p1(newDog,2);
NamedObject<int> p2(oldDog,23);
p1=p2; //error! 因为p1.nameValue是个引用,p1.objectValue是个const,C++不允许reference改指向不同的对象,不允许const对const成员赋值。


如果某个base class将copy assignment操作符声明为private,编译器将拒绝为其derived class生成一个copy assignment操作符。毕竟编译器为derived class生成的copy 
assignment操作符想象中可以调用base class中的copy assignment操作符,但实际上derived class无法调用base class中的private成员。


请记住:
编译器可以自动为class创建default构造函数,copy构造函数,copy assignment操作符,以及析构函数。




条款06:若不想使用编译器自动生成的函数,就该明确拒绝
通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需
要的时候调用它们。可以将拷贝构造函数或拷贝赋值操作符声明为private,阻止编译器暗自创建其专属版本,而由于是private,使得你可以成功阻止人们在外部调用。但是类中的成员函数和友元函数还是可以调用private函数。但可以将成员函数声明为private而且故意不实现它们来防止类中的成员函数和友元函数还是可以调用,因为如果调用会在连接期报错。该小伎俩就被用在C++ iostream程序库中阻止copying行为。
将连接器错误转移至编译期,只要将copy构造函数和copy assignment操作符声明为private就可以办到,但不是在类本身上,而是在一个专门阻止copying动作而设计的base class
内。如下:
class Uncopyable{
protected:
Uncopyable(){}//允许derived对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable &);//但阻止copying
Uncopyable & operator=(const Uncopyable&);
};


class HomeForSale:private Uncopyable{ //用private关系继承,因为不是 is-a的关系,而是implemented-in-terms-of的关系
...
};
编译器为HomeForSale生成的copying函数会尝试调用其base class的copying函数,那些调用会被编译器拒绝,因为其base class的copying是private。




请记住:
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的基类也是一种做法。




条款07:为多态基类声明virtual析构函数
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——基类成分通常会被销毁,而derived class的成分(也
就是声明于derived class内的成员变量)没被销毁。这可能形成资源泄漏、败坏之数据结构、在调试器上消费许多时间。
消除以上问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会删除整个对象包括derived class的成分。
任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。

如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。


如果需要一个抽象类,但是没有纯虚函数,可以将将希望成为抽象类的那个class声明一个纯虚析构函数。


给base class一个virtual的析构函数,这个规则只适用于polymorphic(带多态性质的) base class身上。并非所有base class的设计目的都是为了多态用途,某些base class不是为了多态,如上面的Uncopyable的设计目的是为了来实现某个类的部分功能。


请记住:


带有多态性质的基类应该声明一个virtual析构函数。如果一个类带有任何virtual函数,它就应该拥有一个virtual析构函数。
一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。




条款08:别让异常逃离析构函数
如果你创建一个对象,在销毁时析构函数出现异常,那么可能会造成资源泄露或者并不明确行为。


如果析构函数导致异常:
1.如果抛出异常,就结束程序。(强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去。)
2.捕获异常,但什么也不做。   


如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。


请记住:
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。




条款09:决不让构造和析构过程中调用virtual函数
你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。


因为:基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的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;
创建b时,首先是base class构造,然后是derived class构造,调用的logTransaction是Transaction版本,不是BuyTransaction版本---即使目前建立的对象是BuyTransaction。


base class构造期间virtual函数绝不会下降到derived  class阶层。也就是说:在base class构造期间,virtual函数不是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 =中处理“自我赋值”
  Widget& Widget::operator=(const Widget& rhs) 
     {     
         delete pb;                               //这里对pb指向内存对象进行delete,试想 *this == rhs?情况会如何
         pb = new Bitmap(*rhs.pb);     //如果*this == rhs,那么这里还能new吗?“大事不妙”。
         return *this; 
      }
     也许以下代码能解决以上问题:
     Widget& Widget::operator=(const Widget& rhs)
     { 
         if (this == &rhs) 
         return *this;            //解决了自我赋值的问题。


         delete pb; 
         pb = new Bitmap(*rhs.pb); //但是如果new失败,return *this将返回一个呗删除的对象
         return *this; 
     }
“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码。”,如以上代码。解决方法如下:
    Widget& Widget::operator=(const Widget& rhs)
     { 
         Bitmap *pOrig = pb;                //记住原先的pb
         pb = new Bitmap(*rhs.pb);      //令pb指向*pb的一个复本
         delete pOrig;                           //删除原先的pb
         return *this;  //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
     } 
另外一个方法是使用copy and swap技术。如下:
  Widget& Widget::operator=(const Widget& rhs) 
     {     
        Weight temp(rhs);   
swap(temp);
        return *this; 
      }




请记住:
确保当对象自我赋值时operator =有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。   




条款12:复制对象时勿忘其每一个成员
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符),否则出现局部拷贝。


任何时候只要你承担起“为derived class撰写copying函数”的重大责任时,必须很小心地复制其base class成分。那些成分往往是private,所以你无法直接访问它们,应该让


derived class的copying函数显示调用相应的base class函数。如下:
Derived::Derived(const Derived & rhs):Base(rhs),deri(rhs.deri){//其中deri为Derived的成员变量。
...
}
Derived& Derived::operaor=(const Derived & rhs){
public:
Base::operator=(rhs);
deri=rhs.deri;
return *this;
}


当编写一个copying函数,请确保:    
(1)复制所有local成员变量;
(2)调用所有基类内的适当copying函数。 




如果想要避免copy构造函数和copy assignment操作符之间的代码重复,但copy构造函数和copy assignment操作符之间不能相互调用:
1.copy assignment操作符不能调用copy构造函数,因为这就像试图构造一个已经存在的对象。
2.copy构造函数不能调用copy assignment操作符,因为构造函数用来初始化新对象,而copy assignment操作符只施行于已存在的对象,不能对一个尚未构造好的对象赋值。
解决办法:建立一个新的成员函数给两者调用。这样的函数往往是private的。


请记住:
Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成员”;
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
原创粉丝点击