news 2026/4/17 17:46:16

手写 string 必看:深拷贝、swap、写时拷贝全解析,彻底搞懂 string 底层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 string 必看:深拷贝、swap、写时拷贝全解析,彻底搞懂 string 底层

本文是std::string系列教程的收官之作。前文已全面覆盖 string 各类核心接口的使用方法,本篇将聚焦于实际开发中的常见坑点与疑难问题,深入底层原理进行解析,助你全面掌握 string 的使用与调试技巧。

目录

一、深拷贝的代码优化

1.1 深拷贝与浅拷贝的核心区别

1.2 深拷贝的传统写法与现代写法

二、3个关键的swap函数

2.1 C++ 标准全局std::swap模板

2.1.1 标准原型与头文件变迁

2.1.2 时间复杂度与性能分析

2.2 成员函数swap

2.3 重载的全局 std::swap 特化函数

2.4 一张表格看懂三种swap的区别

三、引用计数与写时拷贝(原理了解)

3.1 引用计数

3.2 写时拷贝

四、VS 与 g++ 下 string 底层结构的区别

五、字符串与数值转换接口(stoi/stod/to_string 等)

5.1 标准原型

5.1.1 字符串转数值

5.1.2 数值转字符串

5.2 核心功能与用法速查表

5.2.1 字符串转数值

5.2.2 数值转字符串

5.3 测试代码

六、自定义 string 构造函数:默认参数选用 "" 而非nullptr的底层原因

6.1 为何禁用 nullptr 作为默认参数?

6.2 空字符串 "" 的底层特性


一、深拷贝的代码优化

1.1 深拷贝与浅拷贝的核心区别

浅拷贝:仅按字节复制对象的内存布局,若对象包含指针成员,只会复制指针的地址值,而非指针指向的实际资源。通俗来说,就是 “拷贝指针”,会导致多个对象共享同一份底层资源,极易引发重复释放、内存泄漏或数据篡改等问题。

深拷贝:不只是复制对象本身,还会为指针成员重新分配独立内存,并完整复制指针指向的实际内容,确保每个对象拥有专属的资源副本。通俗来说,就是 “拷贝指针指向的内容”,从根源上避免资源共享带来的风险。

1.2 深拷贝的传统写法与现代写法

深拷贝的落地实现,核心依赖于类的拷贝构造函数拷贝赋值运算符重载。作为 std::string 类的核心能力,拷贝构造函数负责在创建新对象时,完整复制原对象的字符串资源。接下来我们就以 string 类的拷贝构造为例,拆解深拷贝的 “传统写法” 与 “现代写法”,对比两种实现思路的优劣。

string(const string& s) { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; }

这是最直观的深拷贝实现方式,核心逻辑是 “先开新空间,再拷贝数据,最后同步元数据”,确保新对象拥有独立的资源副本。我们用一张图解释它:

创建一个新的空间并指向,将原来空间的数据拷贝给新空间。修改_size和_capacity等数据。

为了简化代码、降低出错概率,现代写法利用 “复用已有构造函数创建临时对象 + 交换资源所有权” 的思路,将深拷贝的核心逻辑委托给其他函数完成。

string(const string& s) : _str(nullptr) // 只需把指针置空,防止 swap 后析构崩溃 { string tmp(s._str); // 委托构造完成深拷贝 swap(tmp); // 交换资源,tmp 析构时自动释放旧资源 }

我们同样用一张图来解释它:

通过 s._str 指向的字符串构造临时对象 tmp,再将 tmp 与目标对象的资源指针互换,把原本需要 _str 手动完成的深拷贝工作,交由 tmp 代理执行。

二、3个关键的swap函数

2.1 C++ 标准全局std::swap模板

这是 C++ 标准库最早提供的通用交换模板,设计初衷是 “一套代码适配所有类型”,但因未针对复杂对象做优化,在 C++11 之前是出了名的 “性能杀手”。

2.1.1 标准原型与头文件变迁

头文件归属:

  • C++98:定义在 <algorithm> 头文件中;
  • C++11 及以后:移至 <utility> 头文件(更符合 “通用工具” 的定位),但为了兼容旧代码,<algorithm>中仍可通过间接包含使用。
