解引用类型转换的指针将违反严格别名规则。

16

我有一个包含结构体的无符号字符指针。现在我想要做以下操作:

unsigned char buffer[24];

//code to fill the buffer with the relevant information.

int len = ntohs((record_t*)buffer->len);

record_t结构包含一个名为len的字段。我无法这样做,会出现错误。

error: request for member ‘len’ in something not a structure or union.

然后我尝试了:

int len = ntohs(((record_t*)buffer)->len);

为了使运算符优先级正确,我做了以下更改:

警告:对类型转换的指针进行解引用将违反严格别名规则。

然后我声明了:

record_t *rec = null;

rec = (record_t*)

我在这里做错了什么?


1
你的编译器选项是什么?同时,专注于手头的问题,不要告诉我们你遇到过的先前语法错误。 - Kerrek SB
1
在C语言中,应该使用NULL而不是null - Chris Lutz
可能是什么是严格别名规则?的重复问题。 - Sneftel
4个回答

21
根据C和C++标准,通过指向另一种类型的指针访问给定类型的变量是未定义的行为。例如:
int a;
float * p = (float*)&a;  // #1
float b = *p;            // #2

这里的 #2 导致了未定义行为。#1 处的赋值被称为“类型转换”,而“别名”一词指的是多个不同的指针变量可以指向相同的数据,即在此情况下,p 别名为数据 a。合法的别名对于优化是一个问题(这也是某些情况下 Fortran 性能优越的主要原因之一),但我们在这里面临的是彻底的非法别名。

你的情况并不例外;你正在通过指向不同类型(即不是 unsigned char * 的指针)的指针访问 buffer 中的数据。这根本是不允许的。

结论是:你在第一时间就不应该在 buffer 中有数据。

但如何解决呢?确保你拥有有效的指针!有一个例外情况,即通过指向 char 的指针访问数据是被允许的。因此,我们可以这样写:

record_t data;
record_t * p = &data;          // good pointer
char * buffer = (char*)&data;  // this is allowed!

return p->len;                 // access through correct pointer!

关键的区别在于我们将真正的数据存储在正确类型的变量中,只有在分配了该变量之后,我们才将变量视为字符数组(这是允许的)。这里的道理是字符数组始终排在第二位,真正的数据类型排在第一位。


+1 不仅告诉 OP 错在哪里,还展示了正确的做法! - R.. GitHub STOP HELPING ICE
不,不,你夸大了事实并且没有抓住重点。通过另一种类型访问对象可能是未定义的行为,如果这导致例如陷阱表示或错位,但不一定如此。别名的重点完全不同。这是因为标准允许编译器假设不同类型的指针不会彼此别名,并且他可以执行更积极的优化,如果在现实中这些指针指向同一个对象,则可能出现问题。 - Jens Gustedt
@JensGustedt: 我认为对齐在规则中并不起作用。你可以来回转换,这没问题(例如 T * p = &x; S * q = (S*)p; T * r = (T*)q; T y = *r;),但除此之外就是未定义的行为(可能会表现得像预期的那样)。别名是一个广泛的概念(例如考虑“向量加法器”add(float * a, float * b, float * c),在这种情况下,知道ab是否与c别名很有用),但“严格别名规则”指出,不同类型的指针永远不能别名。 - Kerrek SB

5

您看到这个警告是因为您违反了严格别名规则,即两个指向相同位置的指针类型不同。

解决此问题的一种方法是使用联合体:

union{
    unsigned char buffer[24];
    record_t record_part;
};

//code to fill the buffer with the relavent information.

int len = ntohs(record_part.len);

编辑:

严格来说,这并没有比你的原始代码更安全,但它不违反严格别名。


1
访问联合体的顺序不确定是未定义行为。你并没有“绕过”任何东西(只是直接走向坟墓)。 - Kerrek SB
1
@Kerrek SB:你能解释一下你所说的“out-of-order”是什么意思吗?是的,我知道使用union的方法也是未定义的,但原始代码也是如此。 - Mysticial
1
@Mystical,可能应该这样写:int len = ntohs(record_part.len); - Jim Rhodes
从语言角度来看,这样使用union也违反了严格别名规则。Union不能用于内存重新解释。但从GCC的角度来看,这是合法的:GCC保留并支持此类union的使用,特别是在您想要打破语言严格别名规则的情况下。 - AnT stands with Russia
6
C99 TC3允许使用联合体进行类型转换。 (我无法引用,但请查看https://dev59.com/AlvUa4cB1Zd3GeqPrkCj,有些人同意我的观点。) - Chris Lutz
显示剩余8条评论

3
你可以尝试这个:

你可以尝试这个:

unsigned char buffer[sizeof(record_t)];
record_t rec;
int len;

// code to fill in buffer goes here...

memcpy(&rec, buffer, sizeof(rec));
len = ntohs(rec.len);

这实际上是正确的做法(虽然强制转换为 void * 是不必要的)。重新解释一种类型的数据作为另一种类型的数据的适当方法是通过使用 memcpy 在对象之间复制该数据(而不是直接基于指针或联合进行重新解释)。 - AnT stands with Russia
1
我同意这是正确的(我也给了它一个+1),但最好从一开始就不要使用unsigned char[],而是直接将数据读入正确类型的变量中。 - R.. GitHub STOP HELPING ICE

1

你可能设置了一个警告级别,其中包括严格别名警告(它曾经不是默认的,但有一次gcc将其翻转为默认)。尝试-Wno-strict-aliasing-fno-strict-aliasing -- 然后gcc就不会生成警告了。

一个相当好的解释(基于粗略的扫视)是 什么是严格别名规则?


2
无论是否有警告,OP的代码可能存在未定义行为,这一点不应被忽视。 - Kerrek SB
我正在考虑是否要将其减一。给予 OP 关于如何禁用危险 UB 的警告并添加选项以使具有 UB 的代码“按预期”工作的建议似乎是不太建设性的。特别是因为该代码还存在其他由于对齐问题而导致的 UB,这些问题没有被捕获并且也不会被选项解决。它可以在 x86 上正常工作,但在其他架构上稍后会崩溃... - R.. GitHub STOP HELPING ICE
@R。有许多习语明确使用这种行为(特别是在网络编程中),长时间以来,gcc默认禁用警告,以便网络编程不会产生大量警告:) 鉴于我们正在谈论网络编程(基于ntohs调用的假设),至少提到这一事实是有意义的。 - Foo Bah
正如我所指出的那样,这种代码几乎肯定是错误的,原因是gcc无法解决对齐问题。从char缓冲区读取,然后尝试将该缓冲区解释为另一种类型,这是根本上错误的,它表明代码的作者不理解如何将void指针传递给read/recv的实际对象(或在更大的缓冲区的情况下,如何使用memcpy...)。 - R.. GitHub STOP HELPING ICE
@FooBah:你确定你看到的代码不是在做其他事情吗?例如,有很多看起来相似的习语实际上是完全合法的。例如,访问不同结构体的共同初始元素是可以的。如果将一个现有变量的类型转换为字符数组,那么写入字符数组也是可以的...魔鬼就在于这些细节。 - Kerrek SB
@KerrekSB:C89 的作者可能打算让访问结构体的公共初始元素成为合法操作,但 C99 添加了额外的限制,而 gcc 解释这些限制的方式使得“公共初始序列规则”变得无用。 - supercat

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