当前位置: 首页 > article >正文

Effective C++中文版学习记录(二)

Effective C++中文版学习记录(二)

章节二:构造/ 析构/ 赋值运算

进度:12/55


文章目录

  • Effective C++中文版学习记录(二)
  • 条款05、了解C++默默编写并调用哪些函数
  • 条款06、若不想使用编译器自动生成的函数,就该明确拒绝
  • 条款07、为多态基类声明virtual析构函数
  • 条款08、别让异常逃离析构函数
  • 条款09、绝不在构造和析构过程中调用virtual函数
  • 条款10、令operator= 返回一个reference to *this
  • 条款11、在operator= 中处理“自我赋值”
  • 条款12、复制对象时勿忘其每一个成分


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

这一步是编译器帮忙做的,当你创建一个类时,编译器会给这个类构造四个函数:

默认构造函数
拷贝构造函数
拷贝赋值操作符
析构函数

了解即可,但是要注意,编译器为类构建的默认拷贝构造函数是浅拷贝

所以当类需要完成某些拷贝功能时,建议自己重构一个深拷贝的拷贝构造函数

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

这一条款的想法来源于此:

当建立了一个class如

class Father{
public:
...
};
Father Fa1, Fa2;
Father Fa3(Fa1);  // Shouldn't success
Fa1 = Fa2;        // Shouldn't success, either

我们并不希望父亲能够被拷贝,但是正如条款05,即便我们不写拷贝函数,编译器也会有个默认的拷贝

解决方法是将拷贝构造函数设置为private

class Father{
public:
...
private:
	Father(const Father&);
	Father& operator=(const Father&); 
};

这里只需要声明,不用实现,反正不会调用它。这样就能够拒绝编译器默认的拷贝功能了

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

回顾一下virtual的用法:

C++的一大特性是多态,也就是可以通过派生类继承基类后,对基类中同名的函数进行重写,实现同名不同工的功能

class Father
{
	int a,b;
public:
	void test() { std::cout << 1 << std::endl;}
};

class Son : public Father
{
	int c,d;
public:
	void test() { std::cout << 2 << std::endl;}
};

那么运行这样的代码,结果为
在这里插入图片描述
因为p1和p2都是Father类型的指针,调用test自然是会调用到Father的test

那如果想用p2调用到Son的test呢?两个方案:一是将p2变成Son类型的指针,二是将Father的test改成virtual型函数

class Father
{
	int a,b;
public:
	void virtual test() { std::cout << 1 << std::endl;}
};

这样,p2在调用test时,就会看p2指向的元素是谁,再调用它的test,此时也就是Son的test

那么回到条款07,为什么需要让基类的析构函数为virtual呢?

就如同上文的例子,Father的析构函数为non-virtual时,对p2进行析构会发生什么,实际上是调用了Father的析构函数

但是p2指向的是Son类型,它还包含了c、d两个int型变量,而Father的析构函数并不会处理c和d,因为Father本身没有c和d

这样就会造成内存泄漏问题

那么当Father的析构函数变成virtual时,p2在调用析构函数就会看p2指向的元素是谁

这样一来p2就会调用Son的析构函数,防止内存泄漏

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

这条很好理解,当析构时抛出异常,那么析构工作就会停止,此时的内存情况谁也没法说清楚到底是什么样的

所以当析构过程中捕捉到异常时,请设计成它会吞下异常,并继续完成析构动作,或者直接结束程序,防止不明确行为

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

看例子:

class A
{
public:
	A() {test();}
	~A() {test();}
	virtual void test() {...}
};

class B: public A
{
	virtual void test() const override {...}
}

class C: public A
{
	virtual void test() const override {...}
}

int main()
{
	B p1;
}

此时,p1的构造首先是会调用基类的构造函数,即A的构造函数,但是A中有虚函数test,而p1的类型又是A的派生类B

那么理论上讲是不是会调用B的test呢?不会,因为在构造过程中virtual不会下降到派生类,那么这样就会导致一些不明确行为

析构函数同理,这是编译器规定的东西

所以解决方法就是,构造函数和析构函数里,不要有任何virtual函数的调用

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

这条是为了满足连等号的使用,即a=b=c=10;这样的操作

class A
{
public:
	...
	A & operator=(const & a) {...; return *this;}
}

同理,+=和-=也可以这么做

条款11、在operator= 中处理“自我赋值”

这是为了防止诸如a=a的行为

解决方案是在operator=中加入判断条件

A & operator=(const & a)
{
	if(A == &a) return *this;
	...
	return *this;
}

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

情况一是字面意思,就是不要忘记任何一个成分,假如

class A
{
private:
	int a,b;
public:
	int c;   // new element
}

这里c是新加入的元素,此时就要注意之前写好的拷贝构造函数和operator=有没有处理c这个元素

情况二是基类与派生类的关系,比如用class B继承A,并且为B构建拷贝函数,那么A中的private元素可能就没法拷贝

因为没法访问到

所以考虑调用A的拷贝函数后,再针对B的额外元素进行补充

而为了保证安全,最好是构造一个init函数来实现这两个拷贝函数的共同操作部分

避免使用A的拷贝函数实现B的拷贝函数这样的操作


http://www.kler.cn/news/333203.html

相关文章:

  • 【无人机设计与技术】自抗扰控制(ADRC)的建模与仿真研究
  • MongoDB 索引
  • 世优科技出席36氪2024具身智能大会圆桌对话,入选创新应用案例
  • SOMEIP_ETS_166: SD_TestFieldUINT8
  • 【MySQL】数据库中的内置函数
  • 《程序猿之Redis缓存实战 · 有序集合类型》
  • XSLT 实例:掌握 XML 转换的艺术
  • 22.3 解读k8s服务发现源码
  • Vite+Vue3+SpringBoot项目如何打包部署
  • shadcn-vue 快速入门(2)
  • LLaMA 3 和 OpenAI有哪些相同点和不同点?
  • 昇思学习打卡营第32天|基于ResNet50的中药炮制饮片质量判断模型
  • Middleware---RocketMQ
  • [MAUI]数据绑定和MVVM:MVVM的属性验证
  • C++中list的模拟实现
  • 【c++】条件计数器函数设计
  • 1、Spring Boot 3.x 集成 Eureka Server/Client
  • fatal: urdf 中的 CRLF 将被 LF 替换
  • Jupyter | jupyter notebook 使用 conda 环境
  • 【当当网】电子书城-03-注册表单的验证功能