在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?
使用指向one-past-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 Leffler在comment指出的那样,这对动态分配的内存有效的另一个原因是:
§7.22.3(p1):
通过连续调用
aligned_alloc
,calloc
,malloc
和realloc
函数分配的存储顺序和连续性未指定。如果分配成功,则返回指针,以便可以将其分配给指向具有基本对齐要求的任何类型对象的指针,然后用于在分配的空间中访问此类对象或此类对象的数组(直到空间被明确释放)。
malloc
在上面的片段中返回的指针被分配给d
,分配的内存是5个int
对象的数组。
针对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
。
是的,相同的规则适用于具有动态和自动存储持续时间的变量。它甚至适用于单个元素的malloc
请求(标量相当于这方面的单元素数组)。
指针算法仅在数组中有效,包括一个超过数组末尾的数组。
在解除引用时,重要的是要注意一个考虑因素:关于初始化int a[5] = {0};
,编译器不得试图在表达式a[5]
中取消引用int* p = &a[5]
;它必须编译为int* p = a + 5;
再次,同样的事情适用于动态存储。
使用指向one-past-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.31
foo(0)
- > malloc(0)
可能会返回NULL
或non-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;
}
...
}