前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >STL容器的线程安全性了解多少?

STL容器的线程安全性了解多少?

作者头像
用户9831583
发布2022-12-04 16:18:20
1.3K0
发布2022-12-04 16:18:20
举报
文章被收录于专栏:码出名企路码出名企路

容器

STL的意思是与迭代器合作的C++标准库的一部分,包括标准容器(包括string),iostream库的一部分,函数对象和算法,它不包括标准容器适配器(stack,queue和priority_queue)以及bitset和valarray容器,因为它们缺乏迭代器的支持,也不包括数组。数组以指针的形式支持迭代器,但数组是C++语言的一部分,并非库。

STL中的迭代器,算法和函数对象,其中容器用的是最多的,它们比数组更强大和灵活,可以动态增长或缩减

,可以管路自己的内存,跟踪自己拥有的对象数目,限制它们支持操作的算法复杂度。

本章你将学到:

1 选择适当的容器应该面对的约束 2 避免产生为一个容器类型写的代码特可以用于其他容器类型的错觉 3 容器里对象拷贝操作的重要性 4 当指针或auto_ptr存放在容器中时出现的难点 5 删除的输入和输出 6 你可以或不可以使用自定义分配器 7 达到做高效率的技巧和考虑在多线程环境下容器的使用

条款1:仔细选择你的容器

1,标准STL序列容器:vector, string , deue和list 2, 标准STL关联容器:set, multiset,map和multimap 3,非标准序列容器 slist 和 rope, slist是一个单向链表,rope本质上是一个重型字符串,见条款50 4,非标准关联容器 hash_set, hash_multiset, hash_map和hash_multimap 5, vector作为 string的替代品,有时候vector可以在时间和空间上都表现得比标准关联容器好 6,几种标准非stl容器,包括数组,bitset,valarray,stack,queue和priority_queue

//条款50

// slist : 单向链表 ,而 list是双向链表

//https://blog.csdn.net/qq_40843865/article/details/102393667

/**

单链表容器 slist: 消耗的空间更少,功能收到很多限制,由于slist只能向前,当插入操作是将元素插入到迭代器所指位置之前而不是之后,slist没办法快速找到其前一个节点,只能从头遍历,因此 slist不提供push_back操作,只有:insert_after, erase_after,push_front

*/

//代码实现:https://github.com/nguliu/mySTL/blob/master/mySTL/10stl_slist.h

//rope: 超长字符串的类似字符串的容器

//成员函数:https://www.cnblogs.com/wulitaotao/p/11618785.html

/***

rope是个可伸缩的 string的实现,它们被设计为用于把 string看作一个整体的高效操作,赋值,串联和子串的操作不依赖字符串的长度,并且,其接口begin和end成员函数总是返回 const_iterator,阻止了客户进行改变单个字符的操作,针对设计整个字符串动作进行优化

//使用例子:https://blog.csdn.net/qq_36386435/article/details/81239059

vector list 和deque

vector是一种可以默认使用得序列类型 很频繁地对序列中部进行插入和删除时用list 大部分插入和删除发生在序列地头或尾时可以选择deque这种数据结构

连续内存容器:

(基于数组地容器) 在一个或多个动态分配的内存块中保存它们的元素,如果一个新元素被插入或已经存在元素被删除,其他在同一个内存块的元素就必须向上或者向下移动来为新元素提供空间或者填充原来被删除的元素所占的空间。这种移动影响了效率和异常安全,vector, string和deque

基于节点的容器:

在每个内存块动态分配中只保存一个元素,容器元素的插入或删除只影响指向节点的指针,而不是节点自己的内容。所以,当有东西插入或删除时,元素值不需要移动。list和sllist,所有的标准关联容器

条款2:小心对"容器无关代码"的幻想

STL是建立在泛化基础上的:数组泛化为容器,参数化了所包含的对象的类型;函数泛化为了算法,参数化了所用的迭代器的类型;指针泛化为迭代器,参数化了所指向的对象的类型

实例:当你用一个vector结构完成设计时,你总想着泛化容器的不同,想着后面是否可以改成deque或者list等东西代替,善意的泛化,却造成麻烦

结论:写既要和序列容器又要和关联容器一起工作的代码并没有什么意义,因为很多成员函数只存在其中一类容器中,比如,只有序列容器支持 push_front或push_back,只有关联

容器支持 count和lower_bound等等,又如insert或erase在两者表现上却不同。

举例:

1,insert在一个序列容器中,它保留在你放置的位置;但insert到一个关联容器,会按照排列顺序把这个对象移到它应该在的位置 2,erase一个序列容器,会返回一个新迭代器;但在关联容器上什么都不返回

//通过自由地对容器和迭代器类型使用typedef

代码语言:javascript
复制
//这样可以吗?
class Widget{

};
vector<Widget> vw;
Widget bestWidget;
//寻找和bestWidget相等的Widget
vector<Widget>::iterator i = find(vw.begin(), vw.end(), bestWidget);

//改进这样写: 这样改变容器类型变得容易多了
class Widget{

};
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;

WCIterator i = find(cw.begin(), cw.end(), bestWidget);

/** * @brief * typedef 只是其他类型的同义词,所以它提供的封装是纯的词法,不像#define是在预编译阶段替换的,typedef并不能阻止用户使用任何它们不应该用的,如果你不想暴漏出 * 用户对你所决定使用的容器的类型,需要class * * 比如:要限制如果用一个容器类型替换了另一个容器类型可能需要修改的代码,就需要在类中隐藏那个容器,而且要通过类的接口限制容器特殊信息可见性的数量 * 如果你需要建立一个客户列表,请不要直接用list,建立一个类,把list隐藏在它的peivate区域 * */

代码语言:javascript
复制
class Customer{

};
class CustomerList{
    private:
        typedef list<Customer> CustomerContainer;
        typedef CustomerContainer::iterator CCIterator;
        CustomerContainer customers;
    
    public:
        //通过这个接口限制list特殊信息的可见性
};
//一个CustomerList是一个list吗?只用list设计可以吗
//需要快速确定客户列表顶部的20%,使用 nth_element算法,但是该算法需要随机访问迭代器,只适用于 array、vector、deque 这 3 个容器,
// 不能兼容list,这是list可能需要用vector或deque来实现了,此时你要更改的化,需要检查每个CustomerList的成员函数和每个友元,但如果你做好了封装,将影响很小

nth_element

//http://c.biancheng.net/view/7476.html

//https://blog.csdn.net/weixin_52115456/article/details/122652523

代码语言:javascript
复制
 \* @brief 

 \* nth_element

 //排序规则采用默认的升序排序

void nth_element (RandomAccessIterator first,

                 RandomAccessIterator nth,

                 RandomAccessIterator last);

//排序规则为自定义的 comp 排序规则

void nth_element (RandomAccessIterator first,

                 RandomAccessIterator nth,

                 RandomAccessIterator last,

                 Compare comp);

其中,各个参数的含义如下:

first 和 last:都是随机访问迭代器,[first, last) 用于指定该函数的作用范围(即要处理哪些数据);

nth:也是随机访问迭代器,其功能是令函数查找“第 nth 大”的元素,并将其移动到 nth 指向的位置;

comp:用于自定义排序规则。

代码语言:javascript
复制
int a[10]={1,5,6,3,9,2,8,7,4,10};
nth_element(a,a+5,a+10);//查找数组第6小的元素 
cout<<"第6小的数是:"<<a[5];//第6小的数放在了相应的第6个位置了 

