C语言的结构体
在C语言中,普通变量只能存单个数据(一个数字或者一个字符),但是结构体可以把一组相关的、不同类型的数据捆成一个整体。
即:结构体可以把一个数据元素的各个不同的数据项聚合为一个整体,是一种自定义数据类型。核心作用是把多个不同类型的变量打包在一起,用来描述一个事物的多个属性。
本文主要介绍结构体的定义、引用,最后并通过一个实例来测试实际场景中的应用
一、声明格式
struct 结构体名{
成员列表;
};
成员列表包括:成员类型 成员名
不同属性成员类型可以相同,但是成员名必须要不同
例如定义学生这个结构体:
structStudent{intid;charname[20];//不能指针,因为可能野指针intage;floatscore;};structStudentstu1;定义完结构体类型只是声明构造了一种新的类型(struct 结构体名),还没有创建实际变量,未存储实际的数据,不能在{}进行赋值、运算等
实际使用还需需要进行声明(创建具体的变量,可真正存数据的对象),如上面的struct Student stu1
二、引用
c语言结构体有两种成员运算符:点运算符(域运算符)和箭头运算符(分量运算符)
点运算符:用于结构体普通变量访问成员:格式:变量.成员名
箭头运算符:用于结构体指针访问成员,格式:变量->成员名《==》等价于(*指针).成员名
.的优先级高于*,(*p).成员的括号不能省略
1、域运算符
对结构体变量的运算必须通过成员域运算符“.”来访问结构体变量的成员,方式如下
结构体变量名.成员名
intmain(){// 2. 定义结构体变量structStudentstu1;// 3. 给成员赋值stu1.id=888;strcpy(stu1.name,"王五");//字符数组定义的时候,可以name[10]="wangwu";这里不可以使用。使用strcpy或scanf("%s",stu1.name)stu1.age=18;stu1.score=99.5;// 4. 访问成员并打印printf("学号:%d\n",stu1.id);printf("姓名:%s\n",stu1.name);printf("年龄:%d\n",stu1.age);printf("成绩:%.1fd\n",stu1.score);return0;}编译运行后的结果如下:
通过scanf实现字符串的输入:
#include<stdio.h>#include<windows.h>#include<math.h>structtest_person{/* data */charname[20];intage;intclass;charaddress[20];};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);{/* data */structtest_personperson1;scanf("%s %d %d %s",person1.name,&person1.age,&person1.class,person1.address);printf("姓名:%s\n",person1.name);printf("年龄:%d\n",person1.age);printf("年级:%d\n",person1.class);printf("地址:%s\n",person1.address);};return0;}运行结果:
2、分量运算符(箭头运算符)->
#include<stdio.h>#include<windows.h>#include<math.h>structtest_person{/* data */charname[20];intage;intclass;charaddress[20];};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);{/* data */structtest_personperson1;structtest_person*p=&person1;//struct test_person *p=malloc(sizeof(struct test_person));p->age=22;p->class=2026;scanf("%s %s",person1.name,person1.address);printf("姓名:%s\n",p->name);printf("年龄:%d\n",p->age);printf("年级:%d\n",p->class);printf("地址:%s\n",p->address);};return0;}运行结果:
3结构体嵌套的引用
structtest_birthday{intyear;intmouth;intday;};structtest_person{/* data */charname[20];intage;intclass;structtest_birthdaytime;charaddress[20];};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);{/* data */structtest_personperson1;person1.time.year=2008;person1.time.mouth=8;person1.time.day=8;person1.age=18;printf("年龄:%d\n",person1.age);printf("出生年:%d\n",person1.time.year);printf("出生月份:%d\n",person1.time.mouth);};return0;}运行结果:
核心判断规则:每一步看当前是什么:
如果是结构体变量(实体):后面跟.
如果是结构体指针:后面跟->
| 情况 | 写法 |
|---|---|
| 外层变量,内层结构体变量 | obj.in.a |
| 外层变量,内层结构体指针 | obj.pin->a |
| 外层指针,内层结构体变量 | p->in.a |
| 外层指针,内层结构体指针 | p->pin->a |
struct 结构体可以放在main函数,作用域只限于main函数,也可以放到函数外面,此时作用域是全局变量。
三、结构体的特点和内存分布
1、特点
允许存在多种类型:可以把int、char、float、数组甚至其他结构体打包在一起;
自定义性强:可以根据需求创建任意类型
整体管理:一个结构体变量能代表一个完整事物,不用维护一堆零散变量。
2、内存分布
计算结构体的大小,需要考虑到内存单元的分布,结构体在内存中按照单元存储的,每个单元多大,是跟结构体中最大的基本类型有关。
存储方式:
把第一个成员变量的存储地址看成0
把第二个成员变量的存储地址就是它所占的字节数的倍数
。。。。
整个结构体的大小是最大的基本类型的倍数。
(1)示例一
structtest_struct{chara;shortb;shortc;intd;};structtest_person{charname[20];intage;intclass;structtest_structtime;charaddress[20];};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);{printf("test_struct的长度是%d\n",sizeof(structtest_struct));printf("test_person的长度是%d\n",sizeof(structtest_person));};return0;}我们按照上面的存储规则进行计算存储大小:
test_struct的大小如下存储:
char a:从0开始存储,占用1字节
short b:short类型是2字节,需要从2的倍数开始存储,故1#需要填充,b占用2#,3#两个字节
short c:同b,正好可以从4#开始
int d:int占用4字节,需要从4的倍数开始存储,故6#7#需要填充,d占用8-11字节
综上一共12字节。
test_person含有嵌套结构体:
char name[20]:占用0-19#
int age:int需要从4的倍数开始,正好从20#-23
int class;同age,24-27#
struct test_struct time;(内部abcd继续按照上面的原则存储):a:占28#,b:填充29#,占用30-31,c:占用32-33;d:int类型需要从36-39(34、35填充)
char address[20];占用40-59
综上占用60字节
通过程序运行一下,可得到如下结果:
(2)示例二
#include<stdio.h>#include<windows.h>#include<math.h>structtest_struct{chara;intb;shortc;};structtest_person{chard;structtest_structf;intg;};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);{printf("test_struct的长度是%d\n",sizeof(structtest_struct));printf("test_person的长度是%d\n",sizeof(structtest_person));};return0;}运行结果:
分析过程:
test_struct是一个结构体,是4字节对齐,它的第一个成员变量应该放到最大成员变量所占字节数的倍数地址上。
这种数据对齐方式(不限结构体这样存储,其他也是),可以提升访问性能(访问对应的变量可以尽可能少的存取周期),但是造成了空间的浪费。
两个相同结构体,成员相同,但是排列顺序不同,其占空间也是不一样的
四、实际场景练习
1、需求说明
使用结构体描述硬件通信数据的485通信
485通信的核心固定格式是:
| 帧头(固定) | 从机地址 | 功能码 | 数据长度 | 数据段 | 校验和 | 帧尾(固定) |
|---|---|---|---|---|---|---|
| 1字节 | 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节 |
2、实现逻辑
先定义帧结构体、实现求校验和函数,主机声明485结构体,模拟主机发送数据,从机接收数据。
#include<stdio.h>#include<stdint.h>// 1. 定义485通信数据帧---结构体类型typedefstruct{uint8_thead;//帧头uint8_taddr;//设备地址uint8_tfunc;//功能码uint8_tdata_len;//数据长度uint8_tdata[10];//数据区:最多存10个字节uint8_tcheck_h;//校验和uint8_tcheck_l;//校验和uint8_ttail_h;//帧尾uint8_ttail_l;//帧尾}RS485_Frame;intmain(){// ===================== 模拟主机:打包485指令()=====================RS485_Frame tx_frame;// 定义发送帧变量// 填充指令(给从机发:地址1,读数据,数据段2字节)tx_frame.head=0x3A;// 帧头固定tx_frame.addr=0x11;// 目标从机地址=0x11tx_frame.func=0x05;// 功能码=0x05(读数据)tx_frame.data_len=0x01;// 数据段长度=0字节tx_frame.data[0]=0x10;// 数据1tx_frame.check_h=0x27;tx_frame.check_l=0x00;tx_frame.tail_h=0x0D;// 帧尾固定tx_frame.tail_l=0x0A;// 帧尾固定// 主机发送:直接把结构体转成字节流,通过485总线发出去printf("主机发送485帧(十六进制):");uint8_t*tx_buf=(uint8_t*)&tx_frame;// 结构体转字节指针for(inti=0;i<sizeof(RS485_Frame);i++){printf("0x%02X ",tx_buf[i]);}printf("\n");// ===================== 模拟从机:接收485指令(硬件从机用结构体解析)=====================RS485_Frame rx_frame;// 定义接收帧变量// 假设从机通过485接收到数据,直接存入结构体rx_frame=tx_frame;// 从机解析指令:直接访问结构体成员printf("\n从机解析结果:\n");printf("帧头:0x%02X\n",rx_frame.head);printf("从机地址:%d\n",rx_frame.addr);printf("功能码:0x%02X(读数据)\n",rx_frame.func);printf("数据长度:%d字节\n",rx_frame.data_len);printf("数据段:0x%02X 0x%02X\n",rx_frame.data[0],rx_frame.data[1]);printf("校验和:0x%02X(数据有效)\n",rx_frame.check_h);printf("帧尾:0x%02X\n",rx_frame.tail_h);return0;}运行结果如下:
通过实践可以看出:
485通信贴合硬件数据格式,发送时打包一个结构体,接收时直接解析成员,不用拼接字节也不用算偏移。
硬件通信最怕字节顺序错,结构体固定成员顺序,可杜绝bug;
扩展性好,后续增加数据或状态位,直接在结构体里添加成员就可以。
五、注意事项
1、不同编译器以及编译选项的属性,系统为它分配的存储空间会有所不同
在存储该结构时会按照不同的内存对齐规则进行相关处理。
32位的操作系统,是按照最大不超过4字节对齐的;
64位的操作系统,按最大的基本类型对齐
程序员可以自己指定对齐方式:(在程序的最上面增加#pragma pack(n),n就是最大对齐方式)
#include<stdio.h>#include<windows.h>#include<math.h>#pragmapack(2);structtest_struct{chara;intb;shortc;};#pragmapack()structtest_person{chard;structtest_structf;intg;};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);{printf("test_struct的长度是%d\n",sizeof(structtest_struct));printf("test_person的长度是%d\n",sizeof(structtest_person));};return0;}运行结果:
分析过程: