04 | 容器汇编 I:比较简单的若干容器
04 | 容器汇编 I:比较简单的若干容器
讲述:吴咏炜
时长17:16大小15.77M
string
vector
deque
list
forward_list
queue
stack
内容小结
课后思考
参考资料
赞 8
提建议
精选留言(59)
- 中年男子2019-12-04我发现老师的问题基本都可以在文章中找到答案, 1、2就不说了,3说一下我的理解:引用老师在vector那段的话 stack(queue)为保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数, 通常会使用拷贝构造函数,而pop作用是释放元素,c++98还没有移动构造的概念,所以如果返回成员,必须要调用拷贝构造函数,这时分配空间可能出错,导致构造失败,要抛出异常,所以没必要返回成员。展开
作者回复: 很棒👌。 异常安全是关键。
共 2 条评论79 - 禾桃2019-12-05请教一个问题, #1 为什么一定强制移动构造函数不要抛出异常? 移动构造函数抛出异常后,catch处理不可以吗? #2 为什么拷贝构造函数被允许抛出异常? 能麻烦给些代码说明一下吗? 非常感谢!展开
作者回复: catch住也没有用了。仔细想一下,我现在要把vector里的两个对象移到一个新的vector,移第一个成功,第二个时有异常,然后vector该怎么办?现在两个vector都废掉了。 拷贝不影响旧的容器。即使发生异常,至少老的那个还是好的。这就是异常安全。
共 2 条评论55 - YouCompleteMe2019-12-041.都是线性容器 2.不同容器功能,效率不一样 3.实现pop时返回元素时,满足强异常安全,代码实现复杂,可读性差。
作者回复: 3的正解终于出现,有人说到“异常安全”了。👍 再说两句,这是C++98时设计的接口,没有移动就只能那样。有了移动,在多线程的环境里,移动返回加弹出实际上就变得有用了。我对复杂和可读性部分不那么同意。
27 - Alice2019-12-11吴老师您好,我是那个那天问您vector演示代码的学生,还需要接着请教这段代码的一些问题。因为我刚接触c++不久,可能有些基本语法理解的不是很到位还没有那么深,就需要再问几个基础的问题了。就先请教obj1部分的函数吧,先用reserve(2)预留了两个存储空间,然后接着用emplace_back()在最后面构造新元素,所以说因为有两个新开的空间那么前两次用emplace_back()构造元素成功就调用构造函数抛出两个obj1( )不知道理解的对不对?那第三个obj1( )是怎么来的呢?后面两个obj1( const obj1&)怎么来的也不是很理解?这里的obj1&为什么要定义成const类型呢? 还有就是我现阶段对构造函数的理解还停留在初始化的意思上理解地还是太浅吧,不知道该怎么再往深理解一下? 麻烦老师再帮我解答一下问题,辛苦老师了💦展开
作者回复: 头两个在已有空间上成功构造。第三个时发现空间不足,系统会请求更大的空间,大小由实现决定(比如两倍)。有了足够的空间后,就会在新空间的第三个的位置构造(第三个obj1),成功之后再把头两个拷贝或移动过来。
共 2 条评论12 - EncodedStar2019-12-19《现代C++实战31讲》第一天 容器汇编1:比较简单的若干容器 一、容器的输出: 1.简单容器(如:vector)输出就是遍历(v.begin,v.end) 2.复杂容器(如:vector<vector>)就需要工具 xeus-cling 二、string 1.接口中不建议使用const string& ,除非确实知道调用者使用的是string,如果函数不对字符串做特殊处理的话用const char* 可以避免在调用字符串的时候构造string 三、vector 1.vector主要缺陷是大小的增长导致的元素移动,如果可能,尽早使用reserve函数为vector保留所需要的内存,在vector预期会增长很大时带来很大的性能提升 四、deque 1.如果需要经常在头尾增删元素内容,deque会合适 五、list 1.list 是双向链表 2.forward_list是单向链表 六、stack 1.后进先出,底层由deque实现 课后思考: 1.容器有哪些共同点 答:都是线性容器,非关联容器 2.为什么C++有那么多不同的序列容器类型 答:不同容器对应实现不同需求,效率不同 3.为什么stack(或者queue) pop函数返回的是void而不是直接返回内容 答:为了保证异常安全,如果返回的成员构造失败就会抛出异常。展开
作者回复: 学习很认真,回答也基本抓住要点了,尤其问题 2 和 3。👍
8 - Alice2019-12-06老师 您好 我是一个c++的初学者😳,这一讲的容器的概念原理都理解了,就是vector那一段的演示代码推不出老师的结果来,能不能麻烦老师再解释一下那段代码,辛苦老师了👨🏫!
作者回复: 关于几次拷贝/移动的问题?参考 hello world 的评论下的廖熊猫的回答: 在插入的时候,你会发现空间不够了,然后开辟新的空间,在新空间先把最后插入的元素放好,然后再依次把以前的元素一个一个挪过来。空间不够的话最后一个元素是没法插入进去的啊,所以没办法移动三次的。 还有我自己的回答: 两者都是要构造第 3 个对象时空间不足,需要这样: 1. 分配一个足够大的新内存区域。 2. 在上面构造第 3 个对象。 3. 如果成功(没有异常),再移动/拷贝旧的对象。 4. 全部成功,则析构旧对象,释放旧对象的内存。 5. 如果 1 出现异常,直接抛出即可;如果 2–3 出现异常,则析构已成功构造的对象,释放新内存空间,继续抛出异常。 如果不是这个问题。请把问题阐释得更详细些。可以重新开一个新的评论。
共 6 条评论7 - 徐凯2019-12-04第一个问题 今天讲的大多是线性结构的容器,也可以说大多是非关联容器 第二个问题 应该不只是c++ 所有语言都提供了,之所以对其封装是便于使用,不需要用户自己去造轮子。同时有些容器内部有迭代器 与stl算法相结合可以便于实现泛型编程。c++委员会想让c++成为一个多元化的语言支持 面向对象 面向过程 泛型编程 第三个问题 将对容器的操作与获取值的操作分离开,用途会更明确。同时pop由于已经从容器中剔除了那个元素,那么返回的只能是个拷贝不允许返回已销毁元素的引用。这意味着需要一次拷贝构造操作。而top只需要返回指定元素的引用,高效简洁。将两次操作分开使得操作更明确同时没有额外开销。 个人见解 请老师赐教😃展开
作者回复: 挺好。三比其他回答已经进一步了,但还是没有触及到某个关键字。
共 2 条评论5 - englefly2020-03-25吴老师,请教一个容器相关的问题。 vector<string> 在底层我原来理解是保存了一个string对象的数组。每个string对象有一个指针成员,指向string的字面值。这样每个string字面量大小不一,但string对象的大小是固定的。 在《C++导学》里看到一个string的优化,将短字符串直接保存在string对象内部。加上这个优化,导致string对象大小不一,那vector<string> 怎么存储呢? 谢谢解答展开
作者回复: 确实就是像你说的这样。 即使用你说的短字符串优化,string的大小仍然是固定的。它这种优化的做法是,在 string 里(basic_string 里)定义了一个类似于 value_type short_str_[16] 这样的成员。可以用 union 来让这个成员跟其他一些成员来共用空间,来减少额外开销。不管什么情况,basic_string 的大小仍然是固定的。一个类的大小永远是固定的。
2 - 李杰2020-01-01吴老师好,请教个问题。 将对象通过智能指针包装一下再存到容器中,我看文章中说到了其中一条理由,那就是可能这个对象的移动构造函数没有保证一定不抛异常,请问还有其他的原因使用这个用法吗?
作者回复: 有啊,最基本的原因就是多态了:如果你存进去的对象实际是 circle、triangle 等不同对象的话,你声明时用 shared_ptr<shape> 应该就比较合适了。
2 - robonix2019-12-11老师,假如移动构造函数被声明为noexcept了,诱导编译器调用移动构造,而此时却又抛异常了,程序也会直接停止吗?
作者回复: 对,缺省情况下,std::terminate 会被调用。
2 - 皓首不倦2019-12-09老师您好 第三个问题能不能这样理解 pop作用是弹出最后一个对象 弹出后该对象内存已经脱离容器当前所管理的有效的范围 虽然该片内存在后续有push操作时候还会被重复使用到 但是pop执行完后 该片内存逻辑层面看是暂时脱离了容器的管理范围的 显然pop不能将该片内存以引用方式传给外面 否则外部会持有一片目前脱离管理的无效内存 外部再对这片内存不论读还是写都是不合适的 所以pop如果要反返回对象 只能选择拷贝方式返回 会触发拷贝构造 对于内存占用大或者是需要进行深拷贝的对象而言 这个操作开销太大了 所以选择用top 返回可以安全访问的对象引用 而pop就单纯作为退栈操作不返回对象 我个人理解这样设计api 接口是为了避免不安全地访问内存 对比Java的 stack的 pop接口 Java的pop接口就返回了栈顶对象 因为这个对象内存托管给了jvm管理 调用端拿到了这个出栈的对象的引用也不会有访问内存的问题 但是c++如果把对象内存通过引用带给调用端 那调用端就可能直接读写容器内部的私有内存了 这片内存地址随时可能因为容器的扩容行为而变成野地址 对其访问其实并不安全 不知道我这样理解是否正确展开
作者回复: Java是引用语义,返回对象就是返回个指针,没有任何问题。 C++是值语义,以前返回对象只能是拷贝,可能发生异常。一旦发生异常,对象已经被弹出,那它就彻底“丢失”了。
共 2 条评论3 - ano2021-10-07老师请问下,下边我这段 closure 代码 为什么不能正确 capture 变量 weight? struct Mammal { std::function<void()> speak; std::function<int()> getWeight; std::function<void(int)> setWeight; } Mammal *Cow() { int weight = 100; auto *m = new Mammal(); m->speak = []() { puts("momo~~ momo~~"); }; m->getWeight = [&]() { return weight; };; m->setWeight = [&](int w) { weight = w; }; return m; }展开
作者回复: 按引用捕获一个函数的本地变量在函数外面用?程序没死给你看已经很给你面子啦。 请牢记本地变量的生命周期在函数返回时即已结束。另外,请看一下第 16 讲中关于按引用捕获的使用建议。 今天我来重写的话,我可能还会加一条,lambda 表达式产生完立即调用的情况下,也可以使用 [&] 来捕获。
共 4 条评论1 - 怪兽2021-06-11老师,有个疑问:对于只有成员变量的自定义结构体,编译器默认会提供搬移构造函数吧?若这个自定义结构体存储在std::vector中,当vector扩容时,会执行自定义结构体的搬移构造吗?这个我不知道怎么测试,想向老师确认一下会不会走搬移构造。
作者回复: 对,会走移动构造。 你只要在结构体的成员里放一个有真正移动构造行为的对象,在它的移动构造函数里打印信息,可以观察到这一行为。 注意,这个移动构造函数必须标为 noexcept,你才会观察到这一行为。
1 - 农民园丁2021-01-26第三遍学习,产生一个困惑,请教吴老师: 采用不抛出异常的移动构造函数,会不会导致vector的内存不连续呢? 在上面的例子中,构造第3个Obj2对象时空间不足自动扩充,此时的空间为新分配的内存空间。 之后在新的空间第3个位置构造Obj2,然后将原空间第1、2个对象移动到新空间的第1、2个位置。 这个移动的过程是否只是将原空间第1、2个对象的指针移动到了新的空间?这样是不是新的vector空间内存布局就不连续了?展开
作者回复: 首先,vector 的内存空间必然是连续的。 移动是移动对象,而不是指针。对象内部有什么,什么是合理的移动,由这个对象来定义。就我给出的例子,Obj2 大小为 1,所以移动就是把这个 1 字节的对象移动过去,执行的是 Obj2(Obj2&&)——真实的类就靠这个干活了,而我这里实际什么都没有做。
共 2 条评论1 - panda2019-12-26老师您好,vector那个例子我改成 vector<Obj1> v1; v1.reserve(2); v1.push_back(Obj1()); v1.push_back(Obj1()); v1.push_back(Obj1()); vector<Obj2> v2; v2.reserve(2); v2.push_back(Obj2()); v2.push_back(Obj2()); v2.push_back(Obj2()); return 0; 输出是: Obj1() Obj1(Obj1&&) Obj1() Obj1(Obj1&&) Obj1() Obj1(Obj1&&) Obj1(const Obj1&) Obj1(const Obj1&) Obj2() Obj2(Obj2&&) Obj2() Obj2(Obj2&&) Obj2() Obj2(Obj2&&) Obj2(Obj2&&) Obj2(Obj2&&) 想请问,Obj1的移动构造不是没用了吗?为啥Obj1前面创建用的还是移动构造,后面转移时移动构造又被禁用了?展开
作者回复: 这不正是正文里讨论移动构造函数不应该抛异常这个问题么。之所以push_back时没这个要求,因为本来这儿就是临时对象,移动不成功也没有特别的损失。移动现有的元素如果失败了,那vector的状态就彻底混乱了。
共 3 条评论1 - robonix2019-12-16老师,文中提到使用v1.push_back会额外生成临时对象,多一次拷贝构造和一次析构。是不是应该改为多一次移动构造和析构呢?
作者回复: 这个疑问提得好👍。在目前这个例子里,确实是移动构造而不是拷贝构造。
1 - Alice2019-12-12谢谢老师的回复,老师您说“扩充空间是一个编译器自发进行的操作,没有用户控制。一般会类似于reserve(size() * 2)”,我之前没有遇到过这个知识点,那老师我应该看点什么稍微补充一下这块知识?
作者回复: https://www.cnblogs.com/skyfsm/p/7488053.html 这篇中文文章说得还比较清楚,可以看一下。
1 - 花晨少年2019-12-07如果不修改字符串的内容,使用 const string& 或 C++17 的 string_view 作为参数类型。后者是最理想的情况,因为即使在只有 C 字符串的情况,也不会引发不必要的内存复制 ----------------- 没有理解,“只有 C 字符串的情况,也不会引发不必要的内存复制”,对于string_view相对于const string&,能否简单举个例子
作者回复: 只有const char*,目标是const string&,会引发一个临时string的构造,会导致内存复制。 用string_view当然也会产生临时对象,但string_view不会复制字符串的内容。
共 2 条评论1 - 皮皮侠2019-12-04老师,C++11里的“stream流”是不是真的有缺陷,只能在测试等环境下用用?C++后续对此有改进么?
作者回复: 没有问题的。除了写出来的样子有时候不那么方便,输出项多时性能不那么高,完全可用,毕竟有灵活性和类型安全性。 C++20 会引入新的机制,类似于printf系列但类型安全。见: https://en.cppreference.com/w/cpp/utility/format
1 - lyfei2019-12-04为什么我使用output_container.h的时候出现了错误: 我应该开启了c++17标准了(我用的编辑器是CLion,编译器是g++)[但是我不太清楚是否真的开启了] error: ‘is_pair_v’ declared as an ‘inline’ variable
作者回复: GCC 版本是多少?我在老版本的 GCC 上能复现这个问题。 请使用 GCC 7 以上版本。
共 3 条评论1