C语言中的结构体(struct)
Posted __Sunshine_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言中的结构体(struct)相关的知识,希望对你有一定的参考价值。
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修改结构变量会影响原结构变量。
以上是关于C语言中的结构体(struct)的主要内容,如果未能解决你的问题,请参考以下文章
零基础学C语言知识总结八:struct 结构体与 union 共用体