14 C语言进阶自定义类型详解

自定义类型:结构体,枚举,联合

大纲

  1. 结构体

    1. 结构体类型的声明
      1. 结构的自引用
      2. 结构体变量的定义和初始化
      3. 结构体内存对齐
      4. 结构体传参
      5. 结构体实现位段(位段的填充、可移植性)
  2. 枚举

    1. 枚举的定义
    2. 枚举的优点
    3. 枚举的使用
  3. 联合

    1. 联合类型的定义
    2. 联合的特点
    3. 联合大小的计算
讲在前面
	我们学过的char、short、int、long、float、double是叫C语言中的内置类型是C语言自己的函数类型;然而还有一些复杂对象我们无法描述,比如人,书籍等事物。这时我们就出现了结构体、枚举、联合。这些被叫做自定义类型。

结构体

结构体类型的声明

结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同的类型的变量。

结构的声明
struct tag{
    member_list;//成员列表
}variable_list;//变量列表

实例:

//声明一个结构体类型
//声明一个学生类型,想通过学生类型来创建学生变量(对象)
//描述学生:名字 电话 性别 年龄
struct Stu{
    char name[20];
    int age;
    char sex[5];
    char tele[20];
};//这个分号不能丢

image-20220126000454664

创建结构体变量

image-20220126001050062

特殊的一种声明

在声明的时候可以完全声明也可以不完全声明。

//匿名结构体声明
struct{
    int a;
    int b;
    float c;
}x;
struct{
	int a;
    int b;
    float c;
}a[20],*p;//匿名结构体指针

在使用匿名结构体声明的时候,必须创建结构体变量。否则无法使用。

提问:

//在上面的代码中两种结构体的形式是一样的。那么下面的语句合法吗?
p = &x;

警告:编译器会把上面的两个声明当成完全不同的两个类型。是非法的。

结构体的自引用

方式一:

struct Node{
    int data;
    struct Node* next;
};

方式二:

typedef struct Node{
    int data;
    struct Node *next;
}Node;

注意:在方式一种成员变量处的struct Node应该是指针类型。
方式二中的结构体名称的两处都得写,并且在成员变量处必须是带有struct的名称。

两种错误的方式

struct Node{
	int data;
	struct Node next;
};
//不可行原因,sizeof(struct Node)无法计算
//无限套娃
typedef struct{
	int data;
	Node* next; 
}Node;
//不可行原因:这个结构是匿名结构体声明,不存在Node这种类型。

在结构体创建的时候成员变量不能是自己。但是可以是自己的指针。(指针有大小4/8)。

结构体变量的定义和初始化

现在有了结构体类型,怎么定义变量呢?

struct Point{
    int x;
    int y;
}p1;		//声明结构体类型的同时定义变量p1
struct Point p2;//定义结构体变量p2
struct Point p3 = {1,2};//定义变量的同时进行初始化
struct Stu{
	char name[15];//名字
	int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Point{
    int x;
    int y;
};
struct Node{
    int data;
    struct Point p;
    struct Node* next;
};
struct Node n1 = {15,{2,3},NULL};//结构体嵌套初始化

结构体内存对齐

在之前学习的时候就一直存在一个关于结构体的疑问:结构体大小的计算。

先试着猜一下答案:

struct S1 {
    char c1;
    int a;
    char c2;
};

struct S2 {
    char c1;
    char c2;
    int a;
};

int main() {
    struct S1 s1 = {0};
    printf("%llu\n", sizeof(s1));
    struct S2 s2 = {0};
    printf("%llu\n", sizeof s2);
    return 0;
}

结果:image-20220126121220785

具体怎么计算呢?

首先要掌握对齐规则:

# 对齐数=编译器默认的一个对齐数与该成员大小的较小值。
# vs默认对齐数是8,gcc没有默认对齐数,对齐数是成员大小。
1. 第一个成员在结构体变量偏移量为0的地址处存放
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处存放
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
image-20220126124945144

如果出现结构体嵌套的情况呢?

//以下结构体的大小是?
struct S2 {
    char c1;
    char c2;
    int a;
};
struct S3{
    char c1;
    struct S2 s2;
    double d;
};
int main() {
    struct S3 s3 = {0};
    printf("%llu\n", sizeof(s3));
    return 0;
}

image-20220126162156738

为什么要存在内存对齐呢?

大部分资料说:

