结构体直接赋值的实现

拷贝自:https://blog.csdn.net/hazir/article/details/9429017
看到很多C代码的结构体赋值都是用memcpy函数来拷贝,其实根本不需要,用等号直接赋值即可。结构体描述的是的一块连续内存的内存布局,同类型的一个结构体变量给另一个结构体变量赋值,使用等号编译不会有任何问题,其效果显然也和人直觉认为的一样,拷贝对应内存。
通过反汇编看编译器的行为,效果跟memcpy一样,还省掉了函数调用开销,代码更加的简洁明了。
结论:一般来说,C语言中的结构体变量可以用另一个变量对其进行赋值或初始化。简单结构体(不包含指针成员)直接赋值没有问题。但是稍微复杂一点的结构体里面会有指针成员,那么以上的浅拷贝则会有问题。
1)赋值会直接将结构体的指针变量被赋值,赋值之前其所指向的内存单元丢失;
2)赋值之后若该指针指向的内存单元被其它指针释放掉,那么继续使用该成员指针的话则又可能导致内存泄漏。


下面是一个实例:
#include <stdio.h>struct Foo {
    char a;
    int b;
    double c;
}foo1, foo2;          //define two structs with three different fields
void struct\_assign(void)
{
    foo2 = foo1;       //structure directly assignment
}
​
int main()
{
    foo1.a = 'a';
    foo1.b = 1;
    foo1.c = 3.14;
    struct\_assign();
    printf("%c %d %lf\\n", foo2.a, foo2.b, foo2.c);
​
    return 0;   
}
 

我在 Ubuntu 13.04 下使用 gcc 4.7.3 编译运行得到的结果,如下所示:

guohl@guohailin:~/Documents/c$ gcc struct\_test1.c -o struct\_test1
guohl@guohailin:~/Documents/c$ ./struct\_test1 
a 1 3.140000

可以从结果上看出,结构体直接赋值在 C 语言下是可行的,我们看看struct_assign()函数的汇编实现,从而从底层看看 C 语言是如何实现两个结构体之间的赋值操作的:

struct\_assign:
  pushl   %ebp
  movl   %esp, %ebp
  movl   foo1, %eax
  movl   %eax, foo2     //copy the first 4 bytes from foo1 to foo2
  movl   foo1+4, %eax
  movl   %eax, foo2+4   //copy the second 4 bytes from foo1 to foo2      
  movl   foo1+8, %eax
  movl   %eax, foo2+8   //copy the third 4 bytes from foo1 to foo2  
  movl   foo1+12, %eax
  movl   %eax, foo2+12   //copy the forth 4 bytes from foo1 to foo2  
  popl   %ebp
  ret

这段汇编比较简单,由于结构体的对齐的特性,sizeof(srtruct Foo)=16, 通过四次movl操作将 foo1 的结构体内容拷贝到结构体 foo2 中。从汇编上看出,结构体赋值,采用的类似于memcpy这种形式,而不是逐个字段的拷贝。

复杂结构体的赋值

如果结构体中含有其它复杂数据类型呢,例如数组、指针、结构体等,从上面的汇编实现可以看出,只要两个结构体类型相同,就可以实现赋值,如下例:

#include <stdio.h>struct Foo {
    int n;
    double d\[2\];
    char \*p\_c;
}foo1, foo2;
​
int main()
{
    char \*c = (char \*) malloc (4\*sizeof(char));
    c\[0\] = 'a'; c\[1\] = 'b'; c\[2\] = 'c'; c\[3\] = '\\0';
​
    foo1.n = 1;
    foo1.d\[0\] = 2; foo1.d\[1\] = 3;
    foo1.p\_c = c;
​
    foo2 = foo1;     //assign foo1 to foo2
​
    printf("%d %lf %lf %s\\n", foo2.n, foo2.d\[0\], foo2.d\[1\], foo2.p\_c);
​
    return 0;
}

运行结果如下:

guohl@guohailin:~/Documents/c$ gcc struct\_test2.c -o struct\_test2
guohl@guohailin:~/Documents/c$ ./struct\_test2
1 2.000000 3.000000 abc

可以看出结果和我们想象的是一样的。再次验证结构体的赋值,是直接结构体的内存的拷贝!但正是这个问题,如上面的实例,foo1 和 foo2 中 p_c 指针都是指向我们申请的一块大小为 4 个字节的内存区域,这里注意的是,结构体的拷贝只是浅拷贝,即指针 p_c 的赋值并不会导致再申请一块内存区域,让 foo2 的 p_c 指向它。那么,如果释放掉 foo1 中的 p_c 指向的内存,此时 foo2 中 p_c 变成野指针,这是对 foo2 的 p_c 操作就会出现一些不可预见的问题!在 C++ 中引入了一种可以允许用户重载结构体赋值操作运算,那么我们就可以根据语义重载赋值操作。

数组是二等公民

二等公民在维基百科上的解释是:

二等公民不是一个正式的术语,用来描述一个社会体系内对一部分人的歧视或对外来人口的政治限制,即使他们作为一个公民或合法居民的地位。 二等公民虽然不一定是奴隶或罪犯,但他们只享有有限的合法权利、公民权利和经济机会,并经常受到虐待或忽视。法律无视二等公民,不向他们提供保护,甚至在制订法律时可能会根本不考虑他们的利益。划分出二等公民的行为,普遍被视为一种侵犯人权的行为。 典型的二等公民所面临的障碍包括但不仅限于(缺乏或丧失表决权):权利被剥夺,限制民事或军事服务(不包括任何情况下的征兵),以及限制,语言,宗教,教育,行动和结社的自由,武器的所有权,婚姻,性别认同和表达,住房和财产所有权 。

从词条上解释可以看出二等公民与一等公民在权利上是有差别的,这个词很有意思作为计算机专业术语,其含义也有异曲同工之妙!同样我们看看维基百科对计算机的术语”first-class citizen"(一等公民)的定义,一般要满足以下几点,

  • can be stored in variables and data structures

  • can be passed as a parameter to a subroutine

  • can be returned as the result of a subroutine

  • can be constructed at run-time

  • has intrinsic identity (independent of any given name)

对比着上面的定义来看 C 语言数组,数组作为一个函数的参数传递时,退化成一个指针; 同时,数组无法作为函数的返回值; 也许让数组更不服气的是,数组之间不能直接赋值操作,如下面的操作就是非法的:

int a\[10\];
int b\[10\];
a = b;

但是如果数组包装在结构体中,那么就能进行赋值了!相比之下,结构体可以作为函数参数和返回值,这就是一等公民的待遇!至于为什么数组必须是二等公民,这是有历史原因的,大家可以参考 C 语言的发展史来看,有时间这块内容我再补上!


参考资料:

posted @ 2020-10-12 17:46  不明白就去明白  阅读(6289)  评论(0编辑  收藏  举报