条款10和条款13

简介: 条款10和条款13

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

首先来看一个例子:

class A {
public:
  A() {
    cout << "defalut constructor" << endl;
  }
  ~A() {
    cout << "destructor" << endl;
  }
  void operator=(const A &a);
  int number;
};
void A::operator=(const A &a)
{
  this->number = a.number;
  return;
}
int main()
{
  A a;
  a.number = 5;
  A a1;
  a1 = a;
  cout << a1.number << endl;
  return 0;
}

运行这段程序会发现程序并没有报错,并且输出了正确的结果。当然例子比较简单,只进行了一次赋值操作,但如果我们进行连等赋值时,就像这样:

int main()
{
  A a;
  a.number = 5;
  A a1;
  A a2;
  a2 = a1 = a;
  cout << a2.number << endl;
  return 0;
}

会发现程序报错了:

error: 二进制“=”: 没有找到接受“void”类型的右操作数的运算符(或没有可接受的转换)

返观上面我们实现的赋值运算符,发现当完成赋值操作时返回void。按照从右向左赋值的操作a1 = a没有毛病,但是它们结果返回了一个void,而a2 = void显然是一个非法的操作,因为它与我们赋值运算符要求的参数类型不一致。因此,我们应该给a2赋值一个A类型的对象,比如说a1。因此我们又有如下代码:

class A {
public:
  A() {
    cout << "defalut constructor" << endl;
  }
  ~A() {
    cout << "destructor" << endl;
  }
  A operator=(const A &a);
  int number;
};
A A::operator=(const A &a)
{
  this->number = a.number;
  return *this;
}
int main()
{
  A a;
  a.number = 5;
  A a1;
  A a2;
  a2 = a1 = a;
  cout << a2.number << endl;
  return 0;
}

这时代码又行了,因为a1 = a执行后,返回了a1对象,将a1再给a2没有问题。那这不是解决了连等赋值的问题吗。为什么要返回引用呢?

当我们观察上面程序输出时发现:

defalut constructor
defalut constructor
defalut constructor
destructor
destructor
5
destructor
destructor
destructor

三个A类型的对象对应三次默认构造函数没有问题,而下面为什么析构了五次呢?让我们打印一下地址:

class A {
public:
  A() {
    cout << this << " : defalut constructor" << endl;
  }
  ~A() {
    cout << this << " : destructor" << endl;
  }
  A operator=(const A &a);
  int number;
};

程序输出:

000000F14274F504 : defalut constructor
000000F14274F524 : defalut constructor
000000F14274F544 : defalut constructor
000000F14274F644 : destructor
000000F14274F624 : destructor
5
000000F14274F544 : destructor
000000F14274F524 : destructor
000000F14274F504 : destructor

三个A类型对象的构造函数和析构函数,根据地址都可以对应起来,中间两个析构函数是哪里来的呢?

这是因为发生了拷贝操作,因为我们的赋值运算符返回的是值,而不是引用。

来验证一下:

class A {
public:
  A() {
    cout << this << " : defalut constructor" << endl;
  }
  ~A() {
    cout << this << " : destructor" << endl;
  }
  A(const A& a) {
    this->number = a.number;
    cout << "&a : " << &a << endl;
    cout << this << " : copy constructor" << endl;
  }
  A operator=(const A &a);
  int number;
};
A A::operator=(const A &a)
{
  this->number = a.number;
  cout << "&a : " << &a << endl;
  cout << this << " : = operator" << endl;
  return *this;
}

程序输出:

000000AD0AAFF7E4 : defalut constructor
000000AD0AAFF804 : defalut constructor
000000AD0AAFF824 : defalut constructor
&a : 000000AD0AAFF7E4
000000AD0AAFF804 : = operator
&a : 000000AD0AAFF804
000000AD0AAFF904 : copy constructor
&a : 000000AD0AAFF904
000000AD0AAFF824 : = operator
&a : 000000AD0AAFF824
000000AD0AAFF924 : copy constructor
000000AD0AAFF924 : destructor
000000AD0AAFF904 : destructor
5
000000AD0AAFF824 : destructor
000000AD0AAFF804 : destructor
000000AD0AAFF7E4 : destructor

仔细看一些地址变化,是不是一切都迎刃而解了。赋值运算符完成后会发生一个拷贝行为,拷贝给一个临时变量

,然后再用临时变量赋值。当赋值操作很长时,也就意味着会产生很多临时变量,那我们为何不直接返回引用呢,这也正是这条准则说的。

让我们看一下返回引用的结果:

class A {
public:
  A() {
    cout << this << " : defalut constructor" << endl;
  }
  ~A() {
    cout << this << " : destructor" << endl;
  }
  A(const A& a) {
    this->number = a.number;
    cout << "&a : " << &a << endl;
    cout << this << " : copy constructor" << endl;
  }
  A &operator=(const A &a);
  int number;
};
A &A::operator=(const A &a)
{
  this->number = a.number;
  cout << "&a : " << &a << endl;
  cout << this << " : = operator" << endl;
  return *this;
}

程序输出:

000000FECFFEF824 : defalut constructor
000000FECFFEF844 : defalut constructor
000000FECFFEF864 : defalut constructor
&a : 000000FECFFEF824
000000FECFFEF844 : = operator
&a : 000000FECFFEF844
000000FECFFEF864 : = operator
5
000000FECFFEF864 : destructor
000000FECFFEF844 : destructor
000000FECFFEF824 : destructor