#include<iostream>
#include<vector>
#include<algorithm>//必要头文件 
using namespace std;
struct node{
 int x,y;
// 构造函数 
 node(int x,int y){
  this->x=x;
  this->y=y;
 }
};
bool cmp(node n1,node n2){
 return n1.x<n2.x;
}
int main(){
 vector<node> v;
 node n1(1,3);
 node n2(5,2);
 node n3(3,6);
 node n4(2,6);
 node n5(4,4);
 v.push_back(n1);
 v.push_back(n2);
 v.push_back(n3);
 v.push_back(n4);
 v.push_back(n5);
 nth_element(v.begin(),v.begin()+1,v.end(),cmp);
 cout<<"按x排序,第2小点的数是:"<<endl;
 cout<<"x:"<<v[1].x<<' '<<"y:"<<v[1].y;
} 

条款3:使容器里对象的拷贝操作轻量而正确

/** * @brief * * 条款3:使容器里对象的拷贝操作轻量而正确 * * 当你向容器中添加一个对象(insert或push_back等),进入容器的是你指定的对象的拷贝 * * 因此: * 1,容器容纳了对象,但不是你给它们的那个对象 * 2,从容器中获取一个对象,你所得到的对象不是容器的那个对象 * * 因此,本条款关注的是: * 如果你用一个拷贝过程很昂贵对象填充一个容器,性能会产生瓶颈。容器移动越多的东西,你就会在拷贝上浪费越多的内存和时钟周期。 * */

//拷贝是怎么完成的呢?

代码语言:javascript
复制
class Widget{
    public:
        Widget(const Widget&);//拷贝构造函数
        Widget& operator=(const Widget&);//拷贝赋值操作符
};

//切记,由于继承的存在,拷贝会导致分割
//如果你以基类对象建立一个容器,而你试图插入派生类对象,那么当对象(通过基类的拷贝构造函数)拷入容器的时候对象的派生部分会被删除
//分割问题暗示把一个派生类对象插入基类对象的容器几乎总是错的
class SpWidiget:public Widget{

};
vector<Widget> vw;
SpWidiget sw;
vw.push_back(sw);//sw被当作基类对象拷入vw, 当拷贝时它的特殊部分丢失了

//如何解决以上问题呢?

代码语言:javascript
复制
//建立指针的容器而不是对象的容器,即建立一个 Widget* 的容器,不是建立一个 Widget的容器
//1,指针拷贝快 2,当指针拷贝时没有分割; 3, 智能指针更是好的选择
vector<Widget*> vw;
vector<shared_ptr<Widget>> vw;

//虽然STL进行了大量拷贝,但其存在也是为了避免不必要的拷贝,如下做个对比:

代码语言:javascript
复制
//1,数组
Widget w[maxNumWidgets];//建立一个大小为maxNumWidgets的Widgets数组,默认构造每个元素
//2,需要的时候才增长的vector
vector<Widget> vw;//建立一个0个Widget对象的vector,需要的时候可以扩展
//建立一个足够包含namxNumWidgets个Widget的空vector,但没有构造Widget
vector<Widget> vw;
vw.reserve(maxNumWidgets);

条款4:用empty来代替检查size()是否为0

/** * @brief * * 条款4:用empty来代替检查size()是否为0 * * 因为对于所有的标准容器,empty是一个常数时间的操作,但对于一个list实现,size花费线性时间 * * 为什么list不能也提供一个常数时间的size? * * 答案是对于list特有的splice有很多要处理的东西 * */

代码语言:javascript
复制
//任意容器c
if(c.size() == 0)
//等价于
if(c.empty())

//list的splice处理
list<int> list1;
list<int> list2;
//把list2中从第一次出现5到最后一次出现10的所有节点移到list1的结尾
list1.splice(list1.end(), list2, 
    find(list2.begin(), list2.end(),5),//1
    find(list2.rbegin(),list2.rend(),10).base());//2

//https://blog.csdn.net/qq_27198345/article/details/108066510

splice(position, list2): 将list2中的所有元素剪贴到list1的position位置;

splice(position, list2, iter): 将list2中某个位置的迭代器iter指向的元素剪贴到list1中的position位置;

splice(position, list2, iter1, iter2): 将list2中的某一段位置iter1 ~ iter2的元素剪贴到list1中的position位置

代码语言:javascript
复制
#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
 list<int> list1 = { 1, 2, 3, 4, 5 };
 list<int> list2 = { 11, 12, 13, 14, 15 };

 list<int>::iterator iter = list1.begin();
 iter++;

 //把list2全部剪贴到list1的iter位置
 list1.splice(iter, list2);
 cout << "list1: ";
 for (auto& num : list1) {
  cout << num << " ";
 }
 cout << endl;

 cout << "list2: ";
 for (auto& num : list2) {
  cout << num << " ";
 }
 cout << endl;

 cout << "---------------------" << endl;

 list<int> list3 = { 1, 2, 3, 4, 5 };
 list<int> list4 = { 11, 12, 13, 14, 15 };
 list<int>::iterator iter1 = list4.begin();
 iter1++;

 //把list4中iter1位置处的值剪贴到list3的list3.begin()位置
 list3.splice(list3.begin(), list4, iter1);
 cout << "list3: ";
 for (auto& num : list3) {
  cout << num << " ";
 }
 cout << endl;
 cout << "list4: ";
 for (auto& num : list4) {
  cout << num << " ";
 }
 cout << endl;

 cout << "-----------------------" << endl;

 list<int> list5 = { 1, 2, 3, 4, 5 };
 list<int> list6 = { 11, 12, 13, 14, 15 };
 list<int>::iterator iter2 = list6.begin();
 list<int>::iterator iter3 = iter2;
 iter3++; iter3++; iter3++;

 //把list6中从iter2到iter3的元素剪贴到list5中的list5.begin()处
 list5.splice(list5.begin(), list6, iter2, iter3);
 cout << "list5: ";
 for (auto& num : list5) {
  cout << num << " ";
 }
 cout << endl;
 cout << "list6: ";
 for (auto& num : list6) {
  cout << num << " ";
 }
 cout << endl;

 return 0;
}

list1: 1 11 12 13 14 15 2 3 4 5 list2: list3: 12 1 2 3 4 5 list4: 11 13 14 15 list5: 11 12 13 1 2 3 4 5 list6: 14 15

条款5:尽量使用区间成员函数代替它们的单元素兄弟

//给定两个vector, v1和v2,使v1的内容和v2的后半部分一样的最简单形式是什么?

//方式一:区间成员函数

代码语言:javascript
复制
v1.assign(v2.begin() + v2.size()/2, v2.end());
//assign 对所有标准序列容器(vector, string, deque和list)都有效,完全替代一个容器的内容

//为什么区间成员函数优先于它们的单元素的替代品

//区间成员函数是一个像 STL 算法的成员函数,使用两个迭代器参数来指定元素的一个区间来进行某个操作

//方式二:手写显式循环

代码语言:javascript
复制
vector<Widget> v1,v2;
v1.clear();
for (vector<Widget>::const_iterator ci = v2.begin() + v2.size() /2; ci != v2.end(); ++ci)
    v1.push_back(*ci);

//方式三:使用一个算法

代码语言:javascript
复制
v1.clear();
copy(v2.begin() + v2.size()/2, v2.end(), back_inserter(v1));
//copy中也存在一个循环

//方式四:copy的调用可以用一个insert的区间版本替代

代码语言:javascript
复制
v1.insert(v1.end(), v2.begin() + v2.size() /2, v2.end());

//我们要知道:几乎所有目标区间被插入迭代器指定的copy的使用都可以用调用的区间成员函数来代替

//区间成员函数的优点

//1,可以输入更少的代码 //2,导致代码更清晰更直截了当 //3,效率 避免更多内存分配,频繁拷贝对象

//实例2:假设你要把一个 int 数组拷贝到 vector前端

