理论的东西必须要实践才能真的变成自己的
转载:https://www.cnblogs.com/sea520/p/12711565.html
https://www.cnblogs.com/renboyu/p/13150264.html -》很详细
一.背景
电脑主板的检测:有一张主板的图片,需要对上面的各个器件进行检测,每个器件调用不同的算法。
逻辑设计:算法的参数保存在一个容器中,一个线程(线程A)向容器push参数,另一个线程(线程B)一直从容器取参数,并开启线程调用算法
参数结构体:
//小ROI的检测信息 struct TestParam { bool NeedSavePic; bool OnlySaveResultPic; int msg; int configVecIndex; int GroupIndex; //不为1则为secSource string SmlroiType;//检测点的器件类型(DIP_AOI使用) string strROIID; //小矩形ID string BigROIID; //大矩形ID string SaveImgPath;//存小图路径 string dllName; string funcName; vector<Mat> vecMat; vector<string> vecString; vector<int> vecInt; vector<Rect> vecRect; //固定信息 vector<ConfigMsg> g_ConfigVec;//检测点的大ROI的类型 如:SSD , HDD SmallROITestParam() { msg = 0; } };
二.保存参数结构体容器的选择
C++下面编程,首先就是想到使用STL提供的vector来保存TestParam。问题由此产生。
A线程一直向vector push参数,
B线程循环从vector中取参数:
//B线程 int index = 0; while ( testSmallROIEndFlag ) { std::thread aa = std::thread(Func, ref(TestParam.at(i))); aa.detach(); index++; }
问题:运行过程中随机出现crash。
log分析显示,调用算法crash。单独把算法和参数拿出来调试又没有问题。在参数push进vector前和传进算法前都数出来做对比,发现参数在传进算法的时候已经变成了一堆随机的值,并且有时还没有输出参数就已经crash。在这个过程中也没有对参数的读写动作。
卡了很久。。。
寻找同事帮助: 1.会crash 2.参数自己被改变。 两个问题产生的原因 : 第一个原因多数都是内存的非法访问导致,第二个原因是访问未初始化的内存。为什么会访问非法的内存???中间就是去读取了vector而已,怎么可能会访问非法内存。难道是vector传进B线程
就访问不了?vector地址失效?反正就是各种猜测。最后就是突然想到vector自动扩容的实现方式,找到了问题点。
划重点:
vector的内存结构和自动扩容的实现:
内存结构:一块连续的内存,对其中元素的访问可以通过下标的方式。也就是基地址+偏移地址就能直接访问到对应的元素。
自动扩容:一开始定义一个空的vector的时候,预先分配的大小为0(可以通过capacity()查看),后面每次push元素的时候,如果vector当前的容量已达到最大,就会自动申请一块 1.5-2倍于之前内存大小的连续内存,然后把元素移动到新内存上,释放掉之前的内存。
2个问题产生的原因:就在于vector的自动扩容机制。在push参数的时候,会发生内存重新分配,但是算法当中使用的还是之前的vector地址,造成了非法内存访问以及随机内存数据。
解决方法:1.使用链表或者队列
2.vector有提供reserve() 函数。在定义的时候,可以预先分配一个最小指定容量大小的连续内存,在vector的容量超过设定值之前,都不会重新分配内存。
学习:理论和实践要结合。
对vector的实现都有了解,但是使用过程中都不会去思考到这些问题。
三.常见容器的初步了解
STL容器主要分为两大类:1.顺序容器 2.关联容器
顺序容器主要有:string vector list deque stack...
关联容器主要有:map set...
1.vector
vector是一段连续的内存地址,
我写代码过程中选择vector的原因:
1.支持随机访问:想访问哪一个元素,就访问哪一个元素(最大的优点)
2.不用像数组一样事先分配好一块固定大小的内存,而且不用管当前vector的内存是否够用,因为vector内部有实现自动扩容的机制:如果当前vector内剩余的空间已经使用完,再向vector中push元素时,会自动申请一块当前
内存1.5-2倍大小的新的连续内存,然后把所有元素迁移到新内存,释放之前的地址。以此完成自动扩容。
3.vector中元素的访问快,因为是一段连续的内存空间,所以可以直接通过 基地址+偏移地址 的方式访问。vec[1] 直接到 begin+1 的内存位置取一个元素 vec.at(1)会先判断是否越界,然后再取第一个元素
缺点:
1.因为每次是固定分配前一次1.5-2倍大小的内存,所以当数据量大时,存在浪费内存空间的问题。(不过因为工作中没遇到过需要处理大量数据的情况,所以从来没考虑过这个问题)
2.每次vector扩容时,花费的时间长以及会产生开篇的那个问题。
3.插入和删除涉及到其它位置数据的移动
//1.定义和初始化 vector<int> vec1; //默认初始化,vec1为空 vector<int> vec2(vec1); //使用vec1初始化vec2 vector<int> vec3(vec1.begin(),vec1.end());//使用vec1初始化vec2 vector<int> vec4(10); //10个值为0的元素 vector<int> vec5(10,4); //10个值为4的元素 //2.常用操作方法 vec1.reserve(10); //预设大小为10 vec1.push_back(100); //尾部添加元素 int totalSize = vec1.capacity(); //容器大小 int size = vec1.size(); //元素个数 size<=totalSize bool isEmpty = vec1.empty(); //判断是否为空 cout<<vec1[0]<<endl; //取得第一个元素 vec1.insert(vec1.end(),5,3); //从vec1.back位置插入5个值为3的元素 vec1.pop_back(); //删除末尾元素 vec1.erase(vec1.begin(),vec1.begin()+2);//删除vec1[0]-vec1[2]之间的元素,不包括vec1[2]其他元素前移 cout<<(vec1==vec2)?true:false; //判断是否相等==、!=、>=、<=... vector<int>::iterator iter = vec1.begin(); //获取迭代器首地址 vector<int>::const_iterator c_iter = vec1.begin(); //获取const类型迭代器 vec1.clear(); //清空元素 vec1.shrink_to_fit(); //把capacity大小改为当前size大小 //3.遍历 //下标法 int length = vec1.size(); for ( int i=0; i<length; i++ ) { cout<<vec1[i]; } cout<<endl<<endl; //迭代器法 vector<int>::iterator iter = vec1.begin(); for ( ; iter != vec1.end(); iter++ ) { cout<<*iter; }
2.list
双向链表,每个元素都有两个指针,一个指向前一个元素的地址,一个指向后一个元素的地址。头的前向指针为空,尾的后向指针为空。不是一块连续的内存,每个元素通过指针关联起来。
优点:
1.数据的插入和删除很简单(简单是指实现简单,对使用者来说:插入和删除都一样,就是调用接口)
2.不会浪费内存,没有预分配内存的动作,当需要一块内存时,才会申请一块内存
缺点:
1.好像STL中没有提供随机访问的支持。
2.如果实现随机访问,也会很慢,每次的随机访问都需要遍历list,然后找到对应的数据
//1.定义和初始化 list<int> lst1; //创建空list list<int> lst2(3); //创建含有三个元素的list list<int> lst3(3,2); //创建含有三个元素为2的list list<int> lst4(lst2); //使用lst2初始化lst4 list<int> lst5(lst2.begin(),lst2.end()); //同lst4 //2.常用操作方法 lst1.assign(lst2.begin(),lst2.end()); //分配值,3个值为0的元素 lst1.push_back(10); //末尾添加值 lst1.pop_back(); //删除末尾值 lst1.begin(); //返回首值的迭代器 lst1.end(); //返回尾值的迭代器 lst1.clear(); //清空值 bool isEmpty1 = lst1.empty(); //判断为空 lst1.erase(lst1.begin(),lst1.end()); //删除元素 lst1.front(); //返回第一个元素的引用 lst1.back(); //返回最后一个元素的引用 lst1.insert(lst1.begin(),3,2); //从指定位置插入个3个值为2的元素 lst1.rbegin(); //返回第一个元素的前向指针 lst1.remove(2); //相同的元素全部删除 lst1.reverse(); //反转 lst1.size(); //含有元素个数 lst1.sort(); //排序 lst1.unique(); //删除相邻重复元素 //3.遍历 //迭代器法 for ( list<int>::const_iterator iter = lst1.begin(); iter != lst1.end(); iter++ ) { cout<<*iter; }
3.deque
双端队列:由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。
deque 容器用数组(数组名假设为 map)存储着各个连续空间的首地址。也就是说,map 数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间。
通过建立 map 数组,deque 容器申请的这些分段的连续空间就能实现“整体连续”的效果。换句话说,当 deque 容器需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾添加指向该空间的指针,由此该空间就串接到了 deque 容器的头部或尾部。
如果 map 数组满了,再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间。
(所以deque的底层实现很复杂)
//后面的使用过再说
4.map
容器提供一个键值对(key/value)容器
5.set