“ int * nums = {5,2,1,4}”导致分段错误


81
int *nums = {5, 2, 1, 4};
printf("%d\n", nums[0]);

导致段错误,而

int nums[] = {5, 2, 1, 4};
printf("%d\n", nums[0]);

没有。现在:

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);

打印5。

基于此,我猜想数组初始化符号{}将数据盲目地加载到左侧的任何变量中。当它为int []时,将根据需要填充数组。当它为int *时,指针将被5填充,指针存储之后的内存位置将被2、1和4填充。因此nums [0]尝试取消引用5,从而导致段错误。

如果我错了,请纠正我。如果我是正确的,请详细说明,因为我不理解为什么数组初始化程序会以它们的方式工作。



3
在启用所有警告的情况下进行编译,编译器应告诉您发生了什么。
Jabberwocky

1
@GSerg几乎没有重复的地方。这个问题没有数组指针。即使该帖子中的某些答案与此处的答案相似。
伦丁

2
@Lundin我有30%的把握,所以我没有投票关闭,只张贴了链接。
GSerg

3
养成使用-pedantic-errorsflag运行GCC的习惯,并观察诊断。int *nums = {5, 2, 1, 4};是无效的
C。– AnT

Answers:


113

C语言中有一个(愚蠢的)规则,说任何简单变量都可以用大括号括起来的初始化程序列表进行初始化,就好像它是一个数组一样。

例如,您可以编写int x = {0};,它完全等同于int x = 0;

因此,在编写时,您int *nums = {5, 2, 1, 4};实际上是在为单个指针变量提供一个初始化列表。但是,它只是一个变量,因此只会分配第一个值5,其余的列表将被忽略(实际上,我不认为带有过多初始值设定项的代码甚至不应使用严格的编译器进行编译)-不会完全写入内存。该代码等效于 int *nums = 5;。也就是说,nums应该指向address 5

此时,您应该已经收到了两个编译器警告/错误:

  • 在不强制转换的情况下将整数分配给指针。
  • 初始化程序列表中的多余元素。

然后,当然,代码将崩溃并烧毁,因为5很可能不是您可以取消引用的有效地址nums[0]

附带说明,您应该printf使用说明%p符指向指针地址,否则将调用未定义的行为。


我不太确定您要在这里做什么,但是如果您想设置一个指向数组的指针,则应该这样做:

int nums[] = {5, 2, 1, 4};
int* ptr = nums;

// or equivalent:
int* ptr = (int[]){5, 2, 1, 4};

或者,如果您要创建一个指针数组:

int* ptr[] = { /* whatever makes sense here */ };

编辑

经过一些研究,我可以说“多余的元素初始化列表”实际上不是有效的C-它是GCC扩展

标准6.7.9初始化说(强调我的):

2 初始化程序不得尝试为未包含在正在初始化的实体内的对象提供值。

/-/

11标量的初始值设定项应为单个表达式,可以选择用大括号括起来。对象的初始值是表达式的初始值(转换后);采用与简单赋值相同的类型约束和转换,将标量的类型作为其声明类型的非限定版本。

“标量类型”是一个标准术语,指的是不具有数组,结构或联合类型的单个变量(这些变量称为“集合类型”)。

因此,该标准用通俗易懂的英语说:“初始化变量时,只要可以就随意在初始化表达式周围加一些花括号。”


11
用括在中的单个值初始化标量对象的能力没有什么“愚蠢”的{}。相反,它促进了C语言最重要,最方便的习惯用法之一-{ 0 }通用零初始化程序。C中的所有内容都可以通过进行零初始化= { 0 }。这对于编写与类型无关的代码非常重要。
AnT

3
@AnT没有“通用零初始化器”之类的东西。对于聚合,{0}仅意味着将第一个对象初始化为零,然后将其余对象初始化为好像具有静态存储期限。我会说这是巧合,而不是一些“通用初始化”的故意语言设计,因为{1}不初始化所有的对象为1
伦丁

3
@Lundin C11 6.5.16.1/1的内容p = 5;(为指针分配整数不满足列出的情况);6.7.9 / 11说分配的约束也用于初始化。
MM

4
@Lundin:是的,有。哪种机制初始化对象的哪个部分完全无关紧要。是否{}为此目的专门允许标量的无尾化也完全无关紧要。唯一重要的是,= { 0 }保证初始化器可以对整个对象进行零初始化,这正是使它成为C语言的经典之作,也是最优雅的C语言惯用法之一。
AnT

2
@伦丁:我也完全不清楚你对{1}这个话题的看法。从来没有人声称将其{0}解释0为聚合的每个成员的多重初始化器。
AnT

28

场景1

int *nums = {5, 2, 1, 4};    // <-- assign multiple values to a pointer variable
printf("%d\n", nums[0]);    // segfault

