C语言中的结构体(struct)

2023-11-17

C语言中,结构体类型属于一种构造类型(其他的构造类型还有:数组类型,联合类型)。本文主要介绍关于结构体以下几部分。
这里写图片描述

1、概念

为什么要有结构体?

因为在实际问题中,一组数据往往有很多种不同的数据类型。例如,登记学生的信息,可能需要用到 char型的姓名,int型或 char型的学号,int型的年龄,char型的性别,float型的成绩。又例如,对于记录一本书,需要 char型的书名,char型的作者名,float型的价格。在这些情况下,使用简单的基本数据类型甚至是数组都是很困难的。而结构体(类似Pascal中的“记录”),则可以有效的解决这个问题。
结构体本质上还是一种数据类型,但它可以包括若干个“成员”,每个成员的类型可以相同也可以不同,也可以是基本数据类型或者又是一个构造类型。
结构体的优点:结构体不仅可以记录不同类型的数据,而且使得数据结构是“高内聚,低耦合”的,更利于程序的阅读理解和移植,而且结构体的存储方式可以提高CPU对内存的访问速度。

结构声明(structure declaration)

结构声明(也见有称做定义一个结构体)是描述结构如何组合的主要方法。
一般形式是:
struct 结构名{
成员列表
};
struct关键词表示接下来是一个结构。
如声明一个学生的结构:

struct Student{         //声明结构体
    char name[20];      //姓名
    int num;            //学号
    float score;        //成绩
};

上面的声明描述了一个包含三个不同类型的成员的结构,但它还没创建一个实际的数据对象,类似C++中的模板。每个成员变量都用自己的声明来描述,以分号结束。花括号之后的分号表示结构声明结束。结构声明可以放在函数外(此时为全局结构体,类似全局变量,在它之后声明的所有函数都可以使用),也可以放在函数内(此时为局部结构体,类似局部变量,只能放在该函数内使用,如果与全局结构体同名,则会暂时屏蔽全局结构体)。

要定义结构变量,则一般形式是:
struct 结构体名 结构体变量名;
如:

struct Student stu1;    //定义结构体变量

这里写图片描述
1)、结构体变量的定义可以放在结构体的声明之后:

struct Student{         //声明结构体
    char name[20];      //姓名
    int num;            //学号
    float score;        //成绩
};
struct Student stu1;    //定义结构体变量

2)、结构体变量的定义也可以与结构体的声明同时,这样就简化了代码:

struct Student{        
    char name[20];       
    int num;             
    float score;         
}stu1;                  //在定义之后跟变量名

3)、还可以使用匿名结构体来定义结构体变量:

struct {                //没有结构名
    char name[20];       
    int num;            
    float score;         
}stu1;  

但要注意的是这样的方式虽然简单,但不能再次定义新的结构体变量了。

访问结构成员

虽然结构类似一个数组,只是数组元素的数据类型是相同的,而结构中元素的数据类型是可以不同的。但结构不能像数组那样使用下标去访问其中的各个元素,而应该用结构成员运算符点(.)。即访问成员的一般形式是:
结构变量名 . 成员名
如 stu1 . name 表示学生stu1的姓名。

但如果结构体中的成员又是一个结构体,如:

struct Birthday{                //声明结构体 Birthday
    int year;
    int month;
    int day;
};
struct Student{                 //声明结构体 Student
    char name[20];              
    int num;                    
    float score;                 
    struct Birthday birthday;   //生日
}stu1;

则用 stu1.birthday.year 访问出生的年份。

结构体变量的初始化

1)、结构体变量的初始化可以放在定义之后:

可以对结构体的成员逐个赋值:

struct Student stu1, stu2;      //定义结构体变量
strcpy(stu1.name, "Jack");
stu1.num = 18;
stu1.score = 90.5;

注意:不能直接给数组名赋值,因为数组名是一个常量。如:

stu1.name = "Jack"; //…main.c:26:15: Array type 'char [20]' is not assignable

或者可以对结构体进行整体赋值:

stu2 = (struct Student){"Tom", 15, 88.0};

注意:此时要进行强制类型转换,因为数组赋值也是使用{},不转换的话系统无法区分!如:

int arr[5] = {1, 2, 3, 4, 5};       //数组的初始化
stu2 = {"Tom", 15, 88.0};           //…main.c:31:12: Expected expression

2)、结构体变量的初始化也可以与定义同时:

