使用指向一次性分配内存的指针是明确定义的吗?

47
在C语言中,可以创建一个指针,使其指向数组最后一个元素的下一个位置,并在指针算术运算中使用它,只要你不对其进行解引用操作就可以。
int a[5], *p = a+5, diff = p-a; // Well-defined

然而,这些是未定义行为:

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?

2
好的,你标记了C。而C++的new是一个不同的东西。它不仅仅是内存分配。此外,C++语言专家会说,仅仅写入由malloc返回的内存并不会在那里创建一个对象,更不用说使内存具有有效类型了。 - StoryTeller - Unslander Monica
10
只要不对指针进行解引用操作,实际上可以将指针指向任何地方。即使这样可能毫无意义,你仍然可以将其与其他指针进行比较。 - Some programmer dude
3
@Someprogrammerdude,那太疯狂了。那不是 UB 吗? - iBug
6
@Someprogrammerdude - 但我认为你不能通过任何方式在任何地方获取那个指针。例如,正如 iBug 指出的那样,您无法执行指针算术运算。这本身就是未定义行为。您可以将整数常量强制转换为指针,但不能保证它与 a + 6 相同。 - StoryTeller - Unslander Monica
2
ISO/IEC 9899:2011 §7.22.3 内存管理函数 ¶1 对于连续调用aligned_alloccallocmallocrealloc函数所分配的存储空间的顺序和连续性是未指定的。如果分配成功,则返回的指针适当地对齐,以便将其分配给任何具有基本对齐要求的对象类型的指针,然后用于访问在分配的空间中分配的此类对象或此类对象的数组(直到显式释放该空间)。 它说“这样的对象数组”——对于数组来说是可以的;因此在这里也是可以的。 - Jonathan Leffler
显示剩余11条评论
4个回答

26

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
3
是的,你不能指望它能够正常工作。如果指针操作数和结果分别指向同一数组对象的元素或者越过该数组对象的最后一个元素,则此评估不会产生溢出;否则,行为未定义。 - user694733
@iBug 标准规定的定义行为仅限于 粘贴数组对象的最后一个元素。如果您继续前进(超过最后一个元素2个),标准未指定足够的内容,这就足以成为未定义行为。 - Serge Ballesta
@iBug,你的例子引发了一个特别的问题,即指针算术中的溢出在C++中是未定义行为。因此,规则基本上规定,malloc永远不会分配内存的最后一个字节,除非编译器同时定义了溢出的方式,使得这些溢出问题变得不可见。 - Cort Ammon
1
目前最高N编号的WG14论文是N2184。你从哪里得到了N4296? - T.C.
@T.C. N4296听起来像是C++17的早期草案。 - iBug

23

如果p指向已分配内存的末尾并且没有被解引用,那么使用指向一次性malloc的指针是良好定义的。

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 Leffler评论中指出的那样,这也适用于动态分配内存的另一个原因是:}}

§7.22.3 (p1)

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

上面代码段中malloc返回的指针被分配给d,所分配的内存是5个int对象的数组。


2
从正式的角度来看,指向 d 的数据如何最终成为一个数组?根据 C 标准,malloc 分配的数据的有效类型是用于 lvalue 访问的类型。这个类型是 int,而不是 int[5] - Lundin
2
@Lundin;不,它并不是。d是一个指向由malloc分配的内存块的第一个块的指针。 - haccks
1
引用的文本只显示分配的存储可以用于存储数组,而不是数据如何成为数组。假设我执行 int(*ptr)[5] = malloc_chunk; memcpy(something, ptr, 5*sizeof(int); 然后我将有效类型设置为数组类型。但是如果没有这样的代码,“块”就不是正式的数组类型。我认为标准中没有任何有意义的文本可引用此处,关于有效类型(和严格别名)的规则非常糟糕。 - Lundin
“until”这个词在这里是含糊的(甚至是错误的):直到指针指向分配内存的末尾之一,它才被定义明确。 根据您的回答,当指针指向一个过去的位置时仍然是正确的,但是“until”的意思是“当它发生时不再正确”,因此最好找到更好的措辞。 - iBug
1
编译器可能会认为两个lvalue的使用在它们之间没有任何外部访问的特定证据的情况下是未排序的,并且如果代码中没有外部访问的证据,编译器可能会将对函数或循环的访问提升或推迟到其开头/结尾。给定 void test(int *ip, float *fp, int mode) { *ip=1; *fp=2; if (mode) *ip=1;}; 如果 ipfp 别名,则 Effective Type 规则要求存储的 Effective Type 保持为 intfloat,具体取决于 mode,但没有证据表明这一点有任何影响。 - supercat
显示剩余3条评论

7
是的,动态存储期和自动存储期的变量都适用相同的规则。这甚至适用于单个元素的malloc请求(在这方面,标量等效于一个元素数组)。
指针算术运算仅在数组内有效,包括数组结束后的一个位置。
在解引用时,需要注意以下一点:关于初始化int a[5] = {0};,编译器不得尝试解引用a[5]在表达式int* p = &a[5]中;它必须将其编译为int* p = a + 5;同样的事情也适用于动态存储。

int* p = &a[5]; 中,a[5] 没有被解引用。它相当于 int p = a + 5; 或者我可能理解错了这段话。 - haccks
4
我想说的是,使用表达式&a[5]不会出现未定义行为,因为编译器必须将其视为a + 5。这句话读起来不好吗?由于周末实现了这个功能,我感冒了。链接如下:https://meta.stackexchange.com/questions/303920/winter-bash-2017-counting-down-page-whats-with-the-fence/303921#303921 - Bathsheba

7

指向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) 可以返回NULL非NULL。在第一个实现中,返回NULL不是“内存分配失败”。
这意味着代码正在尝试使用int *p = NULL + 0;来进行指针数学运算,而这与int *p = a+n;的保证不符,或者至少会引起这样的代码问题。
可移植代码通过避免零大小分配获益。
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;
  }
  ...
}

我真的很想知道为什么标准规定malloc()在传入0时不需要返回NULL指针。为什么标准要费力地声明:“要么返回null指针,要么行为就像大小是非零值一样。”? - machine_1
2
@machine_1 - 我猜在制定第一个标准时已经存在两种备选实现。 - Oliver Charlesworth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接