Effective C++【二】(Scott Meyers著 侯捷译)

构造函数、析构函数和Assignment运算符
条款11:如果class内动态配置有内存,请为此class声明一个copy construction和一个assignment运算符。

// 一个很简单的string类
class string {
public:
string(const char *value);
~string();
... // 没有拷贝构造函数和operator=
private:
char *data;
};
string::string(const char *value)
{
if (value) {
data = new char[strlen(value) + 1];
strcpy(data, value);
}
else {
data = new char[1];
*data = '\0';
}
}
inline string::~string() { delete [] data; }

以上程序会引发多种问题。现在用其生成两个对象 string a("hello");string b("world");
如果执行b=a操作,则会执行默认的assignment运算符,只是一个简单的把a的地址赋值给了b,引发了两个问题,b所指的内容丢失并且所占内存没有释放;a和b中任意一个析构,那么另外一个指针将指向一个一个已经被删除的内存地址。同样的,未定义copy构造函数也会造成这样的结果。都会造成memory leak。
copy constructor的情况和assignment运算符的情况有一点点不同,因为只要程序中有pass by value的动作,就会调用copy constructor,尽量不要以by value的方式来传递对象。像上边的程序,考虑下边情况:

void donothing(string localstring) {}
string s = "the truth is out there";
donothing(s);

所有事情看起来都没什么危害,但是由localstring是以by value的方式传递,他就要使用缺省的copy constructor,因此 localstring有一个s所含指针的副本。当donothing完成后推出它的scope后,其destructor被调用,后边的你就很熟悉了,s内含指针所指内容已经被destructor删除了。
凡此指针别名问题,解决之道就是,如果你的类里含有任何指针,那么请撰写你自己的copy constructor和assignment operator。
但有时候是不需要新分配内存,仅需要一个引用时,可以实现某种reference counting策略,追踪记录目前有多少个对象指向该数据结构,这样就能在内存和时间上获得巨大节省。
对某些classes而言,实现copy constructor和assignment operator会有点得不偿失,特别是当你有理由相信你的clients不会执行copy或assignment时也是可以省略掉的,但应遵循一点忠告:将这些函数声明为private,并且不要定义之,这样就阻止了clients调用。(很好的的点子!)
这里也要特别注意,当operator new使用了[]时,delete 也要相应的有[]。

条款12:在constructor 中尽量以initialization动作取代assignment动作,即尽量运用constructor的member initialization list。
简单来说呢,就是constructor内assignment能初始化的成员 member initialization list也能够完成,并且能够做到constructor不能初始化的const member,不仅如此,在效率上 initialization list更胜前者,也会节省空间成本。在编码上也会得到方便,class成员类型有变化的时候也无需操心初始化的问题。
如果要初始化成员太多,使用initialization list会让人抓狂,那么就另外建一个private的init()函数吧,注意private啊。
另外解释一下为什么const决不能在constructor初始化,因为一个class会生成很多实例,每次都会调用constructor,这样你就懂了吧,所以要在外边单独初始化。另外记住对static class member进行initialization和nonstatic class member 初始化完全不同,差异大到形成另一条款47。

条款13:initialization list中的members初始化次序应该和其在class内的声明次序相同
具体原因呢就是为了destructor的时候节约各种时间内存成本,因为次序相同了会比较容易析构。
另外说明一下只有nonstatic data members的初始化才适用这条规则。Static data members的作用有点像global objects和namespace objects,所以他们只需要初始化一次,如果运用了继承机制,你应该在你的member initialization list起始处就列出base class的初值设定。如果使用的是多重继承机制,base class将以“被继承的次序“来初始化;此时base class出现在member initialization list中的次序亦不予考虑。使用多重继承将有更多事情需要操心,关注条款43.

条款14:总是让base class拥有virtual destructor。
了解虚函数,了解多态,之后对这个条款就理所当然了。很容易理解,为了避免继承之后的类析构出现各种内存管理方面的问题。如果一个class没有把析构函数设置为virtual,那么说明作者并没有打算让别人继承这个类。
这里书中解释了一下virtual function的实现,我了解这块,但还是提一下。实现虚函数必须夹带一些额外的信息,因为在执行时方便协助决定”哪一个虚函数被调用“。大部分编译器是一个所谓的virtual table pointer来呈现这份额外信息。vptr指向一个由函数指针形成的数组,称为vtbl(virtual table);每一个带虚函数的class都有一个相应的vtbl,当某个对象调用虚函数时,编译器循着对象的vptr所指的vtbl,决定实际调用哪一个函数。这样会产生一个class占用内存多少的问题,就是多了一个函数指针的大小。
另外需要注意的是这种c++语言的virtual设计,也会过犹不及,无条件的将destructors都声明为virtual,和从不声明virtual一样是不明智的,有一条好的规则,大部分情况都可以有效运作:只有当class内含至少一个虚函数时,将destructor声明为virtual。
但不幸的是即使虚函数缺席的情况下,扔有可能遭遇nonvirtual destructor的问题,如果该类被继承,而继承类加了一些功能,并未重新定义base class的任何行为,这时候仍然会出现nonvirtual destructor问题。
另外提一点,某些classes内声明一个pure virtual destructor会带来不少方便,因为pure virtual functions会导致抽象类无法实例化,有时候既想让一个类无法实例,又没有合适的函数使之成为pure virtual function,那就声明一个pure virtual destructor吧。