  1. 平台原因(移植原因)
	不是所有的硬件平台都能访问任意地址上的任意数据的。某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  1. 性能原因
	数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
	原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总结:结构体内存对齐就是用空间来换时间的思想实现的。

那么在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到呢?

解决方式:让占用空间小的成员集中在一起。

举个例子:
在上面的结构体定义的时候都是两个char和一个int但是定义顺序不同,得到的结构体的大小就不相同。

修改默认对齐数

有一个#pragma的预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

#include <stdio.h>
//设置默认对齐数为8
#pragma pack(8)
struct S1
{
     char c1;
     int i;
     char c2;
};
//取消设置的默认对齐数,还原为默认d
#pragma pack()
#pragma pack(1)//设置默认对齐数为1
struct S2
{
     char c1;
     int i;
     char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}

小结:在对齐方式不合适的时候我们可以修改默认的对齐数。

计算某个变量的偏移量

使用<stddef.h>中的offsetof宏。

该函数形式的宏返回数据结构或联合类型中成员成员的字节偏移值。 返回的值是size_t类型的无符号整数值,其中包含指定成员与其结构开头之间的字节数。

官网实例:

/* offsetof example */
#include <stdio.h>      /* printf */
#include <stddef.h>     /* offsetof */

struct foo {
      char a;
      char b[10];
      char c;
};

int main ()
{
      printf ("offsetof(struct foo,a) is %d\n",(int)offsetof(struct foo,a));
      printf ("offsetof(struct foo,b) is %d\n",(int)offsetof(struct foo,b));
      printf ("offsetof(struct foo,c) is %d\n",(int)offsetof(struct foo,c));
      return 0;
}

结构体传参

先看一下代码

struct S {
    int a;
    char b;
    double c;
};
// 直接初始化结构体变量
void Init(struct S ps) {
    ps.a = 100;
    ps.b = 'a';
    ps.c = 3.1415;
}
// 使用地址初始化结构体变量
void Init2(struct S *ps) {
    ps->a = 101;
    ps->b = 'b';
    ps ->c = 1.6782;
}

int main() {
    struct S s = {0};
    Init(s); // 传结构体
    Init2(&s);// 传结构体地址
    return 0;
}

初始化时使用的应该是Init还是Init2函数呢?

答: 应该使用的是Init2函数。

原因:使用Init达不到修改参数的目的。打印的时候,也是传地址,因为传参的时候,参数是需要压栈的,会有时间空间上的系统开销(会临时拷贝)。

如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,会导致性能的下降。、

结论:结构体传参的时候要传结构体的地址。

结构体实现位段

什么是位段

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

