C语言中自定义类型
- 结构体
- 结构体变量的创建和初始化
- 结构体传参
- 结构体内存对齐(如何存储)
- 联合体(共用体)
- 联合体创建和初始化
- 联合体大小(如何存储)
- 枚举类型
- 枚举类型创建
- 枚举类型初始化
- 枚举的优点(相较于define)
前言
C语言中有内置类型和自定义类型,内置类型就像int 、double等等,其自带的一些类型,但有时候这些类型满足不了一些特定的要求,所以C语言也提供了一些自定义类型:结构体、联合、枚举
结构体
struct tag
{//结构体成员变量member-list;
}variable-list;//结构体变量名称
//可以在使用时候创建,也可以在创建结构体时创建
例如:创建一个学生的结构体
struct Stu {char name[20];int age;char sex[5];
}; //这里的分号不可以省略
这上面就相当于一个结构体的声明,创建了一个结构体类型,那如何使用呢?
结构体变量的创建和初始化
可以创建的同时初始化
struct Stu {char name[20];int age;char sex[5];
};int main()
{
//创建结构体类型变量并初始化struct Stu s = { "张三",18,"male" };struct Stu s1 = {"lisi",18,"male"};
}
#include<stdio.h>
struct Stu {char name[20];int age;char sex[5];
};int main()
{
//创建一个结构体变量并初始化struct Stu s = { "张三",18,"male" };printf("%s\n", s.name);printf("%d\n", s.age);printf("%s\n", s.sex);//可以使用指针来指向这个结构体,来进行访问struct Stu* p = &s;printf("%d\n", (*p).age);printf("%s\n", p->name);return 0;
}
这里使用变量名.结构体成员来访问
如果是一个指针指向一个结构体的话,有两种访问形式
变量名->成员变量
(*变量名).结构体成员
运行结果如下
结构体传参
结构体传参分为传值调用和传址调用
#include<stdio.h>
struct Stu {int age;char name[20];
};
//传值
void print1(struct Stu p)
{printf("%d\n", p.age);
}
//传址
void print2(struct Stu* p)
{printf("%d\n", (*p).age);
}
int main()
{struct Stu s = { 18,"sansan" };print1(s);print2(&s);return 0;
}
运行结果如下
传址调用可以修改结构体变量的值,但是传值调用则不可以
#include<stdio.h>
struct Stu {int age;char name[20];
};
//传值
void print1(struct Stu p)
{p.age = 19;
}
//传址
void print2(struct Stu* p)
{(*p).age = 20;
}
int main()
{struct Stu s = { 18,"sansan" };print1(s);printf("调用传值调用后age:%d\n", s.age);print2(&s);printf("调用传址调用后age:%d\n", s.age);return 0;
}
这里我们通过传值调用和传地址调用,发现这里的传地址调用可以修改结构体的变量的值,但是传值调用则不可以
结构体在进行传参的时候,传递地址比较好,因为如果传值调用的话,形参是实参的一份临时拷贝,如果这个结构体变量非常大,这可能在是将和空间上的开销比较大,所以传地址调用更好一点
结构体内存对齐(如何存储)
上面我们已经对结构体有所了解了,但是这有个新问题,结构体存储的时候是占多少字节呢?这就引出了:结构体内存对齐
对齐规则
1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2.剩下的成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
这里的对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩整数倍的较⼩值。
-VS 编译器中默认的值为 8
-Linux系统中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
4.如果嵌套了其他结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,这个结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍
也就是要考虑结构体变量以及嵌套结构体变量其中的最大对齐数
我们就以下面这两个结构体来举例
#include<stdio.h>
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{//计算S1与S2分别的字节大小printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));return 0;
}
运行结果如下
从运行结果我们发现一个问题这里的结构体变量的大小,并不是简单的成员变量所占字节数相加,而是经过内存对齐得出的结果
S1 和 S2结构体带下分析如下
这里的两个结构体S1大小为12,S2大小8
嵌套类型的结构体总大小计算
#include<stdio.h>
struct S3
{double d;char c;int i;
};//结构体嵌套问题
struct S4
{char c1;struct S3 s3;double d;
};
int main()
{printf("%zd\n", sizeof(struct S3));printf("%zd\n", sizeof(struct S4));return 0;
}
这里我们在VS2022这个编译器下,分析的结果是结构体S3的总大小是16,S4的总大小是32
运行结果如下
和我们推测一样
为什么要有这种对齐规则呢
1.平台原因(硬件原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,
否则出现硬件异常
2.性能原因
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬,原因是这种访问需要访问两次,假设一个编译器只能8个地
地址进行存储,这样我们每次保证存储是8的倍数,这样就方便访问,提高效率
3.我们可以发现上面结构体就是按照对齐规则存储,这是为了快捷访问,相当于牺牲了一部分空间来换取时间
但是我们可以减少浪费
就像上面的两个结构体S1和S2,虽然成员变量的一样,但位置不同结果也不同,我们可以让占用小的空间尽量集中一起,这样可以节省空间
我们这里的VS2022有自己对齐规则,我们可以修改其 默认的对齐数,来改变其大小
#pragma 这个预处理指令,可以改变编译器的默认对⻬数
#include<stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{char c1;int i;char c2;
};int main()
{//输出的结果是什么? printf("%zd\n", sizeof(struct S));return 0;
}
我们如果按照VS编译器的话,这里的大小是12,但是我们这里设置默认对齐数为1,也就是每个变量是紧挨着存储,所以这里大小是其相加也就是6
联合体(共用体)
联合体创建和初始化
联合体和结构体类似,都是由多个成员组成,这些成员可以是不同类型
但是与结构体不同的是,联合体是所有成员共用同一块内存,所以联合体也叫做共用体
union tag
{//成员变量member-list;
}variable-list;
创建了一个共用体类型
#include<stdio.h>
union Un
{char c1;int i;char c2;
};
共用体创建和初始化
以及共用体是所有成员占用一块内存吗?我们以下面这个代码来举例
union Un
{char c1;int i;char c2;
};
int main()
{union Un un = { 0 };printf("%zd\n", sizeof(un));return 0;
}
运行结果如下
从结果上可以看出,它并不是连续存储,也不像结构体那样根据对齐原则存储,那他是如何存储的呢
上面的联合体的大小是4个字节,因为这里的最大成员变量是int 占4个字节,并且是最大对齐的数4的整数倍
联合体大小(如何存储)
联合体大小计算
联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍
那我们来看下面的代码
#include<stdio.h>
union Un1
{char c[5];int i;
};
union Un2
{short c[7];int i;
};
int main()
{//下⾯输出的结果是什么? //最大的是5,但又要是最大对齐数4的整数倍数,所以这里是8printf("%zd\n", sizeof(union Un1));//最大是14,但又要是最大对齐数4的整数倍,所以结果是16printf("%zd\n", sizeof(union Un2));return 0;
}
运行结果如下
他们真的是占用同一块内存吗?
#include<stdio.h>
union Un
{char c1;int i;char c2;
};
int main()
{union Un un = { 0 };//%p是打印地址的,我们看他们是不是真的存储在同一内存printf("%p\n", &(un.c1));printf("%p\n", &(un.i));printf("%p\n", &(un.c2));return 0;
}
运行结果如下
从这个结果可以看出,共用体它们所占用的是同一个内存
这里如果修改一个成员数值有成员的结果就会改变
#include<stdio.h>
union Un
{char c1;int i;char c2;
};
int main()
{union Un un = { 0 };//%p是打印地址的,我们看他们是不是真的存储在同一内存un.i = 200;printf("%d\n", un.c1);printf("%d\n", un.c2);printf("%d\n", un.i);return 0;
}
char类型的范围是-128 ~ 127,如果超过范围结果是怎样呢
虽然它们占用同一块内存,但所占的字节数不同,这也导致结果不一样,就像这里的char只会占1个字节8个比特位,而int占4个字节,32比特位,所以这里的char类型打印的和int类型数值不同
枚举类型
枚举顾名思义就是将其成员一一列举
就像生活中一年有12个月,一周有7天一样可以一一列举
枚举类型创建
enum Day//星期
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};
enum Color//三颜⾊
{Red,Green,Blue
};
这里的enum Day 和 enum Color都是枚举类型
{ }下都是可能取值,枚举常量
这些枚举常量都是从0开始,依次增加当然我们也可以给它赋值
枚举类型初始化
默认初始化
我们知道枚举常量都是从0开始,我们那上面的Color枚举来举例
#include<stdio.h>
enum Color//三颜⾊
{Red,//默认存储的是0Green,//1Blue//2
};
int main()
{//创建一个c来接收Redenum Color c = Red;printf("%d\n", c);//0printf("%d\n", Red);//0printf("%d\n", Green);//1printf("%d\n",Blue);//2return 0;
}
运行结果如下
从结果中可以看出,枚举常量是都是从0开始的,依次增加,并且创建的变量也是存储其常量值
为什么我们这里是说这是枚举常量呢
这里我们给枚举常量进行修改,发现是不可以的,因为常量值是不可以修改的,枚举常量是常量
就地初始化
全部初始化
#include<stdio.h>
enum Color//三颜⾊
{Red = 1,Green = 3,Blue = 7
};
int main()
{printf("%d\n", Red);printf("%d\n", Green);printf("%d\n",Blue);return 0;
}
这里我们对Color这个枚举中的枚举常量全部都初始化了,但是不可以在枚举类型外部修改
运行结果如下
那如果我们只是部分初始化呢
#include<stdio.h>
enum Color//三颜⾊
{Red = 1,Green,Blue
};
int main()
{printf("%d\n", Red);printf("%d\n", Green);printf("%d\n",Blue);return 0;
}
这里原本第一个是从0开始的,但是我们初始化为1,但是依旧满足依次增加的原则,所以结果为
那我们只修改中间的呢,依然满足上面依次增加的规则,如果初始化的话,按照初始化的值
就像下面这个,我们只初始化了前面两个枚举常量,那第三个常量值就是第二个常量值+1
枚举的优点(相较于define)
我们可以使⽤ #define 定义常量,为什么⾮要使⽤枚举?
就像上面的代码,我们用#define来写
#include<stdio.h>
#define Red 1
#define Green 2
#define Blue 3
int main()
{printf("%d\n", Red);printf("%d\n", Green);printf("%d\n", Blue);return 0;
}
运行结果如下,可以生成与枚举类型相同的结果
我们可以发现这里的#define每次只能定义一个,而枚举类型一个可以定义多个对象
枚举的优点
1.可以增强代码的可读性 ,因为define只是简单的代码替换,无法调试观察
2.和#define使用枚举比较严谨
3.方便使用,可以连续创建多个常量,而#define依次只能定义一个
到这里就结束了,希望大家有所帮助,欲知后事如何,请听下回分解