一、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. Codeforces Round #830 (Div. 2) A-D

    比赛链接 A 题解 知识点:贪心,数论. 先求出序列最大公约数 \(d\) ,如果为 \(1\) 直接输出 \(0\) . 否则,尝试用最后一个数操作, \(gcd(d,n) = 1\) 则可以,花费 ...

  2. SpringCloud微服务实战——搭建企业级开发框架(四十八):【移动开发】整合uni-app搭建移动端快速开发框架-使用第三方UI框架

      uni-app默认使用uni-ui全端兼容的.高性能UI框架,在我们开发过程中可以满足大部分的需求了,并且如果是为了兼容性,还是强烈建议使用uni-ui作为UI框架使用.   如果作为初创公司,自 ...

  3. SpringBoot 过滤器和拦截器

    过滤器 实现过滤器需要实现 javax.servlet.Filter 接口.重写三个方法.其中 init() 方法在服务启动时执行,destroy() 在服务停止之前执行. 可用两种方式注册过滤器: ...

  4. Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化

    承接上文:Java函数式编程:一.函数式接口,lambda表达式和方法引用 这次来聊聊函数式编程中其他的几个比较重要的概念和技术,从而使得我们能更深刻的掌握Java中的函数式编程. 本篇博客主要聊聊以 ...

  5. 【深入浅出 Yarn 架构与实现】2-1 Yarn 基础库概述

    了解 Yarn 基础库是后面阅读 Yarn 源码的基础,本节对 Yarn 基础库做总体的介绍.并对其中使用的第三方库 Protocol Buffers 和 Avro 是什么.怎么用做简要的介绍. 一. ...

  6. Unity之"诡异"的协程

    为什么说是诡异的协程呢?首先从一个案例说起吧,示例如下: 游戏目标:让小车进入到对应颜色屋子里,即可获得一分.(转弯的道路可控)   为了让小车能够平滑转弯,小车的前进方向需要和车子的位置与圆心组成的 ...

  7. jvm调优思路及调优案例

    jvm调优思路及调优案例 ​ 我们说jvm调优,其实就是不断测试调整jvm的运行参数,尽可能让对象都在新生代(Eden)里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时 ...

  8. C语言实验手册

    在三位整数(100~999)中寻找符合条件的整数,并以此从小到大存到数组当中,它既是完全平方数,又是两位数字相同,例如144,676等. #include<stdio.h> #includ ...

  9. linux 使用ACR122U-A9设备读写M1卡

    前言 很久之前我在windows用过这个ACR122U-A9设备, 还挺好用,但是换了linux后,突然想又想用这个设备又是一顿折腾- 关于这个设备 其实这个设备只能读取M1卡(水卡这种),当时什么都 ...

  10. 漫谈计算机网络:网络层 ------ 重点:IP协议与互联网路由选择协议

    面试答不上?计网很枯燥? 听说你学习 计网 每次记了都会忘? 不妨抽时间和我一起多学学它 深入浅出,用你的空闲时间来探索计算机网络的硬核知识! 博主的上篇连载文章<初识图像处理技术> 图像 ...