一、std::string 的底层实现

1、深拷贝

1 class String{
2 public:
3 String(const String &rhs):m_pstr(new char[strlen(rhs) + 1]()){
4 }
5 private:
6 char* m_pstr;
7 };

这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。如果对一块空间只是进行读,就没必要采用深拷贝,当需要进行写的时候,再使用深拷贝申请新的空间

2、写时复制 (浅拷贝+引用计数)

当只是进行读操作时,就进行浅拷贝,如果需要进行写操作的时候,再进行深拷贝;再加一个引用计数,多个指针指向同一块空间,记录同一块空间的对象个数

  • std::string之写时复制

当两个std::string发生复制或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当修改其中一个字符串内容时,才执行真正的复制。

  • 引用计数存在哪里?

堆区,为了好获取将将引用计数与数据放在一起,并且最好在数据前面,这样当数据变化的时候不会移动引用计数的位置

  1 class String{
2 public:
3 String():m_str(new char[5]() + 4){
4 //new 5 表示4个字节存放引用计数,一个字节存放\0 +4 是为了将指针指向字符串位置
5 cout << "String()" << endl;
6 //引用计数初始化为1 (字符指针向前偏移,指向引用计数,并且转为int型指针,解引用得引用计数的值)
7 InitRefCount();
8 }
9
10 String(const char* str):m_str(new char[strlen(str) + 5] + 4){ //有参构造
11 //申请空间大小,4B引用计数,还有\0
12 cout << "Sting(const char *)" << endl;
13 strcpy(m_str, str);
14 InitRefCount();
15 }
16
17 String(const String &rhs):m_str(rhs.m_str){ //浅拷贝
18 cout << "String(const String &rhs)" << endl;
19 IncreaseRefCount();
20 }
21
22 String &operator=(const String &rhs){ //赋值
23 if(this != &rhs){
24 DecreaseRefCount();
25 if(0 == getRefcount()){ //如果该空间引用计数为0,删掉该空间
26 delete [] (m_str - 4);
27 }
28 m_str = rhs.m_str; //浅拷贝,引用计数++
29 IncreaseRefCount();
30 }
31 return *this;
32 }
33 private:
34 //CharProxy中去重载=与<<运算符,争对读写有不同的操作
35 class CharProxy{
36 public:
37 CharProxy(String &self, size_t idx):_self(self), _idx(idx){
38
39 }
40 //写操作
41 char &operator=(const char &ch); //string是不完整类型,所以要在类外实现
42 //读操作
43 friend std::ostream &operator<<(std::ostream &os, const CharProxy &rhs);
44
45 private:
46 String &_self; //CharProxy在string里面写,是不完整类型,无法创建对象
47 size_t _idx; //self找到m_pstr,可以去下标,去除string中每一个字符
48 };
49
50 public:
51
52 CharProxy operator[](size_t idx){
53 return CharProxy(*this, idx); //临时对象,所以不能返回引用
54 }
55
56 #if 0 这样去重载[],无法区分读写操作 若对s1[0]进行读,依然会修改引用计数
57 char &operator[](size_t idx){
58 if(idx < size()){
59 if(getRefcount() > 1){ //共享空间,[idx]修改时进行深拷贝
60 char *tmp = new char[size() + 5]() + 4; //深拷贝
61 strcpy(tmp, m_str);
62 DecreaseRefCount();
63 m_str = tmp; //浅拷贝
64 InitRefCount();
65 }
66 return m_str[idx];
67 }else{
68 static char charNull = '\0';
69 return charNull;
70 }
71 }
72 #endif
73 ~String(){
74
75 DecreaseRefCount();
76 if(0 == getRefcount()){
77
78 delete [] (m_str - 4);
79 }
80 }
81
82 int getRefcount() const{ //获取引用计数
83 return *(int*)(m_str - 4);
84 }
85
86 const char* c_str() const{
87 return m_str;
88 }
89
90 size_t size() const{
91 return strlen(m_str);
92 }
93
94 friend std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs);
95 friend std::ostream &operator<<(std::ostream &os, const String &rhs);
96 private:
97 char *m_str;
98 /* static int refCount; //静态变量为所有对象所共享,无法表示不同对象的引用计数 */
99
100 void InitRefCount(){ //初始化引用计数
101 *(int*)(m_str - 4) = 1;
102 }
103
104 void IncreaseRefCount(){ //引用计数++
105 ++*(int*)(m_str - 4);
106 }
107
108 void DecreaseRefCount(){ //引用计数--
109 --*(int*)(m_str - 4);
110 }
111 };
112
113
114 std::ostream &operator<<(std::ostream &os, const String &rhs){
115 if(rhs.m_str){
116 os << rhs.m_str;
117 }
118 return os;
119 }
120
121 //写操作
122 char &String:: CharProxy::operator=(const char &ch){
123 if(_idx < _self.size()){
124 if(_self.getRefcount() > 1){ //共享空间,[idx]修改时进行深拷贝
125 char *tmp = new char[_self.size() + 5]() + 4; //深拷贝
126 strcpy(tmp, _self.m_str);
127 _self.DecreaseRefCount();
128 _self.m_str = tmp; //浅拷贝
129 _self.InitRefCount();
130 }
131 _self.m_str[_idx] = ch; //真正的写操作
132 return _self.m_str[_idx];
133 }else{
134 static char charNull = '\0';
135 return charNull;
136 }
137
138 }
139
140 std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs){
141 os << rhs._self.m_str[rhs._idx];
142 return os;
143 }
  • 关于重载 [ ]运算符遇到的问题

