[C++] 构造/析构/赋值运算

构造/析构/赋值运算

1. 了解C++默默编写并调用哪些函数

  • 当你声明了一个empty class时,编译器会自动为你完成copy构造函数,copy assignment操作符和析构函数,如果你没有声明任何构造函数,还会自动声明default构造函数。并且这些函数都是public且inline。
  • 但是,当你的class中出现了引用或const的成员变量时,编译器不再为你完成copy assignment操作符。因为引用不可以指向不同对象,const也不会改变对象。
  • 还有,如果某个base class将copy assignment操作符声明为private,编译器会拒绝为其dereived class生成copy assignment操作符。因为编译器为derived class所生成的copy assignment操作符想象中可以处理base class成分!

2. 若不想使用编译器自动生成的函数,就该明确拒绝

有时候,某个对象是独一无二的,不能没复制也不能被赋值!所以我们要强制编译器不允许使用=和copy 构造函数,但如果你不写他们,编译器又会自动帮你加上,问题由此引发。

  • 把copy构造哈数和copy assignment操作符定义为private,并且只有声明没有实现。
class Home {
public:
    ...
private:
    ...
    Home(const Home&);
    Home& operator = (const Home&);
    // 只有声明!

但也有可能会被友元函数和member函数内部使用copy构造哈数和copy assignment操作符,所以此方法并不安全。

  • 编写一个不能使用copy构造哈数和copy assignment操作符的基类。
class Uncopyable {
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator = (const Uncopyable&);
};
class Home : private Uncopyable {
    ...
};

用这个方法可以有效地阻止Home对象被赋值和复制!因为子类相应的函数必须调用基类相应的函数,而基类不允许它调用。

3. 为多态基类声明virtual析构函数

当基类没有virtual析构函数时,我们把一个基类指针指向子类指针,并且删除基类指针时,子类的析构函数是不会起作用的。 于是,我们就把基类析构函数定为virtual来解决这个问题。

  • virtual析构函数
class TimeKeeper {  // A factory class.
public:
    TimeKeeper();
    virtual ~TimeKeeper();
    ...
    Timekeeper* getTimeKeeper(); // 指向子类指针。
};
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;

几乎可以确定:==任何class只要带有virtual函数都几乎确定应该也带有一个virtual析构函数。==

  • 如果class不含有virtual函数,通常表示他并不愿意被用作一个base class。而如果此时将其析构函数声明为virtual,往往是错误的。

(欲实现virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由某一个所谓vptr(virtual table pointer)指针所指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于对象的vptr所指的那个vtbl–编译器在其中寻找适当的函数指针。)

  • 不要试图把一个non-virtual析构函数的类当做基类!
    例如标准库string 不含任何的virtual函数,如果把他当做基类就会出现问题。
class specialString : public string {
...
};
    specialString* pss = new specialString();
    string* ps;
    ps = pss;
    ...
    delete ps;
    // 没有定义!ps的specialString的资源会泄露!

相同的分析使用于任何不带virtual析构函数的class,包括STL容器如vector,list, set, map…

