【Effective C++详细总结】第二章 构造/析构/赋值运算

✍个人博客:https://blog.csdn.net/Newin2020?spm=1011.2415.3001.5343
📚专栏地址:C/C++知识点
📣专栏定位:整理一下 C++ 相关的知识点,供大家学习参考~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
🎏唠叨唠叨:在这个专栏里我会整理一些琐碎的 C++ 知识点,方便大家作为字典查询~

二、构造/析构/赋值运算

条款05:了解 C++ 默默编写并调用了哪些函数

在创建类时,如果自己不定义默认构造,拷贝构造(拷贝运算符),析构函数,那么编译器会自动生成这些函数。

//拷贝运算符:
classname& operator=(const classname& cn){......}

但是有些情况下编译器不会自动生成,拿下面这段代码举例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gj8IMl8Z-1678928093786)(《Effective C++》笔记.assets/image-20230305154051407.png)]

可以发现由于 class 里出现了引用类型和 const 类型,故编译器就不会自动生成拷贝复制和移动赋值函数。

这是因为引用类型是引用其他地方的内容,如果调用拷贝赋值和移动赋值可能会顺带改变引用处的值。

而 const 类型本身就是不可改动的,所以不会生成拷贝赋值和移动赋值函数。

另外 mutex 本身不能被拷贝和移动,所以拷贝构造和移动构造函数也不会自动生成。

条款06:若不想使用编译器自动生成的函数,就应该明确拒绝

对于类中拷贝构造函数,我们应当阻止他们。但若是不声明,编译器也会自动生成拷贝构造函数。

在现代 c++ 中我们可以通过 delete 关键字对编译器自动生成的函数进行删除,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UEMTv6xR-1678928093789)(《Effective C++》笔记.assets/image-20230306121822807.png)]

但在之前的 C++ 中并没有该关键字,所以可以用私有化的方式进行。

class person
{
private:
    person(const person&);
    person& operator=(const person&);
    //参数是不必要写的,毕竟这个函数不会被实现
public:
    ......
};

编译器自动生成的函数都是 public 函数,所以我们将 public 改为 private,就可以防止对象调用拷贝构造。

注:private 只有成员函数和友元函数可以调用。

同时也产生了一个问题,如何防止拷贝在成员函数或友元函数中被调用?

答案是建立一个父类,在父类中定义 private 拷贝函数,子类( person 等等)继承父类。因为子类不可以调用父类的 private 函数:

class uncopyable
{
private:
    uncopyable(const uncopyable&);
    uncopyable operator=(const uncopyable&);
};
 
class person{......};

条款07:为多态基类声明virtual析构函数

多态把父类当作一个接口,用以处理子类对象:利用父类指针,指向一个在堆区开辟的子类对象。

class person
{
public:
    person();
    ......
    ~person();
};
 
class teacher: public person{......};
 
person* p = new teacher(...); 
...
delete p;
//在堆区开辟的数据要手动删除

上述代码是有问题的。

我们知道,在普通类继承里,删除子类对象会先调用子类的析构,再调用父类的析构。但在多态里情况有所不同。我们删除的是父类指针,调用的只是父类的析构函数,子类析构不会被调用,也就是说,子类对象没有被删除,而指针却没了。这是局部销毁,会造成资源泄漏等错误。

幸运的是,我们可以通过虚函数来解决这个问题。

在多态里,虚函数可以让子类重写父类的函数,同时在虚函数表中生成一个指针,找到子类重写函数的地址,从而让我们可以通过父类访问子类重写的函数。

class person
{
public:
    person();
    ......
    virtual ~person();
};
 
class teacher: public person{......};
 
person* p = new teacher(...); 
...
delete p;
//删除 p 的时候调用 virtual ~person();
//virtual 找到子类析构函数的地址,导致子类也可以被删除

纯虚函数使得父类更像一个接口,这里不用多说。

注:多态里父类应该至少有一个虚函数(virtual 析构),若不用做多态,则类里不应该有虚函数。

条款08:别让异常逃离析构函数

释义:在析构函数内部处理异常

我们来看以下案例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sW0npgKS-1678928093790)(《Effective C++》笔记.assets/image-20230306123852685.png)]

如果在 db.close() 处发生异常,则会导致不可预料的情况。

首先介绍一下异常处理的办法:

