《Effective C++》读书笔记(二):构造/析构/赋值运算(条款05~条款12)

news2024/12/24 0:27:21

目录

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

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

3. 条款07:为多态基类virtual析构函数

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

5.条款09:绝不在构造和析构过程中调用virtual函数

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

7.条款11:在operator=中处理“自我赋值”

8.条款12:赋值对象时勿忘其每一个成分


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

说白了,看到这个条款,我就马上想到了类和对象的六个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载、普通对象和const对象取地址的重载。

对于这六大默认成员函数,详细的解析在类和对象这篇博客中,我将在这里简单的总结一下编译器是如何调用它们的。

对于构造函数和析构函数:对于内置类型,C++中选择不处理,也就是内置类型在构造函数中会是随机值,因此在C++11中,可以在声明的时候顺带定义一下。而对于类中的自定义类型,它们会自动调用的构造和析构函数,如果是别的类的自定义类型,则会到它们自己的类中去调用它们的构造和析构函数。在多态中,基类先构造,然后再是派生类构造。析构的时候,先是派生类先析构,然后是基类析构。

书中的补充①:需要注意的是编译器产生的析构函数并非虚函数。

书中补充②编译器拒绝为类生出operator=的情况:

第一种情况:类的成员变量中,存在引用的声明

第二种情况:存在const修饰的成员变量。

#include<iostream>
#include<string>

template<class T>
class NamedObject
{
public:
	NamedObject(std::string& name = "", const T& value = 0)
		:nameValue(name)
		,objectValue(value)
	{}
	//...没有声明赋值重载,按道理来说会默认生成出来

private:
	std::string& nameValue;//引用类型
	const T objectValue = 0;//const 类型
};

int main()
{
	std::string newDog("feifei");//我现在的狗叫肥肥,是柯基跟田园犬的结合
	std::string oldDog("tiantian");//肥肥的妈妈叫天天,是一个聪明的柯基
	NamedObject<int> p(newDog, 4);//肥肥四岁了
	NamedObject<int> s(oldDog, 4);//天天的年龄永远停留在了四岁......

	p = s;//error,报错显示operator是已删除的函数

	return 0;
}

赋值不成功的理由很简单,引用的指向是不可以被改变的,赋值的话就说明要改变引用指向的对象。同样的,对const成员函数也是一样不能被改变!这种情况就必须自己定义一个赋值重载函数。

还有一种情况是在继承的情况下,基类将自己的赋值重载函数设为私有的,那么编译器就会拒绝给派生类默认生成赋值重载函数。理由是,派生类继承基类的时候,会继承基类的某些成分,编译器要处理这些成分,但是因为无法调用派生类无权调用的基类成员函数,因此也就没办法了。

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

其实这里就是不想让用户能够调用这些成员函数,比如拷贝构造,赋值重载,一开始想到的办法就是不实现它们,但是上面我们说了,我们不写,编译器会自己生成的!

因此办法就是设置为私有的,并且不实现这个函数。这样,没有人能够在类外调用这个函数,也不能在类中调用了。

class A
{
public:
	//...
	A() {}

	~A() {}
private:
	//只声明,并且设为私有
	A(const A&);
	A& operator=(const A&);
};

或者直接删除,这是C++11新增的一种方法,使用delete关键字

class A
{
public:
	//...
	A() {}

	~A() {}
private:
	//删除了
	A(const A&) = delete;
	A& operator=(const A&) = delete;
};

还有一种方法就是继承的方法,只要把基类的拷贝构造函数和赋值重载私有或者删掉,派生类就不可能会默认生成拷贝构造和函数重载出来!理由是派生类实例化出来对象后,有一部分的成分是基类的,因此需要调用基类的对应的成员函数,如果进行了赋值或者拷贝的操作,就需要调用基类的对应的函数,而基类的这些函数被删掉了或者私有化,反正调用不了,此时派生类的拷贝构造和赋值重载也不能用了!

