C语言自定义类型:结构体

news/2024/9/28 10:08:25 标签: c语言, 开发语言

目录

  • 前言
  • 一、 结构体类型的声明
    • 1.1结构体的回顾
      • 1.1.1 结构的声明
      • 1.1.2 结构体变量的创建和初始化
    • 1.2 结构的特殊声明
    • 1.3 结构的⾃引⽤
  • 二、 结构体内存对齐
    • 2.1 对齐规则
    • 2.2 为什么存在内存对⻬?
    • 2.3 修改默认对齐数
  • 三、结构体传参
  • 四、结构体实现位段
    • 4.1 什么是位段
    • 4.2 位段的内存分配
    • 4.3 位段的跨平台问题
    • 4.4 位段的应用
    • 4.5 位段使用的注意事项
  • 总结


前言

我们知道C语言有内置的类型,char,int,float等,但是在日常生活中会存在很多复杂的类型无法用内置类型来表示,比如一个人,人有身高,体重,年龄等,这个时候我们就要人为的创造类型;—结构体。


一、 结构体类型的声明

前⾯我们在学习操作符的时候,已经学习了结构体的知识,这⾥稍微复习⼀下。<点击跳转前面链接>

1.1结构体的回顾

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

1.1.1 结构的声明

struct tag
{
 member-list;
}variable-list;

例如描述⼀个学⽣:

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

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

#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 = "lisi", .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;
}

1.2 结构的特殊声明

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

比如

struct 
{
	int a;
	char b;
	float c;
}x = { .a = 1, .b = 'a', .c = 3.4 } ;


不完全声明只能在定义结构体的时候使用一次;
在这里插入图片描述
我们来看看下面的代码:

//匿名结构体类型
struct
{
 int a;
 char b;
 float c;
}x;
struct
{
 int a;
 char b;
 float c;
}a[20], *p;

上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?

//在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;

警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次

1.3 结构的⾃引⽤

在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?
⽐如,定义⼀个链表的节点:

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

上述代码正确吗?如果正确,那 sizeof(struct Node) 是多少?

仔细分析,其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤⼩就会⽆穷的⼤,是不合理的。

正确的⾃引⽤⽅式:

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

我们在定义一个结构体变量的时候,我们会写struct Node S,这样写比较麻烦,所以我们在编写代码是通常将其与typedef相结合;

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

在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引⼊问题,看看
下⾯的代码,可⾏吗?

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

答案是不⾏的,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使
⽤Node类型来创建成员变量,这是不⾏的!

解决⽅案如下:定义结构体不要使⽤匿名结构体

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

二、 结构体内存对齐

我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。

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


int main()
{
	struct S s = { 0 };
	printf("%zd\n", sizeof(s));
	return 0;
}

大家可以猜猜这个结构体的长度是多少?
相信很多人猜的是6,是这样吗?
在这里插入图片描述
12?why,我们继续来学习以下内容

这也是⼀个特别热⻔的考点: 结构体内存对⻬

2.1 对齐规则

⾸先得掌握结构体的对⻬规则:

  1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
    对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值
  • VS 中默认的值为 8
  • Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩;
  1. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

我们先来看看不嵌套结构体的情况,即前三个规则:

利用这个规则来过一遍这个过程,首先存储第一个成员,char c1,存到偏移量为0的地址处,也就是0处;
再来存储int i,int i类型的大小是4,VS默认值是8,所以对齐数取4,向下找4的倍数,即4,往下存储,到7;
同理存储char c2,类型大小是1,VS默认是8,对齐数取1,向下找1的倍数,即8,char类型只要一个字节;
但是,结构体的总大小为最大对齐数的整数倍,即4的整数倍,从9开始找,即到11;
所以S的大小是12(0-11);

在这里插入图片描述

我们来点练习:

//练习1
struct S2
{
	char c1;
	char c2;
	int i;
};
printf("%zd\n", sizeof(struct S2));  //8
//练习2
struct S3
{
	double d;
	char c;
	int i;
};
printf("%zd\n", sizeof(struct S3));   // 16

我们再来看看嵌套结构体的情况,即前四个规则:

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};


int main()
{
	printf("%zd\n", sizeof(struct S4));
	return 0;
}

我们继续来一步一步的分析:
第一个成员char c1,存到偏移量为0的地方;
第二个成员即struct S3,是嵌套结构体,对齐到自己的成员的最大对齐数的整数倍,即8的整数倍,对齐到偏移量为8的位置;(8-23)
第三个成员即double d,对齐数为8,即从偏移量为24的地方开始存(24-31)

所以struct S4的大小为32
在这里插入图片描述
运行结果:在这里插入图片描述

2.2 为什么存在内存对⻬?

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

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

  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取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 所占空间的⼤⼩有了⼀些区别。

2.3 修改默认对齐数

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

#pragma pack(1)//设置默认对⻬数为1

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

//#pragma pack()//取消设置的对⻬数,还原为默认

int main()
{
	//输出的结果是什么?
	printf("%zd\n", sizeof(struct S));
	return 0;
}

我们一般设置是2的n次方;

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对齐数。

三、结构体传参

我们如果将结构体当参数传进函数,很多人可能会这样写:

struct S
{
	int arr[20];
	int n;
	float 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("%lf ", tmp.d);
}



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