  1. 位段的成员必须是int、unsigned int 或者 signed int。(不仅限,只要是整型就可以了)
  2. 位段的成员后边有一个冒号和一个数字。

举例:

struct A {
    int a: 2;
    int b: 5;
    int c: 10;
    int d: 30;
};

int main() {
    struct A a;
    printf("%d \n", (int) sizeof(a));//8个字节
    return 0;
}

位段中的位是二进制位。

如果在使用int类型的时候,你使用的仅仅是0、1、2、3四个数字,你就没有必要使用int这么大的空间,这时只需要两个bite位就可以表示了。以此类推,a、b、c、d就可以自己定义需要的bite位的大小。

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

举个例子:

struct A {
    int a: 2;
    int b: 5;
    int c: 10;
    int d: 30;
};

图解:

image-20220208120732227

没有使用的空间就直接浪费掉了。但是比起来直接使用4个int的空间已经节省了很多了。

注意位段后面的数字是不能大于前面类型的字节大小的。

实例:

struct B{
    char a:3;
    char b:4;
    char c:5;
    char d:4;
};
int main() {
    struct B b = {0};
    b.a = 10;
    b.b = 20;
    b.c = 3;
    b.d = 4;
    return 0;
}

存储结果:image-20220208174607889

因为已经锁定了可以占用的位数,固定的位数是代表存储时会有一定的限度值。如果超出这个限定范围就像之前的存储情况一样发生截断现象,从而不是你自己想要存入的值。

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

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

位段的应用

网络传输的时候,数据前面的首部部分就是用位段来定义的。如过不使用位段的话,就会产生大量的空间浪费。

image-20220208202223282

枚举

枚举是什么:枚举就是我们经常说的列举。

把可能出现的值一一列举出来。

枚举的定义及使用

//定义一个星期
enum Day{
    //内容是可能的取值-常量。
    Mon,
    Tues,
    Web,
    Thur,
    Fri,
    Sat,
    Sun
};
//定义一个性别枚举类型
enum Sex{
    //枚举常量
    Male,//0
    Female,//1
    secret//2
};
enum Color{
  	//枚举常量第一个值默认为0,但是是可以修改的。
    //枚举常量赋予初值
    RED = 2,
    GREEN = 4,
    //如果不赋予初始值,则该枚举常量的默认值为上一个枚举变量的值+1。
    BLUE
};
int main() {
    enum Sex s = Male;
    s = Female;
    enum Day d = Mon;
    return 0;
}

在上面的代码中enum Day,enum Sex,enum Color都是枚举类型。
在大括号中包括的内容都是枚举类型的可能取值,也叫枚举常量。

可能的取值都是从0开始的,一次递增1,在定义的时候也可以赋予初值。

枚举的优点

我们可以使用#define来定义常量,为什么非要使用枚举呢?

枚举使用的时候有以下的有点

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,相对而言更加的严谨。
  3. 防止命名污染(封装)。
    在定义的时候多种类型的都写成#define的形式可能会出现枚举常量的命名相同的问题。
  4. 便于调试
    define是完全替换的。所以在调试的时候没有枚举方便。
  5. 使用方便,一次可以定义多个常量。

枚举的使用

enum Color
{
    RED = 1,
    GREEN = 2,
    BLUE = 3
};
int main()
{
    //只能拿枚举常量给枚举变量赋值,才不会出现类型差异
    enum Color clr = GREEN;
    clr = BLUE;
    clr = 1;//???这样的方式是否可行呢?
    return 0;
}

解答:右边时int类型的数据左边是枚举类型,是不能进行赋值的。

枚举变量的大小就是一个整形的大小。

联合(共用体)

联合类型的定义

联合也是一种特殊的自定义类型。这种类型定义的变量也包含一系列成员,特征是这些成员公用一块空间(所以联合也叫共用体。)。

举例:

union Un {
    char c;
    int i;
};

int main() {
    //联合变量的定义
    union Un un;
    //计算联合变量的大小
    printf("%d\n", (int )sizeof(un));
    return 0;
}

联合的地址

    printf("%p\n", &un);
    printf("%p\n", &un.c);
    printf("%p\n", &un.i);

结果是一个地址。

联合的特点

联合的成员是公用一块内存空间的,这样一个联合体变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

联合体使用的时候,就上面的例子而言c和i是不能同时使用的。

面试题:

判断计算机大小端存储。

int check_sys(){
    union Un{
        char c;
        int i;
    }u;
    u.i = 1;
    //如果是小端返回1大端返回0
    return u.c;
}
int main(){
    int res = check_sys();
    if(1==res){
        printf("小端\n");
    }
    else{
        printf("大端\n");
    }
}

解析:利用联合体的特性。
将1的值存入一个联合体的int中char用来使用,如果是小端存储char类型变量的值为1,大端存储的情况是char类型变量的值为0。

联合大小的计算

联合的大小至少是最大成员的大小。

当最大的成员大小不是最大对齐数的整数倍的时候,就于要对齐到最大对齐数的整数倍。

union Un {
    int a ;
    char arr[11];
};

int main() {
    union Un u;
    printf("%d\n", (int)sizeof(u));//12
    return 0;
}

计算的时候一定注意联合的特性,第一个地址是公用的。
计算机大小端存储。

int check_sys(){
    union Un{
        char c;
        int i;
    }u;
    u.i = 1;
    //如果是小端返回1大端返回0
    return u.c;
}
int main(){
    int res = check_sys();
    if(1==res){
        printf("小端\n");
    }
    else{
        printf("大端\n");
    }
}

解析:利用联合体的特性。
将1的值存入一个联合体的int中char用来使用,如果是小端存储char类型变量的值为1,大端存储的情况是char类型变量的值为0。

联合大小的计算

联合的大小至少是最大成员的大小。

当最大的成员大小不是最大对齐数的整数倍的时候,就于要对齐到最大对齐数的整数倍。

union Un {
    int a ;
    char arr[11];
};

int main() {
    union Un u;
    printf("%d\n", (int)sizeof(u));//12
    return 0;
}

计算的时候一定注意联合的特性,第一个地址是公用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黎丶辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值