阅读侯捷的《STL
源码剖析》有感,于是想把这些想法记录下来。
用STL也有一段时间了,容器,迭代器,算法,仿函数等等都是工作中常用的STL组件。
可是往往我们忽略了这些组件的根源——内存管理,因为这个对于STL的使用者可以说是透明的。
内存分配分为两种方式,一是直接调用C++的new和delete,另一种直接调用C的melloc和free。
对于第一种形式,它的工作方式是先分配内存,然后对内存进行初始化。
对于第二种形式,它只对其进行内存分配,不进行其他操作。
而对于SGI(STL的一个实现版本),它采用了第二种形式。
今天汤小磊(Fox)同学对这种分配方式的稳定性提出了置疑:如果只分配空间,不进行初始化会不会不稳定?
从理论上讲会的,因为如果对一个对象进行内存分配,不进行初始化,那调用类中成员函数对类属性进行操作时,的确会造成未定义行为。
但SGI为什么能正常的工作呢?我认为要归功于STL的巧妙设计和C++的编译器。
首先,模板是一个“编译前行为”。在进行编译前,编译器要替换所有的宏定义,即简单的把宏定义的内容进行替换,接着就要进行模板分析了。对于一个容器vector<int>,int类型会被替换到vector所定义的模板中,如果是尖括号里面写的是类,则会定义一个类的类型写进去。也就是说在尖括号里填写不同的内容,就像是定义了一个不同数据结构的vector,数据操作的行为也是vector的。这样就保证了数据操作行为的一致性。
其次,我们看见的容器并完全不是我们“看见的”。咱们随便一个容器的定义(SGI版本):
template <class T, class Alloc = alloc>
class vector{…};
这后面的这个模板参数就是我们现在讨论的内存分配器。也就是说每个容器在定义的时候就已经确定了内存分配的方法,进一步说容器定义内存管理的方法是透明的。(当然这种方法可改,可是如果不看源码,这个操作就是透明的。)
第三,容器使用方法确定了内存分配行为。看一个例子:
如果尖括号里添的是类:
class A
{
public:
A():m_data(0){}
int getData() const
{
return m_data;
}
void setData(const int data)
{
m_data = data;
}
private:
int m_data;
}
int main()
{
vector<A> testVector;
A tempA;
testVector.reserve(100); //告诉vector我要用一个大小为100*sizeof(A)的内存
for (int i = 0; i < 100; ++i ) //在循环中没有A的初始化、没有内存再分配
{
tempA.setData(i);
testVector.push_back(tempA);
}
}
从例子中我们看见一个类作为模板参数传给了vector,因此vector知道了类的数据结构大小。
声明tempA分配空间,并且调用A的构造函数。
调用reserve成员函数,让vector有一个“心理准备”说我要用一个100*sizeof(A)的内存。然后进入循环给tempA中的m_data赋值。
关键一步, testVector.push_back(tempA); 没有调用A的构造函数,而是直接将内存拷贝到vector事先分配好的空间里。
在STL中,内存分配的方法全都用容器的成员函数包裹起来了,并且传入成员函数的参数已经被限定为一个对象,这个对象在你声明的时候已经初始化过了,因此,只要你容器用的对,就不用担心你放进容器中的对象没有初始化。这就是SGI采用malloc和free作为内存分配的巧妙之处。