《Effective STL》读书笔记(一):容器

news2024/9/17 7:11:23

容器类型:

  • 标准STL序列容器:vector, string, deque, list
  • 标准STL关联容器:set, multiset, map, multimap
  • 非标准序列容器slistrope
  • 非标准关联容器:hash_set, hash_multiset, hash_map, hash_multimap
  • 标准的非STL容器:数组, bitset, valarray, stack, queue, qriority_queue

不要试图编写独立于容器类型的代码

可能有的方法是某种容器独有的,有的方法虽然名字相同,但返回值不同,迭代器/指针的失效情况也不尽相同,要编写通用的代码只能取每个容器功能的交集,但是这样又在功能上产生了很大的局限性。在选择容器时要谨慎。

有时候不可避免的要更换容器类型,一种常规的方法是:使用封装技术。最简单的方式是使用typedef对容器类型和其迭代器起别名:

// 不推荐
class Widget{...};
vector<Widget> vw;
Widget bestWidget;
//...
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);

这样改变容器类型就简单的多,编写代码时使用别名也更加方便。

确保容器中的对象拷贝正确高效

容器中很多操作都需要调用容器所存放的对象的拷贝构造函数。比如在vector, string, deque执行元素的插入或删除操作,可能会对现有的元素进行移动,移动过程中就会产生拷贝事件。

在存在继承关系的情况下,拷贝动作会导致对象切片(slicing)。如果创造了一个存放基类对象的容器,但是向其中添加了派生类对象,那么派生类对象通过基类的拷贝构造函数拷贝进容器,派生类独有的部分就会丢失。

可以通过在容器内存放指针来避免这种切片现象,而且指针拷贝的效率也很高,同时考虑智能指针会避免传统指针可能导致的内存泄露的问题,所以容器内存放智能指针是比较好的选择。

但是容器相对传统数组还是有优势的:

Widget w[maxNumWidgets];	// 每个对象都会使用默认构造函数来创建

vector<Widget> vw;			// 不会调用任何默认构造函数
vw.reserve(maxNumWidgets);	// 同样不会调用任何默认构造函数

调用empty而不是检查size()

对于大部分容器来说if (c.size() == 0)本质上和if(c.empty())是等价的,empty()通常被实现为内联函数,并且所做的仅仅是返回size是否为0

但是对于一些list实现,size()会消耗线性时间。为什么list不提供常数时间的size()呢,原因在于list所独有的链接操作,要保证链接操作是常数时间,那么size()就不能实现为常数时间。

区间成员函数优先于单元素成员函数

给定v1v2两个vector,使v1的内容和v2的后半部分相同推荐操作是:

v1.assign(v2.begin() + v2.size() / 2, v2.end());

为什么不推荐循环赋值呢?

  • 使用区间成员函数,通常可以少些一些代码

  • 使用区间成员函数通常会得到意图更加清晰的代码

  • 同时效率也有一定差别
    加入要把一个int数组插入到一个vector的最前面,使用区间函数如下:

    int data[numValues];
    vector<int> v;
    // ...
    v.insert(v.begin(), data, data + numValues);
    

    而如果要通过循环操作的话:

    vector<int>::iterator insertLoc(v.begin());
    for (int i = 0; i < numValues; ++i) {
        insertLoc = v.insert(insertLoc, data[i]);
        ++insertLoc;
    }
    

    每次循环都需要进行很多额外的赋值操作,这样会更频繁的进行内存分配,更频繁的拷贝对象,还有可能有冗余操作。

更详细对比单元素版本的insert()和区间版本的insert(),单元素版本总共在三个方面影响的效率

  1. 不必要的函数调用,在循环中对insert()进行了numValues次调用,虽然这种调用产生的影响可以通过内联来避免,但是实际insert()的实现不一定是内联的
  2. 会把v中已有的元素频繁的移动到插入后它们所在的位置,这种影响是内联也无法避免的
  3. 如果容器内是自定义类型的话,还会额外调用拷贝构造函数

区间版本的insert()函数直接把现有的元素移动到他们最终的位置上;单元素插入过程中还有可能发生多次扩容,而区间版本由于知到自己需要多少新内存,所以不必多次重新分配内存。

上面的大部分论断对于其他序列容器都适应,但是对于list而言,虽然拷贝和内存分配的问题不存在,但是额外的函数调用问题依旧存在,与此同时还出现了新问题,对于指针的额外赋值操作。

