Effective C++ ——构造/析构/赋值运算符

时间:2021-06-15 01:37:04

条款五:了解C++默认编写并调用那些函数

 

       是否存在空的类?

       假设定义类为class Empty{};当C++编译器处理过后会变成如下的形式:

class Empty{Empty(){}
~Empty(){}
Empty& operator=(const Empty& rhs){.....}
Empty(const Empty& rhs){.....}
}
       可以分别称为构造函数,析构函数,赋值构造函数,复制构造函数!惟有当这些函数被需要(被调用),它们才会被编译器创建出来。下面代码造成每一个函数被编译器产出:

Empty e1;//默认构造                //析构函数Empty e2(e1);//拷贝构造e2 = e1;//赋值操作符
       唯有这些函数被调用时,它们才会被编译器创建出来,且都是public && inline

       在有些情况下编译器会阻止产生赋值操作符和复制构造函数,例如当函数的成员变量中存在引用变量或者const变量的时候,因为此时编译器不知道怎样将右边的对象的相应成员赋值给左边的成员,此时要主要构造函数中对于const或者引用的成员一定要采用初始化列表的形式,不然会出错!例如对于下面的类将不会产生对应的赋值或者复制构造函数

class People{private:    const int age;    Addredd& addr;     .......public:    ......}

       如果某个base classes将赋值操作符声明为private,编译器将拒绝为其derived classes生成一个赋值操作符。原因为:编译器为derived classes所生成的赋值操作符想象中可以处理base classes成分,但它们无法调用derived classes无权调用的成员函数。编译器两手一摊,无能为力。


条款六:如果不想使用编译器自动生成的函数,就应该明确的拒绝


       编译器默认生成的函数都是public。为了阻止这些函数被创建出来,可以将(赋值操作符、拷贝构造)函数声明为private。但是一般而言这个做法并不安全,因为成员函数和友元函数还是可以调用你的private函数。解决办法我们只声明,不做任何的定义,这样如果对这些函数进行对应的调用一定会出现链接错误,例如:

class People{private:    People(const People&);    const People& operator=(const People&);    ......}

       还有一种做法就是写一个不能进行赋值或者复制的父类,然后继承它!例如:

class Uncopyable{protected:    Uncopyable(){}    //允许derived对象构造和析构    ~Uncopyable(){}private:    Uncopy(const Uncopyable&);   //阻止拷贝构造和赋值操作符    Uncopyable& operator=(const Uncopyable&);};
       为了阻止HomeForSale对象被拷贝,我们被唯一需要做的就是继承Uncopyable:

class HomeForSale:private Uncopyable  

       任何方式尝试拷贝 HomeForSale对象,编译器便试着生成一个拷贝构造函数和一个赋值操作符,这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,那么调用会被编译器拒绝,因为其base class的拷贝函数是private。


条款七:为多态的基类声明virtual析构函数

       使用非虚函数,编译器在解析的过程就会使用”早捆绑“的方式进行解析,意思就是在编译过程中,就确定好了调用哪个函数。反之,如果使用了虚函数,编译器就会使用”晚捆绑“的方式进行解析,这样在执行过程中,在调用虚函数的地方会进入一个查虚函数表的例程中,这个例程就会帮我们选择使用对应的函数方法。        在下面的这个例子中,编译器在执行到delete语句时,会认为b是一个Base对象,然后Base对象的析构函数为非虚函数,这样编译器就会采用”早捆绑“的方式,认为这里调用的其实就是Base的析构函数,这样Derive的析构函数就不会被执行,但是我们实际的意图是希望调用派生类中的析构函数。
class Base {};class Derive : public Base {};int main() {    Base *b = new Derive;    delete b;    return 0;}
       如果出现上面这种情况就容易发生局部析构的危险,当然在上面这个例子中是不会出现局部析构的危险了,但是如果在Derive中有一个指针指向了堆中的一块内存,此时如果仅仅调用Base类中的析构函数,则就不会执行这个Derive中的析构,这块内存就泄露了。这就是它最大的一个危害。        使用下面的这种方式就不怕出现上述的问题了,因为编译器会知道这个地方需要采用”晚捆绑“,这样在执行delete过程中,就会进入到虚函数表的查询例程中开始执行派生类中的析构函数。
class Base {public:    virtual ~Base() {} // 虚析构函数,确保了可以调用派生类中的析构};class Derive : public Base {};int main() {    Base *b = new Derive;    delete b;    return 0;}

