Effective C++学习笔记(第二章)

2022-03-06  本文已影响0人  crazyhank
条款05:了解C++默默编写并调用的哪些函数
class B {
private:
  const int value = 100;
};
class D : public B {
};
// 编译器不会自动为B产生赋值运算符!
条款06:若不想用编译器自动生成的函数,就该明确拒绝

如果在声明一个类后,没有声明拷贝构造函数或者赋值运算符的情况下,按照上一条规则,编译器是会为你自动生成对应的函数的。(注意:这里的自动是指你的代码里产生了这样的调用的时候
但是,实际场景下,往往存在不能拷贝构造或者赋值运算的情况(这两种操作没有实际意义),就需要将这两个函数定义成不能被使用。

class A {
public:
private:
  A(const A& other); // 不定义它
  A& operator=(const A& other);  // 不定义它
};

外部代码肯定不能使用这两个函数了,并且A类内部成员函数使用的时候会在链接的时候报错,因为我们没有定义他们。

class UnCopy {
public:
  UnCopy() {}
  ~UnCopy() {}
private:
  UnCopy(const UnCopy&);
  UnCopy& operator=(const UnCopy&);
};
class MyClass : private UnCopy { // 这里如果用public继承也可以达到同样的效果
};

MyClass obj1, obj2;

obj1 = obj2;   // 错误,编译器自动为MyClass生成赋值运算符的时候会调用UnCopy的赋值运算符,而它在MyClass类中是不可见的。

这一条规则在现代C++(11之后),实现起来就非常简单了,使用delete关键字即可做到,如下所示:

class MyClass {
public:
  MyClass() {}
  ~MyClass() {}
  MyClass(const MyClass& other) = delete;
  MyClass& operator=(const MyClass& other) = delete;
};

MyClass obj1, obj2;
obj1= obj2; // 错误
条款07:多态基类声明virtual析构函数
class B {
public:
  B() {}
  ~B() {}
};
class D : public B {
};

B* p = new D();
delete p; // 将会产生局部销毁的问题,产生资源泄露
条款08:别让异常逃离析构函数

这条规则背后的原因是如果在类的析构函数中抛出异常,外部的代码很难捕捉到,考察以下代码:

class A {
  public:
    A() {}
    ~A() {
      throw 1;
    }
};
int main()
{
    try {
        A tmp;
    } catch (...) {
        std::cout << "Caught" << std::endl;
    }

    return 0;
}

上面这段代码在A的析构函数中抛出了异常,外面的代码虽然进行了捕捉,但是是捕捉不到的,程序会直接退出。

如果类的析构函数真的可能会出现抛出异常的情况,推荐的处理方法有两种:

class A {
public:
  A() {}
  ~A() {
    try {
      DoSomething();    // 可能产生异常的函数
    } catch(...) {
      std::abort();  // 直接退出或者在这里吞掉异常
    }
  }
};
class A {
public:
  A() {}
  ~A() {
    try {
      DoSomething();    // 可能产生异常的函数
    } catch(...) {
      std::abort();  // 直接退出或者在这里吞掉异常
    }
  }
  void DoSomething() {...}
};
条款09:不在构造、析构函数中调用virtual函数
条款10:令operator=返回一个引用指向*this

这一条不是一个强制性的规范,而是大家都遵循的协议,比如你在类的定义中实现operator=的时候,最好是返回一个T&引用,如:

class A {
public:
  ...
  A& operator=(const A& other) 
  {
    ...;
    return *this;
  }
};

A a1, a2, a3;
a1 = a2 = a3; //用户可能这样写
条款11:operator=处理自我赋值

这条规则是接上一条规则的,如果在实现operator=的时候,如果针对自我赋值的情况没有处理好,会出现以下两种结果:

class T;
class A {
public:
  A& operator=(const A& other)
  {
    delete pObj;  // 销毁原有的T对象
    pObj = new T(*other.pObj);   // T的拷贝构造函数
    return *this;
  }
private:
  T* pObj;
};

很显然,如果是自我赋值的情况,上面的语句会将自己的T对象释放掉,显然后面就会出问题了。

class T;
class A {
public:
  A& operator=(const A& other)
  {
    if (this == &other) return *this;  // 加入一个是否是自我赋值的判断
    delete pObj;  // 销毁原有的T对象
    pObj = new T(*other.pObj);   // T的拷贝构造函数,这个过程可能出现异常,如果出现异常,那么pObj就指向了一个已经销毁了的对象。
    return *this;
  }
private:
  T* pObj;
};
class T;
class A {
public:
  A& operator=(const A& other)
  {
    T* pObjOrig = pObj;
    pObj = new T(*other.pObj);    
    delete pObjOrig;
    return *this;
  }
private:
  T* pObj;
};
条款12:复制对象时勿忘其每一部分

这里的复制对象是指类的拷贝构造函数(copying)和赋值运算符(copy assignment),前者用于一个类实例对象的定义,后者用于两个已经定义的类实例之间的赋值。

class B {
public:
  B() {}
  virtual ~B() {}
  B(const B& other) {}
};
class D : public B {
public:
  D(const D& other) {}   // 这里没有定义基类部分的初始化,则默认调用基类的默认构造函数,即B()进行初始化
};

按照上面的代码,当使用D的拷贝构造函数进行初始化一个D类实例对象时,对于基类部分可能没有达到预期的复制效果,一般建议如下方式进行子类的拷贝构造函数定义:

class B {
public:
  B() {}
  virtual ~B() {}
  B(const B& other) {}
};
class D : public B {
public:
  D(const D& other) : B(other) {}  // 调用B类的拷贝构造函数对基类部分进行初始化   
class B {
public:
  B& operator=(const B& other) {}
};

class D : public B {
public:
  D& operator=(const D& other)
  {
    B::operator=(other);    // 调用基类的赋值运算符,对基类部分进行赋值
    ...
  }
};
上一篇 下一篇

猜你喜欢

热点阅读