首页 > 解决方案 > 从未初始化的变量创建指针是否有副作用?

问题描述

我在空闲时间学习 C。当程序行为让我感到困惑时,我正在玩指针。请有人解释(或参考一些读数)为什么我在以下情况下有不同的结果?

Ubuntu 19.04
cc (Ubuntu 8.3.0-6ubuntu1) 8.3.0
英特尔 i7-8565U

#include <stdio.h>

int main() {
//    int a = 6;
    int i1 = 5;
    printf("&i1 = %p\n", &i1);

    size_t i1_address = (size_t) &i1;
    int *p = (int *) (i1_address + 4);
    printf("p = %p\n", p);
    *p = 12;

    int i2;
//    printf("&i2 = %p\n", &i2);
    printf("i2 = %d\n", i2);

    return 0;
}

上面的代码完全符合我的期望:

&i1 = 0x7ffd86048110
p = 0x7ffd86048114
i2 = 12

如果我取消注释两个注释行,输出几乎相同(i2 = 12)。
但是如果我只取消注释第一个注释的行 ( int a = 6;)i2等于某个随机数:

&i1 = 0x7ffd539630fc
p = 0x7ffd53963100
i2 = 21901

任何解释究竟如何int a = 6;影响程序,所以我得到意想不到的结果以及如何printf("&i2 = %p\n", &i2);修复它?

标签: cpointers

解决方案


在这个答案中,我将讨论每条重要的线。

int a = 6;

此行的存在与 C 代码的含义无关(即,与 C 标准对其行为的规定)。如果它影响正在运行的程序,很可能只是因为它恰好影响了编译器以某种不受控制的方式(即,以某种并非特别刻意设计到编译器中的方式)在内存中排列局部变量的方式。它影响程序的事实是分散注意力并且不是很有意义。

int i1 = 5;

好吧,这是一条正常的线路。

printf("&i1 = %p\n", &i1);

这在技术上是错误的;它应该是printf("&i1 = %p\n", (void *) &i1);,因为%p指定用于void *但不与其他指针类型一起使用。但是,它不会影响大多数 C 实现。

size_t i1_address = (size_t) &i1;

size_t不保证保存有关指针的所有信息。最好#include <stdint.h>使用和使用uintptr_t而不是size_t.

int *p = (int *) (i1_address + 4);

这假设(我们从上下文推断) 的大小int是 4,并且转换&i1size_t、加 4 和转换为的结果会int *产生一个指向刚刚超出 的指针i1。我认为问题中提到的“cc”是 GCC 的某个版本,在这种情况下,这是可以的,因为 GCC 支持执行这种地址算术(我相信从记忆中,无需查找特定文档)。

printf("p = %p\n", p);

如上所述,这应该是printf("p = %p\n", (void *) p);.

*p = 12;

这是不好的。p不是指向已知对象。在 C 标准使用的计算模型中,它根本不指向对象,因此表达式的行为*p不是由标准定义的,也没有为它分配任何东西。与 C 标准未定义的某些行为(例如某些地址算术)不同,GCC 不对此类滥用做出任何承诺。

int i2;

美好的。

printf("&i2 = %p\n", &i2);

这也应该printf("&i2 = %p\n", (void *) &i2);

printf("i2 = %d\n", i2);

在标准的模型中,i2不确定的,因为它还没有被初始化(包括通过赋值)。“不确定”不仅意味着它没有特定的价值,而且可能根本没有任何价值,因为它具有从使用到使用持续存在的价值。虽然 的值i2是不确定的,但 C 标准允许对它的每次使用都表现得好像它具有不同的值或陷阱表示。(在没有包含 的先前声明的情况下,&i2使用i2在此语句中将具有未定义的行为,因为 C 标准中的特定规则说使用具有本地存储持续时间且尚未获取其地址的未初始化对象具有 C 标准未定义的行为。使用前面的语句,只有一个不确定的值,而不是未定义的行为。)

据我所知,Ubuntu 上的 GCC 没有int对象的陷阱表示,因此printf("i2 = %d\n", i2);它本身i2. 这不是未定义的行为,只是没有完全指定的行为。(然而,由于该语句之前有未定义行为的语句,我们不知道程序执行是否会到达该语句,并且,如果确实如此,C 标准不会告诉我们会发生什么,因为之前的未定义行为会使随后的行为也未定义。)

可能*p = 12;12 放在用于 的空间中i2,因此printf("i2 = %d\n", i2);可能会显示 12 用于i2。当然,C 标准不以任何方式要求这一点,但 GCC 可能会这样做,并且是否这样做可能会受到语句是否存在或不存在的int a = 6;影响printf("i2 = %p\n", &i2)。然而,再一次,由于陈述的存在或不存在而导致的这些行为变化都不是非常有意义的。了解编译器如何运行的更好方法是检查它生成的汇编语言以及源代码和编译器开关中的各种变体。(使用 GCC,用于-S生成汇编语言。)

(人们可以通过阅读源代码来了解更多关于编译器行为的信息,但这对很多人来说并不是更好,因为在源代码可以被敏感解释之前,它需要大量的工作来积累所需的知识。)


推荐阅读