- 一、结构体基础
- 1.1 声明和定义
- 1.1 初始化和赋值
- 1.3 访问结构体成员
- 二、结构体数组
- 2.1 定义和初始化
- 2.2 访问
- 三、结构体的嵌套
- 五、指向结构体的指针
- 六、向函数传递结构体
- 6.1 只传递结构体成员
- 6.2 传递结构体指针
- 6.3 传递结构体
- 七、结构体的其他特性——不容小觑
- 7.1 结构体的大小问题
- 7.2 成员变量是字符数据和字符指针
- 7.21 注意事项
- 7.22 指针型成员变量的正确使用方式
- 7.3 结构体的复合字面量——临时结构体
- 7.4 匿名结构体
- 7.5 伸缩型数组成员
- 7.6 应用——结构体与文件、数据库
- 八、结构体的应用——链式结构
一、结构体基础
1.1 声明和定义
结构体(也有称:结构)是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member),使用结构体,有助于提高数据的表示能力。
如:
struct name{
char first[10];
char last[10];
int age;
};
声明了一个结构体name,现在并没有创建实际的数据对象,编译器没有分配存储空间,只是声明了该对象由什么组成,即模板。
上述结构中,有3个结构体成员,结构体成员可以使任何数据类型:基本数据类型,结构体,结构体指针等。
可以用struct name
来定义来定义变量,变量的结构和模板一样,相当于使用int来定义一个int型变量。
struct name Jay;
这样就定义了一个结构体变量。编译器为他分配24字节的空间。
当然也可以声明多个结构体变量,以及指向struct name类型结构的指针:
struct name Jay,David,*Jolin;
通常我们使用typedef
对结构体类型重命名:
typedef struct {
char first[10];
char last[10];
int age;
}Name;
现在:Name
就相当于前面的struct name
,这样是的代码更简洁,可读性更强。
1.1 初始化和赋值
(1)初始化
定义结构体变量的同时可以进行初始化,使用花括号括起来的初始化列表进初始化,各项之间逗号隔开,各项按照其初始化规则进行初始化,比如成员变量是一个数组,该变量应该按照数组初始化方式初始化。
Name Jay = {"Jay","Chou",18};
结构体变量也支持初始化器,即指定某几项进行初始化:
Name Jay = {.age = 18}; //只初始化age成员变量
(2)赋值
只有初始化的时候可以使用花括号进行整体复制,后面需要单独对成员进行复制。或者使用结构体字面量(后面将)。
结构体变量之间支持直接赋值,这和数组不同。
//struct name Jay={"Jay","Chou",18};
// Name Jay = {"Jay","Chou",18};
//Name Jay = {.age=18};
Name Jay;
//Jay = {"Jay","Chou",18}; //这是错误写法
//Jay = (Name){"Jay","Chou",18}; //正确
Name David = {"David","Tao",18};
Jay =David;
Jay.age = 888;
1.3 访问结构体成员
使用结构体成员运算符:.
来访问结构中的成员,访问成员变量的元素则由成员类型决定,如数组使用下标访问各个元素。
如访问结构体变量的成员age:
Jay.age
本小节测试代码:
#include <stdio.h>
typedef struct{
char first[10];
char last[10];
int age;
}Name;
int main(){
//struct name Jay={"Jay","Chou",18};
// Name Jay = {"Jay","Chou",18};
//Name Jay = {.age=18};
Name Jay;
//Jay = {"Jay","Chou",18}; //这是错误写法
//Jay = (Name){"Jay","Chou",18};
Name David = {"David","Tao",18};
Jay =David;
Jay.age = 888;
printf("%s %s,%d\n",Jay.last,Jay.first,Jay.age);
return 0;
}
输出:
本小节的内容仅适用于结构体变量成员中不含指针的情况。
二、结构体数组
结构体数组:数组中每个元素都是结构体。当素组每个元素都有多个属性时,就可以使用结构体数组,比如用它来记录一个班每个学生的信息。
2.1 定义和初始化
定义并初始化一个结构体数组,包含2个元素,每个元素都是结构体:
Name singer[2]={
{"Jay","Chou",18},
{"David","Tao",888},
};
2.2 访问
访问数组元素使用下标,访问结构体变量使用.
比如第一个数组元素的age成员
singer[0].age
本小节代码:
#include <stdio.h>
typedef struct{
char first[10];
char last[10];
int age;
}Name;
int main(){
Name singer[2]={
{"Jay","Chou",18},
{"David","Tao",888},
};
for(int i=0;i<2;i++)printf("%s %s,%d\n",singer[i].first,singer[i].last,singer[i].age);
return 0;
}
输出:
三、结构体的嵌套
即结构体的成员变量也可以是结构体。
例:
#include <stdio.h>
typedef struct{
char early_aalbum [2][20];
}Album;
typedef struct{
char first[10];
char last[10];
int age;
Album album;
}Name;
int main(){
Name singer[2]={
{"Jay","Chou",18,{"《Jay》","《Fantasy》"}},
{"David","Tao",888,{"《David Tao》","《I'm OK》"}},
};
for(int i=0;i<2;i++)printf("%s %s, %d, Album: %s, %s\n", singer[i].first,singer[i].last,singer[i].age,
singer[i].album.early_aalbum[0],singer[i].album.early_aalbum[1]);
return 0;
}
输出:
- 结构体的大小等于各成员大小之和;
- 上面的Name结构体大小时64字节,则singer这个结构体数组大小时128字节;
- 结构体大小也有可能大于成员大小之和,因为有的系统将成员放在偶数地址上。
五、指向结构体的指针
(1)声明:
Album *p;
指针p现在可以指向任意一个Album类型的结构体。如:
p = &singer[0].album
p指向了数组singer的第一个元素的成员album,该成员是个Album类型的结构体。
注意:结构体变量名不是结构体的地址。
(2)用指针访问成员
使用箭头:->
下面3种写法等价:
printf("His first albul is: %s\n",p->early_aalbum[0]);
printf("His first albul is: %s\n",singer[0].album.early_aalbum[0]);
printf("His first albul is: %s\n",(*p).early_aalbum[0]);
六、向函数传递结构体
结构体:
typedef struct{
int num1;
int num2;
}Num;
...
Num num = {888,999};
Num *p =#
6.1 只传递结构体成员
// 只传递结构体成员
int func_1(int a,int b){
return a>b? a:b;
}
...
func(num.num1,num.num2);
6.2 传递结构体指针
// 传递结构体指针
int func_2(Num *tmp){
return tmp->num1 > tmp->num2? tmp->num1 : tmp->num2;
}
...
func(p);
6.3 传递结构体
// 传递结构体
int func_3(Num tmp){
return tmp.num1 > tmp.num2? tmp.num1 : tmp.num2;
}
...
func(num);
完整实例:
#include <stdio.h>
typedef struct{
int num1;
int num2;
}Num;
// 只传递结构体成员
int func_1(int a,int b){
return a>b? a:b;
}
// 传递结构体指针
int func_2(const Num *tmp){
return tmp->num1 > tmp->num2? tmp->num1 : tmp->num2;
}
// 传递结构体
int func_3(const Num tmp){
return tmp.num1 > tmp.num2? tmp.num1 : tmp.num2;
}
int main(){
Num num = {888,999};
Num *p = #
printf("%d\n",func_1(num.num1,num.num2));
printf("%d\n",func_2(p));
printf("%d\n",func_3(num));
return 0;
}
输出:
root@CQUPTLEI:~/Linux_test/LinuxC_learn/c_struct# gcc -Wall -o test transport.c
root@CQUPTLEI:~/Linux_test/LinuxC_learn/c_struct# ./test
999
999
999
注:
- 使用
const
防止结构体被篡改;- 传递结构体的优缺点:
- 简单直接;
- 内存开销大,效率低:这种方式会在栈上创建一个结构体的副本,以保证原始结构体不变。
- 使用结构体指针的优缺点:
- 节省内存和时间;
- 可修改原始结构体(如果你想避免,可以使用const);
- 可能会增加代码复杂度;
- 要注意空指针的问题。
七、结构体的其他特性——不容小觑
7.1 结构体的大小问题
结构体的大小是由其成员变量的类型和对齐方式决定的。
-
成员变量的大小:结构体的大小受到其成员变量大小的影响。基本数据类型的大小在大多数编译器中是固定的,例如
int
通常是4个字节。对于数组或指针等类型,大小取决于其元素类型的大小。结构体的成员变量按照其定义的顺序依次存储在内存中。 -
对齐方式:为了提高内存访问的效率,结构体的成员变量通常会按照一定的对齐方式进行排列。对齐方式可以通过编译器的默认规则或指令进行控制。常见的对齐规则是按照成员变量的大小将其放置在内存地址能被整除的位置上。例如,如果一个结构体成员变量的大小为4字节,那么它通常会被放置在内存地址是4的倍数的位置上。
-
填充字节:为了满足对齐要求,有时候编译器会在结构体的成员变量之间插入额外的填充字节,以保证对齐。填充字节不属于结构体成员变量,但会影响结构体的大小。填充字节的大小取决于编译器和对齐方式。
-
结构体大小计算:结构体的大小可以使用
sizeof
运算符来计算。sizeof
返回结构体所占用的字节数,包括成员变量和填充字节。注意,sizeof
的计算结果可能会受到编译器和编译选项的影响,因此在跨平台或多编译器环境中,可能需要注意结构体大小的一致性。
需要注意的是,结构体的大小可能因编译器、对齐规则和平台而异。为了确保结构体的大小和布局符合预期,可以使用编译器提供的对齐指令或预处理指令进行控制。例如,#pragma pack
指令可以在某些编译器中用来指定结构体的对齐方式。
例:
typedef struct{
int num1;
int num2;
}Num;
...
Num num = {888,999};
...
printf("%zd\n",sizeof(num));
在Ubuntu上使用gcc编译,结果是8字节。
7.2 成员变量是字符数据和字符指针
7.21 注意事项
当结构体的成员是字符数组或字符指针的时候,要注意:
-
字符数据(字符数组):
- 确保字符数组有足够的空间来存储所需的字符串。如果字符串长度超过数组大小,可能会导致缓冲区溢出和未定义行为。
- 使用字符串处理函数时,确保输入的字符串以 null 终止(即以
\0
结尾),以避免在处理字符串时出现错误。 - 考虑字符数组的长度和预留空间,以防止内存浪费或不足。
-
字符指针:
- 确保字符指针指向有效的内存区域,且不为 null。使用字符指针之前,应该先进行空指针检查,以避免出现悬空指针错误。
- 注意字符指针的生命周期和内存管理。确保字符指针指向的内存区域在使用期间有效,并在不再需要时正确释放内存,以避免内存泄漏。
-
字符串操作和安全性:
- 当对字符数据或字符指针进行字符串操作时,务必确保输入的字符串是正确的、合法的,并进行适当的输入验证和边界检查,以避免缓冲区溢出和安全漏洞。
- 使用安全的字符串处理函数,如
strncpy()
、strncat()
等,以指定最大操作长度,防止缓冲区溢出。
-
字符编码和字符集:
- 理解所使用的字符编码和字符集。不同的编码和字符集可能影响字符数据的表示方式和处理方法。确保正确地处理和处理不同字符编码和字符集的情况。
-
内存对齐和填充字节:
- 注意结构体中字符数据和字符指针的对齐方式和填充字节,特别是在涉及跨平台或与外部环境交互的情况下。可以使用编译器指令或预处理指令进行控制。
例:
- 成员变量是一个指针,在赋值的时候让它指向一个字符串。这时,计算结构的大小时应该使用指针变量本身的大小,而不是它指向的字符串的大小,因为字符串并不存储在结构体内部,结构体成员只存储了这个指针变量。(在64位系统中,一个指针变量的大小是8字节)
- 成员变量是指针,没有初始化或其他处理就使用,这会有潜在风险,或许你的程序看起来没什么问题。
下面的代码定义了一个Test类型的结构体变量var,它有一个指针类型成员,初始化的时候将它指向一个字符串常量。
#include <stdio.h>
typedef struct{
int a;
int b;
char arr[10];
char *p;
}Test;
int main(){
char str[20]="Hello,C language.";
Test var = {11,22,{[5]='B'},str};
puts("The content of var:");
printf("a: %d\nb: %d\narr: ",var.a,var.b);
for(int i=0;i<10;i++)printf("%c ",var.arr[i]);
printf("\n*p: %s\n",var.p);
printf("Size of var is: %zd\n",sizeof(var));
return 0;
}
程序的输出:
可见,结构体变量的大小时32字节,分析:
- 大小按理应该是:4+4+10+8=26字节;
- 至少不是:4+4+10+18=36字节(那个字符串大小时18字节);
- 这说明大小还与对齐方式有关(7.1节中讲到)。
验证:
这里打印4个成员的地址:
printf("成员变量的地址:\na: %p\nb: %p\narr: %p\narr+10: %p\np: %p\n",&var.a,&var.b,var.arr,var.arr+10,&var.p);
输出:
int a
地址加上4个字节是int b
的地址,int b
加4个字节是char arr[10]
的地址;
但char arr[10]
再加10个字节,应该是:0x7fff27ce8b12
。
这就印证了7.1节中说的对齐问题,p这个指针变量大小时8字节,那么他会放在8字节对齐的地址上,即 0x7fff27ce8b18
。
7.22 指针型成员变量的正确使用方式
其实就是使用指针的注意问题。
当结构体成员是指针类型时, 不是必须要进行初始化。然而,根据具体的使用场景和需求,初始化指针成员可能是一个良好的编程实践。
如果指针成员在结构体中没有被初始化,它的初始值将是未定义的,也就是说它可能指向任意的内存地址。这可能导致在访问或解引用指针成员时出现未定义行为、段错误或崩溃。
因此,为了避免这些问题,建议在创建结构体实例时初始化指针成员,为其分配合适的内存空间,或者将其指向有效的内存位置。这可以通过以下方式之一实现:
-
直接赋值一个有效的指针:
struct MyStruct { int* ptr; }; // 初始化指针成员 struct MyStruct myStruct; int value = 42; myStruct.ptr = &value;
-
使用动态内存分配函数(如
malloc
)为指针成员分配内存(记得要free):struct MyStruct { char* str; }; // 初始化指针成员 struct MyStruct myStruct; myStruct.str = malloc(sizeof(char) * 10); if (myStruct.str != NULL) { // 分配成功,可以使用指针成员 strcpy(myStruct.str, "Hello"); }
-
将指针成员设置为
NULL
,表示它当前不指向任何有效的内存地址:struct MyStruct { float* data; }; // 初始化指针成员 struct MyStruct myStruct; myStruct.data = NULL;
在使用指针成员之前,确保已经进行了适当的初始化或赋值操作,以避免潜在的问题。对于动态分配的内存,还要记得在不需要时及时释放内存,以避免内存泄漏。
7.3 结构体的复合字面量——临时结构体
我前面的代码已经出现过:
Name Jay;
//Jay = {"Jay","Chou",18}; //这是错误写法
Jay = (Name){"Jay","Chou",18}; //正确
符复合字面量是一种语法构造,用于创建临时的、匿名的复合类型的对象,前面文章讲过数组的复合字面量:
intsum = (int []){1,1,4,6};
这里结构体的复合字面量是一个道理。
7.4 匿名结构体
前面有一段代码:
typedef struct{
char early_aalbum [2][20];
}Album;
typedef struct{
char first[10];
char last[10];
int age;
Album album;
}Name;
它可以简写为:
typedef struct{
char first[10];
char last[10];
int age;
struct {char early_aalbum [2][20];};
}Name;
这个嵌套的匿名结构体的成员可以直接作为外层结构以成员使用:Name.early_aalbum[0]
7.5 伸缩型数组成员
伸缩型数组成员(Flexible Array Member)是C语言中的一种特殊结构体成员,允许在结构体的末尾定义一个数组成员,该数组的大小可以根据需要动态调整。伸缩型数组成员在C语言中的使用主要涉及动态内存分配和管理。
详细介绍:
-
定义伸缩型数组成员:
struct MyStruct { int length; int data[]; };
在上面的示例中,
data
成员是一个伸缩型数组,它没有指定数组的大小,只定义了数组的类型。伸缩型数组成员必须是结构体的最后一个成员。 -
动态分配伸缩型数组成员:
伸缩型数组成员不能直接使用常规的结构体定义方式来分配内存,因为数组的大小是变化的。要使用伸缩型数组成员,需要结合动态内存分配函数(如malloc
)来为结构体分配内存,同时考虑数组的大小。struct MyStruct* createMyStruct(int length) { struct MyStruct* myStruct = malloc(sizeof(struct MyStruct) + length * sizeof(int)); if (myStruct != NULL) { myStruct->length = length; } return myStruct; }
在上面的示例中,使用
malloc
动态分配了足够的内存空间来容纳结构体本身的大小以及数组成员的大小。 -
使用伸缩型数组成员:
伸缩型数组成员可以像普通数组一样使用。可以通过索引来访问数组元素,但要注意确保不超出数组的有效范围。void processMyStruct(struct MyStruct* myStruct) { for (int i = 0; i < myStruct->length; i++) { printf("%d ", myStruct->data[i]); } }
在上面的示例中,通过遍历数组成员,打印出数组中的元素。
-
释放伸缩型数组成员的内存:
当不再需要使用结构体及其伸缩型数组成员时,必须记得释放分配的内存以避免内存泄漏。void destroyMyStruct(struct MyStruct* myStruct) { free(myStruct); }
在上面的示例中,使用
free
函数释放了通过malloc
动态分配的内存。
伸缩型数组成员的使用需要小心处理,特别是在内存分配、访问越界等方面。确保正确分配足够的内存,并遵守对数组成员的访问限制,以避免导致未定义行为和内存错误。
7.6 应用——结构体与文件、数据库
结构体在文件和数据库方面的应用广泛:
在文件方面的应用:
-
文件读写:结构体可以方便地用于读取和写入文件。通过将结构体的数据以二进制形式写入文件,可以保留结构体的完整信息,并可以随后从文件中读取数据并重新构建结构体。
-
文件格式:结构体可以用于定义文件的格式。例如,在图像处理中,可以使用结构体定义图像文件的头部信息、像素数据以及其他元数据。这样可以方便地将整个结构体写入文件或从文件中读取,并以结构化的方式处理图像数据。
-
序列化:结构体可以用于对象的序列化和反序列化。通过将结构体的数据转换为特定格式(如JSON、XML等)的字符串,并将其写入文件,可以将对象的状态保存到文件中。然后,可以从文件中读取该字符串,并将其转换回结构体,以还原对象的状态。
在数据库方面的应用:
-
数据库表映射:结构体可以用于将数据库表映射到编程语言中的对象。每个结构体字段可以对应数据库表中的列,从而实现对象和数据库之间的数据交互。通过将结构体实例化并将其数据插入数据库表中,或从数据库中查询数据并填充结构体字段,可以在应用程序和数据库之间方便地传输和处理数据。
-
数据库查询结果:在执行数据库查询操作后,查询结果通常以结构体的形式返回。每个查询结果的行可以映射到一个结构体实例,结构体的字段表示结果集中的列。这样,可以通过访问结构体的字段来获取和操作查询结果的数据。
-
数据库事务:结构体可以用于表示数据库事务中的数据操作。通过将相关数据存储在结构体中,并使用数据库事务管理器执行一系列数据库操作,可以确保数据操作的一致性和原子性。
例:
#include <stdio.h>
#include <mysql.h>
struct Employee {
int id;
char name[50];
double salary;
};
int main() {
// 假设已连接到MySQL数据库并执行查询操作,结果存储在MYSQL_RES对象中
MYSQL_RES *result;
// 定义结构体数组来存储查询结果
struct Employee employees[100];
int numRows = 0;
// 从查询结果中获取数据并填充结构体数组
MYSQL_ROW row;
while ((row = mysql_fetch_row(result)) != NULL) {
struct Employee employee;
employee.id = atoi(row[0]);
strcpy(employee.name, row[1]);
employee.salary = atof(row[2]);
employees[numRows++] = employee;
}
// 打印查询结果
for (int i = 0; i < numRows; i++) {
printf("ID: %d, Name: %s, Salary: %.2f\n", employees[i].id, employees[i].name, employees[i].salary);
}
// 释放查询结果和关闭数据库连接等操作...
return 0;
}
对于文件和数据库的操作,应注意数据的一致性、完整性和安全性。正确地处理文件和数据库操作可以提高应用程序的性能和可靠性。此外,在使用结构体进行文件和数据库操作时,还应注意处理错误、异常情况和数据格式兼容性等问题。
八、结构体的应用——链式结构
结构体的重要用途就是创建新的数据形式,如:队列、二叉树、堆、哈希表、图等。
后面开单独章节讲解。
把 永 远 爱 你 写 进 诗 的 结 尾 ~