c - 指针可以接受什么处理并且仍然有效?

标签 c pointers undefined-behavior

以下哪种处理和尝试恢复 C 指针的方法可以保证有效?

1) 转换为空指针并返回

int f(int *a) {
    void *b = a;
    a = b;
    return *a;
}

2)转换为适当大小的整数并返回
int f(int *a) {
    uintptr_t b = a;
    a = (int *)b;
    return *a;
}

3) 几个简单的整数运算
int f(int *a) {
    uintptr_t b = a;
    b += 99;
    b -= 99;
    a = (int *)b;
    return *a;
}

4) 整数运算足以掩盖出处,但仍然会使值保持不变
int f(int *a) {
    uintptr_t b = a;
    char s[32];
    // assume %lu is suitable
    sprintf(s, "%lu", b);
    b = strtoul(s);
    a = (int *)b;
    return *a;
}

5)更多的间接整数运算,将保持值不变
int f(int *a) {
    uintptr_t b = a;
    for (uintptr_t i = 0;; i++)
        if (i == b) {
            a = (int *)i;
            return *a;
        }
}

显然情况 1 是有效的,情况 2 肯定也是有效的。另一方面,我看到了 Chris Lattner 的一篇帖子——不幸的是我现在找不到它——说类似于案例 5 的东西是无效的,标准许可编译器只将其编译为无限循环。然而,每个案例看起来都是前一个案例的无可争议的延伸。

有效案例和无效案例之间的界线在哪里?

根据评论中的讨论添加:虽然我仍然找不到启发案例 5 的帖子,但我不记得涉及什么类型的指针;特别是,它可能是一个函数指针,这可能就是为什么那个案例显示无效代码而我的案例 5 是有效代码。

第二个补充:好的,这是另一个消息来源,说有问题,我确实有一个链接。 https://www.cl.cam.ac.uk/~pes20/cerberus/notes30.pdf - 关于指针出处的讨论 - 说,并用证据支持,不,如果编译器无法跟踪指针来自哪里,这是未定义的行为。

最佳答案

根据C11 draft standard :

示例 1

有效,根据 §6.5.16.1,即使没有明确的强制转换。

示例 2
intptr_tuintptr_t类型是可选的。将指针分配给整数需要显式转换(第 6.5.16.1 节),尽管 gcc 和 clang 只会在您没有时才会警告您。有了这些注意事项,往返转换在 §7.20.1.4 之前有效。 预计到达时间: John Bellinger 提出只有当您对 void* 进行中间转换时才会指定行为。双向。但是,gcc 和 clang 都允许直接转换作为文档扩展。

示例 3

安全,但只是因为您使用的是无符号算术,它不会溢出,因此可以保证得到相同的对象表示。安 intptr_t可能溢出!如果你想安全地进行指针运算,你可以将任何类型的指针转​​换为 char*然后在同一结构或数组中添加或减去偏移量。记住,sizeof(char)总是 1 . 预计到达时间:该标准保证两个指针比较相等,但您与 Chisnall 等人的链接。给出了编译器仍然假设两个指针互不别名的例子。

示例 4

永远,永远,永远 无论何时读取,尤其是写入缓冲区时,请检查缓冲区溢出!如果您可以数学证明静态分析不会发生溢出?然后写出证明这一点的假设,明确地,和 assert()static_assert()他们没有改变。使用 snprintf() ,而不是已弃用的、不安全的 sprintf() !如果您从这个答案中没有记住任何其他内容,请记住!

绝对迂腐,这样做的便携方法是使用 <inttypes.h> 中的格式说明符。并根据任何指针表示的最大值定义缓冲区长度。在现实世界中,您可以使用 %p 打印出指针。格式。

不过,您打算提出的问题的答案是肯定的:重要的是您要返回相同的对象表示。这是一个不那么做作的例子:

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int i = 1;
    const uintptr_t u = (uintptr_t)(void*)&i;
    uintptr_t v;

    memcpy( &v, &u, sizeof(v) );
    int* const p = (int*)(void*)v;

    assert(p == &i);
    *p = 2;
    printf( "%d = %d.\n", i, *p ); 

    return EXIT_SUCCESS;
}

所有重要的是对象表示中的位。此代码还遵循 §6.5 中的严格别名规则。它在给 Chisnall 等人带来麻烦的编译器上编译并运行良好。

例 5

这工作,与上面相同。

一个永远不会与您的编码相关的非常迂腐的脚注:一些过时的深奥硬件具有带符号整数的补码或符号和大小表示,并且在这些上,可能有一个不同的负零值可能或可能不是陷阱。在某些 CPU 上,这可能是不同于正零的有效指针或空指针表示。在某些 CPU 上,正零和负零可能比较相等。

聚苯乙烯

标准说:

Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.



此外,如果两个数组对象是同一个多维数组的连续行,则第一行末尾之后的行是指向下一行开头的有效指针。因此,即使是故意导致标准允许的尽可能多的错误的病态实现也只能在您操纵的指针与数组对象的地址进行比较时才这样做,在这种情况下,实现理论上可能决定将其解释为取而代之的是某个其他数组对象的最后一个。

预期的行为显然是指针比较等于 &array1+1并到 &array2等同于两者:这意味着让你将其与 array1 内的地址进行比较或取消引用它以获得 array2[0] .然而,标准实际上并没有这么说。

缴费灵

标准委员会has addressed some of these issues并建议 C 标准明确添加有关指针出处的语言。这将确定是否允许符合要求的实现假设由位操作创建的指针不别名另一个指针。

具体来说,提议的更正将引入指针出处,并允许具有不同出处的指针不相等。它还将引入 -fno-provenance选项,这将保证任何两个指针比较相等当且仅当它们具有相同的数字地址。 (如上所述,比较相等别名的两个对象指针。)

关于c - 指针可以接受什么处理并且仍然有效?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41381598/

相关文章:

c - 在不违反 C99 中严格的别名规则的情况下使用 void * 输入双关语

c - 每相差 7 的倍数加 1

c++ - C++ 标准到底在哪里说取消引用未初始化的指针是未定义的行为?

c++ - 用逗号相互依赖初始化?

c - 关于 SNMP 中代理的 MIB 处理

c++ - 如何查看我发送的流 libvlc C/C++

c - 函数中的数组成员比较不起作用

c - 将指针传递给头指针,void insertNode 函数?

c - 找出数字 5 在 0 - 4322 内出现了多少次

C++ - 以下代码会导致未定义的行为吗?