       那什么时候需要使用virtual析构函数就很明显了:只要这个类有可能被作为基类而存在,那就它的析构函数必须是virtual的。当然书中也给出了一个相对容易判断的准则:只要出现了virtual关键字修饰的方法,就必须将析构设置为virtual的(这个道理很容易解释,因为我只要设计了virtual函数,那我就是希望可以有一个派生类继承这个类,所以析构就需要为virtual的了)。

       将一个类的析构函数设置为纯虚函数,它的应用场景就是:我希望一个类不能被实例化,但是我又不希望提供任何虚函数结构,那弄到最后,我只能将析构函数设置为纯虚函数了。但这里还是有一点需要注意,将普通函数方法设置成纯虚函数后,不实现这个函数的定义是可以的。将析构函数设置为纯虚函数,无论如何都需要定义个析构函数,因为派生类在执行析构函数的时候总是要调用基类的析构,所以代码应该如下:

class Base {public:    virtual ~Base() = 0;};Base::~Base() {}class Derive : public Base {};int main() {    Base *b = new Derive;    delete b;    return 0;}

请记住:

  • 带多态性质的base class都应该定义virtual析构函数,如果一个class有任何的virtual函数,那么他也要有一个virtual析构函数
  • 如果一个class不是为了作为一个多态作用,或者一个base class来使用,那么就不要定义virtual 析构函数!

条款八:别让异常逃离析构函数

       异常判断是与C语言不同一个功能,带来方便的同时也会带来一些问题,例如:

class widget{public:....~widget(){}};void doSomething(){        std::vecotr<widget> v;.....}
       在doSomething的函数结束的时候,vector中的对象会对应的调用widget类的析构函数,如果此时有100个成员,在调用的对应成员的析构函数的时候如果出现了多次的析构异常,那么此时程序就可能会异常的停止或者出现不确定的情况,为了阻止这种情况出现一个原则就是:不要让析构函数出现异常!


       为了防止在析构函数中出现异常,主要有以下的几种方法:
1.在可能造成异常的代码中做异常的判断,捕获异常后,要么终止程序的执行,要么就是吞下异常,让程序继续执行。

DBConn::~DBConn(){try{db.close();}catch(...){std::abort();}}
DBConn::~DBConn(){try{    db.close();}catch(...){}}
2.将可能造成异常的代码放到一个独立的函数中,然后在析构的时候提前调用,这样能提前发现异常并做处理,具体的例子请参看书

请记住:

  • 虚构函数不要出现异常,如果一个被析构函数调用的函数中出现了异常,则要么结束程序要么就吞下异常!
  • 如果客户需要对一个运行期间出现异常的函数做出反应,那么应该提供一个独立的函数而非析构函数来执行此操作!

条款九:绝对不要在构造函数或者析构函数中调用virtual函数


       首先上例子:

class People{public:People();virtual void task() const = 0;....};People::People(){....task();}class Student : public People{public:virtual void task() const;.....};
       在People的构造函数中,调用了虚函数task,当我们定义一个student对象的时候,student的构造函数将会调用,在初始化化student的成员函数之前将首先调用父类的构造函数,在父类的构造函数中调用了pure virtual函数,此时本意想通过虚函数来调用子类的对应函数,但是此时子类的构造函数还没有构造成功,因此此时调用的还是父类的pure virtual task,报错!如果task不是纯虚函数,不会报错,但是会出现我们不想要的情况!
       同样的如果再析构函数中调用virtual函数,当在子类中调用virtual函数的时候,子类中的成员变量变量将呈现未定义的状 ,进入父类的析构函数的时候,此时将对象单纯的看作是父类的对象,此时的所有virtual函数调用或者dynamic_cast等都会那么看待!
        既然不能通过父类的虚函数在构造函数中调用子类中的函数,可以通过在子类调用父类的构造函数的时候将子类的信息 传递给父类来执行!
class People{public:People();void task(std::string info) const;....};People::People(std::sting info){....task(info);}class Student : public People{public:Student():People(createinfo(parame)){....}.....private:static std::string createinfo(std::string parame){....}}

请记住:

