C语言

12. C语言进阶--自定义类型

Posted on 2022-02-11,15 min read

4. 自定义类型

4.1 结构体

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

4.1.1 结构体类型声明

  • 普通声明

    struct Person
    {
      char name[10];
      int age;
      float height;
    };
    
    struct Person
    {
      char name[10];
      int age;
      float height;
    }p1, p2; // 声明结构体类型并声明结构体变量p1、p2
    
    
  • 特殊声明(匿名结构体)

    省略标签,不完全声明结构体类型。只能声明一次变量。

    struct
    {
      char name[10];
      int age;
      float height;
    }p; // 声明匿名结构体类型 并声明结构体变量
    
    struct
    {
      char name[10];
      int age;
      float height;
    }*p; // 声明匿名结构体类型 并声明结构体指针变量
    
    

    那么上面的两个结构体变量能否写成如下形式?

    ps = &s;
    

    警告:编译器将两种声明当成完全不同的两个类型

4.1.2 结构体的自引用

一个结构体中可以包含另一个另一个结构体类型。那结构体中能否包含本身结构体类型?

// 答案:不能
// 原因:形成递归 内存大小无法确定

自引用

该结构体变量能找到同类型的另一个结构体变量,声明中包含同类型结构体的指针,而不是同类型结构体的变量。

// 正确自引用
struct Node
{
  int data;
  struct Node* next;
}

// 错误自引用
typedef struct
{
  int data;
  Node* next;
}Node;
// 通过类型重命名得到的Node 不能再结构体声明中直接使用

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

  • 定义

    struct Person
    {
      char name[10];
      int age;
      float height;
    }p1; // 方式一
    
    int main()
    {
      struct Person p2; // 方式二
      return 0;
    }
    
    
  • 初始化

    struct Person
    {
      char name[10];
      int age;
      float height;
    }p1 = {"listen", 20, 185.0}; // 方式一
    
    int main()
    {
      struct Person p2 = { "turbo", 25, 178.5 }; // 方式二
      return 0;
    }
    
  • 结构体嵌套初始化

    struct Person
    {
      char name[10];
      int age;
      float height;
    }
    
    struct Student
    {
      char id[20];
      float score;
      struct Person p;
    }s1 = { "1914121006", 63.95, {"listen", 20, 185.0} }; // 方式一
    
    
    int main()
    {
      struct Student s2 = { "2114134566", 78.50, {"turbo", 19, 178.5} }; // 方式二
      return 0;
    }
    

4.1.4 结构体内存对齐

  • 内存对齐规则

    1. 第一个成员在与结构体变量偏移量为0的地址处
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
      对齐数=编辑器默认的一个对齐数与该成员大小的较小值(VS中默认的值为8,linux系统中没有默认对齐数)
    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
    4. 嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)中的最大值的整数倍
    • 实例1

      struct S1
      {
        char c1;
        int i;
        char c2;
      };
      
      struct S2
      {
        char c1;
        char c2;
        int i;
      };
      
      int main()
      {
        printf("%d\n", sizeof(struct S1)); // 12
        printf("%d\n", sizeof(struct S2)); // 8
      }
      
      // 输出结果:
      // 12
      // 8
      


    • 实例2

      struct S1
      {
        char c1;
        int i;
        char c2;
      };
      
      struct S2
      {  
        int i;
        char c;
        struct S1 s;
      };
      
      int main()
      {
        printf("%d\n", sizeof(struct S2)); // 20
      }
      
      // 输出结果:
      // 20
      


  • 为什么存在内存对齐 — 空间换时间

    1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2. 硬件原因:经过内存对齐之后,可以减少CPU访问内存的次数和运算,提高效率。
  • 修改默认对齐数

    #pragma pack(8) // 设置默认对齐数为8
    #pragma pack() // 取消设置的默认对齐数 还原为默认值
    
    // 一般设置的默认对齐数为2的倍数
    
  • 补充

    通过实例1可得,调整结构体成员顺序,将较小的成员放在一块,可以在一定程度上减小结构体的大小。

4.1.5 结构体传参

struct Data
{
  int id;
  char data[1000];
};

void print1(struct Data d)
{
  printf("%d --> %s\n", d.id, d.data);
}

void print1(struct Data *d)
{
  printf("%d --> %s\n", d->id, d->data);
}

int main()
{
  struct Data d = { 0 };
  print1(d);
  print2(&d);
  return 0;
}

  • print1print2哪个函数更好?
    答案:print2
    原因:如果传递的是结构体变量,函数传参时,将拷贝结构体,将造成一定程度空间上和时间上的系统开销;而传递的是结构体指针,指针变量也就4或8字节,并不会造成过大的系统开销。此外,传递结构体指针,还能在函数体内修改结构体变量的成员值;如果不想修改,只需在函数形参前加上const