为了区分下标访问运算符的读写操作,s3 [0] = ' H '   cout << s1 [ 0 ]  << endl ; 所以需要对 =  与 << 进行重载,重载运算符时,必须有一个是类类型,所以再写一个类 CharProxy ,在该类中重载

把下标访问运算符中的返回类型由 char & 变为 CharProxy

CharProxy中为了操作String中的一个个字符,用到数据成员 String &(用引用是操作的String本身且String为不完整类型)与 size_t

之后对写操作与读操作进行重载,因为String在CharProxy中是不完整类型,所以要在类外实现

测试代码:

 1 void test(){
2
3 String s1("hello");
4 cout << "s1 = " << s1 << endl;
5 cout << "s1.getRefcount()" << s1.getRefcount() << endl;
6 printf("s1 address is %p\n", s1.c_str());
7
8 cout << endl << endl;
9 String s2 = s1;
10 cout << "s1 = " << s1 << endl;
11 cout << "s2 = " << s2 << endl;
12 cout << "s1.getRefcount()" << s1.getRefcount() << endl;
13 cout << "s2.getRefcount()" << s2.getRefcount() << endl;
14 printf("s1 address is %p\n", s1.c_str());
15 printf("s2 address is %p\n", s2.c_str());
16
17 cout << endl << endl;
18 String s3("world");
19 cout << "s3" << s3 << endl;
20 cout << "s3.getRefcount()" << s3.getRefcount() << endl;
21 printf("s3 address is %p\n", s3.c_str());
22
23 cout << endl << endl;
24 s3 = s1;
25 cout << "s1 = " << s1 << endl;
26 cout << "s2 = " << s2 << endl;
27 cout << "s3 = " << s3 << endl;
28 cout << "s1.getRefcount()" << s1.getRefcount() << endl;
29 cout << "s2.getRefcount()" << s2.getRefcount() << endl;
30 cout << "s3.getRefcount()" << s3.getRefcount() << endl;
31 printf("s1 address is %p\n", s1.c_str());
32 printf("s2 address is %p\n", s2.c_str());
33 printf("s3 address is %p\n", s3.c_str());
34
35 cout << endl << "对s3执行写操作" << endl;
36 //s3.operator[](idx)
37 //CharProxy = char;
38 s3[0] = 'H';
39 cout << "s1 = " << s1 << endl;
40 cout << "s2 = " << s2 << endl;
41 cout << "s3 = " << s3 << endl;
42 cout << "s1.getRefcount()" << s1.getRefcount() << endl;
43 cout << "s2.getRefcount()" << s2.getRefcount() << endl;
44 cout << "s3.getRefcount()" << s3.getRefcount() << endl;
45 printf("s1 address is %p\n", s1.c_str());
46 printf("s2 address is %p\n", s2.c_str());
47 printf("s3 address is %p\n", s3.c_str());
48
49 cout << endl << "对s1[0]执行读操作" << endl;
50 //cout << CharProxy
51 cout << "s1[0] = " << s1[0] << endl;
52 cout << "s1 = " << s1 << endl;
53 cout << "s2 = " << s2 << endl;
54 cout << "s3 = " << s3 << endl;
55 cout << "s1.getRefcount()" << s1.getRefcount() << endl;
56 cout << "s2.getRefcount()" << s2.getRefcount() << enl;
57 cout << "s3.getRefcount()" << s3.getRefcount() << endl;
58 printf("s1 address is %p\n", s1.c_str());
59 printf("s2 address is %p\n", s2.c_str());
60 printf("s3 address is %p\n", s3.c_str());
61 }

3、短字符串优化

核心思想:发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大(小字符串复制指针,大字符串复制字符串)

