一、什么是结构体
首先我们为什么要用到结构体?
我们都已经学了很多int char …等类型还学到了同类型元素构成的数组,以及取上述类型的指针,在一些小应用可以灵活使用,然而,在我们实际应用中,每一种变量进行一次声明,再结合起来显然是不太实际的。
类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。
官方来说结构体就是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。说到集合,数组也是集合,但是不同的是数组只能是相同类型元素的集合。
二、结构体的使用
2.1 结构体的声明
struct tag { member1; member2; } variable-list;
- struct是结构体关键字
- tag是结构体的标签名,是自定义的,如book,student等。
- struct tag就是结构体类型
- member1 , member2 是结构体成员,是标准的变量定义,比如 int i; 或者 float f;,也可以定义数组char s[20]。
- variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量,也可以省略。
2.2 结构体的基础结构和类型
2.2.1 普通结构体
(一)先定义结构体类型,再定义结构体变量
struct student //结构体类型 或 结构体名 { int num; char name[20]; //结构体成员 char sex; int age; float score; char addr[30]; }; struct student stu1,stu2; //结构体变量
(二)定义结构体类型的同时定义结构体变量
struct data // 结构体类型 或结构体名 { int day int month; //结构体成员 int year }time1,time2; //结构体变量
2.2.2 嵌套结构体
结构体和函数一样可以嵌套使用,也就是说在一个结构体中可以使用另外一个结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
struct student { int age;//年龄 char sex[8];//性别 int weight;//体重 char tele[20];//电话 }; struct people { int num;//序号 struct student s;//学生 }; struct list { int num;//序号 struct list* next;//指向自己的结构体指针 };
但是结构体中不能包含一个同类型的结构体变量,因为这样结构体大小无法确定
struct node { int num; struct node s; //错误定义 };
2.2.3 匿名结构体
匿名结构体是不定义结构体名称,而直接定义其成员的一种方式。这种结构体只能使用一次。并且两个匿名结构体的成员如果都相同的话,这两个匿名结构体也是不同的。
struct//匿名结构体 { int num; char name[20]; //..... }; struct { int a; char b; float c; }x; struct { int a; char b; float c; }*p; p = &x;//两种结构体不同无法赋值
2.3 结构体的初始化
(一)定义时初始化
#include <stdio.h> #include <stdlib.h> struct books // 结构体类型 { char title[50]; char author[50]; //结构体成员 char subject[100]; int book_id; }book={"C 语言","xingaosheng","编程语言",12345}; //结构体变量的初始化 int main() { printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id); return 0; }
(二)先定义再进行初始化
typedef struct student { int age;//年龄 char sex[8];//性别 int weight;//体重 char tele[20];//电话 }stu; struct people { int num;//序号 struct student s;//学生 }; int main() { struct student s = { 20,"nan",50,"1233455" };//创建变量并初始化 //struct student s; //s= { 20,"nan",50,"1233455" };错误 stu t = { 18,"nan",45,"123444" }; struct people p = { 1,{20,"nan",50,"1233455"} }; //嵌套结构体的初始化 return 0; }
2.4 结构体的成员访问
2.4.1 直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:
struct Point { int x; int y; }; int main() { struct Point p = { 1,2 }; printf("x: %d y: %d\n", p.x, p.y); return 0; }
输出结果:
x:1 y:2
2.4.2 间接访问
除了通过(.)操作符直接访问,我们也可以通过结构体地址,利用(->)操作符间接访问。
#include <stdio.h> struct Point { int x; int y; }; int main() { struct Point p = { 3, 4 }; struct Point* ptr = &p;//结构体指针 ptr->x = 1; ptr->y = 2; printf("x = %d y = %d\n", ptr->x, ptr->y); return 0; }
输出结果:
x:1 y:2
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
三、结构体数组
结构体数组:是指数组中的每一个元素都式结构体, 结构体数组常被用来表示一个拥有相同数据结构的群体。
struct stu { char name[20]; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 }class[5]; //表示一个班有5个人
结构体数组在定义的时候也可以初始化
struct stu { char name[20]; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 }class[5] = { {"Li ping", 5, 18, 'C', 145.0}, {"Zhang ping", 4, 19, 'A', 130.5}, {"He fang", 1, 18, 'A', 148.5}, {"Cheng ling", 2, 17, 'F', 139.0}, {"Wang ming", 3, 17, 'B', 144.5} };
使用方法:
#include <stdio.h> #include <stdlib.h> struct stu { char name[20]; int num; int age; char group; float score; } ban[5] = { {"xing",5,18,'c',145.0},{"ao",4,19,'a',130.5}, {"sheng",1,18,'a',148.5},{"pei",2,17,'f',139.0}, {"yuan",3,17,'b',144.5} }; // 表示一个班有5个人 int main() { int i, n = 0; float sum = 0; for (i = 0; i < 5; i++) { sum += ban[i].score; if (ban[i].score < 140) n++; } printf("sum=%.2f\naverage=%.2f\nn=%d\n", sum, sum / 5, n); return 0; }
输出结果:
sum=707.50
average=141.50
n=2
🟥四、结构体指针(重点)
4.1 指向结构体变量的指针
4.1.1 结构体指针的定义
可以定义指向结构体的指针,方式与定义指向奇特类型变量的指针类似
定义方式:struct 结构体名*结构体指针名
struct books*struct_pointer
其中books是结构体名,struct_pointer为结构体指针名
定义之后可以在上述定义的指针变量中存储结构变量的地址
struct_pointer = &Book1;
为了使用指向该结构的指针访问结构的成员,必须使用->运算符
struct_pointer->title;
代码示例如下:
struct stu // 结构体类型 或 结构体名 { char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1 = { "Tom", 12, 18, 'A', 136.5 }; //结构体指针 struct stu *pstu = &stu1;
也可以定义结构体的同时定义结构体指针:
struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;
注意:
- ▶ 结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&符号,所以给p赋值只能写成。
- ▶ 结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,例如上面的stu,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量(例如stu1)才包含实实在在的数据,才需要内存来存储。不可能去取一个结构体名的地址,也不能将它赋值给其他变量。
4.1.2 结构体指针的成员访问
通过结构体指针可以获取结构体成员,一般形式为:
(*pointer).memberMane //pointer为结构体指针名 pointer->memberName // 或者
- 第一种写法中, . 的优先级高于 * ,(*pointer)两边的括号不能少。如果去掉括号写成*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就不对了。
- 第二种写法中,-> 是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员,
- 这也是 -> 在C语言中的唯一用途。
4.1.3 结构体指针的使用
前面两种写法是等效的,我们通常采用第二种写法,这样更加直观。
#include <stdio.h> #include <stdlib.h> #include <string.h> struct stu { char name[20]; int num; int age; char group; float score; }stu1 = { "Tom",12,18,'A',136.5 }, * pstu = &stu1; int main() { // 读取结构体成员的值 printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", (*pstu).name, (*pstu).num, (*pstu).age, (*pstu).group, (*pstu).score); printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", pstu->name, pstu->num, pstu->age, pstu->group, pstu->score); return 0; }
运行结果如下:
4.2 指向结构体数组的指针
在我们想要用指针访问结构体数组的第n个数据时可以用:
struct Student { char cName[20]; int number; char csex; }student1; //结构体变量 struct Student stu1[5]; //结构体数组 struct Student*p; // 结构体指针 p=stu[n]; (++p).number//是指向了结构体数组下一个元素的地址
结构体指针与结构体数组的联合使用:
#include <stdio.h> #include <string.h> #include <stdlib.h> struct stu //结构体类型 或结构体名 { char name[20]; int num; int age; //结构体成员 char group; float score; }stus[]{ //结构体数组 {"Zhou ping", 5, 18, 'C', 145.0}, {"Zhang ping", 4, 19, 'A', 130.5}, {"Liu fang", 1, 18, 'A', 148.5}, {"Cheng ling", 2, 17, 'F', 139.0}, {"Wang ming", 3, 17, 'B', 144.5} }, * ps; //结构体指针 int main() { //求数组长度 : sieof(结构体变量)/sizeof(结构体类型名) int len = sizeof(stus) / sizeof(struct stu); printf("Name\t\tNum\tAge\tGroup\tScore\t\n"); for (ps = stus; ps < stus + len; ps++) { printf("%s\t%d\t%d\t%c\t%.1f\n", ps->name, ps->num, ps->age, ps->group, ps->score); } return 0; }
输出结果如下:
4.3 结构体成员是指针类型
代码示例如下:
struct Student { char* Name;//这样防止名字长短不一造成空间的浪费 int number; char csex; }student1;
注意:在使用时可以很好地防止内存被浪费,但是注意在引用时一定要给指针变量分配地址,如果你不分配地址,结果可能是对的,但是Name会被分配到任意的一的地址,结构体不为字符串分配任何内存存储空间具有不确定性,这样就存在潜在的危险
代码改进如下:
struct Student { char* Name; int number; char csex; }stu,*stu; stu.name=(char*)malloc(sizeof(char));//内存初始化
如果我们定义了结构体指针变量,他没有指向一个结构体,那么这个结构体指针也是要分配内存初始化的,他所对应的指针类型结构体成员也要相应初始化分配内存
struct Student { char* Name; int number; char csex; }stu,*stu; stu = (struct student*)malloc(sizeof(struct student));./*结构体指针初始化*/ stu->name = (char*)malloc(sizeof(char));/*结构体指针的成员指针同样需要初始化*/
五、结构体的内存对齐
在熟悉了结构体的基本应用之后,下面我们要深入讨论的就是结构体大小,如何计算结构体的大小,就需要知道它在内存中是如何储存的。
而结构体在内存中存在结构体对齐的现象。
我们先参考以下代码:
struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1)); struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2)); struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3)); struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4));
输出结果是:
12
8
16
32
如果直接计算结构体成员的所占的内存之和显然比这小,这是为什么呢?
C语言分配结构体内存时,遵循的是内存对齐规则,那什么是内存对齐规则呢?
内存对齐规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
- 对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。(VS 中默认的值为 8 ,Linux中gcc没有默认对齐数,对⻬数就是成员⾃⾝的⼤⼩)
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
为什么会存在内存对齐呢?相信大部分人都会有这个疑问,其实主要有两个原因:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
上面的代代码图示如下:
六、结构体位段
6.1 什么是位段
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,所以C 语言有一种特别的数据结构名为位段,允许我们按位对成员进行定义,指定其占用的位数,单位为比特位(bit)。一般是用来节约内存,与结构体有两个不同:
位段的实现和结构体类似,只不过位段的成员的类型只能是
unsigned int 或者int类型,char类型的也可以。
每个成员名后面要加上:和数字
代码示例如下:
struct stu { int a : 4; int b : 2; };
后面的数字表示bite位。位段不存在对齐。
位段不具有跨平台性:
- 位段中没有规定在内存使用的过程中,是从左使用还是从右使用。
- 不能满足下一个成员使用的空间是舍弃还是保留的问题没有规定。
- int位段中无符号还是有符号的问题没有规定
6.2 结构体实现位段的内存分配
那么位段的分配到底是怎么样的呢?
当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃剩余的位还是利用呢?
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4;
- 假设位段在一个字节内部是从高地址到低地址分配。
- 假设当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃。
七、结构体传参
我们知道函数传参分为两种,一种是直接传参:直接传变量;一种是间接传参:通过传变量地址间接访问。
struct S1 { int p; int num; }; //结构体传参 void print1(struct S1 s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S1* ps) { printf("%d\n", ps->num); } int main() { struct S1 s = { 1,2 }; print1(s); //传整个结构体 print2(&s); //传地址 return 0; }
但在结构体传参的时候,最好选择传址调用,有两个好处:
1.可以减少对空间的浪费
2.可以对里面的数据进行修改
🟥八、typedef关键字与结构体、结构体指针(重点)
8.1 使用typedef定义结构体
typedef用来定义新的数据类型,通常typedef与结构体的定义配合使用。使用typedef的目的使结构体的表达更加简练(所以说typedef语句并不是必须使用的。)
- struct 是用来定义新的数据类型——结构体
- typedef是给数据类型取别名。
定义一个名字为TreeNode的结构体类型(现在并没有定义结构体变量,并不占用内存空间):
struct TreeNode // 结构体类型 { int Element; struct TreeNode* LeftChild; //结构体成员 struct TreeNode* RightChild; };
为结构体起一个别名Node,这时Node就等价于struct TreeNode
typedef struct TreeNode Node;
将结构体的定义和typedef语句可以连在一起写:
typedef struct TreeNode //结构体类型 { int Element; //结构体成员 struct TreeNode* LeftChild; struct TreeNode* RightChild; }Node; // Node 是 struct TreeNode 的别名
注意 :不要与“定义结构体类型的同时定义结构体类型变量”混淆:
使用typedef关键字定义结构体类型 定义结构体类型的同时定义结构体类型变量
typedef struct student { int age; int height; }std; std std1, std2; //std相当于struct student struct student { int age; int height; }std1,std2; struct student std3, std4; //定义了student数据类型的结构体和std1、std2、std3、std4结构体变量
8.2 使用typedef定义结构体指针
使用typedef关键字用一个单词Node代替struct TreeNode,并定义指向该结构体类型的指针PtrToTreeNode:
struct TreeNode { int Element; struct TreeNode* LeftChild; struct TreeNode* RightChild; }; typedef struct TreeNode Node; //用Node代替struct TreeNode Node *PtrToTreeNode; //定义指针
也可以简化如下所示:
typedef struct TreeNode { int Element; struct TreeNode* LeftChild; struct TreeNode* RightChild; }Node; //定义结构体并用Node代替struct TreeNode Node *PtrToTreeNode; //定义指针