CPPStd涉及到的C++知识20200717
init 20200717
v0.1 20200722
v0.2 20200724 调用某个类的成员函数
v0.3 20200810 左值与右值,C++11中的using v0.4 20210620 补充STL
关于标准库
STL=Standard Template Library,标准模板库
微软在2019年左右开源了MSVC的STL:microsoft/STL: MSVC’s implementation of the C++ Standard Library.
stl的例子:Cpp-STL-Examples/thread_ex0.cpp at master · dimkatsi91/Cpp-STL-Examples
命名空间和作用域符”::”
0723
- C++ “::” 作用域符 双冒号非常道的博客-CSDN博客双冒号
- 全局作用域符
- 类作用域
- 命名空间作用域
using关键字
references:
在C++11中,使用using全面替代typedef。
当调用某个类的某个成员函数
考虑继承,虚函数\动态绑定,隐藏,覆盖,重写,基类与派生类的作用域
四个步骤:
- S1:确定静态类型;
- S2:函数名字查找;
- 查找且只按函数名查找,派生类中的成员函数与基类中的重名会造成(后者)隐藏;
- S3:类型检查;
- S4:判断是否动态绑定;
STL——各类容器简述和使用要点
顺序容器
- vector是可自动扩容(MSVC为1.5倍扩容,GCC为2倍,这个行为由编译器具体实现决定)的动态连续数组;
- 当前如果空间满载,vector内容会搬运到新家;
- 其迭代器行为可以理解为指针;(但实际不是)在insert或erase之后使用迭代器会由于vector自动扩容或跳过元素产生错误;
- insert之后vector可能会扩容,而指向某个元素位置的迭代器没有更新;这种情况下只能重新find;
- erase迭代器所在位置的元素,如果迭代器步进,则跳过了一个元素;
- ——所以vector迭代器在取值后,如果还有insert或erase的行为,就不要再使用了,除非正确更新过;
- PS:可以使用emplace就地插入,避免拷贝构造,提高性能;
- deque是通过中控器(指针数组)映射到多段连续内存组成的双端队列;——具体实现看这个里的图:STL之deque实现详解_一个菜鸟的博客-CSDN博客
- 在map满载时会搬运中控器到新家;
- 其迭代器存储了当前元素的指针和其所属内存段的首尾;
- deque如果增加元素,则可能会触发扩容,由于只搬运用于映射的指针数组,所以之前取到的元素地址或者迭代器指向的元素地址没有变化,还可以继续使用;但迭代器的node指针失效了,即迭代器不能再移动了;(这里待验证)
- deque在非头尾位置增加或删除元素后(会触发移动?)迭代器、指针、引用都会失效;
- deque在头尾位置插入,则可能触发中控器的搬运,造成迭代器失效(node指针失效,不能移动),但元素的位置不会改变,指向元素的指针、引用仍有效;在头尾位置删除,则指向除被删除元素外的其他元素的迭代器、指针、引用仍有效;
- 不同C++版本有不同的string实现策略;
- 要注意c_str()返回值指向的内存是有可能在string对象释放后被释放的,所以不建议保存c_str()的返回值,除非你能保证控制好只在string的生命周期使用;
- std::array是定长数组;
关联容器
- list是双向循环链表;
- 其迭代器行为可以类比为指针;
- insert和delete都不会引起迭代器失效;
- 但也要注意写法,不能像用下标一样用迭代器删除,erase之后iter再步进,迭代器会跑飞;
```cpp
std::list
l1 = {1,2,3,4,5,5,6,7,0,3}; for (auto iter = l1.begin(); iter != l1.end(); ++iter) { if (*iter == 5) l1.erase(iter); // 这样是错误的 }
for (auto iter = l1.begin(); iter != l1.end();) { if (*iter == 5) l1.erase(iter++); // 这里一定是iter++,不是++iter else ++iter; // 这样是正确的 }
1. list的迭代器重载的++i是迭代并返回(更新后的)迭代器引用;i++是迭代并返回(未更新的)迭代器备份(是一个匿名/临时对象)1. 所以不能直接erase(iter),迭代器还没使用节点的next迈到下一步节点就被释放了,所以会跑飞;需要erase(iter++);1. 如果只是要迭代的效果,可以使用前置++,避免临时对象的开销;<a name="wiA0S"></a>### <set/map>- set,元素唯一且有序的红黑树,元素只能增删,不能直接修改;- map,键唯一且有序的键值对红黑树;它的迭代器是一个pair<K,V>;- 插入和删除都不会引起迭代器失效;- map使用须知:- 如果要插入元素- 如果使用下标插入,如m[key] = val;,这样对于原来没有创建key的情况会正常创建key值并写入val,如果原来key下已经有值,则会直接覆盖;- 使用m.insert(make_pair(k,v));插入,如果重复则会插入失败,可以通过返回的迭代器判断;- 如果要访问元素,不能使用[],会在这个key的val不存在的情况下创建一个出来;<a name="lHM5z"></a>### <unordered_set/unordered_map>- 无序容器,是用链表法处理冲突的哈希表;- 使用时如果要装自定义对象,需要重载一个比较函数;<a name="mavKf"></a>## 容器适配器<a name="sQfSA"></a>### <stack>和<queue>- 默认由deque实现的容器适配器;- 默认不支持遍历和迭代器;<a name="w741S"></a>### <priority_queue>- 默认由vector实现的优先队列/堆;- 堆可以方便地找到第k大元素;<a name="7f455a27"></a># `<thread>`并发编程- [std::thread::thread - cppreference.com](https://zh.cppreference.com/w/cpp/thread/thread/thread)- [c++并发编程之thread::join()和thread::detach() - KeepInYourMind - 博客园](https://www.cnblogs.com/zhanghu52030/p/9166526.html)<a name="56fff668"></a>## thread的构造、join和detach- join,结合,等待;- detach,分离,挥手告别;```cpp#include <iostream>#include <thread>#include <chrono>void foo() {// 模拟昂贵操作std::this_thread::sleep_for(std::chrono::seconds(1));}void bar() {// 模拟昂贵操作std::this_thread::sleep_for(std::chrono::seconds(1));}int main() {std::cout << "starting first helper...\n";std::thread helper1(foo); // 创建一个线程对象,便开始执行其函数std::cout << "starting second helper...\n";std::thread helper2(bar);std::cout << "waiting for helpers to finish..." << std::endl;helper1.join(); // join,【结合】当前线程对这个线程等待汇合,具有同步关系;helper2.detach(); // detach,【分离】,当前线程挥手告别这个线程,不再等待;std::cout << "done!\n";}
yield
- yield,退避、让路
void count1m(int id){while (!ready)// wait until main() sets ready...{//若线程还有没创建的,将当前线程分配的cpu时间片,让调度器安排给其他线程,//由于使用了yield函数,在 not Ready 情况下,避免了空循环,在一定程度上,可以提高cpu的利用率std::this_thread::yield();}for ( int i = 0; i < 1000000; ++i) {}std::lock_guard<std::mutex> lock(g_mutex);std::cout << "thread : "<< id << std::endl;}
互斥
mutex-Mutual Exclusion,互斥锁,(线程不安全资源的)守卫
当使用一些线程不安全的数据对象时,例如std::map则可以使用mutex实现互斥;
mutex可以产生lock动作、try_lock动作和unlock动作;也可以使用RAII风格的函数,如std::lock_guardstd::mutex,【在作用域块以内,占有互斥锁,当控制离开lock_guard所在作用域时,自动释放互斥锁】
- lock,当前得不到锁,就阻塞直到得到锁;
- try_lock,当前得不到锁,就返回;
- PS:RAII-Resource Acquisition Is Initialization,资源获取即初始化
std::mutex m;void bad(){m.lock(); // 请求互斥体f(); // 若 f() 抛异常,则互斥体永远不被释放if(!everything_ok()) return; // 提早返回,互斥体永远不被释放m.unlock(); // 若 bad() 抵达此语句,互斥才被释放}void good(){std::lock_guard<std::mutex> lk(m); // RAII类:互斥体的请求即是初始化f(); // 若 f() 抛异常,则释放互斥体if(!everything_ok()) return; // 提早返回,互斥体被释放}
左值与右值,右值引用(C++11)
References:
什么是左值和右值?
- 左值是有名字的变量或对象;
- 右值是没有名字的临时变量、对象,只在一条语句中出现;例如
int i=3;中的3;T().set().get();T()生成的匿名的临时对象;
C++11之后,右值也可以被引用;
- 原来的引用(左值引用)声明如
int& r;;右值引用声明使用&&
template<typename Type, typename... Targs>Type* DrvFactory<Type, Targs...>::Create(DWORD slotId, string name, Targs&&... args) // Create函数的args可以接受右值引用{ // 自注:所谓的右值引用"&&"实际是也接受auto& instanceMap = ManInstance();auto iter = instanceMap.find(name);if (iter == instanceMap.end()) {return nullptr;}pair<BYTE, FuncType> p = iter->second;auto uniquePtr = p.second(std::forward<Targs>(args)...); // std::forward<Targs>() 的动作是: “若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值”Type* ptr = uniquePtr.get();if (ptr != nullptr) {// 保存创建的指针DrvManager<Type>::Insert(slotId, p.first, uniquePtr);}return ptr;}
Lambda表达式(C++11)
<algorithm>算法库
谓词函数
在使用sort()函数时常用
事实上就是定义了一个重载了运算符的类,使用它的对象产生结果;
几种类型转换
C语言的类型转换/强制类型转换/旧式转换:略
C++中的类型转换:
- C++强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast - SpartacusIn21 - 博客园
- C++标准转换运算符reinterpret_cast - Ider - 博客园
头文件
代码往往分为声明和实现;将声明写在头文件中,将实现写在.cpp文件中;
而模板类的声明和实现通常都写在头文件中,因为无法分离编译;0721
C++函数
template语句将它后面的声明作为模板,以将类型作为template形参这样的形式应付多种类型的情况;
template,模板
我们通常说的“用模板”,指的就是定义一个“函数模板”“类模板”:
// 函数模板template <typename T>T add(const T &a, const T &b){return a + b;}
我们注意到
类模板和模板类是什么?
- typename和class
- 模板
使用模板的注意事项
- C++模板的定义是否只能放在头文件中?_imred的专栏-CSDN博客
- c++模板编程应该把实现放在头文件中吗,这样写会不会让头文件变得很难看? - 知乎
- C++模板类代码只能写在头文件?_jinzeyu_cn的博客-CSDN博客
模板和using
template <typename _Key,typename _Tp,typename _Compare = std::less<_Key>,typename _Alloc = std::allocator<std::pair<const _Key, _Tp>>>using map = std::map<_Key, _Tp, _Compare, _Alloc>;
分配器
new-delete/malloc-free,关于分配和释放内存、内存泄漏的理解;
在进程的堆上分配了内存但没有释放,会有什么影响?
- 当系统重启后,内存空间的分配情况会完好如初,所以内存分配相关的问题的影响范围不会超过主机的一次运行时间;
- 当进程退出时,进程占用的所有物理空间都会被释放,所以内存分配相关问题的影响范围也不会超过一个进程的生命周期;
- 虽然泄漏的内存不会跨越进程的生命周期和主机的启动周期,但这并不代表内存泄漏是可以容忍的;
- 如果泄漏内存的地方被调用的频率特别高,比如它写在一个循环里,那么在调试程序的时候就会看到当前进程的内存占用涨得飞快,运行地极为缓慢;
- 这意味着当前这个程序既不能正常、快速地运行,也不能长久地运行;该程序的崩溃只是时间问题;
