结构体和联合体数据类型
所谓的结构体,实际上就是一种特殊的类型,千万不能想的复杂。我的理解为,其产生主要是为了处理一些原本存在的类型之间的一一对应关系,其重点就在于解决一一对应的问题。
试想一下,一个学生的最基本的个人信息,包括姓名和学号,而且这两者之间是一一对应的(让我们暂时忽略掉重名的情况)。但是我们应该怎么表示这个一一对应的关系呢?在没学结构体之前我就思考过类似的问题,当时的想法是:
- 构建一个数组和一个字符串组,然后保证对应序号的名字和学号一一对应就好了
此处这个字符串组的意思是:一个组,组里的每个基本元素都是一个字符串
这样看似问题解决了,但是又引申出了两个新的问题:
- 如果需要移动的话,必须保证学号和姓名同时移动,这样才能保证其一一应对的关系不变
- 怎样定义一个“组”让其基本元素是字符串
而结构体的出现,就完美的解决了我刚才提到的这些问题,只需要定义一个student
变量类型,这个变量类型里有一个char *name
记录名字,同时还有一个int ID
记录学号,用计算机语言来表示就是:
struct student
{
char name[20];
int ID;
};
也就是说每个student
都有其对应的name
和ID
,且name
和ID
保持一一对应的关系,在移动的时候会保证两者一起移动,且对其中的单独元素也可进行修改。
也可也把结构体看成一个大的组,里边可以装各种各样的东西,不仅这些东西可以类型不一样
int
和char
,甚至本身就可以是数组或者字符串,更夸张的是结构体里也能套结构体
1. 结构体类型的定义和使用
而为了便于理解,我将通过一道题目《通讯录排序》为例子进行说明:
该题目要求:输入n个朋友的信息,包括姓名、生日、电话号码,本题要求编写程序,按照年龄从大到小的顺序依次输出通讯录。题目保证所有人的生日均不相同。
1.1 结构体类型的定义
1.1.1 一般形式定义
- 结构体定义类型的一般形式为:【后边了解
typedef
之后还有些写法】
struct 结构体名
{
数据类型名 结构体成员1;
数据类型名 结构体成员i;
};
其实这个定义并不用刻意的去记忆,见过一两次例题,基本上就都知道了
而真正需要注意的是:
-
在调用自己定义的结构体类型的时候需要加上
struct
如区分下边两种情况
- 正确:
struct friend x[100];
- 错误:
friend x[100];
- 正确:
-
在定义结构体的末尾需要加上
;
,c
语言的语句(除了预处理)后边都需要加上分号
1.1.2 typedef定义形式
-
typedef
关键词:其作用在于给现有的数据类型取别名,一般形式为:typedef 数据类型名 别名;
数据类型名放在中间,别名放在后边,有了这种方法过后原本比较复杂的结构体名,就可以用一个简单是形式进行表示。其使用具体方式如下
//定义方式1
typedef struct firend
{
char name[11]; //姓名
char birthday[9]; //生日
char phone[30]; //电话
} FRE;
//定义方式2
typedef struct
{
char name[11]; //姓名
char birthday[9]; //生日
char phone[30]; //电话
} FRE;
这两种方式都可以将原本的struct friend
简化成FRE
,个人更习惯于使用第二种,原因在于可以少想一个结构体名(起名废的快乐就是如此的简单)
1.2 结构体变量的使用方法
刚才的内容,其实都是在说,这种变量类型是如何定义的,但是定义之后的使用才是重点需要掌握的部分。其使用方法和一般变量的使用情况是基本类似的,只是多了一个选择成员的步骤。
1.2.1 变量的初始化(赋值)
- 结构体变量的初始化:需要对每个成员量进行初始化
可以通过直接赋初值和通过输入的形式赋初值两种形式,其要求和每个变量的初值要求相同,具体情况可以参考以下代码:
#include <stdio.h>
#include <stdlib.h>
#define N 10
typedef struct firend
{
char name[11]; //姓名
char birthday[9]; //生日
char phone[30];
} FRE;
void Input(FRE *p) //录入信息
{
scanf("%s %s %s", p->name, p->birthday, p->phone);
}
int main()
{
int n;
scanf("%d", &n);
FRE friends;
//初始化
friends = {"wang", "19821020", "+86-0571-88018448"};
//赋值情况1
Input(&friends); //指针形式
//赋值情况2
scanf("%s %s %s", friends.name, friends.birthday, friends.phone);
return 0;
}
需要注意的是:
- 表达
friend
中的成员name
有两种方式:friend.name
friend->name
- 通过外部输入的时候不要忘记
&
符号(此处均为字符串,本身就是地址) - 初始化的时候,注意区分
' '
和" "
,其中字符串用双引号
1.2.2 变量的输出
和刚才提到的变量的输入一样,变量的输出和输入完全是一样的
在此以上边信息的输出函数为例,展示变量的输出
void Output(FRE *p, int n) //输出信息
{
for (int i = 0; i < n; i++)
printf("%s %s %s\n", (p + i)->name, (p + i)->birthday, (p + i)->phone);
}
输入输出其实都很简单,主要就在于清楚的表示各成员量,因此在此处就不进行过多的说明
1.2.3 变量的运算
结构体虽然不能直接数学运算(主要是因为其内部成员太多太杂),但是其内部的每个成员,都满足和原来单个出现时候一样的运算规则。由于变量的运算可以说基本没有差别,因此此处重点说说,结构体的函数调用形式,在此通过一个排序函数进行说明:
void Sort(FRE *p, int n) //排序
{
FRE temp;
for (int i = 0; i < n; i++)
for (int j = 0; j < n - i - 1; j++)
if (atoi((p + j)->birthday) > atoi((p + j + 1)->birthday))
temp = *(p + j), *(p + j) = *(p + j + 1), *(p + j + 1) = temp;
}
题目要求按照人们的年龄进行排序,其实也就是按照生日进行排序。
atoi
,即ASCII to int
,是一个将字符转化为数字的函数
- 其函数原型为:
int atoi(const char *nptr);
此处根据采用冒泡算法针对年龄进行排序,想说明的有两点:
-
是结构体类型的变量,只要是同类型的就可以直接进行赋值
需要尤其注意的是,即使两个结构体成员内容完全一致,且一一对应,只要不是同一个类型,都不可以
在此通过一个程序更好的进行理解:
#include <stdio.h> typedef struct { char name[11]; } FRE1; typedef struct { char name[11]; } FRE2; int main() { FRE1 a,b; FRE2 c; a = b; a = c; return 0; }
可以看出的是
-
只要是类型名相同的结构体变量,就可以相互赋值
a = b;
-
反之,即使
FRE1
和FRE2
内容完全一样,且对应,依然不能进行赋值a = c;
(会报错)
-
-
结构体变量传入的是值,而不是地址(这里和数组不同,更像一般的变量)
而这要求我们如果希望达到处理数组的效果(影响会在主函数保留),就需要通过传递地址的方式处理。
就如同上一个例子中,排序的处理,传进来的是
*p
,也就是对应的结构体的地址。如果只是一结构体作为参数,传入值的话,那么需要将结构体设置为返回值。但是此处为结构体数组,本身就是一组结构体,处理起来会稍微有些麻烦。个人的建议就是,习惯用传地址的方式进行书写,因为我们能遇到的情况,传地址都能解决问题。
2. 结构体数组和指针的关系
其实在前边的内容里,或多或少的都有提到关于结构体数组和指针的关系,在此单独列出,是为了进行更加详细的解释说明。
2.1 结构体变量和指针的关系
- 定义形式:
FRE *p;
:定义变量类型为FRE
的指针,可以指向FRE
类型的结构体
指针的定义和赋值形式,都和常规变量类型一致,只需要牢牢的保证变量类型和指针类型相对应的原则即可。需要注意的是如何用结构体的指针变量访问成员分量,有两种访问方式:
-
指针指向结构体:
*p = &friend
-
常规式:
(*p).name
:即访问地址p
的结构体的name
分量请务必不要忘记大括号,因为访问分量的
.
和->
的优先级都是最高的如果不加括号,则意味着,先访问分量再取地址,会出现错误
-
箭头式:
p->name
:个人喜欢用第二种,因为简单(不用打括号)具体的例子可以看上边的程序,是通过第二种方式方位
p
处的分量
2.2 结构体类型数组和指针的关系
其实整理到这里的时候,我感觉内容已经有些过于冗长,原因是因为正如前边所说,结构体其实就是一种自己定义的新的变量类型。新加入的规则并不多,按照数组的方式理解结构体数组就完全没有问题。因此在此部分,个人重点介绍,自己认为比较重要的地方:
正如最开始所说,结构体的一个特别之处在于,其可以在结构体里套用结构体,反复嵌套,通过一个程序进行说明:
#include <stdio.h>
struct T1 {
char c[4];
char *s;
} s1= {"abc","def"};
struct T2 {
char *cp;
struct T1 ss1;
} s2= {"ghi","jkl","mno"};
int main() {
printf("%c,%c\n",s1.c[0],*s1.s);
printf("%s,%s\n",s1.c,s1.s);
printf("%s,%s\n",s2.cp,s2.ss1.s);
printf("%s,%s\n",++s2.cp,++s2.ss1.s);
return 0;
}
输出结果:
a,d
abc,def
ghi,mno
hi,no
- 存在嵌套的初始化:按照顺序一一赋值
从定义和输出的结果中我们不难看出,对于内含结构体T1
的结构体T2
,其赋值遵循从上到下一一赋值的规律
struct T2
{
char *cp;
struct T1 ss1;
} s2= {"ghi",{"jkl","mno"}}; //加上大括号会更加的规范
而这样赋予初值的意思就是:给cp
,T1
的第一个分量(c[4]
),T2
的第一个分量(*s
),依次赋值。
- 存在嵌套的指针:只有第一层用指针
如果我想通过指针的方式调用s2.ss1.s
,存在下列两种写法
T2 *p = &s2;
printf("%s,%s\n", ++s2.cp, p->ss1.s); //写法1
printf("%s,%s\n", ++s2.cp, p->ss1->s); //写法2
曾经的我认为两种写法都是可以的,毕竟我平常就习惯用->
的方式写。不过当我真正写过一次之后,我发现写法2
是会报错的。原因是因为ss1
实际上并不是地址。
未完待续