文章目录
0.前言
内置类型:
char
short
int
long
long long
float
double
自定义类型
结构体
枚举
联合体
数组
1.结构体
结构体声明
struct tag//结构体标签
{
member-list;//成员列表
}variable-list;//变量列表
struct Stu
{
char name[20];
int age;
char sex[5];
int hight;
}s2,s3,s4;//全局变量,分号不能丢
struct Stu s5;//全局变量
int main()
{
struct Stu s1;//结构体变量,局部变量
return 0;
}
特殊结构体声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
//匿名结构体类型只能用一次,没有标签,无法再次定义了
//在上面代码的基础上,下面的代码合法吗?
p = &x;
警告:
编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的。
结构体自引用
数据结构:数据在内存中的存储结构
链表:数据域和指针域
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少? 无限大了,显然不行
struct Node
{
int data;
struct Node* next;
};//这样才行,通过地址才能找到下一个元素,地址是用指针接收的
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?
//Node的定义先后顺序出现了问题
int main()
{
Node n;//报错
return 0;
}
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;//这个Node只是重命名,不是定义变量
int main()
{
struct Node n2 = {0};//也对
Node n = {0};//正确
return 0;
}
定义和初始化
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体内存对齐
offsetof
size_t offsetof( structName, memberName );
Return Value
offsetof returns the offset in bytes of the specified member from the beginning of its parent data structure. It is undefined for bit fields.
计算偏移量,计算结构体成员相较于起始位置的偏移量
#include<stdio.h>
#include<stddef.h>
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
int main()
{
printf("%d\n", (int)sizeof(struct S1));//12
printf("%d\n", (int)offsetof(struct S1, c1));//0
printf("%d\n", (int)offsetof(struct S1, i));//4
printf("%d\n", (int)offsetof(struct S1, c2));//8
//(int)这样才不会产生警告,sizeof和offsetof返回的都是size_t
return 0;
}
c1和c2下面的3个字节干啥去了?
结构体对齐规则
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
//最大对齐数为4,当前已经占了9个字节,最近的4的倍数是12,因此结构体大小是12
注意:Linux环境没有默认对齐数,Linux环境下,对齐数就是成员自身大小
struct S2
{
char c1;//1
char c2;//1
int i;//4
};
printf("%d\n", sizeof(struct S2));//8
c2的对齐数1,任何数都是1的倍数,因此c2是紧靠着c1放的
i的对齐数是4,从4开始偏移4个字节大小,此时总大小是8刚好是4的倍速,因此结构体大小就是8
仅仅是换了下顺序,结构体大小就变了
![image-20220124160300095](https://i-blog.csdnimg.cn/blog_migrate/6fde3a2642ba35af89c6239f2252aadc.png)
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));//16
struct S3
{
double d;//8
int i;//4
char c;//1
};
printf("%d\n", sizeof(struct S3));//16
这样放结果依旧为16,结构体大小算的最大对齐数的整数倍,最大对齐数是8, double int char 这样排完后,刚好占了13个字节空间,但13不是8的倍数
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));//32
![image-20220124162107607](https://i-blog.csdnimg.cn/blog_migrate/be67536693aa75c81fbee5f6d3d9199c.png)
为什么存在内存对齐?
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
32位机器(32根地址线)一次访问4个字节,如果不对齐,为了得到i,访问了2次,效率降低
总结:
结构体对齐就是拿空间换时间
那在设计结构体的时候,既要满足对齐,又要节省空间:让占用空间小的成员尽量集中在一起(前后没关系)
修改默认对齐数
#include<stdio.h>
#include<stddef.h>
#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;//1 - 1 - 1
int i;//4 - 1 - 1
char c2;//1 - 1 - 1
};
struct S3
{
double d;//1
int i;//1
char c;//1
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", (int)sizeof(struct S1));//6
printf("%d\n", (int)sizeof(struct S3));//13
//(int)这样才不会产生警告,sizeof和offsetof返回的都是size_t
return 0;
}
默认对齐数改成1了,#pragma pack()是成对使用的
结构体传参
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修饰
void print2(const struct S* ps);
结构体传参优先传地址
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
2.位段
#include<stdio.h>
struct A
{
int _a : 2;//a成员只需要2个bit位
int _b : 5;//5个bit位
int _c : 10;//10个bit位
int _d : 30;//30个bit位
};
int main()
{
printf("%d\n", sizeof(struct A));//8
return 0;
}
位段内存分配
位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
struct A
{
//先开4个byte 32bit
int _a : 2;//a成员只需要2个bit位
int _b : 5;//5个bit位
int _c : 10;//10个bit位,剩余15bit
//再开辟4byte
int _d : 30;//30个bit位
};
//位段在不同编译器实现方式不一样
#include<stdio.h>
struct S
{
//1byte 8bit
char a : 3;//剩余5
char b : 4;//剩余1
//再开1byte
char c : 5;//剩余3
//再开1byte
char d : 4;//剩余1
};
int main()
{
printf("%d\n", sizeof(struct S));//3
return 0;
}
//如果每一个bit都不浪费,那么开辟2byte完全够用,但最终是3byte,说明是有浪费的,C语言标准也没有规定是否该浪费,完全取决于编译器
位段在一定程度节省空间,虽然也会有一点浪费,且位段可移植性不好
int _a : 2;
_a只需要2个bit就能完成任务,大大节省空间,4个状态 00 01 10 11
假设表示年龄
strcut S
{
int age:10;//10个比特位完全够了
0111111111 --> 1023
}
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10; //1010,a是3bit只能放进010
s.b = 12; //1100
s.c = 3; //00011
s.d = 4;//0100
但放在一个byte内部的高地址还是低地址处C标准也没规定,全靠编译器决定
00000000 00000000 00000000
01100010 00000011 00000100
62 03 04
通过F10内存窗口可以验证,说明位段一个字节内部从低位开始使用的
这与栈区开辟局部变量不同,栈区中是字节之间的关系,是从高地址到低地址的
位段跨平台问题
int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
位段应用
网络上传输的数据包如果过大,网络就会堵塞
3.枚举
把可能的取值一一列举出来
枚举定义
enum Day//星期
{
//枚举的可能取值
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
int main()
{
enum Day d = Sun;
enum Sex s = SECRET;
printf("%d\n", MALE);//0
printf("%d\n", FEMALE);//1
printf("%d\n", SECRET);//2
return 0;
}
//默认值从0开始,逐渐递增1
//可以随意初始化
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
//其实还是常量,之后不能修改的
typedef enum Sex//性别
{
MALE=4,
FEMALE,
SECRET
}Sex;
int main()
{
enum Day d = Sun;
Sex s = MALE;
printf("%d\n", MALE);//4
printf("%d\n", FEMALE);//5
printf("%d\n", SECRET);//6
printf("%d\n", sizeof(s));//4,其实就是整型大小
return 0;
}
Sex s = 0;
如果这样胡乱赋值,在c里面是能通过编译,但在cpp里面就不行了,cpp检测更加严格
枚举优点
增加代码的可读性和可维护性
和#define定义的标识符比较枚举有类型检查,更加严谨。
防止了命名污染(封装)
便于调试(#define定义的不能调试,#define是替换)
使用方便,一次可以定义多个常量
//使用枚举配合菜单使用,不用再写孤零零的不清楚具体意思的123456
enum Option
{
EXIT,//0
ADD,//1
DEL,//2
SEARCH,//3
MODIFY,//4
SORT,//5
SHOW//6
};
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");
}
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 0:
break;
printf("退出通讯录\n");
default:
printf("选择错误\n");
break;
}
4.联合(共用体)
定义
#include<stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));//4
printf("%p\n", &u);//00AFF9D8
printf("%p\n", &(u.c));//00AFF9D8
printf("%p\n", &(u.i));//00AFF9D8
return 0;
}
//一个联合变量的大小,至少是最大成员的大小
//i和c共用了第一个字节
//i的值改了,c的值也会变
利用好联合体可以一定程度上节省空间
判断当前机器大小端
#include<stdio.h>
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check_sys();
if (1 == ret)
{
printf("小端\n");//结果为1 证明是小端存储
}
else
{
printf("大端\n");
}
return 0;
}
有时候使用4个字节,有时候使用1个字节,可以用联合体写
int check_sys()
{
union//使用匿名联合体,只用一次,避免之后的命名冲突
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
联合体大小
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include<stdio.h>
union Un1
{
char c[5];//1 - 8 ->1 VS默认对齐数8
int i;//4 - 8 ->4
};
union Un2
{
short c[7];//2
int i;//4
};
int main()
{
printf("%d\n", sizeof(union Un1));//8,最大对齐数是4,
//最大成员大小是5,不是4整数倍
printf("%d\n", sizeof(union Un2));//16
//最大成员大小是14,也不是4的整数倍
return 0;
}
练习
通讯录:
- 存放1000个人的信息
- 人的相关信息,名字,年龄,电话,住址,性别
- 增删查改
- 排序
test.c