自定义类型:结构体

结构体类型的声明

复制代码
struct tag
{
    member-list;

}variable-list;

tag表示结构体的类型名

member-list表示结构体的成员列表

variable-list表示结构体的变量类型

例如描述一个学生

复制代码
struct Stu
{
char name[20];//名字
 int age;//年龄
 char sex[5];//性别
char id[20];//学号
 }; //分号不能丢

这里为什么年龄没有字符数组?名字、性别、学号:本质是文本信息,在C语言中,文本(字符串)需要用字符数组(如char name[20])来存储,因为一个字符串本身就是由一系列字符组成的序列。数组能容纳多个字符,并预留空间用于存放结束符\0

年龄:本质是一个整数值。它表示的是一个数量(岁数),是单一的数值数据。在C语言中,直接用int(整型)这种基本数据类型来存储是最自然、最高效的方式

结构体变量的创建和初始化

复制代码
#include <stdio.h>
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	//按照结构体成员的顺序初始化

	struct Stu s = { "张三", 20, "男", "20230818001" };
	printf("name: %s\n", s.name);
	printf("age : %d\n", s.age);
	printf("sex : %s\n", s.sex);
	printf("id  : %s\n", s.id);
	//按照指定的顺序初始化
	struct Stu s2 = { .age = 18, .name = "李四", .id = "20230818002", .sex = "女"};
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id  : %s\n", s2.id);
	return 0;
}

这里我们定义了一个结构体,但我们没有在结构体结束的位置定义变量,我们是在初始化的时候定义了一个's'变量,但这时候定义变量就要加结构体类型,就好比我们的"int a"一样

'.'运算度是结构体中用于读取对应的数据所发明的,可以按照结构体的顺序进行初始化,也可以按照自定义顺序初始化,在自定义顺序中可以省略变量

typedef

typedef关键字用于为已有的数据类型定义一个新的名字(别名)。它的主要作用是简化复杂数据类型的声明,提高代码的可读性和可维护性

还是拿上面的代码作为示例

复制代码
#include <stdio.h>
typedef struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}S;
int main()
{
	//按照结构体成员的顺序初始化

	S s = { "张三", 20, "男", "20230818001" };
	printf("name: %s\n", s.name);
	printf("age : %d\n", s.age);
	printf("sex : %s\n", s.sex);
	printf("id  : %s\n", s.id);
	//按照指定的顺序初始化
	S s2 = { .age = 18, .name = "李四", .id = "20230818002", .sex = "女" };
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id  : %s\n", s2.id);
	return 0;
}

给结构体通过typedef创建了一个新的名字"S",然后就可以通过"S"作为结构体类型创建变量,从而达到简化代码的效果

结构的特殊声明

在声明结构的时候,可以不完全的声明

复制代码
//匿名结构体类型
#include<stdio.h>
struct
{
    int a;
    char b;
    float c;
}x = {100,'s',3.14};
int main()
{
    printf("%d %c %.2lf",x.a,x.b,x.c);
    return 0;
}

匿名结构体类型是没有(tag)标签,但在结构体尾部创建了一个变量,然后给它初始化,但匿名结构体类型只能使用一次

复制代码
#include<stdio.h>
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}* p;
int main()
{
	p = &x;
	return 0;
}

这个代码我定义了两个匿名结构体类型,编译器就不知道是哪一个匿名结构体,代码会报错

结构体的自引用

在结构中包含⼀个类型为该结构本身的成员是否可以呢?

这里就要给大家简单介绍一下链表,等后面我们会单独讲链表

复制代码
struct Node
{
    int data;
    struct Node* next;
}

这是链表的基本结构,里面存放的是数据和指向下一个节点的指针,那我们也要存储12345这五个位数字就需要5个链表,并且链表里面有指向下一个节点的指针,而最后一个节点指向的就是空指针了

结构体内存对齐方式

