转C++之stl::string写时拷贝导致的问题
前几天在开发某些数据结构到文件的 Dump 和 Load 功能的时候, 遇到的一个 bug 。
【问题复现】
问题主要出在 Load 过程中,从文件读取数据的时候, 直接使用 fread 的去操作 string 的内部指针地址 (char*)s.c_str() 。 简化后的示例代码如下( testdata1 文件内容是12345):
- void Load(string& s, size_t offset, size_t size) {
- s.resize(size);
- FILE* fp = fopen("testdata1", "r");
- assert(fp != NULL);
- fseek(fp, offset, SEEK_SET);
- fread((char*)s.c_str(), sizeof(char), size, fp);
- fclose(fp);
- }
通过 string::resize() 分配内存空间。 通过 string::c_str() 直接获取内存空间的起始地址并写入数据。
这样的用法是典型的使用 string 当数据缓冲区的用法, 省去了 malloc(new) 和 free(delete) 的过程。 通常来讲不会遇到什么问题。
不过这次遇到问题了。
简化问题代码示例如下:
- string s;
- Load(s, 0, 3);
- assert(s == "123"); // success
- string s2 = s;
- Load(s2, 1, 3);
- assert(s2 == "234"); // success
- assert(s == "123"); // failed
注: 因为 testdata1 文件内容是 12345 的纯文本文件。
所以 Load(s, 0, 3) 内容就是 “123” ,依此类推。
但是当后面的 string s2 = s; 定义了一个和 string 变量 s2 。 此时 Load(s2, 1, 3); 时 s2 内容是 “234” 符合预期。
但是问题出在之后 s 的内容也变成了 “234” , 而不是保持原来的 “123” 。
【原因分析】
其实示例代码写成那样,问题也清楚了很多了, 问题就出在
string s2 = s;
和之前 Load 函数中的
fread((char*)s.c_str(), sizeof(char), size, fp);
也就是 string 的 copy-on-write 实现上。
(之前的问题是隐藏在各种代码之间,甚至都很难定位到原来是 string 的问题。)
C++ stl::string 有两种常见的主流实现方式:
『eager-copy』
每个 string 都是一个独立申请的内存空间,每次拷贝都是深拷贝, 哪怕内容是一模一样的, 所以每个 string 的 c_str() 指针地址都是 不一样 的。 这样的优点是内存空间互不干扰, 缺点是内存浪费。
『copy-on-write』
string 之间拷贝时不是深拷贝,只拷贝了指针, 也就是共享同一个字符串内容, 只有在内容被修改的时候, 才真正分配了新的内存并 copy 。 比如 s[0]='1' 之类的修改字符串内容的一些write操作, 就会申请新的内容,和之前的共享内存独立开。 所以称之为 『copy-on-write』
最显然的就是 string s2 = s; 拷贝后, s 和 s2 的 c_str() 返回的指针地址是 一样 的。 这样的优点就是节省内存开销, 当string字符串占用内存较大时, 也可以省去深拷贝时较大的性能开销。
不同的stl标准库实现不同, 比如 Centos 6.5 默认的 stl::string 实现就是 『copy-on-write』, 而 Mac OS X (10.10.5) 实现就是 『eager-copy』。
而这次的 bug 就是和 『copy-on-write』有关,
因为 s2 和 s 的 c_str() 指针是同一个, 所以 Load 函数里面的这行代码:
fread((char*)s.c_str(), sizeof(char), size, fp);
我们以为只是在操作一个字符串, 其实是 s 和 s2 两个字符串的内容都被修改了。 所以就会导致一系列的问题。
完整示例代码请看 stringload
【总结】
总之,原因的源头在于 (char*)s.c_str() , 虽然我在 StackOverFlow 上有些高票答案也经常使用类似的把 string 当成内存缓冲区的写法。 毕竟方便嘛。但是考虑到 stl 的 copy-on-write 实现,会导致把 stl 容器当内存缓冲区的写法变得有隐藏陷阱。
虽然我在解决这个 bug 之前就知道 stl 有 『copy-on-write』 实现这么一说。 但是开发时候往往出现问题的地方并不是直接在有问题的代码那里就出现问题, 导致很难查,更何况不知道 『copy-on-write』这回事的开发者,可能就容易踩大坑了。
转C++之stl::string写时拷贝导致的问题的更多相关文章
- String写时拷贝实现
		头文件部分 1 /* 版权信息:狼 文件名称:String.h 文件标识: 摘 要:对于上版本简易的String进行优化跟进. 改进 1.(将小块内存问题与大块分别对待)小内存块每个对象都有,当内存需 ... 
- String类的实现(4)写时拷贝浅析
		由于释放内存空间,开辟内存空间时花费时间,因此,在我们在不需要写,只是读的时候就可以不用新开辟内存空间,就用浅拷贝的方式创建对象,当我们需要写的时候才去新开辟内存空间.这种方法就是写时拷贝.这也是一种 ... 
- String 类的实现(2)引用计数与写时拷贝
		1.引用计数 我们知道在C++中动态开辟空间时是用字符new和delete的.其中使用new test[N]方式开辟空间时实际上是开辟了(N*sizeof(test)+4)字节的空间.如图示其中保存N ... 
- 标准C++类std::string的内存共享和Copy-On-Write(写时拷贝)
		标准C++类std::string的内存共享,值得体会: 详见大牛:https://www.douban.com/group/topic/19621165/ 顾名思义,内存共享,就是两个乃至更多的对象 ... 
- Linux写时拷贝技术(copy-on-write)
		COW技术初窥: 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内 ... 
- 【转】Linux写时拷贝技术(copy-on-write)
		http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html 源于网上资料 COW技术初窥: 在Linux程序中,fork()会 ... 
- 计算机程序的思维逻辑 (73) - 并发容器 - 写时拷贝的List和Set
		本节以及接下来的几节,我们探讨Java并发包中的容器类.本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理.它们的用法比较 ... 
- 并发容器之写时拷贝的 List 和 Set
		对于一个对象来说,我们为了保证它的并发性,通常会选择使用声明式加锁方式交由我们的 Java 虚拟机来完成自动的加锁和释放锁的操作,例如我们的 synchronized.也会选择使用显式锁机制来主动的控 ... 
- 深拷贝&浅拷贝&引用计数&写时拷贝
		(1).浅拷贝: class String { public: String(const char* str="") :_str(]) { strcpy(_str,str); } ... 
随机推荐
- 如何申请阿里云免费SSL证书(可用于https网站)并下载下来
			前提条件:你要有阿里云的账号,并且要有一个域名. 注意:阿里云系统也在不断更新,界面以后可能会有稍许变化,但是原理是相通的. 具体步骤: 1.登录到阿里云后台,并选择 “SSL证书(应用安全)” 菜单 ... 
- kafka修改topic副本数
			工作案例: 大数据开发用系统脚本自动在kafka建topic,检查后才发现副本数只有1个,存在数据丢失的风险.需要立刻把副本数改为3个. 开始干活,首先想到的是下面的命令: ${BIN_PATH}/k ... 
- [Comet OJ - Contest #6 C][48C 2279]一道树题_树
			一道树题 题目大意: 给定一棵树,边的编号为读入顺序.现在规定,区间$[L, R]$的贡献$S(L,R)$为把编号在该区间里的边都连上后,当前形成的森林中点数大于等于$2$的联通块个数. 求$\sum ... 
- [转帖]nginx1.17.2版本源码安装
			nginx1.17.2版本源码安装 原创: 沧海书生 Ansible爱好者 昨天 公众号里面的内容 这里简单测试了下 在x86的虚拟机里面编译安装 nginx 仅make make install n ... 
- [转帖]如何在VirtualBox中运行macOS Catalina Beta版本
			如何在VirtualBox中运行macOS Catalina Beta版本 secist2019-08-03共2179人围观系统安全 https://www.freebuf.com/articles/ ... 
- redis 持久化之 RDB & AOF
			Redis 持久化实现方式 快照对数据某一时间点的完整备份.例如Linux 快照备份.Redis RDB.MySQL Dump. 日志将数据的所有操作都记录到日志中,需要恢复时,将日志重新执行一次.M ... 
- Rsync快速入门实例(转)
			三种主要数据传输方式 单主机本地目录间数据传输(类似cp) Local: rsync [OPTION...] SRC... [DEST] 借助rcp,ssh等通道来传输数据(类似scp) Access ... 
- Yii2.0 组件
			框架之所以是框架,是因为其强大,其封装了很多实用的功能,开发者可以开箱即用. 下边列举Yii2.0的部分组件: var_dump(Yii::$app->session->getId()); ... 
- CentOS 中利用docker安装MySQL
			1.前提条件 centos7 且内核版本高于3.10, 可通过命令: uname -r 查看内核版本 2.利用yum 安装docker 安装一些必要的系统工具: sudo yum install -y ... 
- C#中判断文件夹或文件是否存在的方法
			一.根据虚拟路径获取文件物理路径: string savePath = Server.MapPath("~/Uploads/RemoteDatum/"); 二.判断文件夹是否存在 ... 
