【C语言】自定义类型详解:结构体、枚举、联合(1)

简介: 【C语言】自定义类型详解:结构体、枚举、联合(1)

前言

C语言的数据类型包括基本类型(内置类型)、构造类型(自定义类型)、指针类型和空类型(void),其中基本类型就是我们常见的整形、浮点型,而自定义类型则包括数组、结构体、枚举、联合(共用体),数组我们已经非常熟悉了,今天我们主要学习自定义类型中其他几种类型:结构体、枚举以及联合。

一、结构体

结构体是一些值的集合,这些值称为成员变量;结构的每个成员可以是不同类型的变量,所以结构常用来描述复杂对象。

1、结构体的声明

一般声明

结构体的声明一般由结构体关键字 + 结构体标签 + 成员列表组成:

struct tag       //struct:结构体关键字  tag:结构体标签
{
  member - list;    //成员列表
}variable - list;  //变量列表(可以省略)

例如描述一本书:

struct Book
{
    char name[20];    //书名
    char author[20];  //作者
    char num[12];     //编号
    float price;      //价格
};    //注意最后面的这个分号不能丢

特殊声明

结构体声明的时候,可以不完全声明,即省略结构体标签,这种结构体被称为匿名结构体:

//匿名结构体
struct 
{
    member-list;
}x;

由于匿名结构体没有名字,所以不能在程序的其他位置使用该结构体创建结构体变量,而只能在结构体声明的同时定义结构体变量,也就是说,匿名结构体只能使用一次

我们可以用匿名结构体来描述一个学生:

struct
{
  char name[20];  //名字
  int age;        //年龄
  char sex[5];    //性别
  char id[20];    //学号
}stu;               //结构体变量

2、结构体的自引用

错误的自引用方式

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

上面这种结构体的声明方式是错误的,因为struct Node 中包含了一个struct Node 的Next,而Next中又会包含一个struct Node 的next,这样无限套娃,使得我们无法计算这个结构体的大小;正确的结构体自引用应该是一个结构体中包含指向该结构体的指针,如下所示:

正确的自引用方式

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

一个结构体中包含了一个指向该结构体的指针,实现了结构体的自引用,同时,由于指针的大小是固定的(4/8个字节),所以该结构体的大小也是可计算的。

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

结构体定义变量一共有两种方式,一种是在进行结构体声明的同时定义结构体变量,另一种是利用结构体类型来定义结构体变量。

struct Point
{
  int x;
  int y;
}p1;        //声明类型的同时定义变量p1
struct Point p2;  //利用结构体类型来定义变量p2

结构体变量的初始化和数组变量的初始化十分类似,在定义结构体变量的同时赋初值即可。

struct Stu        
{
  char name[15];
  int age;      
}s1 = { "zhangsan", 20 };          //初始化
struct Stu s2 = { "lisi", 22 };    //初始化
struct Node
{
  int data;
  struct Point p;
  struct Node* next; 
}n1 = {10, {4,5}, NULL};            //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

4、结构体传参

结构体传参分为两种方式:一种是传递整个结构体,这时形参需要创建一个与源结构体同等大小的空间来接收,结构体过大浪费空间的同时时会十分影响效率;另一种是传递结构体的地址,这时无论源结构体有多大,形都参只需要用一个结构体指针来接收,节省空间的同时提高效率。

struct S
{
  int data[1000];
  int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
  printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
  printf("%d\n", ps->num);
}
int main()
{
  print1(s);  //传结构体
  print2(&s); //传结构体地址
  return 0;
}

print1 和 print2 相比,首选 print2 函数,原因如下:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。


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


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


5、结构体内存对齐(重要)

结构体内存对齐是结构体大小的计算规则,是校招笔试和面试过程中一个十分热门的考点,希望大家认真对待。

在学习结构体内存对齐之前,我们先给两组计算结构体大小的题目,看看你能否做对:

//计算结构体大小
#include <stdio.h>
struct S1 
{
  char c1;
  int i;
  char c2;
};
struct S2 
{
  char c1;
  char c2;
  int i;
};
int main()
{
  printf("%d\n", sizeof(struct S1));
  printf("%d\n", sizeof(struct S2));
  return 0;
}

2020062310470442.png

对答案有疑问的同学不要慌,我们在学习结构体内存对齐的过程中来分析答案的由来。

结构体内存对齐的规则

关于结构体内存对齐规则,大部分参考资料是这样说的:

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


其他成员变量要对齐到它的对齐数的整数倍的地址处。


对齐数 = 编译器默认的对齐数与该成员变量大小的较小值。

VS的默认对齐数是8.

只有VS编译器下才有默认对齐数的概念,其他编译器下变量的对齐数 = 变量的大小

结构体总大小为最大对齐数的整数倍。(最大对齐数为所有变量的对齐数的最大值)


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

知道了最大对齐数的对齐规则,我们再来看上面的练习题:

struct S1 
{
  char c1;  //变量大小为1,默认对齐数为8 -> 对齐数为1
  int i;    //变量大小为4,默认对齐数为8 -> 对齐数为4
  char c2;  //变量大小为1,默认对齐数为8 -> 对齐数为1
};

