自定义类型
- 结构体
- 枚举
- 联合体
结构体
- 结构体关键字
- 结构体标签
- 成员列表里每一项都是成员变量(任意类型)
- 变量列表
定义变量
定义变量有以下三种方式:
//结构体定义变量的三种方式
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score[3];
}s1 = { 0 };//方式一:这里设置的是全局变量
struct Stu s2 = { 0 };//方式二:这里设置的是全局变量
int main()
{
struct Stu s3 = { 0 };//方式三:这里设置的是局部变量
return 0;
}
Tip:跟在结构体后面创建的全局变量后一定要在创建完变量后加分号喔~
Tip:结构体变量不可以重定义,只能将里面的数据定义一次,不可以重新赋值。
特殊声明
匿名结构体声明
定义结构体时,可以不完全声明,这种类型的结构体叫匿名结构体,而这种结构体只能使用一次。
匿名结构体类型的声明:
//匿名结构体的声明与定义变量
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct //注意,这里是没有定义结构体名的,这种就为匿名结构体
{
char arr[10];
int str;
}str1={ "abcdefghij",8 }, str2 = { 0 }, str3 = { 0 };//只能紧跟在匿名结构体后面定义变量,否则后面在main函数内或者
//其他地方处是无法找回这种结构体类型的,因为没有名字无法找到该结构体类型。
int main()
{
//str1 = { "abcdefghij",8 }; //匿名结构体是无法在离开结构体的其他位置初始化的,因为离开之后根本找不到位置去作相对于的初始化,所以只能紧挨着匿名结构体来定义变量
printf("%s\n", str1.arr);
return 0;
}
Tip:匿名结构体是无法在离开结构体的其他位置初始化的,因为离开之后根本找不到位置去作相对于的初始化,所以只能紧挨着匿名结构体来定义变量
Q:匿名结构体指针能否装下匿名结构体类型?
A:不行。编译器会认定为两种不同的结构体类型,所以为非法写法,有问题。
结构体自引用
数据结构
数据在内存中存储的结构
- 顺序表:像数组一样的东西,然后进行增删查改就是顺序表的数据结构
- 链表:也是一种数据结构,存数据不是连续存的,在把每个数据找到之后可以由一个依次找到下一个,Q:如何查找?A:得知道地址,要存下一位元素的地址,最后一个元素存NULL,当找到第一个元素的时候就可以把其他找到了。
- 栈、队列、二叉树...
顺序表和链表都是线性数据结构。
当我们想要实现结构体的自引用时:
要结构体不能包括自己类型的结构体,不然会不知道结构体的大小,会无限计算下去,会error。所以要实现结构体的自引用,我们要使用到链表。
而在链表之中,有一个很重要的东西叫结点。
若想实现链表,要有能力得描述一个结点,那么如何描述这个结点呢?
所以只需要存一个指针变量,指向下一个结构体类型。
结构体自引用
struct Node
{
int data;
struct Node* next;//此时的next就为结点
};
Q:创建新类型把匿名结构体定义为Node然后再使用结点可行吗?
A:不行,error,因为每一次使用时,得先创造这个新类型,才有Node出现
若要使用创建新类型,一定不能用匿名结构体。
typedef struct Node
{
int data;
struct Node* next;
}Node;
如果用typedef定义的struct Node定义觉得每次加上struct麻烦也可以不加上。自己用Node定义新的结构体变量。
int main()
{
struct Node n2={0};
Node n={0};
return 0;
}
关于typedef是否使用,有以下两种观点:
建议不要用typedef,因为一眼看上去不知道这是个结构体,可读性差。
建议使用typedef,因为用起来方便。
结构体变量的定义与初始化
定义的三种方法:(之前写过了)
定义的同时初始化:就是在定义变量的同时赋值
结构体嵌套初始化:
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
};
struct Data
{
struct Stu s;
char ch;
double d;
};
int main()
{
strcut Data d = {{"lisi","nv",30,166},'w',3.14};
return 0;
}
在了解了结构体是怎样定义变量之后,再了解一下结构体的大小是如何计算的吧~
结构体内存对齐
在认识内存对齐前,我们先了解一个东西——offsetof
offsetof——宏
这个函数是用来求出成员变量的偏移量的,计算的是成员在结构体离开辟内存空间的起始位置的偏移量。
就拿:
Struct node {int data; double a; char c ;};
来通过图解来说明偏移量的概念。
再次假设有这样一个结构体:
struct s1
{
char c1;//1
int i;//4
char c2;//1
};
当我们使用sizeof来计算大小的时候,最终输出的结果为12,在了解大小和用offsetof得出各成员的偏移量后,我们能够画出如下图中各成员所占的位置,我们能够发现,在开辟的一共12个字节里,我们所有成员的大小加起来拢共也就6个字节,为什么会出现内存浪费的情况出现?这个就有关结构体的内存对齐相关了。
结构体内存对齐
有下面这样的结构体:
struct Stu
{
char a1;
int i;
char a2;
};
如何计算?
对齐规则:
- 结构体第一个成员放在结构体变量开始处的0偏移量地址处处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数:成员自身大小和默认对齐数的较小值。(ps:在vs中的默认对齐数为8)
3.结构体的总大小必须是最大对齐数的整数倍。
最大对齐数:所有成员对齐数中最大的那个。
Linux环境没有对齐数。
4.如果嵌套类结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么存在内存对齐?
1、平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台 只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次的内存访问;而对齐的内存访问仅需要一次内存访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
怎么做到即节省空间又可以内存对齐?
尽量让占用内存小的空间集中在一起。
修改默认对齐数
用#pragma这个预处理指令,
#prgma pack(1)
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack();
int main()
{
printf("%d\n",sizeof(struct S1));
}
Tips:使用#pragma pack(1)相当于取消了内存对齐。
结构体传参
结构体传参分为传值和传址两方面:
//写结构体传参和传址
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S1
{
char arr[10];
int j;
};
struct S2
{
char p[10];
int y;
};
void print1(struct S1 s1)
{
printf("%s\n", s1.arr);
}
void print2(struct S2* s2)
{
printf("%s\n", s2->p);
}
int main()
{
struct S1 s1 = { "Green",6 };
struct S2 s2 = { "Xiaolv",8 };
print1(s1);//传结构体
print2(&s2);//传地址
return 0;
}
Q:相较之下,传值好还是传址好?
A:其实相对来说,传址是一个更好的选择,因为函数传参时,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。
位段
结构体的一种特殊实现
位段成员必须位int、unsigned int或signed int
位段成员后有一个冒号和一个数字。
struct A
{
int _a:2;
int _b :6;
};
后面的数字:比特位
位段的内存分配
- 位段成员:整形家族
- 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可以指的程序应该避免使用位段(很多细节没有确定)
(当比特位不够时,就开辟新的比特位)
位段只是一定程度上节省空间,但也有一些地方会浪费
位段的跨平台问题
int位段不知道是有符号处理还是无符号处理
int位段中的最大数不确定(16位机器最大为16,32位机器最大32,写成27,在16位机器会出问题)(在早期的16位机器上,sizeof(int)——16bit、32、64位机器sizeof(int)——32bit)
位段成员在内存中是从右向左分配还是从左向右分配这个是不确定的。
不够用时,不确定要不要用之前剩下的位段。
总结:相比结构体,位段也可以很好节省空间,但是有不能跨平台问题的情况存在
枚举
枚举顾名思义——列举
枚举类型的定义
enum Day
{
//枚举的可能取值
Mon,//0 //当我们没有定义Mon的值时,默认为0,然后往下成员依次增大
Tues,//1
Web,//2
Thir,*//3
Fri,
Sta,
Sun
};
int main()
{
enum Day d=Sun;
return 0;
}
当成员变量没有赋初值时会按照012....的顺序往下赋值。
enum里面的值都是常量。
一个枚举变量因为对应的是整型,所以大小是4个字节。
枚举的优点
- 增加可维护性和可读性
- 和#define 定义的标识符比较枚举有类型检查,更加严谨
- 防止命名污染
- 便于调试(因为我们在定义宏的时候编译器会先做预处理,运行时与我们看到的其实不一样)
- 使用方便,可以一次使用多个常量
联合(共用体)
联合类型的定义
联合体的定义和结构体使用起来差不多。
union Un1
{
char c[5];
int i;
};
联合体大小的计算
- 联合的大小至少是最大成员的大小
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
例如:
union Un1
{
char c[5];//这里是一共五个字节大小,但单个还是一个字节大小,所以对齐数还是要以1来算
int i;
};
最终大小为8个字节。