复制代码
#include<stdio.h>
struct S1
{
	char c1;//1个字节
	int i;  //4个字节
	char c2;//1个字节
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

在这里我们可能认为结构体在内存中的占用空间是6个字节,但输出结果是......

复制代码
结构体的大小规则
1.结构体的第一个成员对齐结构体变量起始位置偏移量为0的地址
2.其他成员变量要对齐到对齐数的整数倍的地址
3.对齐数=编译器默认的一个对齐数与成员变量大小的最小值
4.VS中默认为8(可通过#pragma修改)
5.结构体总大小为最大对齐数的倍数
6.若结构体中嵌套了其他结构体,就嵌套的结构体成员对齐到自己的最大对齐数

首先看一个,那第一个成员的地址就是偏移量为0的地址处

再看第二个,对齐数就是编译器和成员变量大小的最小值,i大小=4个字节,VS默认为8,所以对齐数就是4,所以'i'存储的位置就是偏移量为4的地址处,4的一倍是4,所以就可以存放在偏移量位4的地址处

c2占1个字节,和8作比较就是1,所以c2存放的地址就是偏移量为1的地址处

此时结构体占9个字节,现在看第五条,此时 三个数据的最大对齐数为4,所以最后结构体大小就是4的倍数,而如果是4的2倍就还有一个值没有存进来,所以就要浪费三个空间凑成4的整数倍12

所以最后占用的空间就是12个字节

这里给大家留几个题目,可以自己下去算一下

复制代码
typedef struct s
{
	char c1;
	char c2;
	int i;
}s;
int main()
{
	printf("%zd\n", sizeof(s));
	return 0;
}

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

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

为什么存在内存对齐?

大部分的参考资料都是这样说的:

平台原因

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

性能原因

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中

总体来说

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

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

让占用空间小的成员尽量集中在⼀起

复制代码
struct S1
{
char c1;
int i;
char c2;
};


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

S1 和 S2 类型的成员⼀模⼀样,但是S1和 S2所占空间的大小有了⼀些区别

修改默认对齐数

#pragma这个预处理指令,可以改变编译器的默认对齐数

复制代码
#include<stdio.h>
#pragma pack(1)  //设置VS默认对齐数为1
struct S
{
	double d;
	char c;
	int i;
};
#pragma pack()//取消VS默认对齐数为1
int main()
{
	printf("%zd\n", sizeof(struct S));
	return 0;
}

结构体传参

复制代码
#include <stdio.h>

struct S
{
    int arr[1000];
    int n;
    double d;
};

void print1(struct S tmp)
{
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        printf("%d ", tmp.arr[i]);
    }
    printf("%d ", tmp.n);
    printf("%.2lf\n", tmp.d);
}

int main()
{
    struct S s = { {1,2,3,4,5}, 100, 3.14 };
    print1(s);
    return 0;
}

这里写了一个print1的函数,实现输出操作,在这里把s这个结构体变量传过去,就要用一个结构体的空间去接收,再把结构体的数据传过去,这时候就会存在时间和空间上的浪费,所以可以改进一下代码

复制代码
#include <stdio.h>

struct S
{
    int arr[1000];
    int n;
    double d;
};

void print2(struct S* tmp)
{
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        printf("%d ", tmp->arr[i]);
    }
    printf("%d ", tmp->n);
    printf("%.2lf\n", tmp->d);
}

int main()
{
    struct S s = { {1,2,3,4,5}, 100, 3.14 };
    print2(&s);
    return 0;
}

现在我就把s的地址传过去,再创建一个结构体指针变量,一个指针变量的大小无非是4或8个字节,后期我们会讲"函数栈帧"会细讲这个部分

结构体实现位段

什么是位段

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

位段的成员必须是int,unsigned int 或signed int,在C99中位段成员的类型也可以 选择其他类型

位段的成员名后边有⼀个冒号和⼀个数字

复制代码
struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};

这样写就能很清楚的告诉别人这是一个"位段式的结构"表示二进制位,和结构体相比就是在后面多了一个冒号和数字,如果这里面的整型变量存储的数据可以用少于32个比特位表示出来,那四个整型浪费的空间就太大了,所以后面的数就表示的是该变量占用的二进制位数

第一个就是占2个比特位......,所以位段就是用来节省空间的,而这里这个位段式的结构占用空间是47个比特位,就是6个字节,那占用空间真的是6个字节吗?

很显然不是6个字节,虽然没有我们算出来这么极致的节省空间,但也达到了我们的目的,那为什么会出现这种情况呢?

位段的内存分配

  1. 位段的成员可以是int,unsigned int 或signed int,char等
  2. 位段的空间上是按照需要以4个字节或1个字节的方式来开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

先举一个例子

复制代码
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

首先给结构体中的变量赋值,赋值以后再取要占用的比特位

最后输出的结果就是3个字节

但我们赋值的值有些超过了占用的空间大小,就把多余的二进制位丢弃

我们来调试看一下

所以上面的代码大家就知道为什么是8了

位段的跨平台问题

int 位段被当成有符号数还是无符号数是不确定的

位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)

位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义

当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利用,这是不确定的

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

位段的注意事项

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的,所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员

相关推荐
love530love2 小时前
【笔记】把已有的 ComfyUI 插件发布到 Comfy Registry(官方节点商店)全流程实录
人工智能·windows·笔记·python·aigc·comfyui·torchmonitor
智航GIS2 小时前
3.2 列表(List)
数据结构·windows·list
渡我白衣2 小时前
计算机组成原理(10):逻辑门电路
android·人工智能·windows·嵌入式硬件·硬件工程·计组·数电
junlaii2 小时前
Windows Claude Code Git Bash 依赖修复教程
windows·git·bash
TheNextByte13 小时前
如何将Android中的照片传输到Windows 11/10
android·windows
染指11104 小时前
19.0环保护进程-Windows驱动
windows·驱动开发·内核·保护
YJlio5 小时前
Windows Sysinternals 文件工具学习笔记(12.11):综合实战——从磁盘告警到文件替换的一条龙排障
windows·笔记·学习
是一个Bug11 小时前
Java基础50道经典面试题(四)
java·windows·python
OliverH-yishuihan13 小时前
开发linux项目-在 Windows 上 基于“适用于 Linux 的 Windows 子系统(WSL)”
linux·c++·windows