C语言——自定义类型
目录
结构体
概念
结构体变量的创建和初始化
结构体的自引用
结构体的内存对齐
内存对齐存在的原因
合理设计结构体
方法一
方法二
结构体传参
结构体实现位段
什么是位段
位段的内存分配
位段的跨平台问题
注意
联合体
概念
验证
优点
小应用
什么是大小端?
用联合体判断大小端
枚举
概念
优点
使用注意
我们以前见到的 char,bool,short,int,long long,float,double,long double都是内置类型。
自定义类型,从名字上来看,也就是我们自己创造定义的类型
包括数组类型,结构体类型(struct),枚举类型(enum),联合体类型(union)
结构体
概念
结构是⼀些 值的集合 ,这些值称为 成员变量 。结构的每个成员 可以是不同类型的变量 。
struct tag
{
member-list;
//成员变量列表
}variable-list;
//结构体变量列表
例:描述一个学生
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[20];//学号
}st1,st2;//结构体变量列表
结构体变量的创建和初始化
方法一:直接在结构体后面创建结构体变量和初始化
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[10];//学号
}st1 = { "张三",18,"男","1234567" },st2 = { "李丽",24,"女","1234568" };//结构体变量列表
#include<stdio.h>
int main()
{
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st1.name, st1.age, st1.sex, st1.number);
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st2.name, st2.age, st2.sex, st2.number);
return 0;
}
方法二:在需要的函数中进行结构体变量的创建和初始化
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[10];//学号
};//分号
#include<stdio.h>
int main()
{
struct Student st1 = { "张三",18,"男","1234567" };
struct Student st2 = { "李丽",24,"女","1234568" };
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st1.name, st1.age, st1.sex, st1.number);
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st2.name, st2.age, st2.sex, st2.number);
return 0;
}
当然,在初始化的时候我们也可以按照自己的顺序给结构体变量进行初始化
//定义一个结构体
struct Student
{
//结构体成员变量列表
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char number[10];//学号
}st1,st2;//分号
#include<stdio.h>
int main()
{
struct Student st1 = { .name="张三",.sex="男",.number="1234567",.age=18 };
//也可以按照自己的顺序进行初始化
struct Student st2 = { "李丽",24,"女","1234568" };
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st1.name, st1.age, st1.sex, st1.number);
printf("name:%s\nage:%d\nsex:%s\nnumber:%s\n", st2.name, st2.age, st2.sex, st2.number);
return 0;
}
结构体的自引用
我们说结构体里面可以是不同类型的变量,那么结构体里面可不可以包含一个类型为该结构本身的成员呢?
答案是不可以的,比如我们定义一个链表的结点。
struct Node
{
int data;
struct Node next;
};
这样子进行定义的话,⼀个结构体中再包含⼀个同类型的结构体变量,那么结构体变量的⼤⼩就会⽆穷的⼤,是不合理的。
正确方式:
struct Node
{
int data;
struct Node* next;
//next保存下一个结点的地址
};
我们也可以进行一定的优化(使用重命名的方式)
typedef struct Node
{
int data;
struct Node* next;
//虽然后面结构体重命名为Node,但是到这里编译器还没有识别,依然写成
//struct Node的形式
}Node;
结构体的内存对齐
计算结构体大小,就需要知道结构体内存对齐的规则
1. 结构体的 第⼀个成员对⻬到和结构体变量起始位置偏移量为0 的地址处2. 其他成员变量要对齐到 对齐数整数倍 的地址处对⻬数 = 编译器默认对⻬数 与该 成员变量⼤⼩ 的 较⼩值VS 中默认的值为 8- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩3. 结构体总⼤⼩为 最⼤对⻬数 (结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍 。4. 如果嵌套了结构体的情况, 嵌套的结构体成员 对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是 所有最⼤对⻬数 (含嵌套结构体中成员的对⻬数)的 整数倍 。
#include<stdio.h>
int main()
{
//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%zd\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%zd\n", sizeof(struct S2));
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%zd\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%zd\n", sizeof(struct S4));
return 0;
}
请在评论区留下你的答案,我们马上揭晓谜底~
我们通过下面的来进行理解答案
#include<stdio.h>
int main()
{
//练习1
struct S1
{
//vs默认对齐数为8
char c1;//1 8 1
int i; //4 8 要对齐到该成员变量int类型字节大小4的整数倍——4+4
char c2;//1 8 8+1——9
//结构体总大小为成员变量最大对齐数4的整数倍
//12
};
printf("%zd\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;//1 8 1
char c2;//1 8 1+1——2
int i; //4 8 2+4
//结构体总大小为成员变量最大对齐数4的整数倍
//8
};
printf("%zd\n", sizeof(struct S2));
//练习3
struct S3
{
double d;//8 8 8
char c; //1 8 8+1——9
int i; //4 8 12+4——16
//结构体总大小为成员变量最大对齐数8的整数倍
//16满足
};
printf("%zd\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1; //1 8 1
struct S3 s3; //16 8 8+16——24
//结构体S3大小为16,最大对齐数为8
double d; //8 8 24+8——32
//结构体总大小为成员变量最大对齐数8的整数倍
//32满足
};
printf("%zd\n", sizeof(struct S4));
return 0;
}
内存对齐存在的原因
那么为什么会有结构体对齐呢?
平台原因 (移植原因)
性能原因
总结:结构体的内存对⻬是拿 空间来换取时间 的做法,提高程序的运行效率
合理设计结构体
方法一
让占⽤空间⼩的成员尽量集中在⼀起
#include<stdio.h>
int main()
{
struct s1
{
char c1;
char c2;
int n;
};
printf("sizeof(struct s1)==%zd\n", sizeof(struct s1));
struct s2
{
char c1;
int n;
char c2;
};
printf("sizeof(struct s2)==%zd\n", sizeof(struct s2));
return 0;
}
我们可以看见虽然这两个结构体的包含的成员变量相同,但是它们的结构体大小是不一样的,这是因为在对齐结构体的时候它们的对齐位置是不一样的。
所以定义一个结构体的时候让占⽤空间⼩的成员尽量集中在⼀起,就可以节省空间。
方法二
修改默认对⻬数
使用 #pragma 这个预处理指令,改变编译器的默认对⻬数
#include<stdio.h>
#pragma pack(1)//修改默认对齐数为1
struct s1
{
char c1;
char c2;
int n;
};
struct s2
{
char c1;
int n;
char c2;
};
void test1()
{
printf("test1:\n");
printf("sizeof(struct s1)==%zd\n", sizeof(struct s1));
printf("sizeof(struct s2)==%zd\n", sizeof(struct s2));
}
int main()
{
test1();
return 0;
}
如果想让它恢复成原来编译器的默认对齐数只需要加上
#pragma pack()//恢复到编译器默认对齐数
#include<stdio.h>
#pragma pack(1)//修改默认对齐数为1
struct s1
{
char c1;
char c2;
int n;
};
#pragma pack()//恢复到编译器默认对齐数
struct s2
{
char c1;
int n;
char c2;
};
void test1()
{
printf("test1:\n");
printf("sizeof(struct s1)==%zd\n", sizeof(struct s1));
printf("sizeof(struct s2)==%zd\n", sizeof(struct s2));
}
int main()
{
test1();
return 0;
}
结构体传参
在前面学习函数的时候我们知道有传值传参和传址传参
#include<stdio.h>
struct S
{
int data[10];
int num;
};
struct S s = { {1,2,3,4}, 100 };
//结构体传参
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;
}
在上面的代码中,无论传参是传结构体还是传结构体的地址,都达到了我们想要的效果。
那么结构体传参哪一个更好呢?
答案是结构体地址传参
原因
1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。2.如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,会导致 性能的下降 。
结构体实现位段
什么是位段
1. 位段的成员必须是 int 、 unsigned int 或 signed int (结构体可以有其他类型)在C99中位段成员的类型也可以 选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字(结构体成员名后面没有内容)
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:20;
};
位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型2. 位段的空间上是 按照需要 以4个字节( int )或者1个字节( char )的⽅式来开辟的。3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
printf("test1:%zd\n", sizeof(s));
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
}
我们在VS编译器上进行验证,我们猜想首先位段成员类型为char,按照需要以一个字节(8个比特位)空间来开辟,a要占用2个比特位,b占用4个比特位,c占用5个比特位,d占用4个比特位,a、b一起占用了6个比特位(剩余两个比特位,不够存储c,浪费掉这两个比特位,另外开辟一个字节),c占用五个字节,剩余三个比特位,不够存储d,浪费掉这三个比特位,另外开辟一个字节。所以位段A的字节大小为3,我们一起来验证一下。
所以说明在VS编译器上,剩余的比特位是浪费掉的,如果没有浪费那么应该是两个字节(16个比特位)。
后面的代码再对a,b,c,d所在的内存放入数据。
在VS编译器上:1.char——一个字节一个字节进行内存空间开辟2.一个字节内部从右向左使用3.剩余的比特位不够下一个成员使用时,浪费掉,开辟新的内存进行存放
位段的跨平台问题
1. int 位段被当成有符号数还是⽆符号数是不确定的。2. 位段中最⼤位的数⽬不能确定(16位机器最⼤16,32位机器最⼤32,如果写成27,在16位机器会 出问题)3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这也是不确定的。
注意
位段一般是 ⼏个成员共有同⼀个字节 ,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。
内存中 每个字节分配⼀个地址 ,⼀个字节内部的 bit位是没有地址的 。
所以不能对位段的成员使⽤&操作符,这样就 不能使⽤scanf直接给位段的成员输⼊值 ,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 20;
};
int main()
{
struct A sa = { 0 };
//scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
联合体
概念
像结构体⼀样,联合体也是 由⼀个或者多个成员构成 ,这些成员可以不同的类型。
编译器 只为最⼤的成员分配⾜够的内存空间 。
联合体的特点是 所有成员共⽤同⼀块内存空间 ,所 以联合体也叫:共⽤体。
如果 给联合体其中⼀个成员赋值,其他成员的值也会跟着变化。
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = { 0 };
//计算联合体变量的大小
printf("%d\n", sizeof(un));
return 0;
}
大小为4个字节,这是为什么呢?
我们前面提到联合体的特点是所有成员共⽤同⼀块内存空间,编译器只会为最大的联合体成员分配足够的空间。
我们来看看下面的代码
#include <stdio.h>
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//输出的结果是什么?
printf("%zd\n", sizeof(union Un1));
printf("%zd\n", sizeof(union Un2));
return 0;
}
上面进行了分析和解释,所以
⼀个联合变量的大小,⾄少是最⼤成员的大小(因为联合至少得有能⼒保存最⼤的那个成员)
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。
验证
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = { 0 };
// 输出的结果是⼀样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}
我们可以发现,这三个地址是一样的,这也就验证了联合体成员共用了一块内存空间。
我们来看看下面的代码:
#include<stdio.h>
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = { 0 };
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
优点
使用联合体有什么好处呢?
struct goods
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
//特殊属性
char title[20];//书名
char author[20];//作者
int num_pages;//页数
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
};
结构体里面包括了所有的属性,需要哪一个再去进行初始化以及使用,但是这样就会导致结构体偏大,造成空间的浪费。我们可以一起使用结构体和联合体来进行改造,比如:
struct goods
{
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
union {
struct
{
char title[20];//书名
char author[20];//作者
int num_pages;//页数
}book;
struct
{
char design[30];//设计
}mug;
struct
{
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
}shirt;
}item;
};
联合体就会为占内存空间最大的结构体分配足够的空间,这也就节省了内存空间。
小应用
什么是大小端?
⼤端(存储)模式:
数据的 低位字节 内容保存在 内存的⾼地址 处,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:
数据的 低位字节 内容保存在 内存的低地址 处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
用联合体判断大小端
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;//返回1是⼩端,返回0是⼤端
}
int main()
{
if (check_sys)//1为真
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
编译器取地址会从低地址开始取,这里返回了1,也就是低字节的内容存放在低地址处,说明VS为小端机器。
枚举
概念
枚举就是⼀⼀列举, 把可能的取值⼀⼀列举 。
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
//中间用逗号隔开
};//末尾有分号!!!
enum Month
{
Jan,
Feb,
Mar,
Apr,
May,
Jun,
Jul,
Aug,
Sept,
Oct,
Nov,
Dec
};
{ }中的内容是枚举类型的可能取值,也就是枚举常量 。
#include<stdio.h>
enum Color//颜⾊
{
RED,
GREEN = 3,
BLUE
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
//第一个枚举常量默认为0,也可以初始化,没有初始化的枚举常量是上一个枚举常量的值加一
优点
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符⽐较枚举有类型检查,更加严谨。
3. 便于调试,预处理阶段会删除 #define 定义的符号
4. 使⽤⽅便,⼀次可以定义多个常量
5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
使用注意
在C语⾔中是可以 拿整数给枚举变量赋值 的,但是在C++是不⾏的,C++的类型检查⽐较严格。
enum Color//颜⾊
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值