C语言基础18
内容提要
-
构造类型
-
结构体
-
共用体/联合体
-
枚举
-
typedef
-
构造类型
数据类型
-
基本类型/基础类型
-
整型
-
短整型:short [int] -- 2字节
-
基本整型:int -- 4字节
-
长整型:long [int] -- 32位4字节/64位8字节
-
长长整型:long long [int] -- 8字节(大多数现代机器,旧机器可能超过8字节),C99新增
注意:以上类型又都分为signed(默认)和unsigned
-
-
浮点型
-
单精度:float -- 4字节
-
双精度:double -- 8字节
-
长双精度:long double -- C99新增
-
-
字符型:char -- 1字节
-
-
指针类型
-
数据类型*:
int* char* float*
等 -- 8字节 -
void*:通用类型指针(万能指针) -- 8字节
-
-
空值类型
-
void:无返回值,无形参(不能定义变量)
-
-
构造类型(自定义类型)
-
结构体类型:struct
-
共用体/联合体类型:union
-
枚举类型:enum
-
结构体
结构体的定义【定义类型】
-
定义:自定义数据类型的一种,关键字struct,结构体类型的变量可以存储多个不同数据类型的数据。
-
语法:
struct 结构体名 // 我们定义的数据类型的名字 { 数据类型1 成员名称1; // 结构体中的变量叫做成员 数据类型2 成员名称2; ..... };
注意:结构体中定义的变量,称之为成员变量(成员属性)
-
格式说明:
-
结构体名:合法的标识符,建议首字母大写(所谓的结构体名,就是自定义类型的类型名称)
-
数据类型n:C语言支持的所有类型(包括函数,函数在这里用函数指针表示)
-
成员名称n:合法的标识符,就是变量的命名标准
-
数据类型n成员名称n:类似于定义变量,定义了结构体中的成员
-
-
注意:
-
结构体在定义的时候,成员不能赋值,举例:
struct Cat { int age = 5; // 错误,结构体定义的时候,成员不能赋值 double height; // 正确 void (*run)(void); // 正确 };
-
-
常见的定义格式:
-
方式1:常规定义(命名结构体,只定义数据类型) -- 推荐
struct Student { int num; // 学号 char name[20]; // 姓名 char sex; // 性别 int age; // 年龄 char address[100]; // 籍贯 void (*info)(void); // 信息输出(函数指针) };
-
方式2:定义匿名结构体(常用于作为其他结构体的成员使用)
struct Dog { char *name; // 姓名 int age; // 年龄 // 匿名结构体 struct { // 匿名结构体定义时不能省略成员 int year; // 年 int month;// 月 int day; // 日 } birthday; // 生日,匿名结构体作为其他结构体成员时,需添加成员名称,否则无法访问 };
注意:定义匿名结构体的同时必须定义结构体成员,否则编译报错;结构体可以作为另一个结构体的成员
总结:
-
结构体可以定义在局部位置,也可以定义在全局位置(用的比较多,因为可以实现复用)
-
全局位置的结构体名和局部位置的结构体名可以相同,遵循就近原则(和变量的定义同理)
-
-
-
结构体类型的使用:
利用结构体类型定义变量、定义数组,也可以作为函数的返回值和参数;结构体类型的使用与基本数据类型的使用类似。
结构体变量的定义【定义变量】
-
三种形式定义结构体变量
结构体变量也称之为结构体实例
-
第1种:
①先定义结构体(定义数据类型)
②然后使用(使用数据类型定义变量)
举例:
// 定义结构体(定义数据类型) struct A { int a; char b; }; // 定义结构体变量 struct A x; // struct A就是数据类型,x就是变量名 struct A y; // struct A就是数据类型,y就是变量名
-
第2种:
①在定义结构体的同时,定义结构体变量
语法:
struct 结构体名 { 数据类型1 数据成员1; ... } 变量列表;
举例:
struct A { int a; char b; } x,y;
此时定义了一个结构体A,x和y是这个结构体类型的变量
-
第3种:(不推荐)
在定义匿名结构体的同时,定义结构体变量
struct { int a; char b; } x,y; struct { int a; char b; } z;
此时定义了一个没有名字的结构体(匿名结构体);x,y是这个结构体类型的变量
-
-
匿名结构体
-
优点:少写一个结构体名称
-
缺点:只能使用一次,定义结构体类型的同时必须定义变量
-
应用场景:
-
当结构体的类型只需要使用一次,并且定义类型的同时定义变量
-
作为其他结构体的成员使用
-
-
定义结构体的同时,定义结构体变量初始化
struct Cat { int age; char color[20]; } cat;
-
结构体成员部分初始化时,大括号{}不能省略
-
结构体成员,没有默认值,是随机值
-
-
-
案例
/** * 先定义结构体,再定义结构体变量 */ void fun1() { // 定义结构体 struct A { int a; char b; }; // 定义结构体变量/实例 struct A x; struct A y; } /** * 定义结构体的同时,定义结构体变量 */ void fun2() { struct A { int a; char b; } x,y; struct A z; } /** * 定义匿名结构体的同时定义变量 */ void fun3() { struct { int a; // 结构体成员 char b; } x,y; struct { int a; char b; } z; } int main(int argc, char *argv[]) { fun1(); fun2(); fun3(); return 0; }
结构体变量的使用
-
结构体变量访问结构体成员
-
语法:
结构体变量名.成员名;
①可以通过访问给成员赋值(存数据)
②可以通过访问获取成员的值(取数据)
-
结构体变量未初始化,结构体的成员是随机值(和普通的变量、数组同理)
-
-
结构体变量在定义时,可以初始化
-
建议用大括号{}标明数据的范围
-
结构体成员初始化,可以部分初始化(和数组类似),部分初始化时一定要带大括号标明数据的范围
-
-
案例:
/* 定义全局的结构体 */ struct Dog { char *name; // 名字 int age; // 年龄 char sex; // M:公,W:母 void (*eat)(void); // 吃狗粮 }; void eat() { printf("狗狗在吃狗粮!\n"); } /** * 方式1:先定义,再初始化 */ void fun1() { // 定义结构体变量 struct Dog dog; // 给结构体变量成员赋值 dog.name = "旺财"; dog.age = 5; dog.sex = 'M'; dog.eat = eat; // 函数指针,eat指针指向eat函数 // 访问结构体变量成员 printf("%s,%d,%c\n",dog.name, dog.age, dog.sex); // 访问成员方法(函数) dog.eat(); } /** * 方式2:定义的同时初始化 */ void fun2() { // 定义结构体变量的同时,给变量成员初始化 struct Dog dog = {"旺财",5,'M',eat}; // 修改成员的值 dog.name = "金宝"; // 访问结构体变量成员 printf("%s,%d,%c\n",dog.name, dog.age, dog.sex); // 访问成员方法(函数) dog.eat(); } int main(int argc, char* argv[]) { fun1(); fun2(); return 0; }
结构体数组的定义
-
什么时候需要结构体数组
比如:我们需要管理一个学生对象,只需要定义一个
struct Student yixin;
假如:我们需要管理一个班的学生对象,此时就需要定义一个结构体数组
struct Student stus[33];
-
四种形式定义结构体数组
-
第1种:结构体 → 结构体变量 → 结构体数组,举例:
// 定义一个学生结构体(定义数据类型) struct Student { char* name; // 姓名 int age; // 年龄 float scores[3]; // 三门课程的成绩 }; // 定义结构体变量/实例 struct Student zhangsan = {"张三",21,{89,99,78}}; struct Student lisi = {"李四",22,{66,78,98}}; // 定义结构体数组 struct Student stus[3] = {zhangsan,lisi};
-
第2种:结构体 → 结构体数组,举例:
// 定义一个学生结构体(定义数据类型) struct Student { char* name; // 姓名 int age; // 年龄 float scores[3]; // 三门课程的成绩 }; // 定义结构体数组并初始化 struct Student stus[3] = { {"张三",21,{89,99,78}}, {"李四",22,{66,78,98}} };
-
第3种:结构体、结构体数组一体化(含初始化),举例:
// 定义一个学生结构体(定义数据类型) struct Student { char* name; // 姓名 int age; // 年龄 float scores[3]; // 三门课程的成绩 } stus[3] = { {"张三",21,{89,99,78}}, {"李四",22,{66,78,98}} };
-
第4种:结构体、结构体数组一体化(不含初始化),举例:
// 定义一个学生结构体(定义数据类型) struct Student { char* name; // 姓名 int age; // 年龄 float scores[3]; // 三门课程的成绩 } stus[3]; // 赋值 stus[0].name = "张三"; stus[0].age = 21; stus[0].scores[0] = 89; stus[0].scores[1] = 99; stus[0].scores[2] = 78; stus[1].name = "李四"; stus[1].age = 22; stus[1].scores[0] = 66; stus[1].scores[1] = 78; stus[1].scores[2] = 98;
-
结构体数组的访问
语法:
结构体指针 -> 成员名
举例:
// 普通指针访问 (*p).成员名 // 结构体指针访问(结构体指针访问符:->) 等价于上面写法 p -> 成员名
案例:
/* 定义全局Student结构体 */ struct Student { int id; char *name; int age; float scores[3]; void (*info)(char*,int); }; /** * 定义一个函数,输出信息 */ void info(char* name,int age) { printf("大家好,我是%s,今年%d岁了!\n",name,age); } void func() { // 定义结构体实例并初始化 struct Student stu1 = {1,"张三",21,{78,88,98},info}; struct Student stu2 = {2,"李四",22,{90,98,78},info}; // stu1.info = info; // stu2.info = info; // 定义结构体数组并初始化 struct Student stus[] = {stu1,stu2}; // 遍历数组 // 计算len int len = sizeof(stus)/sizeof(stus[0]); // 指针遍历 // 定义一个指针,获取遍历的学生对象 struct Student *p = stus; for(; p < stus + len; p++) { printf("%d,%s,%d,%.2f,%.2f,%.2f\n",p->id,p->name,p->age,p->scores[0],p->scores[1],p->scores[2]); // 调用函数 p-> info 等价于 (*p).info(); p -> info(p -> name, p -> age); } printf("\n"); } int main(int argc, char* argv[]) { func(); return 0; }
注意:
当
->
和[]
共存的时候,它们的优先级关系:[]
>->
结构体类型
结构体数组
案例
-
需求:对候选人得票的统计程序。设有3个候选人,每次输入一个得票的候选人名字,要求最后输出各人得票的结果
-
代码:
#include <string.h> /** * 定义一个候选人结构体 */ struct Person { char name[20]; // 候选人名字 int count; // 候选人票数 }; /** * 定义候选人数组,并初始化 */ struct Person persons[3] = { {"诸葛蛋蛋",0}, {"司马苟丹",0}, {"上官铁祝",0}, }; int main(int argc, char *argv[]) { // 定义循环变量 int i,j; // 创建一个char数组,用来接收控制台输入的候选人的名字 char leader_name[20]; // 使用一个for循环,模拟10次投票 for(i = 0; i < 10; i++) { printf("请输入您要投票的候选人姓名:\n"); scanf("%s",leader_name); // 给被投票的候选人+1票 for(j = 0; j < 3; j++) { // 判断两个字符串的结果是否相同 if(strcmp(leader_name,persons[j].name) == 0) { persons[j].count++; } } } printf("\n投票结果:\n"); // 方式1 struct Person *p = persons; // p就是结构体指针 for(; p < persons + 3; p++) { // printf(" %s:%d\n",(*p).name, (*p).count); printf(" %s:%d\n",p -> name, p -> count); } printf("\n"); // 方式2 for(i = 0; i < 3; i++) { printf(" %s:%d\n",(persons+i) -> name, (persons+i) -> count); } return 0; }
结构体指针
-
定义:结构体类型的指针变量指向结构体变量或者数组的起始地址
-
语法:
struct 结构体名 *指针变量列表;
-
举例:
struct Dog { char name[20]; int age; }; struct Dog dog = {"苟富贵",5}; // 基于结构体的构造体指针 struct Dog *p = &dog;
结构体成员的访问
-
结构体成员访问
-
结构体数组名访问结构体成员
-
语法:
结构体数组名 -> 成员名; // 等价于如下写法 (*结构体数组名).成员名;
-
举例:
printf("%s:%d\n",persons -> name, persons -> count);
-
-
结构体成员访问符
-
.
:左侧是结构体变量,也可以叫做结构体对象访问成员符,右侧是结构体成员 -
->
:左侧是结构体指针,也可以叫做结构体指针访问成员符,右侧是结构体成员 -
举例:
struct Person *p = persons; // p 就是构造体指针 for(; p < persons + 3; p++) printf("%s:%d\n",p -> name, p -> count);
-
-
访问结构体成员有两种类型,三种方式:
-
类型1:通过结构体变量(对象|实例)访问成员
struct Stu { int id; // 结构体成员 char name[20]; } stu; // 结构体变量 // 访问成员 stu.name;
-
类型2:通过结构体指针访问成员
-
第1种:指针引用访问成员
struct Stu { int id; // 结构体成员 char name[20]; } stu; // 结构体变量 | 对象 | 实例 struct Stu *p = &stu; // 指针引用访问成员 p -> name; // 等价于(*p).name
-
第2种:指针解引用间接访问成员
struct Stu { int id; // 结构体成员 char name[20]; } stu; // 结构体变量 | 对象 | 实例 struct Stu *p = &stu; // 指针解引用访问成员 (*p).name; // 等价于 p -> name
-
-
结构体数组中元素的访问
struct Stu { int id; char name[20]; float scores[3]; } stus[3] = { {1,"张三",{67,77,88}},// (stus+1) {2,"李四",{90,99,98}}, {3,"王五",{88,97,77}} }; // 取数据 -- 下标法 printf("%s,%.2f\n",stus[1].name,stus[1].scores[1]); // 李四 99 // 取数据 -- 指针法(->) printf("%s,%.2f\n",stus -> name,stus -> scores[2]); // 张三 88 printf("%s,%.2f\n",(stus+1) -> name,(stus+1) -> scores[2]); // 李四 98 printf("%s,%.2f\n",(*(stus+1)).name,(*(stus+1)).scores[2]); // 李四 98
小贴士:
结构体是自定义数据类型,它是数据类型,用法类似于基本类型的int
结构体数组它是存放结构体对象的数组,类似于int数组存放int数据
基本类型数组怎么用,结构体数组就怎么用--->可以遍历,可以作为形式参数,也可以做指针等
-
-
-
结构体类型的使用案例
结构体可以作为函数的返回类型、形式参数等
举例:
#include <string.h> /** * 定义结构体 */ struct Cat { char *name; // 姓名 int age; // 年龄 char color[20]; // 颜色 }; /** * 结构体类型作为形式参数 */ void test1(struct Cat c) { printf("test1:\n%s,%d,%s\n",c.name, c.age, c.color); } /** * 结构体类型作为形式参数,结构体类型作为返回值类型 */ struct Cat test2(struct Cat c) { return c; } /** * 结构体数组作为形式参数 */ struct Cat test3(struct Cat cats[], int len) { struct Cat *p = cats; for(; p < cats + len; p++) { printf("test3:\n%s,%d,%s\n",p -> name, p -> age, p -> color); } } /** * 结构体指针作为形式参数,结构体指针作为返回类型 */ struct Cat *test4(struct Cat *cats, char *name) { struct Cat *p = cats; // 遍历 for(; p < cats + 3; p++) { if(strcmp(name, p -> name) == 0) return p; } return NULL;// 指针返回空 } struct Cat *test5(struct Cat (*cats)[3], char *name) { //数组指针遍历:用指针偏移 for(int i = 0; i < 3; i++) { if(strcmp(name, (*cats)[i].name) == 0) return &(*cats)[i]; //*cats是整个数组的指针 } return NULL; } int main(int argc,char *argv[]) { // 定义结构体对象 struct Cat cat = {"狗狗", 8, "yellow"}; // 结构体对象作为实际参数 test1(cat); // 结构体作为函数参数和返回值类型 struct Cat res_cat = test2(cat); printf("test2:\n%s,%d,%s\n",res_cat.name, res_cat.age, res_cat.color); // 定义结构体数组 struct Cat cats[] = { {"汤姆",16,"blue"}, {"杰瑞",18,"green"}, {"索菲",19,"red"} }; // 结构体作为函数的实际参数 test3(cats,3); // 结构体指针作为形式参数,结构体指针作为返回类型 struct Cat *p = test4(cats,"汤姆"); printf("test4:\n%s,%d,%s\n", p -> name, p -> age, p -> color); struct Cat *q = test5(&cats, "汤姆"); printf("test5:\n%s,%d,%s\n", q -> name, q -> age, q -> color); return 0; }
结构体类型求大小
字节对齐
-
字节对齐的原因:
-
硬件要求:某些硬件平台(如ARM、x86)要求特定类型的数据必须对齐到特定地址,否则会引发性能下降或硬件异常
-
优化性能:对齐的数据访问速度更快。例如,CPU访问对齐的
int
数据只需一次内存操作,而未对齐的数据可能需要多次操作
-
-
字节对齐规则:
-
默认对齐规则
-
结构体的每个成员按其类型大小和编译器默认对齐数(通常是类型的自然对齐数)对齐
-
结构体的总大小必须是最大对齐数的整数倍
-
-
对齐细节
-
基本类型的对齐数:
char
(1字节)、short
(2字节)、int
(4字节)、double
(8字节) -
结构体成员的对齐:每个成员的起始地址必须是对齐数的整数倍
-
结构体总大小的对齐:结构体的总大小必须是其最大对齐数的整数倍
-
-
#pragma pack(n)
的影响:使用#pragma pack(n)
可以强制指定对齐数为n
(n
为 1、2、4、8、16)。此时:-
每个成员的对齐数取
n
和其类型大小的较小值 -
结构体的总大小必须是
n
和最大对齐数中的较小值的整数倍
-
-
-
对齐示例
-
默认对齐
struct S1 { char c; // 1字节(偏移0) int i; // 4字节(需对齐到4,填充3字节,偏移4-7) double d; // 8字节(需对齐到8,偏移8-15) };
struct S1 { double d; // 8字节(偏移0-7) int i; // 4字节(需对齐到8,偏移8-11) char c; // 1字节(需对齐到12,还需填充3字节) };
struct S1 { char c; // 1字节(偏移0,填充7字节) double d; // 8字节(需对齐到8) int i; // 4字节(需对齐到16,填充4字节) };
总结:结构体中,成员的顺序会影响到结构体最终的大小
-
-
使用
#pragma pack(1)
#pragma pack(1) struct S1 { char c; // 1字节(偏移0) int i; // 4字节(偏移1-4) double d; // 8字节(偏移5-12) }; #pragma pack() // S1 的大小为 13字节
#pragma pack(2) struct S1 { char c; // 1字节(偏移0,填充1字节) int i; // 4字节(偏移2-5) double d; // 8字节(偏移6-13) }; #pragma pack() // S1 的大小为 14字节
#pragma pack(4) struct S1 { char c; // 1字节(偏移0,填充3字节) int i; // 4字节(偏移4-7) double d; // 8字节(偏移8-15) }; #pragma pack() // S1 的大小为 16字节
-
在GNU标准中,可以在定义结构体时,指定对齐规则:
__attribute__((packed)); // 结构体所占内存大小是所有成员所占内存大小之和 ——attribute__((aligned(n))); // 设置结构体占n个字节,如果n比默认值小,n不起作用;n必须是2的次方
案例:
int main(int argc,char *argv[]) { struct Cat { char sex __attribute((aligned(2)));// 1 -- 2 int id;// 4 char name[20];// 20 } __attribute__((packed)); // 结构体所占内存大小是所有成员所占内存大小之和,packed---结构体取消所有填充 printf("%ld\n",sizeof(struct Cat));// 默认字节对齐(28)/ 使用packed后(25) return 0; } // 编译结果是26:存在隐藏行为 // 1. aligned(2) 覆盖结构体对齐:GCC 可能将结构体的整体对齐要求设为 max(2, 1) = 2(忽略 id 的 4 字节对齐,因 packed 冲突)。 // 2. 结构体填充规则:总大小需为 2 的倍数 → 25 → 26 字节
柔性数组
定义:柔性数组不占有结构体的大小
语法:
struct St { ... char arr[0]; };
案例:
int main(int argc,char *argv[]) { struct Cat { char sex __attribute((aligned(2)));// 1 -- 2 int id; // 4 char arr[0]; // 0 柔性数组不占用结构体的大小 char name[20]; // 20 } __attribute__((packed)); // 结构体所占内存大小是所有成员所占内存大小之和 printf("%ld\n",sizeof(struct Cat));// 默认字节对齐(28)/ 使用packed后(25)/ 使用aligned之后(26) return 0; }
课堂练习
计算以下结构体的大小
// 定义测试结构体 struct TEST1 { char a; int b; }; struct TEST1_1 { char a; int b; }__attribute__((packed));// 取消字节对齐,取消之后,结构体数据类型大小就等于其所有成员的数据类型之和 struct TEST1_2 { char a __attribute__((aligned(2))); int b; }; struct TEST2 { char a; short c; int b; }; struct TEST3 { int num; char name[10]; char sex; int age; double score; }; struct TEST3_1 { int num; char name[10]; char sex; double score; int age; }; struct TEST4 { int num; short name[5]; char sex; int age; int scores[2]; }; int main(int argc,char *argv[]) { // 创建结构体变量 struct TEST1 test1; struct TEST2 test2; struct TEST3 test3; struct TEST3_1 test3_1; struct TEST4 test4; struct TEST1_1 test1_1; struct TEST1_2 test1_2; // 计算大小 printf("%lu\n",sizeof(test1));// 8 printf("%lu\n",sizeof(test2));// 8 printf("%lu\n",sizeof(test3));// 32 printf("%lu\n",sizeof(test3_1));// 32 printf("%lu\n",sizeof(test4));// 28 printf("%lu\n",sizeof(test1_1));// 5 printf("%lu\n",sizeof(test1_2));// 8 return 0; }
共用体/联合体类型
-
定义:使几个不同变量占用同一段内存的结构。共用体按定义中需要存储空间最大的成员来分配存储单元,其他成员也是使用该空间,它们的首地址是相同的
-
定义格式:
union 共用体名称 { 数据类型 成员名; 数据类型 成员名; ... };
-
共用体的定义和结构体类似
-
可以有名字,也可以匿名
-
共用体在定义时也可以定义共用体变量
-
共用体在定义时也可以初始化成员
-
共用体也可以作为形参和返回值类型使用
-
共用体也可以定义共用体变量
-
...
也就是说,结构体的用法,共用体都支持
-
-
注意:
-
共用体弊大于利,尽量少用,一般很少用;
-
共用体变量在某一时刻只能存储一个数据,并且也只能取出一个数
-
共用体所有成员共享同一内存空间,同一时间只能存储一个值,可能导致数据覆盖
-
共用体和结构体都是自定义数据类型,用法类似于基本数据类型
-
共用体可以是共用体的成员,也可以是结构体的成员
-
结构体可以是结构体的成员,也可以是共用体的成员
-
-
-
案例:
// 定义共用体 union S { char a; float b; int c; };// S的大小是4字节 // 共用体作为共用体成员 union F { char a; union S s; // 4字节 };// 大小是4字节 // 共用体作为结构体的成员 struct G { int a; union S s; };// 大小是8字节 // 定义一个结构体 struct H { int a; char b; };// 大小是8字节 // 结构体作为结构体成员 struct I { int a; int b; struct H h; };// 大小是16字节 // 共用体作为结构体成员 struct J { int a;// 4 char b;// 1 union S s;// 4 };// 大小是12字节 void test1() { // 定义一个共用体 union Stu { int num; char sex; double score; }; // 定义匿名共用体 union { int a; char c; } c;// 匿名共用体,一定要定义出变量名,否则报错 printf("%lu,%lu\n",sizeof(union Stu),sizeof(c)); } void test2() { union C { int a; char b; }; // 定义变量 union C c; // 存储数据 c.a = 10; c.b = 'A'; printf("%d---%d\n",c.a,c.b); // 65---65 c.a += 5; // c.a = 65 + 5 printf("%d---%d\n",c.a,c.b); // 70---70 union E { char *f;// 8 存储的是 hello world! 的首地址 long a;// 8 int b;// 4 } e = {"hello world!"}; // hello world存储在数据段 printf("%s,%ld,%d\n",e.f,e.a,e.b); } int main(int argc,char *argv[]) { test1(); test2(); return 0; }