结构体定义“结构”是一种构造类型,它是由若干“成员”组成的。
每一个成员可以是一个基本数据类型或者又是一个构造类型。
结构既然是一种“构造”而成的数据类型,那么说明和使用之前必须先定义它,也就是构造它。
-
结构类型一般在函数外定义,其一般定义格式:
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
struct A { int data[1000]; int num; }stu = { {1,2,3,4}, 500}; void print1(int num) { printf("%dn", num); } //结构变量成员作为参数 void print2(struct A stu) { printf("%dn", stu.num); } //结构变量作为参数 int main() { print1(stu.num); print2(stu); return 0; }//编译结果:500 500
也可以用指针变量做函数参数进行传送(传地址)
-
由实参传向形参的只是地址,可以减少时间和空间上的开销
#include
struct A { int data[1000]; int num; }stu1 = { {1,2,3,4}, 500}; void print3(struct A* ps) { printf("%dn", ps->num); } //结构指针变量作为参数 int main() { print3(&stu1); return 0; }//编译结果:500
#includestruct 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
struct s1 { char c1; //1个字节 对齐数为1 int i; //4个字节 对齐数为4 char c2; //1个字节 对齐数为1 }; int main() { printf("%dn", sizeof(struct s1)); return 0; }//编译结果:12
-
-
#include
struct s1 { double d; //对齐数为8 char c; //对齐数为1 int i; //对齐数为4 }; struct s2 { char c1; //对齐数为1 struct s1 s2; //对齐数为8 double d; //对齐数为8 }; int main() { printf("%dn", sizeof(struct s2));//嵌套了结构体的情况 return 0; }//编译结果:32 -
图解(结合对齐规则来看)
-
总体来说:结构体的内存对齐是拿空间来换取时间
-
设计结构体的时候,既要满足对齐,又要节省空间:让占用空间小的成员尽量集中在一起
-
修改默认对齐数
#pragma这个预处理指令可以改变默认对齐数
#include
#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("%dn", sizeof(struct s1)); printf("%dn", sizeof(struct s2)); return 0; }//编译结果:12 6 结构在对齐方式不合适时,可以自己更改默认对齐数
共用体类型的定义与变量定义共用体是在计算机的内存区域中申请一个特殊的空间,该空间由若干个连续的内存单元构成,这片内存空间可以存储各种不同类型的数据,是各种类型数据的“公共存储区”,可以在这个特殊的空间存放各种不同类型的数据。
在C语言中,不同类型的数据存取方式不同,其所占内存单元的字节数也不同,因此,数据的存取与数据类型必须匹配,否则就会引起数据存取混乱,导致数据读写错误。
-
共用体类型一般在函数外定义,其定义的一般格式:
union 类型名 { 类型名 成员; ... 类型名 成员; };- 共用体类型是几种不同数据类型共用一块存储空间,其大小由值域最大的成员所占空间大小决定
-
共用体变量定义:
union data u1, u2;
满足以下2个条件:
-
共用体的大小至少是最大成员的大小
-
当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍(对齐数在结构体内存对齐讲到)
union data { int i; //占用4个字节 对齐数为4 char a; //占用1个字节 对齐数为1 float x; //占用8个字节 对齐数为8 }; sizeof(union data) == 8; //最大成员的大小是8个字节,是最大对齐数8的整数倍#include
union data { char c[5]; //5个字节 对齐数为1 int i; //4个字节 对齐数为4 }; //大小为最大对齐数的整数倍:8 union bs { short c[7]; //10个字节 对齐数为2 int i; //4个字节 对齐数为4 }; //大小为最大对齐数的整数倍;12 int main() { printf("%dt", sizeof(union data)); printf("%dt", sizeof(union bs)); return 0; }//编译结果:8 16
共用体变量的使用需要通过成员变量来实现,其形式如下:共用体变量名.成员名
在共用体变量定义的同时只能用第一个成员的类型值进行初始化,共用体变量初始化的一般形式:
union 共用体类型名 共用体变量 = {第一个成员类型名}
例如:union data u1 = {8};
使用共用体,注意-
在对共用体变量进行初始化时,只能给第一个成员变量赋值,但必须用花括号括起来
-
不能对共用体变量名直接赋值,不能通过引用变量名得到其成员的值。
-
不可以在定义共用体变量结构类型时对它初始化,在定义共用体类型的同时不能初始化
-
共用体变量的地址及其各成员的地址都是同一地址,因为各成员地址的分配都是从共用体变量空间的起点开始
#include
union data { int i; char a; float x; }; union data u; int main() { printf("%pt", &u); //共用体变量的地址 printf("%pt", &(u.i)); //共用体变量成员的地址 printf("%pt", &(u.a)); //共用体变量成员的地址 printf("%pt", &(u.x)); //共用体变量成员的地址 return 0; } //编译结果:0086A13C 0086A13C 0086A13C 0086A13C -
不能使用共用体变量作为函数参数,也不能使用函数返回共用体变量,但可以使用它指向共用体变量的指针(与结构体类似)
-
共用体变量可以出现在结构体类型的定义中,也可以定义结构体数组
-
同一时间,只能使用共用体内变量成员的一个值
#include
int main() { union temp { char a; int b; }t; t.a = 66; t.b = 266; printf("t.a:%d, t.b:%dn", 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
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("%dn", 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; GREEN; BLUE; }; //打印枚举值: 1,2,3
-
-
枚举元素不是字符常量也不是字符串常量,使用时不要加单引号、双引号
-
只能把枚举值赋给枚举变量,不能把枚举元素的数值直接赋值给枚举变量;若一定要把数值赋给枚举变量,则必须用强制类型转换。
enum Color { RED = 1; //1 GREEN; //2 BLUE; //3 }clr; clr = GREEN; //正确 clr被赋值为2 clr = 2; //错误不能将枚举元素的数值直接赋给枚举变量 clr = (enum Color)2; //正确,使用强制类型转换
-
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较,枚举有类型检查,更加严谨
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
C语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”
类型说明符 typedef 即可用来完成此功能。
- typedef 定义的一般形式:typedef 原类型名 新类型名
- 其中原类型名中含有定义部分,新类型名一般用大写表示,以便区别
- 有时也可用宏定义来代替 typedef的功能,但是宏定义是由预处理完成的,而 typedef 则是在编译时完成的,后者更为方便



