文章目录
05:了解C++默认编写、调用的函数
- 如果没声明,编译器会声明一个copy构造函数,一个copy assignment操作符,和一个析构函数;如果没有声明任何构造函数,编译器也会声明要给default构造函数。
- 包含reference和const成员的class,编译器会拒绝自动编写assignment操作;如果base classes将copy assignment声明为private,编译器将拒绝为derived classes生成copy assignment函数。
06:如果不想使用编译器自动生成的函数、那就明确拒绝
建立新类:
class Uncopyable {
protected:
Uncopyable() {} //允许子类构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); //阻止拷贝
Uncopyable& operator=(const Uncopyable&);
};
然后新类就继承这个类。
其实新标准里面可以使用=delete来进行禁止。
07:为多态基类声明virtual函数
- polymorphic(带有多态性质的) base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 析构函数,那他就应该拥有一个 virtual 析构函数。
- classes 设计的目的如果不是作为 base classes 使用,或者不是为了具备多态性(polymorphically),就不该声明为 virtual 函数
原因是,如果基类虚构函数是 non-virtual 的,那么 derived class 由 base 指针删除时,可能 derived 成分,也就是 derived class 中新声明的变量无法被正确析构。
而声明为析构函数会使得对象占用额外的空间用来保存函数指针。
08:别让异常逃离析构函数
- 析构函数不应该抛出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下他们(不传播)或者结束程序。
- 如果用户需要对某个操作函数期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。
比如读取文件操作,最后需要关闭文件。一个较好的的做法可以是:
class FileManager {
bool isClosed;
public:
...
void close () {
file.close();
isClosed = true;
};
~ FileManager() {
if (!closed) {
try {
file.close();
} catch (...) {
//记录对close调用失败,吞下异常或者结束程序
}
}
...
}
};
可以让用户处理关闭操作,并进行异常捕获,如果没有进行操作,默认提供一个,如果出现了异常,就吞下或者结束程序。
09:不在构造和析构过程中调用virtual函数
- 不要在析构或构造期间调用 virtual 函数,因为他们不会下降到 derived class,而是在构造啥类型期间,就调用啥类型的 virtual 函数。
在 derived class 的 base class 构造过程中,对象的类型是 base class,并且不仅 virtual 函数会被编译器解析到(resolve to) base class,运行期间的类型信息(runtime type infomation, e.g. dynamic_cast, typeid)也会把对象当作base class。
析构的时候也是这样,一旦 derived class 析构函数执行,对象内的 derived class 成员变量变成先未定义值,进入 base class 之后就变成一个 base class 对象。
但是,如果想要每一个 derived class 被创建时,都有适当的一些信息被输出,要怎么做呢?可以在 base class 内创建一个 non-virtual function,并在构造函数中调用这个函数;derived class 在构造时,将必要的信息传给这个基类的构造函数。
class BaseClass {
public:
BaseClass(string& info) {
...
printLogInfo();
}
void printLogInfo(string info} {...}
};
class DerivedClass : public BaseClass {
public DerivedClass(param) : BaseClass(createInfo(param)) {...}
string createInfo(parm) {...}
}
10:令operator= 返回一个reference to * this
令赋值操作(assignment) (=, += ,-=, *=)返回一个 reference to *this。
为了实现类似:
int x, y, z;
x = y = z = 15;
之类的连锁赋值。
11:在operator= 中处理自我赋值
- 确保对象自我赋值时,operator = 有良好的行为。其中计数包括
- 比较“来源对象”和“目标对象”的地址
- 精心周到的语句顺序
- copy-and-swap
- 确定任何函数如果操作一个以上的对象,其中多个对象是同一个对象时,行为仍然正确
比如有一个类:
class A {
public:
A(B& b) { pb = &b; }
private:
B* pb;
...
}
使用下面的 operator= 代码,看上去很合理,但是自我赋值会出现问题:
A& A::operator=(const B& rhs) {
delete pb;
pb = new A(*rhs.pb);
return *this;
}
这时,如果当前对象传入的对象是同一个对象,那么当程序执行完之后,rhs就指向了一块被删除的地址。
一个想法是,在最前面加一个“证同测试(identity test)”来达到“自我赋值”的检验
A& A::operator=(const B& rhs) {
if (this == &rhs) return *this;
delete pb;
pb = new A(*rhs.pb);
return *this;
}
但这样并不具备异常安全性,同时效率比较低
- 如果new时,内存不足,或者拷贝构造函数出现异常,那么pb都会指向一块被删除的地址
- if 语句会使程序更大,并且引入一个新的控制流分支,二者都会降低速度。Perfectching, caching 和 pipelining 等指令的效率都会因此降低。
精心安排一下语句的顺序,可以解决异常安全问题:
A& A::operator=(const B& rhs) {
auto tempPb = pb;
pb = new A(*rhs.pb);
delete pb;
return *this;
}
这是,如果 new 语句发生异常,两个变量中的pb仍然会保持原状。
一个替代方案是 copy and swap技术:
class A {
...
void swap(A& rhs); // 交换*this 和 rhs 的函数
...
}
A& A::operator=(const B& rhs) {
A temp(rhs);
swap(temp);
return *this;
}
或者是:
A& A::operator=(const B rhs) {
swap(rhs);
return *this;
}
这种写法利用了两个技术:
- 某 class 的 copy assignment 操作符可以被声明为“以 by value 方式接受实参”
- by value 方式会造成一份副本
这种方式牺牲了清晰性,不过将 copying动作 从函数本体移动到 函数参数构造阶段 有时可能会令编译器产生更高效的代码。
12:赋值对象时勿忘其中每一个成分
- copying 函数应该确保赋值“对象内所有成员变量”以及“所有base class”成分
- 不要尝试以某个 copying 函数实现另一个 copying 函数,应该将共同的技能放在第三方函数中,并且由两个 copying 函数共同调用。