【C语言航路】第十二站:自定义类型:结构体、枚举、联合体

news2024/7/4 6:28:49

目录

 一、结构体

1.结构体的基础知识

2.结构体的声明

3.特殊的声明(匿名结构体)

4.结构体的自引用

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

6.结构体的内存对齐

7.修改默认对齐数

8.结构体传参

二、位段

1.什么是位段

2.位段的内存分配

3.位段的跨平台问题

三、枚举

1.枚举类型的定义和使用

2.枚举的优点

四、联合(共用体)

1.联合类型的定义

2.联合类型的特点

3.联合体大小的计算

4.利用联合体判断大小端

总结


 一、结构体

1.结构体的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
和数组相比较,数组是一些值的集合,这些值的类型是相同的

2.结构体的声明

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

例如:

#include<stdio.h>
struct Stu
{
	char name[20];
	int age;
}s1, s2;//全局变量
int main()
{
	struct Stu s3, s4;//局部变量
	return 0;
}

当然也可以使用typedef来重命名结构体

#include<stdio.h>
typedef struct Stu
{
	char name[20];
	int age;
}Stu;
int main()
{
	Stu s3, s4;//局部变量
	return 0;
}

3.特殊的声明(匿名结构体)

有时候我们会遇见这样的声明结构体

#include<stdio.h>
struct
{
	char name[20];
	int age;
}s1;
int main()
{
	return 0;
}

像这种没有结构体标签的定义结构体,我们称之为匿名结构体,匿名结构体只能在声明的时候就定义好结构体变量。否则之后也没有办法再进行定义了

关于匿名结构体,我们还需要注意的一个点是这个

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

在这段代码中我们要注意的是p和&x的结构体看上去好像一样,但是实际上他们是不一样的,当我们进行编译的时候,就会报警告

因为编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。

4.结构体的自引用

我们先思考一个一个问题,在一个结构体里面是否可以包含一个该结构体的本身的成员呢?如下代码所示

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

其实我们不难推出,这样是不可以的,因为如果是这样的话,假如说我们要对其进行计算它的大小,那么我们会发现它的大小是无穷大。这是肯定不行的

如果想要正确的自引用的话,我们应该这样做,使用一个结构体指针

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

那么我们如果这样自引用是否合理呢?

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

其实这样做也是不合理的。这里有两处错误,首先是我们是使用了typedef,但是我们在结构体里面使用Node是在我们typedef之前进行的。我们编译器此时还不认识Node,所以这里错了,而且对于一个匿名结构体,他是无法自引用的。所以我们正确的代码应该是这样的

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

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

有了结构体变量,我们想要定义和初始化其实就很简单了,下面就是结构体变量的定义了

#include<stdio.h>
struct Point
{
	int x;
	int y;
}p1;

struct Point p2;
int main()
{
	struct Point p3;
	return 0;
}

定义好以后,我们使用{}进行初始化

#include<stdio.h>
struct Point
{
	int x;
	int y;
}p1 = { 10,20 };

struct Point p2 = { 0,0 };
int main()
{
	struct Point p3 = { 1,2 };
	return 0;
}

如果结构体里面嵌套一个结构进行初始化的话,我们也仍然按照相同的方法进行赋值

#include<stdio.h>
struct Point
{
	int x;
	int y;
}p1 = { 10,20 };
struct S
{
	int num;
	char c;
	struct Point p;
	float d;
};
struct Point p2 = { 0,0 };
int main()
{
	struct Point p3 = { 1,2 };
	struct S s = { 1,'w',{1,2},3.14f };
	return 0;
}

当然除此以外,我们也可以进行乱序初始化,并且打印出来,如下所示

我们想要乱序赋值,就得先使用   " ."这个操作符,然后输入我们要赋值的变量名即可,同样打印也是靠这个点操作符的

#include<stdio.h>
struct Point
{
	int x;
	int y;
}p1 = { 10,20 };
struct S
{
	int num;
	char c;
	struct Point p;
	float d;
};
struct Point p2 = { 0,0 };
int main()
{
	struct Point p3 = { 1,2 };
	struct S s = { 1,'w',{1,2},3.14f };
	struct S s2 = { .d = 3.14f,.p.x = 20,.c = 'a',.num = 100,.p.y = 55 };
	printf("%d %c %d %d %f\n", s.num, s.c, s.p.x, s.p.y, s.d);
	printf("%d %c %d %d %f\n", s2.num, s2.c, s2.p.x, s2.p.y, s2.d);

	return 0;
}

6.结构体的内存对齐