4.1.6 百度笔试题

  • 要求:写一个宏,计算结构体中某变量相对于首地址的偏移量,并给出说明
  • 考察offsetof宏的实现

4.2 位段(或位域)

4.2.1 什么是位段

定义结构体时,指定成员变量所占用的二进制位。数据存储时,并不需要一个完整的字节,只需要占用一个或几个字节即可,因此来节省空间。

  • 位段的成员必须是int、unsigned int 或 signed int
  • 位段的成员名后边有一个冒号和一个数字
  • 位域在本质上就是一种结构类型,不过其成员是按二进位分配的

例如

struct bs{
    int a:8;
    int b:2;
    int c:1;
};
// a占8位 b站2位 c站1位

4.2.2 位域定义

struct 位域结构名 
{
  type [member_name] : width ;
  type [member_name] : width ;
  type [member_name] : width ;
  ...
};
// type: 只能为 int、unsigned int、signed int 决定如何解释位域的值
// member_name:位域名称
// width:位域中位的数量 宽度必须小于或等于指定类型的位宽度

4.2.3 位域的使用

位段的使用和结构体变量相同。

位域变量名.位域名
位域指针变量名->位域名

  • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。

    struct bs{
        int a:4;
        int  :4;    /* 空域 */
        int b:4;    /* 从下一单元开始存放 */
        int c:4;
    }
    
    // a 占第一字节的 4 位,后 4 位填 0 表示不使用
    // b 从第二字节开始,占用 4 位,c 占用 4 位
    
  • 位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度。

  • 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。

    struct bs{
        int a:4;
        int  :3;    /* 空域 3不能使用 */
        int b:2;
        int c:1;
    }
    
  • 位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。

4.2.2 位段的内存分配

  • 几点说明

    1. 位段的成员可以是int、 unsigned int、 signed int、char、signed char、unsigned char (整形家族) 类型(其中char、signed char、unsigned char 类型C标准中并没有规定,仅仅是VS编译器对其进行了扩充)
    2. 位段的空间上是按照需要以4byte(int)或者1byte(char)的方式开辟
    3. 位段涉及很多不确定因素,位段是不跨平台的,注意可移植程序应避免使用位段
    4. C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间
  • 位段的大小是多少呢?

    struct bs {
        int a : 4;
        int : 3;
        int b : 2;
        int c : 1;
    };
    
    int main()
    {
        printf("%d\n", sizeof(struct bs)); // 4
        return 0;
    } 
    // 输出结果:
    // 4
    
  • 基本存储规则

  1. 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
  2. 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC会压缩存储,而 VC/VS 不会(与不指定位宽时的存储方式相同)。
  3. 如果成员之间穿插着非位域成员,那么不会进行压缩。
  • VS中,内存分配如图所示

    实例:位段S的内存分配。


4.2.3 位段的跨平台问题

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

4.2.4 位段的应用

TCP头部报文


4.2.5 习题

代码输出结果

#include <stdio.h>
#include <string.h>

int main()
{
  unsigned char puc[4];
  struct tagPIM
  {
    unsigned char ucPim1;
    unsigned char ucData0 : 1;
    unsigned char ucData1 : 2;
    unsigned char ucData2 : 3;
  } *pstPimData;
  pstPimData = (struct tagPIM*)puc;
  memset(puc, 0, 4);
  pstPimData->ucPim1 = 2;
  pstPimData->ucData0 = 3;
  pstPimData->ucData1 = 4;
  pstPimData->ucData2 = 5;
  printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]); // 02 29 00 00 
  return 0;
}


4.2 枚举

C 语言中的一种基本数据类型。将可能的值一一列举。

// 比如:星期 1~7
//      性别 男 女 其他
//      月份 1~12

4.2.1 枚举类型的定义

enum Day // 星期
{
  Mon,
  Tues,
  Wed,
  Thur,
  Fri,
  Sat,
  Sun
};
enum Sex // 性别
{
  Male,
  Female,
  Other
};

// 第一个枚举成员的默认值为整型的 0
// 后续枚举成员的值在前一个成员上加 1
// 如果将第一个枚举的成员变量定义为1
// 那么第二个就为2 一次类推 自增1
// 没有指定枚举元素的值 默认为前一个元素+1
// C语言中 枚举类型是被当做 int 或者 unsigned int 类型来处理

4.2.2 枚举变量的定义

  • 先定义类型,后定义枚举变量

    enum Sex // 性别
    {
      Male,
      Female,
      Other
    };
    
    enum Sex s;
    
  • 定义类型的同时定义枚举变量

    enum Sex // 性别
    {
      Male,
      Female,
      Other
    } s;
    
  • 省略枚举名称,直接定义枚举变量

    enum // 性别
    {
      Male,
      Female,
      Other
    } s;
    

