1. 结构体的概念
构造类型:不是基本类型的数据结构也不是指针,它是若干个相同或不同类型的数据构成的集合。
常用的构造类型:数组、结构体、共用体、枚举
结构体用于将不同类型的变量组合在一起,以便在一个单一的数据单元中处理它们。
例如,可以将一个人的名字、年龄和地址组织在一个结构体中。
char name[50];
int age;
char addr[100];
像上面那样单独定义,在处理时很繁琐,所以可以引入结构体
struct Person {
char name[50];
int age;
char address[100];
};
数组用于保存多个相同类型的数据;
结构体用于保存多个不同类型的数据。
2. 定义、初始化、使用结构体
2.1 结构体的定义方法
方法一:
struct 结构体类型名{
成员列表;
};
定义时不要忘了}
后面的分号
用结构体类型定义变量
通过声明结构体类型的变量,可以创建结构体的实例。
struct Person lucy,bob,lilei;
每个变量都有三个成员,分别是name
, age
和address
。
方法二:在定义结构体类型的时候直接定义结构体变量
struct Person {
char name[50];
int age;
char address[100];
}lucy,bob,lilei;
这种情况下,后续可以继续定义结构体变量。
struct Person libai;
结构体一般在全局定义,相应的变量也是全局变量。
方法三:无结构体类型名定义
struct {
char name[50];
int age;
char address[100];
}lucy,bob,lilei;
这种情况下,后续不可以继续定义结构体变量。
常用定义方法:给结构体类型取别名。
typedef struct Person {
char name[50];
int age;
char address[100];
}PERSON;
typedef
主要用于给一个类型取别名。PERSON
是重新定义的结构体类型名。在定义时,Person
可以省略,因为反正都要取别名,写不写都没区别了。定义结构体变量的时候直接写PERSON
即可,不需要写struct
。
PERSON
相当于struct Person
。
PERSON bob
相当于struct Person bob
。
2.2 结构体的初始化方法
结构体的初始化是指在声明结构体变量时同时为其成员赋值。
//定义一个结构体
struct Person {
int id;
char name[32];
char sex[4];
int age;
}zhangsan, lisi = {0123, "李四", "女", 20};
int main()
{
struct Person wangwu;
//结构体变量的初始化
struct Person zhaoliu = {0144, "赵六", "男", 19};
return 0;
}
若用typedef
#include <stdio.h>
typedef struct {
int a;
int b;
char c[5];
}MSG;
int main()
{
MSG msg1, msg2 = {22, 13, "xixi"};
return 0;
}
2.3 结构体变量对成员的调用方式
结构体变量名.成员名
注:这里的结构体变量主要指普通的结构体变量。
struct Person {
int id;
char name[32];
char sex[4];
int age;
}zhangsan, lisi = {0123, "李四", "女", 20};
int main()
{
zhangsan.id = 1001;
zhangsan.name = "张三";//可以这样直接赋值吗?
zhangsan.sex = "男";//可以这样直接赋值吗?
zhangsan.age = 22;
return 0;
}
数组名(如 zhangsan.name
和 zhangsan.sex
)实际上是指向该数组首元素的 指针 常量。因为它们是指针常量,不能直接被赋值。
zhangsan.name
是一个指向 zhangsan
结构体中 name
数组首元素的 指针 。
字符串字面量"张三"
在内存中是一个静态分配的字符 数组 ,编译时常量,并且它的地址是固定的。
不能直接将一个 字符串字面量 赋值给一个字符 数组 ,因为这相当于试图将一个常量指针的地址赋给另一个常量指针,这是不允许的。在 C 语言中,直接赋值只能用于基本数据类型(如 int、float)或结构体,而不能用于数组。
需要使用 strcpy()
函数来复制字符串。
#include <string.h>
struct Person {
int id;
char name[32];
char sex[4];
int age;
}zhangsan, lisi = {0123, "李四", "女", 20};
int main()
{
zhangsan.id = 1001;
strcpy(zhangsan.name, "张三");
strcpy(zhangsan.sex, "男");
zhangsan.age = 22;
printf("%d - %s - %s - %d\n", zhangsan.id, \
zhangsan.name, zhangsan.sex, zhangsan.age);
return 0;
}
输出:
1001 - 张三 - 男 - 22
2.4 在结构体中嵌套结构体
下面的代码展示了2中初始化形式。
#include <stdio.h>
#include <string.h>
typedef struct{
int year;
int month;
int date;
}BTH;
typedef struct{
int id;
char name[32];
BTH birthday;
}STU;
int main()
{
//形式一
STU xiaoming = {12138, "小明", 2003, 2, 18};
printf("%d - %s - %d - %d - %d\n", xiaoming.id, xiaoming.name, \
xiaoming.birthday.year, xiaoming.birthday.month, xiaoming.birthday.date);
//形式二
STU dali;
dali.id = 48956;
strcpy(dali.name, "大力");
dali.birthday.year = 2001;
dali.birthday.month = 5;
dali.birthday.date = 4;
printf("%d - %s - %d - %d - %d\n", dali.id, dali.name, \
dali.birthday.year, dali.birthday.month, dali.birthday.date);
return 0;
}
输出结果:
12138 - 小明 - 2003 - 2 - 18
48956 - 大力 - 2001 - 5 - 4
2.5 相同类型的结构体变量相互赋值
如果同名同姓,且都是男生,只是ID不同,重复定义、初始化变量太麻烦。
相同类型结构体变量之间可以直接相互赋值。
#include <stdio.h>
#include <string.h>
//定义一个结构体
typedef struct {
int id;
char name[32];
char sex[4];
int age;
}STU;//定义结构体变量
int main()
{
STU zhangsan;
zhangsan.id = 1001;
strcpy(zhangsan.name, "张三");
strcpy(zhangsan.sex, "男");
zhangsan.age = 22;
printf("%d - %s - %s - %d\n", zhangsan.id,
zhangsan.name, zhangsan.sex, zhangsan.age);
STU zhangsan_1;
zhangsan_1 = zhangsan;
zhangsan_1.id = 1105;
printf("%d - %s - %s - %d\n", zhangsan_1.id,
zhangsan_1.name, zhangsan_1.sex, zhangsan_1.age);
return 0;
}
输出结果:
1001 - 张三 - 男 - 22
1105 - 张三 - 男 - 22
3. 结构体数组
结构体数组是 数组 ,是由若干个相同类型的结构体变量构成的集合。
3.1 结构体数组的定义方法
struct 结构体类型名 数组名[元素个数];
struct stu{
int num;
char name[20];
char sex;
};
struct stu edu[3];
struct stu edu[3];
定义了一个 struct stu
类型的结构体数组 edu
,这个数组有 3 个元素分别是 edu[0]
、edu[1]
、edu[2]
。
3.2 结构体数组的引用方法
数组名[下标]
3.3 结构体数组元素对成员的使用
数组名[下标].成员
如edu[0].num
。
#include <stdio.h>
typedef struct{
int num;
char name[32];
float score;
}STU;
int main()
{
//定义一个结构体数组
STU edu[3]={
{101, "Cook", 78},
{102, "Zhuge", 69.4},
{103, "Mamba", 77.9}
};
int i;
float sum = 0;
for(i = 0;i < 3;i++)
{
printf("%d - %s - %.2f\n", edu[i].num, edu[i].name, edu[i].score);
sum += edu[i].score;
}
printf("平均成绩为%.2f\n", sum / 3);
return 0;
}
输出结果:
101 - Cook - 78.00
102 - Zhuge - 69.40
103 - Mamba - 77.90
平均成绩为75.10
4. 结构体指针
4.1 声明和使用结构体指针
结构体指针变量的定义方法:struct 结构体类型名 *结构体指针变量名;
(1) 定义结构体类型
struct Person {
int id;
char name[32];
char sex[4];
int age;
};
(2) 声明结构体指针
struct Person *ptr;
(3) 赋值给结构体指针
结构体指针可以指向现有的结构体变量,也可以指向动态分配的内存空间。
struct Person person;
ptr = &person; // 指向结构体变量
(4) 通过指针访问结构体成员
使用箭头运算符 (->) 访问结构体指针所指向的结构体成员。
或者使用(*结构体指针变量名).成员
(不常用)
ptr->id = 1001;
strcpy(ptr->name, "张三");
strcpy(ptr->sex, "男");
(*ptr).age = 22;
4.2 完整例子
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Person {
int id;
char name[32];
char sex[4];
int age;
};
int main()
{
//定义结构体指针变量
struct Person *ptr;
//在堆区开辟结构体空间,并将其地址保存在结构体指针变量
ptr = (struct Person *)malloc(sizeof(struct Person));
//通过指针访问结构体成员
ptr->id = 1001;
strcpy(ptr->name, "张三");
strcpy(ptr->sex, "男");
ptr->age = 22;
//打印结构体成员
printf("ID: %d\n", ptr->id);
printf("Name: %s\n", ptr->name);
printf("Sex: %s\n", ptr->sex);
printf("Age: %d\n", ptr->age);
return 0;
}
输出结果:
ID: 1001
Name: 张三
Sex: 男
Age: 22
5. 结构体内存分配
规则 1:以多少个字节为单位开辟内存
这条规则的意思是,当给一个结构体变量分配内存时,编译器会根据结构体中成员的数据类型,决定结构体内存的对齐方式和分配单位。具体来说,结构体中的成员数据类型决定了结构体的对齐要求,从而影响内存分配的单位大小。
(1) 结构体中只有 char
型数据,以 1 字节为单位开辟内存
char
类型占用 1 字节的内存空间,并且它的对齐要求也是 1 字节。因此,如果结构体中的成员全部都是 char
类型,那么结构体的每个成员将连续存储,没有额外的填充字节。
示例:
struct Example {
char a;
char b;
char c;
};
- 结构体
Example
的总大小为 3 字节,没有填充字节。 - 成员
a
、b
、c
的地址是连续的。
(2) 结构体中出现了 short
类型数据,没有更大字节数的基本类型数据,以 2 字节为单位开辟内存
short
类型通常占用 2 字节,并且对齐要求也是 2 字节。因此,如果结构体中有 short
类型的数据,而没有其他占用更多字节的类型,那么结构体的对齐要求会是 2 字节。
示例:
struct Example {
char a;
short b;
};
- 结构体
Example
的总大小可能是 4 字节,而不是 3 字节。 - 成员
b
需要从一个偶数地址(2 的倍数)开始,所以在a
后面可能会有 1 个填充字节。
(3) 出现了 int
、float
,没有更大字节的基本类型数据的时候,以 4 字节为单位开辟内存
int
和 float
类型通常占用 4 字节,并且对齐要求也是 4 字节。因此,结构体的对齐要求会是 4 字节。如果没有其他更大字节的类型,这就是结构体的对齐单位。
示例:
struct Example {
char a;
int b;
float c;
};
- 结构体
Example
的总大小可能是 12 字节。 - 成员
b
和c
的地址可能都需要从 4 的倍数地址开始,所以在a
后面可能会有 3 个填充字节。
(4) 出现了 double
类型的数据
-
情况 1:在
VC
(Visual C++)编译器中,double
类型的对齐要求是 8 字节,因此结构体的内存会以 8 字节为单位开辟。 -
情况 2:在
gcc
编译器中,double
类型的对齐要求可能仍然是 4 字节,因此结构体的内存可能会以 4 字节为单位开辟。
无论在何种环境下,double
类型始终占用 8 字节,但它的对齐要求和内存分配单位可能因编译器而异。
示例:
struct Example {
char a;
double b;
};
- 在
VC
编译器中,struct Example
的大小可能是 16 字节,因为double
类型要求 8 字节对齐,b
可能从偏移量 8 开始,前面有 7 个填充字节。 - 在
gcc
编译器中,结构体的大小可能是 12 字节,因为double
的对齐单位可能是 4 字节,这样b
从偏移量 4 开始,前面有 3 个填充字节。
这段话详细描述了在内存分配中,不同数据类型的变量如何在内存中对齐。这种对齐方式是为了提高内存访问的效率。以下是对这段话的详细解释:
规则 2:字节对齐
字节对齐是指变量在内存中存储时,其起始地址必须是某个字节数的倍数,这个字节数称为对齐要求(Alignment)。对齐要求由数据类型决定,不同的数据类型有不同的对齐要求。
(1) char
型变量的对齐:1 字节对齐
- 解释:
char
类型的变量占用 1 字节的内存空间,且对齐要求也是 1 字节。这意味着char
类型的变量可以存放在任何内存地址上,因为任何整数都是 1 的倍数。 - 内存布局:
char
型变量的起始地址可以是任意的,例如 0x00、0x01、0x02 等。
(2) short int
型变量的对齐:2 字节对齐
- 解释:
short
类型的变量通常占用 2 字节的内存空间,且对齐要求是 2 字节。这意味着short
类型的变量的起始地址必须是 2 的倍数。 - 内存布局:
short
型变量可以存放在 0x00、0x02、0x04 等地址上,但不能存放在 0x01、0x03 等非 2 的倍数的地址上。
(3) int
型变量的对齐:4 字节对齐
- 解释:
int
类型的变量通常占用 4 字节的内存空间,且对齐要求是 4 字节。这意味着int
类型的变量的起始地址必须是 4 的倍数。 - 内存布局:
int
型变量可以存放在 0x00、0x04、0x08 等地址上,但不能存放在 0x01、0x02、0x03、0x05 等非 4 的倍数的地址上。
(4) long
型变量的对齐:4 字节对齐(在 32 位平台下)
- 解释:在 32 位平台上,
long
类型的变量通常占用 4 字节,且对齐要求是 4 字节。这与int
类型的对齐要求相同。 - 内存布局:
long
型变量可以存放在 0x00、0x04、0x08 等地址上。
(5) float
型变量的对齐:4 字节对齐
- 解释:
float
类型的变量通常占用 4 字节的内存空间,且对齐要求是 4 字节。这意味着float
类型的变量的起始地址必须是 4 的倍数。 - 内存布局:
float
型变量可以存放在 0x00、0x04、0x08 等地址上。
(6) double
型变量的对齐
a. 在 VC 环境下(Visual C++ 编译器):
- 解释:
double
类型的变量占用 8 字节,并且在 VC 环境中,double
的对齐要求是 8 字节。这意味着double
类型的变量的起始地址必须是 8 的倍数。 - 内存布局:
double
型变量可以存放在 0x00、0x08、0x10 等地址上。
b. 在 GCC 环境下(GNU 编译器):
- 解释:虽然
double
类型的变量仍然占用 8 字节,但是在某些 GCC 环境中,对齐要求可能是 4 字节。这意味着double
类型的变量的起始地址可以是 4 的倍数即可。 - 内存布局:在这种情况下,
double
型变量可以存放在 0x00、0x04、0x08、0x0C 等地址上。这种做法可能是为了减少内存浪费,但在某些硬件平台上,可能会降低内存访问的效率。
为什么需要字节对齐?
字节对齐主要是为了提高 CPU 访问内存的效率。在大多数硬件架构中,CPU 从内存中读取数据时,通常会以块(word)的方式读取。如果数据的地址没有对齐,CPU 可能需要进行多次访问才能取到完整的数据,从而降低访问速度。因此,为了优化性能,编译器通常会对变量进行对齐处理。
例1:
#include <stdio.h>
struct stu{
char a;//占1个字节,补3个字节
int b;//占4个字节
short int c;//占2个字节,补2个字节
}temp;
int main()
{
printf("%d\n", sizeof(temp));
printf("%p\n", &(temp.a));
printf("%p\n", &(temp.b));
return 0;
}
输出结果:
12
00007ff61a24c030
00007ff61a24c034
例2:
#include <stdio.h>
struct stu{
char a[10];//占10个字节,补2个字节,凑成4的倍数
int b;//占4个字节
short int c;//占2个字节,补2个字节
}temp;//10+2+4+2+2=20
int main()
{
printf("%d\n", sizeof(temp));
printf("%p\n", &(temp.a));
printf("%p\n", &(temp.b));
printf("%p\n", &(temp.c));
return 0;
}
输出结果:
20
00007ff6ba6dc030
00007ff6ba6dc03c
00007ff6ba6dc040
6. 位段
位段(Bit Field)是C语言结构体的一种特殊用法,用于按位分配结构体成员。它允许我们在结构体中以位为单位定义变量,而不是通常的字节或多个字节。这在需要紧凑存储数据的场合非常有用,比如表示硬件寄存器、网络协议标志等场景。
位段的定义
位段是通过在结构体中声明带有显式宽度的整型类型来定义的。定义位段的语法如下:
struct {
unsigned int field1 : 3;
unsigned int field2 : 5;
unsigned int field3 : 1;
} bitfield;
关键点解释
-
类型声明:
- 位段成员通常是
int
或unsigned int
类型,但也可以是signed int
。选择无符号类型可以避免负值带来的问题。
- 位段成员通常是
-
位宽:
- 冒号
:
后面的数字表示该字段使用的位数。例如,field1
使用 3 位,field2
使用 5 位,field3
使用 1 位。
- 冒号
-
内存分配:
- 位段的成员按照声明的位数来分配内存,但具体的分配方式依赖于编译器和平台。在多数情况下,位段成员被紧密排列在一个或多个字节中,以最小化内存占用。
例子:简单的位段结构体
#include <stdio.h>
struct stu{
char a : 7;
char b : 7;
char c : 2;
}temp;
int main()
{
temp.a = 2;
temp.b = 12;
temp.c = 1;
printf("%d\n", sizeof(temp));
printf("a = %u, b = %u, c = %u\n", temp.a, temp.b, temp.c);
return 0;
}
运行结果:
3
a = 2, b = 12, c = 1
注意
- 赋值时,不要超出位段定义的范围
- 位段成员的类型必须指定为整形或字符型
- 一个位段必须存放在一个存储单元中,不能跨两个单元
位段的存储单元
- char 型位段存储单元是 1 个字节
- short int 型的位段存储单元是 2 个字节
- int 型的位段存储单元是 4 个字节
- long int 的位段存储单元是 4 字节
位段的使用场景
-
存储效率:
- 位段非常适合于需要在有限空间内存储多个标志或小数据的场合,例如标志寄存器、状态字等。
-
硬件编程:
- 位段经常用于操作特定硬件寄存器的位。这是因为硬件寄存器通常具有多个功能,每个功能可能只需要1或几位。
-
协议字段:
- 位段也常用于实现网络协议的字段解析,例如TCP/IP头部字段,多个标志和字段在位级别操作。
7. 共用体
共用体(union
)是C语言中的一种数据结构,与结构体(struct
)类似,但在共用体中,所有成员共享同一块内存空间。也就是说,共用体的所有成员在内存中的起始地址是相同的,但你只能在任一时刻存储其中一个成员的值。
共用体的定义
共用体的定义方式类似于结构体,使用关键字 union
:
union Data {
int i;
float f;
char str[20];
};
在上面的例子中,union Data
具有三个成员:i
、f
和 str
。这些成员共享相同的内存空间,因此共用体的大小由其最大的成员决定。
共用体的特点
-
内存共享:共用体的所有成员共享相同的内存空间,因此共用体的大小等于它的最大成员的大小。
-
只能存储一个值:在任一时刻,共用体中只能存储一个成员的值。存储新值时会覆盖旧值。
-
共用体的用途:共用体通常用于需要在不同数据类型之间节省内存的场合,例如在某些嵌入式系统或内存受限的应用中。
使用共用体的例子
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i = %d\n", data.i);
data.f = 3.14;
printf("data.f = %f\n", data.f);
strcpy(data.str, "Hello");
printf("data.str = %s\n", data.str);
// 注意:共用体的内存是共享的,因此只会保留最后一个赋值的成员的值
printf("After all assignments:\n");
printf("data.i = %d\n", data.i); // 输出的值是未定义的
printf("data.f = %f\n", data.f); // 输出的值是未定义的
printf("data.str = %s\n", data.str); // 只有最后一个值是有效的
return 0;
}
运行结果:
data.i = 10
data.f = 3.140000
data.str = Hello
After all assignments:
data.i = 1819043144
data.f = 1143139122437582505939828736.000000
data.str = Hello
在这个例子中,union Data
的成员 i
、f
和 str
都共享同一块内存空间:
- 当
data.i
被赋值时,内存中存储了一个整数。 - 当
data.f
被赋值时,i
的值被覆盖,内存中存储了一个浮点数。 - 当
data.str
被赋值时,之前的值都被覆盖,内存中存储了一个字符串。
因此,在最后的输出中,只有 data.str
是有效的,data.i
和 data.f
的值会被覆盖,结果是未定义的。
共用体的大小
共用体的大小等于它的最大成员的大小。在上面的例子中,如果 int
是 4 字节,float
也是 4 字节,而 char str[20]
是 20 字节,那么 union Data
的大小就是 20 字节。
printf("Size of union: %lu\n", sizeof(data)); // 输出 20
何时使用共用体?
-
节省内存:当你确定在同一时刻只需要存储一个值(可能是不同的数据类型)时,可以使用共用体来节省内存。
-
数据类型转换:共用体有时也用于不同类型的数据转换,通过访问同一块内存的不同方式来实现类型转换。
8. 枚举
枚举(enum
)是C语言中的一种用户定义的数据类型,用于将一组相关的常量组合在一起,并为它们赋予更有意义的名字。使用枚举可以提高代码的可读性和可维护性。
枚举的定义
枚举通过关键字 enum
来定义。下面是一个简单的枚举定义示例:
enum Weekday {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
在这个例子中,enum Weekday
定义了一个枚举类型 Weekday
,它包含了七个枚举常量(SUNDAY
到 SATURDAY
)。这些常量默认从 0 开始依次递增:
SUNDAY
对应 0MONDAY
对应 1TUESDAY
对应 2- 以此类推
使用枚举
定义了枚举类型后,你可以像使用其他数据类型一样来定义枚举变量:
enum Weekday today;
然后,你可以将枚举常量赋值给枚举变量:
today = MONDAY;
也可以通过条件语句进行枚举变量的判断:
if (today == MONDAY) {
printf("Today is Monday.\n");
}
指定枚举值
虽然枚举常量默认从 0 开始递增,但你可以手动指定它们的值:
enum Weekday {
SUNDAY = 7,
MONDAY = 1,
TUESDAY = 2,
WEDNESDAY = 3,
THURSDAY = 4,
FRIDAY = 5,
SATURDAY = 6
};
在这个例子中,你可以看到 SUNDAY
被显式地赋值为 7,其他枚举常量也可以根据需要手动赋值。
你还可以只为部分枚举常量指定值,未指定的常量将以递增的方式从前一个常量的值开始:
enum Weekday {
SUNDAY = 7,
MONDAY, // 8
TUESDAY, // 9
WEDNESDAY = 3,
THURSDAY, // 4
FRIDAY, // 5
SATURDAY // 6
};
在这种情况下:
SUNDAY
为 7MONDAY
为 8TUESDAY
为 9WEDNESDAY
为 3(显式赋值)THURSDAY
、FRIDAY
、SATURDAY
分别为 4、5 和 6
枚举的优点
- 可读性:枚举为相关常量赋予了有意义的名字,使代码更易读。
- 防止错误:枚举提供了一种类型安全的方式来使用常量,防止了常量间的混淆。
- 易于维护:枚举可以方便地添加、删除或修改常量,而不影响其他部分的代码。
枚举的底层表示
在C语言中,枚举常量本质上是整数,因此枚举变量可以用于整数计算或赋值:
enum Weekday today = SUNDAY;
int dayValue = today; // dayValue = 7
枚举在C中的使用注意事项
-
枚举值的范围:由于枚举常量是整数,因此它们的取值范围受限于
int
类型的范围。 -
不严格的类型检查:在C语言中,枚举类型和
int
类型之间可以自由转换,因此编译器不会强制检查枚举类型的变量是否只包含定义的枚举常量。 -
用逗号隔开:枚举常量间用逗号隔开,而不是分号。