可以发现中间省略了拷贝行为,提高了效率。

当然这个条款不仅适用于 = ,还有其他的赋值相关运算符比如 += 等。

条款13:以对象管理资源

场景:

void func() {
    Example *example = new Example();
    if (条件满足) {
        //这里也应该回收资源
        return;
    }
    delete example;
    return;
}

上诉代码中如果if条件成立函数直接return ; 没有释放内存,导致了内存泄漏。上述例子在开发中极易遇到,尤其是逻辑较复杂时,很容易忽略这样的细节。对此有一些解决方案,比如说使用goto语句,在结尾处统一释放内存,但实际开发中并不提倡使用goto语句。还有一种do…while(0)的妙用:

void func() {
  Example *example = new Example();
  do {
    if (条件满足) {
      break;
    }
  }while(0);
  delete example;
  return;
}

这里巧妙的用到了break的特性。还有一种RAII(资源获取就是初始化)是指拿到资源后初始化,当不需要资源时,自动释放该资源。比如我们可以用一个单独的类管理资源,当出了作用域后,自动调用该类的析构函数释放资源。(以上参考《c++服务器开发精髓》 张远龙 著 一书)

方法多种多样,然而c++中提供了一种智能指针来管理资源

比如auto_ptr

//需要引入头文件#include <memory>
class A {
public:
  A() {
    cout << "defult constructor" << endl;
  }
  ~A() {
    cout << "destructor" << endl;
  }
  int number;
};
int main() {
  std::auto_ptr<A> a(new A());
  if (true) {
    return 0;
  }
  return 0;
}

当我们用智能指针管理对象时,无论程序什么时候退出,都能正确的析构。

如上程序输出:

defult constructor
destructor

auto_ptr的一个特性是,它在被销毁时会自动删除它所指之物,因此不能让多个auto_ptr同时指向同一对象,这是一种错误的行为:

int main() {
  A *a = new A();
  std::auto_ptr<A> autoptr(a);
  std::auto_ptr<A> autoptr1(a);
  return 0;
}

为了避免这个问题,auto_ptr在发生拷贝或赋值操作时,原来的指针将指向空,而复制的指针将取得资源的唯一拥有权。

看一个例子:

class A {
public:
  A() {
    cout << "defult constructor" << endl;
    cout << "&a : " << this << endl;
  }
  ~A() {
    cout << "destructor" << endl;
  }
};
int main() {
  A *a = new A();
  std::auto_ptr<A> autoptr(a);
  cout << "autoptr.get() : " << autoptr.get() << endl;
  std::auto_ptr<A> autoptr1(autoptr);
  cout << "autoptr1.get() : " << autoptr1.get() << endl;
  cout << "autoptr.get() : " << autoptr.get() << endl;
  std::auto_ptr<A> autoptr2;
  autoptr2 = autoptr1;
  cout << "autoptr2.get() : " << autoptr2.get() << endl;
  cout << "autoptr1.get() : " << autoptr1.get() << endl;
  return 0;
}

程序输出:

defult constructor
&a : 00000215BE4A7DD0
autoptr.get() : 00000215BE4A7DD0
autoptr1.get() : 00000215BE4A7DD0
autoptr.get() : 0000000000000000
autoptr2.get() : 00000215BE4A7DD0
autoptr1.get() : 0000000000000000
destructor

非常直观吧

但是回过来想想,发生拷贝尽然是掠夺行为,这显然不太合适,因此c++中还有一种shared_ptr

它是通过引用计数的方式来管理资源,详情参考我写的关于实现一个智能指针shared_ptr

最后引用一下书中的总结:

由于tr1::shared_ptrs的复制行为“一如预期”,它们可被用于STL容器以及其他“auto_ptr之非正统复制行为并不适用“的语境上。
尽管如此,本条款并不专门针对auto_ptr,tr1::shared_ptr或任何其他智能指针,而只是强调”以对象管理资源“的重要性,
auto_ptr和tr1::shared_ptr只不过是实际例子。
相关文章
|
8月前
|
缓存 编译器 C++
条款23和条款24
条款23和条款24
|
8月前
|
编译器
条款3 尽可能使用const
条款3 尽可能使用const
|
7月前
|
编译器 C++
条款03:尽可能使用const
条款03:尽可能使用const
|
8月前
|
安全
AIGC知识产权赔偿条款
【2月更文挑战第9天】AIGC知识产权赔偿条款
70 3
AIGC知识产权赔偿条款
|
8月前
|
编译器 C++
条款5和条款16
条款5和条款16
|
8月前
条款9和条款22
条款9和条款22
|
8月前
|
编译器 C++
条款6和条款7
条款6和条款7
|
8月前
|
编译器 C语言 C++
条款20和条款21
条款20和条款21
|
存储 安全 JavaScript
延期公告:OV代码签名证书私钥保护新规,延期至明年6月执行
今年6月发布的OV代码签名证书私钥保护新规,原定于2022 年11月15日开始执行,现确定将延期至2023年6月1日执行。
134 0
延期公告:OV代码签名证书私钥保护新规,延期至明年6月执行
|
Android开发 开发者 iOS开发
阿里云软著申请|这项保护,让我得到了10万赔偿!
对于企业来说,申请软件著作权是证明自己和保护自己的强力护盾。除此之外,它还有着很多不可忽视的意义与价值。阿里云软著申请,一站式智能服务,助力企业和开发者高效发展,省时省力更省心。
2226 0
阿里云软著申请|这项保护,让我得到了10万赔偿!