当我们了解了结构体以后,我们还有一个事情我们还需要了解的是如何计算结构体的大小。

我们先看这段代码,并猜测输出结果

#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;
}

我们看到后,肯定会想当然的认为是两个6,但是答案其实是12和8

要了解这个我们得先了解一下结构体对齐规则

1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8,Linux环境下无对齐数
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

所以我们便可以得知S1结构体对齐图如下

 我们也可以验证一下这些对齐数的偏移量

我们要使用offsetof这个宏来计算,这个宏是用来计算偏移量的,当然他需要一个头文件stddef.h

#include<stdio.h>
#include<stddef.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));
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	return 0;
}

可见我们的想法是正确的

我们也根据上面的规则来画一下S2结构体的内存图

 我们在来看一下这个结构体的大小

struct S3
{
	double d;
	char c;
	int i;
};

 我们再看一个,计算S4的大小

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

那么为什么要进行对齐呢?

1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说:

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起

//例如:
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};

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

7.修改默认对齐数

我们知道在vs下默认对齐数是8,但是8一定是最合适的吗,未必,有时候我们就需要自己去修改这个默认对齐数

我们修改默认对齐数是使用一条预处理指令来实现的

//将默认对齐数修改为1
#include<stdio.h>
#pragma pack(1)
struct S
{
	char c1;
	int i;
	char c2;
}s;
//恢复默认对齐数
#pragma pack()
int main()
{
	printf("%d\n", sizeof(s));
	return 0;
}

8.结构体传参

#include<stdio.h>
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函数

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

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

二、位段

1.什么是位段

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

我们先看这一个代码

#include<stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d\n", sizeof(struct A));
	return 0;
}

为什么是8呢?理论上来说,四个整型应该是16才对。

其实所谓的位段,这个位指的是二进制位。

也就是说,a占2个二进制位,b占五个二进制位。c占10个二进制位,d占30个二进制位

因为在我们实际应用结构体的时候,我们发现有些变量不需要那么多的二进制位,有点浪费空间,我们就对他做了更精细的划分,这个可以更加节省我们的空间

2.位段的内存分配

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

我们这里先说一下第二点,也就是位段的空间是按照四个字节或者一个字节一个字节的方式来开辟的

接下来我们来验证一下在vs2022上位段的内存开辟

#include<stdio.h>
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;
}

下图是我们的分析过程

 

最终的十六进制就是62 03 04

在内存中也确实这样分配的

 注意:位段是不需要对齐的,因为他本身就是为了节省空间的

3.位段的跨平台问题

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

三、枚举

枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举

1.枚举类型的定义和使用

枚举类型的定义与结构体是非常类似的,他的关键词是enum

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

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值

比如说我们可以打印出来这些值

#include<stdio.h>
//性别
enum Sex
{
	MALE,
	FEMALE,
	SECRET
};
int main()
{
	printf("%d\n", MALE);
	printf("%d\n", FEMALE);
	printf("%d\n", SECRET);
	return 0;
}

 当然这些都是默认值,我们可以修改这些默认值

这些枚举常量,我们也可以定义一个枚举类型的变量去接受这些值

如下图所示,在c语言中,我们可以直接对其直接赋值1,也是可以的,但是这是由于c语言检查不够严格导致的,这样写其实不好。在c++中就直接报错了

 

还有一点应该注意的是,一个枚举的大小应该是四个字节,也就是一个整下,下面是验证

2.枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量

四、联合(共用体)

1.联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

#include<stdio.h>
union UN
{
	char c;
	int i;
};
int main()
{
	union UN un;
	return 0;
}

2.联合类型的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

#include<stdio.h>
union UN
{
	char c;
	int i;
};
int main()
{
	union UN un;
	printf("%d\n", sizeof(un));
	printf("%p\n", &un);
	printf("%p\n", &(un.c));
	printf("%p\n", &(un.i));
	return 0;
}

也就是说当我们使用c和i共用这四个字节的空间,当然我们一般平时使用的时候只是单独使用其中一个变量。

我们再来分析一下这个代码,假设我们是小端机器

#include<stdio.h>
union UN
{
	char c;
	int i;
};
int main()
{
	union UN un;
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);
	return 0;
}

首先这个联合体的大小是4个字节,然后存放一个十六进制的数赋值给i

然后我们存放一个0x55 给c,内存就变为了

 所以最终打印结果为11 22 33 55

 

3.联合体大小的计算

联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

