第二部分 构造/析构/赋值运算
一个由c/c++编译的程序占用内存分为以下五个部分:
-
栈区—由编译器分配释放,存放函数的参数值,局部变量的值等。
-
堆区—由程序员申请和释放,若程序员不释放,程序结束后由操作系统回收。
-
全局(静态)区—全局变量和静态变量一起存储。初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量存储在另一块区域。程序结束后,由系统释放。
-
文字常量区—常量字符串的存放位置。程序结束后由系统释放。
-
程序代码区—存放程序的二进制代码。
条款05:了解C++默认编写并调用哪些函数
如果程序员没有声明,则编译器会声明的函数包括:
Default构造函数、copy构造函数、copy assignment操作符、析构函数。
所有这些函数都是public且inline。(想想为什么?)
这些函数会在不自觉的时候被调用,是基本的谋生工具。
编译器提供的copy构造函数和copy assignment操作符,只是单纯地将源对象的每一个non-static成员变量拷贝到目标对象。(对象相关的一般都是non-static成员变量,因为static变量不是成员变量,它是属于整个类的,不属于任何一个对象)。面对内置类型的成员变量,编译器会以拷贝源对象内的变量的每一个bits来完成目标对象的内变量的初始化。
成员变量的初始化在声明之后调用构造函数之前。
class base
{
public:
base(string &thename, int x) :name(thename), years(x)
{
}
private:
//默认operator=不可用原因:
//1.编译器不确定不同对象中的引用变量是否可以指向同一个数据
//2.常量不可赋值
string& name;
const int years;
};
如果你打算在一个内含reference成员或者内含const成员的class内支持赋值,你必须自己自己定义copy assignment操作符。
如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为所生成的derived classes生成copy assignment操作符。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。
例如:将base类的copy构造函数和copyassignmet操作符声明为private,并且不予实现,可以阻止copying。凡是继承base的derivedclasses也都阻止了copying。(思考采用什么继承方式好呢?是is-a还是has-a还是is-inplemented-in-term-of)
class uncopyable
{
public:
//在私有部分声明了copy构造函数,编译器便不会再提供任何构造函数
uncopyable() {};
private:
//uncopyable的derived classes也不可复制
uncopyable(const uncopyable&); //阻止copying
uncopyable operator=(const uncopyable&); //阻止copying
};
条款07:为多态基类声明virtual析构函数
“给base classes一个virtual析构函数”,这个规则只适用于polymorphic base classes(多态性质的基类)。这种base classes的设计目的是为了用来”通过base classes接口处理derivedclasses对象“。
class base
{
······
};
class derived : public base
{
······
};
int main()
{
base *p = new derived();
delete p;//如果base的析构函数是non-virtual
//这是一个未定义行为
//通常可能发生的结果是对象的derived部分没被销毁
//形成资源泄露,败坏数据结构
}
并非所有base classes的设计目的都是为了多态用途。例如标准string和STL容器都不被设计作为base classes使用,更别提多态了。某些classes的设计目的是作为base classes使用,但不是为了多态用途。这样的classes例如uncopyable和标准程序库中的input_iterator_tag,它们并非被设计用来“经由base class接口处置derived class对象“,它们被继承往往是为了实现某些功能,而不是重新实现或者变化或丰富某些功能。以上所列这些不需要virtual析构函数。
条款08:别让异常逃离析构函数
析构函数函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
如果客户需要对某个操作函数运行期抛出的异常做出反应,那么class应该提供普通函数(而非在析构函数中)执行该操作。
条款 09:绝不在构造函数和析构过程中调用virtual函数
#include <iostream>
using namespace std;
class base
{
public:
base()
{
display();
}
virtual void display() = 0
{
cout <<"this is base " << endl;
}
void father_use_son_func()
{
display();
}
~base()
{
display();
}
};
class derived :public base
{
public:
derived()
{
display();
}
void display()
{
cout << "this is derived " << endl;
}
~derived()
{
display();
}
};
int main()
{
{ //this is base
derived one; //this is derived
one.father_use_son_func(); //this is derived //this is derived
} //this is base
cin.get();
}
base构造函数执行起来打算初始化derived对象中base成分时,该对象的类型是base。同理,一旦derived析构函数开始执行,对象内的derived成员变量便呈现未定义值,所以C++视它们仿佛不存在。进入base析构函数之后,对象就成为一个base对象。
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived(亦即在构造和析构期间,virtual函数不是virtual)。
在构造期间,如果base需要根据derived信息构造某些部分,采用virtual函数是行不通了,可以在base构造时,将一些derived的相关参数传递上去(令derived将必要的构造信息上传至base)。
条款10:令operator=返回一个reference to *this
为了实现“连锁赋值”,复制操作符必须返回一个指向操作符左侧实参reference。
就像是加勒比海盗中的帕雷协议,条款10只不过是一条guideline,它并不是必须遵守的规定。然而这份协议被所有内置类型和标准程序库提供的类型如string,vector,complex,tr1::shared_ptr或者即将提供的类型共同遵守。
条款11:在operator=中处理”自我赋值“
拷贝构造函数和assignment操作符的区别。
如果你尝试编写自行管理资源的class,那么自我赋值可能会变得不安全,因为你有可能落入“在停止使用资源之前,已经将其释放”的陷阱。
例如:
class bitmap{
······
};
class widget{
public:widget& operator=(const widget& rhs);
private:bitmap * pb;};
widget& widget::operator=(const widget& rhs){
delete pb;pb = new bitmap(*rhs.pb);//此时,如果自我赋值,将会发现rhs.pb所指bitmap对象已经被销毁。return *this; //另外,此时如果new bitmap导致异常,widget对象最终会持有一个指针//指向一块被删除的bitmap。故不具备异常安全性。}
传统做法:添加证同测试。
widget& widget::operator=(const widget& rhs){
if (&rhs == this) return *this;//证同测试
delete pb;pb = new bitmap(*rhs.pb);//此时,如果自我赋值,将会发现rhs.pb所指bitmap对象已经被销毁。return *this; //另外,此时如果new bitmap导致异常,widget最终会持有一个指针//指向一块被删除的bitmap。故不具备异常安全性。}
自我赋值的几率太小的话,证同测试就会浪费很多成本。
下面是另外一种做法,免去了证同测试,同时保证异常安全性:
widget& widget::operator=(const widget &rhs){
bitmap * pOrig = pb; //保存原始对象
pb = new bitmap(*rhs.pb); //创建一个新的对象 ,如果此时异常,原始对象不会被删除
delete pOrig; //删除原始的对象
return *this;
}
在operator=函数内手工排列语句(确保代码“异常安全性“和”自我赋值安全“)的另一个替代方案是copy and swap技术。(关于swap函数,详见条款 25:考虑写出一个不抛出异常的swap函数)假设此时我们有一个安全的swap函数,我们可以这样做:
class bitmap {};
class widget
{
public:
void swap(widget& rhs);//安全地交换*this和swap的内容
widget& operator=(const widget& rhs);
widget& operator=(widget rhs);
private:
bitmap * pb;
};
widget& widget::operator=(const widget &rhs)
{
widget temp(rhs);//operator=不应该改变rhs内容,故先生成一个rhs副本
swap(temp);
return *this;
}
widget& widget::operator=(widget rhs)
{
swap(rhs);//此时的rhs是实参的一个副本
return *this;
}
条款12:复制对象时,不要忘记其每一个成分
当你编写一个copying函数,请确保:
-
复制所有non-static成员变量;
-
调用所有base classes内适当的copying函数,因为你将要处理的很可能都是base classes的private成员变量。
例如:
class base{
``````
};
class derived : public base{
public:derived(const derived & rhs);derived &operator=(const derived &rhs);//必须是成员函数};
derived::derived(const derived & rhs) :base(rhs){
``````
}
derived &derived::operator=(const derived &rhs){
base::operator=(rhs);`````
return *this;}
-
不要尝试以某个copying函数实现另一copying函数。应该将共有的部分提取出来放在一个函数中(一般设置为看private,函数名init),并由两个函数共同调用。
Copy assignment操作符调用copy构造函数就像试图构造一个已经存在的对象。
Copy构造函数调用copy assignment操作符就像试图调用一个已经构造完成的对象(copy assignment操作符只是使用于已经初始化的对象上)。