  • pure virtual 析构函数总是需要有实现的!就算这个实现可能什么都不做。如果不给实现的话,编译器会报错。
    ”给base class一个virtual析构函数“,这个规则只使用与polymorphic base class身上。这种base class的设计目的是为了用来”通过base class接口处理dereived class对象“。同样的,如果不是为了实现多态,就不应该把析构函数声明为virtual。

4. 别让异常逃离析构函数(未详)

C++并不禁止析构函数吐出异常,但它不鼓励你这么做。因为如果析构过程中吐出异常,析构过程就会结束导致不明确的行为或者内存泄露。

请记住:

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

5. 决不再构造和析构过程中调用virtual函数

在基类中调用一个virtual函数,会出现意外情况!原因就在于在base class构造完成前,derived class还没有被构造。

class Transaction {
public:
    Transaction();
    virtual void logTransaction() const = 0;
    ...
};
Transaction::Transaction() {
...
logTransaction();
}
class BuyTransaction : public Transaction {
public:
    virtual void logTransaction() const;
    ...
};
// now!
BuyTransaction b;  // error occurs!!!

问题出现了!BuyTransaction的对象b要先构造Transaction,但在此期间还调用了logTransaction,因为此时子类还没有被构造,所以编译器只能调用基类的logTransaction,又因为基类中logTransaction是一个纯虚函数,无法被调用,于是出现的编译错误。而且还基类的Transaction()函数中,logTransaction会被认为是基类的调用,毫无疑问会出问题!

一种更好的方法是,不使用虚函数!

class Transaction {
public:
    explicit Transaction(const string& info) {
        ...
        logTransaction(info);
    }
    void logTransaction(const string& info) const;
    ...
};
class BuyTransaction : public Transaction {
public:
    BuyTransaction(params) : Transaction(create(params)) {
    ...
    }
    ...
private:
    static string create(params);
};

在构造期间,你可以通过”令derived class将必要的构造信息向上传递给base class构造函数“替换而弥补!。

值得注意的是,static string create(params)。比起成员初值列内给予base class所需数据,利用辅助函数创建一个值传给base class构造函数往往比较方便而可读。令此函数为static,也就不可能以外指向”初期未成熟之BuyTransaction对象内尚未初始化的成员变量“。这很重要,正式因为”那些成员变量处于未定义状态“,所以”在base class构造和析构期间调用virtual函数不可下降至derived class“。

6. 令operator=返回一个reference to *this

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符左侧实参。这是你为class实现赋值操作符时应该遵循的协议。

不仅如此,所有赋值相关运算都应该符合这个协议!

class Widget {
public:
    ...
    Widget& operator+=(const Widget& rhs) {
        ...
        return *this;
    }
    ...

7. 在operator=中处理“自我赋值”

赋值操作总是要先clear,于是如果自我赋值,*this 和 引用的orig都会被clear掉,这样*this就会失去数据。

出现自我赋值是因为别名的存在:所谓别名就是“有一个以上的方法指向某对象”。一般而言如果某段代码操作pointer或reference而他们被用来指向多个相同类型的对象,就需要考虑这些对象是否为同一个。实际上,两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名,因为一个base class的reference或pointer可以指向一个derived class对象。

  • 加上一个“证同测试”:
Widget& Widget::operator=(const Widget& orig) {
    if (this == &orig) {
        return *this;
    }
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

此方法具有“自我赋值安全性”,但不具备“异常安全性”。

如果“new Bitmap”导致遗产(可能因为分配时内存不足或因为bitmap的copy构造函数抛出异常),widget最终会持有一个指针指向一块被删除的Bitmap。
* 只需要注意在赋值pointer所指东西之前别删除pointer。

Widget& Widget::operator=(const Widget& orig) {
    Bitmap* pOrig = this->pb;
    pb = new Bitmap(*orig.pb);
    delete pOrig;
    return *this;
}

现在,如果new过程中抛出异常,pb仍然保持原样。虽然这不是自我赋值的最高效解决方法,但却可行。
当然也可以在这个函数前加上证同测试。

  • 使用copy-and-swap
Widget& Widget::operator=(const Widget& orig) {
    Widget temp(rhs);
    swap(temp);
    return *this;
}

8. 复制对象时勿忘其每一个成分

设计良好的面向对象系统,会将对象的内部封装起来,只留两个函数负责对象拷贝,那是带着适切名称的copy构造函数和copy assignment操作符,我称它们为copying函数。

如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为。但后果就是,当你的实现代码几乎必然出错时,编译器不会告诉你。例如,copying函数执行了局部拷贝(有的变量没有被赋值)

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

令copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象!

同样的,令copy构造函数调用copy assignment操作符也是无意义的!构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象上。对一个尚未构造好的对象赋值,就像在一个尚未初始化的对象身上做“只对初始化对象才有意义”的事一样。

如果copy构造函数和copy assignment操作符代码相似,可以重新写一个函数命名为init,并设为private。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值