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的this关键字
和一些面向对象的语言一样,JavaScript也支持this关键字,顾名思义,this关键字指代本对象,在传统的面向对象语言中,this代表该方法所属的对象,或者该对象的指针。
在JavaScript中没有类的概念,this方法的指代含义也有些许的变化,指代的是调用该方法的对象。
还是以上次的程序作为例子:
var User = function() { this.username = 'username'; this.password = 'password'; } User.prototype.toString = function() { return this.username + ':' + this.password; };
toString方法中就使用了this关键字来指代调用者本身,一般情况下,调用者和该方法的所属对象是一致的,这种情况下,就与其他语言没什么不同。不过,JavaScript中调用者和方法所属的对象并不一定是一致的,许多情况下与闭包特性结合,也能产生许多特殊的效果。下面是一个稍微复杂的例子。
var x = function() { this.show = function(msg, func, scope) { document.write(msg + " : " + this.value + '<br />'); if (scope === undefined) scope = this; func.apply(scope, []); func.call(scope); } }; var y = new x(); x.prototype.value = 4; y.value = 5; var z = new x(); z.value = 6; var w = new x(); x.prototype.value = 7; y.show('value is', function() { document.write('value in scope : ' + this.value + '<br />'); }, z); y.show('value is', function() { document.write('value in scope : ' + this.value + '<br />'); }, x.prototype); y.show('value is', function() { document.write('value in scope : ' + this.value + '<br />'); }, w);
x构造函数定义了一个show方法,首先输出调用者的value值,然后如果scope没有传入,则定义为方法的调用者本身。
随后出现了两个方法,分别为apply和call,这两个方法是所有function都具有的,作用是强制指定某对象作为该方法的调用者,两者除了调用格式略有不同,作用是相同的。这两句话以scope的身份调用了func。
后面定义了y、z、w三个变量,都分别是x的实例,接着以y的身份调用了3次show方法,只有scope参数传入了不同的值。
第一次传入了对象z,此时y作为show的调用者,而show中的参数匿名函数func的调用者却被设定为z,因此输出结果为:
value is : 5 value in scope : 6 value in scope : 6
第二次传入x.prototype,原理如上面相同,show的func参数是由x.prototype调用的,因此输入结果为:
value is : 5 value in scope : 7 value in scope : 7
第三次传入了对象w,原理也和上面相同,func的调用者是w,那么此时w的值是多少呢?我们在实例化w以后,我们修改了x.prototype.value的值,那么此时w的value应该是x.prototype.value修改前的值还是修改后的值呢?
答案是修改后的值,结果是:
value is : 5 value in scope : 7 value in scope : 7
因为w并没有显式定义value属性,因此w的value属性是位于其__proto__属性中,也就是x.prototype的引用,因此当x.prototype发生了改变,对于w也是会同时产生影响,具体关于prototype的原理和行为,请参考之前的其他文章。