C语言程序设计:结构体

基础数据类型用来定义单个变量,数组用来定义某种数据类型的集合,但有些情况需要定义包含多种数据类型的变量,例如定义一个学生变量,该变量可能需要包含字符数组类型的姓名、字符型的性别、整型的年龄、浮点型的成绩等,基础数据类型和数组无法满足此种需求,此时就要用到结构体。

一、结构体类型

1.1、结构体类型的声明

结构体类型,是由多个数据类型不同的变量组成的,组成它的每一个变量都称为该结构体类型的成员。在程序中使用结构体类型之前,需要先对结构体类型进行声明。

结构体类型的声明方式如下:

1
2
3
4
5
6
7
struct 结构体类型名称 
{
数据类型 成员名1;
数据类型 成员名2;

数据类型 成员名n;
};

在该语法格式中,“struct”是声明结构体类型的关键字,在“struct”关键字后声明了“结构体类型名称”,在“结构体类型名称”下的大括号中,声明了结构体类型的成员,每个成员由“数据类型”和“成员名”共同组成。

以描述学生信息为例,假设该信息包含学号(num)、姓名(name)、性别(sex)、年龄(age)、地址(address)5项,这时可以使用下列语句声明一个名为student的结构体类型:

1
2
3
4
5
6
7
8
struct student
{
int num;
char name[10];
char sex;
int age;
char address[30];
};

在该定义中,结构体类型struct student由5个成员组成,分别是num、name、sex、age和address。

这里需要注意的是:

  • 结构体类型声明以关键字struct开头,后面跟的是结构体类型的名称,该名称的命名规则与变量名相同。
  • 结构体类型与整型、浮点型、字符型等类似,只是数据类型,而非变量。
  • 声明好一个结构体类型后,并不意味着分配一块内存单元来存放各个数据成员,它只是告诉编译系统结构体类型由哪些类型的成员构成、各占多少字节、按什么格式存储,并把它们当作一个整体来处理。

1.2、结构体变量的定义

声明结构体类型,相当于自定义了一个数据类型,其中并无具体数据,系统也不会为它分配实际的内存空间。为了能在程序中使用结构体类型的数据,应该定义结构体类型的变量。定义结构体变量有以下两种方式

  • 先声明结构体类型,再定义结构体变量
  • 声明结构体类型的同时,定义结构体变量

1.2.1、先声明结构体类型,再定义结构体变量

声明好结构体类型后,方可定义结构体变量,定义结构体变量的语法格式如下:

1
struct 结构体类型名 结构体变量名;

以前定义的结构体类型struct student为例,定义结构体变量的方式如下:

1
struct student stu1,stu2;

该语法结构定义了2个结构体类型变量stu1和stu2,这时,stu1和stu2便具有了结构体特征,它们各自都存储了一组基本类型的变量,具体如下图所示。

img

从图中可以看出,变量stu1和stu2分别占据了一块连续的内存单元。

1.2.2、在声明结构体类型时,定义结构体变量

在声明结构体类型时,也可定义结构体变量,其语法格式如下:

1
2
3
4
5
6
7
struct 结构体类型名称
{
数据类型 成员名1;
数据类型 成员名2;
...
数据类型 成员名n;
}结构体变量名列表;

以声明struct student结构体,并定义struct student类型的变量stu1、stu2为例,具体示例如下:

1
2
3
4
5
6
struct student
{
int num;
char name[10];
char sex;
}stu1, stu2;

该代码在声明结构体类型struct student的同时定义了结构体类型变量stu1和stu2,该方式的作用与先定义结构体类型,再定义结构体变量作用相同,其中,stu1和stu2中所包含的成员类型都是一样的。

1.3、结构体变量的大小

结构体变量一旦被定义,系统就会为其分配内存。为方便系统对变量的访问,保证读取性能,结构体变量中各成员在内存中的存储遵循字节对齐机制,该机制的具体规则如下。

  • 结构体变量所占内存能够被其最宽基本类型成员的大小所整除。
  • 结构体每个成员相对于结构体首地址的偏移量都是该成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节。

假设程序中声明了如下所示的结构体:

1
2
3
4
5
6
7
struct student
{
char a;
double b;
int c;
short d;
};

按基础数据类型计算,该代码所示的结构体中4项成员共占据15个字节,但作为结构体计算,这4项成员在编译器优化之后共占据24个字节。因为结构体中double型变量b占据最多字节,所以编译器以double类型的长度8字节为准。经扩充后,实现内存对齐的存储结构如下图所示。

img