  • 在构造函数和析构函数中不要调用虚函数,因为此时虚函数不会下降为对应的子类

条款10:让操作符=返回一个reference to *this


       对一些存在的操作符重载的时候,最好是要兼容这些操作符本身就有的属性,例如对于操作符=,可以实现连等的操作,例如
int a = b = c = 10;这样可以认为10先赋值给c,然后c的值又赋值给b,b的值最后赋值给a,同样的对于类中操作符=的重载也要支持
这样的操作,那么可以将operator返回操作符的左值,为了效率问题,返回左值的一个引用,例如:

class People{public:People& operator=(const People& rhs){.....return  *this;}...private:...};
       此外对于类似的+=,-=等也要用同样的准则,不过如果不遵守这个准则,编译也能通过,但是兼容性不理想,因为在库中存在的类中的operator=操作符都是遵循这个原则的,所以没有特殊的原因不要另辟蹊径!


请记住:

  • 领操作符operator=返回一个reference to *this

条款11:在operator=中处理“自我赋值”

       

       “自我赋值”发生在对象被赋值给自己时:

class Widget {...};Widget w;...w = w;//赋值给自己

        当对象进行自我赋值的时候会出现什么问题的呢?看下面的例子:

class Bitmap{.....};class Wdiget{public:Wdiget& operator=(const Wdiget& rhs){delete p;p = new Bitmap(*rhs.p);return *this;}...private:Bitmap* p;};

       在这个例子中,如果rhs与this指向的是同一个对象,那么当delete p以后,就包含一个空的对象,因此最终的p指向一个被删除的对象!为解决这个问题我们可以在delete之前首先做判同操作,如下:

Wdiget& operator=(const Wdiget& rhs){    if(&rhs == this){        return *this;}    delete p;    p = new Bitmap(*rhs.p);    return *this;}

       这样在正常情况下是不会出现自我赋值情况,但是如果在new Bitmap的时候出现异常怎么办呢?此时this的p已经被释放,下面是另外的一种解决办法:

Wdiget& Wdiget::operator=(const Wdiget& rhs){Bitmap* tmp = p;p = new Bitmap(*rhs.p);delete tmp;return *this;};

       这样当在new Bitmap出现异常的情况下,this还是保持原先的不变!

请记住:

  • 确保当对象进行自我赋值的时候有良好的行为,其中包括来源对象和目标对象的比较,设计良好的赋值顺序,以及copy and swap技术
  • 要确保当一个函数操作对个对象,并且多个对象可能是同一个对象的时候行为也是准确的

条款12:复制对象时勿忘其每一个成分


        C++中类会将成员都封装起来,类的copy操作只能通过赋值函数和复制构造函数,在进行对应的copy操作的时候注意要将成员的每一个成员进行对应的复制!例如:

class People{private:string name;public:People(const People& rhs):name(rhs.name){}People& operator=(const People& rhs){name = rhs.name;return *this;}};

       在上面的例子中只有一个成员没有任何的问题,但是当加入另一个成员变量的时候也要注意对应的修改这个拷贝构造函数。否则虽然编译器不会警告任何错误,在后面可能去出现意想不到的问题!
       在继承体系中,主要子类的拷贝构造函数要主动的去调用父类的相关拷贝构造函数,例如:

class Student:public People{private:string school;public:Student(const Student& rhs):school(rhs.school){}Student& operator=(const Student& rhs){school = rhs.school;return *this;}};
        在上面的例子中,复制了子类中的所有成员,但是相关父类中的name成员变量缺没有复制,此时name变量不能被复制为rhs的name值,取代的是调用People的默认构造函数,正确的方式如下:

Student::Student(const Student& rhs):People(rhs),school(rhs.school){}Student& Student::operator=(const Student& rhs){Student::operator=(rhs);school = rhs.school;return *this}

       当编写一个copy函数的,必须要确保(1)复制所有的local变量(2)调用父类的对应的copy函数。

请记住:

  • Copying 函数应该确保复制“对象内的所有成员函数”及“所有base class成分”
  • 不要尝试以某个copying函数去实现另一个copying函数,应将共同的机制放进第三个函数中,并由两个copying函数共同调用
       可以参考以前的博客<< C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容 >>