前几天在开发某些数据结构到文件的 Dump 和 Load 功能的时候, 遇到的一个 bug 。

【问题复现】

问题主要出在 Load 过程中,从文件读取数据的时候, 直接使用 fread 的去操作 string 的内部指针地址 (char*)s.c_str() 。 简化后的示例代码如下( testdata1 文件内容是12345):

  1.  
    void Load(string& s, size_t offset, size_t size) {
  2.  
    s.resize(size);
  3.  
    FILE* fp = fopen("testdata1", "r");
  4.  
    assert(fp != NULL);
  5.  
    fseek(fp, offset, SEEK_SET);
  6.  
    fread((char*)s.c_str(), sizeof(char), size, fp);
  7.  
    fclose(fp);
  8.  
    }
  9.  
     

通过 string::resize() 分配内存空间。 通过 string::c_str() 直接获取内存空间的起始地址并写入数据。

这样的用法是典型的使用 string 当数据缓冲区的用法, 省去了 malloc(new) 和 free(delete) 的过程。 通常来讲不会遇到什么问题。

不过这次遇到问题了。

简化问题代码示例如下:

  1.  
    string s;
  2.  
    Load(s, 0, 3);
  3.  
    assert(s == "123"); // success
  4.  
     
  5.  
    string s2 = s;
  6.  
    Load(s2, 1, 3);
  7.  
    assert(s2 == "234"); // success
  8.  
    assert(s == "123"); // failed
  9.  
     

注: 因为 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写时拷贝导致的问题的更多相关文章

  1. String写时拷贝实现

    头文件部分 1 /* 版权信息:狼 文件名称:String.h 文件标识: 摘 要:对于上版本简易的String进行优化跟进. 改进 1.(将小块内存问题与大块分别对待)小内存块每个对象都有,当内存需 ...

  2. String类的实现(4)写时拷贝浅析

    由于释放内存空间,开辟内存空间时花费时间,因此,在我们在不需要写,只是读的时候就可以不用新开辟内存空间,就用浅拷贝的方式创建对象,当我们需要写的时候才去新开辟内存空间.这种方法就是写时拷贝.这也是一种 ...

  3. String 类的实现(2)引用计数与写时拷贝

    1.引用计数 我们知道在C++中动态开辟空间时是用字符new和delete的.其中使用new test[N]方式开辟空间时实际上是开辟了(N*sizeof(test)+4)字节的空间.如图示其中保存N ...

  4. 标准C++类std::string的内存共享和Copy-On-Write(写时拷贝)

    标准C++类std::string的内存共享,值得体会: 详见大牛:https://www.douban.com/group/topic/19621165/ 顾名思义,内存共享,就是两个乃至更多的对象 ...

  5. Linux写时拷贝技术(copy-on-write)

    COW技术初窥: 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内 ...

  6. 【转】Linux写时拷贝技术(copy-on-write)

    http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html 源于网上资料 COW技术初窥: 在Linux程序中,fork()会 ...

  7. 计算机程序的思维逻辑 (73) - 并发容器 - 写时拷贝的List和Set

    本节以及接下来的几节,我们探讨Java并发包中的容器类.本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理.它们的用法比较 ...

  8. 并发容器之写时拷贝的 List 和 Set

    对于一个对象来说,我们为了保证它的并发性,通常会选择使用声明式加锁方式交由我们的 Java 虚拟机来完成自动的加锁和释放锁的操作,例如我们的 synchronized.也会选择使用显式锁机制来主动的控 ...

  9. 深拷贝&浅拷贝&引用计数&写时拷贝

    (1).浅拷贝: class String { public: String(const char* str="") :_str(]) { strcpy(_str,str); } ...

随机推荐

  1. highway network及mnist数据集测试

    先说结论:没经过仔细调参,打不开论文所说代码链接(fq也没打开),结果和普通卷积网络比较没有优势.反倒是BN对网络起着非常重要的作用,达到了99.17%的测试精度(训练轮数还没到过拟合). 论文为&l ...

  2. 【计算机视觉】行人检测(Pedestrian Detection)资源

    一.论文 综述类的文章 [1]P.Dollar, C. Wojek,B. Schiele, et al. Pedestrian detection: an evaluation of the stat ...

  3. windows 3种方式运行exe文件

    1.双击文件运行 2.打开cmd,cd 到要运行的文件目录下,输入文件名或者文件名.exe 3.将文件目录配置到系统环境变量,按windws+R输入文件名或者文件名.exe

  4. nRF5 SDK Bootloader and DFU moudles(1)

    在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行.可以初始化硬件设备.建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境. 在嵌 ...

  5. 第五周课程总结&试验报告(三)

    第五周课程总结&试验报告(三) 实验三 String类的应用 实验目的 掌握类String类的使用: 学会使用JDK帮助文档: 实验内容 ###1.已知字符串:"this is a ...

  6. Docker 运行hello world

    Docker 允许你在容器内运行应用程序, 使用 docker run 命令来在容器内运行一个应用程序. 输出Hello world docker run ubuntu:15.10 /bin/echo ...

  7. Find Duplicate File in System

    Given a list of directory info including directory path, and all the files with contents in this dir ...

  8. [转帖]InfluxDB 1.2.0安装及新旧版本的注意事项

    InfluxDB 1.2.0安装及新旧版本的注意事项 http://haibing.org/245?zwlqby=npztq3 挺好的文章 很好的解决了 上一个文档里面 关于 web admin 的问 ...

  9. Spark性能调优:广播大变量broadcast

    Spark性能调优:广播大变量broadcast 原文链接:https://blog.csdn.net/leen0304/article/details/78720838 概要 有时在开发过程中,会遇 ...

  10. 怎样禁止 html 中 <textarea> 标签可以拉伸改变大小 ?

    1. 一般来说, 使用 <textarea> 这个标签都会禁用掉它的这个默认属性, 不然可能会被用户玩儿坏, 而且也不利于其他元素的展示, 使用到的属性是 resize , 改为 none ...