详解操作符:结构体操作符
目录
前言(doge)
我重生了,重生回到了刚学结构体的那一天,上一世我不可一世,以为学完C语言已天下无敌,直到遇到了
typedef int ElemType;
typedef struct Node {
ElemType data;
struct Node* next;
}Node;
typedef struct Node *linklist;
数据结构(链表), 我的天哪,我不敢了,结构体大人,我看不懂,我太不自量力了,这一次我下定决心一定要好好学习结构体。因此,我决定写下这篇博客来警醒后面的人(doge)。
总之,结构体是后续C语言实现各种数据结构的基础,加油吧!
一、结构体是什么?
我们初学C语言的时候,了解了很多数据类型,如int(整型),double(浮点类型,描述小数),char(字符类型描述字符),布尔类型等。这是C语言的内置类型。
在这之后我们学习了数组,我们知道数组也是有类型的,比如int [5],就是一个数组类型,它表示有5个int类型元素的数组。我们可以自己创建一个数组,比如char arr[8],它就表示能装8个字符的数组。可以发现数组类型是由无数个,并且数组类型是由我们创建数组的时候自己决定的。C语言中这种叫做自定义类型。
结构体就是自定义类型。它是C语言提供的一种自定义数据类型。
每项事物存在必有其意义,结构体是用来描述复杂事物的。生活中几乎所有事物都很复杂,以人为例,人有姓名,年龄,出生年月日,身高,体重,身份证号码,人的信息很多。如果单独创建变量,这个储存年龄,那个存储身高,每个属性数据类型也不同,还不方便管理。那么能不能创建一个房间专门放人的信息,结构体就是人为用来统一存储复杂信息的房间。
二、简单创建一个结构体和结构体的使用
我们试着创建一个结构体:
struct tag
{
member_list;
}variable_list;
以上为结构体声明,只是个模板,描述结构体如何存储数据的。
struct 是结构体的关键字
tag 是标签,可以任取名。最好取一个与内容相关的名字。
member_list 成员变量,可以创建一系列变量,指针,数组,其它结构体,内置数据类型都可以(先了解,后面会涉及)具体看例子。
variable_list 这里可以创建结构体变量。也可以不在这里创建,具体看例子。
struct person {
char name[20];//姓名
int age;//年龄
char data[20];//出生年月日
double height;//身高
double weight;//体重
} s1;//结构体声明后面创建结构体变量,为全局变量
struct person s2;//全局变量
int main()
{
struct person s3;//主函数内部,为局部变量
return 0;
}
struct person就是一个结构体类型了,作用同int double一样,结构体类型后面跟变量名,就是创建了一个简单的结构体变量了。
请注意上图,结构体变量在创建位置,也分为全局变量和局部变量。上面的结构体是描述一个人的。
结构体初始化如下:
struct person {
char name[20];//姓名
int age;//年龄
char data[20];//出生年月日
double height;//身高
double weight;//体重
};
struct person s1 = { "zhangsan",18,"19830421",187.5,168.0 };//结构体的完全初始化一一对应。
struct person s2 = { 0 };
这种初始化对于字符数组是空字符,int 是 0 ,字符是'\0',double 是小数0.000000的形式。
下面介绍一种运算符,可以进行指定初始化。
结构体的访问运算符其一:.运算符
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct person {
char name[20];//姓名
int age;//年龄
char data[20];//出生年月日
double height;//身高
double weight;//体重
};
int main()
{
struct person s1 = { "zhangsan",18,"19830421",187.5,168.0 };
printf("张三的体重是:%lf", s1.weight);
return 0;
}
变量名.成员变量名,就可以访问结构体成员变量的数据了。如图就是s1.weight访问了对应的数据,并用printf指定格式化打印。
结构体变量也是一种变量,通过.运算符可以访问结构体成员,可以像我们先前处理内置类型的数据一样进行运算操作。
.运算符是结构体的直接访问操作符。
指定初始化
#include<stdio.h>
struct person {
char name[20];//姓名
int age;//年龄
char data[20];//出生年月日
double height;//身高
double weight;//体重
};
struct person s2 = { .name = {"zhangsan"} };//指定初始化变量。
int main()
{
printf("这个人的名字是:%s\n",s2.name);
return 0;
}
结构体成员可以是结构体(自身或者其它结构体)所以称结构体可以嵌套。结构体的嵌套初始化和嵌套访问将在结构体指针处详细呈现。
三、结构体数组
前面提过结构体类型和内置数据类型相同,所以创建结构体数组也可以仿照数组类型。
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
struct book arr[5];//结构体类型 变量名[元素个数] 是一般格式
结构体类型 数组名[元素个数];
我们学过二维数组的初始化,类比下面结构体数组的初始化就好理解了。
花括号里面各套一层花括号分别表示arr[0],arr[1],arr[2],在同初始化结构体变量一样,就很好理解了。
数组通过下标(下标引用操作符【】)访问元素,结构体通过.运算符直接访问结构体内部的成员。那么结构体数组就是综合运用这两个操作符。
结构体数组的每个元素都是一个结构体变量。下列代码中arr[i]访问了一个结构体变量,再在后面加上用.成员变量,就访问这个元素的变量。具体例子看下。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 100
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
struct book arr[3] = { {"One Hundred years of Solitrude","Marquez",50.00},
{"Red Star Over China","Edgar Snow",11.8},
{"Three Days To See","Helen Keller",23.98} };
int main()
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s %s %.2f\n", arr[i].title, arr[i].author, arr[i].value);
}
return 0;
}
格式:
结构体数组名[下标].成员变量。
对上面代码进行总结:
1.arr是book结构的数组。
2.arr[1]是数组的第二个元素,也是book结构。
3.arr[1].title是一个字符数组,在上面代码为字符串。是arr[1]的成员。
4. 3是一个数组,它也可以访问元素,比如arr[1].title[3]就表示访问这个.char数组的第四个元素。
四、结构体指针
4.1什么是结构体指针?
前面学过一般的指针,字符指针,数组指针,函数指针。
结构体指针是一种指针,这种指针变量是指向结构体的。
这种指针有什么用?结构体指针变量存储结构体变量的地址。
&变量名就可以取出地址了,同理解引用*也适用,完全可以迁移过来。
4.2结构体指针声明和初始化
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define length 20
struct name {
char first[length];
char last[length];
};
struct guy {
struct name handle;//嵌套结构体
char job[length];
double wage;
};
struct guy* p = NULL;//创建结构体指针变量并赋值为空指针
int main()
{
//结构体的嵌套初始化
struct guy people[2] = {
{{"Ewen","Villard"},
"personality coach",
78356.00
},
{
{"Rodney", "Swillbelly" },
"tabloid esitor",
43240.00
}
};
struct guy a = { 0 };
struct guy* A = &a;
p = people;
printf("pointer :%p\n", p);
printf("第一个人的信息:");
printf("first name:%s last name: %s\n", (*p).handle.first, (*p).handle.last);
printf("第二个人的信息:");
printf("first name:%s last name: %s\n", (*(p+1)).handle.first, (*(p+1)).handle.last);
}
结构体的嵌套初始化看起来比较复杂,这里还是结构体数组的嵌套初始化。
结构体数组初始化要点:
1.总起大括号。
2.每个数组元素是一个结构体变量,各起一个大括号。元素之间用逗号分隔。
3.数组元素内部按顺序初始化,也可以采取指定初始化。内部有结构体,起大括号填充数据。
p = people;//这里的结构体数组与学过的一维数组类似,数组名就是首元素的地址,可以直接赋值给结构体指针p。
*p 解引用,为数组名,数组名对于一维数组来说就是首元素的地址,等价于people[0],(*p).handle.first==pepple[0].handle.first,这里是结构体的嵌套访问。(*p).handle是name的结构,再用一次·运算符访问name的两个字符数组。
4.3结构体的访问运算符其二:->运算符
这里介绍->运算符
#define len 50
#include<stdio.h>
typedef struct book {
char author[len];
char title[len];
float value;
}Book;
Book* p = NULL;
int main()
{
Book _s1 = { 0 };
p = &_s1;
printf("Please enter the _s1 title\n");
scanf("%s", p->title);
getchar();
printf("Now enter the _s1 author\n");
scanf("%s", p->author);
getchar();
printf("Now enter the _s1 author\n");
scanf("%f", &(p->value));
getchar();
printf("书名:%s ,作者:%s ,价值:%f", p->title, p->author, p->value);
return 0;
}
结构体指针通过->访问成员与结构体通过.运算符访问作用相同。
可以认为p->title==_s1.title,其结果是访问到了内存_s1存储title的数据。
->作用结构体指针,.操作符作用结构体。两者都是结构体的操作符,但适用对象不同。
这里解释一下上面代码,p->title结果是title这个数组,scanf函数占位符要求是地址,这里的p->title是字符数组的首元素的地址。而下面p->value是float的数据需要&。
五、结构体补充(先简单了解一下)
5.1 结构体特殊声明:匿名结构体
在声明结构体,可以不完全声明。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//这种结构体类型没有名字(tag),因此称为匿名结构体。
struct
{
char c;
int i;
double d;
} s1= { 'w',10,7.0 };//匿名结构体变量只能在结构体声明中创建,且初始化也必须在这里进行。
int main()
{
printf("%c %d %lf", s1.c, s1.i, s1.d);
return 0;
}
强调一遍,匿名结构体创建变量和初始化只能在声明中进行。
需要注意如果一个程序同时出现多个匿名结构体,哪怕结构体内部完全相同,编译器还是当作不同的类型。
#include<stdio.h>
struct
{
char c;
int i;
double d;
}s1 = { 'w', 10, 7.0 };
struct
{
char c;
int i;
double d;
}*p;
int main()
{
p = &s1;
printf("&p = %p", p);
return 0;
}
匿名结构体可以通过类型重命名获得名字,但是多此一举,不如一开始就正常声明结构体。
最后,如果不使用typedef ,匿名结构体只能用一次。
总结:最好不要用,否则有一堆使用上的问题等着你解决。比如,下面的自引用,匿名结构体就不能使用。
#include<stdio.h>
typedef struct
{
char c;
int i;
double d;
}S;
int main()
{
S s1 = { 's',10,7.0 };
printf("%c %d %lf",s1.c,s1.i,s1.d);
return 0;
}
5.2 结构的自引用
5.2.1链表
链表是一种数据结构(DataStructure)。
数据结构是计算机存储,组织数据的方式,它是一门CS必修课程
你可能听说过各种类型的数据结构,比如线性表(顺序表,链表,栈,队列,双端队列),串,树,还有更高级的图,并查集,红黑树等等。
链表是一种线性表。
链表就是一个链条把内存空间的相关数据串在一起,每一个数据称为一个结点(节点。)怎么建立它们之间的联系呢?
比如下面的整型1~5,由于在内存储存中不连续(假设),数组就无法使用了。
//错误写法,思考一下为什么不行
struct Node {
int data;
struct Node n;
};
一个结点存储一个整型,并告诉了下一个结点整型的数据。看似好像挺合理的,但如果创建了一个这样的结构体变量。这个变量向内存申请空间,它内部的结构体成员有结构体变量,内部的结构体变量也要重复上面的步骤。有一种陷入了死递归的感觉,已经能想象可用内存被占完,程序崩溃的样子。
因此这种写法大错特错。
sizeof(struct Node),这个结构体类型大小无穷大,不合理。
事实上,vs2022根本不会给你这样写的机会,直接编译错误了。
那怎么写呢?
前面学过指针,数据与数据之间,可以通过地址建立联系。
每个结点储存了下一个结点的地址 ,就可以通过前一个访问下一个结点。这里创建一个结构体变量,不会出现重复向内存申请空间的情况。比如,程序运行时,s1创建一个整型变量和相同结构体类型对应的结构体指针变量,只是申请这两个变量的空间。(结构体变量在内存中存储稍后说明)
struct Node {
int data;//数据
struct Node* n;//指针
}s1;
每个节点分为两部分,一个数据部分,叫做数据域;另一个是地址(指针部分),叫做指针域。
5.2.2自引用
自引用就是结构体内部有它同类型指针。(内部有它同类型的变量的写法是错误的)
typedef struct Node {
int data;//数据
struct Node* next;//指针
} node;
六、结构体与内存
6.1结构体内存对齐
在说明结构体内存对齐前,请先回答一个问题,你认为下面的结构体类型的大小是多少?
#include<stdio.h>
struct S
{
char c;
int i;
char d;
};
int main()
{
size_t ret = sizeof(struct S);
printf("sizeof(struct S) = %zd", ret);
return 0;
}
你的答案是6吗?
作者先前认为结构体的类型大小就是成员变量类型大小的求和。
char c;//一个字节
int i ;//4个字节
char d ;//一个字节
合计6个字节。然而,结果给我啪啪打脸。
为什么结果会是12个字节呢?
没错,这就是接下来要说的结构体内存对齐了。
计算的结构体的大小要符合结构体内存对齐的规则
6.1.1对齐规则(多分析,切勿死记)
struct S s = { 0 };
下面图详解,请结合上述规则分析,耐心看完。
上面没提到规则四,这里单独说一下 ,请看如下代码
#include<stdio.h>
struct S
{
char c;
int i;
char d;
};
struct S2 {
double e;
struct S f;
char g;
};//S2中嵌套了S。
struct S2 s = { 0 };//注意这里创建s变量是S2类型的结构体变量
int main()
{
printf("%zd", sizeof(s));
return 0;
}
如果成员变量是数组,或者指针的情况一样分析,相信聪明的你一定能够举一反三。
编译器会根据数据类型大小,一次性开辟空间,并不会像我们分析的那样一步一步开辟。
6.1.2如何分析一个结构体类型的大小
1.掌握6.1.1的对齐规则,先有个印象。
2./6.1.1 自己创建一个结构体,像作者一样画图分析,得出答案。
3.程序中用sizeof运算符计算检验答案。
6.2为什么存在结构体内存对齐?
前面可知,结构体内存对齐可能会浪费空间。
但实际程序运行,我们考虑时间因素。
结论:结构体内存对齐是以空间换取时间的做法。
这里自行看看(博主从资料上粘贴的)
完结撒花(
缺点:浪费空间
优点:节约时间
解决方案:浪费的同时相对节约空间的做法
1.占用空间小的集中一起
对比一下下面俩,谁内存小,不那么浪费?
struct S1
{
char c;
int i;
char d;
};
struct S2
{
char c;
char d;
int i;
};
2.设置随机默认数。
没错,设置默认随机数原因也是为了节省内存对齐浪费的空间。
1方案可以人为控制成员变量的顺序。方案2不能了对吧。
回答方案2也能人为控制。详情看6.3。
6.3修改默认随机数
当你认为默认对齐数不好时,输入#pragma预处理指令,进行如下操作就可以修改了。
当然贸然修改极有可能弄巧成拙,pack()后面建议填2的次方数。
根据实际情况调整。
#pragma pack(1)//设置默认对齐数为1
#pragma pack(0)//还原成编译器默认对齐数
over!
七、结构体传参
7.1 结构成员传参
这里的结构成员是内置数据类型。
编写一个加法函数,用结构体变量的成员传参。
#include<stdio.h>
#define NAME_MAX 20
typedef struct personinfo {
char name[NAME_MAX];
double bankfund;
}Peo;
double double_Add(double x, double y)//浮点数加法函数
{
return x + y;//返回参数之和
}
int main()
{
Peo s1 = { "zhangsan",5678.09 };
Peo s2 = { "lisi",8782.50 };
double ret=double_Add(s1.bankfund, s2.bankfund);
printf("Total = %.2lf", ret);
return 0;
}
函数形参不关心括号内的参数是不是结构体成员,只关心实参和形参是不是同一数据类型。
7.2 结构体本身传参(了解不要用)
重申不要使用这种写法,看看就行
#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("%lf ", tmp.d);
}
int main()
{
struct S s = { {1,2,3,4,5},100,3.14 };
print1(s);
return 0;
}
解释:
在学习函数中,有一句话,函数在传值调用时,形参是实参的一份拷贝。当你以结构体本身为形参时,形参会把实参的数据(结构体变量)都拷贝一份。了解完结构体内存之后,这种写法,不仅占用内存,而且浪费时间,吃力不讨好,代码一复杂,缺点立马显露无遗。
传地址调用(结构体指针作为函数参数),对于复杂代码效果立马立竿见影了,起码作者心里舒服了不少。
7.3 结构体指针传参
将7.2中代码做如下更改。
#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("%lf ", tmp->d);
}
int main()
{
struct S s = { {1,2,3,4,5},100,3.14 };
print1(&s);//参数传地址
return 0;
}
这种写法的优点:7.2的缺点被解决了,提高了效率。
总结:毫无疑问,结构体传参,传结构体指针。即7.3的方式。
八、结构体实现位段
8.1什么是位段?
注意:需要了解二进制,这里只谈C99前的位段
位段依靠结构体实现,不能认为位段也是一种自定义的数据类型
1.位段的成员必须是 int、unsigned int 或signed int
2.位段的成员名后边有⼀个冒号和⼀个数字。3.C99中位段成员的类型也可以
选择其他类型。(其他类型位段自行了解,这里不介绍)
8.1.1位段的声明
先看如下代码:
//位段式结构
struct S1 {
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
struct S2 {
int _a;//32bit
int _b;//32bit
int _c;//32bit
int _d;//32bit
};
位段的声明和结构体声明相似,区别成员变量类型是“整型家族”,还有冒号数字。
位段中位指的是二进制位。我们知道1个字节=8个bit位,整型占32个bit位。
无符号整型能存储0~2^32-1,有符号整型也能存储相当大的整型数据。
如果这里的_a只存储0,1,2,3的数据,那么32位的整型的高位都是0,只有低位的后两位可能不是0。00, 01, 10, 11知道了整型变量储存数据的大致范围,我们就可以限制整型数据的bit位数。
struct S1 {
int _a : 2;//2个bit
int _b : 5;//5个bit
int _c : 10;//10个bit
int _d : 30;//30个bit
};
这里限制bit位数有什么作用?
相信你已经猜到了,就是节省空间。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S1 {
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
struct S2 {
int _a;//32bit
int _b;//32bit
int _c;//32bit
int _d;//32bit
};
int main()
{
printf("sizeof(struct S1) = %zd\n", sizeof(struct S1));
printf("sizeof(struct S2) = %zd\n", sizeof(struct S2));
return 0;
}
这里位段几乎节约一半的空间。
8.2位段的内存分配
#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;
printf("%zd", sizeof(s));
return 0;
}
8.3位段跨平台问题
1.int 位段被当成有符号数或无符号数不确定。
2.位段中最大位的数目不确定。32位机器和64位机器,int是4字节。但16位机器int为2字节。
所以int最大位数是32,16。位段数>16则会在16位机器出问题。了解一下,因为16位机器被淘汰了。
3.位段中成员在内存中分配顺序这个标准未定义。
4.当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第⼀个位段剩余的位时,是舍弃
8.4位段的应用

8.5 位段使用的注意事项
指针部分,我们知道内存给每个字节分配地址,一个字节的起始位置分配到一个地址。
#include<stdio.h>
struct A{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
/*scanf("%d", &(sa._d)); *///错误的
//正确的
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
九.深浅拷贝问题
观察下面代码,有什么问题?
#include<stdlib.h>
#include<stdio.h>
struct person{
char* name;
int age;
};
int main()
{
struct person p1;
p1.name = (char*)malloc(sizeof(char) * 10);
p1.age = 18;
printf("Enter the name:>");
scanf("%s", p1.name);
struct person p2 = p1;
printf("p1 name:%s,age:%d\n", p1.name, p1.age);
printf("p2 name:%s,age:%d\n", p2.name, p2.age);
//使用完释放内存.
free(p1.name);
free(p2.name);
return 0;
}
问题在于重复释放同一块内存.
深浅拷贝问题,一般出现在结构体含有指针的情况.当结构体内部成员指针指向一块堆内存时.
此时进行struct person p2 = p1;p2和p1的char*成员指针引用了同一块堆内存.那么后续free一次就够了,第二次free由于第一次释放了char*指向的内存空间,导致程序错误.
错误原因:进行了浅拷贝,即值拷贝.导致对一方操作影响了另一方.
正确处理:为每个指针指向内存单独开辟独立空间,保证互不影响.用malloc函数和memcpy函数拷贝即可.
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
struct person{
char* name;
int age;
};
int main()
{
struct person p1;
p1.name = (char*)malloc(sizeof(char) * 10);
p1.age = 18;
printf("Enter the name:>");
scanf("%s", p1.name);
struct person p2;
//单独为p2的char*指针开辟独立堆内存.
p2.name = (char*)malloc(strlen(p1.name) + 1);
p2.age = p1.age;
//往内存空间拷贝字节
memcpy(p2.name,p1.name,strlen(p1.name)+1);
printf("p1 name:%s,age:%d\n", p1.name, p1.age);
printf("p2 name:%s,age:%d\n", p2.name, p2.age);
//使用完释放内存.
free(p1.name);
free(p2.name);
return 0;
}
结束
尾声
早岁已知世事艰,仍许飞鸿荡云间.