vector

vector

1、string的补充

1.1、拷贝构造的现代写法

现在很多人偏向于把string的拷贝构造设计成这样:

拷贝构造这样设计,进行了两步:

  1. 利用参数s存的字符串,构造出string串tmp
  2. 将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++舍弃了这一策略:

原因有三:

  1. 多线程情况下,维护引用计数的线程安全有一定代价。
  2. 动态库下,会存在一些问题。
  3. 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语言二维数组看似二维,实则一维)

相关推荐
khalil10201 小时前
代码随想录算法训练营Day-31贪心算法 | 56. 合并区间、738. 单调递增的数字、968. 监控二叉树
数据结构·c++·算法·leetcode·贪心算法·二叉树·递归
小苗卷不动2 小时前
进程与线程的核心区别
c++
啊我不会诶2 小时前
2024ICPC西安邀请赛补题
c++·算法
ZenosDoron2 小时前
keil软件修改字体,Asm editor,和C/C++ editor的区别
c语言·开发语言·c++
山栀shanzhi2 小时前
C/C++之:构造函数为什么不能设置为虚函数?
开发语言·c++·面试
谭欣辰3 小时前
C++ 版Dijkstra 算法详解
c++·算法·图论
qeen873 小时前
【算法笔记】双指针及其经典例题解析
c++·笔记·算法·双指针
王老师青少年编程5 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:雷达安装
c++·算法·贪心·csp·信奥赛·区间贪心·雷达安装
elseif1235 小时前
分组背包1
c++·学习·算法