条款15:令operator=传回“*this的reference”
这个条款不难理解,很正常的,因为你重载赋值符号,返回的结果一定要是一个引用,只有是引用才能成为赋值符号的左值(左值一般是变量之类的,常量啊什么的不能当做左值,如x=1可以,但是不能1=x,这也是为什么有些书中提示说判断语句中尽量把常量写左边,这样如果你少写一个=号,编译器会提示你错误)。另一个错误是传回operator=传回void,看似合理,但是它会妨碍assignment动作串链的形成。不要这么做。
这里书中提到了一点我想做个补充,书中说令operator=传回一个“代表某const object”的reference,像:const Widget& operator=(const Widget& rhs)这么做是为了阻止client做出这样的蠢事:
Widget w1,w2,w3;
(w1=w2)=w3;
将w2赋值给w1然后将w3赋值给前者的运算结果,如果Widget的operator=传回的是个const,就可以在编译时期阻止这件事的发生。书中说这种行为比较愚蠢,但阻止他或许更愚蠢,因为内建型别允许我们这么做,比如int i1,i2,i3; (i1=i2)=i3;。虽然这没什么价值的样子,但如果对int可行,对我以及对我的classed应该也是可行的才对,对任何classes应该都是如此。我想说的是,如果重载++,--这样的操作符时,应该特别注意的就是这样一点,必须加上const,以防止c++++,++++c或者c--++之类的情况出现。因为这个是不需要实现串链的部分,或者是你需要阻止这种串链书写的时候你需要传回值是const。

条款16:在operator=中为所有的data members设定内容
一般你没有声明一个自己的assignment运算符的话,c++不会介意帮你生成一个,但是为什么通常大家都不太在乎这个编译器帮你完成的,条款11告诉过你。所以一般要写自己的assignment运算符。那么就请记住这条规则。最初很容易记住,但是需要注意的是当通过base class生成derived class的时候,大一堆别的事情会轻易让你忘记这条规则。危害示例:

class base {
public:
base(int initialvalue = 0): x(initialvalue) {}
base(const base& rhs): x(rhs.x) {}
private:
int x;
};

class derived: public base {
public:
derived(int initialvalue): base(initialvalue), y(initialvalue) {}
derived& operator=(const derived& rhs):y(rhs.y) { }
private:
int y;
};

逻辑上说,derived的赋值运算符应该象这样:

// erroneous assignment operator
derived& derived::operator=(const derived& rhs){
if (this == &rhs) return *this; // 见条款17
y = rhs.y; // 给derived仅有的
// 数据成员赋值
return *this; // 见条款15
}

不幸的是,它是错误的,因为derived对象的base部分的数据成员x在赋值运算符中未受影响。例如,考虑下面的代码段:

void assignmenttester(){
derived d1(0); // d1.x = 0, d1.y = 0
derived d2(1); // d2.x = 1, d2.y = 1
d1 = d2; // d1.x = 0, d1.y = 1!
}

最直接的办法是在Derived::operator=中对x做赋值动作,不行的是这不合法,因为private。因此必须在Derived的assignment运算符内做一个明显的Bass::operator=(res)的操作。但仍有些编译器会拒绝这种“对base class的assignment运算符”的调用动作,随不正确,但可能存在,为摆平这些变节的家伙,你需要这样:

Derived& Derived::operator=(const Derived& rhs){
if(this==&rhs) return *this;
static_cast(*this) = rhs; //调用operator=,施行于*this的Base成分身上。
y=rhs.y;
return *this;
}

这个转型请注意,是将this指针的值转换成了Base的引用。
另外上例程序中还有一个地方有错误,需要注意。也是c++领域最难缠的臭虫:当derived object以copy方式被构造出来时,并没有连带的复制base class成分,当然默认的base 成分确实被构造出来了,但却不是我们需要的那个拷贝。所以我们应该明确的调用base的copy constructor。直接加入到initialization list中即可:derived(const derived& rhs):base(rhs),y(rhs.y){}

条款17:在operator=中检查是否自己赋值给自己
例如:

class X{...};
X a;
a=a;

这看起来愚蠢的做法是完全合法的。一般不会有这样的,但是一个量会有很多别名,如果a的别名是b,如果a=b;效果是一样的。两个理由促使我们针对这种情况所处assignment运算符中的特殊处理。一个是效率,检测到这种情况直接返回即可。另外一个侦测“自己赋值给自己”行为,非常重要的是先确保安全性。因为assignment运算符在为其左侧对象配置新资源之前,通常必须先将原来的左侧对象的所有资源释放掉,然而如果面对“自己赋值给自己”,这将造成可怕灾难。这种灾难确实显而易见,就不举例了。所以我们必须检测这种情况,现在问题来了,什么叫“两个对象相同”?
技术上称为object identity,有两个基本解决办法,方法一就是看看他们是否有相同的值;另一个方法就是检验他们的内存是否相等。
另外如果你需要一个更精巧的判断两个对象是否相同,你就需要自己实现。最常见方法是:利用一个member function传回某种识别码:
class C{
public:
ObjectId identity()const; //同时见条款36
....
};
如果两个对象a、b相同,那么a->identity()==b->identity();当然此时你需要为ObjectId编写相应的operator==。
另外需要注意的是object identity问题并不仅仅局限于operator=函数内出现,所有出现references和pointers的情况都可能是object identity。
ps:条款17最后说的object identity问题经个人vs2008测试,并没有测试出问题,请明白了解的指教。

我猜你可能也喜欢:

No Comments - Leave a comment

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>


Welcome , today is 星期二, 2017 年 10 月 24 日