C++对于this指针使用placement new的隐患
我们知道使用placement new可以直接在现有的指针上构造对象,而不是先分配内存,再构造对象。这种技术经常被用在内存池管理中来优化内存管理效率。
有些情况下,我们希望能够让类的一个构造函数去调用另一个构造函数帮助我们完成工作,实现某种程度上的代码重用。比如下面这个例子。
class Factory { private: Widget widget; public: Factory() { new (this) Factory("factory"); } Factory(const char *name) : widget(name) { /* something else useful */ } };
当Factory的默认构造函数被调用的时候,我们用Factory的默认名字“factory”去调用它自己的那个接受一个参数的构造函数,因为我们让placement new在自己(this)身上进行就地构造。不幸的是,这个例子里构造函数和析构函数没有配对调用的。
当调用者调用默认构造函数的时候,在函数体任何代码执行之前,所有Factory的子对象,也就是widget的默认构造函数Widget()被调用了。然后Factory决定再次调用自己带有一个参数的构造函数版本来完成构造,这个时候widget将再次被构造。最后当这个Factory的对象被析构时,相应的会调用一次widget的析构函数来销毁widget。
假设我们构造的这个Factory对象叫f。如果在Widget的默认构造函数中申请并占有资源,那么在f.widget没有被析构的情况下再次调用构造函数,那么之前f.widget所占有的资源就泄露了。于是,我们就必须在使用placement new之前,显式调用widget的析构函数来清理掉原来widget占有的资源。
class Factory { private: Widget widget; public: Factory() { widget.~Widget(); // 新加了这一句 new (this) Factory("factory"); } Factory(const char *name) : widget(name) { /* something else useful */ } };
如果Factory有许多的成员,我们是不是要逐个调用这些成员的析构函数呢?这里高端大气的暴力方法是直接调用Factory自身的析构函数,那么Factory的析构函数会负责帮助我们析构所有的子对象并释放资源(反正我们接下来要一切从头来过,用另一个构造函数构造全新的Factory)。这里我们没有定义Factory的析构函数,那么就会使用编译器提供的默认版本,为我们调用所有子对象,也就是widget的析构函数。
class Factory { private: Widget widget; public: Factory() { this->~Factory(); // 调用自身的析构函数 new (this) Factory("factory"); } Factory(const char *name) : widget(name) { /* something else useful */ } };
看起来有些丑陋,我们在自己的构造函数里,调用了自己的析构函数,然后又调用了另一个构造函数。如果要这样做,我们需要确保Factory的析构函数能够正确处理还没有被完全构造的对象,在这个简单的例子里,默认的析构函数已经足够处理这个问题,不过对于更加复杂的结构,我们还是需要认真考虑,对于这种没有完成构造的对象,析构函数能不能正确处理。
另一个问题,如果Factory有一个虚析构函数,那么Factory类的对象都需要一个虚函数表指针vptr来确保对象能够正确调用它的虚成员函数(包括析构函数)。一般实现在对象构造的一开始,任何基类或子对象的构造函数被调用之前,vptr已经有正确的值了,也是比较符合常理的做法。不过因为标准没有规定给vptr赋值的确切时机,所以并没有什么机制来保证我们一定能够运行正确的析构函数。
因此,为了实现借助其他构造函数完成构造的,我们写出来的代码不太安全。就这个简单的例子有以下方法可以取代这种做法。
- 把“factory”作为第二个构造函数name参数的默认值,直接去掉第一个构造函数即可。
- 把构造逻辑的公共部分抽象出来成为一个private成员函数来专门对未完成初始化对象的初始化动作(不要写成虚函数)。
所以,尽量避免对this指针使用placement new,我们一般能够找到更好的设计来解决这样的需求。如果一定需要,那么务必要审慎考虑上面所提到的这些因素。
JavaScript中prototype关键字的作用
JavaScript并不是一种面向对象的程序设计语言,严格来讲,它并没有什么类的概念,顶多只是JSON对象而已,然而在JavaScript中有prototype这个关键字,利用它,我们可以间接地实现一些面向对象的特性。
var User = function() { this.username = 'username'; this.password = 'password'; this.toString = function() { return this.username + ':' + this.password; } } var user = new User(); document.write(user + '<br />');
这是一个很简单的封装实现,此时的User可以被视为一个“类”,这里还有一个方法叫做toString,实际上,在需要字符串的地方,对象的toString方法会自动被调用。
如果在上面这个例子中,toString没有被定义在User里面,也可以通过prototype关键字来定义在外部,效果是等价的。
User.prototype.toString = function() { return this.username + ':' + this.password; };
如果在这个例子中没有写prototype关键字,那么user对象实际上是没有toString这个函数可以调用的,因为没有prototype代表这个toString函数仅被定义在User本身之上。
这是什么原理呢?
首先,使用new关键字进行新对象user的构造时,是调用了上面赋值给User的匿名函数,这个函数赋予了新对象user一些属性,就像在函数体中所写的那样。如果toString没有被定义在里面,那么此时user只有username和password两个属性。
如果在外部定义的toString没有加上prototype关键字,那么会发生什么情况呢?User对象具有了toString方法,然而User本身却没有username和password。因此,此时User的username与password均为undefined,有没有觉得比较像面向对象语言中的静态方法呢?是的,相同点在于:我们可以通过“类名”直接调用该方法,该方法不能使用“非静态”成员。
如果在toString被定义在User.prototype之下,user对象就会具有toString方法,这是为什么呢?因为每个对象都隐含了一个名为__proto__的属性,相当于在User构造函数的最后增加了:
this.__proto__ = User.prototype; //注意,这个__proto__是内部名称,不同浏览器实现不同名称也可能不相同。
prototype这个特殊的属性在构造函数定义时被初始化,初始化时包含了User的构造函数本身和自己的__proto__属性,当用户通过User.prototype的方式定义新属性或方法的时候,这些属性或方法就被增加到User.prototype这个JSON对象之中。由于this.__proto__ = User.prototype;的缘故,调用这个构造函数构造的所有对象的__proto__属性中,都包含有这个新属性或方法。因此,实际上toString并没有成为user下的一个方法,而是user.__proto__下的一个方法。
然后,实际执行user.toString()时,浏览器将检查user是否具有toString方法可以调用,如果没有,浏览器将继续检查其__proto__属性中是否有toString方法,如果仍然没有,则继续递归向上寻找这个__proto__属性的__proto__属性。如此,就可以解释为什么prototype下定义的方法,都可以被该“类”的所有实例调用的原因了。