news 2026/10/19 2:12:01

【C++初阶】模拟实现string类的补充——编码、深浅拷贝的传统现代写法及写时拷贝

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++初阶】模拟实现string类的补充——编码、深浅拷贝的传统现代写法及写时拷贝

◆ 博主名称:Severus-Yuan
◆ 来自泥潭,充满野心,强大冷静,从不后悔
◆ 欢迎光临我的思考天地,期待我们在这刹那的同频共振

Severus-Yuan的专栏链接🔗
C语言C语言
初阶数据结构与算法初阶数据结构与算法
数据结构算法题数据结构算法题
C++C++

文章目录

    • (一)编码(了解即可)
    • (二)深浅拷贝的传统现代写法
      • (1)传统写法
      • (2)现代写法
        • 1.拷贝构造
          • 继续优化:
        • 2.赋值
          • 继续优化:
        • 3.测试
      • (3)全局swap和类成员swap
    • (三)引用计数的写时拷贝(了解即可)
      • vs和g++下string结构的说明:

(一)编码(了解即可)

内存和磁盘中只有0/1,如何用0/1去表示世界上的文字呢?

在C语言中我们了解了ASCII码,ASCII码就是一种编码,编码是值和符号的一种映射关系。


ASCII值在计算机中存的实际就是八个比特位表示的值。(这八个比特位先换成整型,再通过整型去ASCII表找对应的字符。)

但很显然,只有ASCII码是不够的,全世界有这么多文字,又该怎么办呢?这时候就出现了unicode(编码国际所有文字)。

这也能说明为啥string是模板了,因为字符的编码方案有多种,那么对应的类的实例化就有多种。

还有一种编码是GBK(编码中日韩文字)。



(二)深浅拷贝的传统现代写法

(1)传统写法

在博客【C++初阶】string类的模拟实现,近三万字呕心沥血详细分析!中,深拷贝的拷贝函数、赋值运算符重载函数都是自己手动开辟的内存空间,自己完成拷贝或赋值操作。

//string.h//拷贝构造函数string(conststring&s){_ptr=newchar[strlen(s._ptr)+1];strcpy(_ptr,s._ptr);//会拷贝'\0'_capacity=s._capacity;_size=s._size;}
//赋值string&operator=(conststring&s){if(s!=*this){delete[]_ptr;_ptr=newchar[strlen(s._ptr+1)];strcpy(_ptr,s._ptr);_size=s._size;_capacity=s._capacity;}return*this;}



(2)现代写法

现代写法下的深拷贝倾向于说把活外包出去,让别人帮我们干活。

1.拷贝构造
//string.h//*this(s)string(conststring&s){stringtmp(s._ptr);//交换指针、_size和_capacityswap(_ptr,tmp._ptr);swap(_size,tmp._size);swap(_capacity,tmp._capacity);}

分析一下为什么要这么写:

此处拷贝函数的目的是将s的内容拷贝给this指向的内容。首先,我们用s的_ptr指针构造一个tmp的string类对象(有单参数构造函数,此处为隐式类型转换),tmp和s的_ptr指针都指向相同的内容,但_ptr不同,_size相同但是_capacity不一定相同。然后再交换this->_ptr和tmp._ptr(地址交换,_ptr指向的内容没变)、this->_size和tmp._size、this->_capacity和tmp._capacity,这样s的内容就通过tmp间接拷贝给了this。

注意:拷贝构造本质上就是构造,这里新对象的构造调用的只有拷贝构造,进入现代写法的拷贝构造函数之后,并未对*this对象new空间,所以_ptr/_size/_capacity都是随机值。在交换之后,tmp得到的就是这三个随机值。_size和_capacity倒是问题不大,但tmp出函数了是要析构的,而且还是调用手动实现的析构函数(手动实现的析构函数才会对_ptr指向的堆内存进行释放)。delete[]不能是随机值,但可以是nullptr,因为对nullptr的情况做了特殊处理。

所以我们需要在声明处给缺省值。

//string.hnamespaceyuan{classstring{private:char*_ptr=nullptr;size_t _capacity=0;size_t _size=0;conststaticsize_t npos;}};

图示分析:

继续优化:

实现现代写法的拷贝构造函数时连着使用了三个swap函数,不如我们直接实现一个专门用来交换string类对象的_ptr/_size/_capacity的swap函数。

//string.hvoidswap(string&s){std::swap(_ptr,s._ptr);std::swap(_size,s._size);std::swap(_capacity,s._capacity);}//this(s)string(conststring&s){stringtmp(s._ptr);//交换指针、_size和_capacityswap(tmp);}

2.赋值
//string.hvoidswap(string&s){std::swap(_ptr,s._ptr);std::swap(_size,s._size);std::swap(_capacity,s._capacity);}//*this = sstring&operator=(conststring&s){if(&s!=this){//构造stringtmp(s._ptr);//拷贝构造//string tmp(s);swap(tmp);}return*this;}

现代写法下的赋值运算符重载函数和现代写法下的拷贝构造的逻辑简直一模一样,只是说拷贝构造tmp出函数之后析构的是空,但赋值tmp出函数之后是把s的_ptr原来指向的空间真的释放了。

