练习16:结构体和指向它们的指针 · 笨办法学C
在这个练习中你将会学到如何创建struct
,将一个指针指向它们,以及使用它们来理解内存的内部结构。
代码分析:
#include <stdio.h> // 引入标准输入输出库,用于 printf() 函数
#include <assert.h> // 引入 assert 库,用于断言检查
#include <stdlib.h> // 引入标准库,提供 malloc() 和 free() 函数
#include <string.h> // 引入字符串操作库,提供 strdup() 函数
// 定义一个名为 Person 的结构体,包含姓名、年龄、身高和体重
struct Person {
char *name; // 姓名指针,指向一个字符串
int age; // 年龄
int height; // 身高
int weight; // 体重
};
// Person_create 函数负责创建一个新的 Person 实例并初始化其成员,返回类型是struct Person *
struct Person *Person_create(char *name, int age, int height, int weight)
{
struct Person *who = malloc(sizeof(struct Person)); // 为 Person 分配内存
assert(who != NULL); // 确保内存分配成功,否则程序中止
who->name = strdup(name); // 为姓名分配内存并复制传入的姓名字符串
who->age = age; // 设置年龄
who->height = height; // 设置身高
who->weight = weight; // 设置体重
return who; // 返回指向新创建的 Person 结构体的指针
}
// 销毁一个 Person 实例并释放相关的内存
void Person_destroy(struct Person *who)
{
assert(who != NULL); // 确保传入的指针有效,否则程序中止
free(who->name); // 释放姓名字符串的内存
free(who); // 释放 Person 结构体的内存
}
// 打印一个 Person 实例的属性
void Person_print(struct Person *who)
{
printf("Name: %s\n", who->name); // 打印姓名
printf("\tAge: %d\n", who->age); // 打印年龄
printf("\tHeight: %d\n", who->height); // 打印身高
printf("\tWeight: %d\n", who->weight); // 打印体重
}
int main(int argc, char *argv[])
{
// 创建两个 Person 结构体,并传入相应的初始化值
struct Person *joe = Person_create(
"Joe Alex", 32, 64, 140); // 创建 Joe,并设置其姓名、年龄、身高、体重
struct Person *frank = Person_create(
"Frank Blank", 20, 72, 180); // 创建 Frank,并设置其姓名、年龄、身高、体重
// 打印 Joe 的信息,并显示其在内存中的地址
printf("Joe is at memory location %p:\n", joe); // 打印 Joe 结构体的内存地址
Person_print(joe); // 打印 Joe 的详细信息
// 打印 Frank 的信息,并显示其在内存中的地址
printf("Frank is at memory location %p:\n", frank); // 打印 Frank 结构体的内存地址
Person_print(frank); // 打印 Frank 的详细信息
// 修改 Joe 和 Frank 的属性,使他们都变老 20 岁,Joe 身高减少 2,体重大增 40
joe->age += 20; // Joe 增加 20 岁
joe->height -= 2; // Joe 身高减少 2
joe->weight += 40; // Joe 体重增加 40
Person_print(joe); // 打印修改后的 Joe
frank->age += 20; // Frank 增加 20 岁
frank->weight += 20; // Frank 体重增加 20
Person_print(frank); // 打印修改后的 Frank
// 销毁 Joe 和 Frank,释放它们占用的内存
Person_destroy(joe); // 销毁 Joe,释放内存
Person_destroy(frank); // 销毁 Frank,释放内存
return 0; // 程序正常结束
}
- 试着传递
NULL
给Person_destroy
来看看会发生什么。如果它没有崩溃,你必须移除Makefile的CFLAGS
中的-g
选项。
当传递// 添加测试行 Person_destroy(NULL); // Pass NULL here
NULL
给Person_destroy
时,函数首先会执行assert(who != NULL)
。如果程序是在调试模式下运行,assert
会触发一个错误,导致程序崩溃,输出错误信息并终止执行。如果不启用调试选项(即移除-g
选项),assert
会被忽略。这时,NULL
值会被直接传递给free(who->name)
和free(who)
,而free(NULL)
是合法的,并不会导致崩溃。此时,程序会继续执行,不会有任何明显的错误。
- 在结尾处忘记调用
Person_destroy
,在Valgrind
下运行程序,你会看到它报告出你忘记释放内存。弄清楚你应该向valgrind
传递什么参数来让它向你报告内存如何泄露。
# 未使用free释放内容,造成内存泄漏 ==3677== ==3677== HEAP SUMMARY: ==3677== in use at exit: 24 bytes in 1 blocks ==3677== total heap usage: 3 allocs, 2 frees, 1,072 bytes allocated ==3677== ==3677== LEAK SUMMARY: ==3677== definitely lost: 24 bytes in 1 blocks ==3677== indirectly lost: 0 bytes in 0 blocks ==3677== possibly lost: 0 bytes in 0 blocks ==3677== still reachable: 0 bytes in 0 blocks ==3677== suppressed: 0 bytes in 0 blocks ==3677== Rerun with --leak-check=full to see details of leaked memory ==3677== ==3677== For lists of detected and suppressed errors, rerun with: -s ==3677== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) # 使用free释放内容 ==3697== ==3697== HEAP SUMMARY: ==3697== in use at exit: 0 bytes in 0 blocks ==3697== total heap usage: 3 allocs, 3 frees, 1,072 bytes allocated ==3697== ==3697== All heap blocks were freed -- no leaks are possible ==3697== ==3697== For lists of detected and suppressed errors, rerun with: -s ==3697== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
- 这一次,向
Person_print
传递NULL
,并且观察Valgrind
会输出什么。
其中:==3784== Invalid read of size 8 ==3784== at 0x109290: Person_print (ex16.c:40) ==3784== by 0x109437: main (ex16.c:77) ==3784== Address 0x0 is not stack'd, malloc'd or (recently) free'd ==3784== ==3784== ==3784== Process terminating with default action of signal 11 (SIGSEGV) ==3784== Access not within mapped region at address 0x0 ==3784== at 0x109290: Person_print (ex16.c:40) ==3784== by 0x109437: main (ex16.c:77) ==3784== If you believe this happened as a result of a stack ==3784== overflow in your program's main thread (unlikely but ==3784== possible), you can try to increase the size of the ==3784== main thread stack using the --main-stacksize= flag. ==3784== The main thread stack size used in this run was 8388608. ==3784== ==3784== HEAP SUMMARY: ==3784== in use at exit: 1,024 bytes in 1 blocks ==3784== total heap usage: 3 allocs, 2 frees, 1,072 bytes allocated ==3784== ==3784== LEAK SUMMARY: ==3784== definitely lost: 0 bytes in 0 blocks ==3784== indirectly lost: 0 bytes in 0 blocks ==3784== possibly lost: 0 bytes in 0 blocks ==3784== still reachable: 1,024 bytes in 1 blocks ==3784== suppressed: 0 bytes in 0 blocks ==3784== Rerun with --leak-check=full to see details of leaked memory ==3784== ==3784== For lists of detected and suppressed errors, rerun with: -s ==3784== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) 段错误 (核心已转储)
Invalid read of size 8: 尝试读取一个无效的内存地址,大小为 8 字节。一个指针在 64 位系统上通常占用 8 字节,而传递给 Person_print 的 who 参数是 NULL,导致程序试图解引用 NULL 指针,这时会发生无效读取错误。==3784== Invalid read of size 8 ==3784== at 0x109290: Person_print (ex16.c:40) ==3784== by 0x109437: main (ex16.c:77) ==3784== Address 0x0 is not stack'd, malloc'd or (recently) free'd ==3784==
Address 0x0: 这是 NULL 地址,NULL 被表示为 0x0。由于你传递了 NULL 给 Person_print,程序尝试访问 0x0 地址,从而引发错误。==3784== Process terminating with default action of signal 11 (SIGSEGV) ==3784== Access not within mapped region at address 0x0 ==3784== at 0x109290: Person_print (ex16.c:40) ==3784== by 0x109437: main (ex16.c:77)
SIGSEGV: 这是段错误的信号(Segmentation Fault)。它表示程序试图访问它没有权限的内存地址。在这个错误的情况下,程序试图访问 NULL 地址(0x0),这是一个无效的内存地址,因此会崩溃并终止执行。
Access not within mapped region at address 0x0: 程序试图访问地址 0x0,即 NULL 地址。这个地址没有被映射到任何有效的内存区域,所以程序崩溃。
==3784== HEAP SUMMARY: ==3784== in use at exit: 1,024 bytes in 1 blocks ==3784== total heap usage: 3 allocs, 2 frees, 1,072 bytes allocated ==3784== ==3784== LEAK SUMMARY: ==3784== definitely lost: 0 bytes in 0 blocks ==3784== indirectly lost: 0 bytes in 0 blocks ==3784== possibly lost: 0 bytes in 0 blocks ==3784== still reachable: 1,024 bytes in 1 blocks ==3784== suppressed: 0 bytes in 0 blocks
still reachable: 1,024 bytes 是程序在退出时仍然存在的内存。说明分配了内存(通过 malloc),但是这些内存没有被正确释放。程序并没有完全释放所有资源,这可能是由于某个 malloc 分配的内存未被 free,可能是NULL 作为参数时,程序并没有正确释放。
- 如何在栈上创建结构体,就像你创建任何其它变量那样。
struct Person joe = {"Joe Alex", 32, 64, 140};
- 如何使用
x.y
而不是x->y
来初始化结构体。
当有一个结构体变量而不是结构体指针时,使用
.
运算符来访问结构体成员。->
运算符用于结构体指针,而.
运算符用于结构体实例。在初始化结构体时,可以直接使用
.
运算符来设置结构体成员的值,而不需要使用指针。
例如:#include <stdio.h> #include <string.h> struct Person { char *name; int age; int height; int weight; }; int main() { // 在栈上创建结构体 struct Person joe; joe.name = "Joe Alex"; joe.age = 32; joe.height = 64; joe.weight = 140; // 输出结构体内容 printf("Name: %s\n", joe.name); printf("Age: %d\n", joe.age); printf("Height: %d\n", joe.height); printf("Weight: %d\n", joe.weight); return 0; }
- 如何不使用指针来将结构体传给其它函数。
#include <stdio.h> struct Person { char *name; int age; int height; int weight; }; // 通过值传递结构体(不使用指针) void print_person(struct Person p) { printf("Name: %s\n", p.name); printf("Age: %d\n", p.age); printf("Height: %d\n", p.height); printf("Weight: %d\n", p.weight); } int main() { // 在栈上创建一个结构体变量 struct Person joe = {"Joe Alex", 32, 64, 140}; // 通过值传递结构体 print_person(joe); return 0; }
总结结构体变量和结构体指针适用点及其区别:
创建结构体变量(在栈上分配内存)
当直接在函数中创建结构体变量时,结构体实例会被分配到栈上,它的生命周期与所在的作用域(通常是函数作用域)相同。结构体变量会在函数退出时自动销毁,无需手动释放。
适用场景:
临时数据:适用于在函数内使用的数据,生命周期仅限于函数作用域内。
小型数据:如果结构体较小,栈上分配的开销较低,适合用于临时变量。
性能要求高:栈上的分配和回收速度较快,适合对性能有严格要求的场景。
创建结构体指针(在堆上分配内存)
当创建结构体指针时,指针本身会存储在栈上,但结构体实例通常会分配在堆上,需要使用 malloc
或 calloc
动态分配内存。堆上的内存不会自动释放,必须显式地使用 free()
释放。
适用场景:
跨函数或跨模块共享数据:如果需要在多个函数之间共享结构体数据,或者结构体实例的生命周期超出某个函数作用域时,使用堆上的结构体更为合适。
动态内存分配:当不知道结构体需要多少空间时,或者结构体的大小在运行时才能确定,使用堆内存是必需的。
大量数据存储:当结构体较大,栈空间不足时,使用堆内存来存储结构体实例可以避免栈溢出。