【Effective C++】构造/析构/赋值运算

几乎你写的每一个class都会有一个或多个构造函数、一个析构函数,一个copy assignment操作符。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及你的整个classes。所以确保它们行为正确时生死攸关的大事。本章提供的引导可让你把这些函数良好地集结在一起,形成classes的脊柱。

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

如果你自己没有声明,编译器就会声明

  • 默认构造函数
  • copy构造函数                    //单纯地将来源对象的每一个non-static成员变量拷贝到目标对象
  • copy assignment操作符    //同上
  • 析构函数                            //是个non-virtual

唯有这些函数被需要(被调用),它们才会被编译器创建出来,下面代码造成上述每一个函数被编译器产出:

Empty e1;         //默认构造函数
                  //析构函数
Empty e2(e1);     //拷贝构造函数
e2=e1;            //copy assignment操作符

注意:

如果生成的代码不合法或者没有意义时,编译器会拒绝为class生成operate=:

(1)不合法

#include <string>
#include <iostream>
using namespace std;
class Dog {
public:
    Dog(string& namevalue, int agevalue):name(namevalue),age(agevalue){
        //name = namevalue;  当需要初始化const修饰的类成员/引用成员数据应使用成员初始化列表
        //age = agevalue;
    }
    void show() {
        cout << name << " is " << age << " years old." << endl;
    }
private:
    string& name;
    const int age;
};

int main() {
    string s1("Persephone");
    string s2("Satch");    
    Dog d1(s1, 2);
    Dog d2(s2, 36);
    //Dog d1("Persephone", 2);
    //Dog d2("Satch", 36);
    //d1 = d2;//无法引用 函数 "Dog::operator=(const Dog &)" (已隐式声明) -- 它是已删除的函数
  d1.show();
  d2.show();
  getchar();
}

由于C++不允许“让引用改指向不同的对象”,编译器没有自动生成operate=,d1=d2就会报错。

(2)没有意义

如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。毕竟编译器为derived classes所生的copy assignment操作想象中可以处理base class成分(条款12)。

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

有些类,你不想它的对象被拷贝,但如果你不声明拷贝构造函数,编译器会自动给你生成。这时候我们可以,

1)将拷贝构造函数和copy assignment操作符声明为private(此时member函数和friend函数还是可以调用你的private函数),并且不去定义它

