目录
1.结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1.结构体类型的声明
1.1.1.结构的声明
struct tag{member - list ;} variable - list ;
注:
1.tag:结构体标签或结构体名字
2.member-list:成员列表,每一个成员为一个成员变量
3.variable-list:变量列表(变量列表有没有都可以)
结构声明的例子:
#include<stdio.h>
struct Stu
{
char name[20];
char sex[5];
int age;
int height;
}s2,s3,s4; //结构体变量(全局变量)
struct Stu s5; //结构体变量(全局变量)
int main()
{
struct Stu s1 //结构体变量(局部变量)
return 0;
}
1.1.2.特殊的声明(匿名结构体声明)
在声明结构的时候,可以省略结构体名字,这叫做匿名结构体声明(匿名结构体只能使用一次,使用时即创建结构体变量,因为没有名字,后面无法找到该结构体类型)
注:1.匿名结构体声明的话,必须在结构体后面紧跟着定义结构体变量
2.匿名结构体因为没有名字,无法单独在后面定义该结构体变量
匿名结构体声明的例子:
#include<stdio.h>
struct
{
char c;
int a;
double b;
}sa;
int main()
{
return 0;
}
注:
1.如果一个程序里面有两个完全相同的匿名结构体声明,系统也会认为是两种结构体声明,如下面代码所示是错误的(编译器认为ps指针指向的结构体类型和sa结构体类型是两种不同的类型)
#include<stdio.h>
struct
{
char c;
int a;
double b;
}sa;
struct
{
char c;
int a;
double b;
}*ps;
ps=&sa;
int main()
{
return 0;
}
1.2.结构体的自引用和结构体类型重命名
1.2.1.补充数据结构的知识
数据结构:数据在内存中的存储结构
顺序表结构:连续存储
链表结构:找可用空间进行存储,不连续存储,每一个数据存好之后他们之间可以找到(每一个数据在存储的同时还要存储下一个数据存储的地址,这样就可以连起来,最后一个数据存储的同时,存储一个空指针NULL表示结束)
1.2.2.结构体的自引用
错误的自引用方式:
如果用结构体模仿链表的形式,每一次存一个数据并且存下一个存储数据,如下代码所示
struct Node
{
int data;
struct Node next;
}
int main()
{
return 0;
}
注:这种形式是错误的,结构体里面又包含一个自己类型的结构体,其大小无法计算。
正确的自引用方式:
正确的用结构体模仿链表的方式,是上一个结构体成员中存储下一个结构体的地址,如下面代码所示:
struct Node
{
int data;
struct Node* next;
}
int main()
{
return 0;
}
1.2.3.结构体类型重命名
错误的类型重命名代码:
#include<stdio.h>
typedef struct
{
int data;
Node* next;
}Node;
int main()
{
Node n={0};
return 0;
}
注:
1.这段代码的意思是将匿名结构体类型typedef struct{int data;Node* next;}类型重命名为Node
2.这段代码是有问题的,会报错,如下图所示。因为我们对结构体类型重命名为Node的时候,结构体内的类型必须是清晰已知的,必须得先有结构体内的Node类型,才能重命名为Node,这里面先后出现了问题。
3.如果重定义的时候,结构体内用到了我们正在命名的结构体类型名时,我们不能用匿名的方式,正确的方式如下代码所示
正确的类型重命名代码:
#include<stdio.h>
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
Node n={0};
return 0;
}
注:
1.结构体内用到了我们正在定义的结构体类型名时,我们不能用匿名的方式,正确代码如上面代码所示,我们对typedef struct Node{int data;struct Node* next;}类型重命名为Node,结构体内struct Node类型是定义过存在的
2.经过这样重命名后,既可以用前面的struct Node定义结构体变量,也可以用重命名后的Node定义结构体变量
1.3.结构体变量的定义和初始化
1.3.1.结构体变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1,p1为全局变量
struct Point p2; //定义结构体变量p2,p2为全局变量
int main
{
struct Point p3 //定义结构体变量p3,p3为局部变量
return 0;
}
1.3.2.结构体变量的初始化
#include<stdio.h>
typedef struct Node
{
int data;
struct Node* next;
}Node;
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
};
struct Data
{
struct Stu s;
char ch;
double d;
};
int main()
{
struct Node n2 = {100, NULL};
struct Stu s1 = { "zhangsan", "nan", 20, 180 };
struct Data d = { {"lisi", "nv", 30, 166},'w', 3.14};
return 0;
}
1.4.结构体内存对齐
1.4.1.offsetof函数
参数:
structName:结构体名
memberName:成员名
返回:
返回一个偏移量,一个成员在其结构体起始位置的偏移量,偏移量从0开始,偏移量的单位是字节
功能:
计算一个结构体的成员相较于这个结构体起始位置的偏移量
注:
1.使用offsetof需要stddef.h头文件
代码:
#include<stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
运行结果:
1.4.2.结构体内存对齐
1.结构体的第一个成员,存放在结构体变量开始位置的0偏移处
2.从第二个成员开始,都要对齐到对齐数的整数倍偏移量地址处
对齐数:成员自身大小和默认对齐数的较小值
3.结构体的总大小,必须是最大对齐数的整数倍
最大对齐数:所有成员的对齐数中最大的那个
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己成员的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。成员变量为结构体变量的对齐数:该成员变量结构体中的最大对齐数注:默认对齐数在vs编译器中为8,linux环境没有默认对齐数,对齐数就是成员自身的大小
代码1:
#include<stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
运行结果1:
注:
1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示
我们发现,结构体申请了12个字节的空间,但是有6个字节没有用到,这就涉及到结构体内存对齐
2.第一个结构体成员c1存储在偏移量为0的地址处,c1是char类型,占一个字节。
第二个结构体成员i存储在偏移量为4的地址处,因为成员i自身是4字节小于vs默认对齐数8字节,存储在4的整数倍偏移量地址处。
第三个结构体成员c2存储在偏移量为8的地址处,因为成员c2自身是1字节小于vs默认对齐数8字节,存储在1的整数倍偏移量地址处。
3.从0偏移量地址到8偏移量地址9个字节空间,存完了全部的结构体成员。所有结构体成员中最大的对齐数是i成员的4,因此结构体的总大小必须为4的整数倍为12,偏移量地址从0到11。
代码2:
#include<stdio.h>
#include <stddef.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
printf("%d\n", offsetof(struct S2, c1));
printf("%d\n", offsetof(struct S2, c2));
printf("%d\n", offsetof(struct S2, i));
return 0;
}
运行结果2:
注:
1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示
2.第一个结构体成员c1存储在偏移量为0的地址处,c1是char类型,占一个字节。
第二个结构体成员c2存储在偏移量为1的地址处,因为成员c2自身是1字节小于vs默认对齐数8字节,存储在1的整数倍偏移量地址处。
第三个结构体成员i存储在偏移量为4的地址处,因为成员i自身是4字节小于vs默认对齐数8字节,存储在4的整数倍偏移量地址处。
3.从0偏移量地址到7偏移量地址8个字节空间,存完了全部的结构体成员。所有结构体成员中最大的对齐数是i成员的4,因此结构体的总大小必须为4的整数倍为8,偏移量地址从0到7。
4.这里我们看出,改变结构体成员的顺序,节省了4个字节空间,因此适当调整结构体成员顺序可以优化代码。
代码3:
#include<stdio.h>
#include <stddef.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S3));
printf("%d\n", offsetof(struct S3, d));
printf("%d\n", offsetof(struct S3, c));
printf("%d\n", offsetof(struct S3, i));
return 0;
}
运行结果3:
注:
1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示
2.第一个结构体成员d存储在偏移量为0的地址处,d是double类型,占8个字节。
第二个结构体成员c存储在偏移量为8的地址处,因为成员c自身是1字节小于vs默认对齐数8字节,存储在1的整数倍偏移量地址处。
第三个结构体成员i存储在偏移量为12的地址处,因为成员i自身是4字节小于vs默认对齐数8字节,存储在4的整数倍偏移量地址处。
3.从0偏移量地址到15偏移量地址16个字节空间,存完了全部的结构体成员。所有结构体成员中最大的对齐数是d成员的8,因此结构体的总大小必须为8的整数倍为16,偏移量地址从0到15。
代码4:
#include<stdio.h>
#include <stddef.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
printf("%d\n", offsetof(struct S4, c1));
printf("%d\n", offsetof(struct S4, s3));
printf("%d\n", offsetof(struct S4, d));
return 0;
}
运行结果4:
注:
1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示
2.第一个结构体成员c1存储在偏移量为0的地址处,c1是char类型,占1个字节。
第二个结构体成员s3存储在偏移量为8的地址处,因为成员s3自身是一个结构体变量,该结构体变量占16字节,因为如果嵌套了结构体的情况,嵌套的结构体对齐到自己成员的最大对齐数的整数倍处,其自己成员的最大对齐数是8(其成员变量d的对齐数),那么结构体变量s3的对齐数是8,存储在8的整数倍偏移量地址处。
第三个结构体成员d存储在偏移量为24的地址处,因为成员d自身是8字节等于vs默认对齐数8字节,存储在8的整数倍偏移量地址处。
3.从0偏移量地址到31偏移量地址32个字节空间,存完了全部的结构体成员。如果嵌套了结构体的情况,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。c1的对齐数为1,s3和d的对齐数都是8,因此结构体的总大小必须为8的整数倍为32,偏移量地址从0到31。
1.4.3.为什么存在结构体内存对齐
1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因:数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。![]()
32位机读取地址是每四个字节进行读取。如果没有对齐,要读取i时需要读取2次(第一次读取了i的前三个字节,第二次读取了i的最后一个字节);如果对齐了,要读取i时,i的四个字节1次就可以读取完。对齐之后读取效率更高,。
总体来说: 结构体的内存对齐是拿 空间来换取 时间的做法。
1.4.4.高效率设计结构体
在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。
#include<stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
运行结果1:
代码2:
#include<stdio.h>
#include <stddef.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
printf("%d\n", offsetof(struct S2, c1));
printf("%d\n", offsetof(struct S2, c2));
printf("%d\n", offsetof(struct S2, i));
return 0;
}
运行结果2:
注:
1.如上面两个代码进行对比,将占用空间较小的结构体成员放在一起,结构体占用的空间较少
2.结构体我们必须要自己设计好,做到高效率,release版本并不会帮我们改变结构体成员顺序,不会对结构体成员顺序进行优化
1.4.5.修改默认对齐数
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。(一般修改的值为)
代码1:
#include<stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
运行结果1
代码1:(将默认对齐数改成1):
#include<stdio.h>
#include <stddef.h>
#pragma pack(1)
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
运行结果1:(将默认对齐数改成1):
注:
1.#pragma pack(1)是将默认对齐数设置成1
2.#pragma pack()是取消掉对默认对齐数的设置
3.将默认对齐数改成1,那么所有结构体成员的对齐数将都为1,所有结构体成员存储在1的整数倍偏移量地址处,也就是说结构体成员将逐字节进行存储,存完所有成员,结构体占6个字节,结构体最大对齐数为1,6是1的倍数,因此结构体占6字节,下面是修改前后内存占用情况
修改前 | 偏移量 | 修改后 | 偏移量 | |
c1 | 0 | c1 | 0 | |
1 | i | 1 | ||
2 | 2 | |||
3 | 3 | |||
i | 4 | 4 | ||
5 | c2 | 5 | ||
6 | 6 | |||
7 | 7 | |||
c2 | 8 | 8 | ||
9 | 9 | |||
10 | 10 | |||
11 | 11 | |||
12 | 12 | |||
13 | 13 |
代码2:
#include<stdio.h>
#include <stddef.h>
struct S
{
char c1;
double d;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
运行结果2:
代码2(将默认对齐数改成4):
#include<stdio.h>
#include <stddef.h>
#pragma pack(4)
struct S
{
char c1;
double d;
char c2;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
运行结果2(将默认对齐数改成4):
注:修改前后内存占用情况如下表所示
修改前 | 偏移量 | 修改后 | 偏移量 | |
c1 | 0 | c1 | 0 | |
1 | 1 | |||
2 | 2 | |||
3 | 3 | |||
4 | c1 | 4 | ||
5 | 5 | |||
6 | 6 | |||
7 | 7 | |||
d | 8 | 8 | ||
9 | 9 | |||
10 | 10 | |||
11 | 11 | |||
12 | c2 | 12 | ||
13 | 13 | |||
14 | 14 | |||
15 | 15 | |||
c2 | 16 | 16 | ||
17 | 17 | |||
18 | 18 | |||
19 | 19 | |||
20 | 20 | |||
21 | 21 | |||
22 | 22 | |||
23 | 23 | |||
24 | 24 |
1.5.结构体传参
结构体传值代码:
#include<stdio.h>
#include <stddef.h>
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);
}
int main()
{
print1(s);
return 0;
}
结构体传址代码:
#include<stdio.h>
#include <stddef.h>
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print2(&s);
return 0;
}
注:
1.结构体传参的时候,要传结构体的地址,因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
2.结构体传参如果传值,形参是对结构体的临时拷贝,将无法对结构体变量进行修改
结构体传参如果传址,将可以对结构体变量进行修改,如果不想对结构体变量进行更改,形参使用const放在*的左边,对其进行保护即可。
可以看出在功能方面,传址调用功能包含传值调用,因此尽量使用传址调用
1.6.结构体练习:通讯录
通讯录:
1.可以存放1000个人的信息
2.人的信息:名字、年龄、性别、电话、住址
功能:
1.增加联系人(完成)
2.删除联系人(完成)
3.查找联系人(先不完成)
4.修改联系人(先不完成)
5.排序(名字/年龄)(先不完成)
6.展示所有人信息(完成)
0.退出(完成)
test.c代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
enum Oprion
{
EXIT,//0
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
SHOW
};
void menu()
{
printf("***************************************\n");
printf("******** 1.add 2.del *****\n");
printf("******** 3.search 4.modify *****\n");
printf("******** 5.sort 6.show *****\n");
printf("******** 0.exit *****\n");
printf("***************************************\n");
}
int main()
{
int input = 0;
Contact con = { 0 };//通讯录
//初始化通讯录
InitContact(&con);
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DeleteContact(&con);
break;
case SEARCH:
break;
case MODIFY:
break;
case SORT:
break;
case SHOW:
ShowContact(&con);
break;
case EXIT:
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
contact.h代码:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define ADDR_MAX 30
#define TELE_MAX 12
typedef struct PeoInfo
{
char name[NAME_MAX];
int age;
char sex[SEX_MAX];
char addr[ADDR_MAX];
char tele[TELE_MAX];
}PeoInfo;
//通讯录的结构体
typedef struct Contact
{
PeoInfo data[MAX];//存放数据
int sz;//通讯录中有效信息的个数
}Contact;
//初始化通讯录
void InitContact(Contact* pc);
//增加联系人到通讯录
void AddContact(Contact* pc);
//打印通讯录中的信息
void ShowContact(const Contact* pc);
void DeleteContact(Contact* pc);
contact.c代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
void InitContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
memset(pc->data, 0, sizeof(pc->data));
}
void AddContact(Contact* pc)
{
assert(pc);
if (pc->sz == MAX)
{
printf("通讯录满了,无法添加\n");
return;
}
//输入联系人
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("增加联系人成功\n");
}
void ShowContact(const Contact* pc)
{
assert(pc);
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
for (i = 0; i < pc->sz; i++)
{
printf("%-10s\t%-5d\t%-5s\t%-13s\t%-20s\n",
pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
}
}
int FindByName(const Contact* pc, char name[])
{
int i = 0;
for (i = 0; i < pc->sz; i++)
{
if (strcmp(pc->data[i].name, name) == 0)
{
return i;
}
}
return -1;//找不到
}
void DeleteContact(Contact* pc)
{
char name[NAME_MAX] = { 0 };
if (pc->sz == 0)
{
printf("通讯录为空,无法删除\n");
return;
}
printf("请输入要删除人的名字:>");
scanf("%s", name);
//查找指定联系人
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要删除的人不存在\n");
return;
}
else
{
//删除
int j = 0;
for (j = pos; j < pc->sz - 1; j++)
{
pc->data[j] = pc->data[j + 1];
}
pc->sz--;
printf("删除指定联系人成功\n");
}
}
2.位段
2.1.位段的概念
位段的声明和结构是类似的,有两个不同:1.位段的成员必须是 int、unsigned int 、signed int 或char(属于整型家族)2.位段的成员名后边有一个冒号和一个数字注:位段相比于结构体在一定程度上节省了空间(也存在一些空间浪费)
位段的例子:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
代码:
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
运行结果:
注:
1.位段的位代表二进制位,代码 int _a : 2表示的意思是_a只占2个比特位,代码 int _b : 5表示的意思是_b只占5个比特位,依次类推。
2.2.位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型2. 位段的空间上是按照需要以 4 个字节( int )或者 1 个字节( char )的方式来开辟的。3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
代码1:
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
运行结果1:
注:
1.遇到代码int _a:2代码首先开辟4个字节32比特位;int _a : 2需要2个比特位,还剩30个比特位;int _b : 5需要5个比特位,还剩25个比特位;int _c : 10需要10个比特位,还剩15个比特位;int _d : 30需要30个比特位,此时剩余了15个比特位不够了,因此再开辟4个字节32比特位;因此总共开辟8个字节
2.位段涉及不确定因素,不要跨平台使用,比如上面代码运行到int _d : 30时,需要30个比特位,此时只剩下15个比特位,因此再开辟4个字节32比特位,此时开辟的32比特位已经够存_d了,但是存储到底是第一次开辟剩余的15比特位+第二次开辟32比特位中15比特位还是全部存在第二次开辟的32比特位中30比特位是不确定的,因为c语言标准未规定,完全取决于不同编译器的实现
代码2:
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
运行结果2:
注:
1.遇到代码char a : 3代码首先开辟1个字节8比特位;char a : 3需要3个比特位,还剩5个比特位;char b : 4需要4个比特位,还剩1个比特位;char c : 5需要5个比特位,此时剩余了1个比特位不够了,因此再开辟一个字节8比特位,此时分为两种情况:
第一种情况(剩余不够不再使用):第一次开辟剩余的1个比特位不用了,使用第二次开辟的8比特位中5比特位,还剩3比特位;char d : 4需要4比特位,此时剩余3比特位不够,再开辟1字节8比特位,前面第二次开辟剩余的3比特位不用了,使用第三次开辟的8比特位中4比特位;这种情况总共开辟3个字节
第二种情况(剩余不够继续使用):第一次开辟剩余的1个比特位继续使用,并且还需要第二次开辟的8比特位中4比特位,第二次开辟的还剩4比特位;char d : 4需要4比特位,刚好用完;这种情况总共开辟2个字节
2.根据运行结果,我们可以推测vs编译器是以第一种情况进行存储的
代码3:
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;
}
内存中数据显示:
注:
1.假设一个字节中,是从低位往高位进行存储的(c语言标准中没有规定),也就是从右往左进行存储
s.a = 10,10的二进制位为1010,而a只开辟3个比特位,因此a的3个比特位为010,并且从右往左进行存储,如下表所示
00000010 | 00000000 | 00000000 |
s.b = 12,12的二进制位为1100,而b开辟4个比特位,因此b的4比特位为1100,并且接着前一次从右往左进行存储,如下表所示
01100010 | 00000000 | 00000000 |
s.c = 3,3的二进制位为011,而c开辟5个比特位,因此c的5个比特位为00011,根据前面的推测应该在第二次开辟的一个字节中从右往左的5个比特位进行存储,如下表所示
01100010 | 00000011 | 00000000 |
s.d = 4,4的二进制位为100,而d开辟4个比特位,因此d的四个比特位为0100,根据前面的推测应该在第三次开辟的一个字节中从右往左的4个比特位进行存储,如下表所示
01100010 | 00000011 | 00000100 |
因为内存数据显示出来为16进制的,此时这三个字节显示的数值应该是62 03 04,与上面内存中显示数据完全相同
2.我们可以得到位段的结论:(在vs编译器下成立)
(1)若一次申请中存储完数据后剩下的比特位不够进行存储时,此次申请剩下的比特位会浪费 掉,重新申请一次,再进行存储
(2)开辟的一个字节中空间使用是从低位到高位进行存储的,也就是从右往左进行存储
2.3.位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的2. 位段中最大位的数目不能确定。(16位机器int最大16,32/64位机器int最大32,写成27,在16位机器会出问题)3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
2.4.位段的应用
注:用位段设置数据包格式,使数据包尽可能小(比如将版本号设置为4比特,如果不用位段,最小的char类型也需要8比特)
3.枚举
枚举顾名思义就是一一列举。把可能的取值一一列举。比如我们现实生活中:一周的星期一到星期日是有限的7天,可以一一列举。性别有:男、女、保密,也可以一一列举。月份有12个月,也可以一一列举这里就可以使用枚举了。
3.1.枚举类型的定义
代码1:
#include<stdio.h>
enum Day//星期
{
//枚举的可能取值
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
int main()
{
enum Day d = Sun;
enum Sex s = SECRET;
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
运行结果1:
代码2:
#include<stdio.h>
enum Day//星期
{
//枚举的可能取值
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE=4,
FEMALE=8,
SECRET
};
int main()
{
enum Day d = Sun;
enum Sex s = SECRET;
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
运行结果2:
注:
1.以上定义的 enum Day , enum Sex 都是枚举类型。{ }中的内容是枚举类型的可能取值,也叫枚举常量 。 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
2.若枚举常量没有赋值,会默认赋值前一个枚举常量的值+1,如代码2所示
3.必须使用枚举常量给枚举变量赋值,这样才不会出现类型的差异,如下图所示,clr是枚举变量,将5赋值给clr是有问题的
3.2.枚举类型重命名
#include<stdio.h>
typedef enum Sex
{
MALE=4,
FEMALE=8,
SECRET
}Sex;
int main()
{
Sex s2 = MALE;
return 0;
}
3.3.枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:1. 增加代码的可读性和可维护性2. 和 #define 定义的标识符比较,枚举有类型检查,更加严谨(#define定义的标识符没有类型,其就是一个符号,运行时仅进行替换)3. 防止了命名污染(封装):枚举是将枚举常量这些符号放在大括号内封装起来了;#define定义的标识符散落在外面4. 便于调试:枚举定义的枚举常量是可以进行调试的;#define定义的常量无法进行调试,因为在调试的时候已经进行了替换5. 使用方便,一次可以定义多个常量
4.联合(共用体)
4.1.联合类型的定义
联合也是一种特殊的自定义类型这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
#include<stdio.h>
union Un
{
char c;//1
int i;//4
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
return 0;
}
运行结果1:
注:
1.联合体变量u的地址、u里字符成员c的地址、u里整型成员i的地址相同,可以看出,成员变量c和i共用了第一个字节,所以叫做共用体。
2.当给c赋值的时候i的值会改变;当给i赋值的时候c的值也会改变,因此联合体成员同一时间只能用一个
3.联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
代码2:
#include<stdio.h>
union Un
{
char c;//1
int i;//4
};
int main()
{
union Un u = {0};
u.c = 'w';
u.i = 0x11223344;
return 0;
}
运行调试结果2:
4.2.联合大小的计算
联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
代码1:
#include<stdio.h>
union Un1
{
char c[5];//5 1,8 - 1
int i; //4 4,8 - 4
};
int main()
{
printf("%d\n", sizeof(union Un1));
return 0;
}
运行代码1:
注:数组c,占5个字节,其每个元素都是char类型,因此其自身大小为1,默认对齐数为8,较小值为1,因此其对齐数为1;整型变量i,占4个字节,其自身大小为4,默认对齐数为8,较小值为4,因此其对齐数为4;因为最大成员大小数组c占5个字节,而最大对齐数为4,不是最大对齐数的整数倍,因此要对齐到最大对齐数的整数倍为8
代码2:
#include<stdio.h>
union Un2
{
short c[7];//14 2,8-2
int i; //4 4,8-4
};
int main()
{
printf("%d\n", sizeof(union Un2));
return 0;
}
运行代码2:
注:数组c,占14个字节,其每个元素都是short类型,因此其自身大小为2,默认对齐数为8,较小值为2,因此其对齐数为2;整型变量i,占4个字节,其自身大小为4,默认对齐数为8,较小值为4,因此其对齐数为4;因为最大成员大小数组c占14个字节,而最大对齐数为4,不是最大对齐数的整数倍,因此要对齐到最大对齐数的整数倍为16
4.3.练习
判断当前计算机的大小端存储
代码1:
#include<stdio.h>
int cheak_sys()
{
int a = 1;
if (*(char*)&a == 1)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
printf("小端\n");
else
printf("大端\n");
//如果返回1,表示小端
//如果返回0,表示大端
return 0;
}
代码2:(代码1改进)
#include<stdio.h>
int cheak_sys()
{
int a = 1;
return (*(char*)&a);
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
printf("小端\n");
else
printf("大端\n");
//如果返回1,表示小端
//如果返回0,表示大端
return 0;
}
代码3:(使用联合体)
#include<stdio.h>
int cheak_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
printf("小端\n");
else
printf("大端\n");
//如果返回1,表示小端
//如果返回0,表示大端
return 0;
}
代码4:(使用匿名联合体)
#include<stdio.h>
int cheak_sys()
{
union
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
printf("小端\n");
else
printf("大端\n");
//如果返回1,表示小端
//如果返回0,表示大端
return 0;
}