2.1.2 时间复杂度与性能分析

核心时间复杂度:

  • 通用场景(C++11 前):O(n),其中 n 为对象内部资源的大小;
  • 性能瓶颈根源:完全依赖 “拷贝构造 + 两次拷贝赋值” 实现交换,对包含堆内存的深拷贝类型(如 std::string、std::vector)极不友好。

为什么效率低下?

这是这个swap函数的底层。它一共进行了三次深拷贝。如果对于内置类型来说,影响不算什么,但是对于自定义或者是STL中的类型,效率一定会大打折扣。

具体开销拆解(以深拷贝类型为例)

// 通用模板的底层逻辑(C++11 前) template<class T> void swap(T& a, T& b) { T tmp(a); // 1. 拷贝构造:tmp 完整复制 a → 第1次深拷贝 a = b; // 2. 拷贝赋值:a 完整复制 b → 第2次深拷贝 b = tmp; // 3. 拷贝赋值:b 完整复制 tmp → 第3次深拷贝 }

对于长度为 1000 的 std::string,一次 std::swap 会触发 3 次内存分配 + 3 次内存释放 + 3000 个字符拷贝,开销是成员 swap 的成百上千倍。

C++11 之前,严禁用通用 std::swap 交换 std::string、std::vector 等大对象,必须用对应类的成员 swap;
补充:C++11 后的优化:

C++11 引入移动语义后,通用 std::swap 的底层逻辑已优化为 “移动构造 + 两次移动赋值”,将深拷贝转为资源转移,效率大幅提升;同时标准库为 std::string、std::vector 等容器特化了 std::swap,直接调用成员 swap,彻底解决了历史性能问题。

2.2 成员函数swap

这是std::string自带的专属高效交换接口,也是所有交换方式中底层性能最高的选择,专门用于交换两个字符串对象的内容。

仅交换两个字符串对象的内部资源句柄(包括指向字符数组的指针 _str、字符串大小 _size 以及容量 _capacity),完全不涉及任何字符数据的拷贝

// 简化的 string 底层结构示意 class string { char* _str; // 指向堆内存字符数组的指针 size_t _size; // 当前字符串长度 size_t _capacity; // 当前分配的容量 };

成员swap执行时,仅交换这三个成员的值:

  • 交换 _str 指针的指向;
  • 交换 _size 的数值;
  • 交换 _capacity 的数值。

无任何堆内存操作,无字符拷贝,时间复杂度为恒定的 O(1),与字符串长度完全无关。

2.3<string>重载的全局std::swap特化函数

这是C++标准库为std::string量身定制的特化交换函数,专门用于解决早期通用std::swap对深拷贝类型的性能灾难,是 C++11及以后交换std::string的标准通用方式。

作为全局std::swap的特化版本,完全委托给std::string的成员swap实现,内部仅交换两个对象的内部指针、_size 和 _capacity,不涉及任何字符拷贝,时间复杂度为 O(1)。

这是特化版本能“自动生效”的核心原因:当一个函数模板和一个完全匹配的非模板函数(特化 / 重载)同时存在时,C++ 编译器的重载决议机制会优先选择非模板函数。

// 1. 通用模板(慢) template<class T> void swap(T& a, T& b) { /* 三次深拷贝 */ } // 2. string 特化版本(快) void swap(string& a, string& b) noexcept { a.swap(b); // 直接委托成员函数 } int main() { string s1, s2; std::swap(s1, s2); // 编译器发现:有专门针对 string 的非模板 swap // 自动跳过通用模板,选择特化版本,避免三次深拷贝 }
2.4 一张表格看懂三种swap的区别
特性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))直接调用成员函数swap(O(1))
性能慢(大字符串性能极差)快(常数级效率)快(常数级效率)
使用建议不推荐用于string推荐首选推荐(语法更自然)

三、引用计数与写时拷贝(原理了解)

这部分内容涉及 std::string 的底层实现细节,难度稍高,我们仅作原理性了解即可。

深拷贝虽能保证资源安全,但内存分配与数据拷贝的时间、空间开销极大;

浅拷贝虽高效(仅复制指针),却易引发两个致命问题:

  • 一是多对象共享同一块内存时,修改其中一个会导致其他对象数据 “连带修改”;
  • 二是对象析构时会对同一块内存执行 “重复释放”,引发程序崩溃。

