C语言 结构体解析 堆内存存储结构体的适用场景、优势及动态内存分配 结构体在内存中的不同分配情况

练习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;   // 程序正常结束
}
  • 试着传递NULLPerson_destroy来看看会发生什么。如果它没有崩溃,你必须移除Makefile的CFLAGS中的-g选项。
     
    // 添加测试行 
    Person_destroy(NULL);  // Pass NULL here
    当传递 NULLPerson_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)
    段错误 (核心已转储)
    其中:
    ==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==
    
    Invalid read of size 8: 尝试读取一个无效的内存地址,大小为 8 字节。一个指针在 64 位系统上通常占用 8 字节,而传递给 Person_print 的 who 参数是 NULL,导致程序试图解引用 NULL 指针,这时会发生无效读取错误。
    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;
    }
    

    总结结构体变量结构体指针适用点及其区别

创建结构体变量(在栈上分配内存)

        当直接在函数中创建结构体变量时,结构体实例会被分配到栈上,它的生命周期与所在的作用域(通常是函数作用域)相同。结构体变量会在函数退出时自动销毁,无需手动释放。
适用场景:
临时数据:适用于在函数内使用的数据,生命周期仅限于函数作用域内。
小型数据:如果结构体较小,栈上分配的开销较低,适合用于临时变量。
性能要求高:栈上的分配和回收速度较快,适合对性能有严格要求的场景。

创建结构体指针(在堆上分配内存)

        当创建结构体指针时,指针本身会存储在栈上,但结构体实例通常会分配在堆上,需要使用 malloccalloc 动态分配内存。堆上的内存不会自动释放,必须显式地使用 free() 释放。
适用场景:
跨函数或跨模块共享数据:如果需要在多个函数之间共享结构体数据,或者结构体实例的生命周期超出某个函数作用域时,使用堆上的结构体更为合适。
动态内存分配:当不知道结构体需要多少空间时,或者结构体的大小在运行时才能确定,使用堆内存是必需的。
大量数据存储:当结构体较大,栈空间不足时,使用堆内存来存储结构体实例可以避免栈溢出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值