C语言的结构体

前言

在C语言里我们常常会用到很多种数据类型,如int,char,double…你想表示你的年龄你可以用int来定义,表示年龄可以用float类型定义,表示名字可以用一个字符数组来表示。但有没有考虑过,让你定义你这个人,你该用什么样的数据类型来表示?你会发现在之前学过的知识里,你找不到一种数据类型来表示你这个人,所以C语言暖心的给你提供了一种自定义的数据类型–结构体。

一.结构的声明与定义

1.1结构体声明

我们仔细回忆一下之前是如何定义整型,浮点数类型的变量

int a=100;
float b=3.14f;

这一句代码包含了,数据类型+数据名称+数据的值。因为结构体本身也是一种数据类型,所以创建结构体的模板也大致刻画出来了。

struct student//struct是结构体的关键字,说明这是一个结构体。
//既然这是一个自定义的数据类型,这个数据类型叫什么也是由你来自己取
//所以student就是我们创建结构体的标签。
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
    //上面这三个是结构体的成员变量,说明你定义的结构体变量,包含了这三个变量
};//<--注意这里还有一个分号,一定要注意不要忘记写了。但是我用的编译器VS2022,
//他会自动加上

有一点要注意,我们现在只是定义好了一个数据类型,还没有创建实际的对象。

1.2结构体定义

结构的成员可以是标量、数组、指针,甚至是其他结构体

好了,现在我们已经声明好结构体了,但我们接下来要怎么用呢?刚才我也讲过定义一个int类型的变量是类型+名称+值。现在我们需要用刚才自定义好的结构去定义一个变量:

struct student
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
};
	struct student st1;//这样我们就定义结构体变量
	//我们不要把前面的student和st1搞混了。你就把struct student,int,float,double
	//归为一类就好了,student就相当于结构体变量。而st1就相当于结构体的变量名称。

但是定义变量名可不止一种方法。

struct student
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
}st2,st3;
//  ^
//  |
//看!还可以在这里定义

但是这两种有什么区别呢?如果在main函数里面定义一个结构体,可能会使代码变得不是那么清楚,所以我经常把结构体放在外面定义。这样的话像st2,st3这种就相当于全局变量了,而st1就相当于局部变量。
但是声明的同时在定义,可以是代码变得更简洁。

struct student
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
}s1,s2;
struct student s3;
int main()
{
	struct student s4;
	return 0;
}
//在这里s1,s2,s3都是全局变量
//s4是局部变量

换句话来说,这两种定义没什么太大的区别。

1.3匿名结构体类型

现在我们清楚了怎么声明结构体类型,也清楚了怎么定义一个结构体变量
但现在我们还有一个特殊的结构体声明没有学。

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

通过上面的代码可以发现我们图省事把结构体的标签给拿掉了,这样定义可以吗?当然可以,但是有一定的局限性。首先你只能在}的右边定义这个结构体变量了。如果你想这样定义struct stc={…};这样肯定是不行的。
我们在对比上下两个结构体,我们有没有思考一个问题?
p = &x;//这样写合理吗?
答案当然是不合理的,因为编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。

1.4typedef重定义

最后的最后,我还要讲一个小知识点:
可能我们会发现每次定义一个结构体变量前面都要写上struct+结构体标签。有没有感觉这样写很麻烦。所以我们来稍微改进一下:

typedef struct Node
{
	int data;
	struct Node* next;
}No;
//这样我们就可以把struct Node替换为No了!
//我们以后定义就可以直接这样写
No s;

typedef:类型的重定义。
使用关键字 typedef 可以为类型起一个新的别名

我在一个结构体typedef的同时,顺便定义一下这个结构体可以吗?

typedef struct Node
{
	int data;
	struct Node* next;
}No;//我想在No后面在定义一下

当然不行,No后面不管怎么写都不行,没必要这么节省空间。

二.结构体的初始化

接下来我们就要给结构体赋初始值了。如果我们之前接触过数组会发现数组赋初始值是放在{ }内的,既然我们结构体也有很多种类型,所以我们也可以放在大括号中。

struct student
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
};
struct student st1 = {"zhangsan" , 20 , 5999};
//在这里你们可以看到我是跟着结构体内自上而下来赋值的,像名字这类的变量用“”来表示
//其他的就普通的去写,每个元素用,来隔开。