try
{...}
//try 内部写可能产生异常的语句,没有产生异常,则catch语句不执行,产生则一一匹配
//catch 用于捕获并处理异常,和 case 有异曲同工之妙
catch(...)
{
    1、可以使用 abort(); 函数终止程序
    2、可以吞下这个异常,在 catch 内部做一些处理
}

了解如何处理异常之后,我们就可以实现如条款所说,在析构函数内部处理异常

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGGJsYF8-1678928093792)(《Effective C++》笔记.assets/image-20230306124023644.png)]

上面左边的方法是直接调用 abort 终止程序,右边则是直接吞下异常,只是记录个日志,后面再处理。

但是这两种方法都有个缺点就是用户无法参与操作,因此可以写成下面的方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xIVP9x75-1678928093806)(《Effective C++》笔记.assets/image-20230306124236886.png)]

用户可以自己实现一个 close 函数来进行关闭,如果关闭的顺利则 closed=true,反之关闭失败则会进行异常捕捉,在析构函数中帮助用户关闭。

条款09:绝不在构造和析构函数中使用虚函数

众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 virtual 找到子类析构,再析构父类),所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。来看下面这个例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-48pkwFU8-1678928093808)(《Effective C++》笔记.assets/image-20230306125300951.png)]

此时,如果父类的构造和析构函数中有 virtual,则该函数无法找到子类的地址(或者说无视子类,因为子类被销毁/未被初始化),使程序发生不明确的行为。

可以发现上面我是想调用派生类的构造函数和析构函数,但是调用的却是基类的。如果想满足该要求,我们可以去掉虚函数,而是在基类接收一个参数来实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmuRPNIP-1678928093809)(《Effective C++》笔记.assets/image-20230306125520145.png)]

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

释义:让赋值运算符重载版本返回一个自身,以便实现链式编程。

class employee{
public:
	int m_salary;
	
	employee(int a)//有参构造,赋工资初值
	{
		this->m_salary = a;
	}
	
	employee& operator=(const employee& ep)
	{
		this->m_salary = ep.m_salary;
		return *this;
	}
    //返回其本身
};

	employee e1(5000);
	employee e2(50000);
	employee e3(123456);
	
	e1 = e2 = e3;
    //链式编程

条款11:在 operator= 中处理自我赋值

我们来看一段代码:

class person{...};
person p;
p = p;

这是自我赋值,这种操作看起来有点愚蠢,但是并不很难发生。

比如,一个对象可以有很多种别名,客户不经意间让这些别名相等;

或者如之前所说,父类的指针/引用指向子类的对象,也会造成一些自我赋值的问题。

自我赋值往往没有什么意义,还会有不安全性。

class student{...};
class teacher
{
    ...
private:
    student* s;
};
 
teacher& teacher::operator=(const teacher& teach)
{
    if(s != NULL)
    {
        delete s;
        s = NULL;
    }
    s = new student(*teach.s);
    return *this;//便于链式操作
}

上述代码是不安全的。如果 *this 和 teach 是同一个对象,那么客户在删除 *this 的时候,也把 teach 删除了,s 就会指向一个被删除的对象,这是不允许的。

我们提供三种方法以解决这个问题:

1、证同检测:

teacher& teacher::operator=(const teacher& teach)
{
    if (this == &teach)
        return *this;
    //证同检测
    
    if (s != NULL)
    {
        delete s;
        s = NULL;
    }
    s = new student(*teach.s);
    return *this;//便于链式操作
}

遗憾的是,证同检测可以保证自我赋值的安全性,但是不能保证“异常安全性”。即,如果 new student 抛出异常,则 s 就会指向一个被删除的对象,这是一个有害指针,我们无法删除,甚至无法安全读取它。

2、记住原指针:

teacher& teacher::operator=(const teacher& teach)
{
    student* stu = s;            //记住原指针
 
    if(s != NULL)
    {
        delete s;
        s = NULL;
    }
 
    s = new student(*teach.s);   //如果抛出异常,s 也可以找回原来地址
    delete stu;                  //删除指针
 
    return *this;//便于链式操作
}

3、copy and swap:

void swap(const teacher& teach)
{......}
 
teacher& teacher::operator=(const teacher& teach)
{
    teacher temp(teach);    //拷贝一个副本
    swap(temp);             //将副本和 *this 交换 
    return *this;//便于链式操作
}