#include<stdio.h>
union Un1
{
	char c[5];
	int i;
};
union Un2
{
	short c[7];
	int i;
};
int main()
{
	//下面输出的结果是什么?
	printf("%d\n", sizeof(union Un1));
	printf("%d\n", sizeof(union Un2));
	return 0;
}

1.

Un1中,最大的大小是5个字节

但是c的大小是1,与默认对齐数8取得最小值还是1

i的大小是4,与默认对齐数8取得最小值是4

最大对齐数就是1和4的最大值,也就是4

5不是4的倍数,所以应该是八个字节的大小

2.

c数组的大小是14个字节,i的大小是4个字节,最大成员大小是14个字节

c一个元素的大小是2个字节,默认对齐数是8,取得2

i的大小是4个字节,默认对齐数是8,取得4

14不是4的倍数,所以最终大小是16个字节

4.利用联合体判断大小端

我们可以利用联合体来实现判断大小端,代码如下

#include<stdio.h>
union UN
{
	char c;
	int i;
};
int main()
{
	union UN un;
	un.i = 1;
	if (un.c == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}


总结

本小节讲解了结构体,位段,枚举,联合的详细知识点。

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

想获得更多优质内容, 一定要关注我哦!!!

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

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

相关文章

小程序目录结构和全局配置

小程序目录结构和全局配置小程序目录结构目录结构和web结构对比全局配置—pages & windows配置文件简介全局配置pages & window全局配置—tabBartabBar简介页面配置页面配置简介小程序目录结构 目录结构 和web结构对比 全局配置—pages & windows 配置文件简介 …

块级元素、行内元素、元素嵌套

HTML标签有两类&#xff1a;块级元素行内元素 &#xff08;1&#xff09;块级元素-默认总是在新行开始 div、h1~h6、blockquote、dl、dt、dd、form、hr、ol、p、pre、table、ul … 特点&#xff1a;总是在新行上开始&#xff0c;高度、行高以及顶和底边距都可控制&#xff0c;宽…

Kubernetes_HPA实践使用

文章目录一、前言二、配置APIServer和安装Metrics2.1 APIServer开启Aggregator2.2 安装Metrics Server (需要用到metris.yaml)安装metrics Server之前安装metrics Server之中全部命令实践演示安装metrics Server之后三、使用HPA测试 (需要使用到test.yaml&#xff0c;里面包括 …

明清专题数据库:企业匹配官办书局距离、科举考试、商帮文化变量等

一、企业到明清官办书局&#xff0c;印刷局的最小距离测算 以明清进士数量的地域分布测度儒家文化的历史积淀&#xff0c;使用企业到古代印刷局的距离作为工具变量解决内生性问题&#xff01; 数据来源&#xff1a;自主整理 时间跨度&#xff1a;-- 区域范围&#xff1a;全国…

第十四届蓝桥杯单片机组学习笔记(1):点亮板子第一个LED

点亮板子第一个LED前言单片机IO控制M74HC573M1R-数据锁存器74HC138-38译码器代码前言 使用CT107D实验板子的时候可以看到为了IO口对多个外设的复用&#xff0c;所以板子使用了几个锁存器来对LED、数码管、蜂鸣器等外设进行了一个选择&#xff0c;最后再使用38译码器来使用三个…

如何对时间序列进行小波分析,得出其周期?

从信号处理角度进行分析 简单的时间序列直接做各种谱分析&#xff08;频谱&#xff0c;包络谱&#xff0c;平方包络谱&#xff0c;功率谱&#xff0c;倒谱等等&#xff09; 比如一些简单的旋转机械振动时间序列信号 ​如果频谱不好分析&#xff0c;那可以分析如下图所示的时间序…

FL Studio21最新版数字音频工作站(DAW)

FL Studio21首先提供了音符编辑器&#xff0c;编辑器可以针对音乐创作人的要求编辑出不同音律的节奏&#xff0c;例如鼓&#xff0c;镲&#xff0c;锣&#xff0c;钢琴&#xff0c;笛&#xff0c;大提琴&#xff0c;筝&#xff0c;扬琴等等任何乐器在音乐中的配乐。 水果音乐制…

【Linux C编程-高级篇】换行回车探讨printf行缓冲write函数掉电保护线程安全相关

换行回车探讨 \r : 回车&#xff0c;定位到本行开头\n : 换行&#xff0c;光标移到下一行\r\n : 将光标移动到下一行开头windows 下&#xff0c;每行结尾 \r\n类unix&#xff0c;每行结尾 \nMac系统&#xff0c;每行结尾\r \r\n&#xff0c;windows下好像改善了&#xff0c;使…

全文最详细的Apache的管理及优化Web(图文详解)

目录 前言 一、Apache的安装及启用 二、Apache的基本信息 三、Apache的基本配置及修改 1、默认发布文件 2、Apache端口修改 3、默认发布目录 三、Apache的访问控制 1、基于客户端ip的访问控制 2、基于用户认证 四、Apache的虚拟主机 五、Apache的语言支持 六…

React--》超详细教程——React脚手架的搭建与使用

目录 React脚手架的创建 全局安装创建 npx安装创建(官方推荐) 指定React版本安装 脚手架文件介绍 React脚手架是开发现代Web应用的必备&#xff0c;其充分利用Webpack、Babel、ESlint等工具辅助项目的开发&#xff0c;当然这些工具也无需手动配置即可使用&#xff0c;脚手…

Java 在云原生中的内存问题

Java 凭借着自身活跃的开源社区和完善的生态优势&#xff0c;在过去的二十几年一直是最受欢迎的编程语言之一。步入云原生时代&#xff0c;蓬勃发展的云原生技术释放云计算红利&#xff0c;推动业务进行云原生化改造&#xff0c;加速企业数字化转型。 然而 Java 的云原生转型之…

Word目录自动生成,不使用word默认样式的,且指定从某页开始为第一页

文章目录一&#xff0c; 设置正文页为第1页&#xff1a;二&#xff0c;自动生成目录。拓展&#xff1a;需求&#xff1a;文章或者论文往往会先写好标题&#xff0c;摘要&#xff0c;写好内容。最后需要生成目录。但是这样布局后&#xff0c;生成的目录的起始页码不是从第1 页开…

ChatGPT通俗笔记:从GPT-N、RL之PPO算法到instructGPT、ChatGPT

前言 自从我那篇BERT通俗笔记一经发布&#xff0c;然后就不断改、不断找人寻求反馈、不断改&#xff0c;其中一位朋友倪老师(之前我司NLP高级班学员现课程助教老师之一)在谬赞BERT笔记无懈可击的同时&#xff0c;给我建议到&#xff0c;“后面估计可以尝试尝试在BERT的基础上&…

搭建OpenCV环境和Jupyter Notebook

使用Anaconda搭建python和OpenCV环境安装Anaconda&#xff0c;全程下一步&#xff0c;修改了一下默认安装路径&#xff0c;修改为D:\Program Files\Anaconda3同时设置了环境变量&#xff0c;将三个文件夹路径都加到了系统环境变量&#xff0c;path中。打开【开始】菜单&#xf…

MatrixVT:Efficient Multi-Camera to BEV Transformation for 3D Perception——论文笔记

参考代码&#xff1a;BEVDepth 1. 概述 介绍&#xff1a;这篇文章对LSS方法中的瓶颈项进行分析&#xff0c;分别指出其中显存占用问题源自于“lift”操作生成的高维度特征&#xff0c;运行耗时是由于“splat”操作的求和操作&#xff0c;对此文章从矩阵变换的角度对原版的LSS方…

Java面向对象复习

文章目录一、类和对象1. 面向对象2. 类的定义3. 对象的创建4. 类在内存中的存储5.类的成员使用6. toString()方法7. static 关键字静态成员变量静态成员方法二、封装1. 构造方法概念基本使用构造方法的重载this关键字2. private3. 封装的好处三、继承1. 继承的概念2. extends3.…

二维矩阵的元素和

二维矩阵的元素和1.背景2.原理3.实现1.背景 对矩阵元素进行求和&#xff0c;或者求子矩阵的元素和&#xff1b;给定矩阵左上角坐标&#xff08;x1,y1&#xff09;和右下角坐标&#xff08;x2,y2&#xff09;; 如何快速求出 以&#xff08;x1,y1&#xff09;&#xff0c;&#…

SAP入门技术分享五:内表

内表1. 概要2. 内表与表头3.内表的类型&#xff08;1&#xff09;类型&#xff08;2&#xff09;标准表&#xff08;3&#xff09;排序表&#xff08;4&#xff09;哈希表4.比较内表速度&#xff08;1&#xff09;标准表与排序表&#xff08;2&#xff09;二分法查找&#xff0…

Kafka生产者分区

生产者分区 分区的原因 (1&#xff09;便于合理使用存储资源&#xff0c;每个Patition在一个Broker上存储&#xff0c;可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务&#xff0c;可以实现负载均衡的效果。 (2&#xff09;提高并行度&am…

如何设计一个消息队列?

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…