计算机在存储时字节没有对齐的话会进行填充,如上图所示,struct student类型的结构体以成员b的类型double为准,对结构体进行了填充,灰色区域是填充区域,具体填充情况为:变量a占据1字节,扩充7个字节;变量d占据2字节,扩充2个字节。打印struct student类型变量中各成员地址,具体如下所示:

1
2
3
4
a=0x3afa18
b=0x3afa20
c=0x3afa28
d=0x3afa2c

注:程序运行结束后其变量所占内存空间会被回收,因此每次运行程序后打印的地址不相同。

1.4、结构体的嵌套

结构体类型中的成员可以是一个结构体变量。这种情况称为结构体嵌套。简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Date
{
int year;
int month;
int day;
};

struct student
{
char num[12];
double b;
int c;
struct Date d;
};

当结构体中存在结构体类型成员时,结构体在内存中的存储依旧遵循内存对齐机制,此时结构体以其普通成员以及结构体成员中的最长数据类型为准,对各成员进行对齐。如上示例中,结构体struct student中double类型为最长数据类型,因此在内存中该结构体以8字节为单位进行对齐,具体如下图所示。

img

打印该结构体变量各成员的地址,打印结果如下所示:

1
2
3
4
5
6
s.num=0x22fb28 
s.b=0x22fb38
s.c=0x22fb40
s.d.year=0x22fb44
s.d.month=0x22fb48
s.d.day=0x22fb4

1.5、结构体变量的初始化

结构体变量中存储了一组不同类型的数据。因此,结构体变量初始化的过程,其实就是为结构体中各个成员初始化的过程。根据结构体变量定义方式的不同,结构体变量初始化的方式可分为两种。

(1)声明结构体类型和定义结构体变量的同时,对结构体变量初始化,具体示例如下:

1
2
3
4
5
6
struct Student
{
int num;
char name[10];
char sex;
}stu={20140101, "Zhang San", 'M'};

该代码在声明结构体类型struct Student的同时定义了结构体变量stu,并对stu中的成员进行了初始化。

(2)声明结构体类型之后,定义结构体变量并对结构体变量初始化,具体示例如下:

1
2
3
4
5
6
7
8
struct Student
{
int num;
char name[10];
char sex;
};

struct Student stu={20140101, "Zhang San", 'M'};

该代码,首先声明了一个结构体类型struct Student,然后在定义结构体变量stu时对其中的成员进行初始化。

1.6、结构体变量的访问

定义并初始化结构体变量的目的是使用结构体变量中的成员。在C语言中,访问结构体变量中成员的方式如下所示:

1
结构体变量名.成员名

以前面定义的结构体变量stu为例,访问其中成员num的方法如下所示:

1
stu.num;

接下来,我们演示如何输出结构体中的成员变量,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Student
{
char name[24];
int age;
};

int main()
{
struct Student s={"Zhang San", 23};
printf("%s %d\n", s.name, s.age);

return 0;
}

该代码中的结构体struct Student包含了24个字节大小的字符数组name和整型变量age。main()函数在执行时,初始化结构体变量s,分别给name和age赋值。最后终端打印信息如下:

1
Zhang San 23

二、结构体数组

一个结构体变量可以存储一组数据,如一个学生的学号、姓名、性别等。如果有10个学生的信息需要存储,可以采用结构体数组。与普通数组类似,结构体数组中的每个元素都是结构体类型。

2.1、结构体数组的定义

假设一个班有20个学生,如果我们需要描述这20个学生的信息,可以定义一个容量为20的struct Student类型数组,与定义结构体变量一样,可以采用两种方式定义结构体数组。

(1)先声明结构体类型,后定义结构体数组,具体示例如下:

1
2
3
4
5
6
7
8
struct Student
{
int num;
char name[10];
char sex;
};

struct Student stus[20];

(2)声明结构体类型的同时定义结构体数组,具体示例如下:

1
2
3
4
5
6
struct Student
{
int num;
char name[10];
char sex;
}stus[20];

2.2、结构体数组的初始化

结构体数组的初始化方式与普通数组类似,都通过为元素赋值的方式完成。结构体数组中的每个元素都是一个结构体变量,在为元素赋值的时候,需要将每个成员的值依次放到一对大括号中。

例如,定义一个结构体数组students,该数组有3个元素,每个元素有num、name、sex这3个成员,可以采用下列两种方式对结构体数组students初始化。

(1)先声明结构体数组类型,然后定义并初始化结构体数组,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct Student
{
int num;
char name[10];
char sex;
};

struct Student students[3]={
{20140101, "Zhang San",'M'},
{20140102, "Li Si",'W'},
{20140103, "Zhao Liu",'M'}
};

