56

在我的项目中,我们有一段这样的代码:

// raw data consists of 4 ints
unsigned char data[16];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + 4));
i3 = *((int*)(data + 8));
i4 = *((int*)(data + 12));

我和我的技术负责人谈过,这段代码可能不可移植,因为它试图将 aunsigned char*转换为int*通常具有更严格对齐要求的 a。但是技术负责人说没关系,大多数编译器在转换后保持相同的指针值,我可以像这样编写代码。

坦率地说,我不是很相信。经过研究,我发现有些人反对使用像上面这样的指针转换,例如,herehere

所以这是我的问题:

  1. 在实际项目中转换后取消引用指针真的安全吗?
  2. C风格的铸造和之间有什么区别reinterpret_cast吗?
  3. C 和 C++ 之间有什么区别吗?
4

7 回答 7

40

1. 在实际项目中转换后取消引用指针真的安全吗?

如果指针碰巧没有正确对齐,它确实会导致问题。我亲眼看到并修复了由于将 achar*转换为更严格对齐的类型而导致的实际生产代码中的总线错误。即使您没有收到明显的错误,您也可能会遇到不太明显的问题,例如性能下降。即使您没有立即发现任何问题,严格遵循标准以避免 UB 也是一个好主意。(代码打破的一条规则是严格的别名规则,§ 3.10/10*)

更好的选择是使用std::memcpy()或者std::memmove如果缓冲区重叠(或更好bit_cast<>()

unsigned char data[16];
int i1, i2, i3, i4;
std::memcpy(&i1, data     , sizeof(int));
std::memcpy(&i2, data +  4, sizeof(int));
std::memcpy(&i3, data +  8, sizeof(int));
std::memcpy(&i4, data + 12, sizeof(int));

一些编译器比其他编译器更努力地确保 char 数组的对齐比必要的更严格,因为程序员经常会犯这个错误。

#include <cstdint>
#include <typeinfo>
#include <iostream>

template<typename T> void check_aligned(void *p) {
    std::cout << p << " is " <<
      (0==(reinterpret_cast<std::intptr_t>(p) % alignof(T))?"":"NOT ") <<
      "aligned for the type " << typeid(T).name() << '\n';
}

void foo1() {
    char a;
    char b[sizeof (int)];
    check_aligned<int>(b); // unaligned in clang
}

struct S {
    char a;
    char b[sizeof(int)];
};

void foo2() {
    S s;
    check_aligned<int>(s.b); // unaligned in clang and msvc
}

S s;

void foo3() {
    check_aligned<int>(s.b); // unaligned in clang, msvc, and gcc
}

int main() {
    foo1();
    foo2();
    foo3();
}

http://ideone.com/FFWCjf

2. C-style cast 和 reinterpret_cast 有什么区别吗?

这取决于。C 风格的转换根据所涉及的类型做不同的事情。指针类型之间的 C 风格转换将产生与 reinterpret_cast 相同的结果;请参阅 § 5.4显式类型转换(强制转换表示法)和 § 5.2.9-11。

3. C和C++有什么区别吗?

只要您处理的是 C 中合法的类型,就不应该有。


* 另一个问题是 C++ 没有指定从一种指针类型转换为具有更严格对齐要求的类型的结果。这是为了支持甚至无法表示未对齐指针的平台。但是,当今的典型平台可以表示未对齐的指针,并且编译器将这种强制转换的结果指定为您所期望的。因此,此问题是混叠违规的次要问题。见[expr.reinterpret.cast]/7。

于 2012-12-14T15:28:54.563 回答
28

这不好吧,真的。对齐可能是错误的,并且代码可能违反严格的别名。你应该明确地解压它。

i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;

等等。这绝对是定义明确的行为,而且作为奖励,它也是与字节顺序无关的,与您的指针转换不同。

于 2012-12-14T15:30:45.707 回答
7

在示例中,如果初始 char 指针正确对齐,您在此处显示的操作在几乎所有现代 CPU 上都是安全的。一般来说,这是不安全的,也不能保证有效。

如果初始 char 指针未正确对齐,这将在 x86 和 x86_64 上工作,但在其他架构上可能会失败。如果你很幸运,它只会给你一个崩溃,你会修复你的代码。如果您不走运,未对齐的访问将由您的操作系统中的陷阱处理程序修复,并且您将获得糟糕的性能,而没有任何明显的反馈说明它为何如此缓慢(我们正在谈论某些代码的缓慢速度,这是 20 年前 alpha 的一个大问题)。

即使在 x86 & co 上,未对齐的访问也会变慢。

如果你想在今天和未来都安全,就memcpy不要像这样做任务。现代编译器可能会优化memcpy并做正确的事情,如果没有,memcpy它自己将进行对齐检测并做最快的事情。

此外,您的示例在某一点上是错误的: sizeof(int) 并不总是 4。

于 2012-12-14T15:34:05.137 回答
5

解压缓冲数据的正确方法char是使用memcpy

unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
memcpy(&i1, data, sizeof(int));
memcpy(&i2, data + sizeof(int), sizeof(int));
memcpy(&i3, data + 2 * sizeof(int), sizeof(int));
memcpy(&i4, data + 3 * sizeof(int), sizeof(int));

强制转换违反了别名,这意味着编译器和优化器可以自由地将源对象视为未初始化。

关于你的3个问题:

  1. 不,由于别名和对齐,取消引用强制转换指针通常是不安全的。
  2. 不,在 C++ 中,C 样式转换是根据reinterpret_cast.
  3. 不,C 和 C++ 同意基于强制转换的别名。基于联合的别名的处理有所不同(C 在某些情况下允许这样做;C++ 不允许)。
于 2012-12-14T15:33:40.603 回答
1

更新:我忽略了一个事实,即确实较小的类型可能与较大的类型不对齐,就像在您的示例中一样。您可以通过反转转换数组的方式来缓解该问题:将数组声明为 int 数组,并char *在需要以这种方式访问​​它时将其转换为。

// raw data consists of 4 ints
int data[4];

// here's the char * to the original data
char *cdata = (char *)data;
// now we can recast it safely to int *
i1 = *((int*)cdata);
i2 = *((int*)(cdata + sizeof(int)));
i3 = *((int*)(cdata + sizeof(int) * 2));
i4 = *((int*)(cdata + sizeof(int) * 3));

基本类型数组不会有任何问题。在处理结构化数据数组(struct在 C 中)时会出现对齐问题,如果数组的原始原始类型大于它被转换为的类型,请参阅上面的更新。

将 char 数组转换为 int 数组应该是完全可以的,前提是您将 4 的偏移量替换为sizeof(int), 以匹配代码应该运行的平台上的 int 大小。

// raw data consists of 4 ints
unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + sizeof(int)));
i3 = *((int*)(data + sizeof(int) * 2));
i4 = *((int*)(data + sizeof(int) * 3));