代码语言:javascript
复制
int data[numValues];
vector<int> v;
v.insert(v.begin(), data, data + numValues);//把data中的int插入v的前部

//显示循环
vector<int>::iterator insertLoc(v.begin());
for(inr i = 0; i < numValues; ++i)
{
    insertLoc = v.insert(insertLoc, data[i]);//每次调用insert会使insertLoc无效
    ++insertLoc;
}

//copy:  插入迭代器 inserter,back_inserter或front_inserter
copy(data, data + numValues, inserter(v, v.begin()));

//区间成员函数提供以下几个接口

//参数类型iterator的意思使容器的迭代器类型,也就是 container::iterator

//参数类型InputIterator的意思是可以接受任何输入迭代器

代码语言:javascript
复制
//1, 区间构造:所有标准容器都提供这种形式的构造函数
container::container(InputIterator begin, InputIterator end);

//2, 区间插入: 所有标准序列容器都提供这种形式的insert
void container::insert(iterator position,//区间插入位置
                InputIterator begin, //插入区间的起点
                InputIterator end);//插入区间的重点

//关联容器使用它们的比较函数来决定元素要放在哪里,所以省略了 position 参数
void container::insert(InputIterator begin, InputIterator end);
//睁大眼睛:当你看到一个循环用 push_front,push_back,insert 一算法 copy的参数是front_inserter或者back_inserter
//你就可以用一个insert的区间函数代替了

//3,区间删除: 每个标准容器都提供一个区间形式的erase,但是序列和关联容器的返回类型不同
//序列容器
iteraor container::erase(iterator begin, iterator end);
//关联容器
void container:erase(iterator begin, iterator end);

//4,区间赋值:所有标准序列容器都提供了区间形式的assign
void container::assign(InputIterator begin, InputIterator end);

条款6:警惕C++最令人恼怒的解析

代码语言:javascript
复制
/基本说起
//声明一个函数f带有一个double而且返回一个int
int f(double d);
//同上,d的参数左右的括号是多余的,被忽略
int f(double (d)); //有点新鲜
//同上,省略了参数名字
int f(double);

//带函数的说起
//声明一个函数g, 带有一个参数, 那个参数指向一个没有参数, 返回double的函数的指针
int g(double (*pf)());
//同上,pf使用非指针语法
int g(double pf());
//同上, 参数名可以忽略,去掉了pf这个名字
int g(double ());

//注意:参数名左右的括号可以被忽略,但是单独的括号指出存在一个参数列表:声明了存在指向函数的指针的参数
代码语言:javascript
复制
class Widget{

};

Widget w();//他在干什么?
//并没有声明一个 w 的Widget构造函数,而是声明了一个叫做w的没有参数且返回 Widget的函数
代码语言:javascript
复制
std::istringstream str("1 3 5 7 8 9 10");
std::list<int> data(std::istream_iterator<int>(dataFile), std::istream_iterator<int>())
{
   //原本想法是传一对 istream_iterator的list的区间构造函数,把int从文件拷贝到 list中

   //可以编译,但是运行时,什么都不做,因为这并不是声明一个list函数,其实做的是 ???
   std::list<int> result;
   //方式一
   result.assign(dataFile, std::istream_iterator<int>());
   
   //方式二
   //copy(dataFile, std::istream_iterator<int>(), inserter(result, result.begin()));  

   for(auto i: result)
   {
        std::cout<<"i: "<<i<<std::endl;
   }

   return result;
}

int main()
{   
    std::istream_iterator<int> (*eos)();
    data(str,eos);//OK的
}

//std::istream_iterator - cppreference.com

代码语言:javascript
复制
//因此在写一遍
std::list<int> data((std::istream_iterator<int>(dataFile)), std::istream_iterator<int>()); 
/**
 * @brief 
 * 声明一个函数data, 返回类型是 list<int>
 * 1, 第一个参数是 dataFile,类型是 std::istream_iterator<int>, dataFile左右的括号是多余的并且被忽略
 * 2, 第二个参数名字名字,他的类型是 指向一个没有参数而且返回 std::istream_iterator<int>的函数的指针
 * 
 * 几乎任何东西都可以分析成函数声明
 * 
 * @return int 
 */

条款7:当使用new得指针得容器时,记得在销毁容器前delete那些指针

/** * @brief * * 条款7: new得容器指针,在销毁时候记得 delete * * STL可以做得事情 * 1,提供了前向和逆向遍历得迭代器 begin,end, rbegin * 2,告诉你所容纳得对象类型 value_type得tepedef,见情况1 和 2 * 3,在插入和删除时,它们负责任何需要得内存管理 * 4,报告了容纳了多少对象和最多可能容纳得数量 size和max_size * 5, 容器自己被销毁时会自动销毁容纳得每个对象 * * 但是真的不用清楚工作了吗? * * 但是,当容器容纳得是通过 new 分配得对象得指针时,一个指针得容器被销毁,会销毁它包含得每个元素 * ,但指针得 析构函数 是无 操作得,不可不会调用delete ; 看情况3 * */

//情况1

//https://www.cnblogs.com/huty/p/8517000.html

//https://vimsky.com/examples/detail/cpp-ex---value_type---class.html

//所有容器都定义了一个 value_type

代码语言:javascript
复制
class C{  
public:  
    C(int x){  
        std::cout << x << std::endl;  
    }  
    C(){  
        std::cout << 10 << std::endl;  
    }  
  
};

//情况2: 两个包装容器,对任意类型得值进行了一层包装

代码语言:javascript
复制
//情况2: 两个包装容器,对任意类型得值进行了一层包装
template <typename T> class Bag //包装容器袋子
{
public:
    typedef T value_type;
    T val; //内容
    Bag(T value):val(value){};

};
template <typename T> class Box //包装容器箱子
{
public:
    typedef T value_type;
    T val; //内容
    Box(T value):val(value){};
};
//实现一个函数,从两个容器中取出里面得值,并作为返回值
template <typename C> typename C::value_type unpack(C container)
{
    return container.val;
}

//情况3: 内存泄露

代码语言:javascript
复制
//情况3: 内存泄露
void doSomething()
{
    std::vector<Widget*> vwp;
    for(int i=0; i <10; ++i)
    {
        vwp.push_back(new Widget);//Widget在这里泄露
    }
    //当vwp出了生存阈后,vwp得每个元素都被销毁,但是并不改变从没有把 delete 作用于 new得到得对象这件事情
    //delete是你自己负责得事情,而不是 vector,这是一个特性,于是需要自己定义 delete

    for(std::vector<Widget*>::iterator i = vwp.begin(); i != vwp.end();++i)
    {
        delete *i;
    }
    //但是以上代码也不是异常安全得,如果在指针填充了vwp和你要删除它们之间抛出了一个异常,也会再次资源泄露

    //如何解决以上两个问题呢?
    //1,内存泄露
    //2,异常安全  
}

//情况3-1: 把 delete 转入一个函数对象,STL这样玩

代码语言:javascript
复制
//情况3-1: 把 delete 转入一个函数对象,STL这样玩
template<typename T>
struct DeleteObject: public std::unary_function<const T*,void>//这样一个继承
{
    void operator()(const T* ptr) const{
        std::cout<<"de: "<<std::endl;
        delete ptr;
    }
};

//https://runebook.dev/zh-CN/docs/cpp/utility/functional/unary_function

//std::unary_function 是用于使用一个参数创建函数对象得基类,自身没有定义 operator() C++11是已经被弃用 C++17中被删除

代码语言:javascript
复制
struct less_than_7 : std::unary_function<int, bool>
{
    bool operator()(int i) const { return i < 7; }
};

//有了下面得情况 3-1 你可以这样

代码语言:javascript
复制
 //有了下面得情况 3-1 你可以这样
