C++新标准,查漏补缺(2)标准库

  • 顺序容器
    • 1. std::array
    • 2. std::swap
    • 3. vector的内存增长
    • 4. std::string
      • 4.1 find_xxx[_not]_of
      • 4.2 数值转换
    • 5 适配器
  • 泛型算法
    • 1. 泛型算法概要
    • 2. lambda
    • 3. bind
  • 关联容器
    • 1.概述
    • 2. insert的返回值
    • 3. multixxx的删除操作
    • 4. multixxx查找元素
  • 智能指针
    • 1. std::make_shared
    • 2. 数组类型的shared_ptr,unique_ptr
    • 3. std::allocator

本文是读《C++ Primer》(第五版)的笔记,但并不是事无巨细的笔记,而是一个C++98的老鸟,学习C++11,依据以往经验实践,对新标准的一种体会。
本文不面向新手,一方面给自己看,一方面希望有同样背景的C++开发人员能有所得,如有不足,欢迎指正,共同学习。
C++新标准,查漏补缺(1)基础
C++新标准,查漏补缺(2)标准库
C++新标准,查漏补缺(3)类设计者的工具

顺序容器

1. std::array

是新增加的顺序容器,另外还有个forward_list,我觉得难度不大,且用处不多,就不记录了),目的应该是替代原始的C数组。所以,问题是,它比C数组而言,哪里优秀了?
先说优点:

  • 内存
    从VS2022里,C++20,看到数据成员就一个
_Ty _Elems[_Size];

即本质上,就是对一维数组的封装,其_size,必须是常量。如果二维素组,就array, _size2>。
内存上增加不多

  • 操作便利性
    除了通用容器接口,和数组相比,多出了
    (1)size(): 读取数组大小,对自定义对象数组友好,毕竟没人想去sizeof(类);对函数传递有好处,少传一个数组大小。
    (2)构造:支持数组直接赋值,arr1 = arr2
    (3)比较:支持数组直接的比较 if (arr1 == arr2),包括其他!=、>、<、>=、<=等
  • 复杂度不高
    相比vector的复杂代码,array的代码真是简单,代码行数很少,所提供的也只是为了符合标准库容器要求的代码。

再说缺点:

  1. 在C++中并没有强迫使用std::array,也即直接使用裸数组问题并不是很大
  2. std::array本质是增加了一个类型对象,不但增加代码大小,函数传递也是个问题,函数传递,还得提前定义一个参数类型?指针就没这个问题
  3. std::array调用数组,会增加一层函数调用
  4. 不习惯C++ STL写法的,或者原始代码是C++ 98风格的,std::array会显得异类

2. std::swap

包含3种功能

  1. 交换两个相同容器的内容
  2. 交换两个相同类型变量的值,提升代码可读性
  3. 是容器的成员函数,功能同1。书上推荐用非成员版本,但实际上大部分非成员版本,直接就是对成员版本swap的调用。
    注意:
    在交换容器内容时,其效率比直接for循环要高效很多,比如顺序容器,本质是将两个容器的内存(指针)进行了交换,避免了数据拷贝。
    std::swap vector的非成员特化版本
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的数组地址进行交换。

3. 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

4. std::string

4.1 find_xxx[_not]_of

这个是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是否包含非数字字符

4.2 数值转换

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++异常捕获还可能导致栈内存问题?异常机制根本就是个鸡肋!!

5 适配器

这个本质就是设计模式的“适配器模式”,STL里也可以说是基于容器的模板,封装了常用的数据结构。这个除了好看点,实际应用中还是以直接使用顺序容器为主。
适配器的类型有

类型 定义 描述 示例
stack template > class stack 栈算法,VS2022默认基于deque实现,我们修改实现的容器 std::stack st;
queue template > class queue 队列数据结构 std::queue q;
priority_qeue template , class _Pr = less> class priority_queue 优先队列 std::priority_queue pq;

不同的适配器,由于要求支持的方法不同,即对应用的顺序容器有一定要求,比如array的大小无法修改,所有的适配器都不能使用。

泛型算法

1. 泛型算法概要

这里主要介绍stl中的标准算法,这些算法不依赖容器,是针对迭代器的算法,有很高的扩展性。
泛型算法在实际开发中应用较少,一方面源于知道的人较少,另一方面其对输入参数要求有很多前置要求,如果错误使用参数,不是返回失败而是直接崩溃。
算法的特性:

  1. 迭代器算法永远不会用到容器的操作函数,而是依赖于对迭代器的抽象,有高可扩展性
  2. 算法依赖于元素类型的操作,比如find算法,要求元素可以进行==比较

算法的分类:
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

2. lambda

这个我觉得比泛型算法重要,是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);
}

3. bind

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的作用:

  1. 将函数参数减少,比如用于泛型算法的"谓词"函数
  2. 将类成员函数,转换为回调函数进行调用:可以进行函数的注册、反注册

案例:

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));
}

关联容器

1.概述

名称 功能 是否按键值排序 键值是否可重复 value_type成员类型 对关键字要求
map 键值对形式,使用红黑树形式存储 升序 不可 std::pair 支持<操作
set 仅存储键值 升序 不可 同key_type 支持<操作
multimap 键值对形式 升序 可以 std::pair 支持<操作
multiset 键值 升序 可以 等同key_type 支持<操作
unordered_xxx 键值/键值对 无序 同非unorderd类型 同非unorderd类型 支持hash方法,==号操作符

2. insert的返回值

insert返回一个pair类型
元素0:迭代器,指向成功插入的元素的迭代器,插入失败则为容器.end()
元素1:bool类型,false,插入失败,true,插入成功

3. multixxx的删除操作

由于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

4. multixxx查找元素

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的使用情况
注意:共享指针内部没有锁,即线程不安全

1. std::make_shared

共享指针的创建可以直接使用构造函数,但标准库为shared_ptr专门提供了创建的函数
案例:

	std::shared_ptr spStr1(new std::string());
	auto spStr2 = std::make_shared();

但是,shared_ptr只支持单个对象类型,不支持数组类型。数组类型仍然需要用构造函数创建。

2. 数组类型的shared_ptr,unique_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]());

3. std::allocator

我们创建局部对象,或者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;

你可能感兴趣的:(C++11,C++,c++,开发语言)