vector

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. 其他接口
  1. maxsize():最大

  2. capacity

  3. reserve 扩容.resize 扩容加初始化,当空间更小的时候就会进行删除数据。避免前后抖动所以不改变capacity.1664329737997

  4. 断言检查是否越界opeartor[]; (N>size),就会自动出现断言,而at()是抛异常。

  5. push_back() pop_back()

  6. assign(10,5)//用10个5进行覆盖空间内容。

没有提供find()函数,是为了复用。vector list queue都需要find,那么直接就在算法中提供了全局find函数。

5. find()

1664330089056

//在#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';
		}
	}
}

1664330635041

二、应用题

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()拷贝构造

默认是浅拷贝,两个对象析构时会对同一块资源进行释放,在你手动实现了析构函数的情况下会崩溃.所以要手动完成深拷贝.

  • 原始写法就是各种开空间拷贝

  • 现代写法-支持迭代器区间初始化

    image-20230116122726682

迭代器区间构造对象,一个类模板的成员函数也可以是一个函数模板.

//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);
		}

image-20230116132531169

所以并不是只有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是快排三数取中,底层要支持随机访问.

image-20230116133340639

放到静态区,生命周期变成全局的,但是作用域还是在局部,为了避免每次调用函数的时候都需要创建你的变量占用空间.

迭代器失效

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也浅拷贝下来,在析构函数进行释放的时候就会造成释放两次相同的空间造成崩溃.

image-20230202131055790

  • 如何完成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[]不做检查,需要调用者自己判断.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值