vector模拟实现
Tips:new申请空间不用判断,因为失败的话会抛异常。
STL源代码中vector的私有成员变量如下:
private:
iterator _start;//首元素
iterator _finish;//最后一个有效数据的下一个,-_start为size
iterator _endofstorage;//-_start为容量
迭代器失效
情况1 会引起其底层空间改变的操作
会引起其底层空间改变的操作都有可能使迭代器失效,比如:resize、reserve、insert、assign、push_back等。分析:其实就是野指针问题。
例1 insert模拟实现
insert不一定会导致迭代器失效,insert里扩容就一定会导致迭代器失效,看如下代码
void insert(iterator pos, const T& val)
{
if (size() == capacity())
{
size_t newcapacity = (capacity() == 0) ? 4 : 2 * capacity();
reserve(newcapacity);
}
//挪动数据
_end = _finish - 1;
while (_end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
++_finish;
}
//调用
v.insert(v.begin(), 10);
由于一开始传参的时候,传的是v.begin(),扩容后可能是异地扩容,那么v的实际begin()位置就变了,在挪动数据的时候就会出错。_end >= pos
此时pos的指向是错误的,这个循环挪动数据就会出错。
所以在扩容时要注意同步更新pos。
//更正代码如下
if (size() == capacity())
{
size_t len = pos - _start;//记录pos实际长度
size_t newcapacity = (capacity() == 0) ? 4 : 2 * capacity();
reserve(newcapacity);
pos = _start + len;//更新pos
}
同理引起底层空间改变的操作,也不再适合使用pos
vector<int>::iterator it = find(v.begin(), v.end(), 10);
if(it != v.end())
{
v.insert(it, 20);
}
(*it)++;//it失效,不能再使用了,v有可能进行扩容了
出错原因:以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。
情况2 删除指定位置元素 erase
例1 删除末尾数据
it = std::find(v.begin(), v.end(), 3);
if (it != v.end())
{
v.erase(it);
}
//检测it是否失效--VS2019不失效,VS2013失效
//读
std::cout << *it << std::endl;
//写
std::cout << (*it)++ << std::endl;
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
例2 删除偶数
it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
it = v.erase(it);//更新it,认为it失效
else
++it;
}
//测试用例---->结果
//1 2 3 4 --> segmentation fault
//1 2 3 4 5 --> 1 3 5
//1 2 2 3 4 5 --> 1 2 3 5
接下来分析出现这三种情况的原因:
深拷贝
浅拷贝reserve中的memcpy问题
以动态二维数组举例–杨辉三角
void reserve(size_t n)
{
//reserve只扩不缩
if (n > capacity())//扩容
{
size_t old_size = size();//保存size
T* tmp = new T[n];
if (_start != 0)//如果对象本来为空就不用拷贝了
{
memcpy(tmp, _start, old_size * sizeof(T));
delete[] _start;//如果_start==0,这里就会出错
}
_start = tmp;
_finish = _start + old_size;
_endofstorage = _start + n;
}
}
当使用杨辉三角的代码运行我们模拟实现的vector时,调用reserve还是会出问题。
因为对象是vector<vector<int>>
,T可以是int
,也可以是vector<int>
。当T为int时没什么问题,问题出在当T是vector<int>
的时候,会因为memcpy的浅拷贝而报错,因为此时_start
相当于int**
,里面保存的是地址!那使用memcpy就相当于浅拷贝了,下一句代码delete[] _start;
释放原空间,相当于此时tmp拿到的内容(指针指向的空间)失效了。【当T为int时,_start
相当于int*
,里面保存的是数值】
解决方案:
if (_start != 0)
{
//memcpy(tmp, _start, old_size * sizeof(T));
//要用下面这个写法
for(size_t i = 0; i < n; i++)
{
tmp[i] = _start[i];
}
delete[] _start;//如果_start==0,这里就会出错
}
拷贝构造函数
该函数只要求已有的数据一模一样即可,没有要求capacity也要一样。
//传统写法1
vector(vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(v.capacity());
for(const auto& e : v)
{
push_back(e);
}
}
//传统写法2
vector(vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
size_t len = v.size();
_start = new T[v.size()];
_endofstorage = _finish = _start + len;
}
//现代写法
vector(vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
vector<int> tmp(v.begin(), v.end());
swap(tmp);
}
现代写法需要下方这个成员函数来当工具人。
template <class InputIterator>
vector(InputIterator first, InputIterator last);
成员函数再套模板问题
//1
vector (size_t n, const T& val = T());
-------------------
//2
template <class InputIterator>
vector(InputIterator first, InputIterator last)
-------------------
vector<int> v(10, 1);//这个写法本意是调用1,但是编译器会调用2,就会报错--编译错误
vector<int> v(10, 'A');
原因:因为10和1都是字面量,10和1都会被识别为int类型,写法1需要隐式类型转换,故优先匹配写法2模板,在vector下first会被解引用,实际上int类型的值无法再解引用,因此报错;
而A被识别为char,虽然10得隐式转换,但是int和char无法匹配写法2,只能匹配写法1。
解决方案:
为了匹配写多个版本(size_t、int、long)vector (int n, const T& val = T());
vector (size_t n, const T& val = T());
等
题目
电话号码的字母组合
其实是回溯问题。要全排列。得用vector套string
//这个写法更简洁
vector<string> _numStr[10] = { "", "",
"abc", "def", "ghi", "jkl",
"mno", "pqrs", "tuv", "wxyz" };
--------------------最土的方法-------------------
string s0("");string s1("");string s2("abc");
string s3("def");string s4("ghi");string s5("jkl");
string s6("mno");string s7("pqrs");string s8("tuv");
string s9("wxyz");
vector<string> vecStr;
vecStr.reserve(10);
vecStr.push_back(s0);vecStr.push_back(s1);vecStr.push_back(s2);
vecStr.push_back(s3);vecStr.push_back(s4);vecStr.push_back(s5);
vecStr.push_back(s6);vecStr.push_back(s7);vecStr.push_back(s8);
vecStr.push_back(s9);
初始化完成后,建议用递归来完成全排列。代码如下
class Solution {
//把0 1 都放进来,以便下标直接访问
string _numStr[10] = { "", "", "abc", "def", "ghi", "jkl","mno", "pqrs", "tuv", "wxyz" };
public:
void Combine(const string& digits, size_t i, vector<string>& vecStr, string comStr)
{
//递归的层数就是digits的字符个数
//如果i等于字符个数了,即遇到'\0',结束递归
if(i == digits.size())
{
vecStr.push_back(comStr);
return;
}
int num = digits[i] - '0';//取出对应的数字
//[i] -> "数字" -> -'0' -> (int)数字
string str = _numStr[num];//根据数字对应的字符串
//str有几个字符就要走几次递归
for(auto ch : str)
{
Combine(digits, i+1, vecStr, comStr+ch);
}
}
vector<string> letterCombinations(string digits) {
vector<string> vecStr;
//如果为空,直接返回
if(digits.empty())
return vecStr;
string comStr;//保存每次组合出来的字符串
size_t i = 0;//表示递归层数
Combine(digits, i, vecStr, comStr);
return vecStr;
}
};