(2)在定义结构体数组的同时,对结构体数组初始化,具体示例如下:

1
2
3
4
5
6
7
8
9
10
struct Student
{
int num;
char name[10];
char sex;
}students[3]={
{20140101, "Zhang San",'M'},
{20140102, "Li Si",'W'},
{20140103, "Zhao Liu",'M'}
};

2.3、结构体数组的访问

结构体数组的访问是指对结构体数组元素的访问,结构体数组的每个元素都是一个结构体变量,因此,结构体数组的访问就是对数组元素中的成员进行访问,其语法格式如下:

1
结构体数组变量名[下标].成员名

接下来,我们演示如何将结构体数组中的成员输出,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Student
{
char name[50];
int stuId;
};

int main()
{
struct Student Stu[2]={{"Zhang San", 20140000}, {"Li Si", 20140001}};

for(int i=0; i<2; i++)
{
printf("%s %d\n", Stu[i].name, Stu[i].stuId);
}

return 0;
}

该代码,先定义了一个长度为2的结构体数组Stu,并对数组中的元素进行了初始化,然后使用for循环,依次输出了数组元素Stu[0]和Stu[1]中的成员值。终端输出结果如下:

1
2
Zhang San 20140000
Li Si 20140001

三、结构体与指针

除了基本数据类型,指针还可以指向结构体,指向结构体的指针称为结构体指针,它的用法与一般指针用法没有太大差异。

3.1、结构体指针

3.1.1、定义

使用结构体指针之前,需要先定义结构体指针,结构体指针的定义方式与一般指针类似,具体示例如下:

1
2
struct Student Stu={"Zhang San", 20140100, "M", 93.5};
struct Student *p=&Stu;

该代码定义了一个struct Student类型的指针p,并通过“&”符号将结构体变量Stu的地址赋给p,因此,p是指向结构体变量Stu的指针。

3.1.2、访问

结构体指针访问成员变量的语法格式如下:

1
结构体指针名->成员名

当程序中定义了一个指向结构体变量的指针后,就可以通过“指针名->成员变量名”的方式来访问结构体变量中的成员,结构体指针的用法示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Student
{
char name[24];
int studentId;
};

int main()
{
struct Student Stu={"Zhang San", 10001};
struct Student *p=&Stu;
printf("%s %d\n", p->name, p->studentId);

return 0;
}

示例先定义了一个结构体类型变量Stu,并将变量Stu中的成员name初始化为Zhang San,成员studentId初始化为10001,然后定义了一个结构体指针p,将p指向变量Stu的地址,最后通过p->name、p->studentId访问成员name和studentId的值。终端显示结果如下:

1
Zhang San  10001

3.2、结构体数组指针

指针可以指向结构体数组,即指针变量可以存储结构体数组的起始地址。

例如,下面语句定义了struct Student类型的一个结构体数组和一个指向该数组的结构体指针:

1
struct Student stu1[10], *p=&stu1;

该代码中,p是一个student结构体数组指针,从定义上看,它和结构体指针没什么区别,只不过指向的是结构体数组。

接下来,我们来演示如何使用结构体数组指针输出多个学生的信息,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct student
{
int num;
char name[20];
char sex;
int age;
}Stu[3]={
{201401001, "Wang Ming", 'M', 19},
{201401002, "Zhang Ning", 'W', 23},
{201401003, "zhou lan", 'M', 20}
};

int main()
{
struct student *p;
for(p=Stu; p<Stu+3; p++)
printf("%ld\t%-12s\t%-2c\t%4d\n", p->num, p->name, p->sex, p->age);

return 0;
}

该代码在定义结构体数组Stu的同时将其初始化,for循环中的“p=Stu”用于将p指向结构体数组Stu的第1个元素,“p++”用于将指针指向下一个元素,表示每执行一次循环就会跳到下一个数组元素,“Stu+3”表示数组中最后一个元素的地址。程序在for循环中使用指针访问结构体数组元素成员,并使用printf()函数依次打印访问到的成员。终端中输出的结构体数组Stu中所有元素的成员值如下:

1
2
3
201401001   Wang Ming    M  19
201401002 Zhang Ning W 23
201401003 zhou lan M 20

四、结构体与函数

在函数间不仅可以传递简单的变量、数组、指针等类型的数据,还可以传递结构体类型的数据。

4.1、结构体变量作为函数参数

结构体变量作为函数参数的用法与普通变量类似,都需要保证调用函数的实参类型和被调用函数的形参类型相同。将结构体变量作为函数参数传递数据,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Student
{
char name[50];
int stuId;
};

void printInfo(struct Student stu)
{
printf("name: %s\n", stu.name);
printf("id: %d\n", stu.stuId);
}