void DE()
{   
    std::vector<Widget*> vwp;
    std::for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>());
    //这里指定了要删除得对象是Widget* 指针,是冗余得,并且会导致很难追踪到得bug 
}

 //string 缺少析构函数,没有析构函数得类不能公有继承
 //如果你这样写
 class SpString:public std::string{

 };
 //然后你再这样
 void DESP()
 {
    std::deque<SpString*> dssp;
    for_each(dssp.begin(), dssp.end(), DeleteObject<std::string>());
    //行为没有定义,通过没有析构函数得基类指针来删除派生类对象
    //dssp被声明为容纳Spstring*指针,但 for_each循环告诉作者DeleteObject将删除 string*指针,没析构怎么删除
 }

//如何改进以上情况呢?

//通过编译器推断传给 DeleteObject::operator()得指针得类型来消除这个错误

//把模板化从 DeleteObject移到它得 operator()

代码语言:javascript
复制
 struct DeleteObject2
 {
    template<typename T>
    void operator()(const T* ptr) const
    {   
        std::cout<<"DeleteObject2: "<<std::endl;
        delete ptr;
    }
 };
 //这样是一个良好得设计
 void DEE()
 {
    std::deque<SpString*> dssp;
    for_each(dssp.begin(), dssp.end(), DeleteObject2());
    //但是如果再 SPString被 new但在调用 for_each之前抛出一个异常,也会发生泄露
    //智能指针来了
 }

//说了这么多,就是为了引入智能指针

代码语言:javascript
复制
 void DEPTR()
 {
    typedef boost::shared_ptr<Widget> SPW;
    std::vector<SPW> vwp;
    for(int i=0; i <10; ++i)
    {
        vwp.push_back(SPW(new Widget));//Widget在这里泄露
    }
 }

//https://blog.csdn.net/yusiguyuan/article/details/20076061

//boost::shared_ptr 这里讲解的特别详细和注意事项,超值推荐

//因此总结如下

//当你要删除指针得容器时要避免内存泄露,必须用智能引用指针对象代替指针,或者在容器销毁前你手动删除容器中的每个指针

测试代码

代码语言:javascript
复制
int main()
{   
    //条款6
    std::istream_iterator<int> (*eos)();
    data(str,eos);

    //条款7
    //情况1
    std::vector<C> vec;  
    C c1,c2(11);  
    vec.push_back(c1);  
    vec.push_back(c2);  
    std::vector<C>::value_type n1;  
    std::vector<C>::value_type n2(13);  
    vec.push_back(n1);  
    vec.push_back(n2);  
    std::cout << vec.size() << std::endl; 

    //情况2
    Box<int> box(5);
    auto a = unpack(box) ;
    std::cout<<"情况2: "<<a<<std::endl;

    //情况3-1
      std::vector<int> v;
    for (int i = 0; i < 10; ++i) v.push_back(i);
 
    std::cout << std::count_if(v.begin(), v.end(), std::not1(less_than_7()));
 
    //C++11 solution:
    // Cast to std::function<bool (int)> somehow - even with a lambda
    std::cout << std::count_if(v.begin(), v.end(),
        std::not1(std::function<bool (int)>([](int i){ return i < 7; }))
    )<<std::endl;

    DE();

}

i: 1 i: 3 i: 5 i: 7 i: 8 i: 9 i: 10 10 11 10 13 4 情况2: 5 33

条款8:永不建立auto_ptr的容器

/** * @brief * * 条款8: 永不建立 auto_ptr的容器 * * //因为 auto_ptr * //当你拷贝一个 auto_ptr时,auto_ptr所指向对象的所有权被转移到拷贝的auto_ptr,而被拷贝的auto_ptr被设为NULL * //也就是说,拷贝一个 auto_ptr将改变它的值 */

代码语言:javascript
复制
 std::auto_ptr<Widget> pw1(new Widget);//pw1指向一个Widget
 std::auto_ptr<Widget> pw2(pw1);//pw2指向pw1的Widget,pw1被设为NULL(Widget的所有权从 pw1转移到pw2)
 pw1 = pw2; //pw1现在再次指向Widget,pw2被设为NULL

//这会带来什么坏处呢?在代码中会带来哪些隐患呢?

//比如如下:建立一个 auto_ptr的vector,然后使用一个比较指向的Widget的值的函数对它进行排序

代码语言:javascript
复制
 bool widgetAPCompare(const auto_ptr<Widget>& lhs, const auto_ptr<Widget>& rhs)
 {
    return *lhs < *rhs;//假设Widget重载了 operator<
 }
 std::vector<std::auto_ptr<Widget> > widgets;
 sort(widgets.begin(), widgets.end(), widgetAPCompare);//排序这个vector
 //看似合理,但结果却是错的:因为在排序过程中widgets中的一个或多个auto_ptr可能已经被设为NULL

//为什么会这样?

//是 sort算法使用了快速排序的思想:选择容器的某个元素作为主元,然后对大于或小于或等于主元的值进行递归排序

//sort内部大概是这样

代码语言:javascript
复制
 template<class RandomAccessIterator, class Compare>
 void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp)
 {
    typedef typename iterator_traits<RandomAccessIterator>::value_type ElementType;
    //涉及iterator_traits<RandomAccessIterator>::value_type时,必须在它前面写上typename,因为它是一个依赖于模板参数类型的名字,在这里是RandomAccessIterator
    RandomAccessIterator i;//让i指向主元
    ElementType pivotValue(*i);//把主元拷贝到一个局部临时变量,做剩下的排序工作,就是拷贝的问题
    /**
     * @brief 
     * 在这里,auto_ptr<Widget> 这个拷贝默默地把被拷贝的auto_ptr设为NULL, 另外当 pivotValue出了生存期,它会自动删除指向的Widget。
     * 这是 sort 调用返回,vecotor的内容以及改变,而且至少一个 Widget 已经被删除了
     */
 }

条款9:在删除选项中仔细选择

//标准STL容器c, 容纳int, 你想把c中所有值为 1963的对象去掉 //不同容器类型表现为不同的方法:没有一种方法是通用的 //Containerc;

代码语言:javascript
复制
//1,连续内存容器 vector, deque, string 
//最好的方法是 erase-remove
std::vector<int> c={100,192,1963,12,1963,11,23,12};
c.erase(std::remove(c.begin(), c.end(), 1963), c.end());

//erase: https://blog.csdn.net/weixin_41969587/article/details/82587372

/** * @brief * (1)erase(pos,n); 删除从pos开始的n个字符,比如erase(0,1)就是删除第一个字符 (2)erase(position);删除position处的一个字符(position是个string类型的迭代器) (3)erase(first,last);删除从first到last之间的字符(first和last都是迭代器) * @return int */

//std::remove: https://www.jianshu.com/p/097ad523b604

/** * @brief * 返回一个迭代器,这个迭代器指向最后一个不被 remove的元素的下一个值,也就是指向第一个无效值地方 * * remove修改是方式使得 begin到返回值之间的值是删除特定值之后剩下的,且不改变原来的顺序,其余的部分保持原样 * * @return int */

代码语言:javascript
复制
//2, list
//list的成员函数 remove更高效
std::list<int> cc = {100,192,1963,12,1963,11,23,12};
cc.remove(1963);
代码语言:javascript
复制
//3, 标准关联容器 set multiset map 或 multimap
//没有 remove 的成员函数,使用 erase
std::set<int> ccc = {100,192,1963,12,1963,11,23,12};
ccc.erase(1963);

//set: http://c.biancheng.net/view/7192.html

//https://blog.csdn.net/fengbingchun/article/details/63268962