这样做的好处是,如果有人在类中调用了这些被私有化的函数,或者使用友元,那么会在连接期出现错误,而并非编译期的错误。如果是发生在连接期的错误,这种错误很难侦测出来!因此,这种做法就是将连接错误转移到了编译期,那么只要有人试图调用这些规定不能用的函数,就会在编译期报错!

class A_Father
{
public:
	A_Father() {}
	~A_Father(){}
private:
	A_Father(const A_Father&);
	A_Father& operator=(const A_Father&);
};

class A:public A_Father
{
public:
	A() {}
	~A() {}
};

int main()
{
	A a1;
	A a2;
	a1 = a2;//err
	A a3(a1);//err

	return 0;
}

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

在看到这个条款,我立马就会想到它的意思了:那就是在多态中,给基类的析构函数声明为virtual虚函数,这样就会保证资源不会被泄漏,因为当基类的指针或者引用指向了派生类的对象,在析构的时候,先会析构派生类的成分,基类的成分需要调用基类的析构函数。如果不需要构成多态,那么就不需要virtual析构函数。

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

如果在析构函数中进行了抛异常的操作,那么我们要在析构函数内将其捕获之,这样才能继续执行析构函数后面的代码,才能保证资源安全地释放完成,如果让这个异常走出析构函数了,那么就会让程序过早的结束或出现不明确的行为。

做法比较简单,就是使用try{} catch(...) {};捕获,在catch的主体内,可以选择使用abort()来结束程序,也能进行其它操作,比如记下析构的失败等待。

class A
{
	A() {}

	~A()
	{
		try 
		{
			//....
		}
		catch (...)
		{
			//1.析构出现异常,直接结束程序
			std::abort();
			//2.析构出现异常,记下来
			//...
		}
	}
};

但是这两种种做法有个缺点,那就是无法对因为某种原因而出现异常做出反应,因此解决办法就是将这个会抛异常的函数拿出来,不要放到析构函数中,然后使用“双保险”的方式,再在析构函数中判断是否已经将这个函数执行完毕(如果抛异常就是没执行完毕),如果没有执行完毕,再在析构函数中执行,让析构函数去执行它。如果析构函数也执行失败抛出异常,就会捕获异常,虽然此时就会回到上面的两种做法(退出程序或吞下异常)了。但是这种做法的好处是在不知道会不会抛异常的前提下,将调用这个函数的责任转移给了其它的函数,交给用户的手上。(至于这个函数为什么要在析构中执行,因为可能这个函数执行的功能是关闭连接或者关闭什么东西的,关闭了也就结束了,结束了也就要析构了嘛)。

class A
{
	A() {}

	void close() //交给用户去调用
	{
		//关闭连接
		test.close();//假设有个东西要关闭连接
		closed = true;
	}

	~A()
	{
		if (!closed)
		{

			try
			{
				test.close();//关闭连接
			}
			catch (...)
			{
				//1.析构出现异常,直接结束程序
				std::abort();
				//2.析构出现异常,记下来
				//...
			}
		}
	}
private:
	bool closed;
};

5.条款09:绝不在构造和析构过程中调用virtual函数

本条款的重点:不要再构造函数和析构函数执行期间去调用virtual函数。理由是:在构造和析构期间,基类的构造和析构函数内的virtual函数不会下降到派生类阶层。

用一段代码展开聊聊这个条款吧~

//实现一个类的继承体现,基类为Dog,每创建一个对象,就多一条小狗狗
class Dog
{
public:
	Dog();
	virtual void count_Dog() const = 0;;
	//...
};
Dog::Dog()
{
	//...
	count_Dog();
}

class Corgi :public Dog //柯基的类
{
public:
	virtual void count_Dog() const;
};

int main()
{
	Corgi co;
	return 0;
}

分析代码:

代码中,用派生类创建了一个派生类的对象,在构造函数被调用的时候,会先去构造基类的成分,然后才会去构造派生类的从成分,这就意味着,会先去调用基类的构造函数。

