在C语言中,指针和数组之间存在非常重要的区别。

12

我曾经认为我真的理解了这个问题,并且重新阅读标准(ISO 9899:1990)只是确认了我的明显错误的理解,所以现在我在这里提问。

以下程序会崩溃:

#include <stdio.h>
#include <stddef.h>

typedef struct {
    int array[3];
} type1_t;

typedef struct {
    int *ptr;
} type2_t;

type1_t my_test = { {1, 2, 3} };

int main(int argc, char *argv[])
{
    (void)argc;
    (void)argv;

    type1_t *type1_p =             &my_test;
    type2_t *type2_p = (type2_t *) &my_test;

    printf("offsetof(type1_t, array) = %lu\n", offsetof(type1_t, array)); // 0
    printf("my_test.array[0]  = %d\n", my_test.array[0]);
    printf("type1_p->array[0] = %d\n", type1_p->array[0]);
    printf("type2_p->ptr[0]   = %d\n", type2_p->ptr[0]);  // this line crashes

    return 0;
}

根据我对标准的解释,比较表达式my_test.array[0]type2_p->ptr[0]

6.3.2.1 数组下标

“下标运算符[]的定义为E1[E2]与(*((E1)+(E2)))相同。”

应用这个定义可得:

my_test.array[0]
(*((E1)+(E2)))
(*((my_test.array)+(0)))
(*(my_test.array+0))
(*(my_test.array))
(*my_test.array)
*my_test.array

type2_p->ptr[0]
*((E1)+(E2)))
(*((type2_p->ptr)+(0)))
(*(type2_p->ptr+0))
(*(type2_p->ptr))
(*type2_p->ptr)
*type2_p->ptr
type2_p->ptr指向一个类型为"指向int的指针"的对象,其值是my_test的起始地址。因此,*type2_p->ptr评估为一个整数对象,其存储位于与my_test相同的地址处。

进一步来说:

根据C语言规范6.2.2.1中的描述: "除非它是sizeof运算符或一元&运算符的操作数,...,具有类型类型数组的lvalue转换为类型类型的指针的表达式,它指向数组对象的初始元素,并且不是左值。"

my_test.array的类型为"int数组",按照上述规则,被转换为类型为"指向int的指针"的表达式,其值为第一个元素的地址。因此,*my_test.array评估为一个整数对象,其存储位于数组的第一个元素的地址处。

最后,在C语言规范6.5.2.1中描述了结构体和联合体说明符:

针对结构体对象进行适当转换的指针指向其初始成员...反之亦然。在结构体对象内部可能会存在未命名的填充,但不会在其开头处,因为需要满足适当的对齐要求。

由于type1_t的第一个成员是数组,所以该数组和整个type1_t对象的起始地址与上述相同。因此,我认为*type2_p->ptr评估为一个整数对象,其存储位于数组中第一个元素的地址处,因此等同于*my_test.array的值。

但是,这不可能是正确的,因为在solaris、cygwin和gcc版本2.95.3、3.4.4和4.3.2上程序总是崩溃,因此任何环境问题都是完全排除的。

我的推理有什么问题/我没有理解的地方在哪里?如何声明type2_t使得指针ptr指向数组的第一个成员?
4个回答

11

如果我在分析中忽略了任何东西,请原谅我。但我认为所有这一切的根本错误在于这个错误的假设

type2_p->ptr 的类型为 "指向 int 的指针",其值是 my_test 的起始地址。

没有任何东西能使它具有那个值。相反,很可能它指向

0x00000001
因为你所做的就是将组成整数数组的字节解释为指针。然后你会加上一些内容并进行下标运算。
此外,我非常怀疑你转换到另一个结构体是否真的有效(也就是说,保证能够工作)。如果两个结构体都是联合体的成员,则可以转换并读取它们的公共初始序列。但在你的示例中它们不是联合体的成员。你也可以将其转换为第一个成员的指针。例如:
typedef struct {
    int array[3];
} type1_t;

type1_t f = { { 1, 2, 3 } };

int main(void) {
    int (*arrayp)[3] = (int(*)[3])&f;
    (*arrayp)[0] = 3;
    assert(f.array[0] == 3);
    return 0;
}

感谢您正确指出我的错误假设(type2_t *type2_p = (type2_t *) &my_test; 类型转换)。很抱歉没有接受您的答案,但我会选择Chuck的答案,因为我认为它更加精确。 - hlovdal

10

数组是一种存储方式。语法上,它被用作指针,但在物理上,该结构中没有“指针”变量,只有三个整数。另一方面,int指针是实际存储在结构体中的数据类型。因此,当您执行转换时,您可能会使ptr获取数组中第一个元素的值,即1。

*我不确定这是否实际上是定义行为,但至少在大多数常见系统上,它将起作用。


这绝对是定义良好的行为。ptr的地址与my_array的地址相同。my_array实际上是指向结构体的指针,而ptr只是结构体内的整数指针。 - Vitali
2
“定义行为”并不意味着“某些事情发生了”,而是指“发生的某些事情由标准定义”。类型转换是未定义的行为。如果你想看到在类型转换时发生一些令人惊讶的事情,可以将编译器的优化提高一两个档次。 - Logan Capaldo

3

我的推理错在哪里/我不明白的是什么?

type_1::array(不是严格的C语法)不是一个int *类型,它是一个int [3]类型。

如何声明type2_tptr指向数组的第一个成员?

typedef struct 
{    
    int ptr[];
} type2_t;

这声明了一个灵活的数组成员。根据C标准(6.7.2.1第16段):
但是,当一个“.”(或“->”)运算符有一个左操作数,它是一个带有灵活数组成员的结构体(指针),并且右操作数命名该成员时,它的行为就像该成员被替换为最长的数组(具有相同元素类型),这不会使结构体比所访问的对象更大;即使这将与替换数组的偏移量不同,数组的偏移量也应保持为灵活的数组成员。
也就是说,它可以适当地别名type1_t::array。

0

必须是定义行为。 以内存为例思考。

为简单起见,假设my_test位于地址0x80000000处。

type1_p == 0x80000000
&type1_p->my_array[0] == 0x80000000 // my_array[0] == 1
&type1_p->my_array[1] == 0x80000004 // my_array[1] == 2
&type1_p->my_array[2] == 0x80000008 // my_array[2] == 3

当你将它转换为type2_t类型时,

type2_p == 0x80000000
&type2_p->ptr == 0x8000000 // type2_p->ptr == 1
type2_p->ptr[0] == *(type2_p->ptr) == *1

要实现您想要的功能,您需要创建一个次要结构并将数组的地址分配给ptr(例如,type2_p->ptr = type1_p->my_array),或者将ptr声明为数组(或变长数组,例如int ptr[])。
另外,您可以通过一种丑陋的方式访问元素:(&type2_p->ptr)[0]、(&type2_p->ptr)[1]。但是,请注意,(&type2_p->ptr)[0]实际上将是int*而不是int。例如,在64位平台上,(&type2_p->ptr)[0]实际上将是0x100000002(4294967298)。

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