C语言 - 构造类型(结构体、共用体、枚举)

目录

结构体_struct

结构体的概念

结构体的定义

结构体变量的初始化

结构体成员的访问(结构体变量的使用)

结构体的使用案例

结构体的内存分配

偏移量与offsetof函数

结构体的内存对齐规则

为什么要有内存对齐?

柔性数组(变长数组)

结构体注意事项

修改默认对齐数

位段(位域)

共用体(联合体)_union

认识共用体

共用体的定义

共用体的内存分配

共用体大小的计算

枚举_enum

认识枚举

枚举的定义

用法示例

枚举注意事项

最后


引子:构造类型不是基本类型的数据结构也不是指针,是由若干个相同或不同类型的数据构成的集合

结构体_struct

结构体的概念

概念:结构体是一种构造数据类型,是由一种或多种基本数据类型或构造类型的数据的集合

结构体的定义

1、先定义结构体类型,再定义结构体变量

struct 结构体类型名{

        成员列表;

};

struct 结构体类型名; //定义结构体变量

2、定义结构体类型的同时定义结构体变量

用这种方法定义结构体,此后仍然可以定义结构体变量

struct 结构体类型名{

        成员列表;

}结构体变量1,结构体变量2; //定义结构体同时定义结构体变量1,结构体变量2

struct 结构体类型名 变量3,变量4; //定义结构体变量3,4

3、无名结构体类型的定义

在定义结构体类型的时候,由于无名结构体没有结构体类型名,因此只能在定义结构体的同时定义结构体变量,此后就无法再定义结构体变量了

struct{

        成员列表;

}变量 1,变量 2; //以后不能再定义相关结构体类型的数据

4、结构体类型重命名定义

将结构体重命名,为其增添一个新的结构体类型名。下方的代码相当于将结构体重命名为A,此时A和struct stu是等价的

typedef struct stu{

        成员列表;

}重新定义的结构体类型名 A; //用这种方法就不能再定义结构体同时定义结构体变量了

结构体变量的初始化

结构体变量的初始化方式:

1、定义结构体变量时初始化

例如:

struct student
{
    int age;
    char name[100];
    char sex[20];
};
struct student John = {18,"John","man"};

2、可以通过 .成员变量的形式改变初始化顺序

例如:

struct student
{
    int age;
    char name[100];
    char sex[20];
};
struct student John = { .age = 18, .sex = "man", .name = "John" };

3、定义结构体类型的同时定义结构体变量并初始化

例如:

struct student
{
    int age;
    char name[100];
    char sex[20];
}John = {18,"John","man"};

结构体成员的访问(结构体变量的使用)

使用格式:

        结构体变量结构体成员(非指针变量)

        结构体变量->结构体成员(指针变量)

注意,(*x).val的写法完全等效于x->val。->运算符可以看作是对*的封装(类比数组的'[ ]'运算符),并不是只有指针类型才能用->,这只是一种写法的优化。

例如:

John.name=18; 
strcpy(Lucy.name, "John");

结构体的使用案例

结构体的嵌套

相同类型的结构体变量可以相互赋值

结构体指针同样满足此规则

结构体数组

概念:结构体数组是个数组,是由若干个相同类型的结构体变量构成的集合,也就是结构体和数组的结合使用

定义方法:struct 结构体类型名 数组名[元素个数]; 例如:

struct stu{
int num;
char name[20];
char sex;
};
struct stu edu[3];
//定义了一个struct stu 类型的结构体数组edu,
//这个数组有3个元素分别是edu[0] 、edu[1]、edu[2]

结构体数组成员的访问:数组名[下标] .成员

代码案例:

结构体指针

概念:结构体指针即结构体的地址,结构体指针变量用来存放这个地址

定义方法:struct 结构体类型名 *结构体指针变量名; 例如:

struct stu* s;

结构体指针成员的引用:

        方式1: (*结构体指针变量名).成员

        方式2: 结构体指针变量名->成员

例如:

typedef struct{
    int a;
    double b;
    char c[30];
}try;  
int main()
{
    try *x,*y;
     //首先在堆区为结构体开辟空间
        x=(try*)malloc(sizeof(try));
         y=(try*)malloc(sizeof(try));
方式1   (*y).a=18;//*y就相当于p指向的变量 try
方式2   x->a=100;
方式2   x->b=3.71828;
        strcpy(x->c,"hellow!");

    return 0;
}