基类的构造函数最后会去执行count_Dog函数,问题就出现在这里,上面说了,构造函数构造期间,基类的virtual函数不会下降到派生类中,也就是说即使我们创建的对象属于派生类的,但是在调用基类的构造函数期间,对象就会被看做成基类的对象!调用的是基类的count_Dog函数!

这种现象根本的原因在于:在派生类对象调用基类的构造函数期间,由于是基类先构造,那么在此期间,此时的对象被视为是基类的对象,并且派生类的成分并没有初始化,因此C++的做法是视它们不存在,这样才能保证安全。

同样的,对于析构函数也一样,由于是先析构派生类的成分,在派生类析构函数执行的时候,对象内的派生类的成员变量就是变成了未定义值,C++是它们不存在,而进入了基类的析构函数,就会变成基类的对象。

在上面这个例子中,基类的构造函数就直接调用了基类中的virtual函数,并且它是一个纯虚函数,此时连接器就找不到基类中count_Dog的实现代码了,编译器就会报错。

解决这个问题,就要确定我们的析构函数和构造函数都没有调用virtual函数,要保证这一点,我们可以将基类中的count_Dog函数变成非虚函数,另外让派生类在构造函数的时候给基类传递必要的信息给基类的构造函数。并且需要注意的使用static静态成员函数来传递,这样就可以在一开始的时候初始化了。

class Dog
{
public:
	explicit Dog(const std::string& dog);
	void count_Dog(const std::string& dog) const
	{
		std::cout << "小狗狗的数量+1,新来的狗狗叫:" << dog<< std::endl;
	}
	//...
};
Dog::Dog(const std::string& dog)
{
	//...
	std::cout << "调用基类构造函数" << std::endl;
	count_Dog(dog);
}

class Corgi :public Dog //柯基的类
{
public:
	Corgi(const std::string& dogs)
		:Dog(createdogs(dogs))
	{
		std::cout << "调用派生类构造函数" << std::endl;

	}
private:
	static std::string createdogs(const std::string dog);
};
std::string Corgi::createdogs(const std::string dog)
{
	return dog;
}

int main()
{
	Corgi co1("天天");
	Corgi co2("大肥");
	return 0;
}

代码分析:一开始会进入到派生类的构造函数中的初始化列表中,通过调用了createdogs函数,创建了基类Dog的匿名对象,也就是调用了基类的构造函数,然后进入了count_Dog函数,最后再次去调用派生类的构造函数的主体!

我们调试来看一下:

第一步:在派生类的构造函数的初始化列表中。

 

 第二步:进入了createdogs函数,因为此时对于这个函数来说,其参数已经有了

 第三步:进入基类的构造函数

 第四步:进入基类的count_Dog函数

 最后一步:进入派生类构造函数

说到底,正是因为派生类的成员变量没有初始化,所以在基类的构造和析构期间调用的virtual函数不可以下降到派生类阶层。只要我们换一个思路,自底向上地传入信息,即先用static的特性,把派生类的一些必要的数据进行初始化,然后传递给基类就可以了。

总结:我们不要再构造和析构期间调用virtual函数了。

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

也就是让赋值重载函数的返回值是一个引用返回,这样是为了可以实现连锁赋值。

a = b = c = 10;

当然,你也可以根据需求来决定要不要这样做,但是一般大家都这样做,有必要还是这样做吧。

7.条款11:在operator=中处理“自我赋值”

自己给自己赋值,这种情况不能说没有,只能说少见,比如:

class A
{
public:
	A() {}
	~A(){}
};

int main()
{
	//1.对象自我赋值
	A a;
	//...一堆代码
	a = a;//不小心自我赋值

	//2.下标相同时的自我赋值
	int arr[5] = { 0 };
	int i = 0, j = 0;
	arr[i] = arr[j];

	//3.指向是相同的指针自我赋值
	int* px = arr;
	int* py = arr;
	*px = *py;
	return 0;
}

这些其实都不是问题,只是很愚蠢罢了,还有以下这种很难找出的自我赋值:代码中的f和s很可能是同一个对象。

class Father_Class
{
public:
	Father_Class(){}
	~Father_Class(){}
};

class Son_Class :public Father_Class
{
public:
	Son_Class() {}
	~Son_Class() {}
};

void doSomeThing(const Father_Class& f, const Son_Class* s);

看起来,自我赋值好像没什么怕的,但是其实自我赋值是有隐患的,来看以下这段代码:

//建立一个class来保存一个指针指向一块动态分配的位图bitmap
class Bitmap
{
	//...
};
class Widget
{
	//..
	Widget& operator=(const Widget& rhs)
	{
		delete pb;                //释放当前的bitmap
		pb = new Bitmap(*rhs.pb);  //将rhs的pb赋值给当前的pb
		return *this;           //返回
	}
private:
	Bitmap* pb;
};

这里出现的问题是:当我们把当前的bitmap释放,然后再去赋值的时候,如果rhs中的pb和当前的pb是指向同一个对象的时候,在delete的时候,就相当于同时消耗了rhs的bitmap和当前对象的bitmap!此时this指向的是一个已经被删除的对象!

①简单的解决方法是检测一下rhs和当前的*this是否指向同一个对象:

	Widget& operator=(const Widget& rhs)
	{
		if (&rhs != this)
		{
			delete pb;                //释放当前的bitmap
			pb = new Bitmap(*rhs.pb);  //将rhs的pb赋值给当前的pb
		}
		return *this;           //返回
	}

但是还有问题,那就是抛异常的问题!如果new Bitmap的操作抛异常了,那就说明赋值失败,开辟空间失败,此时当前的对象已经被删除了,而赋值又失败了,此时当前的this指针会指向一块被删除的Bitmap。

②解决的方法是换一下顺序,并且保存当前的pb。

	Widget& operator=(const Widget& rhs)
	{
		Bitmap* tmp = pb;         //先保存原本的pb
		pb = new Bitmap(*rhs.pb);  //让pb指向*rhs.pb的一个副本
		delete tmp;					//删除原本的pb
		return *this;           //返回
	}

此时,不仅不需要进行检测,还能处理自我赋值带来的问题。因为对原本的bitmap做了复件,然后才指向新的bitmap,然后删除原先的bitmap。

还有一种办法,这是在我之前的文章中提到过的,在赋值重载中使用所谓的“现代版本”进行赋值。

	void swap(Widget& rhs)
	{
		Bitmap* tmp = pb;
		pb = rhs.pb;//赋值
		rhs.pb = pb;
	}
	//1.引用
	Widget& operator=(const Widget& rhs)
	{
		Widget tmp(rhs);//对rhs做一份拷贝
		swap(tmp);//交换
		return *this;           //返回
	}
	//2.传值传参,rhs的改变不会影响到本体的rhs
	Widget& operator=(Widget rhs)
	{
		swap(rhs);
		return *this;           //返回
	}

8.条款12:赋值对象时勿忘其每一个成分

这个条款的重点就是在继承体系中,要确保派生类的成分和基类的成分都必须得到赋值。做法就是在派生类中的拷贝构造函数和赋值重载中调用基类的拷贝构造和赋值函数。

class father_c
{
public:
	father_c() {}

	father_c(const father_c& f)
		:_data(f._data)
		,_str(f._str)
	{
		std::cout << "调用基类的拷贝构造" << std::endl;
	}

	father_c& operator=(const father_c& f)
	{
		std::cout << "调用基类的赋值重载" << std::endl;
		_data = f._data;
		_str = f._str;
		return *this;
	}
	virtual  ~father_c(){}
private:
	int _data;
	std::string _str;
};

class son_c:public father_c
{
public:
	son_c() {}
	son_c(const son_c& c)
		:_tmp(c._tmp)
		,_cash(c._cash)
		, father_c(c)  //调用基类的拷贝构造,使用切片的特性,给基类成分赋值
	{
		std::cout << "调用派生类的拷贝构造" << std::endl;
	}

