【问题标题】:std string should crash but doesn'tstd 字符串应该崩溃但不会
【发布时间】:2016-05-20 14:38:43
【问题描述】:

我有一堂课:

class A {
  public:
  string B;
};

然后是代码:

A a1;
a1.B = "abc";

printf("%p.\n", a1.B.c_str());

A a2(a1);

printf("%p.\n", a2.B.c_str());

两个实例的c_str都指向同一个地方(我理解,复制构造函数逐位复制A,字符串内部将数据存储在char *中,指针被复制。

但问题是,为什么这段代码不会崩溃? a1 和 a2 是堆栈变量,当解构它们时,字符串 B 也会被解构,这些字符串(指向同一内存位置)的内部 char* 不会被删除两次吗?不是双重删除,应该会导致崩溃吗?顺便说一句,我禁用了 gcc 优化,而 valgrind 也没有显示任何内容。

【问题讨论】:

  • C++ 中的“应该崩溃”几乎总是错误的思考方式。但是,在这种情况下,您认为代码存在的问题由字符串类解决。

标签: c++ string memory-management memory-leaks


【解决方案1】:

不,指针没有被复制。 std::string 的复制构造函数创建一个 new 缓冲区并从另一个字符串的缓冲区复制数据。

编辑:C++ 标准使用 来允许写入时复制语义,这将共享指针(并且需要引用计数来配合它),但从 C 开始这是不允许的++11。显然有 GCC 的版本可以做到这一点。

【讨论】:

  • 首先,在这种情况下会调用 B 的复制构造函数吗?其次,为什么 c_str() 会显示相同的地址呢?
  • @JaniBaramidze 1) 是 2) 在 C++11 中非法的 gcc 优化(即 "Copy on write")并在 gcc5 中被删除。
  • 错了,只要不对字符串做任何修改,缓冲区都是一样的。
  • @Mark 所以你是说指定 -std=c++11 应该使 c_str() 相同?它没有。
  • @JaniBaramidze GCC 从 GCC 5 开始遵守 C++11 中字符串的标准要求。在此之前,它仍然在 C++11 模式下使用非法优化。
【解决方案2】:

对于 GCC 4.*

字符串类中有一个内部计数器,用于知道指向缓冲区的实例数。当计数器变为 0 时,实例有责任释放内存。它与共享指针(boost 或 C++11)的行为相同。

此外,当字符串被修改时,会分配一个新的缓冲区,以避免对共享该缓冲区的其他实例进行修改。

【讨论】:

    【解决方案3】:

    应该崩溃但不会

    这句话应该持保留态度。 C++ 没有“必须崩溃”的概念。它有一个未定义行为的概念,这可能会也可能不会导致崩溃。即便如此,您的代码也没有未定义的行为。

    两个实例的 c_str 指的是同一个地方(这个我明白,复制 构造函数逐位复制,字符串内部存储数据 char*,指针被复制。

    您正在谈论std::string实现。您必须改为查看其接口,以确定哪些操作是安全的,哪些不安全。

    除此之外,您正在谈论的实现称为写时复制或“COW”,is obsolete since C++11。最新的 GCC 版本已放弃它。

    GCC 5 Changes, New Features, and Fixes:

    std::string 的新实现默认启用,使用 小字符串优化而不是写时复制引用计数。

    小字符串优化也使用了相同的技术,例如,在 std::string 的 Visual C++ 实现中。它以完全不同的方式工作,因此如果您使用足够新的 GCC 版本,那么您对 ​​std::string 内部工作原理的理解将不再正确,或者如果您使用 Visual C++,则它永远不会正确。

    但问题是,为什么这段代码不会崩溃?

    因为它根据其接口的文档正确使用std::string 操作,并且因为您的编译器没有完全损坏。

    您基本上是在问为什么您的编译器会为正确的代码生成有效的二进制文件。

    a1 和 a2 是堆栈变量,

    是的(正确的说法是对象具有“自动存储期限”)。

    当解构它们时,字符串 B 也会被解构,这些字符串的内部 char*(指向相同的内存位置)不会被删除两次吗?

    您的编译器的std::string 实现确保不会发生这种情况。要么它根本不使用 COW,要么析构函数包含检查共享缓冲区是否已被删除的代码。

    如果您使用的是较旧的 GCC 版本,那么您只需查看您的 std::string 实现的源代码即可了解它是如何完成的。毕竟它是开源的——但要小心,因为它可能看起来有点吓人。例如,这是an older GCC version 的析构函数代码:

    ~basic_string()
    { _M_rep()->_M_dispose(this->get_allocator()); }
    

    然后查看_M_dispose(在同一个文件中),您会发现这是一个非常复杂的实现,包含各种检查和同步。

    还要考虑这个:

    如果复制std::string 的行为会导致崩溃,那么整个课程将毫无意义,不是吗?

    【讨论】:

      【解决方案4】:

      它不会崩溃,因为字符串复制实际上复制了字符串,因此两个字符串将指向具有相同数据的不同内存位置。

      【讨论】:

        猜你喜欢
        • 2012-09-17
        • 1970-01-01
        • 2018-12-11
        • 1970-01-01
        • 2014-04-30
        • 1970-01-01
        • 2011-05-04
        • 1970-01-01
        • 2015-03-31
        相关资源
        最近更新 更多