结构体的内存分配

偏移量与offsetof函数

偏移量:偏移量就是在个位置相对于首位置的之间发生偏移的量,例如30相对12的偏移量是18。其中,结构体的第一个成员永远都放在0偏移处

offsetof函数:

头文件:stddef.h

宏的原型:offsetof(type, member-designator)

宏的功能:offsetof会生成一个类型为 size_t 的整型常量,它是结构成员相对于结构开头的字节偏移量。

参数说明:

         type - 结构体

        member-designator - 成员

用法示例:

//下面的实例演示了 offsetof() 宏的用法。

#include <stddef.h>
#include <stdio.h>

struct address {
   char name[50];
   char street[50];
   int phone;
};
  
int main()
{
   printf("address 结构中的 name 偏移 = %d 字节。\n",
   offsetof(struct address, name));
   
   printf("address 结构中的 street 偏移 = %d 字节。\n",
   offsetof(struct address, street));
      
   printf("address 结构中的 phone 偏移 = %d 字节。\n",
   offsetof(struct address, phone));
   return(0);
} 

运行结果:

address 结构中的 name 偏移 = 0 字节。

address 结构中的 street 偏移 = 50 字节。

address 结构中的 phone 偏移 = 100 字节。

结构体的内存对齐规则

*规则1:给结构体变量分配内存的时候,以所占字节数最大的基本类型大小为单位开辟内存。

但double比较特殊:

vs环境,double类型以8字节为单位开辟内存

gcc环境,double类型以4字节为单位开辟内存


*规则2:每个成员都要对齐到其对齐数的整数倍处。其中,这个对齐数的大小为:该成员大小与默认对齐数的较小值。当没有默认对齐数时,对齐数就是成员自身的大小。

其中,vs环境下默认对齐数是8,gcc环境下没有默认对齐数。

要注意,double类型比较特殊:

vs环境,double是 8 字节对齐,即起始内存单元的编号是8的倍数

gcc环境,double是 4字节对齐,即起始内存单元的编号是4的倍数

但是无论是哪种环境,double型变量都占8字节大小,只是字节对齐方式不同


规则3:成员中出现了数组时,数组可以看成多个变量的集合;如果嵌套了结构体,可以把其展开看待。


规则4:开辟内存的时候,从上向下依次按成员在结构体中的位置顺序开辟空间。整个结构体的大小,必须是最大对齐数的整数倍,最大对齐数包含中嵌套的结构体成员中的对齐数。


示例图解

代码分析

为什么要有内存对齐?

  1. 性能提升:当数据按照对齐要求存储时,读取和写入操作的效率更高。因为处理器通常会以内存块(比如字、双字、四字等)为单位进行数据传输,而不是以单个字节为单位。如果数据没有对齐,处理器可能需要进行额外的操作来处理不对齐的数据,这会增加访问内存的开销,降低性能。

  2. 缓存优化:现代计算机通常都有多级缓存,而缓存的数据块也是按照一定的规则进行对齐的。当数据按照对齐要求存储时,缓存的命中率更高,从而提高了缓存读取的效率。

  3. 平台兼容性:某些硬件平台要求数据以特定的方式进行对齐,如果不满足对齐要求,可能会导致运行时错误或未定义行为。因此,在跨平台开发时,确保内存对齐可以增加代码的可移植性和可靠性。

柔性数组(变长数组)

在结构体的最后可以定义一个数组,这个数组很灵活,可以在不同情况下开辟不同大小的内存空间。详情可以参考这篇博客:C语言 - 柔性数组(变长数组)_真-小白菜的博客-CSDN博客

结构体注意事项

1、C语言规定,结构体内不能进行赋值操作

2、结构体可以定义在全局,也可以定义在局部,但一般都是定义在全局

3、定义结构体变量时不要忘记写struct

4、位段可以很好的节省空间,但是有跨平台的问题存在,需要谨慎使用。

5、结构体传参最好使用指针。这是因为函数在传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。因此结构体传参的时候,可以用传结构体的地址(即结构体指针)的方式很巧妙的避免这个问题

修改默认对齐数

使用#pragma pack可以改变默认对齐数

使用格式:

        #pragma pack (value) 指定对齐值为value

        #pragma pack() 取消设置的默认对齐数,还原为默认值

注意事项:

1.value最好设置为1、2、4、8等(2^n)

2.实际对齐数为成员自身大小和默认对齐数的较小值,即如果将默认对齐数设置的过大(大于成员自身大小),可能不会产生任何效果,例如:


补充内容:Pragma 

Pragma 指令指定计算机特定或操作系统特定的编译器功能。 以 #pragma 开头行指定 pragma 指令。 使用 Microsoft 特定 __pragma 关键字可以在宏定义内编写 pragma 指令。 C99 中引入并由 C++11 采用的标准 _Pragma 预处理器运算符与之类似。

语法:

#pragmatoken-string

__pragma(token-string) // 两个前导下划线 - Microsoft 特定扩展

_Pragma(string-literal) // C99

详细内容可以查看微软的参考网站:

Pragma 指令与 __pragma 和 _Pragma 关键字 | Microsoft Learn

位段(位域)

        在结构体中,以位为单位的成员被称之为位段(位域)。C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。

        如果想要详细了解位段的话可以看这篇博客:C语言位段_小白麋鹿的博客-CSDN博客

共用体(联合体)_union

认识共用体

共用体也叫联合体,和结构体类似,也是一种构造类型的数据结构。在进行某些算法的时候,需要使几种不同类型的变量存到同一段内存单元中,几个变量所使用空间相互重叠。在C语言中,这种几个不同的变量共同占用一段内存的结构被称作“共用体”。

共用体的定义

定义共用体的方法和结构体类似,直接把用union替换stuct就可以了。例如:

union Un {

        /*这里与结构体类似*/

};

共用体的内存分配

共用体所有成员占有同一段地址空间,即共用体变量的地址和它的各成员的地址都是同一地址。同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用。共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖。例如:

#include <stdio.h>

union Test_Union {
    char ch;
    int num;
};


int main()
{
    union Test_Union Un;
    Un.num = 0x111223344;
    printf("%#x\n", Un.ch);  /* VS2022输出结果:0x44 */
    return 0;

}

共用体大小的计算

共用体的大小也满足对齐原则。所以共用体的大小至少是最大成员的大小,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。例如:

#include<stdio.h>
union Un1
{
    char c[5];
    int i;
};
union Un2
{
    short c[7];
    int i;
};
int main()
{

    printf("%d\n", sizeof(union Un1));  //输出结果:8
    printf("%d\n", sizeof(union Un2));
  //输出结果:16
}

枚举_enum

认识枚举

举可以将变量的值在枚举值表中逐一列举出来,在枚举值表中应列出所有的可用值称为枚举元素,枚举变量只能取枚举值列表中的枚举元素

枚举的定义

枚举的定义与结构体、共用体等基本类似,具体方式如下:

enum 枚举类型名{

        枚举值列表;

};

用法示例

#incLude <stdio.h>

enum week{
    mon, tue, wed, thu, fri, sat , sun
};//定义一个枚举类型week

int main()
{
    //枚举类型变量的定义
    enum week day1;
    //枚举类型变量的初始化
    enum week day2=tue;
    return 0;
}

枚举注意事项

        1、枚举值是常量,不能在程序中用赋值语句再对它赋值。例如   sun=5;mon=2; sun=mon;   等都是错误的

        2、在枚举值表中应列出所有的可用值称为枚举元素,枚举变量只能取枚举值列表中的枚举元素

        3、并不是只有枚举变量才能使用枚举元素,而是枚举变量只能使用枚举元素。枚举元素和宏类似,文件的任何地方都可以直接使用枚举元素

        4、枚举元素默认是从0开始顺序定义为0,1,2等数字。例如在week中,默认mon值为0,tue值为1……sun值为6

        5、在C语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以枚举元素只能为整数

        6、枚举可以改变枚举值的默认值,如

enum week //枚举类型
{
    mon=3, tue, wed, thu, fri=4, sat,sun
};
//枚举元素的值:mon=3, tue=4, wed=5, thu=6, fri=4, sat=5,sun=6

解释:在定义枚举类型时可以用等号给枚举元素赋值,用来代表元素从几开始编号在程序中。但是此后就不能再次对枚举元素赋值了,因为枚举元素是常量

最后

        结构体,共用体,枚举的的使用大同小异,三者之间有着很大的共性,同时它们还保持各自的特性。如果对这篇博客有任何疑惑或建议,欢迎对小白私信反馈。

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值