vector
一、接口了解
1.构造
#include<vector>
vector<int > v1;//动态顺序表,无参,内存池分配空间
vector<int >v2(10,8);//十个8
vextor<int >v3(++v2.begin(),--v2.end());//不想要第一个和最后一个
vector<int >v4(v3);//拷贝构造
string s("hello world");
vector<char>v5(s.begin(),s.end());
-
string和vector<char>能替换吗?
string专有接口+=,还有string最后一位的\0.
没法支持比较大小.
2.析构
3.遍历的几种方式
vector<int>v;
v.push_back(1);//支持尾插尾删,不支持头插头删因为效率太低,可以用insert erase
//[]+i
for(size_t i=0;i<v.size();++i)
{
cout<<v[i]<<" ";
v[i]+=1;//可修改
}
//迭代器.
vector<int >::iterator it=v.begin();
while(it!=v.end())
{
*it-=1;
++it;
}
//范围for
for(auto e:v)
{
cout<<e<<" ";
}
//原生指针就是特殊的迭代器,数组支持范围for ,会被替换成指针int*p=a;
4. 其他接口
-
maxsize():最大
-
capacity
-
reserve 扩容.resize 扩容加初始化,当空间更小的时候就会进行删除数据。避免前后抖动所以不改变capacity.
-
断言检查是否越界
opeartor[];
(N>size),就会自动出现断言,而at()
是抛异常。 -
push_back() pop_back()
-
assign(10,5)//用10个5进行覆盖空间内容。
没有提供find()函数,是为了复用。vector list queue都需要find,那么直接就在算法中提供了全局find函数。
5. find()
//在#include<alogorithm>算法头文件
//迭代器区间都是左闭右开 [first,last)
find();//不是成员函数,而是算法里面的全局函数
//找不到就返回last(最后一个值),找到就返回迭代器位置
vector<int >::iterator ret=find(v.begin(),v.end(),3);
if(ret!=v.end())
{
cout<<"找到了"<<endl;
}
- 为什么string 中有find()?
int main()
{
string name = "yuanweiyuanweiyunawei ";
auto it1 = name.find("yuan");
cout << it1 << endl;//0
}
因为string 是要查找子串的,并不是简单的一个值,可能是一个子串多个值。
内置find成员函数只会返回第一次出现字符串的首部位置.
6. insert()&erase()
vector的位置都是迭代器
v.insert(v.begin(),30);
//删除某个值,之前要先找到那个元素.如果删除一个不存在的迭代器位置就会崩溃
vector<int >::iterator pos=find(v.begin(),v.end(),3);
if(pos!=v.end())
{
erase(pos);//迭代器insert可能会失效,所以不能插入之后就进行删除
}
7. clear()
删除全部数据但是不会删除空间.
8. 是如何增容的?
vs下1.5倍接近(但是不同平台下还是不一样的):测试代码
int main()
{
size_t sz;
std::vector<int> foo;
sz = foo.capacity();
std::cout << "making foo grow:\n";
for (int i = 0; i < 100; ++i) {
foo.push_back(i);
if (sz != foo.capacity()) {
sz = foo.capacity();
std::cout << "capacity changed: " << sz << '\n';
}
}
}
二、应用题
1.只出现一次的数字
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto e: nums)
ret ^= e;
return ret;
}
};
2. 杨辉三角
二维数组的创建M行N列,还需要支持动态开辟。
C语言中需要;一个指针数组,数组中每一个元素是二级指针,每一个位置存放的是int*
两次
C++是vector<vector<int>> vv;
vv[i][j]
两次函数调用。调用两次析构函数将空间释放
class Solution
{
public:
vector<vector<int>> generate(int numRows)
{
vector<vector<int>> vv;//声明一个二维数组
vv.resize(numRows);//设置空间大小
for(size_t i=0;i<numRows;++i)
{
vv[i].resize(i+1);//第n行设置空间为n个
vv[i][0]=vv[i][vv[i].size()-1]=1;
}
for(size_t i=0;i<vv.size();++i)
{
for(size_t j=0;j<vv[i].size();++j)
{
if(vv[i][j]==0)//resize初始化的时候初始化为0,所以当找到0的那一个节点就进行运算。别的都被初始化为1了
{
vv[i][j]=vv[i-1][j]+vv[i-1][j-1];
}
}
}
return vv;
}
}
3. 电话号码的组合
//电话号码的字母组合,将已知数字字符串的字母组合写出来.映射和回溯的过程用递归的方法
class Solution
{
//用常量字符串初始化,建立映射
string arr[10]={"","","abc","def","ghi","jki","mno","pqrs","tuv","wxyz"};
public:
void _letterCombination(const string &digits,size_t i,
string combinStr,vector<string >&strV)
//combinStr设置为传值,就不会在下次调用中修改传的值,也就返回后不用pop最后一个字母
{
if(i==digits.size())
{
strV.push_back(combinStr);//一个组合完成之后就放到指定容器当中
return ;//走递归的返回条件,当一个组合完成之后想回返回
}
string str=arr[digits[i]-'0'];//确定给定数字字符串的第一个数字,对应的字符串
for(size_t j=0; j< str.size();++j)//j表示第i的数字字符的字符串的第j个字符
{
_letterCombination(digits, i+1, combinStr+str[j], strV);
// 下一行的第一个字符 加上第二层的第j单个
}
}
vector<string >lettetCombination(string digits)
{
string combinStr;//需要记录每一个组合出来的字符串
vector<string >strV;//用容器装这些组合下来的数组
if(digits.empty())
return strV;
//在传进来的数字字符串不是空的情况下
_letterCombination(digits,0,combinStr,strV);//数字字符串,第一个位置的字符
}
};
三、vector的实现
内置类型也可以有构造函数,在C++升级模板之后就进行了统一的规划,兼容模板的做法。
int j=int();
int m(20);
void resize(size_t n, const T& val = T())//resize需要提供不同类型的缺省值
- T()匿名对象声明周期只有这一行,为什么缺省值可以在函数体中起作用并且完成内容的初始化?
首先引用匿名对象要用const&,
在加上const&之后由于匿名对象的生命周期得以延长,只有到引用结束时才会结束.
如果类T显示的实现了构造和析构函数,编译器认为匿名对象已经完成了初始化,出了函数的作用域再去调用匿名对象的析构函数.
如果你匿名对象没有显示实现构造函数,也就没什么内容,匿名对象的声明周期只有这一行.VS编译器直接调用了析构函数进行释放。
但是在没有写构造函数的时候,函数体结束时又vs编译器多调用一次匿名对象的析构函数,算是一个bug.
vector()拷贝构造
默认是浅拷贝,两个对象析构时会对同一块资源进行释放,在你手动实现了析构函数的情况下会崩溃.所以要手动完成深拷贝.
-
原始写法就是各种开空间拷贝
-
现代写法-支持迭代器区间初始化
迭代器区间构造对象,一个类模板的成员函数也可以是一个函数模板.
//v2(v1)原始写法
/*vector(vector<T>&v)
{
_start = new T[v.capacity()];
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
memcpy(_start,v._start,sizeof(T)*v.size());
}*/
//现代写法-支持迭代器区间初始化
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector(vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//交换之后随机值给到tmp调用析构函数就崩溃,所以要对他先进行初始化.析构函数不能析构随机值
vector<T> tmp(v.begin(),v.end());
swap(_start, tmp._start);
swap(_finish, tmp._finish);
swap(_end_of_storage, tmp._end_of_storage);
}
vector赋值操作
赋值操作,传参的时候使用传值传参,先拷贝构造一份,之后我们在函数体中直接交换就行了.交换完成之后,v接收*this 不需要的资源,然后函数体结束v自动释放,一石二鸟.
//v2=v1
vector<T>& operator=(vector<T> v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
两个容器之间的交换仅仅是交换内容,所以自己实现swap交换,避免了三次拷贝构造
void swap(vector<T> v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
所以并不是只有string有深拷贝.
函数模板的模板参数要传迭代器区间时,命名规范对类型有暗示。
为什么写成InputIterator
,不都是模板吗?我起别的类型的迭代器名称有什么影响吗?
只读迭代器和只写迭代器:input_iterator output_iterator:没有实际的对应的类型
单向迭代器:
forword_iterator
unordered_map unordered_set forword_list
双向迭代器:bidirectional_iterator
list map
随机迭代器:randomaccess_iterator
deque vector
从上往下为继承关系,下面的满足上面的所有要求。
sort需要至少随机迭代器reverse需要至少双向迭代器.sort是快排三数取中,底层要支持随机访问.
放到静态区,生命周期变成全局的,但是作用域还是在局部,为了避免每次调用函数的时候都需要创建你的变量占用空间.
迭代器失效
vector-Insert()
当插入的时候涉及到扩容的问题时,pos位置指向的是原来空间的内个已经被交还给系统的地方.这是就涉及到了野指针的问题,迭代器上就是迭代器失效的问题。所以在扩容之前要先算出相对位置,更新一下pos.
vector<int>::iterator pos = std::find(v.begin(), v.end(), 2);
if (pos != v.end())
{
//这里的pos,insert是传值,
//如果发生扩容,在函数中pos被修改了,但是这里的pos仍然指向的是已经被释放的位置
//就叫做迭代器失效,本质就是野指针
//如果要用这个位置,你再接收一下
v.insert(pos, 30);
}
处理办法就是在插入完成之后,返回新插入的位置的迭代器 Iterator insert().
vector-erase()
- 迭代器失效:
删除的时候,删除所有的偶数。由于实现erase函数时,用后面的值直接覆盖要删除的位置,导致it指向位置的意义已经变了,直接++it,会有漏网之鱼没被判断,如果存在连续的偶数就会导致后一个偶数还没有判断,没有删掉.
再其次,erase之后有些vector的实现可能会出现缩容,如果是这样,erase之后it 可能是野指针,就像是Insert的扩容一样,但是一般不会缩容.
如果最后一个是偶数,覆盖最后一个位置,--_finish
之后,++it
,就会有越界访问造成崩溃.
所以,erase 会有一个返回值记录着删除位置的后一个位置。在使用的时候做一些处理就好了。
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it=v.erase(it);//重新接收值,避免漏网之鱼的出现
}
else
++it;
}
vector的迭代器失效主要发生在insert和erase中,一个是野指针,一个是指针内容发生改变。使用标准库中的vector,不正确的使用,vs下会对erase这些情况进行强制性检查都报断言错误.
那么string 的insert和erase迭代器是否会失效?有
那么什么时候会失效?和vector完全一样。string一般用下标访问进行插入,所以很少出现迭代器失效.
只要是使用迭代器访问的容器,都会存在迭代器失效的问题。
memcpy()
扩容和深拷贝的时候用到了memcpy,拷贝整数没问题,但是拷贝字符串的时候发生了崩溃,当单个字符串很长时还会出现乱码.
memcpy是浅拷贝,将_start
内容完全拷贝到tmp中,包括每个_str
的各个指针_Ptr
也浅拷贝下来,在析构函数进行释放的时候就会造成释放两次相同的空间造成崩溃.
-
如何完成string对象的深拷贝呢?
void reserve(size_t n) { if (n > capacity()) { size_t sz = size(); T* tmp = new T[n]; if (_start) { //memcpy(tmp, _start, sizeof(T) * size()); for (int i = 0; i < sz; i++) { //一个一个的拷贝,无论是int还是string 都可以 tmp[i] = _start[i]; } delete[] _start; } _finish = tmp + size(); _start = tmp; _end_of_storage = _start +n; } }
-
为甚短一点不会出现乱码?
VS进行优化,当字节小于16时,对象里面有一个数组char_Buf[16]
中,这时就不用去堆上申请和释放空间,用对象的空间大一点换取时间.如果比较大,就放到堆上面去。但是这两种情况的对象大小还是一样的。
class string
{
private:
char _Buf[16];
char* _Ptr;
size_t _mysize;
size_t _myres;
};
sizeof(string())=28;
所以字符串比较短的时候,字符串存在于对象的数组里面,就将内容拷贝下来的了,没啥问题。长字符串会出现问题。
小结
支持高效的随机访问,像指针一样去访问容器。
缺点:空间不够要去增容,代价比较大。存在空间浪费,插入数据涉及到挪动数据效率低下。
迭代器失效之后如果不重新赋值,再进行++的话就会导致程序崩溃。
vector的删除操作不仅会导致被删除元素的迭代器失效,还会导致指向后面数据的迭代器失效。
删除之后迭代器进行返回赋值,不会导致迭代器失效。
at()函数和[]运算符的重载,两者都可以得到相应下标的值,
唯一的区别就是,at函数会对边界作出检查,operator[]不做检查,需要调用者自己判断.