在C中强制转换指针的规则是什么?


70

K&R不会超越它,但他们会使用它。我尝试通过编写示例程序来了解其工作原理,但效果并不理想:

它可以编译,但是我的print语句吐出垃圾变量(每次调用程序时它们都不同)。有任何想法吗?


1
int的大小比char大,因此读取的内容超出了'5'char的空间。尝试使用较小的数据类型(int c,printf“%c”)执行相同的操作
SheetJS 2013年

1
的值*n将是int,应为4个字节。*n指向局部变量cmain()。这意味着您将写出值,'c'然后在内存中跟随三个字节。(我的猜测是的值d。)您可以通过以十六进制写出数字来验证这一点-每次两位数应该相同。
millimoose13年

1
'5'-你觉得mught这看起来像一个int,因为它似乎是一个数字,但它只是一个代表数字5的字符
麻将

我在我的机器上运行了相同的测试(gcc,x86_64),没有编译错误,并且程序每次都运行良好(没有垃圾)。但是我没有做任何与OP不同的事情。奇怪。
安迪J

阅读此答案的任何人都应在下面
polynomial_donut

Answers:


140

考虑指针时,它有助于绘制图表。指针是一个箭头,指向内存中的地址,带有指示值类型的标签。地址指示要看的地方,类型指示要取什么。投射指针会更改箭头上的标签,但不会更改箭头指向的位置。

dinmainc类型为的指针char。Achar是内存的一个字节,因此当d取消引用时,您会在该内存的一个字节中获取值。在下图中,每个单元代表一个字节。

当你施放dint*,你是说d真的指向一个int值。在当今的大多数系统上,一个int占用4个字节。

取消引用时(int*)d,您将获得由这四个字节的内存确定的值。您获得的值取决于这些标记的单元格中的?内容以及int在内存中的表示方式。

甲PC是小端,这意味着,一个值int被计算这种方式(假设它跨越4个字节): * ((int*)d) == c + ?₁ * 2⁸ + ?₂ * 2¹⁶ + ?₃ * 2²⁴。因此,如果您使用十六进制(printf("%x\n", *n)),则最后两位始终是35(即字符的值'5')。

其他一些系统是big-endian,并在另一个方向上排列字节:* ((int*)d) == c * 2²⁴ + ?₁ * 2¹⁶ + ?₂ * 2⁸ + ?₃。在这些系统中,你会发现,价值总是开始35时十六进制打印。某些系统的大小int不同于4个字节。很少有系统int以不同的方式排列,但是您极不可能遇到它们。

根据您的编译器和操作系统的不同,您可能会发现每次运行该程序时该值都不相同,或者始终相同,但是在对源代码进行很小的调整时它就会更改。

在某些系统上,int值必须存储在4(或2或8)倍数的地址中。这称为对齐要求。取决于地址是否c正确对齐,程序可能会崩溃。

与您的程序相反,这是当您有一个int值并指向它的指针时发生的情况。

指针p指向一个int值。箭头上的标签正确描述了存储单元中的内容,因此取消引用时不会感到惊讶。


4
很好的描述。我想指出/讨论,在大多数计算机上,int是32位的值可能是正确的,但是对于嵌入式工程师来说,int通常是16位的,它表明了它的有用性和重要性,可能使用uint16_t,uint32_t,int32_t等。不要试图成为聪明人,请不要冒犯。:)
DiBosco '02

1
“ ...最后两位数字将始终为35(这是字符'5'的值)。” 为什么?
肯尼·沃登

嗨,吉尔斯,当我在这里尝试代码时char *a = "abcd"; int *i = (int *)a; printf("%x\n", *i);,输出为64636261,但我认为应该为61626364。这是否意味着从后到前读取此int块中的内存?
夏季太阳

@SummerSun您为什么认为应该是61626364?如果您使用的是低字节序计算机(所有PC都是低字节序),则应该是64636261。这与读取内存的顺序无关。int无论如何,可能在一条指令中读取了an 。这是关于如何将一个4字节的块解释为一个int值。
吉尔斯(Gillles)“所以-别再邪恶了”

1
@Malcolm这是未定义的行为。取消引用强制转换的结果是UB(例如,它可能未正确对齐),甚至如果取消引用将是UB,则即使只是构造一个指针也通常是UB(我认为唯一的例外是函数指针和指向末尾的指针数组)。在一种情况下,定义了行为,即指针最初是int*指针。任何数据指针都可以unsigned char*前后转换,我认为unsigned char *可以char *前后转换。
吉尔(Gilles)“所以,别再邪恶了”,

