自定义数据类型——结构体、位段、枚举、联合体

  • 自定义类型也属于类型的一种,就如同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 位段是什么

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是整型
  2. 位段的成员名后边有一个冒号和一个数字。
  3. 位段的成员类型相同,有一个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 位段的内存分配

  1. 位段的成员可以是 int、unsigned int、 signed int 或者是 char (属于整形家族)类型。
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

位段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 位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
    器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
    舍弃剩余的位还是利用,这是不确定的。

总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

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 定义常量,为什么非要使用枚举呢?

枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较,枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

4. 联合(共用体)

union联合关键字

  1. 联合也是一种特殊的自定义数据类型。
  2. 这种类型定义的变量也包含一系列的成员。
  3. 特征是这些成员共用同一块空间(地址),所以联合也叫共用体。

4.1 联合体的声明

声明共用体(联合体)的语法格式与结构体是一样的。

union 联合体名称
{
 	联合体成员1;
 	联合体成员2;
 	联合体成员3;
};
  • 只需要将stuct关键字,换成union,结构体就变成了共用体。
  • 虽然结构相似,但是共用体的所有成员拥有同一个内存地址。

4.2 联合体的特点

  1. 联合体的成员是共用同一块内存空间的
  2. 联合体变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
  3. 联合体内的成员一次只能使用一个,不能同时使用
#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

![在这里插入图片描述](https://img-blog.csdnimg.cn/2e931c2450a5461f94cb87692b1807aa.png

把联合体想想象成一个“人格分裂患者”,体内的每个人格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. 总结

  1. 结构体联合体存在内存对齐
  2. 枚举的大小是4个字节。
  3. 位段的成员类型必须一致,第一个是Int就的全是Int.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值