struct Student{                 //声明结构体 Student
    char name[20];               
    int num;                    
    float score;                 
}stu = {"Mike", 15, 91};        //注意初始化值的类型和顺序要与结构体声明时成员的类型和顺序一致

此时不需要强制类型转换
也可以部分初始化:

struct Student stu4 = {.name = "Lisa"};

也可以按照任意的顺序使用指定初始化项目:

    struct Student st = { .name = "Smith",
                          .score = 90.5,
                          .num = 18 };

3)、可以用一个已经存在的结构体去初始化一个新的相同类型的结构体变量,是整体的拷贝(每一个成员都一一赋值给新的结构体变量),而不是地址赋值。如:

stu3 = stu1;
printf("stu1 addr: %p\nstu3 addr: %p\n", &stu1, &stu3);
printf("stu1.num: %d\nstu3.num: %d\n", stu1.num, stu3.num);
printf("stu1.num addr: %p\nstu3.num addr: %p\n", &stu1.num, &stu3.num);
//输出结果:
stu1 addr: 0x10000104c
stu3 addr: 0x100001084
stu1.num: 18
stu3.num: 18
stu1.num addr: 0x100001060
stu3.num addr: 0x100001098

2、结构体变量的存储原理

1)结构体数据成员对齐的意义

内存是以字节为单位编号的,某些硬件平台对特定类型的数据的内存要求从特定的地址开始,如果数据的存放不符合其平台的要求,就会影响到访问效率。所以在内存中各类型的数据按照一定的规则在内存中存放,就是对齐问题。而结构体所占用的内存空间就是每个成员对齐后存放时所占用的字节数之和。
计算机系统对基本数据类型的数据在内存中存放的限制是:这些数据的起始地址的值要求是某个数K的倍数,这就是内存对齐,而这个数 K 就是该数据类型的对齐模数(alignment modulus)。这样做的目的是为了简化处理器与内存之间传输系统的设计,并且能提升读取数据的速度。
结构体对齐不仅包括其各成员的内存对齐(即相对结构体的起始位置),还包括结构体的总长度。

2)结构体大小的计算方法和步骤
i. 将结构体内所有数据成员的长度值相加,记为 sum_a ;
ii. 将各数据成员为了内存对齐,按各自对齐模数而填充的字节数累加到sum_a上,记为sum_b。
对齐模数是 #pragma pack 指定的数值与该数据成员自身长度相比较得到的数值较小者。该数据相对起始位置应该是对齐模数的整数倍。
iii. 将和 sum_b 向结构体模数对齐。
该模数则是 #pragma pack 指定的数值与结构体内最大的基本数据类型成员长度相比较得到的数值较小者。结构体的长度应该是该模数的整数倍。

数据类型自身对齐:
这里写图片描述
所谓“对齐在N上”,是指“存放的起始位置是%N = 0”.

3)在没有#pragma pack宏的情况下:
例子1:

这里写图片描述

内存分配状态为:

这里写图片描述

对于结构体的第一个成员 a,起始位置为0x…38 (也为 4 的倍数),所占内存为 0x…38 ~ 0x…3b,共占4个字节;
对于结构体的第二个成员 b,自身长度为1,对齐模数也为1,所以内存分配可以紧接着a的结尾位置 0x…3b,所以起始位置为 0x…3c,共占1个字节;
对于结构体的第三个成员 c,自身长度为2,对齐模数也为2,所以起始位置距离a 的起始位置应该是2的倍数,所以 0x…3d处只距离5,不符合要求,所以空着,继续往下找,而在 0x…3e处满足要求,所以可以作为c的起始位置,共占2个字节;
此时3个成员及其中间空着的位置所占内存单元总和为8,而结构体内最大的基本数据成员是 a,其长度为4,所以结构体模数为 4,而8是4的倍数,满足要求,故不再加内存。

例子2:
与例子1相比,三个类型的声明顺序变了:

这里写图片描述

内存分配状态为:
这里写图片描述

要注意的是,对 a而言,对齐模数为 4,所以当 b的起始位置在0x7f…830之后,0x7f…831、0x7f…832、0x7f…833的位置距离起始位置0x7f…830分别是1,2,3,都不是 4 的倍数,所以那三个位置都空着,直到0x7f…834才满足要求,所以作为 a 的起始位置。当最后 一个成员 c 占的内存末尾在0x7f…839时,所有数据成员及其之间的空位所占内存单元总和为10,而结构体模数为4,10不是4的倍数,所以要扩大到12才满足要求,此时又多了2个空位置,就是0x7f…83a和0x7f…83b。

