转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"); // successstring s2 = s;Load(s2, 1, 3);assert(s2 == "234"); // successassert(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); } ...
随机推荐
- DRF视图-5个扩展类以及GenericAPIView基类
视图 5个视图扩展类 视图拓展类的作用: 提供了几种后端视图(对数据资源进行曾删改查)处理流程的实现,如果需要编写的视图属于这五种,则视图可以通过继承相应的扩展类来复用代码,减少自己编写的代码量. 这 ...
- Linux 操作系统常用命令
常用命令详解 ls 常用选项: -a:列出所有文件,包括以.为开头的隐藏文件. -d ; 列出目录本身,并不包含目录里的内容. -h:和-l一起使用,文件大小容易阅读.文件的实际大小 ls -d/ro ...
- 安装horizon
在控制节点上安装 controllerHost='controller' ADMIN_PASSWD='Ideal123!' 1.安装dashboard组件 yum -y install opensta ...
- 解决windows server 2019远程桌面许可证问题
解决远程桌面许可证问题,你的远程桌面许可证出现问题,你的会话将在60分钟后断开. 最近装了台windows server 2019服务器做远程桌面连接,也安装了远程桌面许可证,但客户端远程连接时出现你 ...
- lua调用shell 脚本
Lua中,os.execute可以执行dos命令,但是返回的是系统状态码,默认输出.io.popen()也可以执行dos命令,但是返回一个文件.eg: 复制代码 代码如下: local t = io. ...
- SpringBoot中使用aop-测试
面向切面编程(AOP),该种方式主要是为了弥补面向对象编程(OOP)的不足,通过配置切面以及关注点.通知等我们可以在程序的任意位置对我们的代码进行增强(执行一些代码),AOP是Spring的特性之一, ...
- Java时间处理
java.sql.PreparedStatement接口的setDate(int parameterIndex, java.sql.Date x)方法中的Date为java.sql包下的Date,而不 ...
- Open-falcon监控
https://book.open-falcon.org/zh_0_2/ 本文档记录了CentOS7.4下open-falcon-v2监控系统的部署流程,以及一些需要注意的地方. 环境准备 安装Red ...
- 使用Qt 3D Studio 2.4显着提升性能(渲染速度提高了565%)
发布于2019年6月18日星期二11评论Qt 3D Studio 2.4显着改善性能 发表于Biz Circuit&Dev Loop,设计,图形,性能,Qt 3D Studio 除了有效使用系 ...
- hdu 1576
老生常谈的问题 利用同余的思想 抽象出表达式 bx+9973y=n 然后用bx+9973y=1(题目给出了gcd(b,9973)=1) 求出基础解 y0 bx+9973y=n 的 基础解y=n*y0 ...