/** * @brief * set存储的也是键值对,只不过要求 key和value的值必须相等,第二种情况,第一种用map * {<'a', 1>, <'b', 2>, <'c', 3>} {<'a', 'a'>, <'b', 'b'>, <'c', 'c'>} //并且,不能重复,已经默认排序,无法修改 std::multisetmymultiset{1,2,,3,4,5}; * * @return int */

//multiset: http://c.biancheng.net/view/7203.html

/** * @brief * 几乎和set一样,唯一不同的是可以存储多个值相同的元素,而set只能存储互不相同的元素 * std::multisetmymultiset{1,2,2,2,2,3,4,5}; * * @return int */

//map: http://c.biancheng.net/view/7173.html

//multimap: http://c.biancheng.net/view/7190.html

// std::multimap<char, int>mymultimap{ {'a',10},{'b',20},{'b',15}, {'c',30} };

代码语言:javascript
复制
//4, 将 去除特定值改成 消除下面判断返回真的每个对象
bool badValue(int x)
{
    return x < 6;
}
代码语言:javascript
复制
//4-1: 序列容器 vector, string deque list
//只要把 remove替换为 remove_if
std::deque<int> ca = {2,3,4,1,2,6,7,8,4,3,7,5,6};
ca.erase(std::remove_if(ca.begin(), ca.end(),badValue), ca.end());//去掉badValue返回真的最佳方法
代码语言:javascript
复制
//4-2: list
std::list<int> caa = {2,3,4,1,2,6,7,8,4,3,7,5,6};
caa.remove_if(badValue);

//std::remove_if: https://blog.csdn.net/KFLING/article/details/80187847

//第三个参数是回调函数,如果回调函数返回为真,则将当前所指向的参数移到尾部,返回值是被移动区域的首个元素

代码语言:javascript
复制
//4-3: 标准关联容器
//第一种方法:容易编码但是效率低:用 remove_copy_if把我们需要的值拷贝到一个新容器,然后把原容器中的内容和新的交换
std::set<int> caaa = {2,3,4,1,2,6,7,8,4,3,7,5,6};
std::set<int> goodValues;//用于容纳不删除值得临时容器
//从 caaa拷贝不删除得值到 goodValues
std::remove_copy_if(caaa.begin(), caaa.end(), inserter(goodValues,goodValues.end()), badValue);
// //交换caaa和goodValues
caaa.swap(goodValues);
//缺点是它拷贝了所有不删除得元素,开销较大

//std::remove_copy_if: https://en.cppreference.com/w/cpp/algorithm/ranges/remove_copy

//https://blog.csdn.net/phd17621680432/article/details/122349019

代码语言:javascript
复制
//第二种方法:直接从原容器删除元素,但是关联容器没有提供类似 remove_if得成员函数,所有必须自己写一个循环来迭代c中得元素
//很快,你写出了如下代码
std::set<int> caaaa = {2,3,4,1,2,6,7,8,4,3,7,5,6};
for(std::set<int>::iterator i = caaaa.begin(); i != caaaa.end(); ++i)
{
    if(badValue(*i))
    {
        caaa.erase(i);//删除c中badValue返回真得每个元素得代码
    }
    //这里有未定义得行为:当容器得一个元素被删除时,指向那个元素得所有迭代器都失效了
    //当 caaa.erase(i) 返回时, i 已经失效
    //在erase返回后,i通过for循环得 ++i 部分自增,是个坏消息!!!
}
代码语言:javascript
复制
//如果避免这种问题:必须保证在调用 erase之前就得到了c中下一个元素得迭代器,因此在 i 上使用后置递增
for(std::set<int>::iterator i = caaaa.begin(); i != caaaa.end(); )//第三部分是空得,i在下面自增
{
    if(badValue(*i))
    {
        caaaa.erase(i++);
        //对于坏得值,把当前得i 传给erase,然后作为副作用增加 i
        //对于好得值,只增加i
        //i++得值是 i得旧值,旧值(没增加得)传给erase, 但在erase开始执行前i已经自增了,正好是我们想要得
    }
    else
    {
        ++i;
    }
}

//5: 进一步改进该问题

//不仅删除badValue返回真得每个元素,而且每当一个元素被删掉时,我们也想把一条消息写道日志文件中

代码语言:javascript
复制
//5-1 对于关联容器,很容易,加一句话就行
std::ofstream logFile;
set<int> a = {1,2,3,3,4,7,8,9,10};
std::string fn_out = "set.txt";
logFile.open(fn_out.c_str(),std::ios::out);

for(std::set<int>::iterator i = a.begin(); i != a.end(); )//第三部分是空得,i在下面自增
{
    if(badValue(*i))
    {   
        //写日志文件
        logFile << "Erasing "<<*i<<'\n';

        a.erase(i++);
        //对于坏得值,把当前得i 传给erase,然后作为副作用增加 i
        //对于好得值,只增加i
        //i++得值是 i得旧值,旧值(没增加得)传给erase, 但在erase开始执行前i已经自增了,正好是我们想要得
    }
    else
    {
        ++i;
    }
}
代码语言:javascript
复制
//5-2 对于 vector string deque带来麻烦, 因为没有办法让 erase-remove写日志文件
//必须利用 erase 得返回值,一旦删除完成,它就是指向紧接在被删除之后得元素得有效迭代器
std::vector<int> aa = {1,1,1,1,1,7,8,9,10};

for(std::set<int>::iterator i = aa.begin(); i != aa.end(); )//第三部分是空得,i在下面自增
{
    if(badValue(*i))
    {   
        //写日志文件
        logFile << "Erasing "<<*i<<'\n';
        i = aa.erase(i);//通过把 erase得返回值赋给 i来保持 i有效
    }
    else
    {
        ++i;
    }
}
代码语言:javascript
复制
int main()
{
    //1,
    std::vector<int>::iterator iter_return;
    iter_return = std::remove(c.begin(), c.end(), 1963);
    std::cout << "vals after iter_return: " << std::endl;

    for(std::vector<int>::iterator iter =iter_return; iter!=c.end(); iter++)
    {
       std::cout << (*iter) << "\t";//23,12
    }
    std::cout<<"\n";
    std::cout << "after remove()" << std::endl;

    for(std::vector<int>::iterator iter=c.begin(); iter != c.end(); iter++) 
    {
       std::cout << *iter << "\t";//100,192,12,11,23,12,23,12
    }
    std::cout<<"\n";
    c.erase(std::remove(c.begin(), c.end(), 1963), c.end());
    for(auto i:c)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //2
    cc.remove(1963);
    for(auto i:cc)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //3
    ccc.erase(1963);
    for(auto i:ccc)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //4-1
    std::deque<int>::iterator iter_return_;
    iter_return_ = std::remove_if(ca.begin(), ca.end(), badValue);
    std::cout << "vals after iter_return: " << std::endl;

    for(std::deque<int>::iterator iter =iter_return_; iter!=ca.end(); iter++)
    {
       std::cout << (*iter) << "\t";
    }
    std::cout<<"\n";
    std::cout << "after remove()" << std::endl;

    for(std::deque<int>::iterator iter=ca.begin(); iter != ca.end(); iter++) 
    {
       std::cout << *iter << "\t";
    }
    std::cout<<"\n";

    ca.erase(std::remove_if(ca.begin(), ca.end(),badValue), ca.end());
    for(auto i:ca)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //4-2
    caa.remove_if(badValue);
    for(auto i:caa)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //4-3-1
    std::remove_copy_if(caaa.begin(), caaa.end(), inserter(goodValues,goodValues.end()), badValue);
    //交换caaa和goodValues
    caaa.swap(goodValues);
    for(auto i:caaa)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //4-3-2
    // for(std::set<int>::iterator i = caaaa.begin(); i != caaaa.end(); ++i)
    // {
    //     if(badValue(*i))
    //     {
    //         caaaa.erase(i);//Segmentation fault
    //     }
    // }
    // for(auto i:caaaa)
    // {
    //     std::cout<<i<<"\t";
    // }
    std::cout<<"\n";

    //4-3-2----
    for(std::set<int>::iterator i = caaaa.begin(); i != caaaa.end(); )
    {
        if(badValue(*i))
        {
            caaaa.erase(i++);
        }
        else
        {
            ++i;
        }
    }
    for(auto i:caaaa)
    {
        std::cout<<i<<"\t";
    }
    std::cout<<"\n";

    //5-1
    logFile.open(fn_out.c_str(),std::ios::out);

    for(std::set<int>::iterator i = a.begin(); i != a.end(); )//第三部分是空得,i在下面自增
    {
        if(badValue(*i))
        {   
            //写日志文件
            logFile << "Erasing "<<*i<<'\n';

            a.erase(i++);
            //对于坏得值,把当前得i 传给erase,然后作为副作用增加 i
            //对于好得值,只增加i
            //i++得值是 i得旧值,旧值(没增加得)传给erase, 但在erase开始执行前i已经自增了,正好是我们想要得
        }
        else
        {
            ++i;
        }
    }

    //5-2
    for(std::vector<int>::iterator i = aa.begin(); i != aa.end(); )//第三部分是空得,i在下面自增
    {
        if(badValue(*i))
        {   
            //写日志文件
            logFile << "Erasing 5- 2 "<<*i<<'\n';
            i = aa.erase(i);//通过把 erase得返回值赋给 i来保持 i有效
        }
        else
        {
            ++i;
        }
    }


}