请注意,只有当您以某种方式从一个平台以不同的字节顺序将数据共享到另一个平台时,您才会遇到字节顺序问题。否则,它应该完全没问题。

于 2012-12-14T15:30:24.880 回答
1

您可能想向他展示根据编译器版本的不同情况会有所不同:

除了对齐之外,还有第二个问题:标准允许您投射int*tochar*但不能反过来(除非char*最初是从 an投射的int*)。有关更多详细信息,请参阅此帖子。

于 2012-12-14T18:55:24.373 回答
0

是否需要担心对齐取决于指针所源自的对象的对齐方式。

如果您转换为具有更严格对齐要求的类型,则它是不可移植的。

与您的示例一样,数组的基础char不需要比元素类型更严格的对齐方式char

但是,指向任何对象类型的指针都可以转换为 achar *并返回,而不管对齐方式如何。指针保留原始的char *更强对齐。

您可以使用联合来创建更强烈对齐的 char 数组:

union u {
    long dummy; /* not used */
    char a[sizeof(long)];
};

联合的所有成员都从同一个地址开始:开始没有填充。因此,当在存储中定义联合对象时,它必须具有适合最严格对齐的成员的对齐方式。

我们的union u上面对于 type 的对象来说已经足够严格了long

违反对齐限制可能会导致程序在移植到某些架构时崩溃。或者它可能有效,但对性能有轻微到严重的影响,这取决于未对齐的内存访问是在硬件中实现的(以一些额外的周期为代价)还是在软件中(到内核的陷阱,软件在其中模拟访问,需要付出代价)许多周期)。

于 2012-12-14T22:42:29.253 回答