当然我们也可以这样做

struct student
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
}st1 = {"zhangsan" , 20 , 5999};

现在又有一个问题出现了,我们能不能不按着顺序来赋初始值?

struct student
{
	char name[20];      //姓名
    int age;            //年龄
    float score;        //分数
};
struct student st1={.age = 18, .name = "zhangsan", .score = 59.9};//在希望赋值的变量前面加上.就可以了
//还可以这样
st1.name = "C 语言";
st1.age = 19;
st1.score = 60;

当然还可以完全初始化,就是你虽然定义了三个变量name,age,score,但是你可以只对你希望的变量赋值,在这里我自己研究了一下,发现姓名如果没初始化的话就会打印一个空,就是什么都不打印,其他的就默认为0;

像st1.name = "zhangsan"这样赋值是行不通的。因为st1.name是数组名,数组名的意义是首元素的地址,一个地址就相当于你家的门牌号,你说把你家门牌号直接改了你能乐意不?我们需要把“zhangsan”放到这个门牌号指向的空间里面。

strcpy(st1.name,"zhangsan");//我们可以利用函数将zhangsan拷贝到数组里面。

或者可以这样:

struct student
{
	char name[20];      //姓名
	int age;            //年龄
	float score;        //分数
	char* address;      //地址
};

int main()
{
	struct student st1 = { .age = 18, .score = 59.9f };
	st1.address = "zhongguo";
	return 0;
}

但是address是char类型的,name是首元素地址也是char类型的,凭啥一个可以赋值,一个不可以赋值?
首先"zhongguo"这个字符串它本质上是一个常量的地址。""的作用有三个
1.在常量区申请一块空间,用来存放双引号里面的字符串
2.在字符串末尾加上’\0’
3.返回这块空间的地址
char name[20];里面的数组名name可以理解为一个已经改好了的一个房子,数组名是这个房子的门牌号,门牌号你不可能变,也就是说name相当于一个常量,改变一个常量肯定会报错。
char* address;这里的address就有区别了,虽然它的类型也是char*类型的,但是它是一个变量,因为它单纯的就是一个指针,一个地址,地址里面给什么它就指向什么,不会像数组名name那样已经固定死了。所以st1.address = “zhongguo”;这样是可行的。
刚才讲过数组名是一个已经盖好的房子的地址。那我们在声明一个字符数组的同时把它定义好了,这样可以吗?
char name[20] = “zhangsan”.
这样又是可以的,这就说明你在建房子的时候自己规定你的门牌号是多少,同样也能达到赋值的效果。

三.结构体成员的访问

3.1结构体成员访问的基本用法

接下来我们要了解的是结构体成员是如何访问的。

访问结构体成员一般需要两种操作符.和->

struct student
{
	char name[20];      //姓名
	int age;            //年龄
	float score;        //分数
};

int main()
{
	struct student st1 = {"zhangsan",18,59.9};
	printf("%s\n", st1.name);
	printf("%d\n", st1.age);
	printf("%f\n", st1.score);
	return 0;
	//在这里可以通过结构体变量 + (.) + 成员名就可以访问到这个成员。我们就可以
	//操作这个变量了
}

但有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针像这样:

struct student
{
	char name[20];      //姓名
	int age;            //年龄
	float score;        //分数
};

int main()
{
	struct student s = { "zhangsan",18,60 };
	struct student* ps = &s;//把s的地址取出来赋给ps
	printf("%s %d %f\n", s.name, s.age, s.score);
	printf("%s %d %f\n", (*ps).name, (*ps).age, (*ps).score);
	//解引用ps就相当于s
	printf("%s %d %f", ps->name, ps->age, ps->score);
	//如果用地址的话就不能用.操作符了,而是用->操作符
	//所以上面三行代码打印出来的应该是一样的
	return 0;
}

来给你们看一下打印结果:
在这里插入图片描述

3.2结构体成员是结构体

有时候结构体里面的成员的类型会是另一个结构体类型,但是这不影响我们使用。

struct A
{
	int a;
	int b;
};