2)写一个base class,它有私有的拷贝构造函数和copy assignment操作符,然后去继承它,根据上一条,编译器将拒绝为其生成一个copy assignment操作符。(可能会导致多重继承

3)使用Boost提供的版本。(还没学到

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

比如在使用工厂函数时,工厂函数会返回一个base class指针,指向新生成的derived对象。被返回的对象位于heap,因此为了避免泄露内存和其他资源,需要delete该对象。

BasicCamera* pb = CreateCamera();
...
delete pb;

指针指向的时子类对象,但却经由一个base class指针来删除,而目前的base class有一个non-virtual析构函数(条款05指出,编译器自动生成的析构函数是non-virtual的)。这会引来灾难,因为实际执行时,通常发生的是,对象的derived成分没被销毁,base class成分通常会被销毁,造成一个局部销毁的对象。

解决方法:

给base class一个virtual析构函数

注意:

  • 任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
  • 如果class不含virtual函数,通常表示它不意图被用作一个base class,此时令其析构函数为virtual,会导致其对象的体积增大,不能传递至其他语言的函数。
  • 总而言之,只有当class内含至少一个virtual函数,才为它声明virtual析构函数
  • 有时候抽象类不想被实体化,比如说BasicCamera创建对象没有意义,你可以有一个pure virtual函数,这时候可以声明一个pure virtual的析构函数。
class BasicCamera {
public:
    virtual ~BasicCamera() = 0;
};

加上virtual,加上“=0”,就成为pure virtual函数了,但是书上说“必须为这个pure virtual析构函数提供一份定义”???由子类继承后定义可以吗?

试了一下,好像确实如此。

#include<iostream>
using namespace std;
class BasicCamera {
public:
    virtual ~BasicCamera()=0;//如果此处为~BasicCamera(),只会调用父类的析构
};
BasicCamera::~BasicCamera() {
    cout << "调用了BasicCamera的析构函数" << endl;
}//不加会报错,必须要定义
class Hik :public BasicCamera {
public:
    ~Hik() {
        cout << "调用了hik的析构函数" << endl;
    }
};

class Factory {
public:
    BasicCamera* CreateCamera() {
        return new Hik();
    }
};
int main() {
    Factory fac = Factory();
    BasicCamera* camera = fac.CreateCamera();
    delete camera;
    getchar();
    return 0;
}

运行结果:

 可以看出,析构函数的运作方式是,最深层派生的那个class(此处为子类hik)的那个析构函数最先被调用,然后是每一个base class的析构函数被调用。编译器会在子类的析构函数中调用父类的析构函数,所以必须提供一份定义。

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

如果析构函数吐出异常程序可能过早结束或出现不明确行为。比如,HikCamera类在析构时抛出异常:

#include<iostream>
using namespace std;
class HikCamera {
public:
    ~HikCamera() {
        throw 1;
        closed = true;
    }
private:
    bool closed = false;
};
int main() {
    {
        HikCamera cam;
    }
    getchar();
    return 0;
}

上图可以看出,调用了abort函数终止了程序。但如果析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?

1. 如果抛出异常就结束程序(通常通过abort完成):

~HikCamera() {
    try {
        throw 1;
        closed = true;
    }
    catch (int i) {
        abort();
    }
}

如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用abort可以抢先置“不明确行为”于死地。

2.吞下因调用close而引发的异常

~HikCamera() {
try {
throw 1;
        closed = true;}
    catch (int i) {
        cout << "close函数中有异常" << endl;
    }
}

3. 将异常放在析构函数之外

提供一个close函数,赋予客户机会对可能出现的问题作出反应。同时可以在析构函数中追踪,由析构函数关闭之。

class HikCamera {
public:
    void close() {
        throw 1;
        closed = true;
    }
    ~HikCamera() {
        if(!closed){
        try { close(); }
        catch (int i) {
            cout << "close函数中有异常" << endl;
        }}
    }
private:
    bool closed = false;
};

条款09:绝不在构造和析构过程中调用virtual函数

 假如在构造函数中调用了virtual函数:

class BasicCamera {
public:
    BasicCamera() {
        open();//调用了virtual函数
    }
    virtual void open() {
        cout << "打开了相机" << endl;
    }
};
class HikCamera:BasicCamera{
public:
    HikCamera() {
        cout << "创建了hik相机" << endl;
    }
    void open() {
        cout << "打开了海康相机" << endl;
    }
};

运行结果:

在创建子类对象时,不会创建父类对象,只是初始化子类中属于父类的成员。父类的构造函数会被调用,运行父类构造函数时,里面的父类版本的virtual函数被调用了,不是子类中的版本。

原因

1. 当基类的构造函数执行时,子类的成员变量尚未初始化,如果virtual函数(open函数)调用了子类成员变量,这会导致不明确行为。

2. 基类构造期间,对象类型为基类。不只是virtual函数被编译器解析至基类,若使用运行期类型信息,也会把对象视为基类。

同样的道理也适用于析构函数。一旦子类的析构函数开始执行,子类成员变量就会呈现未定义值,进入基类析构函数后对象成为基类对象。

想要确保每一次有子类被建立就调用对应的open(),

解决方法

把父类的virtual函数改为不virtual的,然后子类的构造函数传递必要信息给父类的构造函数,父类的构造函数就可以调用non-virtual的open()了。像这样:

#include<iostream>
using namespace std;
class BasicCamera {
public:
    BasicCamera(const string& info) {
        open(info);
    }
    void open(const string& info) {
        cout << "打开了" << info << "相机" << endl;
    };
};
class HikCamera:BasicCamera{
public:
    HikCamera(string s):BasicCamera(s) {
    }
};
int main() {
    HikCamera cam("Hik");
    getchar();
    return 0;
}

换句话说,无法使用virtual函数从基类向下调用,在构造期间,你可以“令子类将必要的构造信息向上传递至基类构造函数”

条款10:令operate=返回一个reference to *this

关于赋值,可以将它们写成连锁的形式:

int x, y, z;
x = y = z = 15;     //赋值连锁形式

因为赋值所采用的是右结合律,因此,上面的连锁赋值可以解析为:

x = (y = (z = 15));

为了实现这样的“连锁赋值”,赋值操作符必须返回一个reference,指向操作符的左侧的实参,这也是为classes实现赋值操作符是应该遵守的协议:

class Widget {
public:
    ...

    Widget& operator=(const Widget& rhs)    //返回类型是一个reference,指向当前的对象
    {
        ...
        return* this;     //返回左侧的对象
    }
    ...
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算。例如:+=,*=,-=等。需要注意的是,这只是一个协议,并不是强制的。但是,最好还是这样做,因为这份协议被所有的内置类型和标准程序库提供的类型如string,vector,complex,tr1::shared_ptr或即将提供的类型所共同遵守的。

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

自我赋值

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

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

看着有些愚蠢,但它合法,不要以为大家绝对不这么做。以下为某些不明显“自我赋值”产生的情况:

(1)指针/引用指向同一个对象

a[i] = a[j];    //当i = j时
*px = *py    // 当两个指针指向同一个东西时

(2)父类的指针和子类的指针指向同一个对象

class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, Derived* pd);   //rb和pd可能是同一个对象

自我赋值的后果

在我们打算自行管理资源时,自我赋值的情况可能会使我们掉进“在停止使用资源之前意外释放了它”的陷阱。假设建立一个class,来保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap{ ... };
class Widget{
    ...
private:
    Bitmap* pb;   //指针,指向一个从heap分配而得的对象
};

下面是operate=的实现代码,

Widget& operate=(const Widget& rhs){
    delete pb;                 //停止使用当前的bitmap
    pb = new Bitmap(rhs.pb);   //使用rhs‘s bitmap的副本(不new的话,就指向一个bitmap了
    return *this
}

表面合理,但自我赋值出现时不安全:

Widget rhs;
Widget w = rhs;  //这是ok的

rhs = rhs;  //第一步,将rhs.pb指向的对象删掉 

解决方法

想要阻止这种做法,有以下方法:

(1)增加“证同测试”

Widget& operate=(const Widget& rhs){
    if (this == &rhs) return *this; //如果是自我赋值,不要做任何事
    delete pb;  
    pb = new Bitmap(rhs.pb);  
    return *this
}

这样做行得通,具备了“自我赋值安全性”,但是还不具备“异常安全性”。如果”new Bitmap“导致了异常,Widget最终还是有一个指针指向一块被删除的Bitmap。这样的指针有害,你无法安全地删除它们,甚至无法安全地读取它们。

(2)在复制pb所指东西之前不要删除pb:

Widget& operate=(const Widget& rhs){
    Bitmap* pOrig = pb;         //pb和pOrig指向一个对象
    pb = new Bitmap(rhs.pb);  //pb指向rhs.pb的副本
    delete pOrig ;             //利用pOrig删去旧对象
    return *this
}

同时具备了“自我赋值安全性”和“异常安全性”。

(3)copy and swap技术

swap(Widget& rhs){ ... } //交换*this和rhs的数据
Widget& operate=(const Widget& rhs){
    Widget temp(rhs);    //为rhs的数据做一个复件
    swap(temp);            //将*this数据和上述复件的数据交换
    return *this
}

如果创建指针的话,需要delete掉,但是创建对象的话,析构函数会delete掉指针。temp拷贝rhs的成功说明new Bitmap没有异常,然后swap。

更激进版:

swap(Widget& rhs){ ... } //交换*this和rhs的数据
Widget& operate=(Widget rhs){
    swap(rhs);            //将*this数据和上述复件的数据交换
    return *this
}

传入值时,会自动生成复件。此时的rhs就是原rhs的拷贝。

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

copy构造函数和copy assignment操作符我们称之为copying函数,编译器会自动为我们的classes创建copying函数,将拷贝对象的所有成员变量都做一份拷贝。如果你声明自己的copying函数,可能会导致:

1. 如果你漏了一个成员变量没有复制,大多数编译器不会告诉你

2. 继承时,子类的copying函数只复制了子类的成员,而父类的成员变量会被默认的构造函数初始化

 

PriorityCustomer的copying函数没有复制Customer成员变量,PriorityCustomer的copy构造函数并没有指定实参传给其base class构造函数,因此PriorityCustomer对象的Customer成分会被不带实参的父类default构造函数初始化(上面的是伪代码,父类必须要有默认构造函数)。父类的default构造函数将对name和lastTransaction执行缺省的初始化动作。
base class 的成分往往是private, 所以你无法直接访问他们,你应该让derived class的copying函数调用相应的base class函数:

#include<iostream>
using namespace std;
//--------父类--------------------------
class BasicCamera { public: BasicCamera() {}//父类必须要又默认构造函数,如果子类没有在拷贝构造的过程拷贝父类成员,那么子类会调用父类的默认构造函数,对父类的成员变量执行缺省的初始化。 BasicCamera(const BasicCamera& rhs) { name = rhs.name; } BasicCamera& operator=(const BasicCamera& rhs) {} void setname(string s) { name = s; } void show() { cout << "name is " << name << endl; } protected: private: std::string name; };
//---------------子类--------------
class HikCamera : public BasicCamera { public: HikCamera() {}; HikCamera(const HikCamera& rhs) :BasicCamera(rhs), price(rhs.price) { };//调用基类的copy构造函数 HikCamera& operator=(const HikCamera& rhs) { BasicCamera::operator =(rhs);//对基类成分进行赋值 price = rhs.price; return *this; }; void set(int x, string s) { BasicCamera::setname(s); price = x; } void show() { BasicCamera::show(); cout << "price is " << price << endl; } protected: private: int price; }; int main() { HikCamera cam; string s = "hik"; cam.set(100, s); HikCamera cam1(cam); cam1.show(); getchar(); }

运行结果

如果没有BasicCamera(rhs),运行结果:

 

总结:

当你编写一个copying函数,请确保

1.复制所有的local成员变量,

2.调用所有的base class内的适当的copying函数。

当这两个copying函数有近似相同的实现本体,令一个copying函数调用另一个copying函数无法让你达到你想要的目标。

总结:

05:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数。

08:析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

08:如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非析构函数中)执行该操作。

10:令赋值操作符返回一个reference to *this。

确保当对象自我赋值时operate=有良好行为,其中技术包括比较“来源对象”和目标对象“的地址、精心周到的语句顺序、以及cpoy and swap。

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

12:Copying函数应该确保复制“对象内的所有成员变量”及”所有base class成分“。

12:不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

 参考:

1. 《Effective C++》P34-60

posted @ 2022-04-25 22:07  湾仔码农  阅读(88)  评论(0编辑  收藏  举报