点击上方“C语言与CPP编程”,选择“关注/置顶/星标公众号”
干货福利,第一时间送达!最近有小伙伴说没有收到当天的文章推送,这是因为微信改了推送机制,确实会一部分有小伙伴刷不到当天的文章,一些比较实用的知识和信息,错过了就是错过了。所以建议大家加个星标⭐️,就能第一时间收到推送了。
小伙伴们大家好,我是飞宇。
今天继续更新《Effective C++》和《C++并发编程实战》的读书笔记,下面是已经更新过的内容:
《C++并发编程实战》读书笔记(1):并发、线程管控
《C++并发编程实战》读书笔记(2):并发操作的同步
《Effective C++》读书笔记(1):让自己习惯C++
条款5、了解C++默认编写并调用哪些函数
通常情况下,如果代码中没有声明构造函数、拷贝构造函数、拷贝运算符、析构函数,编译器会在需要时创建他们,但这往往只能满足编译器的需求,很可能无法满足程序的需求。
实际的生成规则复杂一些,可以查阅cppreference。
class Empty{};
//上面的空类好比下面的类
class Empty{
public:
Empty() { ... }
Empty(const Empty& rhs) { ... }
~Empty() { ... }
Empty& operator=(const Empty& rhs) { ... }
};
编译器生成的构造函数/析构函数是只是调用基类和非静态成员变量的构造函数/析构函数;生成的析构函数是非虚的,除非基类有虚析构函数。
至于生成的拷贝构造函数和拷贝操作符只是单纯将每个非静态成员变量拷贝;有const成员或者引用成员时,以及基类拒绝拷贝操作符时,默认生成的拷贝操作符没有意义,必须自己定义。
条款6、若不想使用编译器自动生成的函数,就该明确拒绝
很多时候,你并不希望某些类被拷贝,而仅仅不实现拷贝构造/拷贝运算符是不够的,因为编译器可能会自行生成。
为此,可以把拷贝构造/拷贝运算符声明为"=delete",或者声明为private(后者较为过时)。
class Uncopyable{
public:
Uncopyable() = delete;
Uncopyable& operator=(const Uncopyable&) = delete;
};
事实上,对于析构函数中需要释放资源的类,为了防止内存问题,除非真的需要拷贝功能,否则最好都禁止拷贝。
可以让它们继承上面的类,即可禁止编译器生成拷贝操作:编译器试图为它们生成拷贝构造/拷贝运算符时会尝试调用基类Uncopyable的对应操作,而这会被拒绝。
class SomeClass: private Uncopyable{ ... };
条款7、为多态基类声明virtual析构函数
C++中多态性质体现于虚函数:基类指针或引用调用虚函数时会检查指向的对象是基类还是派生类,再调用对应的函数。其具体实现这里不再赘述。
当这样的一个指向派生类的基类指针析构时,如果析构函数不是虚函数,则直接调用基类的析构函数,那么派生类获取的资源未释放,则会造成内存泄漏。
而当析构函数是虚函数时则先调用对应的派生类析构函数,再调用基类析构函数,资源全部释放。
不过这种操作只有在基类是多态用途时才需要注意,也有很多类不是为了多态的用途,例如STL容器和上文的Uncopyable。
条款8、别让异常逃离析构函数
C++中抛出异常时会逐步展开其函数调用栈,清空局部资源,直到异常被catch。
如果析构函数可以抛出异常,那么清空局部资源时局部对象的析构函数再次异常时同时存在两个异常,C++无法处理,可能会过早结束或出现不明确行为。因此,析构函数绝对不要抛出异常,应通过try-catch捕获任何异常。
有时,客户需要处理某些异常,那么类应该提供一个普通成员函数执行相关操作,供用户调用并处理异常。
例如数据库连接这样的类中,假设用户需要处理关闭连接时的异常,同时析构函数不能抛出异常,可以这样:
class DBConn{
public:
void close(){
db.close();
closed = true;
}
~DBConn(){
if(!closed){
try{
db.close();
}catch(...){
//记录相关信息
}
}
}
private:
DBConnection db;
bool closed;
};
条款9、绝不在构造和析构过程中调用virtual函数
C++的构造过程是先构造基类再构造子类、先初始化再进入构造函数体;析构过程相反。
对于派生类的构造函数而言,进入其中时基类部分已构造完而派生类部分未构造完,对象类型是基类,故而此时调用虚函数,实际上使用的是基类的虚函数。
析构函数同理。进入析构函数后派生类部分呈未定义值,对象类型是基类,调用的是基类的虚函数。
总而言之,在构造函数与析构函数中虚函数的行为有特殊变化;为了避免出错,不要在其过程中使用虚函数。如果真的有需求,可以改造成非虚函数再使用。
条款10、令operator=返回一个reference to *this
C++中通常支持连锁赋值,采用右结合律:
int a,b,c;
a = b = c = 15;
// a = (b = (c = 15));
为了支持这个常规,拷贝运算符需要返回一个引用。这一条款并不强制,但约定俗成。
class Widget{
public:
Widget& operator=(const Widget& rhs){
...
return *this;
}
};
条款11、在operator=中处理“自我赋值”
考虑这样一个类,其中管理了一个堆对象
class Widget{
...
private:
Bitmap* pb; //一个堆对象
};
总有些时候,会出现实质上“a=a”这种自我赋值的情况。那么这样简单的拷贝运算符就会出错,先释放了自身的pb,又使用了pb:
Widget& Widget::operator=(const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
传统做法是函数开头添加一个测试:
if(this == &rhs) return *this;
这种做法具备“自我赋值安全性”,但不具备“异常安全性”:例如当new Bitmap异常时,pb指向被删除的内存。
常用的方法有两种,兼顾了“自我赋值安全性”与“异常安全性”:
//第一种
Widget& Widget::operator=(const Widget& rhs){
Bitmap* pb_original = pb;
pb = new Bitmap(*rhs.pb);
delete pb_original;
return *this;
}
//第二种
void Widget::swap(Widget& rhs){
...
};
Widget& Widget::operator=(const Widget& rhs){
Widget temp(rhs);
swap(temp);
return *this;
}
条款12、复制对象时勿忘其每一个成分
当自己实现拷贝构造/拷贝运算符时,编译器不会警告你遗漏了某些成员变量。因此,必须仔细地复制所有派生类成员,并调用基类的拷贝操作来复制基类成员。
Derived::Derived(const Derived& rhs)
:Base(rhs),some_member(rhs.some_member){}
Derived& Derived::operator=(const Derived& rhs){
Base::operator=(rhs);
some_member = rhs.some_member;
return *this;
}
拷贝构造和拷贝运算符中很可能有相当多重复的操作,但因为两个函数性质完全不同,因此不能用其中一个调用另一个来减少冗余。
可以把共同功能放在第三个函数中,并由两个拷贝操作共同调用。
—— EOF —— 你好,我是飞宇,本硕均于某中流985 CS就读,先后于百度搜索以及字节跳动电商等部门担任Linux C/C++后端研发工程师。
同时,我也是知乎博主@韩飞宇,日常分享C/C++、计算机学习经验、工作体会,欢迎点击此处查看我以前的学习笔记&经验&分享的资源。
有个朋友收集了一些C++开发手册、LeetCode刷题模板等精品资料,可加他的微信免费领取。