int main()
{
struct Student Stu={"Zhang San", 10001};
printInfo(Stu);

return 0;
}

该代码中定义了一个用于输出数据的printInfo()函数,函数的形式参数是结构体类型。当将结构体变量作为参数传递给函数时,其传参的方式与普通变量相同。在main()函数中定义结构体变量Stu初始化结构体,调用prinfInfo()函数,终端打印结果如下:

1
2
name: Zhang San
id: 10001

4.2、结构体数组作为函数参数

函数间不仅可以传递一般的结构体变量,还可以传递结构体数组。使用结构体数组作为函数参数传递数据,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Student
{
char name[24];
int stuId;
};

void printInfo(struct Student Stu[], int len)
{
int i;
for(i=0; i<len; i++)
{
printf("name: %s\n", Stu[i].name);
pritnf("id: %d\n", Stu[i].stuId);
}
}

int main()
{
struct Student Stu[3]=
{
{ "Zhang San", 1 },
{"Li Si",2},
{"Wang Wu", 3}
};
printfInfo(Stu, sizeof(Stu)/sizeof(Stu[0]));

return 0;
}

该代码在main()函数中先定义结构体数组Stu并初始化,然后调用printInfo()函数打印该数组。printInfo()函数有两个参数,第1个参数Stu接收结构体数组,第2个参数接收使用sizeof运算符求得的结构体数组长度。printInfo()函数内部接收到传递来的数组名和长度后,使用for循环将结构体数组中的所有成员输出。打印结果如下:

1
2
3
4
5
6
name: Zhang San
id: 1
name: Li Si
id: 2
name: Wang Wu
id: 3

4.3、结构体指针作为函数参数

结构体指针变量用于存放结构体变量的首地址,所以将指针作为函数参数传递时,其实就是传递结构体变量的首地址。结构体指针作为函数参数传递数据的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Student
{
char name[50];
int studentID;
};

void printInfo(struct Student* stu)
{
printf("name: %s\n", stu->name);
printf("id: %d\n\n", stu->studentID);
}

int main()
{
struct Student student={"Zhang San", 1};
printInfo(&student);
return 0;
}

该代码中定义了一个用于输出数据的printInfo()函数,该函数需要接收一个结构体指针类型的参数。结构体指针作为函数参数时,需要传递的是结构体变量的首地址,因此在调用printInfo()函数时通过“&”符号获取结构体变量struct Student的地址作为参数。其终端输出结果如下:

1
2
name: Zhang San
id: 1

五、typedef的使用

typedef关键字用于为现有数据类型取别名,例如,结构体、指针、数组、int、double等都可以使用typedef关键字为它们另取一个名字。使用typedef关键字可以方便程序的移植,减少对硬件的依赖性。

typedef关键字语法格式如下:

1
typedef 数据类型 别名;

数据类型包括基本数据类型、构造数据类型、指针等,接下来我们就针对这几项进行详细讲解。

5.1、为基本类型取别名

使用typedef关键字为unsinged int类型取别名,示例代码如下:

1
typedef unsinged int u8;

该语句为数据类型unsigned int取了别名u8,在程序中可以用u8定义无符号整型变量,示例代码如下:

1
u8 i,j,k;

5.2、为数组类型取别名

使用typedef关键字为数组取别名,示例代码如下:

1
2
typedef char NAME[10];
NAME class1, class2;

该代码为可存储10个元素的字符型数组取了别名NAME,并用别名NAME定义了两个字符数组class1和class2,这两个字符数组等价于使用语句“char class1[10],class2[10];”定义的字符数组class1和class2。

5.3、为结构体取别名

使用typedef关键字为结构体类型struct Student取别名,示例代码如下:

1
2
3
4
5
6
7
8
typedef struct Student
{
int num;
char name[10];
char sex;
}STU;

STU stu1;

该代码先声明了一个struct Student类型的结构体,并使用typedef关键字为其取了别名STU,之后用别名STU定义了结构体变量stu1。此段代码中定义结构体变量的语句等效于下面这行语句:

1
struct Student stu1;

需要注意的是,使用typedef关键字只是对已存在的类型取别名,而不是定义了新的数据类型。有时也可以用宏定义来代替typedef的功能,但是宏定义在预处理阶段只会被替换,它不进行正确性检查,且在某些情况下不够直观,而typedef是直到编译时才替换的,使用typedef更加灵活。

六、小结

七、参考


C语言程序设计:结构体
https://kuberxy.github.io/2024/08/25/C语言程序设计:结构体/
作者
Mr.x
发布于
2024年8月25日
许可协议