总结一下有哪些成员函数支持区间:

  • 区间创建:所有的标准容器都提供了下面形式的构造函数

    container::container(InputIterator begin, InputIterator end);
    
  • 区间插入:所有的标准序列容器都提供了如下形式的insert

    void container::insert(iterator position,		// 插入开始的位置
                          InputIterator begin,		// 区间开始
                          InputIterator end);		// 区间结束
    

    关联容器利用比较函数决定元素该插入何处,它们提供了一个省去position参数的函数原型

    void container::insert(InputIterator begin, InputIterator end);
    
  • 区间删除:所有标准容器都提供了区间形式删除操作,但是序列容器和关联容器的返回值有所不同。序列容器是以下的形式

    iterator container::erase(iterator begin, iterator end);
    

    关联容器提供了如下形式

    void container::erase(iterator begin, iterator end);
    
  • 区间赋值:所有的标准容器都提供了区间形式的赋值操作

    void container::assign(InputIterator begin, InputIterator end);
    

优先选择区间成员函数而不是其对应的单元素成员函数:区间成员函数写起来更容易;能更清晰地表达意图;并且它们表现出了更高的效率。

注意C++编译器的分析机制

假设有一个存有整数(int)的文件,现在要把这些整数拷贝到一个list中去。下面是一种合理的做法

ifstream dataFile("int.dat");
list<int> data(istream_iterator<int>(dataFile),  // 尝试传入指向文件流开始的位置
              istream_iterator<int>());		// 文件流的末尾

image-20230905094707310

上面这段代码也可以通过编译器编译,但是实际上这段代码并不能达到我们想要的效果,实际上上面的这段话声明了一个函数,为什么会这样呢?

先考虑函数的声明

// 下面这行声明了一个合法的函数
int f(double d);

// 下面这种做法也是正确的
int f(double (d));  // d两边的括号会被忽略

// 下面这种做法同样是正确的
int f(double);

再看参数带函数指针的函数的声明

int g(double(*pf) ());
int g(double pf());  // 同上,pf为隐式指针
int g(double ());  // 同上,省去了参数名

再看原来的代码

image-20230905100350177

它实际上声明了一个函数data,返回值为list<int>类型。这个函数有两个参数

  • 第一个参数名为dataFile。它的类型是istream_iterator<int>。形参名两边的括号被忽略了
  • 第二个参数没有名称。它的类型是指向不带参数的函数指针,这个函数的返回类型是istream_iterator<int>

非常的神奇,这是因为C++中有一条普遍的规律,即尽可能地解释为函数声明。实际上在前面编译的时候编译器已经警告我们了:
image-20230905101329163

类似的错误还有下面这种

class Widget{
    // ...
};
Widget w();  // 并不会调用默认构造函数

那该怎么避免这种问题呢?给形式参数的声明用括号括起来是非法的,但是给函数参数加上括号是合法的。所以当出现歧义的时候,我们可以通过加上一对括号来强迫编译器消除这种歧义:

list<int> data((istream_iterator<int>(dataFile)),
              istream_iterator<int>());

这次编译也不会产生警告了,代码也可以达到我们想要的意思了。但是有的编译器不支持这种方式,那么我们通过在构造函数调用的前面先把对象声明好,也同样可以达到效果:

istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

元素的删除

假设有一个标准STL容器Container<int> c;,删除这个容器中特定值的元素会因容器类型而异,没有通用的方法。

如果针对连续内存容器(vector ,deque, string),那么最好的方法就是使用erase-remove方法

c.erase(remove(c.begin(), c.end(), 1963),
       e.end());

如果采用循环erase的方法,每次循环erase都会产生大量元素移动的开销;remove()函数并不真正的删除元素,而是把符合删除条件的元素全部排到容器的末尾,然后返回指向第一个待删除元素的迭代器,通过这样的方法可以提高删除效率。

针对list上面的方法同样使用,但是list的成员函数remove更加有效

c.remove(1963);

当针对标准关联容器,使用removeremove_if都是错误的,他们不能用于返回const_iterator的容器。正确的做法是调用erase成员函数

c.erase(1963);

接下来把上面的问题更改一下,这次不是从c中删除所有特定值,而是通过一个函数来判断一个元素是否需要被删除:

bool badValue(int );

针对序列容器,我们把remove调用换成remove_if就行了

c.erase(remove_if(c.begin(), c.end(),  // 当c为vector, string或deque
        badValue), c.end());

c.remove_if(badValue);  // 当c为list

针对关联容器来说,有两种方法:一种是通过remove_copy_if把我们需要的值拷贝到一个新容器中,然后把原来的容器和新容器的内容进行交换:

AssocContainer<int> c;
AssocContainer<int> goodValues;

remove_copy_if(c.begin(), c.end(),
              inserter(goodValues, goodValues.end()),
              badValue);
c.swap(goodValues);

如果不想付出这么多的拷贝代价,那么我们可以使用另外一种方法:直接从原始的容器中删除元素,但是关联容器没有提供类似remove_if的成员函数,所以我们需要写一个循环来遍历c中的元素,在遍历的过程中删除。