	son_c& operator=(const son_c& c)
	{
		std::cout << "调用基类的拷贝构造" << std::endl;
		_tmp = c._tmp;
		_cash = c._cash;
		father_c::operator=(c); // 调用基类的赋值重载,使用切片的特性,给基类成分赋值
	}
private:
	int _tmp;
	std::string _cash;
};

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue CLI 服务

使用命令 在一个 Vue CLI 项目中&#xff0c;vue/cli-service 安装了一个名为 vue-cli-service 的命令。你可以在 npm scripts 中以 vue-cli-service、或者从终端中以 ./node_modules/.bin/vue-cli-service 访问这个命令。 这是你使用默认 preset 的项目的 package.json&…

2023红明谷杯部分WP

0x00 签到 一直点就能得到flag 0x01 Dreamer 拿到题感觉有点儿懵 先下发靶机看一眼 梦想家CMS&#xff0c;好嘛&#xff0c;我直接一手查找官网 直接一手演示中心碰运气 哎嘿嘿&#xff0c;运气不错进去了&#xff0c;突然想起之前有位大佬写的关于Dreamer CMS的代码审…

【Linux网络设置】

目录 一、查看网络接口信息1.1、查看所有活动的网络接口信息1.2、查看指定网络接口信息 二、查看主机名称2.1、hostname命令2.2、永久设置主机名 三、查看路由表条目route命令 四、查看网络连接情况4.1、netstat命令4.2、ss命令 五、测试网络连接ping命令 6、跟踪数据包tracerr…

CorelDRAW2023最新版本配置及新功能介绍

从简单的线框到令人称叹的水平&#xff0c;使用CorelDRAW Graphics Suite 2023开始您的设计之旅&#xff1a;一套完整的专业图形设计应用程序&#xff0c;用于矢量插图、布局、照片编辑等。CorelDRAW平面设计软件通常也被叫做CDR&#xff0c;CDR广泛应用于排版印刷、矢量图形编…

关于电脑出厂时间查询工具的构思

在做一个单位的计算机盘点、管理的时候&#xff0c;很容易遇见需要知道电脑的采购时间&#xff0c;或者出厂时间。这个信息能够帮助管理人员决定电脑是否该按定期报废制度进行报废或更换。 目前为止&#xff0c;作者接触过的各类电脑&#xff0c;没有看到过哪台电脑有专门的一…

windows系统中安装目标检测平台detectron2

更多内容&#xff0c;欢迎访问老五笔记 detectron2是Facebook研发并开源的目标检测平台&#xff0c;包含了大量业内最具代表性的目标检测、图像分割、关键点检测算法等。Detectron2基于新版的Pytorch进行更新&#xff0c;包含了更大的灵活性与扩展性。​ 笔者将在本文中介绍如…

IS220UCSAH1A利用电子和空穴两种载流子导电的,所以叫做双极型电路

IS220UCSAH1A利用电子和空穴两种载流子导电的&#xff0c;所以叫做双极型电路 美国的通用电气公司&#xff08;General Electric Company&#xff0c;以下简称 GE&#xff09;想要称霸整个工业互联网&#xff0c;但却失败了。为什么呢&#xff1f; 多年来&#xff0c;GE 一直在…

关于CSDN文章内嵌视频自动播放问题

关于CSDN文章内嵌视频自动播放问题 1. 源由2. 分析3. 反馈4. 沟通5. 总结6. 附录-Firefox配置7. 附录-Microsoft Edge配置 1. 源由 这个问题是4月初发现的&#xff0c;主要现象就是页面上的视频一起自动播放了。 鉴于笔者有不少帖子都是文字、表格、图片、视频结合的。视频是…

机器学习:opencv案例——人脸检测

