有的时候,我们所遇到的数据结构,不仅仅是一群数字或者是字符串那么简单。比如我们每一个人的学籍信息,学号是一个长整数,名字却是字符;甚至有更复杂的情况,这种问题在现实生活中并不少见。我们之前学过一种叫数组的数据结构,它可以允许我们把很多同类型的数据集中在一起处理。相对于之前,这已经是一次极大的进步。但是,新的问题,往往又会出现,这个时候,我们就得上更高端的装备——结构体。
相比于数组,结构体有以下的更强大的优势:
结构体的声明与定义
声明
结构体的声明使用struct
关键字,如果我们想要把我们的学籍信息组织以下的话,可以这样表示:
struct Info
{
unsigned long identifier;//学号,用无符号长整数表示
char name[20];//名字,用字符数组表示
unsigned int year;//入学年份,用无符号整数表示
unsigned int years;//学制,用无符号整数表示
}
这样,我们就相当于描绘好了一个框架,以后要用的话直接定义一个这种类型的变量就好了。
定义
我们刚刚申请了一个名叫Info
的结构体类型,那么理论上我们可以像声明其他变量的操作一样,去声明我们的结构体操作,但是C语言中规定,声明结构体变量的时候,struct
关键字是不可少的。
struct 结构体类型名 结构体变量名
不过,你可以在某个函数里面定义:
#include <stdio.h>
struct Info
{
unsigned long identifier;//学号,用无符号长整数表示
char name[20];//名字,用字符数组表示
unsigned int year;//入学年份,用无符号整数表示
unsigned int years;//学制,用无符号整数表示
};
int main(void)
{
/**
*在main函数中声明结构体变量
*结构体变量名叫info
*struct关键字不能丢
*/
struct Info info;
...
}
也可以在声明的时候就把变量名定义下来(此时这个变量是全局变量):
#include <stdio.h>
struct Info
{
unsigned long identifier;//学号,用无符号长整数表示
char name[20];//名字,用字符数组表示
unsigned int year;//入学年份,用无符号整数表示
unsigned int years;//学制,用无符号整数表示
} info;
/**
*此时直接定义了变量
*该变量是全局变量
*变量名叫info
*/
int main(void)
{
...
}
访问结构体成员
结构体成员的访问有点不同于以往的任何变量,它是采用点号运算符.
来访问成员的。比如,info.name
就是引用info
结构体的name
成员,是一个字符数组,而info.year
则可以查到入学年份,是个无符号整型。
比如,下面开始录入学生的信息:
//Example 01
#include <stdio.h>
struct Info
{
unsigned long identifier;//学号,用无符号长整数表示
char name[20];//名字,用字符数组表示
unsigned int year;//入学年份,用无符号整数表示
unsigned int years;//学制,用无符号整数表示
};
int main(void)
{
struct Info info;
printf("请输入学生的学号:");
scanf("%d", &info.identifier);
printf("请输入学生的姓名:");
scanf("%s", info.name);
printf("请输入学生的入学年份:");
scanf("%d", &info.year);
printf("请输入学生的学制:");
scanf("%d", &info.years);
printf("\n数据录入完毕\n\n");
printf("学号:%d\n姓名:%s\n入学年份:%d\n学制:%d\n毕业时间:%d\n", \
info.identifier, info.name, info.year, info.years, info.year + info.years);
return 0;
}
运行结果如下:
//Consequence 01
请输入学生的学号:20191101
请输入学生的姓名:Harris
请输入学生的入学年份:2019
请输入学生的学制:4
数据录入完毕
学号:20191101
姓名:Harris
入学年份:2019
学制:4
毕业时间:2023
初始化结构体
像数组一样,结构体也可以在定义的时候初始化,方法也几乎一样:
struct Info info = {
20191101,
"Harris",
2019,
4
};
在C99标准中,还支持给指定元素赋值(就像数组一样):
struct Info info = {
.name = "Harris",
.year = 2019
};
对于没有被初始化的成员,则数值型成员初始化为0,字符型成员初始化为‘\0’。
对齐
下面这个代码,大家来看看会发生什么:
//EXample 02 V1
#include <stdio.h>
int main(void)
{
struct A
{
char a;
int b;
char c;
} a = {'a', 10, 'o'};
printf("size of a = %d\n", sizeof(a));
return 0;
}
我们之前学过,char
类型的变量占1字节,int
类型的变量占4字节,那么这么一算,一个结构体A型的变量应该就是6字节了。别急,我们看运行结果:
//COnsequence 02 V1
size of a = 12
怎么变成12了呢?标准更新了?老师教错了?都不是。我们把代码改一下:
//EXample 02 V2
#include <stdio.h>
int main(void)
{
struct A
{
char a;
char c;
int b;
} a = {'a', 'o', 10};
printf("size of a = %d\n", sizeof(a));
return 0;
}
结果:
//Consequence 02 V2
size of a = 8
实际上,这是编译器对我们程序的一种优化——内存对齐。在第一个例子中,第一个和第三个成员是char
类型是1个字节,而中间的int
却有4个字节,为了对齐,两个char
也占用了4个字节,于是就是12个字节。
而在第二个例子里面,前两个都是char
,最后一个是int
,那么前两个可以一起占用4个字节(实际只用2个,第一个例子也同理,只是为了访问速度更快,而不是为了扩展),最后的int
占用4字节,合起来就是8个字节。
关于如何声明结构体来节省内存容量,可以阅读下面的这篇文章,作者是艾瑞克·雷蒙,时尚最具争议性的黑客之一,被公认为开源运动的主要领导者之一:
结构体嵌套
在学籍里面,如果我们的日期想要更加详细一些,精确到day,这时候就可以使用结构体嵌套来完成:
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;//学号,用无符号长整数表示
char name[20];//名字,用字符数组表示
struct Date date;/*---入学日期,用结构体Date表示---*/
unsigned int years;//学制,用无符号整数表示
};
int main(void)
{
...
}
如此一来,比我们单独声明普通变量快多了。
不过,这样访问变量,就必须用点号一层层往下访问。比如要访问day
这个成员,那就只能info.date.day
而不能直接info.date
或者info,day
。
//Example 03
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;//学号,用无符号长整数表示
char name[20];//名字,用字符数组表示
struct Date date;/*---入学日期,用结构体Date表示---*/
unsigned int years;//学制,用无符号整数表示
};
int main(void)
{
struct Info info;
printf("请输入学生的学号:");
scanf("%d", &info.identifier);
printf("请输入学生的姓名:");
scanf("%s", info.name);
printf("请输入学生的入学年份:");
scanf("%d", &info.date.year);
printf("请输入学生的入学月份:");
scanf("%d", &info.date.month);
printf("请输入学生的入学日期:");
scanf("%d", &info.date.day);
printf("请输入学生的学制:");
scanf("%d", &info.years);
printf("\n数据录入完毕\n\n");
printf("学号:%d\n姓名:%s\n入学时间:%d/%d/%d\n学制:%d\n毕业时间:%d\n",\
info.identifier, info.name,\
info.date.year, info.date.month, info.date.day,\
info.years, info.date.year + info.years);
return 0;
}
运行结果如下:
//Consequence 03
请输入学生的学号:20191101
请输入学生的姓名:Harris
请输入学生的入学年份:2019
请输入学生的入学月份:9
请输入学生的入学日期:7
请输入学生的学制:4
数据录入完毕
学号:20191101
姓名:Harris
入学时间:2019/9/7
学制:4
毕业时间:2023
结构体数组
刚刚我们演示了存储一个学生的学籍信息的时候,使用结构体的例子。那么,如果要录入一批学生,这时候我们就可以沿用之前的思路,使用结构体数组。
我们知道,数组的定义,就是存放一堆相同类型的数据的容器。而结构体一旦被我们声明,那么你就可以把它看作一个类型,只不过是你自己定义的罢了。
定义结构体数组也很简单:
struct 结构体类型
{
成员;
} 数组名[长度];
/****或者这样****/
struct 结构体类型
{
成员;
};
struct 结构体类型 数组名[长度];
结构体指针
既然我们可以把结构体看作一个类型,那么也就必然有对应的指针变量。
struct Info* pinfo;
但是在指针这里,结构体和数组就不一样了。我们知道,数组名实际上就是指向这个数组第一个元素的地址,所以可以将数组名直接赋值给指针。而结构体的变量名并不是指向该结构体的地址,所以要使用取地址运算符&
才能获取地址:
pinfo = &info;
通过结构体指针来访问结构体有以下两种方法:
(*结构体指针).成员名
结构体指针->成员名
第一个方法由于点号运算符比指针的取值运算符优先级更高,因此需要加一个小括号来确定优先级,让指针先解引用变成结构体变量,在使用点号的方法去访问。
相比之下,第二种方法就直观许多。
这两种方法在实现上是完全等价的,但是点号只能用于结构体变量,而箭头只能够用于指针。
第一种方法:
#include <stdio.h>
...
int main(void)
{
struct Info *p;
p = &info;
printf("学号:\n", (*p).identifier);
printf("姓名:\n", (*p).name);
printf("入学时间:%d/%d/%d\n", (*p).date.year, (*p).date.month, (*p).date.day);
printf("学制:\n", (*p).years);
return 0;
}
第二种方法:
#include <stdio.h>
...
int main(void)
{
struct Info *p;
p = &info;
printf("学号:\n", p -> identifier);
printf("姓名:\n", p -> name);
printf("入学时间:%d/%d/%d\n", p -> date.year, p -> date.month, p -> date.day);
printf("学制:\n", p -> years);
return 0;
}
传递结构体信息
传递结构体变量
我们先来看看下面的代码:
//Example 04
#include <stdio.h>
int main(void)
{
struct Test
{
int x;
int y;
}t1, t2;
t1.x = 3;
t1.y = 4;
t2 = t1;
printf("t2.x = %d, t2.y = %d\n", t2.x, t2.y);
return 0;
}
运行结果如下:
//Consequence 04
t2.x = 3, t2.y = 4
这么看来,结构体是可以直接赋值的。那么既然这样,作为函数的参数和返回值也自然是没问题的了。
先来试试作为参数:
//Example 05
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;
char name[20];
struct Date date;
unsigned int years;
};
struct Info getInput(struct Info info);
void printInfo(struct Info info);
struct Info getInput(struct Info info)
{
printf("请输入学号:");
scanf("%d", &info.identifier);
printf("请输入姓名:");
scanf("%s", info.name);
printf("请输入入学年份:");
scanf("%d", &info.date.year);
printf("请输入月份:");
scanf("%d", &info.date.month);
printf("请输入日期:");
scanf("%d", &info.date.day);
printf("请输入学制:");
scanf("%d", &info.years);
return info;
}
void printInfo(struct Info info)
{
printf("学号:%d\n姓名:%s\n入学时间:%d/%d/%d\n学制:%d\n毕业时间:%d\n", \
info.identifier, info.name, \
info.date.year, info.date.month, info.date.day, \
info.years, info.date.year + info.years);
}
int main(void)
{
struct Info i1 = {};
struct Info i2 = {};
printf("请录入第一个同学的信息...\n");
i1 = getInput(i1);
putchar('\n');
printf("请录入第二个学生的信息...\n");
i2 = getInput(i2);
printf("\n录入完毕,现在开始打印...\n\n");
printf("打印第一个学生的信息...\n");
printInfo(i1);
putchar('\n');
printf("打印第二个学生的信息...\n");
printInfo(i2);
return 0;
}
运行结果如下:
//Consequence 05
请录入第一个同学的信息...
请输入学号:20191101
请输入姓名:Harris
请输入入学年份:2019
请输入月份:9
请输入日期:7
请输入学制:4
请录入第二个学生的信息...
请输入学号:20191102
请输入姓名:Joy
请输入入学年份:2019
请输入月份:9
请输入日期:8
请输入学制:5
录入完毕,现在开始打印...
打印第一个学生的信息...
学号:20191101
姓名:Harris
入学时间:2019/9/7
学制:4
毕业时间:2023
打印第二个学生的信息...
学号:20191102
姓名:Joy
入学时间:2019/9/8
学制:5
毕业时间:2024
传递指向结构体变量的指针
早期的C语言是不允许直接将结构体作为参数直接传递进去的。主要是考虑到如果结构体的内存占用太大,那么整个程序的内存开销就会爆炸。不过现在的C语言已经放开了这方面的限制。
不过,作为一名合格的开发者,我们应该要去珍惜硬件资源。那么,传递指针就是一个很好的办法。
将刚才的代码修改一下:
//Example 06
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;
char name[20];
struct Date date;
unsigned int years;
};
void getInput(struct Info *info);
void printInfo(struct Info *info);
void getInput(struct Info *info)
{
printf("请输入学号:");
scanf("%d", &info->identifier);
printf("请输入姓名:");
scanf("%s", info->name);
printf("请输入入学年份:");
scanf("%d", &info->date.year);
printf("请输入月份:");
scanf("%d", &info->date.month);
printf("请输入日期:");
scanf("%d", &info->date.day);
printf("请输入学制:");
scanf("%d", &info->years);
}
void printInfo(struct Info *info)
{
printf("学号:%d\n姓名:%s\n入学时间:%d/%d/%d\n学制:%d\n毕业时间:%d\n", \
info->identifier, info->name, \
info->date.year, info->date.month, info->date.day, \
info->years, info->date.year + info->years);
}
int main(void)
{
struct Info i1 = {};
struct Info i2 = {};
printf("请录入第一个同学的信息...\n");
getInput(&i1);
putchar('\n');
printf("请录入第二个学生的信息...\n");
getInput(&i2);
printf("\n录入完毕,现在开始打印...\n\n");
printf("打印第一个学生的信息...\n");
printInfo(&i1);
putchar('\n');
printf("打印第二个学生的信息...\n");
printInfo(&i2);
return 0;
}
此时传递的就是一个指针,而不是一个庞大的结构体。