C++中string的实现原理

背景

当我刚开始学习C++,对C还是有一部分的了解,所以以C的思维去学C++,导致我很长一段时间的学习都处于一个懵逼的状态,C++的各种特性,标准库,模板还有版本的迭代,简直是欲仙欲死。

后来在论坛中就有热心的朋友们出招了:你得放弃C的思维去学C++!!嗯,说得好有道理,这就去试试!!

但是我又发现一个问题,不用C的思维学C++,难道我以撸铁(博主业余喜欢健身)的思维来学C++?又在论坛中一问,原来是要用面向对象的思维来学习。

问题依然没有解决,因为博主压根就不知道面向对象的思维是个啥思维?难道是学C++还要先找个对象?那就没时间学习了!!要知道有对象的程序员处于程序员鄙视链的顶端。

结果问来问去,啥思维都没搞懂,反倒是浪费了时间,回过头来才发现,学习这东西,还是得动手动脑去学,思维转换也不是个拨码开关,看得写得多了,自然会有一些心得,在不断的积累中量变成为质变,而别人给的建议往往只能是过眼云烟,当时觉得很有道理,结果回头就忘.

蛋扯得有点长了,我们来回归正题:

C++中string类的实现原理简述

请注意简述这两个字,因为博主目前依旧处于C++初级学习阶段,对C++的理解仍不够透彻,对于底层的原理尤其是内存实现部分还在进行艰难的研究,不敢妄称详解。但是博主可以保证的是,所有的内容都是经过上机实验的,可能不全面,但是已经是非常努力地保证正确性。

string

定义

string的内容其实就是C中的字符串,在C中是char*类型,而在C++中变成了string类型,定义是这样的:

C:
char *str="downey";
C++:
string str="downey";
string str("downey");

操作

对于C中字符串的操作,其实就是对str指针的操作,增删改查都是在内存的基础上进行操作,非常高的自由度,但是对新手并不友好,因为一旦操作失误,将会引起内存上的问题,而内存上的问题往往是很难进行debug的。

而在C++中,string类提供很多內建方法,可以无害地进行增删改查,基本覆盖用户的所有操作,用户并不需要了解底层实现,拿来直接用,也不会造成内存的问题,对初、中级水平程序员非常友好,而且还有一些亮点(仅列出一些博主认为主要的亮点,欢迎指正与补充):

  • 支持运算符,'+'运算符可以直接拼接字符串,以及一些內建的查找替换方法,使用非常方便
  • 迭代器的发明使得数据与操作分离,对程序的移植、接口扩展和维护都是很有优势的。
  • 支持变长。

关于第一点以及其他string方法,其实就是封装的问题,在C标准库中也能找到相应的函数,但是由于关联性不强,强大的C标准库经常被程序员们忽略。

而第二点中,迭代器其实也可以看成是一种泛型指针,由类本身来实现,但是在C中实现泛型是比较麻烦的,很多程序员对void* 以及其转换简直是恨之入骨,C++在这一点上算是一个突破。

而第三点中的C++变长特性,这个特性相对于C而言是个巨大的优势,在C中如果直接定义一个字符串如:

char *str="downey";

这个字符串默认被编译器识别为const类型,也就无法进行写操作。如果要进行写操作,我们就得将其定义成数组:

char str[]="downey";

但是这个数组的长度是固定的,想删除可以,但是如果想增加一个字符,就会发生数组越界的情况,导致内存问题,好像也很难找到一个好的办法来用C语言来实现这个问题。

C++是如何实现字符串变长的

我们可以继续以沿着C语言的思路来思考怎么样实现变长数组:

V1.0

实现

既然数组是固定的无法扩展,那么我们就用动态申请内存的方式来存字符串,在字符串定义的时候申请一片内存空间,在需要增加字符的时候再申请内存,放置增加的那一部分。

问题

