精华内容
下载资源
问答
  • 由于在x86下,GCC默认按4字节对齐,它会在sex后面跟name后面分别填充三个和两个字节使length和整个结构体对齐。于是我们sizeof(my_stu)会得到长度为20,而不是15. 四、__attribute__选项 我们可以按照自己设定的...

    一、概念

    对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的。

    二、为什么要字节对齐

    需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,举个例:

    char ch[8];

    char *p = &ch[1];

    int i = *(int *)p;

    运行时会报segment error,而在x86上就不会出现错误,只是效率下降。

    三、正确处理字节对齐

    对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:

    数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。

    联合 :按其包含的长度最大的数据类型对齐。

    结构体: 结构体中每个数据类型都要对齐。

    比如有如下一个结构体:

    struct stu{

    char sex;

    int length;

    char name[10];

    };

    struct stu my_stu;

    由于在x86下,GCC默认按4字节对齐,它会在sex后面跟name后面分别填充三个和两个字节使length和整个结构体对齐。于是我们sizeof(my_stu)会得到长度为20,而不是15.

    四、__attribute__选项

    我们可以按照自己设定的对齐大小来编译程序,GNU使用__attribute__选项来设置,比如我们想让刚才的结构按一字节对齐,我们可以这样定义结构体

    struct stu{

    char sex;

    int length;

    char name[10];

    }__attribute__ ((aligned (1)));

    struct stu my_stu;

    则sizeof(my_stu)可以得到大小为15。

    上面的定义等同于

    struct stu{

    char sex;

    int length;

    char name[10];

    }__attribute__ ((packed));

    struct stu my_stu;

    __attribute__((packed))得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐.

    五、什么时候需要设置对齐

    在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样.

    一、快速理解

    1. 什么是字节对齐?

    在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

    为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”. 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除.

    2. 字节对齐有什么作用?

    字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

    对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。

    3. 更改C编译器的缺省字节对齐方式

    在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

    · 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。

    · 使用伪指令#pragma pack (),取消自定义字节对齐方式。

    另外,还有如下的一种方式:

    · __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

    · __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

    4. 举例说明

    例1

    struct test

    {

    char x1;

    short x2;

    float x3;

    char x4;

    };

    由于编译器默认情况下会对这个struct作自然边界(有人说“自然对界”我觉得边界更顺口)对齐,结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然边界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大边界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

    例2

    #pragma pack(1) //让编译器对这个结构作1字节对齐

    struct test

    {

    char x1;

    short x2;

    float x3;

    char x4;

    };

    #pragma pack() //取消1字节对齐,恢复为默认4字节对齐

    这时候sizeof(struct test)的值为8。

    例3

    #define GNUC_PACKED __attribute__((packed))

    struct PACKED test

    {

    char x1;

    short x2;

    float x3;

    char x4;

    }GNUC_PACKED;

    这时候sizeof(struct test)的值仍为8。

    二、深入理解

    什么是字节对齐,为什么要对齐?

    TragicJun 发表于 2006-9-18 9:41:00 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

    对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。

    二.字节对齐对程序的影响:

    先让我们看几个例子吧(32bit,x86环境,gcc编译器):

    设结构体如下定义:

    struct A

    {

    int a;

    char b;

    short c;

    };

    struct B

    {

    char b;

    int a;

    short c;

    };

    现在已知32位机器上各种数据类型的长度如下:

    char:1(有符号无符号同)

    short:2(有符号无符号同)

    int:4(有符号无符号同)

    long:4(有符号无符号同)

    float:4        double:8

    那么上面两个结构大小如何呢?

    结果是:

    sizeof(strcut A)值为8

    sizeof(struct B)的值却是12

    结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个,B也一样;按理说A,B大小应该都是7字节。

    之所以出现上面的结果是因为编译器要对数据成员在空间上进行对齐。上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认对齐设置呢,当然可以.例如:

    #pragma pack (2) /*指定按2字节对齐*/

    struct C

    {

    char b;

    int a;

    short c;

    };

    #pragma pack () /*取消指定对齐,恢复缺省对齐*/

    sizeof(struct C)值是8。

    修改对齐值为1:

    #pragma pack (1) /*指定按1字节对齐*/

    struct D

    {

    char b;

    int a;

    short c;

    };

    #pragma pack () /*取消指定对齐,恢复缺省对齐*/

    sizeof(struct D)值为7。

    后面我们再讲解#pragma pack()的作用.

    三.编译器是按照什么样的原则进行对齐的?

    先让我们看四个重要的基本概念:

    1.数据类型自身的对齐值:

    对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。

    2.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

    3.指定对齐值:#pragma pack (value)时的指定对齐值value。

    4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

    有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。

    例子分析:

    分析例子B;

    struct B

    {

    char b;

    int a;

    short c;

    };

    假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B共有12个字节,sizeof(struct B)=12;其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍.其实诸如:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了.

    同理,分析上面例子C:

    #pragma pack (2) /*指定按2字节对齐*/

    struct C

    {

    char b;

    int a;

    short c;

    };

    #pragma pack () /*取消指定对齐,恢复缺省对齐*/

    第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放

    在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.

    四.如何修改编译器的默认对齐值?

    1.在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节。

    2.在编码时,可以这样动态修改:#pragma pack .注意:是pragma而不是progma.

    五.针对字节对齐,我们在编程中如何考虑?

    如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做法是显式的插入reserved成员:

    struct A{

    char a;

    char reserved[3];//使用空间换时间

    int b;

    }

    reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.

    六.字节对齐可能带来的隐患:

    代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:

    unsigned int i = 0x12345678;

    unsigned char *p=NULL;

    unsigned short *p1=NULL;

    p=&i;

    *p=0x00;

    p1=(unsigned short *)(p+1);

    *p1=0x0000;

    最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。

    在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.

    七.如何查找与字节对齐方面的问题:

    如果出现对齐或者赋值问题首先查看

    1. 编译器的big little端设置

    2. 看这种体系本身是否支持非对齐访问

    3. 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作

    举例:

    #include

    main()

    {

    struct A {

    int a;

    char b;

    short c;

    };

    struct B {

    char b;

    int a;

    short c;

    };

    #pragma pack (2) /*指定按2字节对齐*/

    struct C {

    char b;

    int a;

    short c;

    };

    #pragma pack () /*取消指定对齐,恢复缺省对齐*/

    #pragma pack (1) /*指定按1字节对齐*/

    struct D {

    char b;

    int a;

    short c;

    };

    #pragma pack ()/*取消指定对齐,恢复缺省对齐*/

    int s1=sizeof(struct A);

    int s2=sizeof(struct B);

    int s3=sizeof(struct C);

    int s4=sizeof(struct D);

    printf("%d\n",s1);

    printf("%d\n",s2);

    printf("%d\n",s3);

    printf("%d\n",s4);

    }

    输出:

    8

    12

    8

    7

    修改代码:

    struct A {

    // int a;

    char b;

    short c;

    };

    struct B {

    char b;

    // int a;

    short c;

    };

    输出:

    4

    4

    输出都是4,说明之前的int影响对齐!

    展开全文
  • 开发环境 Windows 10 Rust 1.56.1 VS Code 1.62.3 ...要定义结构,我们输入关键字struct并命名整个结构体。结构体的名称应该描述将数据块组合在一起的重要性。然后,在花括号中定义数据片段...

    开发环境

    • Windows 10
    • Rust 1.56.1

     

    •   VS Code 1.62.3

     项目工程

    这里继续沿用上次工程rust-demo

    结构体

     结构体类似于元组。与元组一样,结构的碎片可以是不同的类型。与元组不同的是,我们将命名每个数据段,这样就可以清楚地了解值的含义。由于这些名称,结构体比元组更灵活:我们不必依赖数据的顺序来指定或访问实例的值。

    要定义结构,我们输入关键字struct并命名整个结构体。结构体的名称应该描述将数据块组合在一起的重要性。然后,在花括号中定义数据片段的名称和类型,我们称之为字段。示例如下:

    struct User {
        active: bool,
        username: String,
        email: String,
        sign_in_count: u64,
    }

    为了在定义结构体之后使用它,我们通过为每个字段指定具体的值来创建该结构的一个实例。我们通过声明结构的名称来创建一个实例,然后添加包含key:value对的花括号,其中key是字段的名称,value是我们想要存储在这些字段中的数据。我们不必按照在结构中声明字段的顺序指定字段。换句话说,结构体定义类似于类型的通用模板,实例使用特定的数据填充该模板,以创建类型的值。示例如下:

    let user1 = User {
            email: String::from("someone@example.com"),
            username: String::from("someusername123"),
            active: true,
            sign_in_count: 1,
        };

    要从结构体中得到一个特定的值,我们可以使用点表示法。如果我们只想要这个用户的电子邮件地址,我们可以使用user1.email,只要我们想使用这个值。如果实例是可变的,我们可以通过使用点表示法并将其赋值到特定字段来更改值。如下所示:

       let mut user1 = User {
            email: String::from("someone@example.com"),
            username: String::from("someusername123"),
            active: true,
            sign_in_count: 1,
        };
    
        // 使用结构体User中的email成员
        user1.email = String::from("anotheremail@example.com");

    请注意,整个实例必须是可变的;Rust不允许我们只将某些字段标记为可变的。与任何表达式一样,我们可以构造一个结构体的新实例,作为函数体中的最后一个表达式,以隐式返回该新实例。如下所示:

    struct User {
        active: bool,
        username: String,
        email: String,
        sign_in_count: u64,
    }
    
    // 构建结构体User
    fn build_user(email: String, username: String) -> User {
        User {
            email: email,
            username: username,
            active: true,
            sign_in_count: 1,
        }
    }
    
    fn main() {
        let user1 = build_user(
            String::from("someone@example.com"),
            String::from("someusername123"),
        );
    }
    

    将函数参数命名为结构体字段相同的名称是有意义的,但是必须重复emailusername字段名和变量有点繁琐。如果结构体有更多的字段,重复每个名称将变得更加烦人。幸运的是,有一个方便的速记!

    使用字段Init

    由于参数名称和结构体字段名和上述示例完全相同,我们可以使用字段init速记语法重写build_user,使其行为完全相同,但不会重复emailusername。如下所示:

    fn build_user(email: String, username: String) -> User {
        User {
            email,
            username,
            active: true,
            sign_in_count: 1,
        }
    }

    在这里,我们正在创建一个新的用户结构体实例,其中有一个名为email的字段。我们希望将email字段的值设置为build_user函数的email参数中的值。因为“email”字段和“email”参数具有相同的名称,所以我们只需要编写email,而不是email:email

    从其他实例创建实例

    创建结构体的新实例通常是有用的,它使用了大多数旧实例的值,但更改了一些值。可以使用结构体更新语法来完成此操作。如下所示:

      let user2 = User {
            active: user1.active,
            username: user1.username,
            email: String::from("another@example.com"),
            sign_in_count: user1.sign_in_count,
        };

    使用结构体更新语法,我们可以使用更少的代码实现同样的效果。语法..。指定未显式设置的其余字段应具有与给定实例中的字段相同的值。如下所示:

    // 使用更新语法..构建结构体实例  
    let user2 = User {
            email: String::from("another@example.com"),
            ..user1
        };

    上述代码中还在user2中创建了一个实例,该实例具有不同的email,但与user 1中的username、ative和signin_count字段的值相同。..user1必须最后指定任何剩余字段应该从user1中的相应字段中获取值,但是我们可以选择以任意顺序为任意多个字段指定值,而不管结构定义中字段的顺序如何。

    使用元组结构

    还可以定义看起来类似于元组的结构体,称为元组结构体。元组结构体具有添加的意思,即结构体名称提供,但不具有与其字段相关联的名称;相反,它们只具有字段的类型。当希望为整个元组命名并使元组成为与其他元组不同的类型时,元组结构体非常有用,并且将每个字段命名为常规结构将是冗长或多余的。

    要定义元组结构体,请从struct关键字和结构体名称开始,后面跟着元组中的类型。如下所示:

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    注意,blackorigin是不同的类型,因为它们是不同元组结构的实例。我们定义的每个结构都是自己的类型,即使结构体中的字段具有相同的类型。例如,接受Color类型参数的函数不能将Point作为参数,即使这两种类型都由三个i32值组成。否则,元组结构实例的行为就像元组:我们可以将它们分解为它们的各个部分,我们可以使用一个.后面跟着访问单个值的索引,依此类推。

    无字段的单元式结构体

    还可以定义没有任何字段的结构体,这些被称为unit-like结构体,因为它们的行为类似于()unit-like结构体在需要在某种类型上实现一个特性但不想存储在类型本身中的任何数据的情况下是有用的。如下所示:

    fn main() {
        struct AlwaysEqual;
    
        let subject = AlwaysEqual;
    }
    

    要定义AlwaysEquate,我们使用struct关键字,接着结构体的名称,然后是分号。不需要花括号或括号!然后,我们可以以类似的方式用变量subject获得AlwaysEquate的一个实例:使用我们定义的名称,没有任何花括号或括号。想象一下,我们将为这种类型实现行为,即每个实例总是与每一个其他类型的每个实例相等,为了测试目的,可能是为了获得一个已知的结果。我们不需要任何数据来实现这种行为!

    结构体数据的所有权

    在前面的User结构体定义中,我们使用了自己的字符串类型,而不是&str String字符串切片类型。这是一个深思熟虑的选择,因为我们希望该结构的实例拥有它的所有数据,并且只要整个结构有效,该数据就会有效。

    结构体可以存储对其他东西拥有的数据的引用,但是这样做需要使用lifetimeslifetime确保结构体所引用的数据在结构体的有效期内有效。假设要尝试将引用存储在一个结构体中,而不指定生命周期,就像如下示例,是行不通的。

    struct User {
        username: &str,
        email: &str,
        sign_in_count: u64,
        active: bool,
    }
    
    fn main() {
        let user1 = User {
            email: "someone@example.com",
            username: "someusername123",
            active: true,
            sign_in_count: 1,
        };
    }

    运行上述代码

    cargo run
    

     

    本章重点

    • 结构体概念
    • 定义结构体
    • 实例化结构体
    展开全文
  • 多字段更新?并发编程中,原子更新多个字段是常见的需求。举个例子,有一个 struct Person 的结构体,里面有两个字段。我们先更新 Person.name,再更新 Person.ag...


    多字段更新?

    并发编程中,原子更新多个字段是常见的需求。

    举个例子,有一个 struct Person 的结构体,里面有两个字段。我们先更新 Person.name,再更新 Person.age ,这是两个步骤,但我们必须保证原子性。

    有童鞋可能奇怪了,为什么要保证原子性?

    我们以一个示例程序开端,公用内存简化成一个全局变量,开 10 个并发协程去更新。你猜最后的结果是啥?

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    type Person struct {
        name string
        age  int
    }
    
    // 全局变量(简单处理)
    var p Person
    
    func update(name string, age int) {
        // 更新第一个字段
        p.name = name
        // 加点随机性
        time.Sleep(time.Millisecond*200)
        // 更新第二个字段
        p.age = age
    }
    
    func main() {
        wg := sync.WaitGroup{}
        wg.Add(10)
        // 10 个协程并发更新
        for i := 0; i < 10; i++ {
            name, age := fmt.Sprintf("nobody:%v", i), i
            go func() {
                defer wg.Done()
                update(name, age)
            }()
        }
        wg.Wait()
        // 结果是啥?你能猜到吗?
        fmt.Printf("p.name=%s\np.age=%v\n", p.name, p.age)
    }
    

    打印结果是啥?你能猜到吗?

    可能是这样的:

    p.name=nobody:2
    p.age=3
    

    也可能是:

    p.name=nobody:8
    p.age=7
    

    按照排列组合来算,一共有 10*10 种结果。

    那我们想要什么结果?我们想要 name 和 age 一定要是匹配的,不能牛头不对马嘴。换句话说,name 和 age 的更新一定要原子操作,不能出现未定义的状态。

    我们想要的是 ( nobody:i,i ),正确的结果只能在以下预定的 10 种结果出现:

    ( nobody:0, 0 )
    ( nobody:1, 1 )
    ( nobody:2, 2 )
    ( nobody:3, 3 )
        ...
    ( nobody:9, 9 )
    

    这仅仅是一个简单的示例,童鞋们思考下自己现实的需求,应该是非常常见的。

    现在有两个问题:

    第一个问题:这个 demo 观察下运行时间,用 time 来观察,时间大概是 200 ms 左右,为什么?

    root@ubuntu:~/code/gopher/src/atomic_test# time ./atomic_test 
    p.name=nobody:8
    p.age=7
    
    real 0m0.203s
    user 0m0.000s
    sys 0m0.000s
    

    如上就是 203 毫秒。划重点:这个时间大家请先记住了,对我们分析下面的例子有帮助。

    这个 200 毫秒是因为奇伢在 update 函数中故意加入了一点点时延,这样可以让程序估计跑慢一点。

    每个协程跑 update 的时候至少需要 200 毫秒,10 个协程并发跑,没有任何互斥,时间重叠,所以整个程序的时间也是差不都 200 毫秒左右。

    第二个问题:怎么解决这个正确性的问题。

    大概两个办法:

    1. 锁互斥

    2. 原子操作

    下面详细分析下异同和优劣。

    锁实现

    在并发的上下文,用锁来互斥,这是最常见的思路。 锁能形成一个临界区,锁内的一系列操作任何时刻都只会有一个人更新,如此就能确保更新不会混乱,从而保证多步操作的原子性。

    首先配合变量,对应一把互斥锁:

    // 全局变量(简单处理)
    var p Person
    // 互斥锁,保护变量更新
    var mu sync.Mutex
    

    更新的逻辑在锁内:

    func update(name string, age int) {
        // 更新:加锁,逻辑串行化
        mu.Lock()
        defer mu.Unlock()
    
        // 以下逻辑不变
    }
    

    大家按照上面的把程序改了之后,逻辑是不是就正确了。一定是 ( nobody:i,i )配套更新的。

    但你注意到另一个可怕的问题吗?

    程序运行变的好慢!!!!

    同样用 time 命令统计下程序运行时间,竟然耗费 2 秒!!!,10 倍的时延增长,每次都是这样。

    root@ubuntu:~/code/gopher/src/atomic_test# time ./atomic_test 
    p.name=nobody:8
    p.age=8
    
    real 0m2.017s
    user 0m0.000s
    sys 0m0.000s
    

    不禁要问自己,为啥?

    还记得上面我提到过,一个 update 固定要 200 毫秒。

    加锁之后的 update 函数逻辑全部在锁内,10 个协程并发跑 update 函数,但由于锁的互斥性,抢锁不到就阻塞等待,保证 update 内部逻辑的串行化。

    第 1 个协程加上锁了,后面 9 个都要等待,依次类推。最长的等待时间应该是 1.8 秒。

    换句话说,程序串行执行了 10 次 update 函数,时间是累加的。程序 2 秒的运行时延就这样来的。

    加锁不怕,抢锁等待才可怕。在大量并发的时候,由于锁的互斥特性,这里的性能可能堪忧。

    还有就是抢锁失败的话,是要把调度权让出去的,直到下一次被唤醒。这里还增加了协程调度的开销,一来一回可能性能就更慢了下来。

    思考:用锁之后正确性是保证了,某些场景性能可能堪忧。那咋吧?

    在本次的例子,下一步的进化就是:原子化操作。

    温馨提示

    怕童鞋误会,声明一下:锁不是不能用,是要区分场景,不分场景的性能优化措施是没有意义的哈。大部分的场景,用锁没啥问题。且锁是可以细化的,比如读锁和写锁,更新加写锁,只读操作加读锁。这样确实能带来较大的性能提升,特别是在写少读多的时候。

    原子操作

    其实我们再深究下,这里本质上是想要保证更新 name 和 age 的原子性,要保证他们配套。其实可以先在局部环境设置好 Person 结构体,然后一把原子赋值给全局变量即可。Go 提供了 atomic.Value 这个类型。

    怎么改造?

    首先把并发更新的目标设置为 atomic.Value 类型:

    // 全局变量(简单处理)
    var p atomic.Value
    

    然后 update 函数改造成先局部构造,再原子赋值的方式:

    func update(name string, age int) {
        lp := &Person{}
        // 更新第一个字段
        lp.name = name
        // 加点随机性
        time.Sleep(time.Millisecond * 200)
        // 更新第二个字段
        lp.age = age
        // 原子设置到全局变量
        p.Store(lp)
    }
    

    最后 main 函数读取全局变量打印的地方,需要使用原子 Load 方式:

        // 结果是啥?你能猜到吗?
        _p := p.Load().(*Person)
        fmt.Printf("p.name=%s\np.age=%v\n", _p.name, _p.age)
    

    这样就解决并发更新的正确性问题啦。感兴趣的童鞋可以运行下,结果都是正确的 ( nobody:i,i )。

    下面再看一下程序的运行时间:

    root@ubuntu:~/code/gopher/src/atomic_test# time ./atomic_test 
    p.name=nobody:7
    p.age=7
    
    real 0m0.202s
    user 0m0.000s
    sys 0m0.000s
    

    竟然是 200 毫秒作用,比锁的实现时延少 10 倍,并且保证了正确性。

    为什么会这样?

    因为这 10 个协程还是并发的,没有类似于锁阻塞等待的操作,只有最后 p.Store(lp) 调用内才有做状态的同步,而这个时间微乎其微,所以 10 个协程的运行时间是重叠起来的,自然整个程序就只有 200 毫秒左右。

    锁和原子变量都能保证正确的逻辑。在我们这个简要的场景里,我相信你已经感受到性能的差距了。

    当然了,还是那句话,具体用那个实现要看具体场景,不能一概而论。而且,锁有自己无可替代的作用,它能保证多个步骤的原子性,而不仅仅是字段的赋值。

    相信你已经非常好奇 atomic.Value 了,下面简要的分析下原理,是否真的很神秘呢?

    原理可能要大跌眼镜。

    趁现在我们还不懂内部原理,先思考个问题(不然待会一下子看懂了就没意思了)?

    Value.Store  和 Value.Load 是用来赋值和取值的。我的问题是,这两个函数里面有没有用户数据拷贝?StoreLoad 是否是保证了多字段拷贝的原子性?

    提前透露下:并非如此。

    atomic.Value 原理

     1   atomic.Value 结构体

    atomic.Value  定义于文件 src/sync/atomic/value.go  ,结构本身非常简单,就是一个空接口:

    type Value struct {
        v interface{}
    }
    

    在之前文章中,奇伢有分享过 Go 的空接口类型( interface {} )在 Go 内部实现是一个叫做 eface 的结构体( src/runtime/iface.go ):

    type eface struct {
        _type *_type
        data  unsafe.Pointer
    }
    

    interface {} 是给程序猿用的,eface  是 Go 内部自己用的,位于不同层面的同一个东西,这个请先记住了,因为 atomic.Value 就利用了这个特性,在 value.go 定义了一个 ifaceWords 的结构体。

    划重点:interface {}efaceifaceWords 这三个结构体内存布局完全一致,只是用的地方不同而已,本质无差别。这给类型的强制转化创造了前提。

     2   Value.Store 方法

    看一下简要的代码,这是一个简单的 for 循环:

    func (v *Value) Store(x interface{}) {
        // 强制转化类型,转变成 ifaceWords (三种类型,相同的内存布局,这是前提)
        vp := (*ifaceWords)(unsafe.Pointer(v))
        xp := (*ifaceWords)(unsafe.Pointer(&x))
        for {
            // 获取数据类型
            typ := LoadPointer(&vp.typ)
            // 第一个判断:atomic.Value 初始的时候是 nil 值,那么就是走这里进去的;
            if typ == nil {
                runtime_procPin()
                if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
                    runtime_procUnpin()
                    continue
                }
                // 初始赋值
                StorePointer(&vp.data, xp.data)
                StorePointer(&vp.typ, xp.typ)
                runtime_procUnpin()
                return
            }
            // 第二个判断:这个也是初始的时候,这是一个中间状态;
            if uintptr(typ) == ^uintptr(0) {
                continue
            }
            // 第三个判断:类型校验,通过这里就能看出来,Value 里面的类型不能变,否则会 panic;
            if typ != xp.typ {
                panic("sync/atomic: store of inconsistently typed value into Value")
            }
            // 划重点啦:只要过了初始化赋值阶段,基本上就是直接跑到这行代码啦
            StorePointer(&vp.data, xp.data)
            return
        }
    }
    

    有几个点稍微解释下:

    1. atomic.Value 使用 ^uintptr(0) 作为第一次存取的标志位,这个标识位是设置在 type 字段里,这是一个中间状态;

    2. 通过 CompareAndSwapPointer 来确保 ^uintptr(0)  只能被一个执行体抢到,其他没抢到的走 continue ,再循环一次;

    3. atomic.Value 第一次写入数据时,将当前协程设置为不可抢占,当存储完毕后,即可解除不可抢占;

    4. 真正的赋值,无论是第一次,还是后续的 data 赋值,在 Store 内,只涉及到指针的原子操作,不涉及到数据拷贝

    这里有没有大跌眼镜?

    Store 内部并不是保证多字段的原子拷贝!!!!Store  里面处理的是个结构体指针。 只通过了 StorePointer 保证了指针的原子赋值操作。

    我的天?是这样的吗?那何来的原子操作。

    核心在于:Value.Store()  的参数必须是个局部变量(或者说是一块全新的内存)。

    这里就回答了上面的问题:Store,Load 是否有数据拷贝?

    划重点:没有!没动数据

    原来你是这样子的 atomic.Value

    回忆一下我上面的 update 函数,真的是局部变量,全新的内存块

    func update(name string, age int) {
        // 注意哦,局部变量哦
        lp := &Person{}
        // 更新字段 。。。。
     
        // 设置的是全新的内存地址给全局的 atomic.Value 变量
        p.Store(lp)
    }
    

    又有个问题,你可能会想了,如果 p.Store( /* */ ) 传入的不是指针,而是一个结构体呢?

    事情会是这样的:

    1. 编译器识别到这种情况,编译期间就会多生成一段代码,用 runtime.convT2E  函数把结构体赋值转化成 eface (注意,这里会涉及到结构体数据的拷贝);

    2. 然后再调用 Value.Store 方法,所以就 Store 方法而言,行为还是不变;

    再思考一个问题:既然是指针的操作,为什么还要有个 for 循环,还要有个  CompareAndSwapPointer  ?

    这是因为 ifaceWords 是两个字段的结构体,初始赋值的时候,要赋值类型和数据指针两部分。

    atomic.Value 是服务所有类型,此类需求的,通用封装。

     3   Value.Load 方法

    有写就有读嘛,看一下读的简要的实现:

    
    func (v *Value) Load() (x interface{}) {
        vp := (*ifaceWords)(unsafe.Pointer(v))
        typ := LoadPointer(&vp.typ)
        // 初始赋值还未完成
        if typ == nil || uintptr(typ) == ^uintptr(0) {
            return nil
        }
        // 划重点啦:只要过了初始化赋值阶段,原子读的时候基本上就直接跑到这行代码啦;
        data := LoadPointer(&vp.data)
        xp := (*ifaceWords)(unsafe.Pointer(&x))
        // 赋值类型,和数据结构体的地址
        xp.typ = typ
        xp.data = data
        return
    }
    

    哇,太简单了。处理做了一下初始赋值的判断(返回 nil ),后续基本就只靠 LoadPointer 函数来个原子读指针值而已。

    总结

    1. interface {}efaceifaceWords  本质是一个东西,同一种内存的三种类型解释,用在不同层面和场景。它们可以通过强制类型转化进行切换;

    2. atomic.Value 使用 cas 操作只在初始赋值的时候,一旦赋值过,后续赋值的原子操作更简单,依赖于 StorePointer ,指针值得原子赋值;

    3. atomic.ValueStoreLoad 方法都不涉及到数据拷贝,只涉及到指针操作;

    4. atomic.Value 的神奇的核心在于:每次 Store 的时候用的是全新的内存块 !!!LoadStore 都是以完整结构体的地址进行操作,所以才有原子操作的效果。

    5. atomic.Value 实现多字段原子赋值的原理千万不要以为是并发操作同一块多字段内存,还能保证原子性

    后记

    说实话,原理让我大跌眼镜,当然也让我们避免踩坑,就怕你以为 atomic.Value 是万能的, Store  进去了一个会并发操作的内存块,那就尴了个尬了。

    ~完~

    往期推荐

    往期推荐

    浅析 Go  IO 的知识框架

    Go 并发编程 — sync.Pool 源码级原理剖析 [2] 终结篇

    Go 并发编程 — 深入浅出 sync.Pool ,围观最全的使用姿势,理解最深刻的原理

    Golang 并发编程核心篇 —— 内存可见性

    坚持思考,方向比努力更重要。关注我:奇伢云存储

    展开全文
  • 导读一、结构体的声明以及初始化二、联合体以及嵌套结构体的用法位段小端模式三、数据跨地址内存导致的内存对齐现象 一、结构体的声明以及初始化 联合体实际上来说是特殊数据结构的一类,通过关键词struct来定义。...


    用来描述对象特征的各类信息,通常会被整合成记录,而记录使得信息组织、表示以及存储变得轻松。而记录由字段组成,不同的字段用来表示不同的信息,C中可以通过结构体来组合这些字段(成员)。

    一、结构体的声明以及初始化

    联合体实际上来说是特殊数据结构的一类,通过关键词struct来定义。定义结构体有几种方法:

    struct FUNC1//仅声明一个模板
    {
    	char*  	name;
    	uint32_t		height;
    	uint32_t		length;
    };
    

    第一种方法,将会定义一个名称为struct FUNC1的结构体模板,它里面包含了一个指向字符的指针,两个整形的变量。模板并不代表已经分配了空间,或许可以说定义了一个叫FUNC1的新类型。如果要声明一个这样的变量,那么需要借助关键词struct FUNC1 variable1来完成。

    struct FUNC1//声明一个模板的同时声明一个变量
    {
    	char*  	name;
    	uint32_t		height;
    	uint32_t		length;
    }variable1;
    

    这种写法,实际上与第一种方法没什么差别,只是在声明模板的过程中同时声明了一个变量,此时这个变量已经分配了相应的空间。

    typedef struct FUNC1//声明一个模板并将这个模板的名称重定义
    {
    	char*  	name;
    	uint32_t		height;
    	uint32_t		length;
    }FUNC2;
    

    第三种写法则是在第一种写法的基础上利用了关键词typedef,在看这段的时候,可以将其分解为两个步骤:
    1、声明了一个名称为FUNC1的模板。
    2、将类型 struct FUNC1 映射到FUNC2中。
    由此一来,声明一个变量除了使用struct FUNC1 variable1以外,还能使用来FUNC2 variable1实现。

    typedef struct//直接声明一个模板但名称为FUNC2
    {
    	char*  	name;
    	uint32_t		height;
    	uint32_t		length;
    }FUNC2;
    

    这第四中写法,可以和其他几种进行对比,是常用的方法,省去了声明的struct FUNC1的功夫,新的类型名称也不需要前缀struct,变量类型更具隐蔽性。

    说完了结构体的声明,那么对应的变量在空间中究竟是怎么样排放的?
    在这里插入图片描述

    结构体的空间排放由模板中成员的声明顺序决定,从低地址向高地址生长,而在STM32中内存使用的是小端模式,故变量variable1的内存情况如上图所示,一个指针的长度与地址总线长度一致,占用32bit,整形同理。但是这个模板并不是最佳的说明案例,先介绍联合体再进入主题。

    结构体变量的初始化可以分为两种,第一种是全成员的赋值初始化,例如variable1变量:

    FUNC2 variable1={"xx",20,20};
    

    从C99之后,添加了一个新的初始化方案:标准C的标记化结构初始语法,实际上在Linux内核中这个用法比较常见,例如驱动程序中的file_operation结构体就是使用这个方案。

    FUNC2 variable1={
    .length = 20,
    .height = 20
    };
    

    这个方案大幅度的灵活了初始化的操作,例如结构体的有些成员在一开始就有含义,那么就只需要初始化相应的成员。

    顺便一提,结构体的成员一般情况下必须是大小确认的,即不允许可变长度的变量在结构体中。若是结构体中的最后一个成员,则允许该变量为可变数组。但是该结构体模板的空间大小并没有包含这个弹性可变数字成员,所以在使用malloc时,需要额外分配所需的空间:

    FUNC2*	variable1 = malloc(sizeof(FUNC2)+sizeof(float)*10);
    

    二、结构体成员的内存对齐规则

    结构体成员的内存对齐法则在此处不做详细介绍,位字段的内存对齐才是讨论的重点。
    简言之就是结构体中成员的取址方式可能受到其他成员的影响:

    typedef struct
    {
    	char  	name;
    	uint32_t		height;
    	uint16_t  		length;
    }Type1;
    typedef struct
    {
    	char  	name;
    	uint16_t  		length;
    	uint32_t		height;
    
    }Type2;
    Type1 variable3;
    Type2 variable4;
    

    Type1类型的变量,空间存放有稍许不同,规则规定第一个成员name无需任何的偏移。而第二个成员height的放置则和第一个成员的长度有关,height是一个32位(4个字节)的变量,它的地址必须满足是4的倍数,而第一个成员只占了一个字节,那么后面的3个字节都会被编译器填充(padding),所以height偏移了3个字节。第三个成员length长度为2个字节,当前的地址满足被2整除的要求,所以不用偏移。

    照这么算起来,这个模板的总空间长度应该是10个字节,运行代码sizeof(Type1) 得到12个字节。这是由于编译器为了cpu能够保证所有的成员只通过一次地址访问就可以获得,所以整个结构体都默认以最大地址长度为单位,所以在尾部也填充了2个字节
    在这里插入图片描述
    需要留意的是系统填充的空间由于是匿名状态,所以访问不到,但是可以通过强制类型转换进行访问以及修改。

    三、联合体以及嵌套结构体的用法

    联合体是借助关键词union实现的,所谓联合体实际上就是共用一块内存,而联合体所占用的内存根据成员中占用的最大内存来确认。在声明和初始化上,联合体也支持结构体相关的操作。

    typedef union
    {
    	uint8_t data_8[5];
    	uint16_t data_16[2];
    	uint32_t data_32;
    }FUNC2;
    
    FUNC2 variable2;
    

    在这里插入图片描述
    此时若对变量variable2使用函数sizeof可以得到它的长度为5。联合体在数据协议管理上有着重要的用途,通常在面对一些定制且复杂的协议,根据数据去一个个解析是相当麻烦的,但若是配合联合体嵌套则简化很多。例如:在一个字节中隐藏多个状态位:
    在这里插入图片描述

    若每次接收到这一帧数据时都通过位计算则过于繁琐,那么试一下嵌套以及位字段

    typedef union
    {
    	uint8_t data;
    	struct
    	{
    		uint8_t ERR_BIT : 1;
    		uint8_t STATUS_BIT : 3;	
    		uint8_t IT_BIT : 1;
    		uint8_t OVER_BIT : 1;
    		uint8_t BUSY_BIT : 1;
    		uint8_t ENABLE_BIT : 1;
    	};
    }STATUS;
    

    这个嵌套结构体涉及到了两个概念,一个是位段的概念,另一个是小端模式的概念。

    位段

    在通常情况下,计算机处理的最小单元是字节,一个字节是8bit。但是有时候某些开关量的状态并不需要占用一个字节的空间,为了提高空间效率,便产生了位段的概念,位段可以指定一个变量占用的bit。
    位段声明的方式为:
    类型 成员名称: 宽度;
    类型是用来解释该成员的方式,宽度则是这个成员占有的位数。实际上,在声明的过程中,成员名称是可选的,若是该成员匿名了,则在结构体中无法引用它,一般该方式用来填充空间(可以设成0宽度,编译器将默认从下一个可寻址内存地址来读取成员)。

    例如上述STATUS联合体的匿名成员则指定了位,若将一个字节数据赋值给联合体中的data就可以等价于:

    ERR_BIT 	= (uint8_t)(data & 0x01);
    STATUS_BIT 	= (uint8_t)(data & (0x07<<1));
    IT_BIT 		= (uint8_t)(data & (0x01<<4));
    OVER_BIT 	= (uint8_t)(data & (0x01<<5));
    BUSY_BIT 	= (uint8_t)(data & (0x01<<6));
    ENABLE_BIT 	= (uint8_t)(data & (0x01<<7));
    

    位段的操作可以放到任意的结构体中,包括联合体,但是表现出来的形式则不会一样

    typedef union
    {
    	uint8_t data;
    	uint8_t ERR_BIT : 1;
    	uint8_t STATUS_BIT : 3;	
    	uint8_t IT_BIT : 1;
    	uint8_t OVER_BIT : 1;
    	uint8_t BUSY_BIT : 1;
    	uint8_t ENABLE_BIT : 1;
    }STATUS_union;
    

    若将一个字节数据赋值给联合体中的data就可以等价于:

    ERR_BIT 	= (uint8_t)(data & 0x01);
    STATUS_BIT 	= (uint8_t)(data & (0x07));
    IT_BIT 		= (uint8_t)(data & (0x01));
    OVER_BIT 	= (uint8_t)(data & (0x01));
    BUSY_BIT 	= (uint8_t)(data & (0x01));
    ENABLE_BIT 	= (uint8_t)(data & (0x01));
    

    这是因为这两个数据类型的本质区别,结构体的成员内存地址是向上长的,而联合体的成员内存地址是固定的。

    小端模式

    与小端模式对应的是大端模式,他们代表两种相反的数据存放规律。以往最常用的是大端模式,即一组数据的高字节部分放在地址的最前面(起始地址),而低字节则放在地址的最后(最终地址),例如数据0xabcd,高字节是0xab,低字节是0xcd,若是大端模式:
    在这里插入图片描述

    大端模式这样的排序方式比较符合人类的直觉。若是小段模式:
    在这里插入图片描述

    而stm32是小端模式,那么数据的低位将会被放在起始地址,根据结构体的定义,成员的空间排放顺序与声明顺序一致,也是从小到大。

    三、位段数据跨字节单位导致的内存对齐现象

    上述介绍的只是寻常的结构体、联合体的运用,如若出现某些有效的状态位需要跨越字节单位会怎样?例如:
    在这里插入图片描述
    测试代码(环境: vscode 64位,stm32 32位 ,ubuntu 64位):

    typedef union 
    {
    	uint16_t 	code;
    	
    	struct
    	{
    		uint8_t 		ENABLE			    :	1;
    		uint8_t 	    Channel				:	8;
    		uint8_t 	    STATUS				:	7;
        };	
    
    }Type2;//热电偶配置结构体
    Type2 v3;
    void main(char argc,char* argv[])
    {
        v3.code = 0xa5da;
         printf("v3.code = %x \n v3.ENABLE = %x \n v3.Chunnel = %x \n v3.STATUS = %x \n size of v3  = %d \n ",v3.code,v3.ENABLE,v3.Channel,v3.STATUS,sizeof(Type2));
    }
    

    输出:
    v3.code = a5da
    v3.ENABLE = 0
    v3.Channel = a5
    v3.STATUS = 0
    size of v3 = 4
    按照原本的想法:
    code = 1010 0101 1101 1010 b,ENABLE = 0 ;Channel=0xed;STATUS=0x52; 造成差异的原因是在于第二个成员Channel占用了8位,但是当前字节只剩下7位了(第一个成员占了一位),由于当前空间不合适,所以编译器将会让这个成员从下一个字节开始取值。所以出现了运行结果的情况。为了进一步验证,对STATUS成员进行赋值并重新打印

    void main(char argc,char* argv[])
    {
        v3.code = 0xa5da;
        v3.STATUS = 0x75;
         printf("v3.code = %x \n v3.ENABLE = %x \n v3.Chunnel = %x \n v3.STATUS = %x \n size of v3  = %d \n ",v3.code,v3.ENABLE,v3.Channel,v3.STATUS,sizeof(Type2));
    }
    

    输出:
    v3.code = a5da
    v3.ENABLE = 0
    v3.Channel = a5
    v3.STATUS = 75
    size of v3 = 4
    可以看到STATUS成员被成功赋值,但是联合体中code并没有体现出来,这是为什么?

    实际上,从Type2类型的空间长度中可以窥见,联合体中的匿名体的空间排布应该是
    在这里插入图片描述

    从匿名体的角度去看,它只需要填充1位,因为他的成员中空间长度最大也只是一个字节,所以只需要保证内存长度被1整除即可。但是从联合体的角度去看,他的成员code是2个字节的空间长度,所以他不得不填充2个字节。而之所以打印code时没有表现出STATUS的改变,是因为code的限定词是2个字节的短整型。下面放开限定,直接输出v3的数值。读者有兴趣也可以直接在联合体中添加一个32位的成员,再打印验证。

    void main(char argc,char* argv[])
    {
        v3.code = 0xa5da;
        v3.STATUS = 0x75;
         printf("v3.code = %x \n v3.ENABLE = %x \n v3.Chunnel = %x \n v3.STATUS = %x \n size of v3  = %d \n ",v3,v3.ENABLE,v3.Channel,v3.STATUS,sizeof(Type2));
    }
    

    输出:
    v3.code = 75a5da
    v3.ENABLE = 0
    v3.Chunnel = a5
    v3.STATUS = 75
    size of v3 = 4
    对于此性质,并没有太好的办法,大佬们可以分享自己的想法。

    展开全文
  • 只是一个说明,交易是不必要的:[arr.val] = newVals{:}; % achieves the same as deal(newVals{:})唯一的其他方式我知道如何做(没有foor循环)是使用arrayfun来迭代数组中的每个结构:% make a struct arrayarr = [ ...
  • C语言中的结构体

    2021-09-04 16:09:22
    向函数传递整个结构体变量中的数据2.向函数传递结构体的地址3.向函数传递结构体数组名4.结构体数组名和传递整个结构体变量对比 提示:以下是本篇文章正文内容,下面案例可供参考 一、从数组到结构体   c语言中,...
  • 正文大家好,我是bug菌!最近看到一些朋友在交流结构体对齐方面的一些问题,从他们的交谈中隐隐约感觉有几个朋友对结构体成员的对齐理解上有点偏差,不能说完全不对吧,毕竟这是老生常谈的问题了~所...
  • 指针是地址,而不是具体的标量值,这是指针的精髓,不管是一级指针、二级 指针、 整型指针、浮点数指针、结构体指针等等等等所有类型的指针,内容都是个地址,而指针本身当然也是有地址的,而且容易忽略的是,不管这...
  • 解析JSON数据包,并按照cJSON结构体的结构序列化整个数据包。可以看做是获取一个句柄。 2.cJSON *cJSON_GetObjectItem(cJSON *object,const char *string); 功能:获取json指定的对象成员 参数:*objec:第一个函数...
  • 最近工作中遇到一个问题,在做甲方的代理服务时,要传送一组数据,用结构体格式,在处理数据时,结构体数据正确,但是传出数据处理函数时,数据出现了错误。 首先,结构体的内容很多,类似如下: struct My_msg { ...
  • int main(){//结构体可以定义在函数内,也可以定义到函数外//相当于全局变量与局部变量// struct person// {// char *name;// int age;// };struct person p1;//补齐算法,分配的存储空间为结构体...
  • 那么,在 main 中你定义了 a、b、c 这三个 NODE,并且让 a 链到 b,b 链到 c,c 的 next 指向“空”虽然运行起来没有错,但额外说一下:c.next='\0' 这里你把 next 赋值为 '\0',这是“空字符”。而这里在逻辑上你...
  • 结构体

    2020-12-30 08:59:24
    结构体 引入 存储数据: 变量(int a;)、数组(int a[10]),但是现实存储的对象比较复杂,比如:“狗”、“学生”,该怎么 存储到计算机中?使用对象的属性表示一个对象(名词): 学生的属性: 学号 => char num...
  • 1.给repeated类型的变量赋值 1.1 逐一赋值 定义protobuf结构如下: message Person { required int32 age = 1; required string name = 2; } message Family { repeated Person person = 1; } 对person进行赋值...
  • 结构体创建结构体定义结构体 1. 数组 n[0]相当于n1, 输入5个数字,输出他们的倒序 #include <stdio.h> int main(){ int n[5]; for(int i=0;i<5;++i){ scanf("%d",&n[i]); } for(int i=4;i>=...
  • 结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访问。 C++提供了许多种基本的数据类型(如int、float、double、char等)供用户使用。由于程序需要处理...
  • 1:简单理解,结构体就是数组的进一步发展,数据的优点和缺陷在于数据里面是元素类型必须相同,但是结构体没有这个要求,结构体里面元素的类型可以相同也可以不同。2:结构体的定义:structstudent{intage;charname...
  • [流畅的 C] C语言将结构体转化为字符串本文并非标题的具体实现。而是提供一种编程方式,习惯,一种探讨。本文有一点点门槛,有 socket,开源协议栈学习/开发经验者阅读更佳。Overview[*流畅的 C*] C语言将结构体转化...
  • C语言结构体教程

    2021-05-22 12:38:24
    C语言结构体教程三、结构数组和结构指针结构是一种新的数据类型, 同样可以有结构数组和结构指针。1.结构数组结构数组就是具有相同结构类型的变量集合。假如要定义一个班级40个同学的姓名、性别、年龄和住址, 可以...
  • 结构体的sizeof这是初学者问得最多的一个问题,所以这里有必要多费点笔墨。让我们先看一个结构体:struct s1{char c;int i;};问sizeof(s1)等于多少聪明的你开始思考了,char占1个字节,int占4个字节,那么加起来就...
  • C语言结构体

    2021-02-01 16:00:58
    结构体结构体概述从数组到结构体的进步之处结构体中的元素如何访问结构体的对齐访问结构体为何要对齐访问结构体对齐的规则运算GCC的对齐指令offsetof宏与container_of宏(重要)灵活的使用结构体1灵活的使用结构体2 ...
  • SV结构体

    2021-09-13 20:02:49
    struct是有一组变量或常数组成的集合,可以作为一个整体进行操作,也可以操作其中一部分 将逻辑上相关的信号放在一起...使用结构体的名字操作整个变量 <structure_name>.<variable_name> Instruction_Wo.
  • C++结构体数组一个结构体变量中可以存放一组数据(如一个学生的学号、姓名、成绩等数据)。如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。结构体数组与以前介绍过的数值型数组的不同之处在于...
  • c语言结构体用法

    2021-05-20 04:28:04
    需要指出的是结构指针是指向结构的一个指针, 即结构中第一个成员的首地 址, 因此在使用之前应该对结构指针初始化, 即分配整个结构长度的字节空间, 这可用下面函数完成, 仍以上例来说明如下: student=(struct string...
  • 定义和使用结构体变量自己建立结构体类型用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体声明一个结构体类型的一般形式为:struct 结构体名{ 成员表列 };**************************struct Student...
  • 数组结构体总结

    2021-05-22 10:58:15
    数组定义:数组是有序的并且具有相同类型的数据的集合。一维数组1、一般形式:类型说明符数组名[常量表达式];例如:inta[10];元素为a[0]----a[9].2、常量表达式中不...5、使用数值型数组时,不可以一次引用整个数组...
  • 一、结构体(一)结构体变量的一般定义格式有两种1.边说明变边定义struct 结构体名{类型变量名;类型变量名;......}结构体变量结构体名是结构体的标识符,而不是变量名。如:struct Student{char name[8];char age;...
  • \ 下面看一下宏 GEN_TASK_STK 如何定义了task基地址: #define GEN_TASK_STK(name,_stkSize) \ CPU_STK TASK_##name##_STK[_stkSize] 最后我们把整个过程完整的记录下: task.h: typedef void (*TASK_FUNC_F)(void *...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 43,917
精华内容 17,566
关键字:

整个结构体赋值