结构体和其他构造数据类型
结构体
“结构”是一种构造类型,它是由若干“成员”组成的。
每一个成员可以是一个基本数据类型或者又是一个构造类型。
结构既然是一种“构造”而成的数据类型,那么说明和使用之前必须先定义它,也就是构造它。
结构体定义
-
结构类型一般在函数外定义,其一般定义格式:
struct 结构名 {类型名 成员名; //int num; 这就是结构成员类型名 成员名;…类型名 成员名; };
-
特别注意:在定义结构类型时大括号“
{}
”后的分号;
不可缺少,结构类型定义是一个说明性语句。 -
在C语言中,关键字
struct
和结构名合起来构成结构类型名。 -
结构是一种复杂的数据类型,是数目固定、类型不同的若干有序变量的集合。
-
结构成员的类型:可以是变量、数组、指针,甚至是其他结构体。
-
结构变量定义
-
其一般定义格式:
struct 结构名 变量名;
-
说明结构变量有以下3种方法
-
先定义结构,再说明结构变量。
struct student {int num;char name[20];char sex;float score; }; struct student stu1, stu2; //说明了两个结构变量stu1和stu2为student结构类型 //也可以用一个宏定义使用一个符号常量来表示一个结构类型 #define STU struct student STU {int num;char name[20];char sex;float score; }; STU stu1, stu2;
-
在定义结构类型的同时说明结构变量。例如:
struct student {int num;char name[20];char sex;float score; }stu1, stu2; //这种类型的说明的一般形式: struct 结构名 {成员表 }变量名表;
-
直接说明结构变量。例如:
struct {int num;char name[20];char sex;float score; }stu1, stu2; //这种类型的说明的一般形式: struct {成员 }变量名;
-
-
结构变量的赋值
-
结构变量的赋值就是给各成员赋值,可用输入语句或赋值语句来完成。结构变量可以整体赋值,但是不能整体引用,只能按成员变量分别引用。
struct student {char name[20];int age;char id[20]; }stu; int main() {struct student stu = {"张三", 30, "202105549"}; //整体赋值 }
结构变量的初始化
-
结构变量的初始化其实就是在定义结构变量时对结构变量进行赋值。
struct student {char name[20];int age;char id[20]; }stu = {"张三", 30, "202105549"} //结构变量的初始化
结构变量成员
-
在程序中使用结构变量时,往往不把它作为一个整体来使用。一般对结构变量的使用,包括赋值,输入,输出,运算等都是通过结构变量的成员来实现的。(注意与结构成员作区分)
-
表达结构变量成员的一般形式:
结构变量名 . 成员名
。例如:stu1.num //第一个人的学号 stu2.sex //第二个人的性别
-
也可以直接给结构变量成员直接赋值:
struct student {char name[20];int age;char id[20]; }stu; strcpy(stu.name, "张三"); //不能用赋值运算符“=”将一个字符串直接赋值,只能用strcpy函数来复制字符串 stu.age = 30; strcpy(stu.id, "202105549"); //不能用赋值运算符“=”将一个字符串直接赋值,只能用strcpy函数来复制字符串
注意区分:结构体、结构变量、结构变量成员
结构数组的定义和引用
数组的元素也可以是结构类型,因此可以构成结构性数组,结构数组的每一个元素都是具有相同结构类型的下标结构变量。
结构数组的定义
-
结构数组的定义方法和结构变量类似,只需说明它为数组类型即可。例如:
struct student {int num;char name[20];char sex;float score; }stu[5]; //定义了一个结构数组stu,共有5个元素,stu[0]~stu[4]。每个数组元素都具有 struct student 的结构形式。
结构数组的初始化
struct student
{int num;char name[20];char sex;float score;
}stu[4] = {{101, "张三", 'M', 55},{102, "李四", 'M', 68.5},{103, "王五", 'F', 93.5},{104, "赵六", 'F', 97}
};//当全部元素进行初始化赋值时,也可不给出数组长度。
指向结构变量的指针
一个指针变量当用来指向一个结构变量时,称为结构指针变量。结构指针变量中的值是所指向的结构变量的首地址。
通过结构指针可以访问该结构变量,与数组指针和函数指针类似。
结构指针变量的定义和引用
-
结构指针变量说明的一般形式:
struct 结构名* 结构指针变量名
-
结构指针必须先赋值后才能使用;赋值是把结构变量的首地址赋予给该指针变量,不能把结构名赋予给该指针变量。
-
struct student {int num;char name[20];char sex;float score; }stu1; struct student* pstu; pstu = &stu1; //这是正确的,将变量首地址赋给指针变量 //pstu = &student; //这是错误的,因为 student 是结构名称,不是变量,不会分配内存空间。
-
结构名和结构变量是两个不同的概念:
结构名表示一个结构形式,编译系统不对它分配内存空间;只有当某变量被说明为这种类型的结构时,才对该变量分配内存。
-
有了结构指针变量,可以更好的访问结构变量的各个成员,其访问的一般形式:
结构指针变量->成员名
或者(*结构指针变量).成员名
(注意括号不可少,因为成员符“.
”的优先级高于“*
”)
-
指向结构数组的指针
-
指针变量可以指向一个结构数组,这时结构指针变量的值是整个结构数组的首地址。
-
指针变量也可指向结构数组的一个元素,这时结构指针变量的值是该结构数组元素的首地址。
结构变量与函数参数
-
可以将一个结构变量的值传递给另一个函数,可以采用以下两种方法:(值传递)
-
结构变量的成员做参数,用法和普通参数做实参一样,属“值传递”方式
-
形参和实参都用结构变量,将实参结构变量直接传递给形参的结构变量,此时实参和形参类型应当完全相等
#include <stdio.h> struct A {int data[1000];int num; }stu = { {1,2,3,4}, 500}; void print1(int num) {printf("%d\n", num); } //结构变量成员作为参数 void print2(struct A stu) {printf("%d\n", stu.num); } //结构变量作为参数 int main() {print1(stu.num);print2(stu);return 0; }//编译结果:500 500
-
-
也可以用指针变量做函数参数进行传送(传地址)
-
由实参传向形参的只是地址,可以减少时间和空间上的开销
#include <stdio.h> struct A {int data[1000];int num; }stu1 = { {1,2,3,4}, 500}; void print3(struct A* ps) {printf("%d\n", ps->num); } //结构指针变量作为参数 int main() {print3(&stu1);return 0; }//编译结果:500
-
复杂结构体
结构体作为结构成员
#include <stdio.h>struct date
{int year;int month;int day;
};
struct student
{int num;char name[20];char sex;struct date birthday; //在student这个结构体里面包含了date这个结构体float score;
}stu1 = { 101,"张三", 'M', {2003, 5, 19}, 98 }; //结构体嵌套初始化int main()
{printf("%d %s %c %d %d %d %lf", stu1.num, stu1.name, stu1.sex, stu1.birthday.year, stu1.birthday.month, stu1.birthday.day, stu1.score); //这种成员变量的引用方式称为“逐级引用” ;链式访问return 0;
}//编译结果:101 张三 M 2003 5 19 98.000000
那student
结构体内部能不能包含student
结构体本身呢?(在结构中包含一个类型为该结构本身的成员是否可以呢?)
结构体的自我引用
结构体自我引用是指在结构体内部,包含指向自身类型结构体的指针,此方式的引用在数据结构类型定义应用中较多。
同时,结构体还可以相互引用,也就是在多个结构体中,都包含指向其他结构体的指针。
-
struct student {int num;char name[20];char sex;float score;struct student* link; }stu1;
link
指针是指向struct student
类型的指针,作为自身的一个指针类型成员变量
-
struct student {int num;char name[20];char sex;float score;struct student next; //错误 }stu2;
student
结构体内部不能包含student
结构体本身- 两种做法的区别:
- 采用结构体指针作为成员变量,由于指针变量的内存空间大小是确定的,编译程序能够事先根据变量类型为变量分配确定的内存空间。所以可行。
- 采用结构体成员作为成员变量,由于在编译器中无法确认结构变量的实际内存空间大小,无法正确分配变量内存空间,不符合“变量先定义后使用”的基本原则。所以不行。
结构体内存对齐
-
结构体的对齐规则
- 第1个成员在结构体变量偏移量为0的地址处存放
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值;VS编译器默认对齐数为8.
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的成员的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的成员的最大对齐数)的整数倍。
-
例:
-
#include <stdio.h> struct s1 {char c1; //1个字节 对齐数为1int i; //4个字节 对齐数为4char c2; //1个字节 对齐数为1 }; int main() {printf("%d\n", sizeof(struct s1));return 0; }//编译结果:12
-
#include <stdio.h> struct s1 {double d; //对齐数为8char c; //对齐数为1int i; //对齐数为4 };struct s2 {char c1; //对齐数为1struct s1 s2; //对齐数为8double d; //对齐数为8 };int main() {printf("%d\n", sizeof(struct s2));//嵌套了结构体的情况return 0; }//编译结果:32
-
图解(结合对齐规则来看)
-
总体来说:结构体的内存对齐是拿空间来换取时间
-
设计结构体的时候,既要满足对齐,又要节省空间:让占用空间小的成员尽量集中在一起
-
-
修改默认对齐数
#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; }//编译结果:12 6
结构在对齐方式不合适时,可以自己更改默认对齐数
共用体(联合体)
共用体是在计算机的内存区域中申请一个特殊的空间,该空间由若干个连续的内存单元构成,这片内存空间可以存储各种不同类型的数据,是各种类型数据的“公共存储区”,可以在这个特殊的空间存放各种不同类型的数据。
在C语言中,不同类型的数据存取方式不同,其所占内存单元的字节数也不同,因此,数据的存取与数据类型必须匹配,否则就会引起数据存取混乱,导致数据读写错误。
共用体类型的定义与变量定义
-
共用体类型一般在函数外定义,其定义的一般格式:
union 类型名 {类型名 成员;...类型名 成员; };
- 共用体类型是几种不同数据类型共用一块存储空间,其大小由值域最大的成员所占空间大小决定
-
共用体变量定义:
union data u1, u2;
共用体大小的计算
满足以下2个条件:
-
共用体的大小至少是最大成员的大小
-
当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍(对齐数在结构体内存对齐讲到)
union data {int i; //占用4个字节 对齐数为4char a; //占用1个字节 对齐数为1float x; //占用8个字节 对齐数为8 }; sizeof(union data) == 8; //最大成员的大小是8个字节,是最大对齐数8的整数倍
#include <stdio.h> union data {char c[5]; //5个字节 对齐数为1 int i; //4个字节 对齐数为4 }; //大小为最大对齐数的整数倍:8(编译器默认的对齐数) union bs {short c[7]; //10个字节 对齐数为2int i; //4个字节 对齐数为4}; //大小为最大对齐数的整数倍;16 int main() {printf("%d\t", sizeof(union data));printf("%d\t", sizeof(union bs)); return 0; }//编译结果:8 16
共用体变量的使用
共用体变量的使用需要通过成员变量来实现,其形式如下:共用体变量名.成员名
在共用体变量定义的同时只能用第一个成员的类型值进行初始化,共用体变量初始化的一般形式:
union 共用体类型名 共用体变量 = {第一个成员类型名}
例如:union data u1 = {8};
使用共用体,注意
-
在对共用体变量进行初始化时,只能给第一个成员变量赋值,但必须用花括号括起来
-
不能对共用体变量名直接赋值,不能通过引用变量名得到其成员的值。
-
不可以在定义共用体变量结构类型时对它初始化,在定义共用体类型的同时不能初始化
-
共用体变量的地址及其各成员的地址都是同一地址,因为各成员地址的分配都是从共用体变量空间的起点开始
#include <stdio.h> union data {int i; char a; float x; }; union data u; int main() {printf("%p\t", &u); //共用体变量的地址printf("%p\t", &(u.i)); //共用体变量成员的地址printf("%p\t", &(u.a)); //共用体变量成员的地址printf("%p\t", &(u.x)); //共用体变量成员的地址return 0; } //编译结果:0086A13C 0086A13C 0086A13C 0086A13C
-
不能使用共用体变量作为函数参数,也不能使用函数返回共用体变量,但可以使用它指向共用体变量的指针(与结构体类似)
-
共用体变量可以出现在结构体类型的定义中,也可以定义结构体数组
-
同一时间,只能使用共用体内变量成员的一个值
#include <stdio.h> int main() {union temp{char a;int b;}t;t.a = 66;t.b = 266;printf("t.a:%d, t.b:%d\n", t.a, t.b);return 0; }//编译结果:t.a:10, t.b:266
我们发现,将 66 赋值给
t.a
,但是输出结果却是 10,这是怎么回事呢?
位域
在实际编程过程中,有些信息在储存时,并不需要占用一个完整的字节,而只需占几个或一个二进制位;
为了节省存储空间,并使处理简便,C语言提供了一种数据结构,称为 “位域”或“位段”。
所谓“位域”,就是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位数。每个域都有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位域定义形式
-
位域定义和结构定义相似,但有两个不同:
- 位域的成员必须是
char、unsigned char、int、unsigned int 或者 signed int
。 - 位域的成员名后面有一个冒号和一个数字。
其形式如下:
struct 位域结构名 {位域列表 }; //其中,位域列表的定义形式如下: 类型说明符 位域名:位域长度
- 位域的成员必须是
-
例如:
#include <stdio.h> struct A {int a : 2; //a成员占2个bit位int b : 5; //b成员占5个bit位int c : 10; //c成员占10个bit位int d : 30; //d成员占30个bit位 };//A就是一个位域类型 int main() {printf("%d\n", sizeof(struct A));return 0; }//编译结果:8
说明A这个位域在内存中占了8个字节,那么8是怎么来的呢?
一个
int
类型有32个bit位的存储空间,能够存下a、b和c位域,而d位域需要另外开辟一个int
类型的空间,所以是8个字节。 -
位域的内存分配
-
位域的空间是按照需要以4个字节(
int
)或者1个字节(char
)的方式来开辟的 -
位域涉及很多不确定因素,位域是不跨平台的,注重可移植的程序应该避免使用位域
-
在VS编译器下的内存分配
struct s {char a : 3;char b : 4;char c : 5;char d : 4; }; int main() {struct s stu = { 0 };stu.a = 10;stu.b = 12;stu.c = 3;stu.d = 4;return 0; }
-
对于位域的定义和使用有几点说明:
-
一个位域必须存储在同一个字节中,不能跨2个字节
-
由于位域不能跨2个字节,因此位域的长度不能大于1个字节的长度,也就是说不能超过8个二进制位
-
位域可以无位域名,这时它只能用来做填充或调整位置。无名的位域是不能使用的
-
如果相邻位域字段类型相同(如上例中
char
类型),且其位宽之和(如上例中位域a的位宽为3bit)小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;如果相邻位域字段类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;而如果相邻的位域字段的类型不同,则各编译器的具体实现有差异。
- 一个字节为8个bit位
-
-
-
位域的跨平台问题:
int
位域被当成有符号数还是无符号数是不确定的- 位域中最大的数目不能确定。(16位机器最大16,32位机器最大32,如果位域长度位27时(或者其他>16),在16位机器会出现问题)
- 位域中的成员在内存中从左向右分配,还是从右向左分配标准尚无定义
- 当一个结构包含两个位域,第二个位域成员比较大,无法容纳于第一个位域剩余的位时,是舍弃剩余的位还是利用,是不确定的
-
位域变量的说明和结构变量说明的方式相同。可采用先定义后说明,同时定义说明或者直接说明3种方式
例如:
struct bs {int a:8;int b:2;int c:6; }data; //说明 data 为bs变量,共占2个字节。其中位域a占8位,位域b占2位,位域c占6位。
位域应用
位域的使用和结构成员的使用相同,其一般形式为:位域变量名.位域名
位域允许用各种格式输出
枚举类型
C语言中提供了一种称为“枚举”的类型。在“枚举”类型定义中列举出所以可能的取值,被说明为该“枚举”类型的变量取值不能超过定义的范围。
应该说明的是:枚举类型是一种基本数据类型,而不是一种构造类型。
枚举类型的定义和枚举变量的定义
-
枚举的定义:
-
枚举定义的一般形式:
enum 枚举名{ 枚举值表 }
-
在枚举值中应罗列出所有可用值。这些值被称为枚举元素,也叫枚举常量
-
例:
enum weekday{ sun, mon, tue, wed, thu, fri, sat};
-
-
枚举变量的说明:枚举变量可用不同的方式说明,即先定义后说明、同时定义说明或者直接说明
enum weekday{ sun, mon, tue, wed, thu, fri, sat}; enum weekday a, b, c; //第一种说明方式enum weekday{ sun, mon, tue, wed, thu, fri, sat}a, b, c; //第二种说明方式enum { sun, mon, tue, wed, thu, fri, sat}a, b, c; //第三种说明方式
枚举类型变量的赋值和使用
-
枚举类型在使用时有以下规定:
-
枚举值是常量,不是变量。不能在程序中用赋值语句再对它赋值,但是可以在定义时对其赋初值。
-
枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2 ……
-
在定义的时候也可以赋初值,但其后的值还是依次递增
enum Color {RED = 1; //mei'ju'yuaGREEN;BLUE; }; //打印枚举值: 1,2,3
-
-
枚举元素不是字符常量也不是字符串常量,使用时不要加单引号、双引号
-
只能把枚举值赋给枚举变量,不能把枚举元素的数值直接赋值给枚举变量;若一定要把数值赋给枚举变量,则必须用强制类型转换。
enum Color {RED = 1; //1GREEN; //2BLUE; //3 }clr; clr = GREEN; //正确 clr被赋值为2 clr = 2; //错误不能将枚举元素的数值直接赋给枚举变量 clr = (enum Color)2; //正确,使用强制类型转换
-
枚举的优点
- 增加代码的可读性和可维护性
- 和
#define
定义的标识符比较,枚举有类型检查,更加严谨 - 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
类型定义符typedef
C语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”
类型说明符
typedef
即可用来完成自定义类型
-
typedef 定义的一般形式:
typedef 原类型名 新类型名
- 其中原类型名中含有定义部分,新类型名一般用大写表示,以便区别
- 有时也可用宏定义来代替
typedef
的功能,但是宏定义是由预处理完成的,而typedef
则是在编译时完成的,后者更为方便。
-
例:
typedef char ElemType; //将char类型和ElemType等同
typedef struct Studnote {int no;char name[8];char class[4];struct Studnote* next; //使用指向结构变量的指针 } StudType; //这样,StudType等同于Studnote结构体类型,可以使用该类型定义变量 StuType s1, s2; //等同于 struct Studnote s1, s2