【问题标题】:Is it well-defined to use a pointer pointing to one-past-malloc?使用指向过去的 malloc 的指针是否定义明确?
【发布时间】:2018-06-02 16:26:24
【问题描述】:

在 C 中,创建一个指向数组最后一个元素之后的指针并在指针算术中使用它是非常好的,只要你不取消引用它:

int a[5], *p = a+5, diff = p-a; // Well-defined

但是,这些是 UB:

p = a+6;
int b = *(a+5), diff = p-a; // Dereferencing and pointer arithmetic

现在我有一个问题:这是否适用于动态分配的内存?假设我只在指针算术中使用指向过去最后一个的指针,而不取消引用它,并且malloc() 成功。

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");
// Question:
int *p = a+5;
int diff = p-a; // Use in pointer arithmetic?

【问题讨论】:

  • 好吧,你标记了 C。而 C++ 的 new 是一个不同的野兽。它不仅仅是内存分配。另外,C++ 语言律师会说,仅仅写入 malloc 返回的内存并不会在那里创建对象,更不用说使内存具有有效的类型了。
  • 你实际上可以有一个指向 anywhere 的指针,只要你不取消引用它。您甚至可以使用它与其他指针进行比较,即使它可能没有意义。
  • @Someprogrammerdude 这太疯狂了。那不是UB吗?
  • @Someprogrammerdude - 但我认为您无法以任何方式获得指向任何地方的指针。例如,您不能像 iBug 指出的那样进行指针运算。那就是UB本身。您可以将整数常量转换为指针,但不能保证它与 a + 6 的地址相同。
  • ISO/IEC 9899:2011 §7.22.3 内存管理功能 ¶1 连续调用aligned_alloc,@分配的存储顺序和连续性987654328@、mallocrealloc 函数未指定。如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给具有基本对齐要求的任何类型对象的指针,然后用于访问分配的空间中的此类对象或此类对象的数组(直到空间被显式释放)。 它说“这样的对象的数组”——对于数组来说没问题;所以这里没问题。

标签: c malloc language-lawyer bounds dynamic-allocation


【解决方案1】:

C11 的 n4296 草案明确指出,指向数组后面的指针是完全定义的:6.5.6 语言/表达式/加法运算符:

§ 8 当一个整数类型的表达式被添加到指针或从指针中减去时, 结果具有指针操作数的类型。 ...此外,如果表达式 P 指向最后一个 数组对象的元素,表达式 (P)+1 指向数组对象的最后一个元素 数组对象,如果表达式 Q 指向数组对象的最后一个元素, 表达式 (Q)-1 指向数组对象的最后一个元素...如果结果指向数组对象的最后一个元素,则它 不得用作被评估的一元 * 运算符的操作数。

由于内存的类型从未在子句中明确,它适用于任何类型的内存,包括分配的内存。

这显然意味着之后:

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");

两个

int *p = a+5;
int diff = p-a;

已完美定义,并且按照通常的指针算术规则适用,diff 应接收值 5

【讨论】:

  • 如果我写p = a+6,那么我不能指望p - a == 6按照标准,对吧?
  • @iBug 是的,你不能指望它能工作。 " 如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义"
  • @iBug 标准强制定义的行为仅限于粘贴数组对象的最后一个元素。如果你走得更远(最后一个元素过去 2 个),标准没有指定任何足以成为未定义行为的内容。
  • @iBug 您的示例提出的一个特别问题是指针算术中的溢出是 C++ 中未定义的行为。因此,规则基本上规定 malloc 永远不会分配内存的最后一个字节除非编译器还同时以一种使这些溢出问题不可见的方式定义溢出。
  • 目前 N 数最高的已发表的 WG14 论文是 N2184。您从哪里获得 N4296?
【解决方案2】:

使用指向过去的malloc的指针是否定义明确?

如果p 指向已分配内存之后的一个并且它未被取消引用,则它是明确定义的。

n1570 - §6.5.6 (p8):

[...] 如果结果指向数组对象的最后一个元素后一个,则不应将其用作计算的一元 * 运算符的操作数。

两个指针的减法只有在它们指向同一个数组对象的元素或超过数组对象的最后一个元素时才有效,否则会导致未定义的行为。

(p9)

当两个指针相减时,两个指针都指向同一个数组对象的元素,或者指向数组对象最后一个元素的元素[...]

上面的引用非常适用于动态和静态分配的内存。

