C语言自定义类型

C语言自定义类型

结构体

结构体类型的声明

定义

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

声明

  1. 语法

    struct 自定义类型名
    {
        成员列表;
    }变量列表;
    
  2. 案例

    定义一个学生类型,学生的属性要求包含名字,电话,性别,年龄:

    struct Student
    {
        char name[20];//姓名
        char tele[12];//电话
        char sex[10]; //性别
        int age;	  //年龄
    }std3,std4,std5;			//std3,std4,std5是在结构体定义时就创建好的全局结构体变量
    
    struct Student std2;		// 创建结构体全局变量
    
    int main()
    {
    	struct Student std1;	// 创建结构体变量
        return 0;
    }
    

    说明:

    1. std1是局部变量,std2、std3、std4、std5是全局变量
    2. std3,std4,std5是在结构体定义时就创建好的全局结构体变量,如果不需要定义结构体及创建变量,可以省略std3,std4,std5。注意:变量列表可以省略,但是结尾的分号不能省略(易错)。
结构体的自引用

结构体中不能包含一个类型为该结构体本身的成员变量,但是可以包含一个类型为该结构体指针的成员变量。

  1. 错误案例:

    struct Student
    {
        int age;
        struct Student std;	// 包含一个类型为该结构体本身的成员变量
    };
    int main()
    {
        printf("%d", sizeof(struct Student));	// 思考,如果可以包含相同类型,那么sizeof的计算结果是多少
        return 0;
    }
    
  2. 正确案例:

    struct Student
    {
        int age;
        struct Student* nextStd;
    };
    int main()
    {
        printf("%d", sizeof(struct Student));	// int类型占4字节,指针类型占4字节,所以sizeof的计算结果是8字节
        return 0;
    }
    
结构体的重命名

使用typedef关键字可以对结构体类型进行重命名,例如下列代码段,将struct Student类型重命名为了Std类型。重命名后,使用原结构体名和新结构体名都可以创建结构体变量:

typedef struct Student
{
    int age;
    char name[20];
}Std;

int main()
{
    struct Student std1;	// 使用结构体名创建变量
	Std std2;				// 使用结构体别名创建变量
    return 0;
}
结构体变量的定义和初始化
  1. 定义

    • 方式一:声明结构体的同时,定义结构体变量

    • 方式二:先声明结构体,再定义结构体变量

      /* 案例代码 */
      struct Student
      {
          int age;
          char name[20];
      }std1;	//方式一:声明结构体的同时,定义变量std1
      
      int main()
      {
          struct Student std2;	// 方式二:先声明结构体Student,再定义结构体变量std2
          return 0;
      }
      
  2. 初始化

    • 方式一:定义结构体变量的同时,进行初始化

    • 方式二:先定义结构体变量,再进行初始化

      /* 案例代码 */
      struct Student
      {
          int age;
          char name[20];
      };
      
      int main()
      {
          struct Student std1 = {10,"zhangsan"};	// 方式一:定义结构体变量的同时,进行初始化
          
          
          struct Student std2;	// 方式二:先定义结构体变量,再进行初始化
          std2.age = 20;
          std2.name = "lisi"
      }
      