struct student
{
	char name[20];      //姓名
	int age;            //年龄
	struct A a;         //随便放的
};
int main()
{
	struct student st1;
	st1.a.a = 10;
	st1.a.b = 20;
	return 0;
}

这里不用担心它嵌套多少,照样用,上面这里为了找到结构体struct A里面的值,先st1.a找到这个结构体在.a找到里面的成员a.

3.3结构体成员访问中犯到的一些错误

下面这些代码是因为我学艺不精,犯到的一些错误,我在这里提一嘴。

typedef struct English_Prefix//前缀
{
	int size;      //存放数据的个数
	int capacity;  //最多能存数据的个数
	char* prefix;  //指向动态内存的地址
}Prefix;

typedef struct English_Suffix//后缀
{
	int size;      //存放数据的个数
	int capacity;  //最多能存数据的个数
	char* suffix;  //指向动态内存的地址
}Suffix;

typedef struct English_Root//词根
{
	int size;      //存放数据的个数
	int capacity;  //最多能存数据的个数
	char* root;    //指向动态内存的地址
}Root;

typedef struct english
{
	Prefix* pre;
	Suffix* suf;
	Root* ro;
}English;

这里我是想定义三个结构体,里面可以放英语单词的前缀,后缀,词根。然后我在初始化的时候,发现一下传三个结构体过去会很麻烦,所以我又定义了一个结构体,里面放三个指针,指向指向上面那三个结构体

en->pre->capacity = 0;

我在初始化函数里这样写,想把结构体里的变量初始化成0,这里我乍一看感觉没啥问题en->pre可以找到前缀的那个结构体,然后->capacity不就可以找到那个结构体里面的内容了吗?然后运行的时候就直接崩溃了。
在这里插入图片描述

这是因为我在最后一个结构体里虽然定义了三个指针,但也只是指针,我根本没有说这个指针指向谁,强行对它进行访问换来的只是错误。
但是我这样写就成功了:

typedef struct english
{
	Prefix pre;
	Suffix suf;
	Root ro;
}English;

void Init_English(English* en)
{
	en->pre.capacity = 0;
}

因为这里就相当于直接定义了一个Prefix类型的变量,对这个变量进行访问就可以找到对应结构体的成员。为了理解,我还特意画了张图:
在这里插入图片描述
可能这种问题比较简单,一般人遇不到,我写来就是给你们提个醒。

在这里插入图片描述
这里确实被我初始化成0了。

四.结构体传参