AssocContainer<int> c;
for (AssocContainer<int>::iterator i = c.begin(); i != c.end(); ) {
    if (badValue(*i)) c.erase(i++);  // 注意这里要使用后缀自增, 防止迭代器失效
    else ++i;
}

如果需要在删除元素的同时进行一些其他的操作(如添加日志),那么针对序列容器来说,就需要改用循环删除了

ofstream logFile;

for (SeqContainer<int>::iterator i = c.begin();
    i != c.end();) {
    if (badValue(*i)) {
        logFile << "Erasing " << *i << '\n';
        i = c.erase(i);  // erase返回了下一个可用的迭代器
    }
}

针对关联容器来说只需要在上面的循环删除过程中直接添加就行了

AssocContainer<int> c;
for (AssocContainer<int>::iterator i = c.begin(); i != c.end(); ) {
    if (badValue(*i)){
        logFile << "Erasing " << *i << '\n';
        c.erase(i++);  // 注意这里要使用后缀自增, 防止迭代器失效
    }
    else ++i;
}

了解内存分配器(allocator)

内存分配器是为了内存模型的抽象而产生的,它会负责内存的分配和释放,还负责对象的构造和析构。

这里太抽象了,现在看不懂。

STL的线程安全性

STL能满足的:

  • 多个线程读是安全的。多个线程可以同时读同一个容器中的内容,并且保证是正确的
  • 多个线程对不同的容器进行写操作是安全的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/977924.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LabVIEW利用纳米结构干电极控制神经肌肉活动

LabVIEW利用纳米结构干电极控制神经肌肉活动 随着人口老龄化&#xff0c;长期护理的必要性变得更加重要&#xff0c;医疗中心的压力开始达到惊人的水平。全球对所有社会和经济部门的认识对于更好地协调卫生和社会服务之间的护理以及为更多的院外治疗提供条件至关重要。 关于医…

SSE 和 WebSocket 应用

SSE 和 WebSocket 应用 一.SSE 和 WebSocket 对比二.SSE 和 WebSocket 调试SpringBoot 下 SSE 应用1.依赖2.启动类3.接口类4.Html 测试5.测试结果 SpringBoot 下 WebSocket 应用1.依赖2.启动类3.WS 切点配置4.WS连接类配置5.WS Html 测试6.测试结果 一.SSE 和 WebSocket 对比 …

数据结构与算法-选择冒泡快排

一&#xff1a;选择排序 场景&#xff1a;找出一个班上身高最高的人你会怎么找&#xff1f;A B C D A B 选择排序的思路和插入排序非常相似&#xff0c;也分已排序和未排序区间。但选择排序每次会从未排序区间中找到最小的元素&#xff0c;将其放到已排序区间的末尾。但是不像插…

浅谈硬件连通性测试方法有哪些

硬件连通性测试是一种用于验证硬件设备之间连接的稳定性和可靠性的测试过程。那么&#xff0c;硬件连通性测试方法有哪些呢?下面&#xff0c;就来看看具体介绍吧! 1、电气测试&#xff1a;电气测试用于检查硬件设备之间的电源和信号连接。这包括使用万用表或示波器测量电压、电…

Java开发之Redis(面试篇 持续更新)

文章目录 前言一、redis使用场景1. 知识分布2. 缓存穿透① 问题引入② 举例说明③ 解决方案④ 实战面试 3. 缓存击穿① 问题引入② 举例说明③ 解决方案④ 实战面试 4. 缓存雪崩① 问题引入② 举例说明③ 解决方案④ 实战面试 5. 缓存-双写一致性① 问题引入② 举例说明③ 解决…

面试设计模式-责任链模式

一 责任链模式 1.1 概述 在进行请假申请&#xff0c;财务报销申请&#xff0c;需要走部门领导审批&#xff0c;技术总监审批&#xff0c;大领导审批等判断环节。存在请求方和接收方耦合性太强&#xff0c;代码会比较臃肿&#xff0c;不利于扩展和维护。 1.2 责任链模式 针对…

uboot命令解析流程

uboot命令解析: (1)bootdelay没有打断,跑的是autoboot_command abortboot —>run_command_list (bootcmd) (2)否则走的cli_loop cli_loop –>cli_simple_loop ----> cli_readline —>run_command_repeatable -----> &#xff08;解析命令 匹配命令 运行命令 ) …

lv3 嵌入式开发-8 linux shell脚本函数

目录 1 函数的定义 2 函数的调用 3 变量的作用域 4 练习 1 函数的定义 基本语法&#xff1a; function name() {statements[return value] }function是 Shell 中的关键字&#xff0c;专门用来定义函数&#xff1b; name是函数名&#xff1b; statements是函数要执行…