交换操作不要考虑原本指针内容,可以保证赋值安全性,同时也能保证异常安全性。

条款12:复制对象时勿忘其每一个成分

释义:自定义拷贝函数时,要把类变量写全(子类拷贝不要遗漏父类的变量)。

父类变量通常存储在 private 里,子类不能访问父类 private 对象,所以应该调用父类的构造函数。

class animal
{
public:
    animal(const animal& an)
    {......}
    animal& opeartor=(const animal& an)
    {......}
......
private:
    string typename;
};
 
class cat: public animal
{
public:
    cat(const cat& c);
    cat& operator=(const cat& c);
 
private:
    string cat_type;
};
 
cat::cat(const cat& c)
    :cat_type(c.cat_type),
    //为了不遗漏父类变量,调用父类函数
    animal(c)
{}
 
cat::cat& operator=(const cat& c)
{
    //为了不遗漏父类变量,调用父类函数
    animal::operator=(c);
    this->cat_type = c.cat_type;
    return *this;
}

值得注意的是,上面代码 copy 函数和 “=” 运算符调用的都是和本身一样的函数。究其原因,copy 函数是创建一个新的对象,operator= 是对已经初始化的对象进行操作。

我们不能用 copy 调用 operator=,因为这相当于用构造函数初始化一个新对象(父类尚未构造好)。

同理,也不能用 operator= 调用 copy,这相当于构造一个已经存在的对象(父类已经存在了)。

参考资料:
《Effective C++》侯捷:https://book.douban.com/subject/1842426/
EFFECTIVE C++ (万字详解)(一)_c++ effective:https://blog.csdn.net/qq_62674741/article/details/124896986
一文整理Effective C++ 55条款内容(全):https://blog.csdn.net/weixin_45926547/article/details/121276226?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2defaultCTRLISTRate-1-121276226-blog-124896986.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultCTRLISTRate-1-121276226-blog-124896986.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/467.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

理清gcc、g++、libc、glibc、libstdc++的关系

0 理清gcc、g++、libc、glibc、libstdc++的关系 0.1 $ dpkg -L libc6 $ dpkg -L libc6 /lib/x86_64-linux-gnu /lib/x86_64-linux-gnu/ld-2.31.so /lib/x86_64-linux-gnu/libBrokenLocale-2.31.so /lib/x86_64-linux-gnu/libSegFault.so /lib/x86_64-linux-gnu/libanl-2.31.s…

Java NIO Buffer

Buffer是一块内存,主要用在NIO Channel,比如FileChannel,SocketChannel。 对Channel的读写都是直接操作Buffer对象。 Buffer是一个工具类,提供了操作这个内存块的方法。 Buffer的实现主要有以下几种: Buffer的类型: …

我一个普通程序员,光靠GitHub打赏就年入70万,

一个国外程序员名叫 Caleb Porzio在网上公开了自己用GitHub打赏年入70万的消息和具体做法。 Caleb Porzio 发推庆祝自己靠 GitHub 打赏(GitHub Sponsors)赚到了 10 万美元。 GitHub Sponsors是 GitHub 2019 年 5 月份推出的一个功能,允许开发…

ConvMixer:Patches Are All You Need

Patches Are All You Need 发表时间:[Submitted on 24 Jan 2022]; 发表期刊/会议:Computer Vision and Pattern Recognition; 论文地址:https://arxiv.org/abs/2201.09792; 代码地址:https:…

Redis 主从库如何实现数据一致?

目录 1、主从库间如何进行第一次同步? 2、主从级联模式分担全量复制时的主库压力 3、主从库间网络断了怎么办? 总结 // 好的文章,值得反复去读 Redis 具有高可靠性,这里有两层含义:一是数据尽量少丢失,…