为了在 “安全” 与 “性能” 间找到平衡,引用计数(Reference Counting) 与写时拷贝(Copy-On-Write, COW) 技术应运而生。

3.1 引用计数

通俗来说,引用计数的核心是在堆内存块旁维护一个计数器,记录当前这块内存被多少个对象的指针/引用共同指向

  • 对象析构时,先将计数器减 1;
  • 只有当计数器减至 0 时,才真正释放堆内存。
3.2 写时拷贝

写时拷贝遵循 “受控共享 + 写前分离” 的原则:

  • 默认共享:对象拷贝时(如拷贝构造、拷贝赋值),不做深拷贝,只做浅拷贝(复制指针),并将引用计数 +1,让多个对象 “安全共享” 同一块内存;
  • 写前分离:只有当某个对象需要修改内存数据时,才为其单独分配新的堆空间、执行深拷贝,并将原内存的引用计数 -1,确保修改不会影响其他共享对象。

VS下没有写时拷贝,但是Linux下有写时拷贝。在VS下引入写时拷贝也很容易,加入以下类似代码(不细讲):

四、VS 与 g++ 下 string 底层结构的区别

注意:下述结构是在32 位平台下进行验证,32 位平台下指针占4 个字节

VS 下的string总共占28 个字节,内部结构稍微复杂一点,采用了短字符串优化(SSO)

核心设计:联合体 + 固定缓冲区
首先有一个联合体,用来定义string中字符串的存储空间:

  • 当字符串长度 < 16 时:使用内部固定数组存放(栈上,无堆开销)
  • 当字符串长度 ≥ 16 时:从堆上开辟空间
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;

完整结构组成:

  • 联合体 _Bx:16 字节(短字符串存储 / 长字符串指针)
  • size_t _Size:4 字节 —— 记录当前字符串有效长度
  • size_t _Capacity:4 字节 —— 记录总容量(堆分配时)
  • 额外指针 / 标记:4 字节 —— 用于管理、对齐等
  • 绝大多数场景下,字符串长度都小于 16

小于 16:直接用内部数组,不需要堆内存,效率极高
大于等于 16:才走堆分配,兼顾长短字符串场景
这就是 SSO(Short String Optimization,短字符串优化)。

这样我们看原始视图就看得懂了:

在 GCC(g++)旧版libstdc++标准库的实现中,std::string采用写时拷贝的经典设计方案。该实现下,std::string对象本身仅占用 4 字节栈空间,内部仅封装一个裸指针;该指针指向堆区分配的连续内存缓冲区,缓冲区内部除存储字符串有效字符序列外,还维护三个核心元数据字段:缓冲区总容量(capacity)、字符串有效长度(size)、引用计数(reference count)。

// g++ string 底层控制块(简化版) struct _Rep_base { size_type _M_length; // 字符串有效长度 size_type _M_capacity; // 堆空间总容量 _Atomic_word _M_refcount; // 原子引用计数(支持多线程) };

如上,是string在g++下的一个示意图,我们的指针str如果要访问到h字符,就必须采用str+sizeof(_Rep_base);

写时拷贝的触发逻辑

基于上述内存结构,GCC(g++)libstdc++ 中 std::string 的写时拷贝核心触发逻辑如下:
1. 拷贝构造/赋值重载(共享内存阶段)

  • 不执行性能损耗较高的深拷贝,仅复制对象内部的指针;
  • 堆空间控制块中的引用计数 _M_refcount 原子自增 1;
  • 多个 string 对象共享同一块堆内存,实现零拷贝高效复用。

2. 写入数据操作(触发拷贝阶段)
执行任何修改字符串内容的操作时(如 operator[]、at、append、erase、insert 等):

  1. 先检查引用计数 _M_refcount > 1,判断是否存在其他共享对象;
  2. 若计数大于 1(存在共享),强制触发深拷贝:
  • 为当前对象分配全新的独立堆空间;
  • 将原共享内存的字符串数据完整拷贝至新空间;
  • 原共享空间的引用计数原子自减 1(计数为 0 则释放内存);
  • 当前对象指针指向新空间,独立执行修改操作;