结构体内存对齐
  1. 结构体对齐规则:

    1. 第一个成员在于结构体变量偏移量为0的地址处。

    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍处。

      对齐数 = 编译器默认的一个对齐数与该成员比较,取较小值(如果成员是数组,则用数组中元素类型进行比较)

      例如:假设编译器默认的对齐数是8,一个int类型的大小是4,两者相比较,得出该int类型的对齐数是4

      vs编译器默认的对齐数是8

      gcc编译器没有默认的对齐数

    3. 结构体的总大小为最大对齐数(每个成员变量都有自己的对齐数 )的整数倍。

    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

  2. 案例:

    1. 案例1-请分析以下代码段的输出结果:

      /*假设当前使用的是vs编译器, 默认的对齐数是8*/
      
      struct S1
      {
          char c1;	// 成员自己的对齐数为1,编译器对齐数为8,两者比较取较小值为1
          int a;		// 成员自己的对齐数为4,编译器对齐数为8,两者比较取较小值为4
          char c2;	// 成员自己的对齐数为1,编译器对齐数为8,两者比较取较小值为1
      }
      
      struct S2
      {
          char c1;
          char c2;
          int a;
      }
      
      int main()
      {
          struct S1 s1 = {0};
          printf("%d ", sizeof(s1));
          struct S1 s2 = {0};  
          printf("%d\n", sizeof(s2));
          
          return 0;
      }
      

      输出结果为:12 8

      S1内存分析:

      1. 结构体S1的c1变量存储在偏移量为0的地址处,占用1个字节。(第一个成员在于结构体变量偏移量为0的地址处)
      2. 结构体S1的a变量存储在偏移量为4的地址处,占用4个字节。(成员变量要对齐到对齐数的整数倍处,a的对齐数是4)
      3. 结构体S1的c2变量存储在偏移量为8的地址处,占用1个字节。(变量a占用了偏移量4~8的地址)
      4. 根据上述分析S1已经占用了偏移量0~9的地址,根据结构体对齐规则3,结构体的总大小为最大对齐数的整数倍,最大对齐数是变量a的4字节,所以向上取整占用12字节地址空间

      S2内存分析:

      1. 结构体S2的c1变量存储在偏移量为0的地址处,占用1个字节。
      2. 结构体S1的c2变量存储在偏移量为1的地址处,占用1个字节。
      3. 结构体S1的a变量存储在偏移量为4的地址处,占用4个字节。(成员变量要对齐到对齐数的整数倍处,a的对齐数是4)
      4. 根据上述分析S1已经占用了8个字节的地址空间,已经是最大对齐数a的4字节整数倍,无需再向上空间取整。
    2. 案例2-请分析以下代码段的输出结果:

      /*假设当前使用的是vs编译器, 默认的对齐数是8*/
      
      struct S3
      {
          double d;
          char c;
          int i;
      }
      
      struct S4
      {
          char c1;
          struct S3 s3;
          double d;
      }
      
      int main()
      {
          struct S4 s = {0};
          printf("%d\n", sizeof(s));
          
          return 0;
      }
      

      输出结果为:32

      S4内存分析:

      1. 首先明确S3的大小为16,最大对齐数为8
      2. 结构体S4的c1变量存储在偏移量为0的地址处,占用1个字节。
      3. 结构体S4的c2变量存储在偏移量为8的地址处,占用16个字节。(根据结构体对齐规则第4条,嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处)
      4. 结构体S4的d变量存储在偏移量为24的地址处,占用8个字节。
      5. 根据上述分析S4已经占用了32个字节的地址空间,已经是最大对齐数d的8字节整数倍(参考结构体对齐规则第4条),无需再向上空间取整。
  3. 为什么存在内存对齐?

    • 平台原因:不是所有硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    • 性能原因:数据结构(尤其是栈)应该尽量在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器可能需要进行2次访存才能拿到一个完整的成员变量。而访问对齐的内存,处理器只需要进行1次访存,即可拿到一个完整的成员变量。

      总体来说,内存对齐是一种拿空间换时间的做法。

  4. 设计结构体时,怎样节省空间?

    1. 原则:让占用空间更小的成员尽量集中在一起

    2. 案例:

      请分析以下代码段的输出结果

      /*假设当前使用的是vs编译器, 默认的对齐数是8*/
      struct S1
      {
          char c1;
          int a;
          char c2;
      };
      
      struct S2
      {
          char c1;
          char c2;
          int a;
      }
      
      int main()
      {
          printf("%d, %d\n", sizeof(S1), sizeof(S2));   
          return 0;
      }
      

      输出结果为:12 8

      说明:S1和S2类型的成员一摸一样,但是S1和S2所占的空间大小有了一些区别。

  5. 修改默认对齐数

    C语言提供了#pragma这个预处理命令,可以修改默认对齐数

    语法:#pragma pack(默认对齐数)

    案例:

    请分析以下代码段的输出结果

    /*假设当前使用的是vs编译器, 默认的对齐数是8*/
    
    #pragma pack(4) // 设置默认对齐数
    struct S1
    {
        char c1;
    	double d1;
    };
    #pragma pack() // 取消默认对齐数
    
    struct S2
    {
        char c1;
    	double d1;
    };
    
    int main()
    {
        printf("%d, %d\n", sizeof(S1), sizeof(S2));   
        return 0;
    }
    

    输出结果为:12 16

    S1内存分析:

    1. 结构体S1的c1变量存储在偏移量为0的地址处,占用1个字节。(第一个成员在于结构体变量偏移量为0的地址处)
    2. 结构体S1的d1变量存储在偏移量为4的地址处,占用8个字节。(默认对齐数是4,double类型对齐数是8,两者比较取较小值4)
    3. 根据上述分析S1已经占用了12个字节的地址空间,已经是最大对齐数4的整数倍,无需再向上取整。

    S2内存分析:

    1. 结构体S2的c1变量存储在偏移量为0的地址处,占用1个字节。(第一个成员在于结构体变量偏移量为0的地址处)
    2. 结构体S2的d1变量存储在偏移量为8的地址处,占用8个字节。(默认对齐数是8,double类型对齐数是8,两者比较取较小值8)
    3. 根据上述分析S2已经占用了16个字节的地址空间,已经是最大对齐数4的整数倍,无需再向上取整。

    结论:当结构体对齐方式不适用业务场景时,我们可以自己更改默认对齐数。更改时需要注意,默认对齐数要设置成2的整数倍。

  6. 计算成员变量在结构体中的偏移量

    使用C语言中的offsetof宏可以计算成员变量在结构体中的偏移量

    语法:offsetof(结构体类型, 成员变量名)

    头文件:使用offsetof宏,需要导入<stddef.h>头文件

    案例:

    请分析以下代码段的输出结果

    struct Std
    {
      char c1;
      int a;
      double d;
    };
    
    int main()
    {
        printf("%d, %d, %d", offsetof(Std, c1), offsetof(Std, a), offsetof(Std, d));
    	return 0;
    }
    

    输出结果:0,4,8

