现在的位置: 首页 > 综合 > 正文

Visual Studio 2010 STL的一个bug导致内存泄露

2012年12月11日 ⁄ 综合 ⁄ 共 4857字 ⁄ 字号 评论关闭

 

Visual
Studio 2010 STL
的一个bug导致内存泄露

随着C++ 0x离我们越来越近,Visual Studio 2010已经支持了很多C++0x的语言特性并且改写了STL的实现来支持这些特性。这里有一个Stephan
T. Lavavej
发布的VS2010种支持的C++0x特性列表:

http://blogs.msdn.com/b/vcblog/archive/2010/04/06/c-0x-core-language-features-in-vc10-the-table.aspx

 

C++0x的众多特性中,右值引用无疑是最令人激动的新特性之一。因为它解决了C++长久以来被人诟病的临时对象的效率问题。VS2010已经在STL的容器和算法中加入了对右值引用的实现, 我们只需重新用VS2010编译以前版本的程序,就可以在效率上获得比较明显的提升。然而最近在学习右值引用的过程中,发现VS2010的实现的一个bug会导致内存泄漏。先来看一个简单的例子:

1. std::string s0 = "123";

2. std::string s1(std::move(s0));

3. s0 = s1;

 

这个例子先定义了string类型对象s0, 由于s0是一个具名对象,所以是一个左值。接着定义了s1,并用std::moves0转换为一个右值,用这个右值来初始化s1,最后再给s0赋值,这时用任何string对象给s0都会引起内存泄漏,之所以用s1是为了简单,不需要定义别的变量。如果用VS2010编译运行这段代码,会有以下输出(示例源代码请参考本文附录):

Detected memory leaks!

Dumping objects ->

{164} normal block at 0x002B4A58, 16
bytes long.

 Data: <                > CD CD CD CD CD CD CD CD CD
CD CD CD CD CD CD CD

Object dump complete.

 

为什么就泄漏了?让我们一行一行来分析。很显然第一行代码没有问题,这只是再普通不过的一个对象初始化。那么第二行会有问题吗?表面上看不出来,需要深入的挖掘一点。std::moveSTL的一个模板函数,用来把左值转化为右值,这其中涉及到一些技巧,如特殊的模板参数推断,引用折叠等。细节可以参msdn中关于右值引用和move语义的介绍。现在我们可以知道传给s1的构造函数是一个右值,所以会调用string的右值构造函数basic_string(_Myt&&
_Right)
,由于_Right是右值,所以在这个函数退出前,会把_Right的内部数据清零,像下面这样:

_Right._Mysize = 0;

_Right._Myres = 0;//为内存泄漏埋下隐患

这样_Rightsizecapacity就都是0了。这就为内存泄漏埋下了隐患。因为Visual StudioSTL string 实现了小字符串优化,也就是说string总是至少占16个字符的空间,对于小于15个字符的构造,赋值等,string不需要分配堆内存,在自己的栈上就可以完成了,这样有利于提高string对于小字符串操作的效率。正式因为这样,所以不应该把stringcapacity也清零,而是应该设为缺省的状态,也就是15个字符大小。

 

 

 

接下俩看看第三行的赋值操作,在这个操作中会进行这样的检查:

if (this->_Myres < _Newsize) //_Newsizes1size。 也就是3

       _Copy(_Newsize,
this->_Mysize); // reallocate to grow

 

由于第二行把s0capacity清零了,所以this->_Myres
< _Newsize
会成立,也就是需要在堆上分配内存来容纳是 s1的字符。_Copy会在堆上分配16个字符的内存(为什么是16,具体分配多大的内存有一个算法,可以直接参考string的代码)并且把s0capacity设为15(不包括’/0’),然后把s0已有的字符复制到这块内存上,由于s0size0,所以分配内存后就直接返回了。这块新分配的内存会被用到吗?不会,因为string的小字符串优化使得string总是占有16个字符的空间,如果新的堆内存小于小于等于16,那么string不认为指向堆内存的指针有效,而只会使用栈内存的指针。下面这个表达式描述了选择堆还是栈指针:

//_Bx._Ptr是堆指针,_Bx._Buf是栈指针,_BUF_SIZE是常量16

//_Myres是新分配内存的大小(16-1

this->_BUF_SIZE <= this->_Myres ?
this->_Bx._Ptr : this->_Bx._Buf

 

所以后续的操作会在栈上进行,新分配的堆就被遗忘了,析构时也不会释放(因为string并不认为有堆内存存在)。这样就造成了内存泄漏。

 

到这里,我们已经知道内存泄漏是由于string引入的右值引用特性没有很好的和string的小字符串优化交互引起的,这时有的读者也许会有疑问:上面给出了例子不太实际,第二行把左值转为右值就意味着这个左值一般情况下不会继续被使用了,因为转为右值后会被窃取其内部资源,可是第三行却又继续给这个左值赋值,实际中应该不会这样来用。是的,前面的例子是为了用最简单的代码来说明问题,接下来我们看一个实际一点的例子,引起内存泄漏的原因和前面例子是一回事。请看代码:

 

1. std::vector<std::string> Vec;

2. Vec.push_back("1");

3. Vec.push_back("2");

4. Vec.push_back("3");

5. Vec.push_back("4");

6. Vec.push_back("5");

 

7. const std::string name = "6";

8. Vec.insert(Vec.begin(),name);

 

编译运行这段代码,同样会检测到内存泄漏。原因和第7行与第8行有关。我们先看第8行,其直接导致了内存泄漏。在执行第8行之前,Vec已经插入5个元素,它的size5capacity6,如下图所示,虚线框表示第6个已被分配但尚未初始化的元素。

 __________________........

| 1   |  2  |  3  |  4  |   5 |       :

|___|___|___|___|___|.......:

 

 

8行代码要求在Vec的起始位置新插入一个元素6,目前的capacity刚好有多余的一个元素空间,所以不需要重新分配更大的内存,只需要把元素15都往后挪一个位置,再把新的元素6放到第一个位置即可。VS2010的做法是先把5 拷贝构造到虚线框的位置(由于虚线框的位置不能直接赋值),然后依次把4321往后挪,用赋值操作来完成,最后把新的6放到第一个位置。

 

 

 __________________........

| 6   |  1  |  2  |  3  |   4 |   5  :

|___|___|___|___|___|.......:

 


 

当在第一步把5拷贝构造到虚线框的位置时,VS2010使用了move语义,也就是这一步之后元素5的位置string对象的sizecapacity都变成了0,如下图所示:

 

 

 ______________$$$$$.........

| 1   |  2  |  3  |  4  $   5   $   5  :

|___|___|___|___$$$$$.......:

 

 

 

 

虚线框的位置已经是5,而$框的位置string对象的sizecapacity都被清零了,这样当把4往后赋值的时候就会出现和本文开始的例子一样的结果,造成内存泄漏。

 

到这里基本已经把问题解释清楚了,还有一点需要说的就是这个例子的第7行,只有当这一行定义了一个const string时,才会造成内存泄漏,如果是非const string,则不会有泄漏发生。这是因为string作为参数调用了一个不同的insert函数的重载版本,该函数并不采用对容器元素拷贝和赋值的方式,而是用swap完成了工作。

 

最后,幸运的是,MS已经知道了这个问题并将在VS2010SP1中修正它,目前在SP1RTM版本尚未发布,所以我们在使用stringvector<string>时应该留意一下会不会有内存泄漏的问题。

 


附录:

程序1

  

 

 

程序2

 

 

 

抱歉!评论已关闭.