
◆ 博主名称: 晓此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️C++系列个人专栏:
⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰
目录
[2.3 重载的全局Swap函数](#2.3 重载的全局Swap函数)
0.1概要&序論
这里是此方,久しぶりです! 本篇文章是string的最后一篇 ,在前面我们主要讲解了string的各种接口的使用,本片,我将带领大家解决string的一些疑难杂症 ,帮助大家更好的理解string。这里是「此方」。让我们现在开始吧!
一,深拷贝的代码优化
1.1深拷贝和浅拷贝
**浅拷贝就是按字节拷贝,深拷贝就是创建并拷贝给临时对象再拷贝。**通俗的讲,浅拷贝拷贝指针,深拷贝拷贝指针指向的内容。
两张图看懂:


1.2深拷贝的传统写法与现代写法
深拷贝离不开拷贝构造,我们来看看string的拷贝构造函数时怎么实现的。
cpp
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
这是一种传统的深拷贝方式我们用一张图解释它:

创建一个新的空间并指向,将原来空间的数据拷贝给新空间。修改_size和_capacity等数据。
这样做还是太麻烦了,于是出现了现代写法:
cpp
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
我们同样用一张图来解释它:

用s._str指针指向的字符串构造一个新的tmp对象,并将这个tmp对象的指针与我们的目标对象的指针互换。相比传统方法更省事,将"本应该由_str完成的事情"让"tmp代理完成"。
二,三个swap

2.1C++自带的全局Swap模板
头文件:<algorithm> (C++98),<utility>(C++11)。
时间复杂度 :O(n),因为涉及拷贝构造和赋值操作(对大对象效率低)。
用途 :适用于所有类型,但没有特别优化。
测试代码:
cpp
std::string a = "hello", b = "world";
std::swap(a, b); // 使用通用版本,除非有特化
为什么效率低下?

这是这个swap函数的底层。它一共进行了三次深拷贝。如果对于内置类型来说,影响不算什么,但是对于自定义或者是STL中的类型,效率一定会大打折扣。
2.2成员函数Swap
这是底层最高效的交换方式。
作用:交换当前字符串与另一个字符串的内容。
实现方式:交换内部指针,不复制字符。
时间复杂度:O(1) ------ 常数时间,非常高效。
头文件 :<string>
特点:是类的成员函数,只能通过对象调用。
测试代码:
cpp
a.swap(b); // a 是 this,b 是参数
2.3<string>重载的全局Swap函数
作用 :交换两个 string 对象的值。
实现方式 :通常直接调用 x.swap(y),即委托给成员函数。
时间复杂度:O(1)
测试代码:
cpp
std::swap(a, b); // 优先使用这个特化版本
头文件 :<string>
关键点 :这是对通用**std::swap** 的重载 ,使得当参数为 **string**时,自动选择此版本,避免使用慢的通用模板。(当一个模板和一个现成的函数同时存在时编译器会选择现成函数。)
2.4一张表格看懂区别
| 特性 | std::swap(T&, T&)(通用模板) |
std::string::swap(string&)(成员函数) |
std::swap(string&, string&)(非成员特化) |
|---|---|---|---|
| 定义位置 | <algorithm> / <utility> |
<string> |
<string> |
| 是否模板 | 是 | 否 | 否(特化) |
| 调用方式 | swap(a, b) |
a.swap(b) |
swap(a, b) |
| 实现机制 | 拷贝构造 + 赋值(O(n)) | 交换内部指针(O(1)) | 通常调用 x.swap(y)(O(1))这里利用了编译器的优化 |
| 性能 | 慢(尤其对大字符串) | 快(O(1)) | 快(O(1)) |
| 是否推荐使用 | 不推荐用于**string** |
推荐 | 推荐(更自然) |
三,引用计数与写时拷贝
++难度较高,我们这里仅作了解。++
深拷贝消耗大,浅拷贝又会出现"连带修改"和"多次析构"的问题。于是介于两者之间,引用计数与写时拷贝出现了。(这个技术C++11的移动语义完爆,现已过时,但是算法思想值得学习)
通俗的讲:引用计数记录的是同一块内存空间被多少个指针/引用指着。

写时拷贝的意思是**"受控共享 + 写前分离":**通俗的讲就是:我一般情况我不进行深拷贝,在要写入数据的时候进行深拷贝。以此实现性能优化。
VS下没有写时拷贝,但是Linux下有写时拷贝。在VS下引入写时拷贝也很容易,加入以下类似代码(不细讲):

四,VS下和g++下string的结构不同
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
vs下string的结构 string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义 string中字符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放。
- 当字符串长度大于等于16时,从堆上开辟空间。
cpp
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建 好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量 最后:还有一个指针做一些其他事情。 故总共占16+4+4+4=28个字节。
这样我们看原始视图就看得懂了:

g++下,string是通过写时拷贝实现的 ,string对象总共占4个字节,内部只包含了一个 指针, 该指针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
cpp
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};

如上,是string在g++下的一个示意图,我们的指针str如果要访问到h字符,就必须采用str+sizeof(_Rep_base);
五,一些转换接口
激进一点来说,以下圈出来的有用,其他全部冗余。

- stoi接口是string转化成int类型。
- stod接口时string转化成double类型。
- to_string是将类型转化成string类型。
六,实现构造函数的时候出现的一些问题
cpp
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里我们在初始化的时候使用的是"",这表示的是传递给字符串初始化的是一个'\0'。
为什么不初始化成nullptr?
因为虽然我们访问数据有operator[],但是在一些时候我们也需要解引用,如果在没有数据插入的时候解引用会导致空指针访问错误。
好了,本期内容到此结束,我是此方,我们下期再见。バイバイ!