例子3:
当结构体中有数组时:

这里写图片描述

内存分配状态为:
这里写图片描述

亦即相同类型数据的数组之间多分配的空间会被相邻数组的元素所占用。

4)在存在#pragma pack宏的情况下:
方法类似,只是模数可能会按上面说的规则而有所变化。

这里写图片描述

内存分配状态为:
这里写图片描述

注意,当没有#pragma pack(2)时,成员a要确定自身的q起始位置,是以自身的长度4为对齐模数,但有了#pragma pack(2),则将括号里的2与a的长度4比较,2为较小者,所以以2为a的对齐模数,即地址从0x7f…839往下找到0x7f…83a时,已经距离结构体的起始位置0x7f…838为2,是2的倍数,满足要求(虽然不是4的倍数),可以作为a的起始位置。而最后,所有数据成员及其之间的空位所占内存单元总和为8,因为2和4(结构体中最大的数据成员长度)的较小者为2,而8是2的倍数,所以刚好满足要求,不用在分配空位置,所以结构体总长度即为8。

3、结构体数组

结构类型作为一种数据类型,也可以像基本数据类型那样,作为数组的元素的类型。元素属于结构类型的数组成为结构型数组。如开头提出的问题,生活中经常用到结构数组来表示具有相同数据结构的一个群体,如一个班的学生的信息,一个书店或图书馆的书籍信息等。

1)结构数组定义
一般格式:
struct 结构名 {
成员列表
} 数组名[数组长度];
如:

struct Student{                 //声明结构体 Student
    char name[20];
    int num;
    float score;
}stu[5];                        //定义一个结构结构数组stu,共有5个元素

2)结构数组的初始化

定义结构数组的同时进行初始化

struct Student stu[2] = {{"Mike", 27, 91},{"Tom", 15, 88.0}};  

先定义,后初始化
整体赋值:

stu[2] = (struct Student){"Jack", 12, 85.0};

或者将结构体变量的成员逐个赋值:

strcpy(stu[3].name, "Smith");
stu[3].num = 18;
stu[3].score = 90.5;

输出结构体:

//结构体数组的长度:
int length = sizeof(stu) / sizeof(struct Student);
//逐个输出结构数组的元素
for (int i = 0; i < length; i++) {
    printf("姓名:%s  学号:%d  成绩:%f \n", stu[i].name, stu[i].num, stu[i].score);
}

//输出结果:
这里写图片描述

在这个例子中,要注意的是:
这里写图片描述

4、结构与指针

当一个指针变量用来指向了一个结构变量,这个指针就成了结构指针变量。
结构指针变量中的值是所指向的结构变量的首地址。可以通过指针来访问结构变量。

1)定义结构指针变量的一般形式:
struct 结构名 * 结构指针变量名
如:

struct Student *pstu;       //定义了一个指针变量,它只能指向Student结构体类型的结构体变量

结构指针变量的定义也可以与结构体的定义同时。而且它必须先赋值后使用。
数组名表示的是数组的首地址,可以直接赋值给数组指针。但结构变量名只是表示整个结构体变量,不表示结构体变量的首地址,所以不能直接赋值给结构指针变量,而应该使用 & 运算符把结构变量的的地址赋值给结构指针变量。即:

这里写图片描述

注意:结构名、结构变量名、结构体指针的区别。

2)通过结构指针间接访问成员值

访问的一般形式:
(*结构指针变量). 成员名 或 结构指针变量 -> 成员名
如:

(*pstu).name    
pstu->name

注意(pstu)的小括号不能省略,因为成员符“.”优先级为1,取地址符“”优先级为2,去掉括号就相当于*(pstu.name)了。

5、结构体的嵌套

1)结构体中的成员可以又是一个结构体,构成结构体的嵌套:

struct Birthday{                //声明结构体 Birthday
    int year;
    int month;
    int day;
};
struct Student{                 //声明结构体 Student
    char name[20];              
    int num;                    
    float score;                 
    struct Birthday birthday;   //生日
}; 

这里写图片描述
2)结构体不可以嵌套跟自己类型相同的结构体,但可以嵌套定义自己的指针。如:

struct Student{                 //声明结构体 Student
    char name[20];
    int num;
    float score;
    struct Student *friend;     //嵌套定义自己的指针
}

3)甚至可以多层嵌套:

