是新增加的顺序容器,另外还有个forward_list,我觉得难度不大,且用处不多,就不记录了),目的应该是替代原始的C数组。所以,问题是,它比C数组而言,哪里优秀了?
先说优点:
_Ty _Elems[_Size];
即本质上,就是对一维数组的封装,其_size,必须是常量。如果二维素组,就array
内存上增加不多
再说缺点:
包含3种功能
template
_CONSTEXPR20 void swap(vector<_Ty, _Alloc>& _Left, vector<_Ty, _Alloc>& _Right) noexcept /* strengthened */ {
_Left.swap(_Right);
}
调用vector的swap成员版本
_CONSTEXPR20 void swap(vector& _Right) noexcept /* strengthened */ {
if (this != _STD addressof(_Right)) {
_Pocs(_Getal(), _Right._Getal());
_Mypair._Myval2._Swap_val(_Right._Mypair._Myval2);
}
}
核心的_Swap_val就是将两个vector的数组地址进行交换。
面试中的经常被问到的一个,STL中vector在添加新元素时,内存是如何增长的?
很多人回答是2倍的增长,实际是不准的,vector的内存增长不同stl实现可能都是不同。测试在vs2022,C++20,内存是按旧内存的1/2进行增长。
关键代码:
_CONSTEXPR20 size_type _Calculate_growth(const size_type _Newsize) const {
// given _Oldcapacity and _Newsize, calculate geometric growth
const size_type _Oldcapacity = capacity();
const auto _Max = max_size();
if (_Oldcapacity > _Max - _Oldcapacity / 2) {
return _Max; // geometric growth would overflow
}
const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;
if (_Geometric < _Newsize) {
return _Newsize; // geometric growth would be insufficient
}
return _Geometric; // geometric growth is sufficient
}
const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;
新的内存 = 旧的内存大小 + 旧的内存大小/2
当然,如果新增元素个数,超过预期的内存,则按实际元素大小申请内存。
测试如下:
int main()
{
std::vector vctData;
for (int i = 0; i < 100; ++i)
{
vctData.push_back(1);
printf("size: %llu, caps: %llu\r\n", vctData.size(), vctData.capacity());
}
system("pause");
return 0;
}
size: 1, caps: 1
size: 2, caps: 2
size: 3, caps: 3
size: 4, caps: 4
size: 5, caps: 6
size: 6, caps: 6
size: 7, caps: 9
size: 8, caps: 9
size: 9, caps: 9
size: 10, caps: 13
size: 11, caps: 13
size: 12, caps: 13
size: 13, caps: 13
size: 14, caps: 19
...
size: 94, caps: 94
size: 95, caps: 141
...
size: 100, caps: 141
举例,
size: 6, caps: 6
size: 7, caps: 9
——其中9 = 6 + 6/2
size: 9, caps: 9
size: 10, caps: 13
——其中13 = 9 + 9 / 2
这个是std::string的一类查找函数,在一些分隔符查找(比如说协议)、检测异常字符等比较有用,有四个
函数名 | 功能 |
---|---|
s.find_first_of(arg) | 在s中查找arg中任何一个字符第一次出现的位置 |
s.find_first_not_of(arg) | 在s中查找第一个不在arg中的字符 |
s.find_last_of(arg) | 在s中查找arg中任何一个字符最后一次出现的位置 |
s.find_last_not_of(arg) | 在s中查找最后一个不在arg中的字符 |
其中arg有以下几种形式
形式 | 说明 |
---|---|
c, pos | 从s中位置pos开始查找字符c,pos默认为0 |
s2,pos | 从s中位置pos开始查找字符串s2,pos默认为0 |
cp,pos | 从s中位置pos开始查找字符串cp,cp是字符串数组指针,以空字符串结尾,pos默认为0 |
cp,pos,n | 从s中位置pos开始查找数组指针cp指向数组前前n个字符,pos和n无默认值 |
举例:
std::string strNum("0123456789");
std::string strText("r2d2");
auto pos = strText.find_first_of(strNum); //< 可判断strText是否包含非数字字符
qstring cstring在文本转换上都很方便,std::string就有点不如了,尤其是格式化文本输出std::string,基本都是要自己重写功能函数。
std::string的转换操作相对贫瘠,先比atoi等,C++11 引入了新的函数
函数名 | 功能 |
---|---|
to_string(val) | 是个重载函数,将算术类型转换为string |
stoi/stol/stoul/stoll/stoull(s, p, b) | 返回s的起始字符串的整数值,b是基数,默认是10,p是起始下标 |
stof/stod/stold(s, p) | 返回s的起始字符串的浮点数值,p是s的起始下标 |
但是注意,这里用的s的第一个非空白符必须是符号(+、-)或者数字,必须能转换为数值,否则会抛出异常。如果你恰巧没有捕获异常,程序就崩了。
这也是我很讨厌C++库代码异常机制的地方,你返回错误就好了,直接崩掉是几个意思,你不知道C++异常捕获还可能导致栈内存问题?异常机制根本就是个鸡肋!!
这个本质就是设计模式的“适配器模式”,STL里也可以说是基于容器的模板,封装了常用的数据结构。这个除了好看点,实际应用中还是以直接使用顺序容器为主。
适配器的类型有
类型 | 定义 | 描述 | 示例 |
---|---|---|---|
stack | template |
栈算法,VS2022默认基于deque实现,我们修改实现的容器 | std::stack |
queue | template |
队列数据结构 | std::queue |
priority_qeue | template |
优先队列 | std::priority_queue pq; |
不同的适配器,由于要求支持的方法不同,即对应用的顺序容器有一定要求,比如array的大小无法修改,所有的适配器都不能使用。
这里主要介绍stl中的标准算法,这些算法不依赖容器,是针对迭代器的算法,有很高的扩展性。
泛型算法在实际开发中应用较少,一方面源于知道的人较少,另一方面其对输入参数要求有很多前置要求,如果错误使用参数,不是返回失败而是直接崩溃。
算法的特性:
算法的分类:
STL提供100多个算法,分为只读算法、写元素算法、排序算法
使用的迭代器类别:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器
算法形参模式
alg(beg, end, other, args);
alg(beg, end, dest, other, args);
alg(beg, end, beg2, other, args);
alg(beg, end, beg2, end2. other, args);
alg: 算法名
beg、end: 算法所操作的输入范围
dest、beg2、end2:指定目标范围,算法假定向目标写入元素都是能成功的,不成功就throw
args: 一般会使用匹配的函数进行值得比对、校验等,称作pred"谓词",STL也内置了一些,比如std::less
这个我觉得比泛型算法重要,是C++11的重中之重,在后续的多个C++版本中也一直在迭代完善。
lambda表达式,简单点说,就是个匿名函数。之所以放在泛型算法中介绍,源于其能作为“谓词”函数,提供给算法。
lambda表达式语法:
[捕获列表] (参数列表) -> 返回值 { 函数主体; }
捕获列表: 是lambda表达式所在的,局部变量列表
其他参数列表、返回值、函数主体,和普通函数的定义一样。
捕获列表形式包括
形式 | 说明 |
---|---|
[] | 空列表,lambda表达式,不能使用局部变量,但可以使用全局变量 |
[names] | names是一个逗号分隔的名字列表,默认按值拷贝,可以在名字前加&,则按引用传入 |
[&] | 隐式捕获,可以使用所有的局部变量,并且按引用形式使用 |
[=] | 隐式捕获,可以使用所有的局部变量,并且按值拷贝形式使用 |
[&, identifier_list] | 隐式引用捕获,identifier_list定义例外的变量,按值拷贝捕获,其中不能有&标志 |
[=, identifier_list] | 隐式按值捕获,identifier_list定义例外的变量,按引用捕获,其中不能没有&标志 |
案例:
void TestLambda()
{
using VCT_STR = std::vector;
VCT_STR vctStr = {"123", "1111", "abc", "abc123", "123456789"};
auto nCount = std::count_if(vctStr.cbegin(), vctStr.cend(),
[](const std::string &strItem) -> bool
{
return std::all_of(strItem.cbegin(), strItem.cend(),
[](char c) {return std::isdigit(c) != 0;});
}
);
//! 使用正则
auto nCount2 = std::count_if(vctStr.cbegin(), vctStr.cend(),
[](const std::string &strItem) -> bool
{
std::regex rePattern("^\\d+$");
return std::regex_match(strItem, rePattern);
}
);
printf("nCount(%s) = %d\r\n", typeid(nCount).name(), nCount);
printf("nCount2(%s) = %d\r\n", typeid(nCount2).name(), nCount2);
}
bind在旧版本就有,只是当时做的相当难用,限制太多,新版本就好用很多了。
bind本质就是将函数转换为对象,该对象再重载operator(),实现仿函数调用。
bind语法:
//! 普通函数
auto callable = std::bind(func, arglist)
//! 成员函数
auto callable = std::bind(CType::MemFunc, &obj, arglist)
//! 占位符
auto callable = std::bind(CType::MemFunc, &obj, std::placeholders::_1, std::placeholders::_2, ..., arglst)
注意:在vs2022中,不允许,_1/_2/.../n顺序不正确,比如先_2再_1,或只有_2,没有_1
然而书上写的是可以,个人偏向VS的做法,所以就没去深究了(没准新语法去掉了这个功能?)
bind的作用:
案例:
void TestBind()
{
auto checkSize = [](const std::string &strSrc, unsigned int uiSize) -> bool
{
return (strSrc.length() > uiSize);
};
const unsigned int MAX_STR_SIZE = 6;
auto checkSizeBind = std::bind(checkSize, std::placeholders::_1, MAX_STR_SIZE);
printf("checkSizeBind(%s) = %d\r\n", typeid(checkSizeBind).name(), checkSizeBind("123"));
auto checkSizeBind2 = std::bind(checkSize, std::placeholders::_1, std::placeholders::_2);
printf("checkSizeBind2(%s) = %d\r\n", typeid(checkSizeBind2).name(), checkSizeBind2(std::string("123"), 1u));
//! 绑定类成员函数
CCallFunc calcFunc;
auto calcBind = std::bind(&CCallFunc::calcSum, calcFunc, std::placeholders::_1, std::placeholders::_2);
printf("calcSum(%s) = %d\r\n", typeid(calcBind).name(), calcBind(10, 20));
}
名称 | 功能 | 是否按键值排序 | 键值是否可重复 | value_type成员类型 | 对关键字要求 |
---|---|---|---|---|---|
map | 键值对形式,使用红黑树形式存储 | 升序 | 不可 | std::pair |
支持<操作 |
set | 仅存储键值 | 升序 | 不可 | 同key_type | 支持<操作 |
multimap | 键值对形式 | 升序 | 可以 | std::pair |
支持<操作 |
multiset | 键值 | 升序 | 可以 | 等同key_type | 支持<操作 |
unordered_xxx | 键值/键值对 | 无序 | 同非unorderd类型 | 同非unorderd类型 | 支持hash方法,==号操作符 |
insert返回一个pair类型
元素0:迭代器,指向成功插入的元素的迭代器,插入失败则为容器.end()
元素1:bool类型,false,插入失败,true,插入成功
由于mulitmap、multiset 键值可以重复,调用erase,传入key_type时,可能同时删除多个元素,通过返回值来判断真实删除了几个元素。普通map只会返回1或者0。
class CMapItem; //< 重载operator<
MULTI_SET_ITEM multiSetItem;
using MULTI_SET_ITEM = std::multiset;
MULTI_SET_ITEM multiSetItem;
multiSetItem.insert({ 5 });
multiSetItem.insert({ 2 });
multiSetItem.insert({ 2 });
auto ret = multiSetItem.erase({ 2 });
printf("multiset erase ret type: %s, value: %d, now size: %d\r\n",
typeid(ret).name(), ret, multiSetItem.size());
ret = multiSetItem.erase({ 5 });
printf("multiset erase ret type: %s, value: %d, now size: %d\r\n",
typeid(ret).name(), ret, multiSetItem.size());
ret = multiSetItem.erase({ -1 });
printf("multiset erase ret type: %s, value: %d, now size: %d\r\n",
typeid(ret).name(), ret, multiSetItem.size());
输出
multiset erase ret type: unsigned int, value: 2, now size: 1
multiset erase ret type: unsigned int, value: 1, now size: 0
multiset erase ret type: unsigned int, value: 0, now size: 0
multixxx的查找,由于有多个元素,假如按如下方式查找
multiSetItem.insert({ 5 });
multiSetItem.insert({ 2 });
multiSetItem.insert({ 2 });
multiSetItem.insert({ 1 });
for (auto iter = multiSetItem.find({2}); multiSetItem.end() != iter; ++iter)
{
printf("multiset find value: %d\r\n", iter->getNum());
}
输出:
multiset find value: 2
multiset find value: 2
multiset find value: 5
返回会多出元素5,因为find的只是迭代器的第一次出现该元素的位置,迭代器自然可以继续++,直到end
所以需要配合相同元素个数,来查找所有元素
//! 查找元素
auto nNumSize = multiSetItem.count({ 2 });
for (auto iter = multiSetItem.find({ 2 }); nNumSize > 0; ++iter, --nNumSize)
{
printf("multiset find value: %d\r\n", iter->getNum());
}
以此类推,书中还给了使用泛型算法的方式,目的还是找到迭代的区间
for (auto itBeg = multiSetItem.lower_bound({2}),
itEnd = multiSetItem.upper_bound({2}); itBeg != itEnd; ++itBeg)
{
printf("multiset find value: %d\r\n", itBeg->getNum());
}
for (auto pos = multiSetItem.equal_range({ 2 }); pos.first != pos.second; ++pos.first)
{
printf("multiset find value: %d\r\n", pos.first->getNum());
}
其中lower_bound同pos.first,是第一次出现该元素的位置
其中upper_bound同pos.second,是最后一次出现该元素的位置+1,即可能是end()
再看unordered_multixxx,他是无序的,但无序只是针对key_type而言,实际仍是按照key_type的hash值进行排序,相同的key值,hash值是相同的,所以查找的方案和有序集合相同
using UNORDER_MULTI_SET_STR = std::unordered_multiset;
UNORDER_MULTI_SET_STR unMultiSetStr = {"5", "2", "2", "1"};
for (auto iter = unMultiSetStr.find({ 2 }); unMultiSetStr.end() != iter; ++iter)
{
printf("unordered multiset find value: %s\r\n", iter->c_str());
}
智能指针是依据RAII的一组模板类对象,分为
shared_ptr,共享指针,可用于指针共享式传递,拷贝,直到最后一个共享指针对象不使用了,就销毁(计数器为0)
unique_ptr,独占指针,仅用于局部对象,不可拷贝、赋值,如果要指针将传递,使用release/reset来独占式传递
week_ptr,必须依赖于一个shared_ptr使用,主要用来核查shared_ptr的使用情况
注意
:共享指针内部没有锁,即线程不安全
共享指针的创建可以直接使用构造函数,但标准库为shared_ptr专门提供了创建的函数
案例:
std::shared_ptr spStr1(new std::string());
auto spStr2 = std::make_shared();
但是,shared_ptr只支持单个对象类型,不支持数组类型。数组类型仍然需要用构造函数创建。
数组类型的的shared_ptr,如下:
std::shared_ptr spArr1(new(std::nothrow)char[1024](), [](char *p) {delete[] p;});
需要指定删除器,因为默认的shared_ptr,使用delete删除,会造成内存泄漏。注意,在windows、vs平台下,delete和delete[]对内建类型变量是一样的,如果是换成类,区别就大了。
以上我们还可以换个写法:
std::shared_ptr spArr2(new(std::nothrow)char[1024](), std::default_delete());
其中std::default_delete是unique_ptr的默认删除器,有非数组和数组(特化)两个版本,声明如下
template< class T > struct default_delete;
template< class T > struct default_delete;
所以unique_ptr的数组类型,不用关心这个问题,统一用:
std::unique_ptr upArr(new(std::nothrow)char[1024]());
我们创建局部对象,或者new,总会调用到对象的构造函数,或者初始化。然而,如果只是想创建原始内存,不想执行初始化、构造,而在具体要用的时候再构建,就用:std::allocator
案例
//! 创建10个std::string对象,且不调用构造函数
const int CST_SIZE = 10;
std::allocator allocStr;
auto pStr = allocStr.allocate(CST_SIZE);
allocStr.construct(&pStr[0]); //< 调用pStr[0]的构造函数
allocStr.construct(&pStr[1]); //< 调用pStr[1]的构造函数
allocStr.destroy(&pStr[0]); //< 调用pStr[0]的析构函数
allocStr.destroy(&pStr[1]); //< 调用pStr[1]的析构函数
//! 销毁所有的string对象内存,但不调用析构函数,即:销毁前,程序需要自行保证这些对象析构已经被执行,即destroy被调用过
allocStr.deallocate(pStr, CST_SIZE);
pStr = nullptr;