/*
1.引入函数的模板
2.const修饰匿名函数的生命周期会延长
3.在初始化时,调用要初始化对象的构造函数
4.memcpy进行的“假”浅拷贝
5.等号两边为同类型时,会调用其复制重载。6.迭代器失效,以及如何修正迭代器
7.类中的函数模板
*/
vector作为一个容器,不仅仅可以接收char还可以接收int、double甚至自定义类型,所以它的设计就不能和string的设计一样。
框架如下:
class vector { public: typedef T* iterator; //code... private: iterator _start=nullptr;//指向容器的开头 iterator _end=nullptr;//指向容器已存在内容的结尾 iterator _end_of_storage=nullptr;//指向容器的结尾 };
这里使用了默认参数
1.构造函数:
vector() :
_start(nullptr),
_end(nullptr),
_end_of_storage(nullptr)
{}
vector(size_t n, const T& v = T()) { //queston1
_start = new T[n];
_end = _start;
_end_of_storage = _start + n;
while (_end != _end_of_storage) {
*_end = v; //queston2
_end++;
}
}
//拷贝构造
vector(const vector<T>&v) {
_start = new T[v.capacity()];
_end = _start;
_end_of_storage = _start + v.capacity();
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
}
思考:
一、
vector(size_t n, const T& v = T())
为什么这里要使用const T &v=T(),把这个换成const T&v=0行不行?
答:
不可以
在vector中,其存储的类型不仅仅为int,还要其他类型比如string类甚至包含自定义类型
如果换为const T &v=0,则在传递string类或其他类型时,产生错误。
那么T()是什么?
匿名函数,这里是T类型的构造函数,设计时,我们不可能知道使用者会用vector初始化何种类型,所以只能调用使用者给出的类型的默认构造参数作为v的值,这样不论使用的人传递哪种类型,这里都可以对它进行初始化。
const在此处的另一种含义:已知匿名对象的声明周期只有这一行,这点,我们在前面就已知,那么我们使用const修饰匿名对象,const会延长匿名对象的生命周期,直到对引用对象域结束。这里的const必不可少。
二、此处的等号的含义:
c++中,等号两边都为同种类型时,会调用其赋值重载
比如string s1 string s2; s1=s2,会调用string 类的赋值重载(=号重载)
那么在这里_end为T* ,*_end为T类型,而参数v也为T类型,所以这里满足
T = T,会调用其赋值重载。
另外
此文章对这里的=号进行了对于本次代码设计的进一步介绍。
2.计算size与capacity()
size_t size()const {
return _end - _start;
}
size_t capacity() const{
//cout << _end_of_storage - _start << endl;
return _end_of_storage - _start;
}
3.扩容:
void reserve(size_t n) {
//应该这样写:
if (n > capacity()) { //question1
size_t oldSize = size();
T* tmp = new T[n];
for (int i = 0; i <oldSize; i++) { //quesiton2
tmp[i] = _start[i];
}
delete[]_start;
_start = tmp;
_end = _start + oldSize;
_end_of_storage = _start + n;
}
}
一、重点:
**** 警惕:memcpy造成的"浅拷贝" ***
如果写为
if (n>capacity()) {
size_t oldCapacity = capacity();
T* tmp = new T[n];
memcpy(tmp,_start,sizeof(T)*oldCapacity);
delete[]_start;
_start = tmp;}
这样写可以吗?不可以,虽然我们开辟了空间、也进行了memcpy拷贝,但仍然时浅拷贝。
理由:
假如我们初始化了vector<string>,
vector的成员变量为:_start,_end_end_of_storage;
string的成员变量为:_str,_size,_capacity;
然后_start指向的是string这个容器,而不是_string的_str
这样在进行memcpy时,将_start的东西拷贝给tmp,实则是将string这个容器的地址拷贝了过去
所以是进行的浅拷贝为了直观清楚的表达出问题所在,现在使用string来作为自定义类型,扩容
二、还请注意size_t 与Int的比较,会有类型的隐式提升
思考:
现在假设定义vector<vector>上面这个程序(不是思考中的程序)还正确吗?
很明显不正确,因为我们调用的是vector的等号赋值重载,而
vector的复制重载还没有实现出来,所以是错误的。
下面我们应该把vector的复制重载写出来。void swap(vector<T>& v) { std::swap(_start,v._start); std::swap(_end,v._end); std::swap(_end_of_storage,v._end_of_storage); } vector<T>& operator=(vector<T> v) {//这里会调用拷贝构造,并且要写上模板参数 swap(v); return *this; }
这里的交换函数思路如下:
赋值重载的参数为非引用,会创建传进来的参数的拷贝,然后将这个拷贝放进swap中,与_start _end _end_of_storage互换。
至此,看到这里便可返回上文,查看另一篇文章的链接了,我再把链接贴一遍:
void resize(size_t n,const T&v=T()) {
if (n>size()) {
if (n>capacity()) {
reserve(n);
}
_end_of_storage = _start + n;
while (_end!=_end_of_storage) {
*_end = v;
_end++;
}
}
}
4.迭代器
typedef T* iterator;
typedef const T* const_iterator;
iterator begin() {
return _start;
}
iterator end() {
return _end;
}
const_iterator begin()const {
return _start;
}
const_iterator end()const {
return _end;
}
5.从任意位置插入
iterator insert(iterator it,const T& v) {
if (size()==capacity()) {
size_t len = it - _start;
reserve(capacity() == 0 ? 10 : capacity() * 2); //question1
it = _start + len;//扩容后迭代器失效了,需要修正
}
iterator end = _end;
while (end!=it) {
*(end) = *(end - 1);
end--;
}
*it = v;
_end++;
return it;
}
思考:为什么返回值要使用iterator 为什么函数参数要使用iterator it ,使用pos it不可以吗?
以及迭代器失效问题。
先来看迭代器失效的问题:迭代器失效,实际就是迭代器底层对应指针所指向的 空间被销毁了,而使用一块已经被释放的空间。更广义的迭代器失效,不仅仅指指向的一片被释放的空间
举个例子:
1.所指向的内容被释放掉了
在c++中的扩容都是异地扩容,而不是原地扩容,很容易想象到:
假设
string s1("abcd");
auto it =s1.begin(); //it指向a
s1.resize(100);//对s1进行扩容为100 c++为了处理扩容,就需要开辟新的空间,将旧空间里面的数拷贝过去,然后释放掉旧的空间。
此时我们在对it进行访问,则会处问题因为it指向的是一个被释放掉的空间,成为了野指针。
2.迭代器不失效,但会引起的歧义:
void text_list() { list<int>ret; ret.push_back(0); ret.push_back(1); ret.push_back(2); ret.push_back(3); auto it = ret.begin(); ret.insert(it, 9); for (auto val : ret) { cout << val << " "; } cout << endl; cout << *it << endl; } void text_vector() { vector<int>ret; ret.reserve(10); ret.push_back(0); ret.push_back(1); ret.push_back(2); ret.push_back(3); auto it = ret.begin(); ret.insert(it, 9); for (auto val : ret) { cout << val << " "; } cout << endl; cout << *it << endl; } int main() { cout << "list------------" << endl; text_list(); cout << "vector----------------" << endl; text_vector(); return 0; }
使用了两个函数,分别输出在插入后,打印迭代器所指向的值。
output:
list------------
9 0 1 2 3
0
vector----------------
9 0 1 2 39 //这里其实会报错,vs强制检查,通过调试可以得出这个结果为9
我们发现使用迭代器调用begin,然后再在该迭代器处插入数据,最后打印迭代器,这两个迭代器的值居然不一样。--这也是迭代器引起的第二个错误,严格意义上不能算迭代器的失效,针对不同的实现方法,迭代器的值也会产生变化,那么插入数据时,我们甚至都不会提前知道数组是否扩容,所以使用插入后的没有更新的迭代器是危险的行为!!!
注意:使用erase也会造成迭代器的失效:
#include <iostream> using namespace std; #include <vector> int main() { int a[] = { 1, 2, 3, 4 }; vector<int> ret(a, a + sizeof(a) / sizeof(int)); vector<int>::iterator pos = ret.begin(); // 删除pos位置的数据,导致pos迭代器失效。 ret.erase(pos); cout << *pos << endl; // 此处会导致非法访问 return 0; }
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代 器不应该会失效,这一点和我上述所描述的相同,但是:如果pos刚好是最后一个位置的元素,删完之后pos刚好是end的位置,而end位置是 没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效 了。考虑考虑如果删除list中的,那么不论是否是最后位置的元素,都会造成迭代器失效
思考:什么操作会导致迭代器失效?
答:会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、 push_back等。
那么仅仅是vector、list会引起迭代器失效吗?不不不,string进行相关的操作也会引起迭代器的失效。
迭代器失效的解决办法:
在使用前,对迭代器重新赋值即可。
好,迭代器失效已经解释清楚,下面回答:为什么返回值要使用iterator 为什么函数参数要使用iterator it ,使用pos it不可以吗?
1.为什么返回值要使用iterator。因为迭代器会失效,所以我们在这里返回修正后的迭代器。
2.为什么函数模板要使用iterator,这是在这里使用int pos也可以,只不过到了后面的list再使用int pos就有局限性了,访问慢。
知识点--在类中定义新的类模板
template<class T> class vector{ //code... template<class InputIterator> vector(InputIterator first, InputIterator last) { while (first != last) { push_back(*first); first++; } } //code. }
用处:我们有个string类的对象,我们想用string类去初始化vector<int>,就可以使用在始终定义新的类模板来接收参数,为什么是string,这里因为string为字符,字符可以转换为数字,所以只要能相互转化的都可以这样使用。
string s("abcd"); vector<int> ret(s.begin(),s.end()); for (auto val:ret) { cout << val << " "; }
来看看什么时候会匹配到这里:vector(InputIterator first, InputIterator last)
这里的参数类型都是Inputterator,也就是我们传进来的两个参数类型相同,再没有更好的匹配下,就会匹配这里?
什么是更好的?
vector(int a ,int b) vector(size_t ,int b)
vector(10,1);这个会去匹配vector(int a, int b)因为这个没有涉及到类型转换,与我们传进去参数更加匹配。
但是我们在程序里面加上这个后,再使用vector(10,1)时,则会匹配到这里,而不会匹配到
vector(size_t n, const T& v = T())该构造函数里面,因为该构造函数里面涉及到了类型转换。
所以为了能使vector(10,1)能正常匹配,那么我们就要重载vector(size_t n, const T& v = T())为
vector(int n,const T&v=T())。