条款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只不过是实际例子。
相关文章
|
2天前
|
云安全 数据采集 人工智能
古茗联名引爆全网,阿里云三层防护助力对抗黑产
阿里云三层校验+风险识别,为古茗每一杯奶茶保驾护航!
古茗联名引爆全网,阿里云三层防护助力对抗黑产
|
2天前
|
存储 机器学习/深度学习 人工智能
大模型微调技术:LoRA原理与实践
本文深入解析大语言模型微调中的关键技术——低秩自适应(LoRA)。通过分析全参数微调的计算瓶颈,详细阐述LoRA的数学原理、实现机制和优势特点。文章包含完整的PyTorch实现代码、性能对比实验以及实际应用场景,为开发者提供高效微调大模型的实践指南。
438 1
|
3天前
|
传感器 人工智能 算法
数字孪生智慧水务系统,三维立体平台,沃思智能
智慧水务系统融合物联网、数字孪生与AI技术,实现供水全流程智能监测、预测性维护与动态优化。通过实时数据采集与三维建模,提升漏损控制、节能降耗与应急响应能力,推动水务管理从经验驱动迈向数据驱动,助力城市水资源精细化、可持续化管理。
274 143
|
2天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段四:学术分析 AI 项目 RAG 落地指南:基于 Spring AI 的本地与阿里云知识库实践
本文介绍RAG(检索增强生成)技术,结合Spring AI与本地及云知识库实现学术分析AI应用,利用阿里云Qwen-Plus模型提升回答准确性与可信度。
211 91
AI 超级智能体全栈项目阶段四:学术分析 AI 项目 RAG 落地指南:基于 Spring AI 的本地与阿里云知识库实践
|
17天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
2天前
|
机器学习/深度学习 人工智能 运维
智能照明稳压节能控制器,路灯节能稳压系统,沃思智能
智能照明调控柜集电力分配、远程控制与能耗管理于一体,支持自动调光、场景切换与云平台运维,广泛应用于市政、商业及工业领域,显著节能降耗,助力智慧城市建设。
188 137
kde
|
2天前
|
人工智能 关系型数据库 PostgreSQL
n8n Docker 部署手册
n8n是一款开源工作流自动化平台,支持低代码与可编程模式,集成400+服务节点,原生支持AI与API连接,可自托管部署,助力团队构建安全高效的自动化流程。
kde
291 3

热门文章

最新文章