C语言——结构体,位段,枚举和联合
目录
前言
结构体
1含义
2语法
3匿名结构体
4结构体自引用
5结构体的定义与初始化
6内存对齐
7修改对齐数
8结构体传参
位段
1含义
2位段的内存分配
编辑3位段的问题
4位段的应用
枚举
1含义
2定义
3枚举优点
4枚举使用
联合
1含义
2定义
3特点
4计算
通讯录
前言
C语言的结构体知识学好了的话对后面学习C++类对象就简单很多了
结构体
1含义
结构体是一些值的集合,这些值称为成员变量;每个成员变量可以是不同类型的变量。
2语法
struct tag//类型名
{
member-list;//成员变量
}variable-list;//可在此时定义出结构体类型的变量 它是全局变量
假如我们用结构体来描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}S1;//分号不能丢
3匿名结构体
也就是结构体不完全声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
匿名结构体省略了tag(结构体类型名),而且它只能用一次(声明后立即定义,不能声明后定义)
能否用指针来接收匿名结构体变量?
不能,编译器会认为这个两个类型(vs2019编都编不过,vs2022能编过!)
4结构体自引用
struct Node
{
int data;
struct Node next;
};
int main()
{
printf("%d", sizeof(struct Node));
return 0;
}
上面这种引用行吗?
编译器认为:你在内部定义了同一个结构体成员,那这个成员变量里又包含了同一个结构体成员...这个大小是无穷大的
所以正确的结构体自引用
struct Node
{
int data;
struct Node* next;
};
这种在链表中用的最多
其它的自引用定义:
typedef struct Node
{
int data;
N* next;
}N;
这种写法是错误的:结构体typedef之前内部并不知道
//正确写法
typedef struct Node
{
int data;
struct Node* next;
}N;
5结构体的定义与初始化
struct Poin
{
int x;
int y;
}p1 = {1,2};//可以在这里初始化
struct stu
{
int date;
struct Poin p;
}s1;//全局变量
int main()
{
struct stu s2 = { 100,{20,30} };//局部变量 按顺序初始化
//struct stu s2 = { .p={20,30},.date=100 };//指定成员初始化
printf("%d %d %d\n", s2.date, s2.p.x, s2.p.y);
return 0;
}
6内存对齐
计算结构体的大小:这个非常重要的一个内容!!!
来看下面的一段代码:
struct s1
{
char a;
int b;
char c;
};
struct s2
{
char a;
char c;
int b;
};
int main()
{
printf("s1的大小:%d\n", sizeof(struct s1));
printf("s2的大小:%d", sizeof(struct s2));
return 0;
}
int是4字节,char是1字节,两个int和一个char加起来不是6字节吗?
不同的结构体放着相同的变量,只是顺序不同,为什么大小不一样?
介绍一个宏:offsetof,用来查看结构体变量相对于内存的偏移位置
按照打印出来的内存偏移位置进行安排成员变量的放置
一共9个字节就能结束了,偏偏还要在浪费3个字节,前面也浪费3个字节,为什么? 这时我们来看看内存对齐的规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到对齐数整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
在VS中默认的值为8 而gcc是没有默认对齐数的
3. 结构体总大小为最大对齐数(每个成员变量计算出来的对齐数的最大值)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处
结构体嵌套的结构体大小:
#include<stdio.h>
struct S3//总大小:16
{
double d;//8 8 -> 8
char c; //1 8 -> 1
int i; //4 8 -> 4
};
struct S4
{
char c1; //1 8 -> 1
struct S3 s3;//16 8 -> 8
double d; //8 8 -> 8
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
那为什么存在内存对齐?
1. 平台原因(移植原因):
可能硬件平台只能在某些地址处取某些特定类型的数据(否则硬件中断);
2. 性能原因:
为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问这本质上就是以空间换时间来提高效率!
那在设计结构体时:既要省空间,又要提高效率:
尽量让字节小的成员变量放在一起
#include<stdio.h>
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d %d", sizeof(struct s1), sizeof(struct s2));
return 0;
}
7修改对齐数
如果对应VS的默认对齐数不满意(最好不修改!),可以自己设定对齐数
#pragma pack(1)
//在#pragma的范围内的对齐数是1
struct s3
{
char c1;
int i;
char c2;
};
#pragma()
struct s4
{
char c1;
int i;
char c2;
};
int main()
{
printf("s3=%d s4=%d", sizeof(struct s2), sizeof(struct s3));
return 0;
}
8结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
void print1(struct S s)
{
printf("%d\n", s.num);
}
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
//那种方式更优点?
return 0;
}
关于结构体传参,尽量使用传址传参:
因为一个结构体大小可能很大,如果传值传参会加大系统资源的开销,导致性能的下降;而使用传址传参时数据不希望进行修改,要把const加上!
位段
1含义
位段是一种特殊的自定义类型;
对空间的利用精打细算
a 位段的成员必须是 int、char(整形家族);
b 位段的成员名后边有一个冒号和一个数字
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
与结构体相比大小方面有什么不同吗?
struct A//如果是结构体
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
struct B
{
int _a;
int _b;
int _c;
int _d;
};
int main()
{
printf("A=%d B=%d", sizeof(struct A),sizeof(struct B));
return 0;
}
答案:位段比结构体大小要小,更节省空间了!那这是怎么实现的呢?
2位段的内存分配
位段后面的数字代表要分配多少个bit空间给该成员;
位段的空间是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
3位段的问题
1. int 位段被当成有符号数还是无符号数?
2. 位段中最大位的数目?(16位机器最大16,32位机器最大32,如果写成27,在16位机器会出问题)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配?(在VS时从右向左)
4. 开辟的空间无法容纳下个位段时,是舍弃后另外开辟空间还是利用?(在VS时舍弃剩余的位)
4位段的应用
应用于各种协议(信息传到下一层时需要用到)
枚举
1含义
枚举就是一一列举;在现实生活中把一个星期进行枚举:星期一,星期二,星期三... 那么枚举怎么用代码来表示呢?
2定义
enum Day//星期
{
Mon = 1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Day是一个枚举类型;
枚举括号内的内容都是常量;
枚举默认从0开始(与数组一样);但你也可以自己修改(Mon = 1)
3枚举优点
以上对于星期的枚举:我们也可以用#define Mon 1,#define Tues 2来表示,那是不是就说枚举就可以被替换了?
a 使用枚举方便调试(而#define在调试前就被替换成常量);
b 枚举有类型检查,更严谨写;
b 使用枚举能增加代码的可读性与可维护性;
c 枚举内定义变量不存在命名冲突(而#define可能有命名污染问题)
4枚举使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
return 0;
}
联合
1含义
联合也是一种特殊的自定义类型;
这种类型定义的变量也包含一系列的成员,这些成员公用同一块空间(所以联合也叫共用体)。
2定义
#include<stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
union Un un;
printf("%d\n", sizeof(un));
return 0;
}
3特点
a 由于联合类型共有内存空间,所以比其它的结构体而言节省空间;
b 成员共有内存空间,所以定义变量时成员不能同时出现(后一个会改变前一个的数据)
使用场景1:判断大小端(之前通过地址方式访问判断)
#include<stdio.h>
int sys_call()
{
//匿名联合
union
{
char c;
int i;
}Un;
Un.i = 1;
// 01 00 00 00 还是 00 00 00 01
//(c与i公用1字节的内存空间)
return Un.c;
}
int main()
{
if (sys_call())
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
使用场景2:设计一个礼物清单
struct git_list
{
double price;
char name[20];
char author[20];
int page;
char form[20];
double size;
char flavour[20];
char materials[20];
};
//利用联合体设计清晰且省空间
struct git_list
{
double price;
char name[20];
union
{
char author[20];
int page;
}book;
union
{
char form[20];
double size;
}mug;
union
{
char flavour[20];
char materials[20];
}cooike;
};
4计算
a 联合的大小至少是最大成员的大小;
b 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍;所以:联合大小 != 最大成员大小
#include<stdio.h>
union Un1
{
char c[5];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un1));
return 0;
}
你可能会因为大小是5(char c[5]一共是5字节,比i 4字节大),但实际上:
最大是5没错,但它不是最大对齐数(max(char,int) = 4)的整数倍,进行要进行对齐
通讯录
实现一个通讯录,具备以下功能:
1增加联系人(名字,年龄,性别,电话,地址);
2删除联系人的信息;
3查找联系人的信息;
4更改联系人的信息;
5打印通讯录;
6对通讯录进行排序;
//Contact.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#define Max_Size 100
#define Max_NameSize 20
#define Max_SexSize 5
#define Max_CallSize 20
#define Max_AddrSize 30
enum
{
ext,
add,
del,
check,
modify,
print,
sort,
};
typedef struct Mess
{
char name[Max_NameSize];
int age;
char sex[Max_SexSize];
char call[Max_CallSize];
char addr[Max_AddrSize];
}Mess;
typedef struct Contact
{
int sz;
Mess message[Max_Size];
}Contact;
void Add(Contact* con);
void Del(Contact* con);
void Check(const Contact* con);
void Modify(Contact* con);
void Print(const Contact* con);
void Sort(Contact* con);
//Contact.c
#include"contact.h"
void Add(Contact* con)
{
assert(con);
if (con->sz == Max_Size)
{
printf("Contact Is Full\n");
return;
}
printf("Input Name:");
gets(con->message[con->sz].name);
//scanf("%s", &con->message[con->sz].name);
printf("Input Age:");
scanf("%d", &con->message[con->sz].age);//如果输入的是非法字符串?
getchar();//清空缓冲区的'\n'
printf("Input Sex:");
gets(con->message[con->sz].sex);
//scanf("%s", &con->message[con->sz].sex);
printf("Input Call:");
gets(con->message[con->sz].call);
//scanf("%s", &con->message[con->sz].call);
printf("Input Addr:");
gets(con->message[con->sz].addr);
//scanf("%s", &con->message[con->sz].addr);
con->sz++;
printf("Add Finish\n");
}
void Print(const Contact* con)
{
assert(con);
if (con->sz == 0)
{
printf("Contact Is Empty\n");
return;
}
printf("%-20s %-5s %-5s %-11s %-30s\n", "name", "age", "sex", "call", "addr");
for (int i = 0; i < con->sz; i++)
{
printf("%-20s %-5d %-5s %-11s %-30s\n",
con->message[i].name, con->message[i].age,
con->message[i].sex,con->message[i].call,
con->message[i].addr);
}
}
static int Find(const Contact* con,char Name[])
{
for (int i = 0; i < con->sz; i++)
{
if (strcmp(con->message[i].name,Name)==0)//用strcmp比
{
printf("Find Sucess\n");
return i;
}
}
printf("Find Error\n");
return -1;
}
void Del(Contact* con)
{
assert(con);
char InputName[Max_NameSize];
printf("Input Del Name:");
gets(InputName);
int DelPos = Find(con, InputName);
if (DelPos == -1) return;
for (int i = DelPos; i < con->sz - 1; i++)
{
con->message[i] = con->message[i + 1];//后一个往前一个覆盖
}
con->sz--;
printf("Del Sucess\n");
}
void Check(const Contact* con)
{
assert(con);
printf("Input Check Name:");
char Check_Name[Max_NameSize];
gets(Check_Name);
int pos = Find(con, Check_Name);
if (pos == -1) return;
printf("Check Result:\n");
printf("%-20s %-5s %-5s %-11s %-30s\n", "name", "age", "sex", "call", "addr");
printf("%-20s %-5d %-5s %-11s %-30s\n",
con->message[pos].name, con->message[pos].age,
con->message[pos].sex, con->message[pos].call,
con->message[pos].addr);
}
void Modify(Contact* con)
{
assert(con);
char Modify_Name[Max_NameSize];
printf("Input Modify Name:");
gets(Modify_Name);
int pos = Find(con, Modify_Name);
if (pos == -1) return;
printf("Modify Name:");
gets(con->message[pos].name);
printf("Input Modify Age:");
scanf("%d", &con->message[pos].age);
getchar();
printf("Input Modify Sex:");
gets(con->message[pos].sex);
printf("Input Modify call:");
gets(con->message[pos].call);
printf("Input Modify addr:");
gets(con->message[pos].addr);
printf("Modify Sucess\n");
}
int cmp(const void* a, const void* b)
{
return ((*((Contact*)a)).message->age - (*((Contact*)b)).message->age);
}
void Sort(Contact* con)
{
qsort(con, con->sz, sizeof(con->message[0]), cmp);
printf("排序完成\n");
}
//test.c
#include"contact.h"
void memu()
{
printf("********************************\n");
printf("***** 0.exit 1.add *****\n");
printf("***** 2.del 3.check *****\n");
printf("***** 4.modify 5.print *****\n");
printf("***** 6sort *****\n");
}
int main()
{
Contact con;
//对con空间进行清理
memset(&con, 0, sizeof(con));
int input;
do
{
memu();
printf("Please Select:");
//防止读到字符串时发生死循环
input = 123456;//防止下一次输入的是字符后input还是上一次的值
scanf("%d", &input);
getchar();//输入字符串时:清空缓冲区的'\n'
switch (input)
{
case ext:
printf("Process Exit");
break;
case add:
Add(&con);
break;
case del:
Del(&con);
break;
case check:
Check(&con);
break;
case modify:
Modify(&con);
break;
case print:
Print(&con);
break;
case sort:
Sort(&con);
break;
default:
printf("Select Error,Please Try Again\n");
break;
}
}while(input);
return 0;
}
但上面的通讯录无法做到:
通讯录满了无法再存信息(学习动态内存管理后解决);
关闭程序时通讯录信息还存在(学习完文件操作后解决);
以上便是全部内容,有问题欢迎在评论区指正,感谢观看!