这样有一个弊端,内存动态申请是个十分费时的操作,这样频繁地申请刚好合适的大小在空间上能够达到最优,但是在执行效率上是一个非常大的损失,在目前普遍接受的时间复杂度重要性>空间复杂度重要性的软件环境下,这个是完全不能接受的。


V1.1

实现

在V1.0上做改版:在第一次申请的时候就申请一块大一些的内存,比如初始化的字符串长度为10,那我就申请一块大小为20的内存,如果用完了就又申请一块比目前需要的空间大一些的内存空间,这种情况下,会浪费一部分空间,只要策略得当,浪费的空间是可以接受的。

问题

但是仔细一想,这样又有问题了,经过多次字符串变长之后,一个字符串中的内容很可能对应好几个分段的存储地址,这样会给底层实现带来更大的难度以及更多的操作时间,比如遍历、删除、内存回收等操作。


V1.2

实现

既然上述的版本都存在内存不连续的问题,那我就在V1.1的版本上做相应改进,让它实现内存连续。具体的实现方法为:

还是在第一次申请的时候就申请一块大一些的内存,比如初始化的字符串长度为10,那我就申请一块大小为20的内存,如果这块内存用完了而字符串还需要扩展,那我就去找一块更大的内存,能够同时容纳需要的内存空间,而且还有一些余量,直接将字符串整体迁移到新内存空间中,放弃原来那部分内存空间,这样就实现了字符串的内存连续。

问题

仔细一想,这样又存在一个问题,如果在操作之前未扩展的字符串时,我使用的是指针访问,那在字符串扩展之后,字符串存储地址已经改变了,那这个指针就成了无效指针,再次操作它甚至可能导致严重的内存问题。


V1.3

博主智商余额已经不足,想不出更好的实现方法,欢迎各路大神补充....

STL的string实现

事实上,在STL的实现中,用的就是上述的V1.2版本,在C++ reference 中,明确指出base_string类型是连续存储的,而string继承自base_string类型,实现也是一样的,尽管确实存在指针失效的问题,但是在找不到理想的解决方案时,如果没有比它更好的,那么它就是最好的。


string的实现细节以及示例

注:所有示例代码运行环境为:平台:ubuntu 16.04,编译工具链版本:gcc 5.4.0

上面既然说了string类的内存是连续的,口说无凭,当然是要上代码才有说服力:

char *p=NULL;
string str="(downey)";
p=&str[0];
/*如果直接输出p,cout方法会识别p为指向字符串的指针,将p指向的字符串输出,转换成void*类之后就会输出地址值*/
cout<<"addr of p is "<<(void*)p<<endl;
cout<<"str= ";
for(int i=0;i<str.size();i++)
cout<<*p++;
cout<<endl;
str+="+(abcdefghijklmnopqrstuvwxyz)";
p=&str[0];
cout<<"addr of p is "<<(void*)p<<endl;
cout<<"str= ";
for(int i=0;i<str.size();i++)
cout<<*p++;
cout<<endl;

执行结果:

addr of p is 0x7ffd330eeb40
str= (downey)
addr of p is 0x237b030
str= (downey)+(abcdefghijklmnopqrstuvwxyz)

上述示例的内容为,将字符指针p赋值为str的首地址,然后用指针自增的方式遍历整个类内字符串,同时打印出p的地址,这里面还有一次字符串的扩展,从这里我们可以看出两点:

  • 在字符串扩展前后,将str[0]的地址赋值给p,p都能以地址自增的方式访问整个字符串,表明string类的存储为连续的。
  • 在字符串的扩展前后,p的地址出现了变化,表明string类在扩展之后改变了地址,而存储空间是连续的,可以推出整个str都进行了地址迁移。

问题到这里并没有结束,因为我们还需要弄清楚string在的内存分配策略,我们可以用string內建方法 capacity()方法来获取目前string对象申请的内存大小,继续看下列代码:

string str;
cout<<str.capacity()<<endl;
str.append(16,'c');
cout<<str.capacity()<<endl;
str.append(15,'c');
cout<<str.capacity()<<endl;

