chq*_*lie 16 c type-conversion language-lawyer
这是C11标准的引用:
6.5表达式
......6 访问其存储值的对象的有效类型是对象的声明类型(如果有).如果通过具有非字符类型的左值的值将值存储到没有声明类型的对象中,则左值的类型将成为该访问的对象的有效类型以及不修改该值的后续访问的有效类型储值.如果使用
memcpy或将值复制到没有声明类型的对象中memmove,或者将其复制为字符类型数组,则该访问的修改对象的有效类型以及不修改该值的后续访问的有效类型是有效类型复制值的对象,如果有的话.对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型.7对象的存储值只能由具有以下类型之一的左值表达式访问:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,- 与对象
的有效类型对应的有符号或无符号类型的类型,
- 类型这是对象的有效类型的限定版本对应的有符号或无符号类型,
- 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员) ,或
- 字符类型.
这是否意味着memcpy不能以这种方式用于打字:
double d = 1234.5678;
uint64_t bits;
memcpy(&bits, &d, sizeof bits);
printf("the representation of %g is %08"PRIX64"\n", d, bits);
Run Code Online (Sandbox Code Playgroud)
为什么它不会给出相同的输出:
union { double d; uint64_t i; } u;
u.d = 1234.5678;
printf("the representation of %g is %08"PRIX64"\n", d, u.i);
Run Code Online (Sandbox Code Playgroud)
如果我使用我的memcpy使用字符类型的版本怎么办:
void *my_memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
for (size_t i = 0; i < n; i++) { d[i] = s[i]; }
return dst;
}
Run Code Online (Sandbox Code Playgroud)
编辑: EOF评论说,第6段中的部分memcpy()不适用于这种情况,因为uint64_t bits它具有声明的类型. 我同意,但不幸的是,这无法回答是否memcpy可以用于类型惩罚的问题,它只是使第6段与评估上述例子的有效性无关.
这里是另一种打击类型的尝试memcpy,我认为第6段将对此进行讨论:
double d = 1234.5678;
void *p = malloc(sizeof(double));
if (p != NULL) {
uint64_t *pbits = memcpy(p, &d, sizeof(double));
uint64_t bits = *pbits;
printf("the representation of %g is %08"PRIX64"\n", d, bits);
}
Run Code Online (Sandbox Code Playgroud)
假设sizeof(double) == sizeof(uint64_t),上述代码是否根据第6和7段规定了行为?
编辑:一些答案指出来自阅读陷阱表示的未定义行为的可能性.这是不相关的,因为C标准明确排除了这种可能性:
7.20.1.1精确宽度整数类型
1 typedef名称
intN_t指定有符号整数类型,其宽度为N,无填充位和二进制补码表示.因此,int8_t表示这样的带符号整数类型,其宽度恰好为8位.2 typedef名称
uintN_t指定宽度为N且无填充位的无符号整数类型.因此,uint24_t表示这样的无符号整数类型,其宽度恰好为24位.这些类型是可选的.但是,如果实现提供宽度为8,16,32或64位的整数类型,没有填充位,并且(对于具有二进制补码表示的有符号类型),它应定义相应的typedef名称.
Type uint64_t有64个值位且没有填充位,因此不能有任何陷阱表示.
有两种情况需要考虑:memcpy()进入具有声明类型的对象,进入不具有声明类型memcpy()的对象.
在第二种情况下,
double d = 1234.5678;
void *p = malloc(sizeof(double));
assert(p);
uint64_t *pbits = memcpy(p, &d, sizeof(double));
uint64_t bits = *pbits;
printf("the representation of %g is %08"PRIX64"\n", d, bits);
Run Code Online (Sandbox Code Playgroud)
行为确实是未定义的,因为将指向的对象的有效类型p将变为double,并且通过类型double的左值访问有效类型的对象uint64_t是未定义的.
另一方面,
double d = 1234.5678;
uint64_t bits;
memcpy(&bits, &d, sizeof bits);
printf("the representation of %g is %08"PRIX64"\n", d, bits);
Run Code Online (Sandbox Code Playgroud)
是不是不确定的.C11标准草案n1570:
7.24.1字符串函数约定
3对于本子条款中的所有函数,每个字符都应解释为它具有unsigned char类型(因此每个可能的对象表示都是有效的并且具有不同的值).
和
6.5表达式
7对象的存储值只能由具有以下类型之一的左值表达式访问:88)- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,- 与对象
的有效类型对应的有符号或无符号类型的类型,
- 类型这是对象的有效类型的限定版本对应的有符号或无符号类型,
- 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员) ,或
- 字符类型.脚注88)此列表的目的是指定对象可能或可能不具有别名的情况.
所以它memcpy()本身就是明确的.
由于uint64_t bits 具有声明的类型,即使其对象表示从a复制,它也会保留其类型double.
作为chqrlie指出,uint64_t不能有陷阱的表示,所以访问bits后memcpy()是不是不确定的,提供sizeof(uint64_t) == sizeof(double).然而,值的bits将是依赖于实现的(例如由于字节顺序).
结论:memcpy() 可以用于类型惩罚,前提是它的目的地memcpy()确实具有声明的类型,即未分配[m/c/re]alloc()或等效.
您提出了三种使用C标准都存在不同问题的方式。
标准库 memcpy
double d = 1234.5678;
uint64_t bits;
memcpy(&bits, &d, sizeof bits);
printf("the representation of %g is %08"PRIX64"\n", d, bits);
Run Code Online (Sandbox Code Playgroud)
该memcpy部分是合法的(在实现sizeof(double) == sizeof(uint64_t)中提供,但不能按标准保证):您可以通过char指针访问两个对象。
但printf行不是。现在的表示形式bits为双精度。它可能是的陷阱表示uint64_t,如6.2.6.1常规§5中所定义
某些对象表示形式不必表示对象类型的值。如果对象的存储值具有这种表示形式,并且由不具有字符类型的左值表达式读取,则该行为是不确定的。如果这样的表示是由副作用产生的,该副作用通过不具有字符类型的左值表达式修改对象的全部或任何部分,则该行为是不确定的。这样的表示称为陷阱表示。
6.2.6.2整数类型明确表示
对于除无符号字符以外的无符号整数类型,对象表示的位应分为两组:值位和填充位……任何填充位的值均未指定。53
注释53中说:
填充位的某些组合可能会生成陷阱表示,
如果您知道在您的实现中没有填充位(仍然从未见过填充...),则每种表示形式都是有效值,并且该print行再次变为有效。但这仅取决于实现,在一般情况下可以是未定义的行为
联盟
union { double d; uint64_t i; } u;
u.d = 1234.5678;
printf("the representation of %g is %08"PRIX64"\n", d, u.i);
Run Code Online (Sandbox Code Playgroud)
联合的成员不共享公共子序列,并且您正在访问的成员不是最后写入的值。好的通用实现会给出预期的结果,但是根据标准,它没有明确定义应该发生什么。6.5.2.3结构和联合成员§3中的脚注指出,如果导致与前面的情况相同的问题:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示形式的适当部分将重新解释为新类型的对象表示形式,如下所示:在6.2.6中描述(有时称为“类型校正”的过程)。这可能是陷阱表示。
习俗 memcpy
您的实现只执行始终允许的字符访问。与第一种情况完全相同:定义实现。
根据标准明确定义的唯一方法是将的表示形式存储double在正确大小的char数组中,然后显示char数组的字节值:
double d = 1234.5678;
unsigned char bits[sizeof(d)];
memcpy(&bits, &d, sizeof(bits));
printf("the representation of %g is ", d);
for(int i=0; i<sizeof(bits); i++) {
printf("%02x", (unsigned int) bits[i]);
}
printf("\n");
Run Code Online (Sandbox Code Playgroud)
而且只有实现恰好使用8位的结果才是可用的char。但这是可见的,因为如果其中一个字节的值大于255 ,它将显示8个以上的十六进制数字。
以上所有内容仅由于bits具有声明的类型才有效。请参阅@EOF的答案以了解为什么分配的对象会有所不同