struct Time{                    //声明结构体 Time
    int hh;                     //时
    int mm;                     //分
    int ss;                     //秒
};
struct Birthday{                //声明结构体 Birthday
    int year;
    int month;
    int day;
    struct Time dateTime        //嵌套结构
};
struct Student{                 //声明结构体 Student
    char name[20];
    int num;
    float score;
    struct Birthday birthday;   //嵌套结构
}
//定义并初始化
struct Student stud = {"Jack", 32, 85, {1990, 12, 3, {12, 43, 23}}};
//访问嵌套结构的成员并输出
printf("%s 的出生时刻:%d时 \n", stud.name, stud.birthday.dateTime.hh);
//输出结果:Jack 的出生时刻:12时 

注意如何初始化和对嵌套结构的成员进行访问。

6、结构与函数

结构体的成员可以作为函数的参数,属于值传递(成员是数组的除外)。如:

struct Student{                 //声明结构体 Student
    char name[20];
    int num;
    float score;
};
void printNum(int num){         //定义一个函数,输出学号
    printf("num = %d \n", num);
}
    struct Student student0 = {"Mike", 27, 91};
    printNum(student0.num);     //调用printNum 函数,以结构成员作函数的参数
//运行结果:num = 27 

注意,函数printNum并不知道也不关心实际参数是不是结构成员,它只要求实参是int类型的就可以了。

结构变量名也可以作为函数的参数传递,如:

void PrintStu(struct Student student){      //定义 PrintStu 函数,以结构变量作函数的形参
    student.num = 100;                      //修改学号
    printf("PrintStu 修改后:姓名: %s, 学号: %d, 内存地址: %p \n", student.name, student.num, &student);
}
    struct Student student0 = {"Mike", 27, 91};
    PrintStu(student0);                     //调用 PrintStu 函数,以结构变量名作函数的参数
    printf("           原来:姓名: %s, 学号: %d,  内存地址: %p \n", student0.name, student0.num, &student0);

//输出结果:
这里写图片描述

形参和实参的地址不一样,是在函数中创建了一个局部结构体,然后实参对形参进行全部成员的逐个传送,在函数中对局部结构体变量进行修改并不影响原结构体变量。这样传送的时间空间开销都比较大,特别是当成员有数组的时候,程序效率较低。所以可以考虑使用指针:

void PrintStu2(struct Student *student){      //定义 PrintStu2 函数,以结构指针作函数的形参
    student->num = 100;                       //修改学号
    printf("PrintStu2 修改后:姓名: %s, 学号: %d, 内存地址: %p \n", student->name, student->num, student);
}
    struct Student student0 = {"Mike", 27, 91};
    PrintStu2(&student0);                     //调用 PrintStu 函数,以结构变量的地址作函数的参数
    printf("           原来:姓名: %s, 学号: %d,  内存地址: %p \n", student0.name, student0.num, &student0);

//输出结果:
这里写图片描述

形参和实参的地址是一样的,所以是地址传递,在 PrintStu2 函数中,student 与&student0 指向同一块内存单元,用指针student修改结构变量会影响原结构变量。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言中的结构体(struct) 的相关文章

