【问题标题】:Is reusing a memory location safe?重用内存位置安全吗?
【发布时间】:2016-02-11 08:16:16
【问题描述】:

这个问题是基于一些现有的移植到 C++ 的 C 代码。我只是对它是否“安全”感兴趣。我已经知道我不会这样写。我知道这里的代码基本上是 C 而不是 C++,但它是用 C++ 编译器编译的,我知道标准有时会略有不同。

我有一个分配一些内存的函数。我将返回的 void* 转换为 int* 并开始使用它。

稍后我将返回的 void* 转换为 Data* 并开始使用它。

这在 C++ 中安全吗?

示例:-

void* data = malloc(10000);

int* data_i = (int*)data;
*data_i = 123;
printf("%d\n", *data_i);

Data* data_d = (Data*)data;
data_d->value = 456;
printf("%d\n", data_d->value);

我从不读取通过不同类型使用的变量,但担心编译器可能会看到 data_idata_d 是不同的类型,因此不能合法地相互别名并决定重新排序我的代码,例如将商店放在第一个 printf 之前的 data_d。这会破坏一切。

但是,这是一种一直使用的模式。如果您在两个访问之间插入 freemalloc,我认为它不会改变任何内容,因为它不会触及受影响的内存本身并且可以重用相同的数据。

我的代码是坏了还是“正确”?

