✍个人博客:https://blog.csdn.net/Newin2020?spm=1011.2415.3001.5343
📚专栏地址:C/C++知识点
📣专栏定位:整理一下 C++ 相关的知识点,供大家学习参考~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
🎏唠叨唠叨:在这个专栏里我会整理一些琐碎的 C++ 知识点,方便大家作为字典查询~
二、构造/析构/赋值运算
条款05:了解 C++ 默默编写并调用了哪些函数
在创建类时,如果自己不定义默认构造,拷贝构造(拷贝运算符),析构函数,那么编译器会自动生成这些函数。
//拷贝运算符:
classname& operator=(const classname& cn){......}
但是有些情况下编译器不会自动生成,拿下面这段代码举例:
可以发现由于 class 里出现了引用类型和 const 类型,故编译器就不会自动生成拷贝复制和移动赋值函数。
这是因为引用类型是引用其他地方的内容,如果调用拷贝赋值和移动赋值可能会顺带改变引用处的值。
而 const 类型本身就是不可改动的,所以不会生成拷贝赋值和移动赋值函数。
另外 mutex 本身不能被拷贝和移动,所以拷贝构造和移动构造函数也不会自动生成。
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
对于类中拷贝构造函数,我们应当阻止他们。但若是不声明,编译器也会自动生成拷贝构造函数。
在现代 c++ 中我们可以通过 delete 关键字对编译器自动生成的函数进行删除,如下图所示:
但在之前的 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:别让异常逃离析构函数
释义:在析构函数内部处理异常
我们来看以下案例:
如果在 db.close() 处发生异常,则会导致不可预料的情况。
首先介绍一下异常处理的办法:
try
{...}
//try 内部写可能产生异常的语句,没有产生异常,则catch语句不执行,产生则一一匹配
//catch 用于捕获并处理异常,和 case 有异曲同工之妙
catch(...)
{
1、可以使用 abort(); 函数终止程序
2、可以吞下这个异常,在 catch 内部做一些处理
}
了解如何处理异常之后,我们就可以实现如条款所说,在析构函数内部处理异常:
上面左边的方法是直接调用 abort 终止程序,右边则是直接吞下异常,只是记录个日志,后面再处理。
但是这两种方法都有个缺点就是用户无法参与操作,因此可以写成下面的方式:
用户可以自己实现一个 close 函数来进行关闭,如果关闭的顺利则 closed=true,反之关闭失败则会进行异常捕捉,在析构函数中帮助用户关闭。
条款09:绝不在构造和析构函数中使用虚函数
众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 virtual 找到子类析构,再析构父类),所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。来看下面这个例子:
此时,如果父类的构造和析构函数中有 virtual,则该函数无法找到子类的地址(或者说无视子类,因为子类被销毁/未被初始化),使程序发生不明确的行为。
可以发现上面我是想调用派生类的构造函数和析构函数,但是调用的却是基类的。如果想满足该要求,我们可以去掉虚函数,而是在基类接收一个参数来实现。
条款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