以下C代码编译运行,但是否是undefined行为?

Following C code compiles and runs, but is it undefined bahaviour?

我发布了一个关于我之前在这个问题中遇到的一些指针问题的问题:

根据一些评论,我相信以下内容:

#include <stdlib.h>
#include <stdio.h>
int main(){
   int *p;
   *p = 1;
   printf("%d\n", *p);
   return 0;
}

是未定义的行为。这是真的?我一直这样做,我什至在我的 C 课程中看到过它。 然而,当我这样做时

#include <stdlib.h>
#include <stdio.h>
int main(){
   int *p=NULL;
   *p = 1;
   printf("%d\n", *p);
   return 0;
}

在打印 p 的内容之前(在行 *p=1; 之后),我遇到了段错误。这是否意味着我应该一直 mallocing 任何时候我实际为指向的指针赋值?

如果是这样,那为什么 char *string = "this is a string" 总是有效?

我很困惑,请大家帮忙!

是的,int *p; *p = 1; 是未定义的行为。您正在取消引用未初始化的指针(访问它指向的内存)。如果它有效,那只是因为 p 中的垃圾恰好是某个可写内存区域的地址,并且其内容不够重要,不会在您覆盖它们时立即导致崩溃。 (但您仍然可能已经损坏了一些重要的程序数据,导致您直到后来才注意到的问题...)

像这样明显的例子应该触发编译器警告。如果没有,请弄清楚如何调整编译器选项。 (在 gcc 上,尝试 -Wall -O)。

指针必须指向有效内存才能取消引用。这可能是由 malloc 分配的内存,或现有有效对象的地址 (p = &x;)。

char *string = "this is a string"; 非常好,因为这个指针没有被初始化;你初始化了! (char *string 中的 * 是其声明的一部分;您没有取消引用它。)具体来说,您使用您要求编译器保留并填充的内存地址初始化它字符 this is a string[=18=]。完成后,您可以安全地取消引用该指针(尽管只能读取,因为写入字符串文字是未定义的行为)。

is undefined behaviour. Is this true?

当然是。看起来它正在您的系统上使用您尝试过的方法运行,但您正在执行无效的写入。由于无效写入,您首先将 p 设置为 NULL 的版本是段错误,但它在技术上仍然是未定义的行为。

您只能写入已分配的内存。如果您 不需要 指针,最简单的解决方案就是使用常规的 int.

int p = 1;

一般来说,尽可能避免使用指针,因为自动变量更容易使用。

您的 char* 示例之所以有效,是因为字符串在 C 中的工作方式——内存中某处有一个序列为“this is a string[=27=]”的内存块,您的指针指向在那。不过,这将是只读内存,尝试更改它(即 string[0] = 'T';)是未定义的行为。

这个:

int *p;
*p = 1;

是未定义的行为,因为 p 没有指向任何地方。它未初始化。因此,当您尝试取消引用 p 时,您实际上是在写入一个随机地址。

undefined behavior的意思是不能保证程序会做什么。它可能会崩溃,可能会输出奇怪的结果,或者看起来工作正常。

这也是未定义的行为:

int *p=NULL;
*p = 1;

因为您正试图取消引用 NULL 指针。

这个有效:

char *string = "this is a string" ;

因为您正在使用字符串常量的地址初始化 string。这与其他两种情况不同。其实是这样的:

char *string;
string = "this is a string";

请注意,此处 string 并未被解除引用。指针变量本身正在被赋值。

同线

char *string = "this is a string";

您正在使指针 string 指向只读存储器中包含字符串 "this is a string" 的位置。 compiler/linker 将确保将此字符串放置在适合您的位置,并且指针 string 将指向正确的位置。因此,可以保证指针 string 指向有效的内存位置,而无需您采取任何进一步的操作。

但是,在代码中

int *p;
*p = 1;

p 未初始化,这意味着它未指向有效的内存位置。因此,取消引用 p 将导致未定义的行为。

不必总是使用malloc来使p指向有效的内存位置。这是一种可能的方式,但还有许多其他可能的方式,例如以下:

int i;
int *p;
p = &i;

现在 p 也指向一个有效的内存位置,可以安全地解除引用。

考虑代码:

#include <stdio.h>
int main(void)
{
  int i=1, j=2;
  int *p;
  ... some code goes here
  *p = 3;
  printf("%d %d\n", i, j);
}

语句 *p = 2; 会写入 ij 还是两者都不写入?如果 p 指向该对象,它将写入 ij,但如果 p 指向其他地方则不会。如果代码的 ... 部分没有对 p 做任何事情,那么 p 可能会指向 i,或者 j,或者stdout 对象,或任何东西。如果它恰好指向 ij,那么写入 *p = 3; 可能会影响该对象而没有任何副作用,但如果它指向 stdout 中控制位置的信息输出,它可能会导致以下 printf 以不可预测的方式运行。在一个典型的实现中,p 可能指向任何地方,并且 p 可能指向的东西太多了,不可能预测写入它们的所有可能影响。

请注意,该标准将许多操作归类为“未定义行为”,目的是许多甚至大多数实现将通过记录其行为来扩展语言的语义。例如,大多数实现扩展了 << 运算符的含义,以允许将其用于将负数乘以 2 的幂。即使在扩展语言以指定像 *p = 3; 这样的赋值的实现中, 总是 执行字大小的值 3 写入指定地址,无论结果如何,在对 p 的值一无所知的情况下,可以完全描述该操作的所有可能影响的平台 (*) 相对较少。在读取指针而不是写入指针的情况下,某些系统可能能够提供有关任意杂散读取影响的有用行为保证,但不是所有 (**)。

(*) 一些将代码保存在只读存储中的独立平台可能能够维护某些行为保证,即使代码写入任意指针地址也是如此。这种行为保证在状态可能被电子干扰破坏的系统中可能很有用,但即使针对此类系统写入杂散指针也永远不会有用。

(**) 在许多平台上,杂散读取要么产生没有副作用的无意义值,要么强制程序异常终止,但在 Apple II 上,Disk II 卡位于通常的插槽 6 位置,如果代码在执行磁盘访问后的一秒内从地址 0xC0EF 读取,驱动器磁头开始覆盖上次访问的磁道上的任何内容。这是设计使然(需要写入磁盘的软件通过访问地址 0xC0EF 来完成,并且让硬件响应读取和写入需要少一个逻辑门——因此也少一个芯片——比硬件所需的少一个芯片)仅响应写入)但确实意味着代码必须小心不要执行任何杂散读取。