结构体传参就比较简单了,先看代码。

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个字节(在32位系统内是4字节,64位系统是8字节

五.结构的自引用

结构体成员的类型可以是标量、数组、指针,甚至是其他结构体,这我在前面就讲过,但是如果包含的这个结构体是自己会怎么样呢?
我们可不可以这样写?

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

首先我先说答案:不可以。但是为什么呢?我们可以这样想想如果是这样写,那这种结构体的大小是多少呢?这样会说不清的,我们假设这个结构体大小是x字节,那里面包含的那个结构体是多少字节呢?x-4吗?当然不是这样就产生悖论了。
但既然这样不行,我们怎么包含自己呢?
答案是包含一个地址

struct Node
{
	int data;
	struct Node* next;
};
//这样就是自引用的合理办法

这样写的话,一个结构体里面的成员是一个Int型数据,一个是下一个形同类型成员的地址。这样就连起来了。一般我们会在数据结构中的链表里会用到,所以在这里我就不细说了。

六.结构体内存对齐

6.1结构体内存分析

我们先看一段代码,并回答一个问题:这两个结构体的大小分别是多少?

struct S1
{
	char c1;
	int i;
	char c2;
};
printf("%zd\n", sizeof(struct S1));
struct S2
{
	char c1;
	char c2;
	int i;
};
printf("%zd\n", sizeof(struct S2));

我们首先排除6,6.因为这么简单我就不会问了。好了我们来看执行结果。
在这里插入图片描述
哦?这样一看,和6,6差了十万八千里了。但是为什么呢?我接下来带你们好好讲讲。
我们先看结构体内存对齐的规律:

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

可能看着感觉好麻烦,但是别急,我一个一个讲。
我们现在在内存中为结构体S1开辟一段空间:
假设一个空格为1个字节。
在这里插入图片描述
注意这个数字代表地址的偏移量。0代表这块空间的地址偏移0,1就是偏移1…
第一个规则:第一个成员在与结构体变量偏移量为0的地址处。
所以S1的c1就占据了数字0对应的那个地址。并且占了一个字节。
第二个规则: 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
VS2022中默认的值为8.
也就是说从第二个变量开始,就要运用到第二个规则了。第二个变量是int类型的,而int类型的对齐数是多少呢?在我用的VS2022中默认的值是8字节,而int类型是4字节,他们的最小值是4所以int要存到的地址的偏移量应该是4的倍数,所以int存放的位置应该是4,8,12…而最前面的是4,所以Int是从偏移量为4的地方开始。又因为int为4个字节。所以int占据的空间是4-7
同理因为第三个类型是char为1个字节,所以最小值应该是1的倍数,任何正整数都是1的倍数,所以这个char类型直接放在偏移量为8的位置上就可以了。
我们在看个图来加深理解:
在这里插入图片描述
我们再看第三个规则:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
我们经过上面的分析,int的对齐数是4,char类型的对齐数是1,其中最大的对齐数就是4了,所以总大小应该是4的倍数。我们通过上图计算可以发现上面占用内存从0-8是9个字节,不是4的倍数,所以4的倍数最小也要上12,所以偏移量9,10,11的地址也被浪费了。
在这里插入图片描述
经过这样分析,我想大家知道前三个规则是怎么运用的了。
但是还没有结束,我们再来分析一下这个代码:

struct S3
{
	double d1;
	char c2;
	int i;
};
struct S4
{
	char c3;
	struct S3 s3;
	double d1;
};

这里我们判断S4的大小
现在我们可以运用第四个规则了:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
我们发现,S3中的三个数据的对齐数分别为8,4,1,最大的是8,所以这个结构体应该放在内存偏移量为8的倍数里,所以struct S3 s3是从8开始。但是struct S3 s3的大小是多少呢?你们可以通过上面三个规则自行判断,我直接说结果是16。所以struct S3 s3占的内存是从8-23。double因为对齐数是8应该从8的倍数开始,所以double占用的内存是从24-31。
最后我们来计算S4整体的大小,既然是所有对齐数的最大值,S3中分别是8,1,4,S4中char为1,double为8(不包括struct S3 s3)。所以最大值应该是8,所以内存的大小应该是8的倍数。而0-31总共32字节正好是8的倍数。所以S4大小为32个字节。
最后根据图来好好理解:
在这里插入图片描述

6.2为什么存在内存对齐?

我们通过上面的分析,可能会想,一个普通的结构体,直接按顺序放不好吗?为啥整那么麻烦?

  1. 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    为什么不对齐需要访问两次呢?我们通过图来了解。
    在这里插入图片描述
    假如这两次都是从c前面那里开始读(这里为结构体的起始位置),因为32位计算机一次读4个字节。
    不对齐的情况下,想要读c读一次就行,但是读i的话,你从c那里开始读你一次读不完i,要读两次(32位计算机走一步只能固定的四个字节,所以你不能直接从i前面那里开始读i,还是要从头开始走)。
    但是对齐的情况下,你读c也是读一次就读到了,但是读i你就可以直接从i前面那个地方开始读,这样你只用一次就能读到i。

我们可以认为,虽然我们浪费了一些空间,但是我们因此节省了时间。

但是怎么才能既节省时间又节省空间呢?我们可以回头看最开始S1,S2的大小。

struct S1
{
	char c1;
	int i;
	char c2;
};
printf("%zd\n", sizeof(struct S1));
struct S2
{
	char c1;
	char c2;
	int i;
};
printf("%zd\n", sizeof(struct S2));

在这里插入图片描述
我们会发现虽然元素相同,知识排序不同,第二种大小却小一些。对于第二个结构体大小的分析,你们可以自行分析。

结论:让占用空间小的成员尽量集中在一起。

七.修改默认对齐数

刚才我也说过了VS2022的默认对齐数是8,但是我们可以更改吗?答案是当然的。
这样我们就要用到一个预处理指令:

#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1;
	int i;
	char c2;
};
//当我们用完的的时候也可以恢复
#pragma pack()

结束

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值