C++数组上的指针数学中的未定义行为
Undefined behavior from pointer math on a C++ array
为什么这个程序的输出是4
?
#include <iostream>
int main()
{
short A[] = {1, 2, 3, 4, 5, 6};
std::cout << *(short*)((char*)A + 7) << std::endl;
return 0;
}
据我了解,在 x86 小端系统中,char 有 1 个字节,短 2 个字节,输出应该是0x0500
的,因为数组A
中的数据在十六进制中休耕:
01 00 02 00 03 00 04 00 05 00 06 00
我们从开头的 7 个字节向前移动,然后读取 2 个字节。我错过了什么?
您在此处违反了严格的别名规则。你不能只读到一个对象的一半,然后假装它是一个单独的对象。你不能像这样使用字节偏移量来发明假设的对象。海湾合作委员会完全有权做疯狂的事情,比如回到过去并谋杀猫王,当你把它交给你的程序时。
您可以做的是使用char*
检查和操作构成任意对象的字节。使用该权限:
#include <iostream>
#include <algorithm>
int main()
{
short A[] = {1, 2, 3, 4, 5, 6};
short B;
std::copy(
(char*)A + 7,
(char*)A + 7 + sizeof(short),
(char*)&B
);
std::cout << std::showbase << std::hex << B << std::endl;
}
// Output: 0x500
(现场演示)
但是你不能只是"编造"原始集合中不存在的对象。
此外,即使你有一个编译器可以被告知忽略这个问题(例如,使用 GCC 的-fno-strict-aliasing
开关),虚构的对象也没有针对任何当前的主流架构正确对齐。†,short
不能合法地存在于记忆中的那个奇数位置,所以你不能加倍假装那里有一个。只是没有办法绕过原始代码的行为是多么未定义;事实上,如果你通过GCC的-fsanitize=undefined
开关,它会告诉你很多。
†我正在简化一点。
由于将错误对齐的指针投射到(short*)
,程序具有未定义的行为。这违反了 C11 中 6.3.2.3 p6 中的规则,这与其他答案中声称的严格混叠无关:
可以转换为指向其他对象类型的指针。如果生成的指针未与引用的类型正确对齐,则行为未定义。
在 [expr.static.cast] p13 中,C++表示将未对齐的char*
转换为short*
会给出一个未指定的指针值,该值可能是无效指针,无法取消引用。
检查字节的正确方法是通过char*
,而不是通过转换回short*
并假装short
无法驻留的地址有short
。
这可以说是GCC中的一个错误。
首先,需要注意的是,由于违反了严格别名的规则,您的代码正在调用未定义的行为。
话虽如此,这就是为什么我认为它是一个错误:
-
当首次分配给中间
short
或short *
时,相同的表达式会导致预期的行为。只有当将表达式直接作为函数参数传递时,才会出现意外行为。 -
即使使用
-O0 -fno-strict-aliasing
编译也会发生这种情况。
我用 C 重新编写了您的代码,以消除任何C++疯狂的可能性。毕竟,您的问题已被标记为c
!我添加了pshort
函数,以确保不涉及可变参数性质printf
。
#include <stdio.h>
static void pshort(short val)
{
printf("0x%hx ", val);
}
int main(void)
{
short A[] = {1, 2, 3, 4, 5, 6};
#define EXP ((short*)((char*)A + 7))
short *p = EXP;
short q = *EXP;
pshort(*p);
pshort(q);
pshort(*EXP);
printf("n");
return 0;
}
使用gcc (GCC) 7.3.1 20180130 (Red Hat 7.3.1-2)
编译后:
gcc -O0 -fno-strict-aliasing -g -Wall -Werror endian.c
输出:
0x500 0x500 0x4
当表达式直接用作参数时,GCC 似乎实际上正在生成不同的代码,即使我显然使用相同的表达式 (EXP
)。
倾倒objdump -Mintel -S --no-show-raw-insn endian
:
int main(void)
{
40054d: push rbp
40054e: mov rbp,rsp
400551: sub rsp,0x20
short A[] = {1, 2, 3, 4, 5, 6};
400555: mov WORD PTR [rbp-0x16],0x1
40055b: mov WORD PTR [rbp-0x14],0x2
400561: mov WORD PTR [rbp-0x12],0x3
400567: mov WORD PTR [rbp-0x10],0x4
40056d: mov WORD PTR [rbp-0xe],0x5
400573: mov WORD PTR [rbp-0xc],0x6
#define EXP ((short*)((char*)A + 7))
short *p = EXP;
400579: lea rax,[rbp-0x16] ; [rbp-0x16] is A
40057d: add rax,0x7
400581: mov QWORD PTR [rbp-0x8],rax ; [rbp-0x08] is p
short q = *EXP;
400585: movzx eax,WORD PTR [rbp-0xf] ; [rbp-0xf] is A plus 7 bytes
400589: mov WORD PTR [rbp-0xa],ax ; [rbp-0xa] is q
pshort(*p);
40058d: mov rax,QWORD PTR [rbp-0x8] ; [rbp-0x08] is p
400591: movzx eax,WORD PTR [rax] ; *p
400594: cwde
400595: mov edi,eax
400597: call 400527 <pshort>
pshort(q);
40059c: movsx eax,WORD PTR [rbp-0xa] ; [rbp-0xa] is q
4005a0: mov edi,eax
4005a2: call 400527 <pshort>
pshort(*EXP);
4005a7: movzx eax,WORD PTR [rbp-0x10] ; [rbp-0x10] is A plus 6 bytes ********
4005ab: cwde
4005ac: mov edi,eax
4005ae: call 400527 <pshort>
printf("n");
4005b3: mov edi,0xa
4005b8: call 400430 <putchar@plt>
return 0;
4005bd: mov eax,0x0
}
4005c2: leave
4005c3: ret
- 我从 Docker 集线器使用 GCC 4.9.4 和 GCC 5.5.0 得到相同的结果
- C++编程从外部文本文件定义数组大小
- 如何在子类中重新定义数组大小?
- 有没有办法根据命令行参数定义数组大小?运行时与编译时实例化?
- 我可以使用常量定义数组的长度,那么为什么 int d[b] 不起作用呢?
- 表达式必须具有常数值,变量不能用作定义数组大小的常数
- 在类中定义数组的方法和字段
- (C++)我的自定义数组无法初始化(编译错误)
- 程序在初始化期间未与数组一起运行
- 重写自定义数组类的运算符/开始/结束
- 如何在 C++ 中定义数组数组
- 您可以在类初始化器列表中定义数组大小吗?
- 通过Overloading Operator []访问自定义数组包装器中的元素
- 为自定义数组实现迭代器
- 递归定义数组中的数据对齐和排序
- 如何从类中返回自定义数组项目并操纵其属性?C
- 如何使用类 .h 文件中的静态常量来定义数组的长度
- 使用类定义数组创建C++类
- C++自定义数组容器动态大小
- C++中使用什么类型来定义数组大小
- 在源文件中定义数组,然后在其他源文件中使用它