int a[5];
ptrdiff_t diff = &a[5] - &a[0]; // Well-defined

int *d = malloc(5 * sizeof(*d));
assert(d != NULL, "Memory allocation failed");
diff = &d[5] - &d[0];        // Well-defined

正如Jonathan Lefflercomment 中指出的那样,这对动态分配的内存有效的另一个原因是:

§7.22.3(p1)

连续调用aligned_alloccallocmallocrealloc 函数分配的存储顺序和连续性未指定。如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给指向具有基本对齐要求的任何类型对象的指针,然后用于访问此类对象或此类对象的数组在分配的空间中(直到空间被显式释放)。

上面sn-p中malloc返回的指针赋值给d,分配的内存是一个由5个int对象组成的数组。

【讨论】:

  • 正式来说,d 指向的数据怎么会变成一个数组呢?根据 C 标准,malloc:ed 数据的有效类型是用于左值访问的类型。这是int,而不是int[5]
  • @Lundin;不,它没有。 d 是一个指针,指向由malloc 分配的内存块的第一个块。
  • 引用的文字仅说明分配的存储可用于存储数组,而不是数据如何变成数组。假设我做int(*ptr)[5] = malloc_chunk; memcpy(something, ptr, 5*sizeof(int); 然后我将有效类型设为数组类型。但是如果没有这样的代码,“块”就不是正式的数组类型。我认为标准中没有任何值得在这里引用的文本,关于有效类型(和严格别名)的规则简直太差了。
  • 这里的“直到”这个词是模棱两可的(甚至是错误的):定义明确直到指针指向已分配内存之后的位置。 根据你的回答,当指针指向过去时它仍然是正确的,但“直到”的意思是“当它发生时它不再是正确的”,所以你最好找到一个更好的措辞。
  • @iBug:“直到”的用法有什么歧义或错误?在标准中,它适用于“空间被显式释放”的条款。一旦空间被释放,指向它的指针就不再有效。
【解决方案3】:

是的,相同的规则适用于具有动态和自动存储期限的变量。它甚至适用于对单个元素的 malloc 请求(在这方面,标量等效于单元素数组)。

指针运算仅在数组内有效,包括数组末尾的一个。

在取消引用时,重要的是要注意一个注意事项:关于初始化 int a[5] = {0};,编译器不得尝试在表达式 int* p = &a[5]取消引用 a[5];它必须将其编译为 int* p = a + 5; 同样,同样的事情适用于动态存储。

【讨论】:

  • int* p = &a[5]; a[5] 中未取消引用。它相当于int p = a + 5;,或者我可能把那个段落弄错了。
  • 我想说的是没有带有表达式 &a[5] 的 UB,因为编译器必须将其视为 + 5。它读起来不好吗?我在实施这个周末后感冒了:meta.stackexchange.com/questions/303920/…
【解决方案4】:

使用指向过去的malloc的指针是否定义明确?

是的,但存在一个没有明确定义的极端情况:

void foo(size_t n) {
  int *a = malloc(n * sizeof *a);
  assert(a != NULL || n == 0, "Memory allocation failed");
  int *p = a+n;
  intptr_t diff = p-a;
  ...
}

内存管理函数 ...如果请求的空间大小为零,则行为是实现定义的:要么返回空指针,要么行为就像大小是非零值,但返回的指针不得用于访问对象。 C11dr §7.22.3 1

foo(0) --> malloc(0) 可能返回NULLnon-NULL。在第一个实现中,NULL 的返回不是“内存分配失败”。 这意味着代码正在尝试使用 int *p = NULL + 0;int *p = a+n;,这无法保证指针数学 - 或至少使此类代码受到质疑。

可移植代码通过避免 0 大小分配而受益。

void bar(size_t n) {
  intptr_t diff;
  int *a;
  int *p;
  if (n > 0) {
    a = malloc(n * sizeof *a);
    assert(a != NULL, "Memory allocation failed");
    p = a+n;
    diff = p-a;
  } else {
    a = p = NULL;
    diff = 0;
  }
  ...
}

【讨论】:

  • 我真的很想知道为什么标准不需要在 0 被传递给 malloc() 的情况下返回 NULL 指针。为什么标准会遇到这样的麻烦:“要么返回空指针,要么行为就像大小是某个非零值一样”。?
  • @machine_1 - 我猜在编写(第一个)标准时已经存在两种替代实现。
猜你喜欢
  • 2015-10-03
  • 2012-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-13
相关资源
最近更新 更多