为什么会出现这一段错误?

您声明nums了一个指向int的指针-nums应该在内存中保存一个整数的地址。

然后,您尝试初始化nums多个值的数组。因此,在不深入研究细节的情况下,这在概念上不正确的-将多个值分配给应该保存一个值的变量是没有意义的。在这方面,如果您这样做,将会看到完全相同的效果:

int nums = {5, 2, 1, 4};    // <-- assign multiple values to an int variable
printf("%d\n", nums);    // also print 5

在任何一种情况下(将多个值分配给一个指针或一个int变量),然后发生的事情是该变量将获得第一个值5,而其余​​的值将被忽略。这段代码符合要求,但是您会为分配中不应包含的每个其他值收到警告:

warning: excess elements in scalar initializer

对于为指针变量分配多个值的情况,在访问时程序会出现段错误nums[0],这意味着您将按原义推迟地址5中存储的任何内容。nums在这种情况下,您没有为指针分配任何有效的内存。

值得注意的是,为int变量分配多个值的情况下没有段错误(您在这里没有引用任何无效的指针)。


场景2

int nums[] = {5, 2, 1, 4};

这不会发生段错误,因为您正在合法地在堆栈中分配4个int数组。


场景3

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);   // print 5

这不会像预期的那样发生段错误,因为您正在打印指针本身的值,而不是指针所引用的值(这是无效的内存访问)。


其他

每当您像这样对指针的值进行硬编码时,几乎总是注定会出现段错误(因为确定任务是哪个进程可以访问哪个内存位置是操作系统的任务)。

int *nums = 5;    // <-- segfault

因此,经验法则是始终初始化一个指向某些已分配变量地址的指针,例如:

int a;
int *nums = &a;

要么,

int a[] = {5, 2, 1, 4};
int *nums = a; 

2
+1这是一个很好的建议,但是考虑到许多平台上的神奇之处,“永不”实际上太强大了。(将常量表用于这些固定地址并没有指向现存变量,因此违反了您所说的规则。)诸如驱动程序开发之类的低级内容经常会处理这种情况。
Nate

3
“这是有效的”-忽略多余的初始化程序是GCC扩展;在标准C这是不允许的
MM

1
@TheNate-是的,您是正确的。我根据您的评论进行了编辑-谢谢。
artm

@MM-感谢您指出这一点。我进行了删除。
artm

25

int *nums = {5, 2, 1, 4};是格式错误的代码。有一个GCC扩展名,该扩展名将以下代码视为:

int *nums = (int *)5;

试图形成一个指向内存地址5的指针。(这对我来说似乎不是一个有用的扩展,但是我想开发人员很希望这样做)。

为了避免这种行为(或至少得到警告),您可以在标准模式下进行编译,例如-std=c11 -pedantic

有效代码的另一种形式是:

int *nums = (int[]){5, 2, 1, 4};

指向与相同存储时间的可变文字nums。但是,该int nums[]版本通常更好,因为它使用较少的存储空间,并且可以sizeof用来检测阵列的长度。


复合文字形式的数组是否可以保证至少具有的使用寿命nums
超级猫

@supercat是的,如果nums是自动的,它是自动的,如果nums是静态的,它是静态的
MM

@MM:即使nums在函数中声明了静态变量,这是否也适用?或者,即使分配给静态变量,编译器是否有权将数组的生存期限制为封闭块的生存期?
超级猫

@supercat是(第一位)。第二个选项表示第二次调用该函数的UB(因为静态变量仅在第一次调用时初始化)
MM

12
int *nums = {5, 2, 1, 4};

nums是类型的指针int。因此,您应该将此指向某个有效的内存位置。num[0]您正在尝试取消引用某些随机存储器位置,从而导致分段错误。

是的,指针持有值5,并且您正在尝试取消引用它,这是系统上未定义的行为。(看起来5不是您系统上的有效内存位置)

鉴于

int nums[] = {1,2,3,4};

是一个有效的声明,您要说的nums是类型数组,int并且根据初始化期间传递的元素数量分配内存。


1
“是的,指针保持值5,并且您正在尝试取消引用它,这是未定义的行为。” 完全没有,这是完美的行为和定义明确的行为。但是在OP使用的系统上,它不是有效的内存地址,因此会导致崩溃。
伦丁

@伦丁同意。但是我认为OP从来都不知道5是有效的内存位置,所以我在这些行上说过。希望编辑对您
有所

应该是这样吗?int *nums = (int[]){5, 2, 1, 4};
Islam Azab

10

通过分配 {5, 2, 1, 4}

int *nums = {5, 2, 1, 4};

您将分配给5 nums(在从int到int的指针的隐式类型转换之后)。取消引用它会调用处的存储位置0x5。程序可能无法访问该目录。

尝试

printf("%p", (void *)nums);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.