我们假设struct S1的起始位置为图中箭头所示位置,则各位置的偏移量如图;由内存对齐的规则:


第一个成员在与结构体变量偏移量为0的地址处:所以c1在偏移量为0处,且c1占一个字节;


其他成员变量要对齐到它的对齐数的整数倍的地址处:由于 i 的对齐数是4,所以 i 只能从偏移量为4的位置开始存储,且 i 占四个字节;


其他成员变量要对齐到它的对齐数的整数倍的地址处:由于 c2 的对齐数是1,所以 c2 紧挨着 i 存储,且 c2 占一个字节;


结构体总大小为最大对齐数的整数倍:由于最大对齐数为4,所以总对齐数要为4的倍数,大于9的最小的4的倍数为12,所以整个结构体的大小为12个字节。

2020062310470442.png

struct S2 
{
  char c1;  //变量大小为1,默认对齐数为8 -> 对齐数为1
  char c2;  //变量大小为1,默认对齐数为8 -> 对齐数为1
  int i;    //变量大小为4,默认对齐数为8 -> 对齐数为4
};

如图:

2020062310470442.png

c1 从0偏移处开始,占一个字节;c2 对齐数为1,所以紧挨着 c1 存储,占一个字节;i 对齐数为4,所以在4的整数倍位置 – 4偏移处开始存储,占4个字节;存放完毕后0~7一共占8个字节,因为最大对齐数为4,8为4的整数倍,所以不变。

6、offsetof 宏

offsetof 的介绍

offsetof 是C语言中定义的一个用于求结构体成员在结构体中的偏移量的一个宏,其对应的头文件是 ,由于 offsetof 的使用方法与函数一样,所以它经常被错误的认为是一个函数;我们可以在VS中右键单击offsetof转到定义,查看offsetof的在VS中的实现方式。

2020062310470442.png

offsetof 的参数

size_t offsetof( structName, memberName );
# size_t 返回值,返回成员的偏移量;
# structName 参数,结构体变量名;
# memberName 参数,成员变量名;

offsetof 的使用

#include <stdio.h>
#include <stddef.h>  //offsetof对应头文件
struct S1
{
  char c1;
  int i;
  char c2;
};
struct S2
{
  char c1;
  char c2;
  int i;
};
int main()
{
  printf("%d\t", offsetof(struct S1, c1));
  printf("%d\t", offsetof(struct S1, i));
  printf("%d\n", offsetof(struct S1, c2));
  printf("%d\t", offsetof(struct S2, c1));
  printf("%d\t", offsetof(struct S2, c2));
  printf("%d\n", offsetof(struct S2, i));
  return 0;
}

2020062310470442.png

offsetof 的模拟实现

我们以上面的 struct S1为例,经过上面的分析我们已经知道了 struct S1的大小为12,并且画出来具体的图示:

2020062310470442.png

我们观察后发现:结构体成员在结构体中的偏移量 = 结构体成员的地址 - 结构体的起始地址,比如 struct S1中 i 的地址 - 结构体的起始地址可以得到结构体成员 i 的偏移量等于4;那么如果结构体的起始地址在0处,那么结构体成员的偏移量 = 结构体成员的地址 - 0 = 结构体成员地址,所以我们可以把0强转为对应结构体指针类型,然后返回结构体成员的地址即可得到结构体成员的偏移量,具体代码如下:

#include <stdio.h>
#define OFFSETOF(type, member) (size_t)&(((type*)0)->member)
struct S1
{
  char c1;
  int i;
  char c2;
};
int main()
{
  printf("%d\n", OFFSETOF(struct S1, c1));
  printf("%d\n", OFFSETOF(struct S1, i));
  printf("%d\n", OFFSETOF(struct S1, c2));
  return 0;
}

2020062310470442.png

7、为什么存在内存对齐

从上面的例子我们可以看到,结构体内存对齐会浪费一定的内存空间,但是计算机不是要尽可能的做到不浪费资源吗?那为什么还要存在内存对齐呢?关于内存对齐存在的原因,大部分的参考资料是这样说的:


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

性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。所以内存对齐能够提高访问效率。

总体来说:结构体的内存对齐是拿空间来换取时间的做法。

这里我对原因中的第二点做一下解释:

大家都知道,我们的机器分为32位机器和64位机器,这里的32位和64位其实指的是CPU的位数,而CPU的位数对应着CPU的字长,而字长又决定着CPU读取数据时一次访问多大即空间,即一次读取几个字节,我们以32位机器为例:

2020062310470442.png

如图,32位机器一次访问四个字节的大小,如果不存在内存对齐,那么要取出 i 中的数据需要两次读取,存在内存对齐则只需要读取一次。

设计结构体的技巧

在了解了结构体的对齐规则之后,有没有一种方法能让我们在设计结构体的时候既满足对齐规则,又能尽量的节省空间呢?其实是有的,方法就是:**让占用空间小的成员尽量集中在一起。**就像的习题,我们把占用空间下的 c1 和 c2 放在一起,从而使得 struct S2 比 struct S1 小了四个字节。