40

char在堆栈上的address分配了一个(1字节)0x12345678

你获得的地址,c并将其存储在d,所以d = 0x12345678

您可以强制编译器假定它0x12345678指向一个int,但一个int不仅仅是一个字节(sizeof(char) != sizeof(int))。根据体系结构甚至其他值,它可以是4或8个字节。

因此,当您打印指针的值时,将通过取第一个字节(即c)和其他连续的字节(位于堆栈上,仅出于您的意图)而考虑整数。


3
其他连续字节不是垃圾,而是的值d,即0x12345678在您的示例中。
凯恩

d还不够大,无法容纳0x12345678
2014年

1
@APerson为什么?
yyny

char c [] =“ 5”; 字符d = c; int * e =(int)d; printf(“%p \ n”,e);
Martian2049 '18


17

强制转换指针在C中通常是无效的。有以下几个原因:

  1. 对准。由于对齐方面的考虑,目标指针类型可能无法表示源指针类型的值。例如,如果int *本质上是4字节对齐的,则强制转换char *int *将丢失低位。

  2. 混叠。通常,除非通过该对象的正确类型的左值,否则禁止访问该对象。有一些例外,但是除非您非常了解它们,否则您就不想这样做。请注意,仅当您实际上取消引用指针时(将*->运算符应用于它,或将其传递给将对其取消引用的函数),别名才是一个问题。

强制转换指针正常的主要情况是:

  1. 当目标指针类型指向字符类型时。保证字符类型的指针能够代表任何类型的任何指针,并在需要时将其成功地往返回原始类型。指向void(void *)的指针与字符类型的指针完全相同,不同之处在于您不允许对其取消引用或对其进行算术,并且它无需转换就可以自动与其他指针类型进行转换,因此,指向为此,通常比使用指针指向字符类型更好。

  2. 当目标指针类型是指向结构类型的指针时,其成员与原始指向的结构类型的初始成员完全匹配。这对于C中的各种面向对象编程技术很有用。

在语言要求方面,其他一些晦涩的情况在技术上还可以,但存在问题,最好避免。


6
您可以将这些晦涩的案例链接到官方文档吗?
艾瑞克(Eric)

我在一些地方看到过使用char *并将其转换为其他指针的代码,例如int。例如,从摄像机流式传输RGB值,或在网络外传输字节。您的引用是否表示该代码无效?对齐数据是否足以使代码正确,还是仅仅是我们的普通编译器对此用法有所宽容?
伊万·本恩

1
@EvanBenn:可能。如果通过获得缓冲区malloc,并且您通过fread或类似方式将数据按字节存储在缓冲区中,则只要偏移量适当对齐即可(通常很难确定偏移量,但是如果它们是类型大小的倍数,则肯定是正确的),应该符合将其转换为适当的指针类型并以该类型访问数据的要求。但是,如果使用的缓冲区的实际类型为char[N]或其他某种类型,则该缓冲区无效。
R .. GitHub停止帮助ICE

3

我怀疑您需要一个更一般的答案:

在C中没有强制转换指针的规则!该语言使您可以将任何指针转换为任何其他指针而无需注释。

但问题是:没有数据转换或完成任何操作!系统不会在转换后错误地解释数据,这完全是您自己的责任,通常是这种情况,这会导致运行时错误。

因此,在完全由您决定时,要确保如果从强制转换指针中使用数据,则数据是兼容的!

C针对性能进行了优化,因此缺乏指针/引用的运行时自反性。但这是有代价的-作为程序员,您必须更好地照顾自己的工作。您必须了解自己想要做的是“合法的”


10
有一些关于强制转换指针的规则,C 2011标准的第6.3.2.3节中有许多规则。除其他外,指向对象的指针可以强制转换为指向对象的其他指针,并且如果转换回去,则将与原始指针进行比较。指向函数的指针可以转换为指向函数的其他指针,如果转换回去,它们的比较将相等。将指针转换为函数,再将指针转换为对象会导致未定义的行为。指向对象的指针可以转换为指向字符的指针,并用于访问对象的字节。
Eric Postpischil 2013年

允许将指向函数的指针转换为指向对象的指针。“ J.5.7
aqjune 17'Jul

2
@aqjune您引用的是C的流行扩展,根据定义,它不是标准C。它仅提供信息。
管道

2

您有一个指向的指针char。因此,如您的系统所知,在该内存地址上有一个空间charsizeof(char)。将其转换为时int*,将使用的数据sizeof(int),因此将char和一些内存垃圾作为整数打印出来。

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.