目录标题 实验数据实验原理实验步骤实验结果 实验数据 lena.jpg face3.jpg video.mp4 实验原理 &#xff08;1&#xff09;图片灰度转换 OpenCV 中有数百种关于在不同色彩空间之间转换的方法。 当前&#xff0c; 在计算机视觉中有三种常用的色彩空间&#xff1a; 灰度、 BG…

redis lpush rpop List消息队列实现

List 队列&#xff1a; 生产者存入消息&#xff1a; LPUSH queue2 msg1 LPUSH queue2 msg2 LPUSH queue2 msg3 消费者消费消息&#xff1a; RPOP queue2 RPOP queue2 RPOP queue2写个死循环消费&#xff1a; while true://没消息阻塞等待,3秒超时返回null,设置0时没消息一直浪…

JavaSE学习进阶day07_02 异常

第三章 异常 3.1 异常概念 异常&#xff0c;就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是&#xff1a; 异常 &#xff1a;指的是程序在执行过程中&#xff0c;出现的非正常的情况&#xff0…

Android---屏幕适配

为什么要适配 由于 Android 系统的开放性&#xff0c;任何用户、开发者、OEM 厂商、运营商都可以对 Android 进行定制&#xff0c;于是导致运行 Android 的设备多种多样&#xff0c;它们有着不同的屏幕尺寸和像素密度。尽管系统可以通过基本的缩放和调整大小功能使界面适应不同…

【LeetCode: 53. 最大子数组和 | 暴力递归=>记忆化搜索=>动态规划 | 分治法 】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

抽象类,内部类,匿名类

java学习第十天 抽象类 1.用abstract关键字来修饰一个类时,这个类就叫抽象类 访问修饰符 abstract 类名 { } 2.用abstract关键字来修饰一个方法时,这个方法就是抽象方法访问修饰符abstract返回类型方法名(参数列表);//没有方法体 3.抽象类的价值更多作用是在于设计,是设计者…

ROS学习第十四节——参数服务器控制小乌龟

1.使用命令修改参数服务器 单独使用命令启动小乌龟节点&#xff0c;不是用lanuch文件&#xff0c;不启动键盘控制节点 rosrun turtlesim turtlesim_node 使用命令打印参数服务器参数列表 rosparam list 修改小乌龟节点的背景色 rosparam set /turtlesim/background_b 自定…

网络安全:一次艰难的 WAF 绕过

0x00&#xff1a;前言 做之前没想过有这么难 0x01&#xff1a;后缀绕过 首先看一下 waf 咋工作的&#xff0c;当数据包匹配到 waf 规则后&#xff0c;数据包就会被丢弃掉&#xff0c;就像这样 waf 是拦截后缀的&#xff0c;首先 fuzz 一波换行 失败 多个等于号 失败 单双引号…

入门Pytorch时的一些报错

Anaconda是一个很好的可以管理python环境的工具&#xff0c;推荐大家结合pycharm使用。 我入门时是参照up我的土堆的Pytorch教程来安装学习的&#xff0c;其中也遇到了一些问题。 1. torch.cuda.is_available结果显示False import torch torch.cuda.is_available 用anacon…

4月17号软件资讯更新合集.....

CrateDB 5.3.0 发布&#xff0c;分布式 SQL 数据库 CrateDB 是一个分布式的 SQL 数据库&#xff0c;使得实时存储和分析大量的机器数据变得简单。CrateDB 提供了通常与 NoSQL 数据库相关的可扩展性和灵活性&#xff0c;最小的 CrateDB 集群可以轻松地每秒摄取数万条记录。这些…

通过Python的PIL库给图片添加文本水印

文章目录 前言一、PIL是什么&#xff1f;二、安装PIL三、查看PIL版本四、使用PIL库给图片添加文本水印1.引入库2.打开图片文件3.新建一个Draw对象4.设置水印文字、字体、大小5.设置水印颜色5.1通过名称设置颜色5.2通过RGB值设置颜色5.3通过RGBA值设置颜色5.4通过十六进制设置颜…

Word控件Spire.Doc 【脚注】字体(3):将Doc转换为PDF时如何使用卸载的字体

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…