结构体传参

结构体既可以值传递,也可以地址传递

  1. 值传递

    struct Std
    {
      char c1;
      int a;
      double d;
    };
    
    void Init(struct Std std1)
    {
        std1.c1 = 'b';
        std1.a = 10;
        std1.d = 10.0
    }
    
    
    int main()
    {
        struct Std std1 = {'a', 1, 2.0};
        Init(std1);
        printf("%c, %d, %f", std1.c1, std1.a, std1.d);
    	return 0;
    }
    

    输出结果:a, 1, 2.0

    值传递会在内存中拷贝一份相同的结构体类型传递给形参,形参被修改不会影响实参的值

  2. 地址传递

    struct Std
    {
      char c1;
      int a;
      double d;
    };
    
    void Init(struct Std* std1)
    {
        std1.c1 = 'b';
        std1.a = 10;
        std1.d = 10.0
    }
    
    
    int main()
    {
        struct Std std1 = {'a', 1, 2.0};
        Init(&std1);
        printf("%c, %d, %f", std1.c1, std1.a, std1.d);
    	return 0;
    }
    

    输出结果:b, 10, 10.0

    地址传递形参和实参指向了内存中的同一个结构体变量,修改形参,实参的值也会改变。

  3. 总结

    • 使用结构体值传递方式时,如果结构体过大,参数压栈的系统开销比较大,会导致程序性能下降。
    • 所以结构体传参时,推荐使用地址传递方式。如果期望该结构体不要在传递过程中被修改,可以将形参用const关键字进行修饰。
结构体实现位段(位段的填充&可移植性)
  1. 什么是位段

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

    1. 位段的成员必须是整型(int、 unsigned int 、signed int、short、char …)。
    2. 位段的成员名后有一个冒号和数字。
  2. 位段的声明

    /* 位段的声明案例 */
    struct S
    {
        int a : 2;
        int b : 5;
        int c : 10;
        int d : 30;
    };
    
    int main()
    {
        struct S s;
        return 0;
    }
    
  3. 位段位的说明

    1. 位段的成员名后冒号紧接着的数字,表示这个整型占几个比特位。例如:
      1. 结构体S中 int a : 2;表示变量a占用2个bit位(a的值在0~2^2之间)
      2. 结构体S中 int b : 5;表示变量a占用2个bit位(a的值在0~2^5之间)
      3. 结构体S中 int c : 10;表示变量a占用2个bit位(a的值在0~2^10之间)
      4. 结构体S中 int c : 30;表示变量a占用2个bit位(a的值在0~2^30之间)
  4. 位段的内存分配规则

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

    请分析以下代码段的输出结果

    struct S
    {
        int a : 2;
        int b : 5;
        int c : 10;
        int d : 30;
    };
    
    int main()
    {
        printf("%d\n", sizeof(S));
        return 0;
    }
    

    输出结果:8

    说明:按照位段的内存分配规则2,结构体S的空间是按4字节为单位开辟的,首先开辟一个4字节空间(031bit),元素a占用01bit,元素b占用26bit,元素c占用716bit。由于剩余的空间不够分配给元素d(元素d需要占用30个bit),所以需要再开辟一个4字节的空间,前0~29bit用于存放变量d。所以,结构体S总共占用8字节。

  6. 位段的跨平台问题

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

    思考,既然位段既对数据类型有严格限制,由不支持跨平台,这么难用的数据类型,到底有什么应用场景那?

    答案:大家知道IP数据报的首部都是以4字节的整数倍进行填充的,这种数据类型就非常适合使用位段进行填充。

    IP数据报首部

