像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
注意:本文值讲解结构体、枚举、共用体在C语言中的浅层语法及用法,并不涉及其实现原理(如内存对齐和位段),这些原理将在C语言进阶时再做讲解!
结构体Struct
结构体的理解:
- 数组可以存放同一种数据类型的数据,那么想存储一组不同数据类型的数据,该如何?
- 使用结构体(Struct)来存放一组不同类型的数据。
- 结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。
结构体的特点:
- C语言要求结构体至少得有一个成员,也就是说C语言并不允许空结构体的出现!
- 可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。
结构体的两种常见分类:
- 标准结构体:标准情况“struct 结构体名 { 数据类型…… }结构体变量名;”
- 匿名结构体:锁定变量变量个数“struct { 数据类型…… }结构体变量名;”即没有结构体名
结构体变量
总共有三种方法可以定义结构体变量。
1、先定义结构体类型,在定义变量
struct stu { //定义一个结构体,这种数据类型是 “struct stu”;
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2; //stu1、stu2就是结构体变量;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。stu 就像一个“模板”,定义出来的变量都具有相同的性质。
2、定义结构体的同时就定义变量
struct{ //没有写 stu,后面就没法用该结构体定义新的变量。
char *name; //姓名,结构体成员变量
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2; //在定义时就初始化两个结构体变量;
3、直接定义结构体类型变量,省略类型名
struct {
char *name;
int age;
} stu;
结构体变量的引用方式:
- 1、结构体变量名.成员名:stu1.name
- 2、结构体指针变量->成员名:ps -> name
- 3、(*结构体指针变量).成员名:(*ps).name 因为优先级的问题,所以要加括号
- 4、结构体变量数组名.成员名:stu[0].name
结构体变量的赋值
总共有两种方式。
第一种方式:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义结构体类型时不要直接给成员赋值,因为结构体只是一个类型,还没有分配空间
//而只有根据其类型定义变量时,才分配空间,有空间后才能赋值
typedef struct Teacher
{
char name[50];
int age;
}Teacher;
int main()
{
Teacher t1 = { "lily", 22 };
//相同类型的两个结构体变量,可以相互赋值
//原理是:把t1成员变量内存的值拷贝给t2成员变量的内存
//本质上:t1和t2没有关系
Teacher t2 = t1;
printf("%s, %d\n", t2.name, t2.age);
}
第二种方式:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义结构体类型时不要直接给成员赋值,因为结构体只是一个类型,还没有分配空间
//而只有根据其类型定义变量时,才分配空间,有空间后才能赋值
typedef struct Teacher
{
char name[50];
int age;
}Teacher;
void copyTeacher(Teacher to, Teacher from) //结构体拷贝的时候,不能用这种方式,这种是副本机制
{
to = from;
printf("[copyTeacher] %s, %d\n", to.name, to.age);
}
void copyTeacher2(Teacher *to, Teacher *from) //只有这种才能完成结构体的真正拷贝,传递进来地址
{
*to = *from;
//printf("[copyTeacher] %s, %d\n", to.name, to.age);
}
int main()
{
Teacher t1 = { "lily", 22 };
Teacher t3;
memset(&t3, 0, sizeof(t3));
copyTeacher2(&t3, &t1); //t1拷贝给t3
printf("[t3]%s, %d\n", t3.name, t3.age);
}
结构体数组
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Teacher //定义一个结构体
{
char name[50];
//char *name;
int age;
}Teacher;
int main(void)
{
Teacher a[3] = { //结构体数组初始化,方式1
{ "a", 18 },
{ "a", 18 },
{ "a", 18 }
};
//结构体数组初始化方式2,等价于方式1。都是静态的分配
Teacher a2[3] = { "a", 18, "b", 28, "c", 38 };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s, %d\n", a2[i].name, a2[i].age);
}
//结构体数组初始化方式3。动态分配内存
Teacher *p = (Teacher *)malloc(3 * sizeof(Teacher)); //申请内存
if (p == NULL)
{
return -1;
}
char buf[50];
for (i = 0; i < 3; i++) //写入数据
{
sprintf(buf, "name%d%d%d", i, i, i); //把"name%d%d%d"格式化到buf中去
strcpy(p[i].name, buf);
p[i].age = 20 + i;
}
for (i = 0; i < 3; i++) //显示
{
printf("第%d个:%s, %d\n", i + 1, p[i].name, p[i].age);
}
printf("\n");
if (p != NULL)
{
free(p);
p = NULL;
}
system("pause");
return 0;
}
结构体作为函数参数
#include<stdio.h>
#include<stdlib.h>
// 定义一个结构体
struct Student {
int age;
};
void test(struct Student stu) {
printf("修改前的形参:%d \n", stu.age);
// 修改实参中的age
stu.age = 10;
printf("修改后的形参:%d \n", stu.age);
}
int main() {
struct Student stu = { 30 };
printf("修改前的实参:%d \n", stu.age);
// 调用test函数
test(stu);
printf("修改后的实参:%d \n", stu.age);
system("pause");
return 0;
}
/*output:
修改前的实参:30
修改前的形参:30
修改后的形参:10
修改后的实参:30
*/
结构体指针
结构体指针的理解:
- 结构体指针和其他类型的指针都是一样的理解,在32位平台不管啥类型的指针都占4个字节的空间。
- 结构体指针就是指向结构体变量的指针;
- 如果一个指针变量中保存了结构体变量的首地址,那么这个指针变量就指向该结构体变量.
- 通过结构体指针即可访问该结构体变量,这与数组指针和函数指针的情况是相同的
- 结构指针变量说明的一般形式为:
struct 结构体名 *结构体指针变量名;//规范是要初始化,即struct name *var = NULL;
struct student *p = &Boy; //假设事先定义了 struct student Boy;
将结构体变量传递给函数,可有三种方法:
- 形参是结构体成员,实参是对应结构体成员的值,参数传递是将结构体成员的值传递给形参。
- 形参是结构体变量,实参是结构体变量的值,参数传递是将结构体变量的值传递给形参。
- 形参是指向结构体类型的指针,实参是结构体变量的地址或指向结构体变量的指针,参数传递是将结构体变量的首地址传递给形参。
前两种方法属于值传递方式(副本机制),结构体规模较大时,空间和时间的开销很大,一般较少使用。最后一种是地址传递方式。
结构体指针
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Teacher
{
char *name;
int age;
}Teacher;
int main()
{
//1、结构体中含指针的结构体变量初始化,第一种方式
//原理是:直接在堆一个空间,使结构体变量指向该空间
Teacher t;
t.name = (char *)malloc(30);
strcpy(t.name, "lily");
t.age = 22;
printf("name = %s, age = %d\n", t.name, t.age);
if (t.name != NULL)
{
free(t.name);
t.name = NULL;
}
//2、第二种方式
Teacher *p = NULL; //结构体指针变量
p = (Teacher *)malloc(sizeof(Teacher)); //为结构体指针变量p,在堆上分配空间
p->name = (char *)malloc(30); //为结构体中的某个成员再分配内存。
strcpy(p->name, "lilei");
p->age = 22;
printf("name = %s, age = %d\n", p->name, p->age);
if (p->name != NULL)
{
free(p->name);
p->name = NULL;
}
if (p != NULL)
{
free(p);
p = NULL;
}
}
枚举(Enum)
枚举是一种高级数据类型
一般形式为:enum 枚举名 {枚举元素1,枚举元素2,……};
例如:enum Season {spring, summer, autumn, winter};
枚举的定义
1.先定义枚举类型,再定义枚举变量
enum Season { spring, summer, autumn, winter };
enum Season s;
2.定义枚举类型的同时定义枚举变量
enum Season { spring, summer, autumn, winter } s;
3.省略枚举名称,直接定义枚举变量
enum { spring, summer, autumn, winter } s;
相关语法
1、C语言编译器会将枚举元素(spring、summer等)作为整型常量处理,称为枚举常量。
2、枚举元素的值取决于定义时各枚举元素排列的先后顺序。默认情况下,第一个枚举元素的值为0,第二个为1,依次顺序加1。
enum Season {spring, summer, autumn, winter};
也就是说spring的值为0,summer的值为1,autumn的值为2,winter的值为3
3、也可以在定义枚举类型时改变枚举元素的值
enum season {spring, summer=3, autumn, winter};
没有指定值的枚举元素,其值为前一元素加1。也就说spring的值为0,summer的值为3,autumn的值为4,winter的值为5。
操作枚举变量
1.赋值:可以给枚举变量赋枚举常量或者整型值
enum Season {spring, summer, autumn, winter} s;
s = spring; // 等价于 s = 0;
s = 3; // 等价于 s = winter;
2.遍历枚举元素
enum Season {spring, summer, autumn, winter} s;
// 遍历枚举元素
for (s = spring; s <= winter; s++) {
printf("枚举元素:%d \n", s);
}
输出:
枚举元素:0
枚举元素:1
枚举元素:2
枚举元素:3
共用体(Union)
对共用体的理解:
- 共用体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。可定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。
- 共用体中的所有成员是共享一段内存,因此每个成员的存放首地址相对于联合体变量的基地址的偏移量为 0,即 所有成员的首地址都是一样的。为了使得所有成员能够共享一段内存,因此该空间必须足够容纳这些成员中最宽的成员。
共用体的定义:
union [共用体名,可有可无]
{
member definition; //共用体成员的定义
member definition;
...
member definition;
}[一个或多共用体变量,可有可无];
共用体的特性:
- 同一内存在每一瞬时只能保存一个成员
- 起作用的成员是最后一次赋值的成员
- 只能对联合体/共用体的第一个成员进行初始化,不然后面的赋值会覆盖掉第一个的值
- 共用的体变量的地址和他的各成员地址都是同一个地址,就是首地址
- 共用体结构可以互相嵌套
共用体的赋值和使用问题
共用体的赋值和使用问题
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
//共用体占用的内存是共用体中最大的成员所占内存,因为这样才能足够存储共用体中最大的成员。
//例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。
int main()
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy(data.str, "C Programming");
printf("data.i : %d\n", data.i);
printf("data.f : %f\n", data.f);
printf("data.str : %s\n", data.str);
return 0;
}
运行结果:
由运行结果可以得出:共用体的 i 和 f 成员的值不是所期望的,因为最后赋给变量的值占用了内存位置,变量的赋值以最后一次赋值为准,这也是 str 成员能够完好输出的原因。
正确地使用共用体
正确使用共用体
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main()
{
union Data data;
data.i = 10;
printf("data.i : %d\n", data.i);
data.f = 220.5;
printf("data.f : %f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str : %s\n", data.str);
return 0;
}
运行结果:
分析:一个时间只使用一个变量,这是共用体真正的目的;