vector
1、string的补充
1.1、拷贝构造的现代写法
现在很多人偏向于把string的拷贝构造设计成这样:

拷贝构造这样设计,进行了两步:
- 利用参数s存的字符串,构造出string串tmp
- 将tmp与*this交换
拷贝构造的现代写法,核心就是代码复用。
由于一开始*this未初始化,可能有程序崩溃的风险。所以我们可以给缺省值:

1.2、重载=的现代写法

思路与拷贝构造的现代写法是一样的,都是代码复用。
甚至我们可以让重载=的代码更简洁一点:

我们设计的参数是string s,这是传值传参,string自定义对象传值传参时自动调用拷贝构造。然后再用这份拷贝与*this交换。
这里巧妙的一点是,我们利用传值传参自动调用拷贝构造的特点,让编译器自动为我们进行拷贝构造,所以我们不需要显示写拷贝构造。
但有一点需要强调:以上现代写法,均没有提升效率。该申请资源还是得申请资源,该释放资源还是得释放资源。
1.3、SBO
有一个问题:
cpp
void test1()
{
std::string s1("Hello world!");
std::string s2;
}
s1、s2,哪一个占用空间大?
按照我们之前自主实现string的思路,声明私有成员变量时,有三个:
- char* _str(存储的指针指向资源)
- size_t _size
- size_t _capacity
相同的机器下,三个私有变量大小相同(64位:char*, size_t都是8;32位:char*, size_t都是4)。所以我们推理,大小都应是12/24。


结果却是28/40?
调试看看vs2022下string对象里面到底有啥?
有时候vs2022调试时看不到string对象里面具体包含的成员。
我们可以在vs2022做如下修改:
工具 ---> 选项 ---> 调试 ---> 常规 ---> 勾选上在变量窗口中显示对象的原始结构

其中,_Mysize就是_size,_Myres就是_capacity。
还有_Buf,_Ptr。
实际上,这里使用了SBO。
SBO = Small Buffer Object,小缓冲区对象
也叫SSO = Small String Optimization,小字符串优化
简单来说,就是内容小于等于15字节,存放入_Buff字符数组中,就不需要申请资源。
而内容大于15字节,就必须申请资源,此时_Ptr指向申请的空间。
1.4、引用计数的写时拷贝
以上是MSVC管理资源的策略。
而早年g++版本的策略,是这样的:

在没有任何一个对象资源上的内容需要被修改时,其实就是浅拷贝。
只不过,多了一个引用计数:

于是,每次要销毁一个对象,如果引用计数大于1,只需要引用计数 - 1;如果引用计数等于1,才释放空间。
这看起来非常高效,但是这种策略只解决了一个问题:指向同一块空间会导致多次析构。
还有一个问题没有解决:改变一个对象,其它对象跟着改变。
这时就必须拷贝了:
- 开辟新空间,拷贝内容。新对象的引用计数起始为1
- 原对象的引用计数 - 1
这就是引用计数的写时拷贝。
采取写时拷贝策略的原因是:有的需要拷贝对象行为的场景,并不会修改对象。比如传值返回包含大资源的对象。
但是新版Linux的g++舍弃了这一策略:

原因有三:
- 多线程情况下,维护引用计数的线程安全有一定代价。
- 动态库下,会存在一些问题。
- C++11以后,编译器有各种优化,传值返回的代价较低,而且有了移动构造、移动赋值的方式。
2、vector的使用
构造

想打印?三种方法:
- 重载[ ]
- 范围for
- 迭代器

2.1、initializer_list
我们还可以这样初始化vector< int >:


其中,我们用花括号包含了一段序列,这段序列的类型是:initializer_list。

initializer_list类包含三个成员:

我们看到成员包含迭代器。所以initializer_list类也支持范围for:

2.2、vector的扩容机制
像string一样,我们设计代码观察vector的扩容机制。

MSVC下:

1.5倍扩容。
g++下:

2倍扩容。
为了减少扩容次数,我们可以用:reserve()。

2.3、其它接口
resize()
resize()的作用是:缩容就删除(部分)数据,扩容就用指定内容填充。
那么我们就可以用特定值初始化,也可以理解为批量操作:


insert() && erase()
vector的插入和删除接口是通过迭代器实现的:



2.4、emplace简单介绍
我们定义一个多参数的类行:

然后创建一个vector顺序表,每一个元素是AA类型的:

我们不难知道,里层花括号的逻辑代表:多参数隐式类型转换,外层花括号的逻辑代表:initialize_list类型对象。
但是不能直接打印:
所以我们可以这样打印:
emplace的行为与insert相似,emplace_back与push_back相似:

但是emplace_back可以这样用:

也就是传入emplace_back的参数,可以是:
- AA对象
- 构造AA对象的参数
emplace也是类似。
但是push_back和insert只能传对象。
2.5、几道算法题
只出现一次的数字
这道题的核心思路是:相同的值,按位异或后得0。
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (int i = 0; i < nums.size(); ++i)
{
ret ^= nums[i];
}
return ret;
}
};
杨辉三角
题目中要求我们返回数据的类型是:vector<vector<int>>。
这个类型的意思是,有一个vector顺序表v1,v1的每一个元素,都是vector<int>顺序表。

我们可以理解为,最顶上的模板类实例化出了两种类:T = vector< int >, T = int。然后这两种类的关系如上图。
cpp
class Solution {
public:
vector<vector<int>> generate(int numRows) {
//开辟numRows个空间,对应numRows行序列
vector<vector<int>> v1(numRows);
for (int i = 0; i < numRows; ++i)
{//每行序列开辟空间,初始化成1
v1[i].resize(i+1, 1);
}
//行
//从第3行开始
for (int i = 2; i < numRows; ++i)
{
//列
//从第2个数开始,一直到i-1
for (int j = 1; j < i; ++j)
{
v1[i][j] = v1[i-1][j-1] + v1[i-1][j];
}
}
return v1;
}
};
上面代码演示中,v1[i][j]左右两个[]代表不同的重载:
cpp
vector<int>& operator[](size_t n)// 左边的[]
{
// ...
}
int& operator[](size_t n)// 右边的[]
{
// ...
}
而重载放入类域,则可能会inline内联展开。
我们可以将vector的二维数组,与C语言的二维数组做一个比较。(C语言二维数组看似二维,实则一维)


