在C语言中,获取无效指针的值是未定义的行为还是实现定义的行为?

5

在C++中,获取无效指针的值是一种实现定义的行为,参考此处。现在考虑以下C程序:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int* p=(int*)malloc(sizeof(int));
    *p=3;
    printf("%d\n",*p);
    printf("%p\n",(void*)p);
    free(p);
    printf("%p\n",(void*)p); // Is this undefined or implementation defined in behaviour C? 
}

但是在C语言中,行为是否相同?上述C程序的行为是未定义的还是实现定义的?C99/C11标准对此有何规定? 请告诉我C99和C11中的行为是否不同。


3
在C语言中,你不应该转换malloc返回的值。对于你的问题,我不知道是否有规定,但是free不会改变p的内容。随时读取(或更改)它的内容都是有效的,就像任何变量一样(当然,前提是它已经被初始化,这在这里是成立的)。唯一禁止的操作是解除其值的引用,就好像它已经被释放一样。 - hexasoft
1
为什么不会是undefined?如果malloc()实现使用mmap()来满足malloc()请求,那么在调用free()时它很可能会munmap()指针地址的内存。该指针很可能包含无效地址。 - Andrew Henle
2
@PeterCordes 那就太简单了,我们也不会探索 C 标准的边界了。 - Andrew Henle
@AndrewHenle:哦,是的,从标题上我看到这实际上是预期问题。很明显。显然,我太过于热衷于汇编语言,甚至没有考虑答案不是一个微不足道的“不,它是允许的”的可能性。 - Peter Cordes
2
“@TheParamagneticCroissant,我认为Peter Cordes在他对我的答案的评论中说得最好 - 一些硬件可以将指针视为特殊类型的数据,并且访问不确定的指针值可能*会触发陷阱。 因此,是未定义行为。” - Andrew Henle
显示剩余9条评论
4个回答

7
扩展Andrew Henle的回答:
根据C99标准,6.2.4:
“一个对象具有确定其生命周期的存储期。有三种存储期:静态、自动和分配。分配的存储在7.20.3中描述。当指向它(或刚刚过去)的对象到达其生命周期的末尾时,指针的值变得不确定。”
然后,在7.20.3.2中,标准继续描述了malloc()、calloc()和free(),并提到:
“free函数导致ptr指向的空间被释放。”
在3.17.2中:
"不确定值"可以是未指定的值或陷阱表示。
在6.2.6.1.5中:
某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示形式,并且由不具有字符类型的lvalue表达式读取,则行为是未定义的。这种表示称为陷阱表示。
由于指针变得不确定,不确定值可能是陷阱表示,并且您有一个lvalue变量,并且读取lvalue陷阱表示是未定义的,因此行为可能未定义。

6.2.6.1.5的措辞意味着,只有当p实际上包含一个陷阱表示时,它才是未定义行为。由于普通平台没有指针的任何陷阱表示,因此在那些非常奇怪的平台上才会出现未定义行为,这些平台可能会在将指针加载到特殊指针寄存器时检查指针的有效性。(也许允许实现在指针加载时执行虚拟到物理的转换,而不是在每次解引用时执行?)在像x86这样的普通CPU上,只有FP类型具有陷阱表示。 - Peter Cordes
@PeterCordes 啊,是的,你说得对!我已经修改了我的回答措辞。 - The Paramagnetic Croissant
总之,与大多数未定义行为不同,这取决于目标平台。因此,除非编译针对非常奇特的硬件,否则没有编译器做奇怪的事情的风险。实际上可能没有硬件是按照这种方式工作的。我猜C标准最终涵盖了这种情况,是因为它选择如何定义术语和措辞。(即指向已释放内存的指针与未初始化的FP数据属于同一类别。)无论如何,挖掘所有拼图碎片以制作这个答案非常出色。 - Peter Cordes
1
@PeterCordes “可能没有实际工作的硬件是这样的” - 当然,我并没有断言相反的事情。只是说它可能是未定义的,从语言律师的角度来看,这是唯一重要的事情。 - The Paramagnetic Croissant
是的,没错。这就是为什么意识到它不仅在真实硬件上运行,而且在任何正常的CPU上都不是未定义行为,这一点非常重要。如果你的目标是超级奇怪的硬件,在这种情况下,这将是许多移植问题之一,因此在我看来,编写在这些系统上可能具有未定义行为的代码通常不是一个问题。 - Peter Cordes

4
根据C标准第6.2.4节
对象的生命周期是程序执行期间为其保留存储空间的部分。对象存在,具有固定地址,并在其生命周期内保留其最后存储的值。如果在对象的生命周期外引用对象,则行为未定义。当指向的对象(或刚刚超过其生命周期)到达其生命周期的末尾时,指针的值变得不确定。