java八股文面试[数据库]——自适应哈希索引

自适应Hash索引&#xff08;Adatptive Hash Index&#xff0c;内部简称AHI&#xff09;是InnoDB的三大特性之一&#xff0c;还有两个是 Buffer Pool简称BP、双写缓冲区&#xff08;Doublewrite Buffer&#xff09;。 1、自适应即我们不需要自己处理&#xff0c;当InnoDB引擎根…

FOXBORO FBM232 P0926GW 自动化控制模块

Foxboro FBM232 P0926GW 是 Foxboro&#xff08;福克斯博罗&#xff09;自动化控制系统的一部分&#xff0c;通常用于监测和控制工业过程。以下是关于这种类型的自动化控制模块可能具有的一些常见功能&#xff1a; 数字输入通道&#xff1a; FBM232 P0926GW 控制模块通常具有多…

2、在Windows 10中安装和配置 PostgreSQL 15.4

一、PostgreSQL 安装前简介 PostgreSQL&#xff08;通常简称为PG SQL&#xff09;是一个强大、开源的关系型数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;它具有广泛的功能和可扩展性&#xff0c;被广泛用于企业和开发项目中,PostgreSQL 具有如下一些关键特点&…

VUE3+TS项目无法找到模块“../version/version.js”的声明文件

问题描述 在导入 ../version/version.js 文件时&#xff0c;提示无法找到模块 解决方法 将version.js改为version.ts可以正常导入 注意&#xff0c;因为version.js是我自己写的模块&#xff0c;我可以直接该没有关系&#xff0c;但是如果是引入的其他的第三方包&#xff0c…

Windows系统的桌面显示信息工具___BGInfo使用

一、BGInfo简介 BGInfo(桌面显示信息工具)是微软开发的用于在Windows系统中实现将Windows系统信息【如:当前用户名、CPU、操作系统版本、IP地址、硬盘等】或自定的内容显示在桌面壁纸上的操作工具,用户可以根据自己的需要定制属于自己的桌面内容(特别是对应企业来说通过域…

原理之Thread与Runnable的关系

原理之Thread与Runnable的关系 附录 课程 附录 1.Thread和Runnable的关系

深入实现 MyBatis 底层机制的任务阶段4 - 开发 Mapper 接口和 Mapper.xml

&#x1f600;前言 在我们的自定义 MyBatis 底层机制实现过程中&#xff0c;我们已经深入研究了多个任务阶段&#xff0c;包括配置文件的读取、数据库连接的建立、执行器的编写&#xff0c;以及 SqlSession 的封装。每个任务阶段都为我们揭示了 MyBatis 内部工作原理的一部分&a…

机器学习算法基础--批量随机梯度下降法回归法

目录 1.算法流程简介 2.算法核心代码 3.算法效果展示 1.算法流程简介 """ 本节算法是梯度下降方法的小批量随机梯度下降法,算法的思路是从数中随机取出n个数据进行数梯度下降,再进行相应的迭代, 最后能够获得一个效果不错的回归方程/最优解. 算法的公式就不…

lv3 嵌入式开发-9 linux TFTP服务器搭建及使用

目录 1 TFTP服务器的介绍 2 TFTP文件传输的特点 3 TFTP服务器的适用场景 4 配置介绍 4.1 配置步骤 4.2 使用 5 常见错误 1 TFTP服务器的介绍 TFTP&#xff08;Trivial File Transfer Protocol&#xff09;即简单文件传输协议 是TCP/IP协议族中的一个用来在客户机与服务器…

9.3.3网络原理(网络层IP)

一.报文: 1.4位版本号:IPv4和IPv6(其它可能是实验室版本). 2.4位首部长度:和TCP一样,可变长,带选项,单位是4字节. 3.8位服务类型 4.16位总长度:IP报头 IP载荷 传输层是不知道载荷长度的,需要网络层来计算. IP报文 - IP报头 IP载荷 TCP报文 TCP载荷 IP载荷(TCP报文) …

解决方案|电子签加速证券业“数字革命”

电子签在各行各业中的快速普及已成为近年来的新趋势。与此同时&#xff0c;电子签也在证券行业中掀起一场数字化转型的新“革命”。 2020年10月&#xff0c; 中基协《私募投资基金电子合同业务管理办法&#xff08;试行&#xff09;&#xff08;征求意见稿&#xff09;》明确了…

《代码随想录》刷题笔记——链表篇【java实现】

链表节点定义 public class ListNode {// 结点的值int val;// 下一个结点ListNode next;// 节点的构造函数(无参)public ListNode() {}// 节点的构造函数(有一个参数)public ListNode(int val) {this.val val;}// 节点的构造函数(有两个参数)public ListNode(int val, ListNo…