Effective C++ 构造/赋值/析构

C++默认的类函数

class A {
    A() = default;
    ~A() = default;
    A(const A &) = default;
    A &operator=(const A &) = default;
    A(A &&) = default;
    A &operator=(A &&) = default;
};

会默认生成默认构造函数和析构函数,拷贝、移动对应的构造函数和赋值运算符。

一般来说当这些函数需要被调用,而我们没有声明的时候,编译器会自动生成。其自动生成的是没有虚函数属性,除非他是一个派生类,而其基类对应的函数是虚函数

如果类成员对象有引用或者const成员,不会生成默认的拷贝相关的函数。

struct A {
    const int a;
    int &b;

    A(int c, int &d) : a(c), b(d) {}
};

int main() {
    int i = 3;
    A a(3, i);
    A b(4, i);
    // b = a; 报错
}

只能手动定义拷贝赋值运算符

不需要自动生成函数时,使用delete

  • C++11 中可以通过关键字delete禁止编译器默认生成
  • 老旧版本可以通过继承UnCopyable基类来完成禁止默认生成
class UnCopyable {
public:
    UnCopyable() = default;

    ~UnCopyable() = default;

private:
    UnCopyable(const UnCopyable &);

    UnCopyable &operator=(const UnCopyable &);
};

class A : private UnCopyable{
	// ...
}

这样子后续派生类都不允许自动生成拷贝族函数了

为多态基类声明虚析构函数

  • 带多态性质的基类应该声明一个虚析构函数。如果类带有任何虚函数,其析构函数应被声明为虚函数。
  • 一个类如果不是被设计为基类或者不带有多态性质,就不应被声明为虚函数

将为多态设计的类的析构函数设置为虚函数

实现动态绑定,使指向基类指针的派生类的资源能被正确析构

struct Base {
    virtual ~Base() {
        printf("Delete Base\n");
    }
};


struct A : Base {
    ~A() {
        printf("Delete A\n");
    }
};

struct B : A {
    ~B() {
        printf("Delete B\n");
    }
};

int main() {
    Base *b = new B;
    delete b;
    {
		Base b;
	}
}
/*
Delete B
Delete A
Delete Base
---------
Delete B
*/

如果没有将其设为虚函数

struct Base {
	// 设为非虚函数
    ~Base() {
        printf("Delete Base\n");
    }
};
int main() {
    {
        Base *b = new B;
        delete b;
    }
    cout << "---------" << endl;
    {
        Base b;
    }
    cout << "---------" << endl;
    {
        B *b = new B;
        delete b;
    }
    cout << "---------" << endl;
    {
        B b;
    }
}
/*
Delete Base
---------
Delete Base
---------
Delete B
Delete A
Delete Base
---------
Delete B
Delete A
Delete Base
*/

所以这一条针对的是指向基类指针的派生类对象类型的正确析构,即:

Base p = new P();

纯虚函数可以被定义

有时候该纯虚函数的定义代码重复率较高,就可以将其显式定义出来,在派生类中可以显式调用
但是存在问题:纯虚函数一般是希望派生类定义自己的相关代码,定义了纯虚函数可能使使用者忽略之

struct Base {
    virtual void f1() = 0;

    virtual void f2(const string &) = 0;
};


struct A : Base {
    virtual void f1() override {

    }
};

struct B : Base {
    virtual void f1() override {
        Base::f1();
    }

    virtual void f2(const string &s) override {
        Base::f2(s);
    }
};

void Base::f1() { printf("F1\n"); }

void Base::f2(const string &s) { printf("F2-> %s\n", s.c_str()); }

int main() {
//    A 没有实现所有纯虚函数,不能被实例化
//    A a;
    B b;
    b.f1();
    b.f2("Hello");
}
// F1
// F2-> Hello

虚函数会扩大对象的大小

有虚函数的类的对象会有一个虚函数指针,指向虚函数表。会占据一个指针大小(按系统32位还是64位划分)。因此没有设计为多态特性的类没必要有虚函数。会浪费空间。

struct Base {
    virtual ~Base() {
        printf("Delete Base\n");
    }

    void func() {

    }
};


struct A : Base {
    ~A() {
        printf("Delete A\n");
    }
};

struct B : Base {
    int i;
};

int main() {
    int *ptr = nullptr;
    cout << "size of ptr is " << sizeof(ptr) << endl;
    cout << "size of Base is " << sizeof(Base) << endl;
    cout << "size of A is " << sizeof(A) << endl;
    cout << "size of B is " << sizeof(B) << endl;
}
/*
size of ptr is 8
size of Base is 8
size of A is 8
size of B is 16
*/