枚举

枚举顾名思义就是把可能的值一 一列举

比如我们现实生活中:

一周的星期只有七天

性别只有男女

月份只有12个月

这些有限的取值范围代表特定的含义,就可以使用枚举类型

枚举类型的定义
enum Day	// 星期
{
    Mon,
    True,
    Wed,
    Thur,
    Fir,
    Sat,
    Sun
};

enum Sex	// 性别
{
    MALE = 0,
    FEMALE = 2,
    SECRET = 4
};

以上定义的Day和Sex都是枚举类型。{ }中的内容是枚举的可能取值,也叫枚举常量。使用枚举值时,需要注意以下事项:

  • 枚举常量间用逗号隔开,最后一个枚举常量后不需要符号。(易错)
  • { }后需要以分号结尾。(易错)
  • 枚举常量都是有值的,默认从0开始,一次递增1。
  • 当某个枚举常量被赋初值,后续的枚举常量如果没有被赋初值,会接着上一个枚举常量的值递增1。
枚举的优点
  1. 使用枚举可以增加代码的可读性和可维护性。
  2. 和#define定义的标识符比,枚举有类型检查,更加严谨。
  3. 使用枚举可以防止命名污染(封装)
  4. 便于调试
  5. 使用方便,一个枚举类型中,一次可以定义多个枚举常量
枚举的使用
enum Color	// 颜色
{
    RED = 1,
    GREEN = 2,
    BLUE = 4
};

int main()
{
    enum Color clr = RED;	// 只能拿枚举常量给枚举赋值
    clr = (enum Color)1;	// 整数值赋值必须在枚举常量的取值范围内,否则可能会导致不可预测的结果。
    return 0;
}

联合(共用体)

联合类型的定义

联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间

union Un	// 联合体的声明
{
    char c;
    int 1;
};

int main()
{
    union Un u;	// 联合体的定义
    printf("%d\n", sizeof(u));	// 计算联合体的大小
	return 0;   
}
联合的特点
  1. 特点:

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

  2. 案例:

    假设结构体union Un在内存中的起始地址是0x000000,请分析以下代码段的输出结果

    union Un
    {
        char c;
        int 1;
    };
    
    int main()
    {
        union Un u;
        printf("%d\n", sizeof(u));
        printf("%p\n", &u);    
        printf("%p\n", &(u.c));   
        printf("%p\n", &(u.i));   
        return 0;
    }
    

    输出结果:

    ​ 4

    ​ 0x000000

    ​ 0x000000

    ​ 0x000000

    内存分析:

    ​ 因为联合体占用的是同一块内存空间,所以char cint i的起始地址都是0x000000。

    ​ 因为联合体至少要能装的下最大成员,所以Un至少需要4字节的空间,才能装下int i

联合大小的计算
  1. 大小计算原则

    • 联合体的大小至少是最大成员的大小
    • 当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍
  2. 案例:

    请分析以下代码段的输出结果

    union Un
    {
        int a;
        char arr[5];
    };
    
    int main()
    {
        union Un u;
        printf("%d\n", sizeof(u));
        return 0;
    }
    

    输出结果:8

    内存分析:结构体如果想存下char arr[5]成员,至少需要5个字节。而Un的最大对齐数是4字节,所以根据大小计算原则2,结构体需要对齐到最大对齐数的整数倍,5字节向上取整,刚好是8字节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值