C++Day09 深拷贝、写时复制(cow)、短字符串优化的更多相关文章

  1. Rust写时复制Cow<T>

    写时复制(Copy on Write)技术是一种程序中的优化策略,多应用于读多写少的场景.主要思想是创建对象的时候不立即进行复制,而是先引用(借用)原有对象进行大量的读操作,只有进行到少量的写操作的时 ...

  2. c++ string写时复制

    string写时复制:将字符串str1赋值给str2后,除非str1的内容已经被改变,否则str2和str1共享内存.当str1被修改之后,stl才为str2开辟内存空间,并初始化. #include ...

  3. JAVA中写时复制(Copy-On-Write)Map实现

    1,什么是写时复制(Copy-On-Write)容器? 写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改.修改 ...

  4. fork()和写时复制

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

  5. Linux的fork()写时复制原则(转)

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

  6. Linux进程管理——fork()和写时复制

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

  7. 写时复制和fork,vfork,clone

    写时复制 原理: 用了“引用计数”,会有一个变量用于保存引用的数量.当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时, ...

  8. Java进阶知识点6:并发容器背后的设计理念 - 锁分段、写时复制和弱一致性

    一.背景 容器是Java编程中使用频率很高的组件,但Java默认提供的基本容器(ArrayList,HashMap等)均不是线程安全的.当容器和多线程并发编程相遇时,程序员又该何去何从呢? 通常有两种 ...

  9. 用户空间缺页异常pte_handle_fault()分析--(下)--写时复制【转】

    转自:http://blog.csdn.net/vanbreaker/article/details/7955713 版权声明:本文为博主原创文章,未经博主允许不得转载. 在pte_handle_fa ...

  10. Redis持久化之父子进程与写时复制

    之所以将Linux底层的写时复制技术放在Redis篇幅下,是因为Redis进行RDB持久化时,BGSAVE(后面称之为"后台保存")会开辟一个子进程,将数据从内存写进磁盘,这儿我产 ...

随机推荐

  1. nrf9160做主控连接阿里云——(mqtt_simple例程)

    简介:基本每一个云都支持MQTT,这种轻量级协议在数据量不大的应用上是一个很好的选择.上一篇博客使用SLM例程去连接了阿里云,本次使用mqtt_simple去连接云进行测试,关于一些已近在前面文章中演 ...

  2. 论文笔记 - Active Learning by Acquiring Contrastive Examples

    Motivation 最常用来在 Active Learning 中作为样本检索的两个指标分别是: 基于不确定性(给模型上难度): 基于多样性(扩大模型的推理空间). 指标一可能会导致总是选到不提供有 ...

  3. (线段树) P4588 数学计算

    小豆现在有一个数 x,初始值为 1.小豆有 QQ 次操作,操作有两种类型: 1 m:将 x变为 x × m,并输出 x mod M 2 pos:将 x 变为 x 除以第 pos次操作所乘的数(保证第  ...

  4. 异步编排 Spring(线程池)

    目录 异步编排 CompletableFuture 的详解 代码测试 配置类的引入 Demo1 Demo2 CompletableFuture的async后缀函数与不带async的函数的区别 Thre ...

  5. linux下进程的实际用户ID(有效组)和有效用户ID(有效组ID)

    实际用户ID(实际组ID):标识当前用户(所属组)是谁,当用户登陆时取自口令文件. 有效用户ID(有效组ID):用来决定我们(当前进程)对文件的访问权(即实际该进程的是以那个用户运行的). 一般情况下 ...

  6. dlv远端调试go的问题

    1.golang采用dlv 时提示 "could not launch process: could not open debug info " 在用dlv 远程debug 代码时 ...

  7. 关于 .NET 在不同操作系统中 IO 文件路径拼接方法结升级 .NET 7 后注意到的一个小坑

    .NET 现在支持跨平台这件事情已经是众所周知的特点了,虽然平台整体支持跨平台了,但是我们的代码如果真的想要实现跨平台运行其实还是有些小细节要注意的,今天想要记录分享的就是关于 文件I/O操作时路径的 ...

  8. PP视频(PPTV聚力)web接口分析

    前言 前几天我想看一个番剧, 正好搜索到了 PP视频,我才知道PP视频就是PPTV聚力,我想把番剧下载下来,结果发现视频竟然不是m3u8格式,而是多段mp4,所以简单的写了个脚本,可以在不登录的情况下 ...

  9. JavaScript入门③-函数(2)原理{深入}执行上下文

    00.头痛的JS闭包.词法作用域? 被JavaScript的闭包.上下文.嵌套函数.this搞得很头痛,这语言设计的,感觉比较混乱,先勉强理解总结一下. 为什么有闭包这么个东西?闭包包的是什么? 什么 ...

  10. go-carbon 1.5.3 版本发布, 修复已知 bug 和新增俄罗斯语翻译文件

    carbon 是一个轻量级.语义化.对开发者友好的golang时间处理库,支持链式调用. 目前已被 awesome-go 收录,如果您觉得不错,请给个star吧 github.com/golang-m ...