4.2.2 枚举的优点

  • 增加代码的可读性和可维护性
  • 和#define定义的标识符比较枚举有类型检查,更加严谨 枚举是一种类型
  • 防止命名污染(封装)
  • 便于调试。#define在预编译时替换,在调试时只有被替换值
  • 使用方便,一次定义多个常量(相比于#define)

4.2.3 枚举的使用

enum Sex// 性别
{
  Male,
  Female,
  Other
}s;

int main()
{
  enum Sex s = Female;
  printf("%d\n", s); // 1
  printf("%d\n", sizeof(s)); // 4
  return 0;
}

4.2.4 枚举遍历

enum Day // 星期
{
  Mon,
  Tues,
  Wed,
  Thur,
  Fri,
  Sat,
  Sun
};

int main()
{
  enum Day day;
  for (day = Mon;  day <= Sun; day++)
  {
    printf("%d\n", day);
  }
  return 0;
}

// 输出结果:
// 0
// 1
// 2
// 3
// 4
// 5
// 6

4.3 联合体(或共用体)

4.3.1 联合类型的定义

是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

  • 格式

    union [union tag]
    {
      member definition;
      member definition;
      member definition;
       ...
    } [union variables];
    // [] 内的值是可选的
    // union定义与结构体定义相似
    
  • 实例

    union Data
    {
      char c;
      int i;
    } d;
    

    共用体变量d的内存占用情况

4.3.3 共用体使用

共用体成员的访问,使用成员访问操作符.

// 实例

union Data
{
  char c;
  int i;
} d;

int main()
{
  d.i = 10000000;
  printf("%d\n", d.i); // 10000000
  printf("%d\n", d.c); // -128

  d.c = 'a';
  printf("%d\n", d.i); // 9999969
  printf("%d\n", d.c); // 97
  return 0;
}
  • 应用—判断机器大小端字节序

    • 字符指针

      int sysinfo()
      {
        // 返回值 0 -- 大端存储
        // 返回值 1 -- 小端存储
        int a = 1;
        return *(char*)(&a);
      }
      
      int main()
      {
        printf("%d\n", sysinfo()); // 1
        return 0;
      }
      
    • 共用体

      int sysinfo()
      {
        // 返回值 0 -- 大端存储
        // 返回值 1 -- 小端存储
        union Data
        {
          char c;
          int i;
        } d;
        d.i = 1;
        return d.c;
      }
      
      int main()
      {
        printf("%d\n", sysinfo()); // 1
        return 0;
      }
      

4.3.3 联合的特点

联合体成员是共用一块内存空间,一个联合变量的大小,至少是最大成员的大小。

union Data
{
  char c;
  int i;
} d;

int main()
{
  printf("%p\n", &d);
  printf("%p\n", &d.c);
  printf("%p\n", &d.i);
  return 0;
}


4.3.4 联合大小的计算

  • 联合的大小至少是最大成员的大小
  • 当最大成员大小不是最大对齐数的正数倍时,对齐到最大对齐数的整数倍
union U1
{
  char c[5]; // 对齐数 1  占用 5byte
  int i;     // 对齐数 4  占用 4byte
};
// 最大共用体成员占用内存5byte 最大对齐数4 所以共用体总占用内存 8byte (4byte × 2 > 5byte) 

union U2
{
  short c[7]; // 对齐数 2 占用 14byte
  int i;      // 对齐数 4 占用 4byte
};
// 最大共用体成员占用内存14byte 最大对齐数4 所以共用体总占用内存 16byte (4byte × 4 > 14byte)

int main()
{
  printf("%d\n", sizeof(union U1)); // 8
  printf("%d\n", sizeof(union U2)); // 16
}

4.3.5 练习

代码输出结果。

int main()
{
  union
  {
    short k;
    char i[2];
  }*s, a;
  s = &a;
  s->i[0] = 0x39;
  s->i[1] = 0x38;
  printf("%x\n", a.k);
  return 0;
}


对于a.k,short类型,在vs中小端存储,数值低位存储在低地址内存中,数值高位存储在高地址中。所以s.k=0x3839

4.4 实战-通讯录

  • 要求
    • 通讯录中能够存放1000个人的信息
      • 信息:名字、年龄、性别、电话、地址
    • 增加人的信息
    • 删除指定人名的信息
    • 修改指定人名的所有信息
    • 查找指定人名的信息
    • 排序通讯录的信息

下一篇: 11. C语言进阶--字符串+内存函数→