3
关键的句子是最后一句,而不是你加粗的那句话。这里没有访问指向的对象,只有指针本身被访问。 - interjay
但是在这里,他只是打印了p的内容。它的内容仍然存在(因为p仍然存在于main的上下文中),即使地址没有意义。它仍然可以用于其他目的(不知道是什么目的,而且似乎是个坏主意,但p是可用的)。 - hexasoft
1
@AndrewHenle 为什么是死路?如果它是一个陷阱表示,那么显然使用它是未定义行为。 - The Paramagnetic Croissant
1
@TheParamagneticCroissant说,即使检查一个不指向有效内存的指针的值也是未定义行为。也许某些硬件将指针与整数区别对待,例如将值加载到指针寄存器中会进行访问检查?(编译器可能通过加载/存储到/从指针寄存器来复制指针值?)我猜这就像动态定义陷阱表示一样。非常奇怪,但我想我可以想象有一些工作方式类似的硬件。 - Peter Cordes
1
@PeterCordes:将一个指针作为右值进行评估可能会导致编译器生成代码,尝试将指针的一部分用作类似于段描述符的东西,并且在无效指针上尝试这样的操作可能会导致糟糕的结果。实际上,编译器不太可能浪费时间为从未解引用并且从未用于指针算术运算的指针加载段描述符,但委员会希望留下编译器可能会因无效指针而崩溃的可能性,即使这样做会排除本来有用的优化... - supercat
显示剩余8条评论

0

如果编译器正确确定代码将不可避免地获取已传递给“free”或“realloc”的对象的指针,即使代码不会使用由此标识的对象,标准也不会对编译器在此之后可以或不可以做出什么要求。

因此,使用类似以下结构的语句:

char *thing = malloc(1000);
int new_size = getData(thing, ...whatever); // Returns needed size
char *new_thing = realloc(thing, new_size);
if (!new_thing)
  critical_error("Shrinking allocation failed!");
if (new_thing != thing)
  adjust_pointers(thing, new_thing);
thing = new_thing;

在大多数实现中,使用realloc收缩已分配块时,代码可能允许保存重新计算某些指针的工作量,但是如果实现无条件报告收缩分配失败,则没有任何不合法之处,因为如果它没有失败,代码将不可避免地尝试涉及realloc'ed块的指针比较。同样,对于实现来说,保留检查realloc是否返回null,但允许任意代码执行(如果它没有失败)也是同样合法的,尽管这样做效率较低。

就个人而言,我看不出阻止程序员确定测试何时可以跳过某些步骤有什么好处。如果指针没有更改,则跳过不必要的代码可能会在使用realloc收缩内存块的情况下产生显着的效率改进(这种操作允许移动块,但在大多数实现中通常不会),但是当前流行的编译器会应用自己的激进优化,这将破坏试图使用这种技术的代码。


-2

继续讨论评论。我认为对于指针的有效性或无效性的困惑在于询问了指针的哪个方面。在上面,free(p); 影响的是由 p 指向的内存块的起始地址,而不是 p 本身的地址,后者仍然有效。 p 不再持有地址(作为它的值),使其变得不确定,直到重新分配。下面是一个简短的例子:

#include <stdio.h>
#include <stdlib.h>

int main (void) {

    int *p = NULL;

    printf ("\n the address of 'p' (&p) : %p\n", &p);

    p = malloc (sizeof *p);
    if (!p) return 1;

    *p = 3;

    printf (" the address of 'p' (&p) : %p   p points to %p   with value  %d\n",
            &p, p, *p);

    free (p);

    /* 'address of p' unchanged, p itself indeterminate until reassigned */
    printf (" the address of 'p' (&p) : %p\n\n", &p);

    p = NULL;  /* p no longer indeterminate and can be allocated again */

    return 0;
}

输出

$ ./bin/pointer_addr

 the address of 'p' (&p) : 0x7fff79e2e8a0
 the address of 'p' (&p) : 0x7fff79e2e8a0   p points to 0x12be010   with value  3
 the address of 'p' (&p) : 0x7fff79e2e8a0

mallocfree不会改变p本身的地址。受影响的是p的值(或更正确地说,p存储为其值的地址)。在free时,p存储的地址被释放到系统中,并且不能再通过p访问。一旦您明确重新分配p = NULL;p就不再是不确定的,可以再次用于分配。)


2
“它并不影响p本身” - 大多数情况下是这样的。但是实现可以将其更改为NULL或其他任何值,因为其值变得不确定。 “它输出我在我的系统上预期的东西”并不是证明它不是未定义行为。 - The Paramagnetic Croissant
1
在现代的64位CPU上工作并不意味着它能够在所有硬件上都正常运行。显然,一个具有通用寄存器的“普通”CPU不会有问题。我认为值很可能不会改变。我敢打赌,它要么按预期工作,要么出现故障。 - Peter Cordes
@TheParamagneticCroissant,这里没有尝试访问一个不确定的值。你认为这个例子不能证明什么? - David C. Rankin
@DavidC.Rankin:“没有尝试访问上述不确定值”的说法是错误的。因为调用free()后指针本身变得不确定,指向的对象的生命周期已经结束。这个例子并不能证明您的断言,即使用已释放的指针不是未定义行为。 - The Paramagnetic Croissant
1
谢谢。我知道使用方法,但是没有考虑到为了示例而将p的地址作为引用不确定值(实际上并不是这样),但现在从讨论的角度来看,p(而不是p的地址)在那个时间点上是不确定的。(我猜需要再喝一杯咖啡...) - David C. Rankin
显示剩余4条评论

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