Effective C++中文版学习记录(二)
章节二:构造/ 析构/ 赋值运算
进度:12/55
文章目录
条款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的拷贝函数这样的操作