/** * @brief * * 总结如下: * * 1,去除一个容器中有特定值得所有对象 * 1.1 如果容器是 vector string 或 deque, 使用 erase-remove * 1.2 如果容器是 list 使用 list::remove * 1.3 如果容器是标准关联容器,使用它得 erase成员函数 * * 2, 去除一个容器中满足一个特定判定式得所有对象 * 2.1 如果是 vector string deque 使用 erase-remove_if * 2.2 如果是list 使用 list::remove_if * 2.3 如果容器是标准关联容器 使用 remove_copy_if 和 swap 或写一个循环来遍历容器得元素,当你把迭代器传给erase时记得后置递增它 * * 3,在循环内做某些事情(除了删除对象之外) * 3.1 如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它得返回值更新你得迭起器 * 3.2 如果是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增 * */

条款10:注意分配器得协定和约束

什么是STL内存分配器?

//https://blog.csdn.net/tang123246235/article/details/121775914

/*** * STL中的内存分配器 allocator * * 定义:c++标准库的一个组件,用来处理所有给定容器(vector ,list,map等)内存的分配和释放 * 默认使用的通用分配器是 std::allocator,开发者还可以自定义分配器 * * 同时也提供了以下分配器 * __pool_alloc: SGI内存池分配器 * __mt_alloc: 多线程内存池分配器 * array_allocator:全局内存分配,只分配不释放,交给系统来释放 * malloc_allocator:堆std::malloc和std::free进行的封装 * * 为什么会有 allocator? * * operator new() / operator delete() : 分配内存和执行构造函数是一起做的,对象析构和内存释放一起做的 * allocator: 将内存分配和对象构造分离,先分配大块内存,只在需要的时候才真正执行对象的构造函数 * 因此,可以提供更好的性能和更灵魂的内存管理能力,在定义一个 allocator对象时,必须指明 allocator可以分配的对象类型 * 当 allocator分配内存时,会根据给定的对象类型来确定恰当的内存大小和对奇位置 * * /

//https://blog.csdn.net/JMW1407/article/details/106868553

代码语言:javascript
复制
auto p  = new string[100]
for (int i=0; i<5; ++i)
{
    p[i] = "babba...";

   // 只需要5个string,而new把100个对象全部构造好了(每个string已经被初始化为空字符串,也就是"")
   // 也就是前面将p[0-4]赋值为空字符串的操作,变得毫无意义
}

自定义内存分配器

代码语言:javascript
复制
template<class T >
inline T*_allocate(ptrdiff_t num,T*){
    std::cout<<"_allocate"<<endl;
    return static_cast<T*>(::operator new(num*sizeof(T)));
}
template <class T>
inline void _deallocate(T* buff){
    std::cout<<"_deallocate"<<endl;
    ::operator delete(buff);
}

template <class T ,class U>
inline void _construct(T* p,const U& value){
    new(p)T(value);
}
template<class T>
inline void _destory(T* p){
    p->~T();
}
template<typename T>
class MyAlloctor{
    public:
        typedef T        value_type;
        typedef T*       pointer;
        typedef T&       reference;
        typedef const T* const_pointer;
        typedef const T& const_reference;
        typedef size_t     size_type;
        typedef ptrdiff_t  difference_type;
        template<typename _Tp1>struct rebind{ typedef MyAlloctor<_Tp1> other; };

    MyAlloctor()=default;
    ~MyAlloctor()=default;
    pointer address(reference x){
        return static_cast<pointer>(&x);
    }
    const_pointer address(const_reference x){
        return static_cast<const_pointer>(&x);
    }

    pointer allocate(size_type _n,const void* hit=0 ){
        return _allocate(_n,(pointer)0);
    }

    void deallocate(pointer p,size_type _n){
        _deallocate(p);
    }

    size_type max_size()const throw(){
        return static_cast<size_type>(INT_MAX/sizeof(T));
    }

    void construct(pointer p,const_reference value){
        _construct(p,value);
    }

    void destroy(pointer p){
        _destory(p);
    }

};

如何使用呢?

如何使用? 1,allocator与类绑定,因为allocator是一个泛型类 2,allocate()申请指定的空间,只分配空间,不构造对象,返回第一个元素的起始地址 3,construct()构造对象,其参数是可变参数,可以选择匹配的构造函数 4,使用,与其他指针使用无异 5,destory()析构对象,此时空间还是可以使用的,不会释放空间 6, deallocate()回收空间,释放先前allocate分配的且没有释放的存储空间

实例:

代码语言:javascript
复制
//https://www.cnblogs.com/SimonKly/p/7819122.html
//#include "CAnimal.h"
#include <memory>
#include <iostream>

using namespace std;

class Animal
{
public:
#if 1        //即使为0,没有默认构造也是可以,
    Animal() : num(0)
    {
        cout << "Animal constructor default" << endl;
    }
#endif
    Animal(int _num) : num(_num)
    {
        cout << "Animal constructor param" << endl;
    }

    ~Animal()
    {
        cout << "Animal destructor" << endl;
    }

    void show()
    {
        cout << this->num << endl;
    }

private:
    int num;
};

int main()
{
    allocator<Animal> alloc;        //1.
    Animal *a = alloc.allocate(5);    //2.

    //3.
    alloc.construct(a, 1);
    alloc.construct(a + 1);
    alloc.construct(a + 2, 3);
    alloc.construct(a + 3);
    alloc.construct(a + 4, 5);

    //4.
    a->show();
    (a + 1)->show();
    (a + 2)->show();
    (a + 3)->show();
    (a + 4)->show();

    //5.
    for (int i = 0; i < 5; i++)
    {
        alloc.destroy(a + i);
    }
    //对象销毁之后还可以继续构建,因为构建和内存的分配是分离的
    //6.
    alloc.deallocate(a, 5);

    cin.get();
    return 0;
}

// 因此,我们可以看到,分配器是对象,有成员功能,内嵌类型和 typedef等等

// 但标准允许 STL实现认为所有相同类型的分配器对象都是等价的而且比较起来总是相等

代码语言:javascript
复制
class Widget{

};

//一个用户定义的分配器
template<class T >
inline T*_allocate(ptrdiff_t num,T*){
    std::cout<<"_allocate"<<endl;
    return static_cast<T*>(::operator new(num*sizeof(T)));
}
template <class T>
inline void _deallocate(T* buff){
    std::cout<<"_deallocate"<<endl;
    ::operator delete(buff);
}

template <class T ,class U>
inline void _construct(T* p,const U& value){
    new(p)T(value);
}
template<class T>
inline void _destory(T* p){
    p->~T();
}
template<typename T>
class SpAllocator{
    public:
        typedef T        value_type;
        typedef T*       pointer;
        typedef T&       reference;
        typedef const T* const_pointer;
        typedef const T& const_reference;
        typedef size_t     size_type;
        typedef ptrdiff_t  difference_type;
        template<typename _Tp1>struct rebind{ typedef SpAllocator<_Tp1> other; };

    SpAllocator()=default;
    ~SpAllocator()=default;
    pointer address(reference x){
        return static_cast<pointer>(&x);
    }
    const_pointer address(const_reference x){
        return static_cast<const_pointer>(&x);
    }

    pointer allocate(size_type _n,const void* hit=0 ){
        return _allocate(_n,(pointer)0);
    }

    void deallocate(pointer p,size_type _n){
        _deallocate(p);
    }

    size_type max_size()const throw(){
        return static_cast<size_type>(INT_MAX/sizeof(T));
    }

    void construct(pointer p,const_reference value){
        _construct(p,value);
    }

    void destroy(pointer p){
        _destory(p);
    }

};
代码语言:javascript
复制
std::list<Widget,SAW> L1;
std::list<Widget,SAW> L2;
L1.splice(L1.begin(),L2);//把L2的节点移到L1前端

//这里list2结合到list1中,并没有拷贝什么,只是调整了一些指针
//当L1被销毁时,L1的分配器必须回收最初由L2的分配器分配的节点
//因此,只有相同的分配器,一个分配器对象分配的内存才可以安全地被另一个分配器对象回收

//比较一下 operator new和 allocator地声明

代码语言:javascript
复制
void* operator new(size_t bytes);//指定分配内存的字节数 例如 sizeof(int) == 4, 容纳一个int内存,传 4
pointer allocator<T>::allocate(size_type numObjectos);//pointer总是 T* 的typedef, 指定是内存里能容纳多少个T 对象 传1 

//2: 实现 list

//list本身由节点组成,每个节点容纳一个T对象和到list中后一个和前一个节点的指针

代码语言:javascript
复制
//list本身由节点组成,每个节点容纳一个T对象和到list中后一个和前一个节点的指针
template<typename T, typename Allocator = allocator<T>>
class list{
    private:
        Allocator alloc;//用于T类型对象的分配器

        struct ListNode
        {
            T data;
            ListNode *prev;
            ListNode *next;
        };
        //你能发现什么问题?
        /**
         * @brief 
         * 
         * 当添加一个新节点到 list时,需要从分配器为他获取内存,我们要的不是 T 的内存,要的是包含了一个 T的 ListNode
         * 的内存,那使 我们的 Allocator对象没有用了,因为它不为 ListNode分配内存,他为 T分配内存
         * 因此,list从未让它的 Allocator做任何分配了,分配器不能提供 list需要的
         * 
         * list需要的是什么呢?
         * 
         * 是从它的分配器类型那里获得用于 ListNode的对于分配器的方法,怎么去实现呢?看下面代码
         * 
         */
};

