C 语言学习笔记(结构体1)
内容提要
- 构造类型
- 结构体
- 共用体/联合体
构造类型
数据类型
1.基本类型/基础类型
-
整型
-
短整型:short[int] – 2字节
-
基本整型:int – 4字节
-
长整型:long – 32位系统4字节/64位系统8字节
-
长长整型:long log[int] – 8字节(大多数现代机器,旧机器可能超过8字节),C99新增
注意:以上类型又都分为signed(默认)和unsigned
-
-
浮点型
- 单精度型:float – 4字节
- 双精度型:double – 8字节
- 长双精度型:long double – C99新增
-
字符型:char – 1字节
2.指针类型
- 数据类型*:
int* char* float*
等 --32位系统4字节/64位系统8字节 - void*:通用类型指针(万能指针)-- 32位系统4字节/64位系统8字节
3.空值类型
- void:无返回值,无形参(不能定义变量)
4.构造类型(自定义类型)
- 结构体类型: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; // A就是数据类型,x就是变量名 struct A y; // A就是数据类型,y就是变量名
-
第2种:
① 在定义结构体的同时,定义结构体变量(同时定义数据类型和变量)
语法:
struct 结构体名 { 数据类型1 数据成员1;... } 变量列表;
示例:
struct A {int a;char b; } x,y; // A就是数据类型,x,y就是变量名
此时定义了一个结构体A,x和y是这个结构体类型的变量。
-
第3种:
① 在定义匿名结构体的同时,定义结构体变量。
struct {int a;char b; } x,y;
此时定义了一个没有名字的结构体(匿名结构体),x和y是这个结构体类型的变量。
匿名机构体
- 优点:少写一个结构体名称
- 缺点:只能使用一次,定义结构体类型的同时必须定义变量。
- 应用场景:
- 当结构体的类型只需要使用一次,并且定义类型的同时定义了变量。
- 作为其他结构体的成员使用。
定义结构体同时变量初始化
说明:定义结构体的同时,定义结构体变量并初始化
struct Cat
{int age;char color[20];
}cat;
- 结构体成员部分初始化,大括号不能省略
- 结构体成员,没有默认值,都是随机值,和局部作用域的变量一致。
案例:
/**
* 先定义结构体,再定义结构体变量(实例)
*/
void fun1()
{// 定义结构体struct A{int a;char b;};// 定义结构体变量struct A x;struct A y;struct A x1, y1;
}/**
* 定义结构体的同时定义结构变量
*/
void fun2()
{struct A{int a;char b;} x,y;struct A Z;struct A x1,y1;
}/**
* 定义结构体的同时定义结构体变量
*/
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;// 结构体变量成员访问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 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;// 结构体变量成员访问printf("%s,%d,%c\n", dog.name, dog.age, dog.sex);// 访问成员方法(函数)dog.eat(dog.name); }/** * 方式2:定义的同时初始化 ---> 结构体变量定义时初始化 */ void fun2() {// 定义结构体变量的同时,给变量成员初始化struct Dog dog = {"旺财", 5,'M', eat};// 修改成员的值dog.name = "金宝";// 访问结构体变量成员printf("%s.%d.%c\n", dog.name, dog.age, dog.sex);// 访问成员方法(函数)dog.eat(dog.name); }int main(int argc,char *argv[]) {fun1();fun2();return 0; }
结构体数组的定义【数组定义】
什么时候需要结构体数组
比如:我们需要管理一个学生对象,只需要定义一个struct Student zhangsan
例如:我们需要管理一个班的学生对象,此时就需要定义一个结构体数组struct Student stus[50];
四种形式定义结构体数组
- 第1种:结构体 → 结构体变量 → 结构体数组,举例:
// 定义一个学生结构体(定义数据类型)
struct Student
{char *name; // 姓名int age; // 年龄float scores[3]; // 三门课程的成绩
};// 定义结构体变量/示例
struct Student zhangsan = {"张三",21, {88,89,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, {88,89,78}};{"李四",22, {66,78,98}}
};
- 第3种:结构体与数组一体化(含初始化),举例:
// 定义一个学生结构体数组,并初始化
struct Student
{char *name; // 姓名int age; // 年龄float scores[3]; // 三门课程的成绩
}stus[3] = {{"张三",21, {88,89,78}};{"李四",22, {66,78,98}}
};
- 第4种:结构体与数组一体化(不含初始化),举例:
// 定义一个学生结构体数组
struct Student
{char *name; // 姓名int age; // 年龄float scores[3]; // 三门课程的成绩}stus[3];// 赋值
suts[0].name = "张三";
suts[0].age = 21;
suts[0].scores[0] = 88;
suts[0].scores[1] = 89;
suts[0].scores[2] = 78;suts[1].name = "李四";
suts[1].age = 22;
suts[1].scores[0] = 66;
suts[1].scores[1] = 78;
suts[1].scores[2] = 98;
结构体数组的访问【数组访问】
语法:
结构体指针 -> 成员名
举例:
// 方式1:普通的指针访问
(*p).成员名
// 方式2:结构体指针访问(结构体指针访问符号 ->),等价与上面写法
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);
}int main()
{// 定义结构体实例并初始化struct Student zhangsan = {1, "张三", 21,{78,88,98}, info};struct Student lisi = {2, "李四", 22,{78,88,98}, info}; // 定义结构体数组并初始化struct Student stus[] = {zhangsan,lisi};// 计算数组的大小int len = sizeof(stus) / sizeof(stus[0]);// 指针法遍历数组struct Student *p = stus;// 表格-表头printf("序号\t姓名\t年龄\t语文\t数学\t英语\t\n")for (; p < stus + len; p++){/* 普通指针的访问,不推荐printf("%d\t%s\t%d\t%.2f\t%.2f\t%.2f\t\n", (*p).id, (*p).name, (*p).age, (*p).scores[0], (*p).scores[1], (*p).scores[2]);(*p).info(*p.name, (*p).age); */// 结构体指针访问printf("%d\t%s\t%d\t%.2f\t%.2f\t%.2f\t\n", p -> id, p -> name, p -> age, p -> scores[0], p -> scores[1], p -> scores[2]);p -> info(p -> name, p -> age); }printf("\n");return 0;
}
注意:当
->
和[]
共存的时候,他们的优先级关系:[]
>->
结构体类型
结构体数组
案例:
- 需求:对候选人得票的统计程序。设有3个候选人,每次输入一个得票的候选人名字,要求最后输出各人得票的结果。
- 代码:
#include <stdio.h>
#include <string.h>/**
* 定义一个候选人结构体
*/
struct Person
{char name[20]; // 名字int count; // 票数
};/**
* 定义候选人数组,并初始化
*/
struct Person persons[] = {{"张三", 0},{"李四", 0},{"王五",0}
};int main()
{// 定义循环变量int i, j;// 创建一个数组,用来接收控制台录入的候选人名字char leader_name[20];// 使用一个for循环,模拟10个人投票for(i = 0; i < 10; i++){printf("请输入您要投票的候选人姓名:\n");scanf("%s, leader_name");// 从候选人列表中匹配被投票的人 +1票for(j = 0; j < sizeof(persons) / sizeof(persons[0]); j++){// 判断两个字符串 strcmpif(strcmp(leader_name, persons[j].name) == 0){persons[j].count++;}}}printf("\n投票结果:\n");// 遍历数组:指针法struct Person *p = persons;for(;p < persons + 3; p++){// printf(" %s:%d\n",(*p).name,(*p).count);printf(" %s:%d\n",p -> name,p -> count);}pritnf("\n");// 遍历数组:指针法for(i = 0; i < 3; i++){printf(" %s:%d\n",(persons + i) -> name, (persons + i) -> count);}pritnf("\n");// 遍历数组:下标法for(i = 0; i < 3; i++){printf(" %s:%d\n",persons[i].name, persons[i].count);}pritnf("\n");return 0;
}
结构体指针
- 定义:结构体类型的指针变量指向结构体变量或者数组的起始地址。
- 语法:
struct 结构体名 *指针变量列表;
- 举例:
struct Dog
{char namr[20];int age;
};struct Dog dog = {"苟富贵", 5};// 基于结构体的结构体指针
struct Dog *p = &dog;
结构体成员的访问
结构体成员访问
-
结构体数组访问结构体成员
-
语法:
结构体数组名 -> 成员名; (*结构体数组名).成员名; // 等价于上面写法
-
举例:
printf("%s:%d\n",persons->name, persons->count);
-
-
结构体成员访问符
.
:左侧是结构体变量,也可以叫做结构体对象访问成员符,右侧是结构体成->
:左侧是结构体指针,也可以叫做结构体指针访问成员符,右侧是结构体成员- 举例:
struct Persin *p = persons; // p机会结构体指针 for(;p < persons + len; p++)printf("%s:%s\n", p->name, p->count);
-
访问结构体成员有两种类型,三种方式:
-
类型1:通过结构体变量(对象|示例)访问成员
struct Stu {int id;char name[20]; }stu; // 结构体变量// 访问成员 stu.namee;
-
类型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, "张三", {90,89,78}},{2, "李四", {90,88,78}},{3, "王五", {77,89,78}}, };// 取数据 --- 下标法 printf("%s,%.2f\n", stus[1].name, stus[1].scores[1]); // 李四 88 // 取数据 --- 指针法 printf("%s,%.2f\n", stus -> name, stus -> scores[2]); // 张三 78 printf("%s,%.2f\n", (stus+1) -> name, (stus+1) -> scores[1]); // 李四 88 printf("%s,%.2f\n", (*(stus+1)).name, (*(stus+1)).scores[1]); // 李四 88
小贴士:
结构体是自定义数据类型,它是数据类型,用法类似于基本类型的int;
结构体数组它是存放结构体对象的数组,类似于int数组存放int数据;
基本类型数组怎么用,结构体数组就怎么用 —>可以遍历,可以作为形式参数,也可以做指针等;
-
-
结构体类型的使用案例
结构体可以作为函数的返回类型、形式参数等。
举例:
#include <stdio.h> #include <string.h>/** * 定义一个结构体(数据类型) */ struct Cat {char *namr; // 姓名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, char* name) {struct Cat *p = cats;for(; p < cats + len; p++){if(strcmp(name, p -> name) == 0) return p;}return NULL; }/** * 结构体指针作为形式参数 */ struct Cat *test4(struct Cat *cats, int len, char* name) {struct Cat *p = cats;for(; p < cats + len; p++){if(strcmp(name, p -> name) == 0) return p;}return NULL; }int main() {// 定义结构类型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"}};// 结构体数组作为参数struct Cat *p1 = test3(cats,3,"汤姆");printf("test2:\n%s,%d,%s\n", p1 -> name, p1 -> age, p1 -> color);// 结构体指针作为参数struct Cat *p2 = test4(cats,3,"汤姆");printf("test2:\n%s,%d,%s\n", p2 -> name, p2 -> age, p2 -> 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字节,偏移0int i; // 4字节,(需对齐到4,填充3字节,偏移4-7)double d; // 8字节(需对其到8,偏移8~15) } // 16字节
struct s2 {double d; char c; int i; } // 16字节
struct s3 {char c; double d; int i; } // 24字节
内存分析:
总结:结构体中,成员的顺序会影响到结构体最终的大小。
-
-
使用
#param pack(1)
#pragma pack(1) struct Sl {char c;// 1字节(偏移0)int i;// 4字节 (偏移1~4)double d;// 8字节 (偏移5~12) }; #pragma pack() //s1 的大小为 13字节
#pragma pack(2) struct Sl {char c;// 1字节(偏移0,填充1字节) 2int i;// 4字节 (偏移2~5) 4double d;// 8字节 (偏移6~13) 8 }; #pragma pack() //s1 的大小为 14字节
#pragma pack(4) struct Sl {char c;// 1字节(偏移0,填充3字节) 4int i;// 4字节 (偏移2~5) 4double d;// 8字节 (偏移6~13) 8 }; #pragma pack() //s1 的大小为 16字节
-
在GNU标准中,可以在定义结构体时,指定对齐规则:
__attribute__((packed)); -- 结构体所占内存大小是所有成员所占内存大小之和 --attribute__((aligned(n))); -- 设置结构体占n个字节,如果n比默认值小,n不起作用;n必须是2的次方
-
案例:
#include <stdio.h>int main(int argc,char *argv[]) {struct cat{char sex_attribute((aligned(2)));//4 2int id;//4char name[20];//20}_attribute_((packed));//结构体所占内存大小是所有成员所占内存大小之和printf("%]d\n",sizeof(struct cat));//默认字节对齐(28)/使用packed后(25)return 0; }
柔性数组
定义:柔性数组不占结构体的大小。
语法:
struct St
{...char arr[0];
}
案例:
#include <stdio.h>
int main(int argc,char *argv[])
{struct Cat{char sex __attribute((aligned(2)));// 1 -- 2int id; // 4char arr[0]; // 0 柔性数组不占用结构体的大小 char name[20]; // 20 } __attribute__((packed)); // 结构体所占内存大小是所有成员所占内存大小之和printf("%ld\n",sizeof(struct Cat));// 默认字节对齐(28)/ 使用packed后(25)/ 使用aligned之后(26)return 0;}
共用体/联合体类型
-
定义:使几个不同变量占用同一段内存的结构,共用体按定义中需要存储空间最大的成员来分配存储单元,其他成员也是使用该空间,他们的首地址是相同。
-
定义格式
union 共用体名称 {数据类型 成员名;数据类型 成员名;... }
-
共用体的定义和机构体类似。
-
可以有名字,也可以匿名
-
共用体在定义时也可以定义共用体变量
-
共用体在定义时也可以初始化成员
-
共用体也可以作为形参和返回值类型使用
-
共用体也可以定义共用体变量
-
…
也就是说,结构体的语法,共用体都支持
-
-
注意:
-
共用体弊大于利,尽量少用,一般很少用:
-
共用体变量在某一时刻只能存储一个数据,并且也只能取出一个数
-
共用体所有成员共享同一内存空间,同一时间只能存储一个值,可能导致数据覆盖
-
共用体和结构体都是自定义数据类型,用法类似于基本数据类型
- 共用体可以是共用体的成员,也可以是结构体的成员
- 结构体可以是结构体的成员,也可以是共用体的成员
-
-
案例:
/** * 定义共用体 */union S {cahr a;float b;int c; }; // S的大小是4字节// 共用体作为共用体成员 union F {char a;union S s; // 4字节 }; // F的大小是4字节// 定义一个结构体 struct H {int a;char b; }; // H的大小是8字节// 结构体作为结构体成员 struct I {int a;int b;struct H h; }; // I的大小是16;// 共用体作为结构体成员 strcut J {int a; // 4char b; // 1 + 3union S s; // 4 }; // J的大小是12void test1() {// 定义一个共用体union Obj{int num;char sex;double score;};// 定义匿名共用体union{int a;char c;} c;// 定义变量union Obj obj;// 存储数据obj.num = 10; // 共用体空间数据:10obj.sex = 'A';// 共用体空间数据:'A' = 65// 运算obj.num += 5; // 共用体空间数据:70 覆盖数据 'F'printf("%lu,%d,%c,%.2lf\n",sizeof(obj),obj.num, obj.sex, obj.score); }int main() {text1();return 0; }
可以是结构体的成员
- 结构体可以是结构体的成员,也可以是共用体的成员
-
案例:
/** * 定义共用体 */union S {cahr a;float b;int c; }; // S的大小是4字节// 共用体作为共用体成员 union F {char a;union S s; // 4字节 }; // F的大小是4字节// 定义一个结构体 struct H {int a;char b; }; // H的大小是8字节// 结构体作为结构体成员 struct I {int a;int b;struct H h; }; // I的大小是16;// 共用体作为结构体成员 strcut J {int a; // 4char b; // 1 + 3union S s; // 4 }; // J的大小是12void test1() {// 定义一个共用体union Obj{int num;char sex;double score;};// 定义匿名共用体union{int a;char c;} c;// 定义变量union Obj obj;// 存储数据obj.num = 10; // 共用体空间数据:10obj.sex = 'A';// 共用体空间数据:'A' = 65// 运算obj.num += 5; // 共用体空间数据:70 覆盖数据 'F'printf("%lu,%d,%c,%.2lf\n",sizeof(obj),obj.num, obj.sex, obj.score); }int main() {text1();return 0; }