随机推荐

  • 魔方机器人之硬件篇

    待续 点击打开链接 思睿硬件设计博客
  • 面向组织分析的内容

    声明 本文是学习GB T 42859 2023 航天产品质量问题三个面向分析方法实施要求 而整理的学习笔记 分享出来希望更多人受益 如果存在侵权请及时联系我们 1 范围 本文件规定了航天产品质量问题三个面向分析方法实施的一般要求 程序和分析
  • 进制转换(C++)

    文章目录 一 任意2 36进制数转换为10进制数 1 1 c 代码实现 二 十进制转换为其他进制 2 1 方法一 2 2 c 代码实现 2 3 方法二 2 4 Demo 一 任意2 36进制数转换为10进制数 以二进制转换为十进制为例 基本
  • Vue2 vue-cropper裁剪图片-使用方法及注意事项

    记录vue croppe的使用及过程中遇到的问题 参考文章 Vue2中使用vue croper插件实现图片上传裁剪 超详细 效果图 安装 npm install vue cropper 或 yarn add vue cropper 封装vu
  • Linux之gdb的使用

    当我们能够在windows下 使用vs 2019等编译器去进行调试的时候 我们可以将在Linux下使用gdb调试这两者之间进行对比 调试这个操作 在方法上有区别吗 Linux和windows 其实 在调试思路上是一样的 在调试的操作方式上一
  • 超详细的移动Web知识树状图(flex、移动适配、响应式)

    前言 学习任何新知识 最重要的永远都是搭建属于自己的知识框架 随后学习的细碎知识点往框架里面填入 最后形成一棵属于自己的知识大树 本系列的博客专注更新总结好的思维导图 希望可以帮助大家快速理清知识结构 注意 本系列文章是拿来建立知识体系 没
  • 基于51单片机的热水器设计

    概述 本实例是基于51单片机的智能热水器控制系统 主要硬件由51单片机最小系统 LCD1602显示屏屏电路 水位传感器电路 ADC转换电路 DS18B20数字温度传感器 蜂鸣器报警电路 按键电路 加水继电器电路 加热继电器电路电路构成 功能
  • IPv6 ‘dadfailed‘异常问题:重复地址检测【已解决】

    ipv6地址显示异常 inet6 x x x x 1 64 scope global tentative dadfailed 无法使用命令ping6 这个地址 原因是当另一台主机关闭 但ipv6地址未被删除 并且新主机无法使用此地址时 为防
  • Kali开启ssh服务

    Kali开启ssh 尝试Powershell连接ssh服务 输入密码 发现被拒绝 ssh root kali的ip 一般出现这个问题大概率可能是ssh服务没有开启 开启一下即可 注 密码错误也会报该错误提示 这里是正确密码的情况下 第一步
  • 将网站打包成桌面程序并生成安装包(跨平台)

    一 Nativefier将网站打包成桌面程序 介绍 Nativefier 是一个命令行工具 仅仅通过一行代码就可以轻松地为任何的网站创建桌面应用程序 应用程序通过 Electron打包成系统可执行文件 app exe等 对应的可执行文件分别
  • spring mvc oracle 配制,Springmvc+mybatis配置动态切换数据源 并实现mybatis同时支持mysql和oracle数据库...

    注意 配置切换数据源和实现mybatis支持多种数据库为独立的配置 二者不相干 一 实现mybatis支持多种数据库 这里配置扫描不需要区分 扫描全部即可 classpath com mapper xml oracle mysql myba
  • 邮箱发送html php源码,php发送邮件函数,支持html和普通文本

    eol headers Reply To namefrom eol headers Return Path namefrom eol these two to set reply address headers Message ID eol
  • 分享一百多套开发视频教程的下载地址

    原文地址 http www cnblogs com dennisit p 3184225 html 北京圣思Java培训教学视频 资源共享网 2 Lucene WebService SVN Ant SpringMVC视频 学习资料库网 3
  • CMake入门教程:configure_file构建配置编程

    CMake入门教程 configure file构建配置编程 在进行软件开发过程中 配置文件的生成和管理是一个重要且常见的任务 为了简化这一过程 CMake提供了configure file函数 它可以帮助我们在构建过程中动态生成配置文件
  • Python----Python调用C语言方法

    原文链接 1 为什么要使用Python调用C语言 可以将一些耗时的操作的函数使用C语言编写 然后主题代码是python 这样使用python去调用C语言 就可以做既能有python语言的简单性 有可以利用C语言的高性能 从而达到整体既简单又
  • 萌梦聊天室16.9.23.77更新

    App名称 萌梦聊天室 App格式 apk 安卓适用 测试机型Android 4 4 红米2 exe Windows 10 App大小 18 2MB Android 27 7MB Windows 10 更新履历 1 修正Android等移动
  • pcre c语言,pcre函数详细解析

    PCRE是一个NFA正则引擎 不然不能提供完全与Perl一致的正则语法功能 但它同时也实现了DFA 只是满足数学意义上的正则 1 pcre compile 原型 include pcre pcre compile const char pa
  • Windows 2012 DHCP超级作用域的一个坑

    今天在将Cisco交换机DHCP服务改为Windows2012服务器DHCP服务的过程中 发现一个奇怪的现象 环境 Cisco2960S交换机上划分有多个VLAN interface Vlan3 ipaddress 10 86 24 1 2
  • (2021-8-16) QT5 信号与槽

    1 概念 信号 Signal 就是在特定情况下被发射的事件 如点击按钮会发送 click 事件 槽 Slot 就是对信号响应的函数 槽就是一个函数 与一般的 C 函数是一样的 可以 定义在类的任何部分 public private 或 pr
  • C语言中的结构体(struct)

    C语言中 结构体类型属于一种构造类型 其他的构造类型还有 数组类型 联合类型 本文主要介绍关于结构体以下几部分 1 概念 为什么要有结构体 因为在实际问题中 一组数据往往有很多种不同的数据类型 例如 登记学生的信息 可能需要用到 char型