当类内至少含有一个虚函数时,才将析构函数声明为虚函数

析构函数声明为纯虚函数

当希望一个类为虚基类,但是没有其他任何虚函数的时候,可以将析构函数声明为纯虚函数,以此将这个类定义为虚基类。
但是注意纯虚函数的析构函数需要被定义!!否则连接时会报错

struct Base {
    virtual ~Base() = 0;
};


struct A : Base {
    ~A() {
        printf("Delete A\n");
    }
};

// 需要定义
Base::~Base() noexcept {}

int main() {
    Base *p = new A;
    A *a = new A;
}

不要让异常逃离析构函数

意思是:析构函数里的异常要在析构函数里捕获,不要让其抛出异常。在析构函数中抛出异常可能会导致程序提前结束或者未定义行为

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

在析构函数中处理异常

常见的RAII管理资源手段:

class DB{
public:
	static DB create();
	void close();
};

class DBConn{
private:
	DB db;
public:
	// ...
	~DBConn{
		db.close();
	}
};

做到了自动释放数据库连接。但是在析构函数中调用关闭数据库操作(close),该操作可能会导致抛出异常。上诉例子中没有对异常处理,允许其离开析构函数,可能会造成错误。
常见处理办法:

// 方法一,结束程序
~DBConn{
	try{
		db.close();
	}catch(...){
		// 记录下异常
		// 结束程序
		std::abort();
	}
};

// 方法二,吞下异常
~DBConn{
	try{
		db.close();
	}catch(...){
		// 记录下异常
		// ...
	}
};

提取出可能抛出异常的操作,提前处理

class DB{
public:
	static DB create();
	void close();
};

class DBConn{
private:
	bool closed = false;
	DB db;
public:
	// 该函数可以抛出异常,被处理
	void close(){
		db.close();
		closed = true;
	}
	~DBConn{
		// 避免忘了手动释放
		if(!closed){
			try{
				db.close();
			}catch(...){
				// 记录下异常
				// ...
			}
		}
	}
};

不要在构造函数或者析构函数中直接或间接调用虚函数

  • 不要在构造函数或者析构函数中调用虚函数,因为构造派生类对象时,其基类部分先于派生类部分构造
    假设一个交易日志体系,希望在创建的时候自动记录操作,因此在构造函数内加了log操作。
struct Transaction {
    Transaction() {
        printf("Construct Base\n");
        log();
    }

    virtual void log() const = 0;
};

struct Buy : Transaction {
    Buy() {
        printf("Construct Buy\n");
        log();
    }

    void log() const override {
        printf("Buy log\n");
    }
};

struct Sell : Transaction {
    void log() const override {
        printf("Sell log\n");
    }
};

void Transaction::log() const {
    printf("Base log\n");
}


int main() {
    Buy b;
}
/*
Construct Base
Base log
Construct Buy
Buy log
*/

在构造派生类对象时,其基类部分先于其他类部分被构造。这时候调用虚函数,是调用的其基类版本的虚函数。会有几个错误:

  • 如果该函数是个纯虚函数且没有被定义(很多情况都会这样),那么会发生连接错误
  • 调用的是其基类版本的虚函数,不会按照动态绑定原则调用其派生类定义的虚函数,与设计目的不同
  • 可能会有未定义行为错误

解决办法 将该函数设计为普通函数,派生类的构造函数向基类的构造函数传递必要信息。

struct Transaction {
    Transaction(const string &s) {
        log(s);
    }

    void log(const string &) const;
};

struct Buy : Transaction {
    Buy(parameters) : Transaction(createLogString(parameters)) {}

    static string createLogString(parameters);
};

这里通过静态函数生成log信息,可以避免指向派生类中还未被构造出来的成员,造成程序错误。

令operator=返回一个*this的引用

这样方便于连续赋值

struct Base {
    int v;

    Base(int i = 0) : v(i) {}

    Base &operator=(const Base &rhs) {
        if (this != &rhs) {
            this->v = rhs.v;
        }
        return *this;
    }
};

int main() {
    Base a(-1);
    Base p, q;
    p = q = a;
    cout << p.v << " " << q.v << endl;
}
//-1 -1

在operator=中处理自赋值情况

  • 确保对象自我赋值时有正确的行为。其中技术包括①比较地址 ②设计好语句顺序 ③copy-and-swap
  • 确定任何函数如果操作一个以上的对象,而其中多个对象仍是同一个对象时(常见于指向同一个对象的不同的指针、引用),其行为依然正确

