文章目录
自定义类型也属于类型的
一种,就如同Int、char、double这些内置类型一样。- 类型本身不占用空间,使用类型创建一个变量才会根据使用的类型开辟空间。
1. 结构体
- 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
- 数组是相同的类型的集合,结构体就是不同的数据类型的集合。
1.1 结构体的声明和定义
1.1.1结构体声明
类型有Int、float、char…,声明的一个结构体类型也是个类型
struct 结构体名称
{
结构体成员1;
结构体成员2;
结构体成员3;
}; ←(最后有个分号,不能忘)
结构体成员可以是任何一种基本的数据类型,也可以是另一个个结构体,如果是后者,那么就相当于结构体的嵌套。
1.1.2 结构体定义
在创建结构体类型的时候定义一个全局变量,或者在main函数里进行局部变量定义。
struct 结构体名称
{
结构体成员1;
结构体成员2;
结构体成员3;
}变量列表;
例如想要描述一个学生
学生的属性:姓名、性别、年龄、学号、专业…
#include <stdio.h>
#define MAX 20
struct student//声明一个描述学生的结构体类型
{
//成员变量
char name[MAX];//姓名
char id[MAX];//学号
char sex[MAX];//性别
char specialty[MAX];//专业
int age;//年龄
}s3,s4;//s3,s4为全局变量
int main()
{
struct student s1;
struct student s2;
//s1 s2都是学生的结构体类型局部变量
return 0;
}
1.2 结构体自引用
在一个结构体中包含的一个成员为该结构本身。
struct node
{
int data;
struct node* next
};
next里面存放着下一个节点的地址。
1.3 结构体类型重命名
typedef 类型定义
将类型改个名字,功能不变
如:
typedef unsigned int u int(将unsigned int 改个名字叫u int).
unsigned int num = 20<——>u int num 20
给一个人起了个外号,那个人他还是他自己并没有改变。
结构体类型重定义:
typedef struct student
{
//成员变量
char name[MAX];//姓名
char id[MAX];//学号
char sex[MAX];//性别
char specialty[MAX];//专业
int age;//年龄
}stu;
将struct student 这个结构体类型重命名为stu ,创建结构体变量的时候不用再打一大串了。
struct student s1<——>stu s1;两种方式定义变量都可以。
1.4 初始化结构体
在定义一个变量或数组的时候可以对其进行初始化;
int a = 110;
int str[] = {1,2,3,4,5};
同理,定义结构体变量的时候也可以对其初始化。
#define MAX 20
struct student
{
//成员变量
char name[MAX];//姓名
char id[MAX];//学号
char sex[MAX];//性别
char specialty[MAX];//专业
int age;//年龄
};
int main()
{
//给结构体中的每个成员都给一个值
struct student s1 = {"张三","123456789","男","土木",20};
return 0;
}
1.5 访问结构体成员
结构体变量名.结构体成员名
struct stu s1 = {"张三","123456789","男","土木工程",20};
printf("名字:%s\n",s1.name);
printf("学号:%s\n",s1.id);
printf("性别:%s\n",s1.sex);
printf("专业:%s\n",s1.specialty);
printf("年龄:%d\n",s1.age);
名字:张三
学号:123456789
性别:男
专业:土木工程
年龄:20
1.6 结构体内存对齐
计算结构体的大小
s1和s2所占空间分别是多少?
struct s1
{
char c1;
int a;
char c2;
};
struct s2
{
char c1;
char c2;
int a;
};
int main()
{
struct s1 s1 = {0};
struct s2 s2 = {0};
printf("s1 = %d \n",sizeof(s1));
printf("s2 = %d \n",sizeof(s2));
return 0;
}
s1 = 12
s2 = 8
为什么会是这样的结果?这就和结构体的对齐规则有关了。
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数
:该成员类型的大小(如char的对齐数是1,int是4)gcc底下没有默认对齐数。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
s1
- 第一个成员c1在结构体偏移量为0的地方。
- a是int,对齐数是4,要放到偏移量为4的倍数的位置上去,导致c1占1个字节却浪费3个字节,
- c2是char,对齐数是1,放到1的倍数的偏移量上,这个时候c2放在了第9个字节的位置
- 因为结构体总大小为最大对齐数的倍数,所以只能再补三个字节到12.
s2
- c1是第一个成员,放在偏移量为0的位置
- c2的对齐数是1,要放到偏移量是1的倍数位置上去,所以c2放到偏移量为1的位置
- a的对齐数是4,要放到4的倍数的偏移量去,也就是从偏移量为4的地方开始,存4个字节
- 此时结构体成员所占内存正好是8(最大对齐数4的倍数),这样结构体s2的总大小就是8个字节。
总而言之
- 除了结构体的第一个成员是从偏移量0的位置开始存储,其余所有成员都是
从对齐数的倍数的偏移量开始存储
,(如int对齐数是4,只能从4的倍数位置开始存,double要从8的倍数位置开始存。)结构体大小必须是最大对齐数的的倍数
,例如:最大对齐数是8,结构体大小就只能是8的倍数,不管浪费多少空间。
既然两个结构体的成员变量都是一样的,只是因为放的位置不同而导致结构体占的内存不同,
那么在设计结构体的时候,应该尽量做到把占用空间小的成员集中到一起来节省空间。
1.7 结构体传参
函数里面想改变函数外面的值,用传址调用。
而只是函数里做打印访问,不需要改变函数外的变量的内容,可以传值调用
1.7.1 传值调用
值传递:t是对s的一分临时拷贝,对t的内容修改无法y影响s的内容,因为拷贝的临时变量t出了函数之后数据就会被销毁。
#include <stdio.h>
struct stu
{
int a;
char b;
double c;
};
void print1(struct stu t)
{
t.a = 100;
t.b = 'w';
t.c = 3.14;
printf("t.a = %d\n",t.a);
}
int main()
{
struct stu s = {0};
print1(s);
printf("s.a = %d\n",s.a);
return 0;
}
t.a = 100
s.a = 0
1.7.2 传址调用
函数内部想改变函数外部的某个值的时候就要使用传址调用了。
指针访问结构体成员
指针变量名 -> 成员名
#include <stdio.h>
struct stu
{
int a;
char b;
double c;
};
void print2(struct stu* t)
{
t-> a = 100;
t-> b = 'w';
t-> c = 3.14;
}
int main()
{
struct stu s = {0};
print2(&s);
printf("%d\n",s.a);
return 0;
}
100
结构体传参的时候,要传结构体的地址。
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
- 传址调用时,传过去的是个地址,占的空间无外乎就是4/8字节。
- 传值调用时,因为是对传来的值得一份临时拷贝,传过来多少拷贝多少。
2. 位段
2.1 位段是什么
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是
整型
。- 位段的成员名后边有一个冒号和一个数字。
- 位段的
成员类型相同
,有一个int就全是Int,有一个char就全是char
比如:
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
A就是一个位段类型,abcd就是是位段A的成员。
位段A的大小应该是多少?
#include <stdio.h>
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
int main()
{
struct A s;
printf("%d\n",sizeof(s));//8个字节
return 0;
}
位段A的大小是8个字节
,如果把冒号数字去掉,就是个标准的结构体,结构体A的大小应该是16个字节。
位段——二进制位
a:2表示a只需要2个比特位,对应2个二进制位,b:5表示b只需要5个比特位,对应5个二进制位,以此类推abcd是占47个比特位,对应47个二进制位。
47个比特位应该是对应6个字节,为什么结果会是8呢?
2.2 位段的内存分配
- 位段的成员可以是
int、unsigned int、 signed int
或者是char
(属于整形家族)类型。- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段A的成员都是int型的,所以在开辟空间的时候是按照整型的方式来开辟的,一次就开辟4个字节给abcd去瓜分。
abc分了17个bit的空间走了之后,剩下的空间装不下d的30个bit,所以丢掉,再开辟一块整型空间装d的30bit,所以位段A的大小就是8字节。
注意:位段后面的数字不能大于32
再举个例子:
#include <stdio.h>
struct A
{
char a:3;
char b:4;
char c:5;
char d:4;
};
int main()
{
struct A s = {0};
s.a = 10;
s.b = 20;
s.c = 3;
s.d = 4;
printf("%d %d %d %d\n",s.a,s.b,s.c,s.d);
return 0;
}
结果是多少?
2 4 3 4
- 因为都是char类型,所以一次开辟一个字节的空间来存放abcd,要用3个字节才能装下abcd。
因为abcd都已经被划分了好应该占用多少个bit的空间。 - 将10(二进制位1010)赋给了a,又因为a只有三个bit的空间所以实际a存着010。
- 将20(10100)赋给了b,b只能存4个bit所以实际a存着0100。
- c是011,但是划给了c5个bit的空间,所以要在前面补0
以此类推abcd的值用十进制打印出来就是2 4 3 4.
2.3 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
3. 枚举
使用enum(枚举关键字)
,可以创建一个新"类型"并指定它可以具有的值(实际上,enum常量是Intl类型,因此,只要能使用int类型的地方,就可以使用枚举类型)
枚举顾名思义就是一 一列举。
把可能的取值一 一列举出来。
比如在生活中:
- 一周的星期一到星期日是有限的7天,可以一一列举。
- 性别有:男、女、保密,也可以一一列举。
- 月份有12个月,也可以一一列举。
3.1 枚举类型的定义
枚举关键字 枚举类型名
{
枚举成员1,
枚举成员2,
枚举成员3
};
枚举成员都属于是枚举的可能取值也称为枚举常量
。
例如:
enum Day//命名为星期的枚举类型
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
3.2 枚举的使用
定义类枚举类型之后,就可以使用枚举类型来创建一个枚举类型变量。
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
enum Color c = BLUE;//只能给c赋值颜色枚举类型里的三个成员
printf("RED = %d GREEN = %d BLUE = %d\n",RED,GREEN,BLUE);
return 0;
}
c是一个关于颜色类型的变量,以后给c赋值的时候,只能赋红绿蓝这三者之一。
这些可能取值都是有值的,默认从0开始,依次递增1。
RED = 0 GREEN = 1 BLUE = 2
当然在定义的时候也可以赋初值,即使枚举成员属于枚举常量,也应该有个初始值。
例如:
enum Color
{
RED = 2,
GREEN,
BLUE = 5
};
int main()
{
enum Color c = BLUE;
printf("RED = %d GREEN = %d BLUE = %d\n",RED,GREEN,BLUE);
return 0;
}
RED = 2 GREEN = 3 BLUE = 5
3.3 枚举的优点
明明可以使用 #define
定义常量,为什么非要使用枚举呢?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较,枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
4. 联合(共用体)
union
联合关键字
- 联合也是一种特殊的自定义数据类型。
- 这种类型定义的变量也包含一系列的成员。
- 特征是这些成员共用同一块空间(地址),所以联合也叫共用体。
4.1 联合体的声明
声明共用体(联合体)的语法格式与结构体是一样的。
union 联合体名称
{
联合体成员1;
联合体成员2;
联合体成员3;
};
- 只需要将stuct关键字,换成union,结构体就变成了共用体。
- 虽然结构相似,但是共用体的所有成员拥有同一个内存地址。
4.2 联合体的特点
- 联合体的成员是共用同一块内存空间的
- 联合体变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
- 联合体内的成员一次只能使用一个,不能同时使用
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un u;
printf("%d\n",sizeof(u));//联合的大小是联合体内最大成员的大小
printf("u = %p\n c = %p\n i = %p\n",&u,&(u.c),&(u.i));//共用同一个地址
return 0;
}
4
u = 0x7ffcc8ea6480
c = 0x7ffcc8ea6480
i = 0x7ffcc8ea6480
把联合体想想象成一个“人格分裂患者”,体内的每个人格ABCD共用一具身体,有时候是A在使用身体,有时候是B在使用,总之ABCD这4个人格不能同时出现,只是不断的切换而已。
举个栗子:
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un u;
u.i = 5;
u.c = 'a';
printf("u.i = %d\n",u.i);
printf("u.c = %c\n",u.c);
return 0;
}
u.i = 97
u.c = a
分析:只有最后一个的值是正确的,两个联合体成员用的都是同一个内存地址,对他们进行赋值会导致互相覆盖,所以只有最后被赋值的u.c才能正确打印。
4.3 联合体判断字节序
判断当前机器的字节序
给 i 赋个1,i占了4个字节,c占的是i的第一个字节,把c拿出来就相当于是拿出来 i 的第一个字节,如果c的值为1的话, 则说明是小端,反之大端。
//判断当前机器字节序——联合体法
#include <stdio.h>
int check(void)
{
union Un
{
char c;
int i ;
}u;
u.i = 1;
return u.c;
}
int main()
{
if(1 == check())
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
小端
4.4 联合体大小的计算
和结构体的内存对齐类似
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
举个栗子:
#include <stdio.h>
union Un
{
int a;
char arr[5];
};
int main()
{
union Un u;
printf("%d\n",sizeof(u));
return 0;
}
结果是8
- 联合体中,占内存最大的成员是占5个字节的arr
- 但是char的对齐数是1,int的对齐数是4
- 联合体的大小必须是最大的对齐数的整数倍,也就是4的整数倍,但是最大的成员只有5个字节,所以浪费3个字节提升到8。
5. 总结
- 结构体和联合体存在
内存对齐
- 枚举的大小是4个字节。
- 位段的成员类型必须一致,第一个是Int就的全是Int.