【问题标题】:Does char[] + memcpy() violate strict aliasing?char[] + memcpy() 是否违反严格的别名?
【发布时间】:2017-01-07 04:37:56
【问题描述】:

我在结构中使用char 数组来保存一些通用数据,就像这样(输入类型可能是未知大小的结构,所以我不能只使用联合;这段代码被大大简化了) :

typedef struct {
    char buf[256];
} data;

void data_set_int(data *d, int a) {
    memcpy(d->buf, &a, sizeof(a));
}

int data_get_int(data *d) {
    int ret;
    memcpy(&ret, d->buf, sizeof(ret));
    return ret;
}

void data_set_float(data *d, float a) {
    memcpy(d->buf, &a, sizeof(a));
}

float data_get_float(data *d) {
    float ret;
    memcpy(&ret, d->buf, sizeof(ret));
    return ret;
}

int main(void) {
    data d;

    data_set_int(&d, 3);
    int int_result = data_get_int(&d);

    data_set_float(&d, 10.0f);
    float float_result = data_get_float(&d);

    return 0;
}

如果我从不尝试编写 float,然后将数据读取为 int,反之亦然,那么这段代码在 C(99) 中是否定义良好?

使用 GCC 编译不会产生警告,并且运行代码会产生预期的行为(int_result == 3float_result == 10.0f)。将 memcpy 更改为普通指针取消引用 (int ret = *(int *)d->buf) 也可以正常工作,不会出现警告。

我读过的所有关于严格别名的资料都说您可以将任何类型读取为char *(所以我认为这意味着set 应该没问题),但您不能像任何类型一样读取char *其他类型(不太确定get 是否可以)。我误解了规则吗?

【问题讨论】:

  • 这很好,直到您到达*(int *)d->buf。那时所有的赌注都被取消了,因为int 可能有一个对齐约束,d->buf 可能会也可能不会满足。
  • @AndrewSun 否。对齐限制意味着某些处理器和编译器会出错,而其他的则不会。严格的别名禁止通过两个不同 non-character 类型的指针取消引用同一块内存。你没有这样做。
  • @AndrewSun 另一方面,这不是一个聪明的方法。这正是unions 的用途,它们将生成更高效的代码,因为它们will 保证所有字段类型的对齐,因此不需要按字节复制。
  • @chux 在这种情况下字节布局重要吗?我是来自int -> char[] -> int(或float -> char[] -> float)的memcpying,从未接触过缓冲区中的字节。
  • @AndrewSun 我读错了你的问题“如果我从不尝试编写浮点数然后将数据读取为 int 或反之亦然,这段代码在 C(99) 中是否定义良好?” .我错过了“从不”的部分。所以关于我以前的 cmets,never mind

标签: c undefined-behavior strict-aliasing


【解决方案1】:

在 C89 下,memcpy 的行为类似于使用unsigned char* 读取源的每个字节,并使用unsigned char* 写入目标的每个字节;因为字符指针可用于访问任何其他内容,这使得 memcpy 通用用于数据转换。

C99 为 memcpy 添加了一些新限制,这仍然允许在目标对象具有声明类型或将用于读取的所有非字符指针的有效类型的情况下使用它目标对象与源的有效类型一致,但使没有声明类型的对象处于只能使用源类型读取的状态。我不认为 C11 以任何有意义的方式放宽了这些限制。

您的代码不应受到 memcpy 规则的影响,因为每个 memcpy 操作要么写入具有声明类型的对象,要么写入仅通过 memcpy 变为红色的存储到具有声明类型的对象。 C99 的 memcpy 规则的主要问题情况发生在代码需要就地更新对象而不知道下一次读取对象的类型时。

例如,在intlong 都具有相同的32 位表示的系统上,应该可以编写一个函数来将数据加载到int[]long[] 而无需知道它正在接收哪种指针(机器操作的顺序在任何一种情况下都是相同的)。如果代码将一些数据读入临时int[],然后使用memcpy 将其移动到最终目的地,则如果目的地是int[] 或@ 类型的实际声明对象,则标准将保证序列正常工作987654332@,或者如果它是一个将被读取为int[]的分配存储区域,但如果它是一个分配存储区域,接下来将被读取为long,则不能保证工作。

【讨论】:

  • 所以如果我理解正确的话,只要源和目标类型相同,memcpy 一些数据到中间缓冲区,然后memcpy 到它的目标总是安全的(T -> char[] -> T)?
  • 如果源具有有效类型,目标没有声明类型,并且下一次使用源以外的类型读取目标,则会出现问题情况。在您的示例中,从 char[] 复制数据的 memcpy 操作将其写入具有声明类型的局部变量。所写的标准允许实现假设从char[] 到从memcpy 接收的对象的memcpy 不会影响使用int*float* 等访问的任何对象。虽然这看起来很荒谬实现会这样做...
  • ...实现使用别名规则来证明许多同样荒谬的行为。
猜你喜欢
  • 1970-01-01
  • 2020-04-11
  • 1970-01-01
  • 2016-02-24
  • 1970-01-01
  • 2017-05-08
  • 2014-02-08
相关资源
最近更新 更多