c++ 三五法则 自己理解
简介
三五法则规定了什么时候需要 1 拷贝构造函数 2 拷贝赋值函数 3 析构函数
1. 需要析构函数的类也需要拷贝构造函数和拷贝赋值函数。
通常,若一个类需要析构函数,则代表其合成的析构函数不足以释放类所拥有的资源,其中最典型的就是指针成员(析构时需要手动去释放指针指向的内存)。
所以,若存在自定义(且正确)的析构函数,但使用合成的拷贝构造函数,那么拷贝过去的也只是指针,此时两个对象的指针变量同时指向同一块内存,指向同一块内存的后果很有可能是在两个对象中的析构函数中先后被释放两次。所以需要额外的拷贝控制函数去控制相应资源的拷贝。
所以这类例子的共同点就是:一个对象拥有额外的资源(指针指向的内存),但另一个对象使用合成的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后,析构函数释放了资源,这时另一个对象便失去了这块资源(但程序员还不知道)。
class person
{
public:
std::string *name;
int age;
person(const char* the_name, int the_age)
{
name = new std::string(the_name);
age = the_age;
}
~person()
{
delete name;
}
};
int main(void)
{
person a("me", 20);
person b(a);
std::cout << *b.name << std::endl;
return 0;
}
在上面的代码中对象b使用合成的拷贝构造函数拷贝对象a的值,这个程序没有什么实际意义。
在main函数返回时,a,b变量会分别被析构,它们的成员name指向同一块内存,所以在程序结束时便会发生错误。
2. 需要拷贝操作的类也需要赋值操作,反之亦然。
需要拷贝操作代表这个类在拷贝时需要进行一些额外的操作。 赋值操作 <<< = >>> 先析构+拷贝,所以拷贝需要的赋值也需要。反之亦然。
3. 析构函数不能是删除的
如果类的析构函数是删除的,那么成员便无法销毁。所以在程序中不能定义这个类的对象。可以动态分配该对象并获得其指针,但无法销毁这个动态分配的对象(delete 失效)。
若上面的类的定义是
class person
{
public:
std::string *name;
int age;
person(const char* the_name, int the_age)
{
name = new std::string(the_name);
age = the_age;
}
private:
~person()
{
delete name;
}
};
则在main函数中定义变量a,b就会发生编译错误,然而,这样的定义却可以通过编译 person *p;
p = new person("me", 20)
但是,这样动态分配的变量是不能被释放的,在调用 delete p
会发生编译错误, 内存泄露就这样发生了。
如果类的某个成员的析构函数是删除的或不可访问的(例 private的)则类的合成析构函数被定义为删除的 (小析构NO 大析构NO)
如果类的某个成员的拷贝构造函数是删除的或不可访问的(例 private的)则类的合成拷贝函数被定义为删除的 (小拷贝NO 大拷贝NO)
如果类的某个成员的析构函数是删除的或不可访问的(例 private的)则类的合成拷贝函数被定义为删除的 (小析构NO 大拷贝NO)
如果类的某个成员的析构函数是删除的或不可访问的(例 private的)则类的合成拷贝函数被定义为删除的
4. 如果一个类成员有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的。
如果没有这条规则,可能会创造出无法被删除的对象。 理论上来说,当析构函数不能被访问时,任何静态定义的对象都不能通过编译器的编译,所以这种情况只会出现在与动态分配有关的拷贝/默认构造函数身上。
5. 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作。(无法默认构造的const成员的类 则该类就无默认构造函数)
原因很简单,const或引用成员只能在初始化时被赋值一次,而合成的拷贝赋值操作会对所有成员都进行赋值。显然,它不能赋值const和引用成员,所以合成的拷贝构造函数不能被使用,即会被定义为删除的。
本质上,当不可能拷贝、赋值、或销毁类的所有成员时,类的合成拷贝控制函数就被定义成删除的了。
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
以下这些内容 来自 c++ primer 第五版一书 归纳而来 详细内容可以 看书 值和指针类型的类设计等
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
拷贝构造函数第一个参数必须是一个引用类型。此参数几乎总是一个const的引用。拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit的。
合成拷贝构造函数
与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。
对某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象。而一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。每个成员的类型决定了它如何拷贝。
拷贝初始化
直接初始化和拷贝初始化的差异。
string dots(10,','); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
拷贝初始化通常使用拷贝构造函数来完成。拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的。
拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生
•将一个对象作为实参传递给一个非引用类型的形参。
•从一个返回类型为非引用类型的函数返回一个对象。
•用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
参数和返回值
拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果其参数不是引用类型,则调用永远也不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又必须调用拷贝构造函数,如此无限循环。
拷贝初始化的限制
vector<int> v1(10); //直接初始化
vector<int> v1 = 10; //错误:接受大小参数的构造函数是explicit的
如果我们希望使用一个explicit构造函数,就必须显式的使用:
void f(vector<int>); //f的参数进行拷贝初始化
f(10); //错误:不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10)); //正确:从一个int直接构造一个临时vector
如果我们希望使用一个explicit构造函数,就必须显式的使用:
编译器可以绕过拷贝构造函数
编译器被允许将下面的代码string null_book = "9-999-99999-9";
给写成string null_book("9-999-99999-9");//编译器略过了拷贝构造函数。
拷贝赋值运算符
类通过拷贝赋值运算符控制其对象如何赋值。
重载赋值运算符
重载赋值运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。因此,赋值运算符就是一个名为operator=的函数。类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表。
值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
合成拷贝赋值运算符
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为他它生成一个合成拷贝赋值运算符。类似拷贝构造函数,对于某些类,合成拷贝构造运算符用来禁止该类型对象的赋值。
析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。
由于析构函数不接受参数,因此它不能被重载,对一个给定类,只会有唯一一个析构函数。
析构函数完成什么工作
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常,析构函数释放对象在生存期分配的所有资源。
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式的销毁一个内置指针类型的成员不会delete它所指向的对象。
什么时候会调用析构函数
无论何时一个对象被销毁,就会自动调用其析构函数:
•变量在离开其作用域时被销毁。
•当一个对象被销毁时,其成员被销毁。
•容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
•对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
•对于临时对象,当创建它的完整表达式结束时被销毁。
由于析构函数自动运行,我们的程序可以按需要分配资源,而(通常)无须担心何时释放这些资源。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
合成析构函数
下面的代码片段等价于Sales_data的合成析构函数:
class Sales_data{
public:
~Sales_data(){}
};
在(空)析构函数体执行完毕后,成员会被自动销毁。认识到析构函数体自身并不直接销毁成员是非常重要的。
c++ 三五法则 自己理解的更多相关文章
- [C++]类的设计(2)——拷贝控制(析构和三五法则)
1.析构函数:释放对象使用的资源,并销毁对象的非static数据成员:析构函数不接受参数,因此不能被重载.对于一个给定类,有且只有一个析构函数. 2.析构函数的组成:一个函数体+一个析构部分(im ...
- 通俗的理解java设计模式的准则
本文部分内容摘抄自https://www.cnblogs.com/dolphin0520/p/3919839.html,加入了自己的理解: 一.单一职责原则 原文链接:http://blog.csdn ...
- [C++ Primer] : 第13章: 拷贝控制
拷贝, 赋值与销毁 当定义一个类时, 我们显示地或隐式地指定在此类型的对象拷贝, 移动, 赋值和销毁时做什么. 一个类通过定义5种特殊的成员函数来控制这些操作, 包括: 拷贝构造函数, 拷贝赋值运算符 ...
- 《More Effective C++》读书笔记(零)Basic 基础条款
这是篇读书笔记,只记录自己的理解和总结,一般情况不对其举例子具体说明,因为那正是书本身做的事情,我的笔记作为梳理和复习之用,划重点.我推荐学C++的人都好好读一遍Effective C++ 系列,真是 ...
- 技术学到多厉害,才能顺利进入BAT?
简介 本科的时候对 Linux 特别感兴趣,心中向往成为一名运维工程师,就开始没日没夜的看相关的书籍,到了大约2013年前后的时候发现 DevOps 开始流行起来了,就开始学习 Python 希望成为 ...
- 【c++ Prime 学习笔记】第15章 面向对象程序设计
15.1 OOP:概述 面向对象程序设计(object-oriented programming)的核心思想是:数据抽象.继承.动态绑定 使用数据抽象,可将类的接口与实现分离 使用继承,可定义相似的类 ...
- [刷题]算法竞赛入门经典(第2版) 4-7/UVa509 - RAID!
书上具体所有题目:http://pan.baidu.com/s/1hssH0KO 代码:(Accepted,0 ms) //UVa509 - RAID! #include<iostream> ...
- C#之设计模式之六大原则(转载)
设计模式之六大原则(转载) 关于设计模式的六大设计原则的资料网上很多,但是很多地方解释地都太过于笼统化,我也找了很多资料来看,发现CSDN上有几篇关于设计模式的六大原则讲述的比较通俗易懂,因此转载过来 ...
- C# 23种设计模式
目录 0).简单工厂模式 1).工厂方法模式 2).抽象工厂模式 3).单例模式 4).构建者模式 5).原型模式 6).适配器模式 7).修饰者模式 8).代理模式 9).外观模式 10).桥接模式 ...
随机推荐
- 微信小程序--关于加快小程序开发的几个小建议
加快小程序开发的几个小建议 1.使用 app.json创建页面 按照我们平常的开发习惯,创建一个新的页面,一般都会先创建文件夹,再创建对应page的形式,创建完成后,app.json中会自动注册该 ...
- [leetcode]61. Rotate List反转链表k个节点
类似于找链表的后k个节点 不同的是要把前边的接到后边 public ListNode rotateRight(ListNode head, int k) { //特殊情况 if (head==null ...
- 如何重新加载 Spring Boot 上的更改,而无需重新启动服务器?
这可以使用 DEV 工具来实现.通过这种依赖关系,您可以节省任何更改,嵌入式 tomcat将重新启动.Spring Boot 有一个开发工具(DevTools)模块,它有助于提高开发人员的生产力.Ja ...
- ATM管理系统
一.题目要求 编写一个ATM管理系统,语言不限,要求应包括以下主要功能: (1)开户,销户 (2)查询账户余额 (3)存款 (4)取款 (5)转账(一个账户转到另一个账户)等 二.代码提交 开户 pu ...
- 改进你的c#代码的5个技巧(一)
亲爱的读者,在这篇文章中,我提供了一些c#编程的最佳实践. 你是否在用户输入验证中使用异常处理机制? 如果是,那么你就是那个把你的项目执行速度降低了62倍的人.你不相信我吗?等几分钟;我来教你怎么做. ...
- 第九章节 BJROBOT 多点导航【ROS全开源阿克曼转向智能网联无人驾驶车】
1.把小车平放在地板上,用资料里的虚拟机,打开一个终端 ssh 过去主控端启动roslaunch znjrobot bringup.launch. 2.再打开一个终端,ssh 过去主控端启动 rosl ...
- Alpha冲刺——代码规范与计划
这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzzcxy/2018SE1 这个作业要求在哪里 https://edu.cnblogs.com/campus/fz ...
- python常用操作和内置函数
一.常用数据处理方法. 1.索引:按照号码将对应位置的数据取出使用 2.list将任意类型数据用逗号分割存在列表中 3.range:产生一堆数字(顾头不顾尾) 4.切片:可以从复制数据的一部分,不影响 ...
- 几幅图,拿下 HTTPS
我很早之前写过一篇关于 HTTP 和 HTTPS 的文章,但对于 HTTPS 介绍还不够详细,只讲了比较基础的部分,所以这次我们再来深入一下 HTTPS,用实战抓包的方式,带大家再来窥探一次 HTTP ...
- Go从入门到放弃(笔记存档)
前言 考虑到印象笔记以后不续费了,这里转存到博客园一份 因内容是自己写的笔记, 未作任何润色, 所以看着很精简, 请见谅 查看官方文档 在新的go安装包中,为了减小体积默认去除了go doc 安装go ...