【Copula】基于二元Frank-Copula函数的风光出力场景生成方法【考虑风光出力的不确定性和相关性】(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

SpringBoot:SpringBoot 的底层运行原理解析

声明原文出处:狂神说 文章目录1. pom.xml1 . 父依赖2 . 启动器 spring-boot-starter2. 主启动类的注解1. 默认的主启动类2. SpringBootApplication3. ComponentScan4. SpringBootConfiguration5. SpringBootApplication 注解6. spring.factories7. 结论8. 简单图解3…

【Python】如何使用Pandas进行数据可视化?

如何使用Pandas进行数据可视化?1. 如何创建简单图?1.1 创建线型图1.2 绘制直方图1.3 绘制条形图1.4 绘制饼图1.5 绘制散点图2. Plot方法有哪些?3. 如何定制图表的样式和颜色?4. 如何同时对多个DataFrame绘图?5. 总结参…

K8s运维-高级网络策略介绍

1什么是NetworkPolicy?如果你希望在 IP 地址或端口层面(OSI 第 3 层或第 4 层)控制网络流量, 则你可以考虑为集群中特定应用使用 Kubernetes 网络策略(NetworkPolicy)。NetworkPolicy 是一种以应用为中心的…

【1615. 最大网络秩】

来源:力扣(LeetCode) 描述: n 座城市和一些连接这些城市的道路 roads 共同组成一个基础设施网络。每个 roads[i] [ai, bi] 都表示在城市 ai 和 bi 之间有一条双向道路。 两座不同城市构成的 城市对 的 网络秩 定义为&#xff…

从0到1构建springboot web应用镜像并使用容器部署

文章目录一、生成镜像的两种方法1.1、使用commit生成镜像1.1.1、拉取Centos基础镜像1.1.2、启动Centos容器并安装Go1.1.3、commit生成新镜像1.1.4、使用新镜像验证Golang环境1.2、使用Dockerfile生成镜像二、基于Dockerfile生成一个springboot镜像2.1、准备springboot应用jar包…

python自动化办公(一)

本文代码参考其他教程书籍实现。 文章目录文件读写open函数读取文本文件写入文本文件文件和目录操作使用os库使用shutil库文件读写 open函数 open函数有8个参数,常用前4个,除了file参数外,其他参数都有默认值。file指定了要打开的文件名称&a…

FreeRTOS系列第1篇---为什么选择FreeRTOS?

1.为什么学习RTOS? 作为基于ARM7、Cortex-M3硬件开发的嵌入式工程师,我一直反对使用RTOS。不仅因为不恰当的使用RTOS会给项目带来额外的稳定性风险,更重要的是我认为绝大多数基于ARM7、Cortex-M3硬件的项目,还没复杂到使用RTOS的地…

【华为机试真题详解 Python实现】最差产品奖【2023 Q1 | 100分】

文章目录 前言题目描述输入描述输出描述示例 1题目解析参考代码前言 《华为机试真题详解》专栏含牛客网华为专栏、华为面经试题、华为OD机试真题。 如果您在准备华为的面试,期间有想了解的可以私信我,我会尽可能帮您解答,也可以给您一些建议! 本文解法非最优解(即非性能…

SpringBoot和Spring AOP默认动态代理方式

SpringBoot和Spring AOP默认动态代理方式 目录SpringBoot和Spring AOP默认动态代理方式1. springboot 2.x 及以上版本2. Springboot 1.x3.SpringBoot 2.x 为何默认使用 CglibSpring 5.x中AOP默认依旧使用JDK动态代理SpringBoot 2.x开始,AOP为了解决使用JDK动态代理可…

做技术,最忌讳东张西望

又好长时间没更新,研二了,忙着做实验、写论文、发论文,再加上给我导做一些事情(都习惯了,以前很不爽的事情,现在居然能这么平静的说出来)。 但这不是我今天说的重点,而是另外一件事…

【开发工具】idea配置全局变量Jdk、maven仓库、maven(全文图解)

文章目录IDEA配置JDK1、点击File -->Project Structure;2、点击左侧标签页SDKs选项,再点击左上角“”,选择JDK;3、在弹出框选择JDK安装路径,点击OK即可配置成功。配置maven仓库(阿里云)1、配…

素材要VIP咋整?看python大展神通

前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 再我们缺少素材的时候,我们第一反应 我们肯定会去网上寻找,但是!! 有的素材需要VIP!这可咋整呢? 看我利用python大展神通,采集某图网图片…

面试官:关于CPU你了解多少?

CPU是如何执行程序的? 程序执行的基本过程 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准…

Altium Designer(AD)软件使用记录11-PCB布线部分之走线

目录Altium Designer(AD)软件使用记录11-PCB布线部分之走线核心-SDRAM-FLASH 模块走线BGA 滤波电容放置处理其他杂线走线清理Altium Designer(AD)软件使用记录11-PCB布线部分之走线 核心-SDRAM-FLASH 模块走线 走线总结: 走线从核心器件部分,线路密度最…