3.若计数等于1(无共享),直接在原内存上修改,无额外开销。

五、字符串与数值转换接口(stoi/stod/to_string 等)

5.1 标准原型
5.1.1 字符串转数值
// 整数类转换 int stoi(const string& str, size_t* idx = 0, int base = 10); long stol(const string& str, size_t* idx = 0, int base = 10); unsigned long stoul(const string& str, size_t* idx = 0, int base = 10); long long stoll(const string& str, size_t* idx = 0, int base = 10); unsigned long long stoull(const string& str, size_t* idx = 0, int base = 10); // 浮点类转换 float stof(const string& str, size_t* idx = 0); double stod(const string& str, size_t* idx = 0); long double stold(const string& str, size_t* idx = 0);
5.1.2 数值转字符串
// 普通字符串版本 string to_string(int value); string to_string(long value); string to_string(long long value); string to_string(unsigned value); string to_string(unsigned long value); string to_string(unsigned long long value); string to_string(float value); string to_string(double value); string to_string(long double value); // 宽字符串版本 wstring to_wstring(int value); wstring to_wstring(long value); wstring to_wstring(long long value); wstring to_wstring(unsigned value); wstring to_wstring(unsigned long value); wstring to_wstring(unsigned long long value); wstring to_wstring(float value); wstring to_wstring(double value); wstring to_wstring(long double value);
5.2 核心功能与用法速查表
5.2.1 字符串转数值
函数目标类型核心功能典型用法关键特性
stoiint字符串转 intstoi("123")→ 123支持进制转换,如stoi("101", nullptr, 2)→ 5
stollong int字符串转长整型stol("9876543210")处理大整数,避免 int 溢出
stoulunsigned long字符串转无符号长整型stoul("4294967295")处理无符号大整数
stolllong long字符串转长长整型stoll("1234567890123")处理超大整数
stoullunsigned long long字符串转无符号长长整型stoull("18446744073709551615")处理无符号超大整数
stoffloat字符串转单精度浮点数stof("3.14f")→ 3.14f兼容科学计数法"1.2e3"
stoddouble字符串转双精度浮点数stod("3.1415926")→ 3.1415926日常浮点转换首选
stoldlong double字符串转长双精度浮点数stold("1.234567890123456789L")高精度浮点转换
5.2.2 数值转字符串
函数目标类型核心功能典型用法关键特性
to_stringstring数值转普通字符串to_string(123)"123"to_string(3.14)"3.140000"支持所有基础数值类型,自动适配
to_wstringwstring数值转宽字符串to_wstring(123)L"123"适配宽字符场景(如 Windows API
5.3 测试代码
#include <iostream> #include <string> #include <stdexcept> using namespace std; int main() { // ===================== 1. 字符串转数值 ===================== string int_str = "12345"; string double_str = "3.1415926"; string hex_str = "1A3F"; string mix_str = "123abc"; // stoi 基础用法 + 进制转换 int num1 = stoi(int_str); int num2 = stoi(hex_str, nullptr, 16); // 16进制转10进制 cout << "=== 字符串转整数 ===" << endl; cout << "stoi(\"" << int_str << "\") = " << num1 << endl; cout << "stoi(\"" << hex_str << "\", nullptr, 16) = " << num2 << endl; // stod 浮点转换 + 部分转换 size_t idx; double num3 = stod(double_str); double num4 = stod(mix_str, &idx); // 提取前半部分数字 cout << "\n=== 字符串转浮点数 ===" << endl; cout << "stod(\"" << double_str << "\") = " << num3 << endl; cout << "stod(\"" << mix_str << "\", &idx) = " << num4 << ",未转换字符下标: " << idx << ",字符: " << mix_str[idx] << endl; // ===================== 2. 数值转字符串 ===================== int a = 67890; double b = 2.71828; long long c = 1234567890123LL; string s1 = to_string(a); string s2 = to_string(b); string s3 = to_string(c); wstring ws1 = to_wstring(a); cout << "\n=== 数值转字符串 ===" << endl; cout << "to_string(" << a << ") = \"" << s1 << "\"" << endl; cout << "to_string(" << b << ") = \"" << s2 << "\"" << endl; cout << "to_string(" << c << ") = \"" << s3 << "\"" << endl; wcout << "to_wstring(" << a << ") = \"" << ws1 << "\"" << endl; // ===================== 3. 异常捕获演示 ===================== cout << "\n=== 异常捕获测试 ===" << endl; try { // 溢出测试:int最大值为2147483647 stoi("9999999999"); } catch (const out_of_range& e) { cout << "捕获溢出异常: " << e.what() << endl; } return 0; }

字符串转数值用stoi/stod,数值转字符串用to_string

六、自定义 string 构造函数:默认参数选用""而非nullptr的底层原因

在实现自定义字符串类的构造函数时,我们将默认参数设为""而非nullptr,这并非随意选择,而是为了从根源避免空指针访问,保证字符串对象的合法性与鲁棒性。

class string { private: char* _str; size_t _size; size_t _capacity; public: // 带默认参数的构造函数 string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); } };
6.1 为何禁用nullptr作为默认参数?

构造函数内部依赖strlenstrcpy等C标准库函数,这类函数的底层逻辑必须对传入的指针进行解引用。若将默认参数设为nullptr,无参构造对象时,strlen(nullptr)会直接触发空指针解引用,导致程序崩溃。

我们访问数据有operator[],但是在一些时候我们也需要解引用,如果在没有数据插入的时候解引用会导致空指针访问错误。

同时,即便构造函数规避了该问题,初始化后的_strnullptr,后续调用operator[]c_str()等接口时,依然会因解引用空指针引发程序异常。

6.2 空字符串""的底层特性

""是 C/C++ 合法的空字符串字面量,存储在程序只读数据段,本质是一个仅包含结束符\0的字符数组。它是有效的非空指针strlen("")会安全返回 0,strcpy也能正常完成拷贝,保证字符串始终以\0结尾,完全符合 C 风格字符串的规范。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 15:43:01

moto 手机必看!官方 log 抓取教程,排查故障一步到位

很多 moto 手机用户在遇到卡顿、闪退、异常重启等问题时&#xff0c;往往不知道从何入手排查。其实抓取系统日志是定位故障最直接、最有效的方式&#xff0c;既能快速锁定问题根源&#xff0c;也能为售后检测提供精准依据&#xff0c;避免盲目操作浪费时间。 这篇来自联想官方…

作者头像 李华
网站建设 2026/4/14 15:43:00

Qwen3.5-9B-AWQ-4bit镜像部署:免编译、免依赖、Web界面开箱即用

Qwen3.5-9B-AWQ-4bit镜像部署&#xff1a;免编译、免依赖、Web界面开箱即用 1. 镜像概述 Qwen3.5-9B-AWQ-4bit是一个支持图像理解的多模态模型&#xff0c;能够结合上传图片与文字提示词&#xff0c;输出中文分析结果。这个镜像特别适合需要处理图片识别、场景描述、图片问答…

作者头像 李华
网站建设 2026/4/14 15:42:57

2025届毕业生推荐的十大AI学术平台推荐榜单

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek DeepSeek身为智能写作辅助工具&#xff0c;于学术论文撰写里有着显著应用价值&#xff0c;研…

作者头像 李华
网站建设 2026/4/14 15:42:05

2026年软件测试工程师的终极晋升路线图

在AI与云原生技术深度融合的2026年&#xff0c;软件测试领域正经历前所未有的变革。测试工程师不再局限于缺陷发现者的角色&#xff0c;而是转型为质量保障的架构师和业务价值的驱动者。随着DevOps和智能测试工具的普及&#xff0c;测试从业者面临巨大机遇&#xff1a;掌握前沿…

作者头像 李华
网站建设 2026/4/14 15:39:34

华芯微特SWM341S调试实录:SDRAM映射SPI Flash存字库,串口DMA配置那些坑

华芯微特SWM341S嵌入式开发实战&#xff1a;SDRAM资源优化与外设配置避坑指南 在嵌入式系统开发中&#xff0c;资源管理和外设配置往往是决定项目成败的关键因素。华芯微特SWM341S作为一款内置8MB SDRAM的MCU&#xff0c;为图形界面开发提供了硬件基础&#xff0c;但如何高效利…

作者头像 李华