继续优化:
//string.hstring&operator=(string tmp){//不用再考虑自己给自己赋值了,等进入函数内部,早拷贝完了swap(tmp);return*this;}



3.测试
//test.cppnamespaceyuan{voidtest15(){strings15("hello");strings16(s15);cout<<s16<<endl;strings17("bank");strings18("yuan");cout<<s18<<" ";s18=s17;cout<<s18<<endl;}}intmain(){yuan::test15();return0;}



(3)全局swap和类成员swap

全局:


string类对象的全局swap是由库里的swap模板实例化而来的,模板如下:

类成员:

类成员的swap函数就和我们在这手动实现的swap函数逻辑一样,交换两个string类对象的_ptr、_size和_capacity。

但全局swap函数代价就太大了,根据模板可知,会进行三次深拷贝。


不过其实调用哪个没差,根据string类的全局swap可知,其实最后也会调用string类的成员函数swap,可以理解成在模板和现成的成员函数之间,那肯定还是调现成的,除非模板能生成更匹配的函数。



(三)引用计数的写时拷贝(了解即可)

浅拷贝会带来改值、覆盖、析构两次或内存泄漏等多个问题,所以我们选择深拷贝,但是深拷贝的代价是要申请一块新空间,我们不禁想:所有情况下都必须要进行深拷贝吗?有没有即使有_ptr但也可以浅拷贝的情况呢?

其实是有的。

对于析构两次或内存泄漏的问题,使用浅拷贝完全没问题。但需要额外加一个引用计数,用于记录有多少个对象指向了同一块内存空间。

  • 析构两次:譬如此时有n个对象指向了同一块内存空间,那么引用计数就是n,每当一个对象不指向这块空间时,就n–,只有当n减到0时的那个对象才会调用析构函数,完成对内存空间的回收释放;
  • 内存泄漏:进行赋值前的赋值空间失去了唯一的指向对象,那n–得0,会调用析构函数完成对内存空间的回收释放,这样就不会造成内存泄漏的问题了。

对于改值、覆盖的问题,如果引用计数为1,那就可以放心修改,但是如果引用计数>1就不能原空间更改了,还是要老老实实地创建空间进行深拷贝。这也就是为啥叫做引用计数的写时拷贝——等要写值(修改)的时候才进行深拷贝。

既然有时还是得深拷贝,那为啥还费这么大的劲搞一个引用计数的写时拷贝出来?可以理解成博弈思想吧,但凡出现不用改值的情况可以用浅拷贝的情况,那就是赚了。

vs和g++下string结构的说明:




初学C++,如有错误,欢迎支持。

——end——

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

Linux ---- 线程同步与互斥

1 线程互斥1-1 进程线程间的互斥相关背景概念临界资源(共享资源)&#xff1a;多线程执行流被保护的共享的资源就叫做临界资源 临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区 互斥&#xff1a;任何时刻&#xff0c;互斥保证有且只有一…

作者头像 李华
网站建设 2026/10/19 2:11:32

Agent 的中断与恢复:跑到一半能停下来吗

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录Agent 的中断与…

作者头像 李华
网站建设 2026/10/19 2:11:30

快速排序(力扣基础)(递归)

核心目标&#xff1a;在无序数组中&#xff0c;不用完全排序&#xff0c;找到第 k 大 / 第 k 小元素 推荐&#xff1a;数据结构合集 - 快速排序(算法过程, 效率分析, 稳定性分析)_哔哩哔哩_bilibili int quickselect(int[] nums, int l, int r, int k) {if (l r) return num…

作者头像 李华
网站建设 2026/10/19 2:11:28

基于TRL库的SFT微调实现

一.TRL库的介绍 TRL库提供一系列的Trainer&#xff0c;实现大语言模型的微调和对齐训练。 使用TRL进行SFT微调的时候&#xff0c;需要先配置SFTTrainer实例。SFTTrainer包含了所需要训练的model,tokenizer以及SFTConfig参数。配置完成之后&#xff0c;即可调用SFTTrainer实例…

作者头像 李华
网站建设 2026/10/19 2:10:49

Flash 缓存没刷新读旧值——写了缓存未回写,写后 invalidate/回写

Flash 缓存没刷新读旧值——写了缓存未回写&#xff0c;写后 invalidate/回写文章标签&#xff1a;#嵌入式开发 #单片机 #Flash #OTA升级 #STM32 #扇区 #擦写 #缓存 #回写一、开篇&#xff1a;那个"写进去了却读不出"的缓存 做 STM32F7/H7&#xff08;带 DCache/ICac…

作者头像 李华
网站建设 2026/10/19 2:10:10

联调时我当了太久人肉快递员,最后让两个 agent 自己聊上了

开发最耗人的地方&#xff0c;我一直觉得不是写代码。 前几天有个活&#xff1a;一部分功能跑在无影云上&#xff0c;手机插在本地这台 PC 上。做起来是这样——云上跑一遍&#xff0c;看日志&#xff0c;复制结论&#xff1b;切到本地&#xff0c;接上手机&#xff0c;再跑一遍…

作者头像 李华