这种方法无疑是可以的,但,我们如果直接将结构体传进函数,也就是将结构体整个复制一份给函数栈帧空间,但我们学习了结构体的内存结构,为了访问方便,我们会“浪费一些”空间,那如果我们这样传参,也会浪费函数的栈帧空间;

所以我们一般传给函数一个指针变量:

struct S
{
	int arr[20];
	int n;
	float d;
};

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


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

这样传参的时候只会传进一个地址,也可以达到效果,但内存的空间大大减少;

上⾯的 print1 和 print2 函数哪个好些?
答案是:⾸选print2函数。

原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

大家如果不知道什么是压栈,可以看看小编的这篇文章:函数栈帧

四、结构体实现位段

结构体讲完就得讲讲结构体实现 位段 的能⼒

4.1 什么是位段

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

  1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。
  2. 位段的成员名后边有⼀个冒号和⼀个数字。

注:有的课本会说一般结构体成员的名字都是‘_’开头;

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

A就是⼀个位段类型。
那位段A所占内存的⼤⼩是多少?

printf("%zd\n", sizeof(struct A));

在这里插入图片描述

4.2 位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
//⼀个例⼦
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是从右到左的使用)
  • 剩余的空间,不足下一个成员使用,是浪费,还是继续使用(VS是将其浪费)

在这里插入图片描述

4.3 位段的跨平台问题

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

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

那如何解决问题?
我们可以针对不同的平台写不同的代码,来解决这个问题;

4.4 位段的应用

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥
使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络
的畅通是有帮助的。

在这里插入图片描述

4.5 位段使用的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的

所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

struct A
{

 int _a : 2;
 int _b : 5;
 int _c : 10;
 int _d : 30;
};
int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的
 
 //正确的⽰范
 int b = 0;
 scanf("%d", &b);
 sa._b = b;  //一般先给一个变量赋值,再将这个变量赋给位段成员
 return 0;
}

在这里插入图片描述
注:使用位段的时候,创建的类型要尽量保持一致;


总结

这期我们学到了一种自定义类型—结构体,并讨论了一下结构体的大小,和用结构体实现位段,下期见!



http://www.niftyadmin.cn/n/5680870.html

相关文章

【frp】frp重启、frp启动、frp后台启动、frps dashboard等等

我写的关于frp配置的文章&#xff1a;frp配置 服务端frps 1. 创建服务文件 sudo nano /etc/systemd/system/frps.service2. 添加服务配置 在打开的文件中添加以下内容&#xff1a; [Unit] DescriptionFRPS Server Afternetwork.target[Service] Typesimple ExecStart/root…

开卷可扩展自动驾驶(OpenDriveLab)

一种通用的视觉点云预测预训练方法 开卷可扩展自动驾驶&#xff08;OpenDriveLab&#xff09; 自动驾驶新方向&#xff1f;ViDAR&#xff1a;开卷可扩展自动驾驶&#xff08;OpenDriveLab&#xff09;-CSDN博客 创新点 在这项工作中&#xff0c;本文探索了专为端到端视觉自动…

卷轴模式商城APP开发指南

卷轴模式商城APP的开发是一项融合了技术创新、用户体验优化与商业策略实施的综合性工程。本文将从程序员的角度出发&#xff0c;详细介绍该类型应用的开发流程&#xff0c;涵盖从需求分析到后期维护的各个环节。 一、需求分析 首先&#xff0c;明确APP的核心功能需求&#xff…

深圳·2025胶粘剂展会 BOND第六届胶展

BOND第六届胶展、2025大湾区国际胶粘剂及密封剂展览会 时间&#xff1a;2025年6月25-27日 地址&#xff1a;深圳国际会展中心&#xff08;新馆&#xff09; UV胶、快干胶、结构粘结胶、导热胶、低温黑胶、硅胶、SMT贴片红胶、底部填充胶、低温热固胶、COB黑胶、围堰填充胶、U…

认知杂谈91《菜鸟的自我修炼:减少过度干预》

内容摘要&#xff1a;          在投资和生活中&#xff0c;动作过多往往因情绪波动和缺乏计划而引发亏损。历史上的安史之乱和现代投资中的频繁交易都是例证。要管理情绪&#xff0c;首先要认识自己的情绪模式&#xff0c;然后改变消极的思考方式&#xff0c;并通过合…

golang web笔记-1.创建Web Server和Handler请求

1. 创建http web server的两个方法 1.1. 方式一&#xff1a;http.ListenAndServe(addr string, handler Handler) addr string&#xff1a;监听地址&#xff0c;如果为"" ,那么就是所有网络接口的80接口handler Handler&#xff1a;如果为nil&#xff0c;那么就是D…

idea.vmoptions 最佳配置

1. 推荐的 idea64.exe.vmoptions 配置&#xff1a; -Xms1024m -Xmx4096m -XX:ReservedCodeCacheSize512m -XX:UseG1GC -XX:SoftRefLRUPolicyMSPerMB50 -XX:CICompilerCount4 -XX:HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Dsun.io.useCanonCachesfalse -Dj…

文心一言 VS 讯飞星火 VS chatgpt (356)-- 算法导论24.2 2题

二、假定将 DAG-SHORTEST-PATHS 的第3行改为&#xff1a;3 for the first |V|-1 vertices, taken in topologically sorted order。证明&#xff1a;该算法的正确性保持不变。如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 首先&#xff0c;我们来证明将 DAG-S…