不要忽视自赋值情况

并不是只有显式自赋值

并不是所有的自赋值都是形如x = x;这种情况,当涉及到指针和引用时,会存在潜在的自赋值,比如

Base *p = new Base;
Base *a = p, *b = p;
// 发生了自赋值
*a = *b;

Base bN[3];
int i = 0, j = 0;
// 发生了自赋值
bN[i] = bN[j];

所以不要觉得自己不会写出x = x;这种代码就忽视自赋值

不正确自赋值的危害

struct Data {
	// ...
};

struct User {
private:
    Data *data;
public:
    User &operator=(const User &user) {
        delete data;
        data = new Data(*user.data);
        return *this;
    }
};

正确处理自赋值

比较地址

struct User {
private:
    Data *data;
public:
    User &operator=(const User &user) {
        if(this == &user) return *this;
        delete data;
        data = new Data(*user.data);
        return *this;
    }
};

**缺点:**不具备异常安全
如果new Data(*user.data)抛出异常,那么data将指向一个被释放过的内存,这样的指针是有害的。
具备异常安全性,将自动满足自赋值安全性

正确的赋值顺序

struct User {
private:
    Data *data;
public:
    User &operator=(const User &user) {
        Data *cache = data;
        data = new Data(*user.data);
        delete cache;
        return *this;
    }
};

这样当new Data(*user.data)抛出异常时,data将依旧指向原来的内存。

copy and swap

自动具备异常安全

struct User {
private:
    Data *data;
public:
	// 注意这里的参数是非引用
    User &operator=(const User user) {
    	// 使用了std::swap();
    	swap(*this, user);
    	return *this;
    }
};

或者

struct User {
private:
    Data *data;
public:
    User &operator=(const User& user) {
    	User temp(user);
    	swap(*this, temp);
    	return *this;
    }
};

这里注意,如果是内置类型的话,一般会自定义swap函数,来完成高效的交换

struct User{
	void swap(User &rhs){
		// ...
	}
}

复制对象时不要遗忘其每个成分

  • 拷贝函数应确保复制对象内的每个成员变量以及其基类的成员
  • 不要尝试使用某个拷贝函数来实现另一个拷贝函数,共同代码可以抽象为公共函数

派生类的拷贝函数不要遗漏了其基类成员

该派生类的拷贝函数看上去没有什么问题,但是:

struct Base {
    int val;
    string s;

    Base(int v = -1, string ss = "default") : val(v), s(ss) {}

    Base(const Base &rhs) : val(rhs.val), s(rhs.s) {}

    Base &operator=(const Base &rhs) {
        val = rhs.val;
        s = rhs.s;
        return *this;
    }

    virtual void show() const { printf("%s %d\n", s.c_str(), val); }
};

struct P : Base {
    int k;

    P(int j = -1) : k(j) {}

    P(const P &rhs) : k(rhs.k) {}

    P &operator=(const P &rhs) {
        k = rhs.k;
        return *this;
    }

    void show() const override { printf("%s %d %d\n", s.c_str(), val, k); }

};

int main() {
    P p(6);
    p.s = "Hello", p.val = 1;
    p.show();

    P newP(p);
    newP.show();

    P newP2(1);
    newP2.s = "newP2";
    newP2 = p;
    newP2.show();
}
/*
Hello 1 6
default -1 6
newP2 -1 6
*/

可见派生类的拷贝构造函数只拷贝了派生类的部分,基类部分调用了其默认构造函数。派生类的拷贝赋值运算符只拷贝了派生类部分,保持基类部分不变。

在派生类中调用基类的复制函数
所以派生类的拷贝函数需要手动拷贝基类的成员,基类的成员一般都是private的,所以一般调用基类的拷贝函数。

struct P : Base {
    int k;

    P(int j = -1) : k(j) {}

	// 调用基类的拷贝构造函数
    P(const P &rhs) : Base(rhs), k(rhs.k) {}

    P &operator=(const P &rhs) {
    	// 调用基类的拷贝赋值运算符
        Base::operator=(rhs);
        k = rhs.k;
        return *this;
    }

    void show() const override { printf("%s %d %d\n", s.c_str(), val, k); }

};

不同的拷贝函数之间不可以互相调用

**拷贝赋值运算符不可以调用拷贝构造函数:**试图构造一个已经存在的对象,显然不合理
拷贝构造函数不可以调用拷贝赋值运算符: 可能某些成员还没有被初始化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值