在 C 中复制一个带有字符串成员的结构

Copy a struct with a string member in C

我有一个简单的结构,其中包含一个定义为 char 数组的字符串。我认为使用赋值运算符将结构的一个实例复制到另一个实例只会复制存储在 char 指针中的内存地址。相反,字符串内容似乎被复制了。我举了一个非常简单的例子:

#include <stdio.h>
#include <string.h>

struct Test{
  char str[20];
};

int main(){

  struct Test t1, t2;
  strcpy(t1.str, "Hello");
  strcpy(t2.str, "world");
  printf("t1: %s %p\n", t1.str, (char*)(t1.str));
  printf("t2: %s %p\n", t2.str, (char*)(t2.str));
  t2 = t1;
  printf("t2: %s %p\n", t2.str, (char*)(t2.str));
  return 0;
}

用 gcc 4.9.2 编译这段代码我得到:

t1: Hello 0x7fffb8fc9df0
t2: world 0x7fffb8fc9dd0
t2: Hello 0x7fffb8fc9dd0

据我了解,在 t2 = t1 之后 t2.str 指向它在赋值之前指向的同一内存地址,但现在在该地址内有在 t1.str 内找到的相同字符串。所以在我看来,字符串内容已经自动从一个内存位置复制到另一个内存位置,我认为 C 不会这样做。我认为这种行为是由于我将 str 声明为 char[] 而不是 char* 而触发的。实际上,尝试使用 t2.str = t1.str 将一个字符串直接分配给另一个字符串会出现此错误:

Test.c: In function ‘main’:
Test.c:17:10: error: assignment to expression with array type
   t2.str = t1.str;
      ^

这让我觉得在某些情况下数组的处理方式与指针不同。我仍然无法弄清楚数组赋值的规则是什么,或者换句话说,为什么当我将一个结构复制到另一个结构时复制结构内的数组,但我不能直接将一个数组复制到另一个数组。

该结构不包含指针,但包含 20 个字符。 t2 = t1之后,t1的20个字符被复制到t2.

在 C 中,struct 是编译器了解如何构造内存区域的一种方式。 struct 是一种模板或模版,C 编译器使用它来确定如何计算结构的各个成员的偏移量。

第一个 C 编译器不允许 struct 赋值,所以人们不得不使用 memcpy() 函数来分配结构,但后来的编译器允许了。 C 编译器将通过复制 struct 内存区域的字节数来执行 struct 赋值,包括可能为从一个地址到另一个地址的地址对齐添加的填充字节。源内存区域中的任何内容都会被复制到目标区域。副本没有做任何聪明的事情。它只是将这么多字节的数据从一个内存位置复制到另一个内存位置。

如果您在 struct 中有一个字符串数组或任何类型的数组,那么整个数组将被复制,因为它是结构的一部分。

如果struct包含指针变量,那么这些指针变量也会从一个区域复制到另一个区域。这样做的结果是您将拥有两个具有相同数据的结构。每个结构中的指针变量将具有相似的地址值,这两个区域是彼此的副本,因此一个结构中的特定指针将与另一个结构中的相应指针具有相同的地址,并且都指向相同的位置。

请记住,结构赋值只是将数据字节从一个内存区域复制到另一个内存区域。例如,如果我们有一个简单的 struct 和一个 char 数组,C 源代码如下所示:

typedef struct {
    char tt[50];
} tt_struct;

void test (tt_struct *p)
{
    tt_struct jj = *p;

    tt_struct kk;

    kk = jj;
}

Visual Studio 2005 C++ 编译器在调试模式下为 kk = jj; 分配的汇编程序列表输出如下:

; 10   :    tt_struct kk;
; 11   : 
; 12   :    kk = jj;

  00037 b9 0c 00 00 00   mov     ecx, 12            ; 0000000cH
  0003c 8d 75 c4     lea     esi, DWORD PTR _jj$[ebp]
  0003f 8d 7d 88     lea     edi, DWORD PTR _kk$[ebp]
  00042 f3 a5        rep movsd
  00044 66 a5        movsw

这段代码正在将 4 字节字的数据从内存中的一个位置复制到另一个位置。 char 数组大小较小时,编译器可能会选择使用不同系列的指令来更有效地复制内存。

在 C 中,数组的处理方式并不巧妙。数组不像 Java 看待数组那样被视为数据结构。在 Java 中,数组是由对象数组组成的一种对象。在 C 中,数组只是一个内存区域,数组名实际上被视为常量指针或无法更改的指针。结果是在 C 中你可以有一个数组说 int myInts[5];,其中 Java 将被视为一个包含五个整数的数组,但是对于 C 来说它实际上是一个带有标签 myInts 的常量指针。在 Java 中,如果您尝试访问超出范围的数组元素,例如 myInts[i],其中 i 的值为 8,您将收到运行时错误。在 C 中,如果您尝试访问超出范围的数组元素,比如 myInts[i],其中 i 的值为 8,您将不会收到运行时错误,除非您正在使用一个不错的 C 编译器进行调试构建运行时检查。然而,有经验的 C 程序员倾向于将数组和指针视为类似的构造,尽管数组作为指针确实有一些限制,因为它们是常量指针的一种形式,并不完全是指针,但具有一些类似于指针的特征。

这种缓冲区溢出错误在 C 语言中很容易发生,方法是访问超过其元素数量的数组。典型的例子是将一个 char 数组的字符串复制到另一个 char 数组中,而源 char 数组中没有零终止字符,导致在您期望十个或十五个字节的字符串副本中有几百个字节。

在你的案例中确实有 20 个字符,就像你将结构声明为 struct Test {char c1, char c2, ...}

如果您只想复制指向字符串的指针,您可以如下更改结构声明并通过函数Test_initTest_delete 手动管理字符串的内存。

struct Test{
  char* str;
};

void Test_init(struct Test* test, size_t len) {
  test->str = malloc(len);
}

void Test_delete(struct Test* test) {
  free(test->str);
}

如果你运行下面的简单程序

#include <stdio.h>

int main( void )
{
    {
        struct Test
        {
            char str[20];
        };
        printf( "%zu\n", sizeof( Test ) );
    }

    {
        struct Test
        {
            char *str;
        };
        printf( "%zu\n", sizeof( Test ) );
    }
    return 0;
}

你会得到类似下面的结果

20
4

因此第一个结构包含一个包含 20 个元素的字符数组,而第二个结构仅包含一个类型为 char * 的指针。

当一个结构被分配给另一个结构时,它的数据成员被复制。所以对于第一个结构,数组的所有内容都被复制到另一个结构中。对于第二个结构,只复制指针的值(它包含的地址)。指针指向的内存不会被复制,因为它不包含在结构本身中。

虽然表达式中的数组名称(极少数例外)通常会转换为指向其第一个元素的指针,但数组不是指针。