8、修改默认对齐数

我们可以使用 “#pragma pack(num)” 命令来修改VS中的默认对齐数。例如:

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
  char c1;
  int i;
  char c2;
};
#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;
}

2020062310470442.png

在 struct S2 中,我们通过 " #pragma pack(1) " 命令把VS的默认对齐数设置为1(相当于不对齐),使得其大小变为6。

9、结构体大小计算习题

习题1

#include <stdio.h>
struct S3
{
  double d;
  char c;
  int i;
};
int main()
{
  printf("%d\n", sizeof(struct S3));
  return 0;
}

2020062310470442.png


d 从0偏移处开始存储,占8个字节,所以0~7;c 紧挨 d 存储,占一个字节,所以8,i 从4的整数倍即12处开始存储,占4个字节,所以12~15;所以0 ~ 15合计16个字节,16为最大对齐数8的倍数,所以不变。

习题2

#include <stdio.h>
struct S3
{
  double d;
  char c;
  int i;
};
struct S4
{
  char c1;
  struct S3 s3;
  double d;
};
int main()
{
  printf("%d\n", sizeof(struct S4));
  return 0;
}

c1 从0偏移位置开始存储,占一个字节,所以0;struct S3 s3 我们上面已经算出占16个字节,又因为嵌套的结构体对齐到自己的最大对齐数的整数倍处,所以从8的整数倍即8偏移处开始存储,所以8~23;d 从8的整数倍即24偏移处开始存储,占8个字节,所以24~31;合计32个字节,且为最大偏移数8的整数倍,所以不变。

习题3

#include <stdio.h>
#pragma pack(4)
struct tagTest1
{
    short a;
    char d;
    long b;
    long c;
};
struct tagTest2
{
    long b;
    short c;
    char d;
    long a;
};
struct tagTest3
{
    short c;
    long b;
    char d;
    long a;
};
#pragma pack()
int main(int argc, char* argv[])
{
    struct tagTest1 stT1;
    struct tagTest2 stT2;
    struct tagTest3 stT3;
    printf("%d %d %d", sizeof(stT1), sizeof(stT2), sizeof(stT3));
    return 0;
}

2020062310470442.png

stT1:

a: 0~1 d:2 b:4~7 c:8~11 合计:0~11 = 12(4的倍数);

stT2:

b:0~3 c:4~5 d:6 a:8~11 合计:0~11 = 12(4的倍数);

stT3:

c:0~1 b:4~7 d:8 a:12~15 合计:0~15 = 16(4的倍数);





相关文章
|
25天前
|
存储 C语言
如何在 C 语言中实现结构体的深拷贝
在C语言中实现结构体的深拷贝,需要手动分配内存并逐个复制成员变量,确保新结构体与原结构体完全独立,避免浅拷贝导致的数据共享问题。具体方法包括使用 `malloc` 分配内存和 `memcpy` 或手动赋值。
31 10
|
24天前
|
安全 编译器 Linux
【c语言】轻松拿捏自定义类型
本文介绍了C语言中的三种自定义类型:结构体、联合体和枚举类型。结构体可以包含多个不同类型的成员,支持自引用和内存对齐。联合体的所有成员共享同一块内存,适用于判断机器的大小端。枚举类型用于列举固定值,增加代码的可读性和安全性。文中详细讲解了每种类型的声明、特点和使用方法,并提供了示例代码。
23 3
|
24天前
|
存储 大数据 编译器
C语言:结构体对齐规则
C语言中,结构体对齐规则是指编译器为了提高数据访问效率,会根据成员变量的类型对结构体中的成员进行内存对齐。通常遵循编译器默认的对齐方式或使用特定的对齐指令来优化结构体布局,以减少内存浪费并提升性能。
|
28天前
|
编译器 C语言
共用体和结构体在 C 语言中的优先级是怎样的
在C语言中,共用体(union)和结构体(struct)的优先级相同,它们都是用户自定义的数据类型,用于组合不同类型的数据。但是,共用体中的所有成员共享同一段内存,而结构体中的成员各自占用独立的内存空间。
|
28天前
|
存储 C语言
C语言:结构体与共用体的区别
C语言中,结构体(struct)和共用体(union)都用于组合不同类型的数据,但使用方式不同。结构体为每个成员分配独立的内存空间,而共用体的所有成员共享同一段内存,节省空间但需谨慎使用。
|
1月前
|
编译器 C语言 C++
C语言结构体
C语言结构体
25 5
|
存储 C语言
【C语言】 条件操作符 -- 逗号表达式 -- []下标访问操作符,()函数调用操作符 -- 常见关键字 -- 指针 -- 结构体
【C语言】 条件操作符 -- 逗号表达式 -- []下标访问操作符,()函数调用操作符 -- 常见关键字 -- 指针 -- 结构体
【C语言】——define和指针与结构体初识
【C语言】——define和指针与结构体初识
|
存储 C语言
C语言初识-关键字-操作符-指针-结构体
C语言初识-关键字-操作符-指针-结构体
63 0
【C语言】指针,结构体,链表
【C语言】指针,结构体,链表