在C语言中使用memcpy函数将数组复制到结构体中,反之亦然

4
假设我有这样一种结构:
typedef struct {
double re;
double im;
}ComplexStruct;

数组相关:

typedef double ComplexArr[2];// [0] -> 实部, [1] -> 虚部

今天我使用简单的 for 循环从 ComplexStruct 复制到 ComplexArr,反之亦然:

//ComplexArr to  ComplexStruct
for (i = 0 ; i < NumEl; i++)
{
     ComplexStructVec[i].re = ComplexArrVec[i][0];
     ComplexStructVec[i].im = ComplexArrVec[i][1];
}

//ComplexStruct to ComplexArr
for (i = 0 ; i < NumEl; i++)
{
     ComplexArrVec[i][0] = ComplexStructVec[i].re;
     ComplexArrVec[i][1] = ComplexStructVec[i].im;
}

有没有一种安全使用 memcpy 至少其中一个方向的方法?还有比 for 循环更快的方法吗?


不是回答你的问题,但请看这里的建议(https://dev59.com/P3A85IYBdhLWcg3wCfLm#2963928)。 - Federico klez Culloca
1
虽然结构体中很可能没有填充,但并不是百分之百保证。这意味着你真的不应该使用 memcpy 在两个数组之间进行复制。但是在(很可能的)情况下,如果 sizeof(ComplexStruct) == sizeof(ComplexArr),那么使用 memcpy 就可以工作。 - Some programmer dude
如果你想保险起见,仍然使用显式循环,那么考虑循环展开作为加速的一种方式。或者并行复制的方法。但是无论你做什么优化,首先确保它真正是你程序中的主要瓶颈(通过编译器优化构建进行测量),并且要彻底记录你自己的优化过程。 - Some programmer dude
3个回答

3

你编译器中的优化程序应该非常擅长处理这段代码,你几乎不需要进行太多更改来使其达到最优状态。然而,如果你将ComplexStructVec和ComplexArrVec传递到函数中,你应该将它们标记为restrict,以便编译器知道没有别名发生。像这样:

void copy(ComplexStruct* restrict ComplexStructVec, const ComplexArr* ComplexArrVec)
{
    unsigned NumEl = 1000;
    for (unsigned i = 0 ; i < NumEl; i++)
    {
        ComplexStructVec[i].re = ComplexArrVec[i][0];
        ComplexStructVec[i].im = ComplexArrVec[i][1];
    }
}

通过这样做,您可以消除大量的生成代码,因为它不需要处理两个参数重叠的可能性。
示例:https://godbolt.org/z/F3DUaq(只需删除“restrict”即可看到区别)。如果NumEl小于18,则会将整个内容展开成每次迭代一个加载和一个存储。

严谨地说,你应该对两个指针都使用 restrict 限定符。在这种特定情况下可能不重要。 - Lundin
@Lundin: 如果你想的话,可以restrict同时限制两个指针,但与我只在一个指针上使用它并没有什么不同。这里有一些关于此的详细信息:https://dev59.com/3HRB5IYBdhLWcg3w9Lvn#43671121 。一个外行人的解释(不是针对你):如果一个房间里有两个人,并且其中一个人有一个唯一的名字,那么房间里是否有任何一个人的名字不是唯一的? - John Zwinck
但是如果函数中存在其他指针,访问全局变量就会变得复杂。因为编译器无法假设你留下没有使用 restrict 的参数不与全局变量别名。 - Lundin

2

是的,你可以使用memcpy,但需要注意以下几点:

  1. 数组和结构体的布局必须完全相同,这意味着编译器不会对数组中的项或结构体中的条目进行对齐。
  2. 与结构体相关联的内存大小与数组相同。
  3. 您不关心可移植性到其他架构(可能会改变问题#1和/或#2的答案)。
  4. 这不是理想的编程技术,因为它有一些潜在的缺陷,如上所述。

如果在上述条件下您仍想这样做,则以下代码应该可以解决问题:

/* NOTE: sizeof(ComplexStructVec) === sizeof(ComplexArrVec) */
memcpy((void *) ComplexStructVec,
       (void *) ComplexArrVec,
       sizeof(ComplexStructVec)*NumEl);

这样做的作用是,由于在两种情况下你都使用了向量(数组),因此你只需使用它们的名称就可以得到它们的地址。 memcpy 将目标地址和源地址定义为 void *,因此我进行了强制类型转换。要复制的字节数是结构体或数组的大小(请参见注意事项)乘以向量中条目的数量。 (void *) 转换可能不是必需的。这取决于编译器、语言标准级别和其他编译时限定符。
另外请注意,我故意没有为返回值留下位置,它是指向目标的指针。如果你需要这个信息,请小心,将它保存到 ComplexStructVec 可能会导致编译器问题(或更糟糕的运行时问题),具体取决于它是由编译器还是在运行时分配的。
一个更完整的例子:
void copy(ComplexStruct* ComplexStructVec, ComplexArr* ComplexArrVec)
{
    unsigned NumEl = 1000;
    memcpy(ComplexStructVec, ComplexArrVec, sizeof(ComplexStruct)*NumEl);
}

如果在OP的循环中添加了restrict,则生成的代码与许多情况下的memcpy相同。如果启用优化编译,则使用memcpy不会加速。 - John Zwinck
1
并非完全正确。在这种特殊情况下,我同意。但是,如果您有一个不规则的结构,其中嵌入了填充,则优化器需要尊重这些内容。这就是为什么我认为组织您的结构以最小化此填充非常重要。 - JonBelanger

1
最便携的方法是使用循环,就像您的示例中一样。这有时被称为结构体的序列化/反序列化。
结构体的问题在于它们不能保证具有像数组那样的一致的内存布局。为了避免对齐问题,编译器可以在任何地方添加填充字节。如果一个结构体仅由8字节的double组成,则填充是极不可能的。但是,从正式上讲,它并不具备可移植性。
但是,您可以相当安全地执行以下操作:
_Static_assert(sizeof(ComplexStruct) == sizeof(double[2]), 
                 "Weird systems not supported");

ComplexStruct cs;
double arr[2];
memcpy(&cs, arr, sizeof arr);
memcpy(arr, &cs, sizeof arr);

这段内容涉及编程,意思是该程序可以在所有现实世界的系统上“相对便携”。另一个选项是通过添加一个联合体(union)来给结构体增加两种不同的变量表示方式,就像这样:
typedef union {
  struct // C11 anonymous struct
  {
    double re;
    double im;
  };
  double arr[2];
}ComplexStruct;

内部结构可能仍然具有填充,因此您应该添加一个正式的静态断言。但是,这使您可以灵活地将数据内容作为单个成员或作为数组使用。
最后,C实际上支持复数。 double _Complex是标准C,complex.h是一个标准化的复数库。请参阅如何在C中使用复数?

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