首先我们来实现一个能装string的容器,只实现了尾插和打印接口。
template <class T>
class SeqList{
public:
SeqList()
:_a(NULL), _size(0), _capacity(0)
{}
void PushBack(const T& x)
{
if (_size >= _capacity){
Extend();
}
_a[_size++] = x;
}
void Print()
{
for (size_t i = 0; i < _size; i++){
cout << _a[i] << " ";
}
cout << endl;
}
~SeqList()
{
if (_a){
delete[] _a;
_size = _capacity = 0;
}
}
private:
void Extend()
{
if (_size >= _capacity){
//扩容时,如果容量为0,那就给它赋值为3,否则就二倍的扩容
_capacity = _capacity > 0 ? _capacity * 2 : 3;
T* tmp = new T[_capacity];
//使用memcpy浅拷贝的形式扩容
memcpy(tmp, _a, sizeof(T)*_size);
delete[] _a;
_a = tmp;
}
}
private:
T* _a;
size_t _size;
size_t _capacity;
};
注意:扩容时,现在采用的是memcpy浅拷贝。
接下来进行测试:
void Test()
{
SeqList<string> s;
s.PushBack("aaa");
s.PushBack("bbb");
s.PushBack("ccc");
s.PushBack("ddd");
s.Print();
}
由于构造函数里全部初始化为0,第一次扩容时定容量为3,所以在插入第四条数据“ddd”的时候一定会再次扩容。
扩容时,new一个新的空间,大小是之前容量的二倍,然后通过浅拷贝的方式把老空间的数据赋值给新空间,然后delete老空间,_a指向新空间。
这里有一个疑问,string类里不是有一个指针成员指向字符串吗?那么扩容方式如果为浅拷贝,在delete老空间的时候,每一个string对象调用它自己的析构函数,那么”aaa”,”bbb”,”ccc”这些空间就被释放了,所以新空间里string对象的指针不就成野指针了吗?之后不应该崩溃吗?
如果这样插入数据:
void Test()
{
SeqList<string> s;
s.PushBack("aaa");
s.PushBack("bbb");
s.PushBack("cccccccccccccccccccccccccccccc");
s.PushBack("ddd");
s.Print();
}
程序直接就崩溃了。
难道崩溃不崩溃,还和我插入数据的长度有关吗?
有关
string底层实现机制是:
class string{
......
private:
char _Buf[16];//如果存的数据小于16字节,就存在数组里,大于16字节才用到指针
char* _ptr;
size_t _Mysize;
size_t _Myres;
......
};
所以才有了刚才怪异的问题,第一个例子,我存的都是小于16字节的数据,都是用数组存的,扩容时,浅拷贝不影响什么;第二个例子,我存了一个很大的数据,这时是用指针指向的,扩容时,浅拷贝当然就崩溃了。
解决办法:
扩容拷贝数据时,使用深拷贝,利用string的operator=
void Extend()
{
if (_size >= _capacity){
_capacity = _capacity > 0 ? _capacity * 2 : 3;
T* tmp = new T[_capacity];
//memcpy(tmp, _a, sizeof(T)*_size);
for (size_t i = 0; i < _size; i++){
tmp[i] = _a[i];//利用string的operator=
}
delete[] _a;
_a = tmp;
}
}
这样就不会崩溃了:
然后深拷贝的代价是很大的,需要拷贝数据,最理想的做法是,对于一些没有指针的对象,比如int,char,再比如上文中存数组的string对象,我们可以采用浅拷贝的方法;只有当对象有指针时,才只能使用深拷贝。
关于类型萃取的实现,我在之前有介绍过,附上链接:
https://blog.youkuaiyun.com/han8040laixin/article/details/78575736