在程序中,添加字符串,让其一次一次超出原始内存容量,输出结果:

15
30
60
120

看起来像是以 2^n*15的方式增长,我们再来试试:

string str;
cout<<str.capacity()<<endl;
str.append(10,'c');
cout<<str.capacity()<<endl;
str.append(20,'c');
cout<<str.capacity()<<endl;
str.append(40,'c');
cout<<str.capacity()<<endl;

在这次扩展中,并没有每次都超出原始内存容量,结果:

15
15
30
70

跟上述结果不一致,事实证明,string的内存扩展并非遵循某个单一规则,博主的猜测应该是遵循某种扩展算法,根据之前的应用情况采取弹性的策略(博主目前还没搞懂具体分配算法....)。

但是可以确认的是,每个初始化长度不超过15的字符串初始容量是15.

同时我们可以通过reserve()方法来修改内存分配容量的大小,如果我们提前知道需要操作的字符串大小,例如一个几K的文件,我们可以直接这样写:

std::ifstream file ("test.txt",std::ios::in|std::ios::ate);
if (file) {
std::ifstream::streampos filesize = file.tellg();
str.reserve(filesize);

如果不直接指定,一个几K的文件肯定会触发好几次内存重新分配,导致时间和空间上的浪费。

从字符串层面看C和C++比较(仅从字符串层面!)

在这里博主斗胆从C++ string类和C char*字符串层面对比C和C++,如果你看了上面博主的分析,就会觉得不管是从易用性,容错性还是开发效率上C++都要优于C,但是这有个前提,这个前提就是对初、中级程序员而言,因为对初、中级程序员而言,标准化意味着高效,而灵活性反而像是个累赘。

其实在上面的string例子中,不管string实例化的对象中有没有字符串,字符串的长度都是15,这对硬件资源来说无疑是一种浪费,(当然C++有其他策略来弥补,比如写时复制,但是也无法避免浪费),如果能精确地控制每块内存的应用,可以将硬件资源发挥到极致,同时封装本身也将带来资源的浪费,同时C++很多操作中,伴随着一些隐形的临时变量,这些临时变量的构造和析构也是比较费时的。

但是很多盆友就要说了,将硬件资源用到极致必然带来的是开发难度的大幅上升,是的,但是如果用C,我们至少有选择!

在一般的开发过程中,我们一般会从开发效率、执行效率、硬件资源这几个层面来考虑软件的实现,在实际的项目中,很可能会对其中的一项作严格要求,比如硬件资源,比如执行效率,又或者开发效率,C语言至少提供了这么一种可能性,以牺牲其他性能为代价来将一种性能做到极致。

而对于已经标准化的C++而言,便失去了这种柔韧性,虽然说C++是C的超集,但是就如同我在文章开头说的一样,两种语言的设计目的有根本上的区别,或者说思维的不一致注定了C++不会像C那样进行开发。

其实说到这里,C和C++并没有高下之分,语言本身是工具,工具只有合适不合适,没有绝对的谁比谁好,如果有,那么其中一个肯定会被马上淘汰,但是就目前而言,C和C++的存在证明了这两种语言各有各的应用场合。

(这里好像跑题了??)

好了,关于C++标准库string实现的讨论就到此为止啦,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.

C++中string的实现原理的更多相关文章

  1. java中string内存的相关知识点

    (一):区别java内存中堆和栈: 1.栈:数据可以共享,存放基本数据类型和对象的引用,其中对象存放在堆中,对象的引用存放在栈中: 当在一段代码块定义一个变量时,就在栈中 为这个变量分配内存空间,当该 ...

  2. Java中String创建原理深入分析

    创建String对象的常用方式: 1.  使用new关键字 String s1 = new String(“ab”);  // 2.  使用字符串常量直接赋值 String s2 = “abc”; 3 ...

  3. java中String的相等比较

    首先贴出测试用例: package test; import org.junit.Test; /** * Created by Administrator on 2015/9/16. * */ pub ...

  4. java中String类型变量的赋值问题

    第一节 String类型的方法参数 运行下面这段代码,其结果是什么? package com.test; public class Example { String str = new String( ...

  5. java中String s="abc"及String s=new String("abc")详解

    1.   栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方.与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆. 2.   栈的优势是,存取速度比堆要快,仅次于直 ...

  6. java中String相等问题

    java中判断两个字符串是否相等的问题   判断两个字符串是否相等的问题.在编程中,通常比较两个字符串是否相同的表达式是"==",但在java中不能这么写.在java中,用的是eq ...

  7. java中String类为什么不可变?

    在面试中经常遇到这样的问题:1.什么是不可变对象.不可变对象有什么好处.在什么情景下使用它,或者更具体一点,java的String类为什么要设置成不可变类型? 1.不可变对象,顾名思义就是创建后的对象 ...

  8. Java中String和byte[]间的转换浅析

    Java语言中字符串类型和字节数组类型相互之间的转换经常发生,网上的分析及代码也比较多,本文将分析总结常规的byte[]和String间的转换以及十六进制String和byte[]间相互转换的原理及实 ...

  9. 从虚拟机指令执行的角度分析JAVA中多态的实现原理

    从虚拟机指令执行的角度分析JAVA中多态的实现原理 前几天突然被一个"家伙"问了几个问题,其中一个是:JAVA中的多态的实现原理是什么? 我一想,这肯定不是从语法的角度来阐释多态吧 ...

随机推荐

  1. 【SpringBoot】SpringBoot配置文件及YAML简介(三)

    SpringBoot配置文件 SpringBoot使用一个全局的配置文件,配置文件名是固定的; application.properties application.yml 配置文件的作用:修改Spr ...

  2. echarts柱状图坐标文字显示不完整解决方式

    echarts柱状图坐标文字显示不完整解决方式 本文转载自:https://jingyan.baidu.com/article/ab69b2707a9aeb2ca7189f0c.html echart ...

  3. Spring Boot程序正确停止的姿势

    Spring Boot提供了2种优雅关闭进程的方式: 基于管理端口关闭进程 基于系统服务方式关闭进程 基于管理端口关闭进程 基于管理端口方式实现进程关闭实际上是模块spring-boot-actuat ...

  4. 元素高度变化使用动画transition

    高度变化,使用transition,没有效果,可以使用max-height替换. 思路: 初始元素max-height:0; 不显示,父元素hover时,重新设置元素的max-height的值, 可以 ...

  5. 局域网-断网&劫持(kali)

    1.查看局域网中的主机 fping –asg 192.168.1.0/24 2.断网 arpspoof -i wlan0 -t 192.168.100 192.168.1.1 (arpspoof  - ...

  6. 【记录】【windows】下查看端口是否被占用并杀死该进程

    查看端口是否被占用 netstat -aon|findstr "端口号" 比如 netstat -aon|findstr "6340" 杀死该进程 taskki ...

  7. PHP防止刷微信红包方法

    PHP防止刷微信红包方法1 输入验证码2授权登陆后 领取红包记录下 openid ip 第二次用openid或者ip(ip)连接同一个路由器是一样的 所以用ip 判断最好是判断有没有6个以上 判断有没 ...

  8. kafka备份原理

  9. javascript (0, obj.prop)()的用法

    我第一次看到这种奇怪的用法是在babel的源码中, 其实它的原理就是使得在prop这个方法里无法获取this, 从而无法对类中的其他变量或方法做操作. obj.prop() 这是一个方法调用, pro ...

  10. Django框架深入了解_05 (Django中的缓存、Django解决跨域流程(非简单请求,简单请求)、自动生成接口文档)

    一.Django中的缓存: 前戏: 在动态网站中,用户所有的请求,服务器都会去数据库中进行相应的增,删,查,改,渲染模板,执行业务逻辑,最后生成用户看到的页面. 当一个网站的用户访问量很大的时候,每一 ...