目录
1、结构体
字符型、整型、单精度浮点型等基本数据类型,都是由 C 编译系统事先定义好的,可以直接用来声明变量。结构体类型是一种由我们自己根据实际需要自己构造的数据类型,所以必须要 “ 先定义,后使用 ” 。也就是说,必须先构造一个结构体类型,然后才能使用这个结构体类型来定义变量或数组。
1.1 结构体类型
“ 结构体类型 ” 是一种构造数据类型,它由若干个 “ 成员 ” 组成,每一个成员可以是相同、部分相同,或者完全不同的数据类型。对每个特定的结构体都需要根据实际情况进行结构体类型的定义,也就是构造它,以明确该结构体的成员及其所属的数据类型。
定义结构体类型的语句格式为:
struct 结构体类型名
{
数据类型 1 成员名 1;
数据类型 2 成员名 2;
......
数据类型 n 成员名 n;
};
struct 是 C 语言中的关键字,表明是在进行一个结构体类型的定义。结构体类型名是一个合法的 C 语言标识符,对 “ 结构体类型名 ” 的命名要尽量做到 “ 见名知义 ”。比如,描述一个学生的信息可以用 “ student ”,描述一本图书的信息可以使用 “ bookcard ” 等。由定义格式可以看出,结构体数据类型由若干个数据成员组成,每个数据成员可以是任意一个数据类型,最后的分号表示结构体类型定义的结束。如,定义一个学生成绩的结构体数据类型如下:
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
};
在这个结构体中有 6 个数据成员,分别是num、name、Chinese、English、Math 和average,前2个是字符数组分别存放学生的学号和姓名信息;Chinese、English、Math、average 是单精度实型,分别存放语文、英语、数学以及平均成绩。
结构体可以嵌套定义,即一个结构体内部成员的数据类型可以是另一个已经定义过的结构体类型。例如:
struct data
{
int year;
int month;
int day;
};
struct student
{
char name[10];//姓名
char sex[5];//性别
struct data birthday;//出生年月日
int age;//年龄
float score;//成绩
};
定义了一个结构体类型 struct date,然后在定义第 2 个结构体类型时其成员 birthday 被声明为 struct date 结构体类型。这就是结构体的嵌套定义。
注意:
(1) 结构体的成员名可以与程序中其他定义为基本类型的变量名同名,同一个程序中不同结构体的成员名也可以相同,它们代表的是不同的对象,不会出现冲突。
(2)若结构体类型的定义在函数内部,则这个类型名的作用域仅为该函数;若是定义在所有函数的外部,则可在整个程序中使用。
1.2 定义结构体变量
结构体类型的定义只是由我们构造了一个结构体,但定义结构体类型时系统并不为其分配存储空间。结构体类型定义好后,就可以像 C 中提供的基本数据类型一样使用,即可用它来定义变量、数组等,称为结构体变量或结构体数组,系统会为该变量或数组分配相应的存储空间。
定义结构体类型变量的方法有以下:
(1)先定义一个结构体类型,后定义变量
struct student { char num[10];//学号 char name[10];//姓名 float Chinese;//语文成绩 float English;//英语成绩 float Math;//数学成绩 float average;//平均成绩 };
可以用定义好的结构体类型 struct student 来定义变量,该变量就可以用来存储一个学生的信息。如:
struct student stu;//定义一个结构体类型的变量
也可以用定义好的结构体类型 struct student 来定义数组。如:定义了一个包含 50 个元素的数组,每个数组元素都是一个 结构体类型 的数据,可以保存 50 个学生的信息。
struct student stuarr[50];//定义结构体类型的数组
注意:当一个程序中多个函数内部需要定义同一结构体类型的变量时,应采用 先定义一个结构体类型,后定义变量 的方法,而且应将 结构体类型 定义为全局类型。
(2)定义结构体类型的同时定义变量
struct 结构体标识符 { 数据类型 1 成员名 1; 数据类型 2 成员名 2; ...... 数据类型 n 成员名 n; }变量 1,变量 2,...变量 n;
变量 1,变量 2,...变量 n 为变量列表,遵循变量的定义规则,彼此之间通过逗号分割。
说明:在实际应用中,定义结构体类型的同时定义变量 适合于定义局部使用的结构体类型或结构体类型变量,如在一个文件内部或函数内部。
(3)直接定义结构体类型变量
这种定义方式是不指出具体的结构体类型名,而直接定义结构体成员和结构体类型的变量。如下:
struct { 数据类型 1 成员名 1; 数据类型 2 成员名 2; ...... 数据类型 n 成员名 n; }变量 1,变量 2,...变量 n;
这种定义的实质是先定义一个匿名结构体,之后再定义相应的变量。由于此结构体没有标识符,所以无法采用定义结构体变量的第 1 种方法来定义变量。
注意:在实际应用中,此方法适合于临时定义局部变量或结构体成员变量。
1.3 初始化结构体变量
定义结构体变量的同时就对其成员赋初值的操作,就是对结构体变量的初始化。结构体变量的初始化方式与数组的初始化类似,在定义结构体变量的同时,把赋给各个成员的初始值用 “ { } ” 括起来,称为初始值表,其中各个数据以逗号分割。具体形式如下:
struct 结构体标识符
{
数据类型 1 成员名 1;
数据类型 2 成员名 2;
......
数据类型 n 成员名 n;
}变量名 = { 初始化值 1 ,初始化值 2 ,......初始化值 n};
如:
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
}stuarr[50],stu = {"12345678","zhangsan",70.0,80.0,90.0,80.0};
在定义结构体类型 struct student 的同时定义了一个结构体数组和一个结构体变量,并对变量 stu 进行了初始化,变量 stu 的 6 个成员分别得到了一个对应的值,变量 stu 中就存放了一个学生的信息。
1.4 结构体变量的引用
结构体变量的引用分为结构体成员变量的引用和将结构体变量本身作为操作对象的引用两种。
1. 结构体变量成员的引用
结构体变量包括一个或多个结构体成员变量,引用其成员变量的语法格式如下:
结构体变量名.成员名
“ . ” 是专门的结构体成员运算符,用于连接结构体变量名和成员名,属于最高级运算符,结构成员的引用表达式在任何地方出现都是一个整体。如 stu.num、stu.name等。嵌套的结构体定义中成员的引用也一样。例如,有以下代码:
struct data
{
int year;
int month;
int day;
};
struct student
{
char name[10];//姓名
char sex[5];//性别
struct data birthday;//出生年月日
int age;//年龄
float score;//成绩
}stu;
结构体变量 stu 的成员 birthday 也是一个结构体类型的变量,这是嵌套的结构体定义。对该成员的引用,要用结构体成员运算符进行分级运算。也就是说,对成员变量 birthday 的引用是这样的:stu.birthday.year、stu.birthday.month、stu.birthday.day。
结构体成员变量和普通变量一样使用,比如,可以对结构体成员变量进行赋值操作,如:stu.age = 20。
2. 对结构体变量本身的引用
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
};
struct student stu = {"12345678","zhangsan",70.0,80.0,90.0,80.0},stu1;
c语言规定,同类型的结构体变量之间可以进行赋值运算,如:
stu = stu1;
此时,系统将按成员一 一对应赋值。也就是说,上述赋值语句执行完后,stu1 中的 6 个成员变量分别得到数值:"12345678"、"zhangsan"、70.0、80.0、90.0、80.0。
在C 语言中规定,不允许将一个结构体变量作为整体进行输入或输出操作。如以下语句是错误的:
scanf("%s %s %f %f %f %f",&stu1);
printf("%s %s %f %f %f %f",stu1);
2、结构体数组
数组是一组具有相同数据类型变量的有序集合,可以通过下标获得其中的任意一个元素。结构体类型数组与基本类型数组的定义与引用规则是相同的,区别在于结构体数组中的所有元素均为结构体变量。
2.1 定义结构体数组
结构体数组的定义和结构体变量的定义一样,有 3 种方法。
(1)先定义结构体类型,再定义结构体数组
struct 结构体标识符 { 数据类型 1 成员名 1; 数据类型 2 成员名 2; ...... 数据类型 n 成员名 n; }; struct 结构体标识符 数组名[数组长度];
(2)定义结构体类型的同时,定义结构体数组
struct 结构体标识符 { 数据类型 1 成员名 1; 数据类型 2 成员名 2; ...... 数据类型 n 成员名 n; }数组名[数组长度];
(3)不给出结构体类型名,直接定义结构体数组
struct { 数据类型 1 成员名 1; 数据类型 2 成员名 2; ...... 数据类型 n 成员名 n; }数组名[数组长度];
“ 数组名 ” 为数组名称,遵循变量的命名规则;“ 数组长度 ” 为数组的长度,要求为大于零的整型常量。
结构体数组定义好后,系统即为其分配相应的内存空间,数组中的各元素在内存中连续存放每个数组元素都是结构体类型,分配相应大小的存储空间。如:
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
};
struct student stuarr[50];
stuarr 在内存中的存放顺序如下图所示。
2.2 初始化结构体数组
结构体类型数组的初始化遵循基本数据类型数组的初始化规律,在定义数组的同时,对其中的每一个元素进行初始化。如:
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
};
struct student stuarr[2] = {{"12345678","zhangsan",70.0,80.0,90.0,80.0},{"87654321","lisi",880.0,88.0,88.0,88.0}};
在定义结构体类型的同时,定义长度为2的结构体数组 stuarr[2],并分别对每个元素进行初始化。在定义数组并同时进行初始化的情况下,可以省略数组的长度,系统会根据初始化数据的多少来确定数组的长度。
2.3 结构体数组元素引用
对于数组元素的引用,其实质为简单变量的引用。对结构体类型的数组元素的引用也是一样,其语法形式如下:
数组名[数组下标];
“ [ ] ”为下标运算符。对于结构体数组来说,每一个数组元素都是一个结构体类型的变量,对结构体数组元素的引用遵循对结构体变量的引用规则。如:
数组名[数组下标].成员名
3、结构体指针
指针变量非常灵活方便,可以指向任一类型的变量。如整型指针指向一个整型变量、字符指针指向一个字符型的变量。同样,也可以定义一个结构体类型的指针,让它指向结构体类型的变量,该结构体变量的指针就是该变量所占内存空间的首地址。
3.1 定义结构体指针
和其他指针一样,结构体变量的指针在使用前必须先定义,并且要初始化一个确定的地址值后才使用。定义结构体指针变量的一般形式如下:
struct 结构体名* 指针变量名;
如:struct student *p,stu;
其中,struct student 是一个已经定义过的结构体类型,这里定义的指针变量 p 是 struct student 结构体类型的指针变量,它可以指向一个 struct student 结构体类型的变量,例如:p=&stu。定义结构体类型的指针也是有 3 种,和定义结构体类型的变量和数组基本一致。
3.2 初始化结构体指针
结构体指针变量在使用前必须进行初始化,其初始化的方式与基本数据类型指针变量的初始化相同,在定义的同时赋予其一个结构体变量的首地址,使结构体指针指向一个确定的地址值。如:
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
}stu,*p=&stu;
定义了一个结构体类型的变量 stu 和一个结构体类型的指针变量 p ,定义的时候编译系统会为 stu 分配该结构体类型所占字节数大小的存储空间,通过 “ *p=&stu ” 使指针变量 p 指向结构体变量 stu 存储区域的首地址。指针变量 p 就有了确定的值,就可以通过 p 对该结构体变量进行操作。
注意:结构体类型的指针在初始化时只能指向一个结构体类型的变量。
3.3 使用指针访问成员
定义并初始化结构体类型的指针变量后,通过指针变量可以访问它所指向的结构体变量的任何一个成员。如:
struct student
{
char num[10];//学号
char name[10];//姓名
float Chinese;//语文成绩
float English;//英语成绩
float Math;//数学成绩
float average;//平均成绩
}stu,*p=&stu;
p 是指向结构体变量 stu 的结构体指针,使用指针 p 访问变量 stu 中的成员有以下方法。
(1)使用运算符 “ . ”,如:stu.num、stu.Math。
(2)使用 “ . ” 符,通过指针变量访问目标变量,如:(*p).num、(*p).Math(由于运算符 “ . ” 的优先级高于 “ * ”,因此必须使用圆括号把 *p 括起来,把 (*p) 作为一个整体)。
(3)使用 “ -> ” 运算符,通过指针变量访问目标变量,如:p->num、p->Math。
注意:结构体指针在程序中的使用很频繁,为了简化引用形式,C 提供有结构成员运算符 “ -> ”,利用 “ -> ” 可简化结构体指针引用结构体成员的形式。并且结构成员运算符 “ -> ” 和 “ . ”的优先相同。
3.4 指向结构体数组的指针
结构体指针变量的使用,和其他普通变量的指针使用方法和特性是一样的。结构体变量指针除了指向结构体变量,还可以用来指向一个结构体数组,此时,指向结构体数组的结构体指针变量加 1 的结果是指向结构体数组的下一个元素,那么结构体指针变量的地址值的增量大小就是 “ sizeof(结构体类型) ” 的字节数。 如:
struct student
{
int num;//学号
char name[10];//姓名
}stuarr[10],*parr=stu;
定义了一个结构体类型的指针 parr 指向结构体数组 stuarr 的首地址,即初始时是指向数组的第 1 个元素,那么 (*parr).num 等价于 stuarr[0].num,(*parr).name 等价于 stuarr[0].name。如果对 parr 进行加 1 运算,则指针变量 parr 指向数组的第 2 个元素,即 stuarr[1],那么 (*parr).num 等价于 stuarr[1].num,(*parr).name 等价于 stuarr[1].name。总之,指向结构体类型数组的结构体指针变量使用起来并不复杂,但要注意区分以下情况:
parr->num++ /* 等价于(parr->num)++,先取成员 num 的值,再使 num 自增 1 */
++parr->num /* 等价于++(parr->num),先对成员 num 进行自增 1,再取 num 的值*/
(parr++)->num /* 等价于先取成员 num 的值,用完后再使指针 parr 加 1 */
(++parr)->num /* 等价于先使指针 parr 加 1,然后再取成员 num 的值 */
4、结构体与函数
4.1 结构体作为函数的参数
结构体作为函数的参数,有两种形式。
(1) 在函数之间直接传递结构体类型的数据——传值调用方式
由于结构体变量之间可以进行赋值,所以可以把结构体变量作为函数的参数使用。具体应用中,把函数的形参定义为结构体变量,函数调用时,将主调函数的实参传递给被调函数的形参。
(2)在函数之间传递结构体指针——传址调用方式
运用指向结构体类型的指针变量作为函数的参数,将主调函数的结构体变量的指针(实参)传递给被调函数的结构体指针(形参),利用作为形参的结构体指针来操作主调函数中的结构体变量及其成员,达到数据传递的目的。
结构体作为函数的参数 两种方式的比较:
使用结构体指针作为函数参数比使用结构体变量作函数参数的效率高。原因:因为无需传递各个成员的值,只需传递一个地址,且函数中的结构体成员并不占据新的内存单元,而与主调函数中的成员共享存储单元。使用结构体指针作为函数参数的方式,可通过修改形参所指的成员的值来影响实参所对应的成员的值。
4.2 结构体作为函数的返回值
通常情况下,一个函数只能有一个返回值。但是如果函数确实需要带回多个返回值的话,根据前面的学习,可以利用全局变量或指针来解决。学习了结构体以后,就可以在被调函数中利用 return 语句将一个结构体类型的数据结果返回到主调函数中,从而得到多个返回值,这样更有利于对这个问题的解决。 如:
/* 输入三角形的三边,求三角形的周长和面积 */
#include<stdio.h>
#include<math.h>
struct cir_area
{
float Circumference;
float area;
};
/*自定义函数,功能是根据 3 条边求三角形的半个周长和面积*/
struct cir_area c_area(float a, float b, float c)
{
struct cir_area result;
result.Circumference = (a + b + c) / 2;
result.area = sqrt(result.Circumference * (result.Circumference - a) * (result.Circumference - b) * (result.Circumference - c));
return result;
}
int main()
{
float a = 0.0;
float b = 0.0;
float c = 0.0;
struct cir_area variable;
printf("请输入三角形的 3 条边:> ");
scanf("%f %f %f", &a, &b, &c);
variable = c_area(a, b, c);
printf("三角形的周长是:> %.2f\n三角形的面积是:> %.2f\n", variable.Circumference * 2, variable.Circumference);
return 0;
}
5、结构体内存对齐
结构体内存对齐是怎么对齐的呢?看一段代码:计算结构体的大小?
#include<stdio.h>
struct S1
{
char c1;
int a;
char c2;
};
struct S2 //成员变量调整一下位置
{
char c1;
char c2;
int a;
};
int main()
{
struct S1 s1 = { 0 };//定义一个结构体变量并且初始化为 0; 也就是把第一个变量进行初始化为 0 之后,后面的变量值编译器都默认为初始化为 0;
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));//结果是什么呢?
return 0;
}
为什么是 12 和 8 呢?那么就要涉及到结构体内存对齐了,通过结构体内存对齐来计算结构体的大小。如何计算呢?
★首先得掌握结构体的对齐规则:
(1)第一个成员在与结构体变量(内存)偏移量为0的地址处。
(2)其他成员变量要对齐到对齐数的整数倍的地址处。
对齐数 = 编译器默认的对齐数 与 该成员字节大小的较小值(两个数进行比较,最小的数即为对齐数)。(VS编译器中默认的对齐数为8 )(3)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。(4)如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处。结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
分析如下图:
(1)sizeof(s1)
(2)sizeof(s2)
再计算一道结构体嵌套的大小:
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 s4 = { 0 };//定义一个结构体变量并且初始化为 0; 也就是把第一个变量进行初始化为 0 之后,后面的变量值编译器都默认为初始化为 0;
struct S3 s3 = { 0 };
printf("%d\n", sizeof(s3));
printf("%d\n", sizeof(s4));//结果是什么呢?
return 0;
}
运行结果:
想要计算 s4 的大小,就得必须计算 s3 的大小,因为 S4 结构体嵌套结构体 S3 ,S3 属于 S4 的一部分。分析如下图:
(1)sizeof(s3)
(2)sizeof(s4)
那为什么会存在内存对齐呢?
1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总结:结构体的内存对齐是拿空间来换取时间的做法。
在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。如:
//S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。 struct S1 //12个字节 { char c1; int i; char c2; }; struct S2 //8个字节 { char c1; char c2; int i; };
修改默认对齐数
我们可以使用 #pragma 这个预处理指令,改变我们的默认对齐数。如:#include <stdio.h> #pragma pack(8)//设置默认对齐数为8 struct S1 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 #pragma pack(1)//设置默认对齐数为1 struct S2 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
总结:结构在对齐方式不合适的时候,那么可以自己更改默认对齐数。