【问题讨论】:

  • 如果 Data 不是 POD,您应该使用placement new (isocpp.org/wiki/faq/dtors#placement-new) 并确保调用了它的析构函数。
  • 我想我们可以假设一个数据适合 10000 字节?
  • 顺便说一句,我意识到您只是在移植而不是创建代码,但是这种事情是使用联合的绝佳机会,实际上是为它们创建的目的。可能值得在这里使用一个。它还会增加一些小的类型安全性。

标签: c++


【解决方案1】:

“OK”,它可以按照您编写的方式工作(假设原语和普通旧数据类型 (POD))。这是安全的。它实际上是一个自定义内存管理器。

一些注意事项:

  • 如果在分配内存的位置创建了具有非平凡析构函数的对象,请确保它被调用

    obj->~obj();
    
  • 如果创建对象,请考虑使用 placement new syntax 而非普通类型转换(也适用于 POD)

    Object* obj = new (data) Object();
    
  • 检查nullptr(或NULL),如果malloc失败,则返回NULL

  • 对齐应该不是问题,但在创建内存管理器时始终注意它并确保对齐正确

鉴于您使用的是 C++ 编译器,除非您想保留代码的“C”性质,否则您还可以查看全局 operator new()

和往常一样,完成后不要忘记free()(或delete,如果使用new


您提到您目前还不会转换任何代码;但是,如果您确实考虑了它,那么您可能希望在 malloc 甚至全局 ::operator new 上使用 C++ 中的一些惯用特性。

您应该查看智能指针std::unique_ptr<>std::shared_ptr<> 并让它们处理内存管理问题。

【讨论】:

  • 谢谢!是的,我当然会在新的 C++ 代码中完成所有这些工作,这是来自旧 C 项目的现有代码,在一些地方已更改为使用 C++。当我想到这可能也不安全时,我正在寻找严格的混叠问题。我现在不打算更改代码,只是想确保它没有被破坏!
  • 我在严格别名方面遗漏了什么吗?为什么这没关系...编译器不能将指令 data_d->value = 456 重新排列在 *data_iprintf 之前,因为它们是不同的指针类型,编译器可以假设它们访问不同的内存区域(即使在很明显指向同一地区!)。我原以为需要一个内存栅栏。
  • @dave:根据 C++ 标准,当 POD 对象的内存以 OP 所要求的方式重用时,它们就会被销毁。然而,这并非适用于任何远程有趣的对象。
【解决方案2】:

根据Data 的定义,您的代码可能会被破坏。无论哪种方式,它都是糟糕的代码。

如果Data 是一个普通的旧数据类型(POD,即基本类型的 typedef、POD 类型的结构等),分配的内存针对该类型正确对齐 (*),那么您的代码是定义明确的,这意味着它会“工作”(只要您在使用之前初始化*data_d 的每个成员),但这不是一个好习惯。 (见下文。)

如果Data 是非 POD 类型,那么您将面临麻烦:例如,指针分配不会调用任何构造函数。 data_d,其类型为“指向Data”的指针,实际上是在撒谎,因为它指向某物,但某物不是Data 类型因为没有创建/构造/初始化这样的类型。到那时,未定义的行为将不远。

在给定内存位置正确构造对象的解决方案称为placement new

Data * data_d = new (data) Data();

这指示编译器在位置 data 处构造一个 Data 对象。这适用于 POD 和非 POD 类型。您还需要调用析构函数 (data_d->~Data()) 以确保它在delete内存之前运行。

请注意不要混合分配/释放功能。无论你malloc() 需要是free()d,分配new 需要delete,如果你是new [],你必须delete []。任何其他组合都是UB。


在任何情况下,在 C++ 中都不鼓励使用“裸”指针来获取内存所有权。你应该

  1. new放入构造函数中,将对应的delete放入类的析构函数中,使对象成为内存的所有者(包括在对象退出时正确释放范围,例如在例外情况下);或

  2. 使用smart pointer,它可以有效地为您完成上述工作。


(*):已知实现定义了“扩展”类型,malloc() 不考虑其对齐要求。实际上,我不确定语言律师是否还会称他们为“POD”。例如,MSVC 在 malloc() 上执行 8-byte alignment,但将 SSE 扩展类型 __m128 定义为具有 16-byte alignment 要求。

【讨论】:

  • 假设用户不会在返回的指针上使用指针算术运算,那么使用 malloc 分配的内存何时不会针对 POD 类型正确对齐?据我所知,在 C 上是不可能的; C++ 有什么不同吗?
  • @user694733:已知实现定义了“扩展”类型,malloc() 未考虑其对齐要求。实际上,我不确定语言律师是否还会称他们为“POD”。例如,MSVC 使用 8-byte alignment on malloc(),但将 SSE 扩展类型 __m128 定义为具有 16 字节对齐要求。
  • 不应该是Data * data_d = new (data) Data()吗?
  • 澄清:它可能是糟糕的 C++,但它是非常好的 C。
  • 你的回答可能更明确一点,混合使用 newfree()malloc()delete 是严重违法的。
【解决方案3】:

围绕严格别名的规则可能相当棘手。

一个严格别名的例子是:

int a = 0;
float* f = reinterpret_cast<float*>(&a);
f = 0.3;
printf("%d", a);

这是一个严格的别名违规,因为:

  • 变量的生命周期(及其使用)重叠
  • 他们通过两个不同的“镜头”解读同一段记忆

如果您没有同时执行两者,那么您的代码不会违反严格的别名。


在 C++ 中,对象的生命周期在构造函数结束时开始,在析构函数开始时停止。

在内置类型(无析构函数)或 POD(普通析构函数)的情况下,规则是,只要内存被覆盖或释放,它们的生命周期就会结束。

注意:这是专门用来支持写内存管理器的;毕竟malloc 是用C 编写的,operator new 是用C++ 编写的,并且它们被明确允许共享内存。


我专门用lenses代替types,因为规则有点难。

C++ 通常使用名义类型:如果两种类型具有不同的名称,则它们是不同的。如果您像访问U 一样访问动态类型T 的值,那么您就违反了别名。

这条规则有很多例外:

  • 基类访问
  • 在 POD 中,作为指向第一个属性的指针访问

而最复杂的规则与union 相关,其中 C++ 转换为 结构类型:你可以通过两种不同的类型访问一块内存,如果你只访问开头的部分两种类型共享相同初始序列的内存。

§9.2/18 如果一个标准布局联合包含两个或多个共享一个共同初始序列的标准布局结构,并且如果标准布局联合对象当前包含这些标准之一 -布局结构,允许检查其中任何一个的公共初始部分。如果对应的成员具有布局兼容的类型,并且对于一个或多个初始成员的序列,两个成员都不是位域或两者都是具有相同宽度的位域,则两个标准布局结构共享一个共同的初始序列。

给定:

  • struct A { int a; };
  • struct B: A { char c; double d; };
  • struct C { int a; char c; char* z; };

union X { B b; C c; };内,您可以同时访问x.b.ax.b.cx.c.ax.c.c;但是,如果当前存储的类型不是B(分别不是C),则访问x.b.d(分别是x.c.z)违反了别名。

注意:非正式地,结构类型就像将类型映射到其字段的元组(将它们展平)。

注意:char* 是特别豁免这条规则的,你可以通过char*查看任何一块内存。


在您的情况下,如果没有 Data 的定义,我不能说是否会违反“镜头”规则,但是因为您是:

  • 在通过Data*访问之前用Data覆盖内存
  • 之后不再通过int* 访问它

那么您就符合生命周期规则,因此就语言而言不会发生别名。

【讨论】:

  • 我认为给定您上面的声明加上A a; B b; C c;,以下是可以的,正如您所说:int i = ((A *)&amp;B)-&gt;a;(即通过基类表达式访问),而以下违反了别名规则:int i = ((A *)&amp;C)-&gt;a;(通过不相关类型的表达式访问)。我弄错了吗?你的意思是不同的吗?
  • @PeterA.Schneider:int i = ((A *)&amp;c)-&gt;a; 专门用于琐碎的布局类型(任何地方都不允许使用virtual)。在这里将类型视为元组很有帮助:B 最终是 (int, char, double),而 C(int, char, char*),因此该语言允许访问前两个元素。这仅适用于微不足道的布局,因为如果您混合使用虚拟指针、虚拟基类、多个基类,则无法保证布局兼容性......然而,尝试访问第三个 ((B*)&amp;c)-&gt;d 将违反别名:不是常见的一部分前缀子序列。
  • 你能引用支持它的标准吗?我认为Python PyObject bug 正是这种情况(在以相同字段类型序列开头的两个结构之间进行转换)。我看到 3.10/10,“在其元素或非静态数据成员中包含上述类型之一的聚合或联合类型”——这对我来说是不可理解的:那个条件是如此广泛(没有提到元素顺序,或者它必须在结构的开头!)它会允许太多的别名。
  • @PeterA.Schneider:我对 C 没有任何承诺,但是在 C++14 标准中(尽管我认为它至少与 C++11 没有变化)你会对 §9.2 感兴趣/18:如果一个标准布局联合包含两个或多个共享一个共同初始序列的标准布局结构,并且如果标准布局联合对象当前包含这些标准布局结构之一,则允许检查它们中任何一个的共同初始部分。 => 似乎我有点热情,它仅限于union 中的类型,而不是任意指针强制转换;我会编辑。
  • @jcoder:其实我觉得这段代码很好,因为每个变量的内存使用时间没有重叠。
【解决方案4】:

只要内存一次只用于一件事,它就是安全的。您基本上将分配的数据用作union

如果您想将内存用于类的实例,而不仅仅是简单的 C 风格结构或数据类型,您必须记住执行 placement new 来“分配”对象,如这实际上会调用对象的构造函数。使用完对象后必须显式调用的析构函数,不能delete它。

【讨论】:

    【解决方案5】:

    只要您只处理“C”类型,就可以了。但是一旦你使用 C++ 类,你就会遇到正确初始化的麻烦。例如,如果我们假设Datastd::string,那么代码就会非常错误。

    编译器无法真正将存储移动到对printf 的调用,因为这是一个可见的副作用。结果必须好像副作用是按照程序规定的顺序产生的。

    【讨论】:

      【解决方案6】:

      实际上,您已经在malloc/free 之上实现了自己的分配器,在这种情况下重用了一个块。那是绝对安全的。只要块足够大并且来自保证充分对齐的来源(malloc 确实如此),分配器包装器当然可以重用块。

      【讨论】:

        【解决方案7】:

        只要Data 仍然是 POD,这应该没问题。否则,您将不得不切换到新的展示位置。

        但是,我会放置一个静态断言,以便在以后的重构过程中不会改变

        【讨论】:

          【解决方案8】:

          我在重用内存空间方面没有发现任何错误。只有我关心的是悬空的参考。正如你所说的那样重用内存空间我认为它对程序没有任何影响。
          你可以继续你的编程。但总是比free() 的空间更好,然后分配给另一个变量。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2023-03-03
            • 2020-12-19
            • 2015-09-02
            • 1970-01-01
            • 2014-08-19
            • 2020-03-30
            • 2017-06-18
            • 1970-01-01
            相关资源
            最近更新 更多