//标准分配器像这样声明
template<typename T>
class allocator{
    public:
        template<typename U>
        struct rebind
        {
            typedef allocator<U> other;
        };    
};

//因此,在 list<T>的实现代码里,需要确定我们持有的 T的分配器所对应的 ListNode的分配器类型,T的分配器类型是模板参数ALlocator
//ListNodes的对应的分配器类型是:
Allocator::rebind<ListNode>::other

/**
 * @brief 
 * 如何解释以上定义?
 * 
 *  每个分配器模板A(例如,std::allocator,SpecialAllocator,等)都被认为有一个叫做rebind的
    内嵌结构体模板。rebind带有一个类型参数,U,并且只定义一个typedef,other。 other是A<U>的一个简单名
    字。结果,list<T>可以通过Allocator::rebind<ListNode>::other从它用于T对象的分配器(叫做Allocator)获取对
    应的ListNode对象分配器。
 * 
 * 
 */

条款11:理解自定义分配器的正确用法

/** * @brief * 如果你认为: * * 1, 默认的STL内存管理器 (allocator)在你的STL需求中太慢,浪费内存或造成过度的碎片 * 2, 你发现 allocator对线程安全采取了措施,但是你只对单线程的程序感兴趣,不想花费不需要的同步开销 * 3, 你知道在某些容器里的对象通常一同被使用,所以你想在一个特别的堆里把它们放的很近使引用的区域性最大化 * 4, 你想建立一个相当共享内存的唯一的堆,然后把一个或多个容器放在那个内存里,因为这样它们可以被其他进程共享 * */

代码语言:javascript
复制
//起初,你设计了管理共享内存的堆
template<class T>
inline T* mallocShared(size_t bytesNeeded,T*){
    std::set_new_handler(0);
    T *tmp = (T*)(::operator new((size_t)(bytesNeeded * sizeof(T))));
    if (tmp == 0)
    {
        std::cout<<" out of memory"<<std::endl;
        exit(1);
    }
 std::cout<<"mallocShared "<<std::endl;
    return tmp;
 
}

template<class T>
inline void freeShared(T *ptr){
  std::cout<<"freeShared "<<std::endl;
  ::operator delete(ptr);
}

//https://www.jianshu.com/p/1e8aa13e9ced

//并且,你希望把STL容器的内容放在共享内存中

代码语言:javascript
复制
template<typename T>
class SharedMemoryANocator{
    public:
        typedef T*          pointer;
        typedef size_t      size_type;

        typedef T           value_type;
        // 重新绑定分配器(rebind allocator of type U) //不能少自定义分配器中
        template <class U>
        struct rebind
        {
            typedef SharedMemoryANocator<U> other;
        };
            


        pointer allocate(size_type numObjecrs, const void *localityHint=0)
        {
            return static_cast<pointer>(mallocShared(numObjecrs * sizeof(T), (pointer)0));
        }

        void deallocate(pointer ptrToMemory, size_type numObjects)
        {
            freeShared(ptrToMemory);
        }
};
代码语言:javascript
复制
int main()
{
    typedef std::vector<double, SharedMemoryANocator<double> > SharedDoubleVec;

    {
        //开始一个块
        
        //建立一个元素在共享内存中的 vector
        SharedDoubleVec v = {1,2,3};

        //v分离来容纳它元素的内存将来自共享内存,单v本身包括它的全部数据成员,几乎肯定不被放在
        //共享内存里,v只是一个普通的基于堆的对象,所以它将被放在运行时系统为所有普通的基于堆的对象使用的任何内存
        //那几乎不会是共享内存

        //为了把 v的内容和v本身放进共享内存,你需要这样做

        //结束这个块
    }
}
代码语言:javascript
复制
    ////为了把 v的内容和v本身放进共享内存,你需要这样做
    void *localityHint=0;
    void *pVectorMemory = mallocShared(sizeof(SharedDoubleVec),localityHint);//分配足够的共享内存来容纳一个 SharedDoubleVec对象

    SharedDoubleVec *pv = new (pVectorMemory)SharedDoubleVec;// 使用 placement new 来在那块内存中建立一个SharedDoubleVec对象

    pv->~SharedDoubleVec();//销毁共享内存中的对象

    freeShared(pVectorMemory);//销毁原来的共享内存块

/** * @brief * 你获得一些共享内存,然后在里面建立一个用共享内存为 自己内部分配的vector。当你用完这个vector时,你调用它的析构函数,然后释放vector占用的内存。代码不很 复杂,但我们在上面所做的比仅仅声明一个本地变量要苛刻得多。除非你真的要让一个容器(与它的元素相 反)在共享内存里,否则我希望你能避免这个手工的四步分配/建造/销毁/回收的过程 * */

代码语言:javascript
复制
//第二个例子:假设你有两个堆,每个堆类由进行分配和回收的静态成员函数
class Heap1{
    public:
        static void* alloc(size_t numBytes, const void *memoryBlockToBeNear);
        static void dealloc(void *ptr);
};

void* Heap1::alloc(size_t numBytes, const void *memoryBlockToBeNear){
    std::set_new_handler(0);
    void *tmp = (void*)(::operator new((size_t)(numBytes * sizeof(memoryBlockToBeNear))));
    if (tmp == 0)
    {
        std::cout<<" out of memory"<<std::endl;
        exit(1);
    }
    std::cout<<"mallocShared "<<std::endl;
    return tmp;
}

void Heap1::dealloc(void *ptr){
    std::cout<<"freeShared "<<std::endl;
  ::operator delete(ptr);
}


class Heap2{
    public:
        static void* alloc(size_t numBytes, const void *memoryBlockToBeNear);
        static void dealloc(void *ptr);
};

void* Heap2::alloc(size_t numBytes, const void *memoryBlockToBeNear){
    std::set_new_handler(0);
    void *tmp = (void*)(::operator new((size_t)(numBytes * sizeof(memoryBlockToBeNear))));
    if (tmp == 0)
    {
        std::cout<<" out of memory"<<std::endl;
        exit(1);
    }
    std::cout<<"mallocShared "<<std::endl;
    return tmp;
}

void Heap2::dealloc(void *ptr){
    std::cout<<"freeShared "<<std::endl;
  ::operator delete(ptr);
}
代码语言:javascript
复制
//你想在不同的堆里联合定位一些STL容器的内容,设计一个分配器,使用像 Heap1和Heap2那样用于真实存在管理的类
template<typename T, typename Heap>
class SpHeapAllocator{
    public:

     typedef T*          pointer;
        typedef size_t      size_type;

        typedef T           value_type;
    

    // 重新绑定分配器(rebind allocator of type U) //不能少自定义分配器中
        template <class U>
        struct rebind
        {
            typedef SharedMemoryANocator<U> other;
        };
            

        T* allocate(size_type numObjecrs, const void *localityHint=0)
        {
            return static_cast<T*>(Heap::alloc(numObjecrs * sizeof(T), localityHint));
        }

        void deallocate(T* ptrToMemory, size_type numObjects)
        {
            Heap::dealloc(ptrToMemory);
        }
};
代码语言:javascript
复制
// //使用SpHealAllocator来把容器的元素集合在一起
std::vector<int,SpHeapAllocator<int,Heap1> > v;
std::set<int,SpHeapAllocator<int, Heap1> > s;
// //把v和s的元素放进Heap1

std::list<WIdget,SpHeapAllocator<Widget,Heap2> > L;
std::map<int, string, less<int>, SpHeapAllocator<std::pair<const int,string>, Heap2>> m;
// //把L和m的元素放进Heap2

/** * @brief * 注意 这里 Heap1和Heap2是类型而不是对象,STL为用不同的分配器对象初始化相同类型的不同STL容器提供了语法 * 如果Heap1和Heap2是对象而不是类型,那么它们将是不等价的分配器,那就违反了分配器的等价约束 */

条款12:对STL容器线程安全性的期待现实一些

/** * @brief * * STL容器当前支持的线程安全如下: * 1,多个读取者是安全的,多线程可能同时读取一个容器的内容,将正确地执行。当前,在读取时,不能有任何写入者操作这个容器 * 2,对不同容器的多个写入者是安全的,多线程可以同时写不同的容器 * * 这些还不够,因此,我们希望STL实现是完全线程安全的,这样我们就不用自己做并行控制了,于是你试图这样实现: * 1,在每次调用容器的成员函数期间都要锁定该容器 * 2,在每个容器返回的迭代器 begin或end的生存期之内都要锁定该容器 * 3,在每个在容器调用的算法执行期间锁定该容器(没有意义,因为算法没有办法识别出它们正在操作的容器) * */

代码语言:javascript
复制
//1,搜寻一个 vector<int> 中第一次出现 5 这个值得地方,找到了改为0
std::vector<int> v = {1,2,5,6,7};
std::vector<int>::iterator first5(find(v.begin(), v.end(),5)); //1
if (first5 != v.end()) //2
{
    *first5 = 0;//3 多线程中 1,2,3并不能保证顺序执行
}

代码语言:javascript
复制
//因此需要加锁
std::vector<int> v = {1,2,5,6,7};

getMutexFor(v);
std::vector<int>::iterator first5(find(v.begin(), v.end(),5)); //1
if (first5 != v.end()) //2
{
    *first5 = 0;//3  保证123顺序执行
}
releaseMutexFor(v);

//有没更优雅得方式

代码语言:javascript
复制
//面向对象得解决方案是创建一个 Lock类,在它得构造函数里获取互斥量并在析构函数里释放它,这样可以减小上面两队函数得不匹配概率
template<typename Container>
class Lock{
    public:
        Lock(const COntainers container):c(container){
            getMutexFor(c);//在构造函数获取互斥量
        }

        ~Lock(){
            releaseMutexFor(c);//在析构函数里释放它
        }

    private:
        const Container& c;
};
代码语言:javascript
复制
std::vector<int> v = {1,2,5,6,7};
{    //建立新块
    Lock<std::vector<int> > lock(v);
    std::vector<int>::iterator first5(find(v.begin(), v.end(),5)); //1
    if (first5 != v.end()) //2
    {
        *first5 = 0;//3
    }
}

//为社么要加一个 { } 建立一个新块呢?

/** * @brief * * 如果忘记为Lock建立一个新块,互斥量一样会释放,但是它可能发生的比它应该得更晚—— * * 当控制达到封闭快得末端,如果我们忘记调用 releaseMutexFor,将不会释放互斥量 * * 1,基于Lock得方法可以保证在异常情况下依然是稳健得,C++保证如果抛出了异常,局部对象就会被销毁 * ,所有即使当我们正在使用 Lock对象时有异常抛出,Lock也将释放它得互斥量。如果换做手动调用 getMutexFor 和 releaseMutexFor * ,那么在两者之间如果有异常抛出,将不会释放互斥量 * * 2,当涉及到线程安全和STL容器时,你可以确定库实现允许在一个容器上的多读取者和不同容器上的多写入者。你不能希望库 消除对手工并行控制的需要,而且你完全不能依赖于任何线程支持。 * */

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-10-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码出名企路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 容器
    • 条款1:仔细选择你的容器
      • 条款2:小心对"容器无关代码"的幻想
        • 条款3:使容器里对象的拷贝操作轻量而正确
          • 条款4:用empty来代替检查size()是否为0
            • 条款5:尽量使用区间成员函数代替它们的单元素兄弟
              • 条款6:警惕C++最令人恼怒的解析
                • 条款7:当使用new得指针得容器时,记得在销毁容器前delete那些指针
                  • 条款8:永不建立auto_ptr的容器
                    • 条款9:在删除选项中仔细选择
                      • 条款10:注意分配器得协定和约束
                        • 条款11:理解自定义分配器的正确用法
                          • 条款12:对STL容器线程安全性的期待现实一些
                          相关产品与服务
                          容器服务
                          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档