MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言结构体与动态内存分配的结合

2022-05-171.8k 阅读

C语言结构体与动态内存分配的结合

结构体基础回顾

在C语言中,结构体(struct)是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起形成一个单一的实体。例如,我们要描述一个学生,可能需要学生的姓名(字符串)、年龄(整数)和成绩(浮点数),使用结构体就可以将这些不同类型的数据组合到一起。

// 定义一个学生结构体
struct Student {
    char name[50];
    int age;
    float score;
};

在上述代码中,我们定义了一个名为Student的结构体,它包含三个成员:name(字符数组,用于存储姓名)、age(整数,代表年龄)和score(浮点数,记录成绩)。

结构体变量的声明和初始化

一旦定义了结构体类型,就可以声明该类型的变量。声明变量的方式与声明基本数据类型变量类似,只不过这里使用的是结构体类型。

struct Student stu1; // 声明一个Student类型的变量stu1

也可以在定义结构体类型的同时声明变量:

struct Student {
    char name[50];
    int age;
    float score;
} stu2; // 定义结构体类型的同时声明变量stu2

初始化结构体变量时,可以按照结构体成员的顺序提供初始值。

struct Student stu3 = {"Alice", 20, 85.5};

访问结构体成员

通过结构体变量名和成员运算符(.)可以访问结构体的成员。

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student stu = {"Bob", 21, 90.0};
    printf("Name: %s\n", stu.name);
    printf("Age: %d\n", stu.age);
    printf("Score: %.2f\n", stu.score);
    return 0;
}

在上述代码中,我们通过stu.namestu.agestu.score分别访问结构体stu的各个成员,并将其打印出来。

动态内存分配基础

在C语言中,动态内存分配允许程序在运行时根据需要分配和释放内存。这与静态内存分配(在编译时分配内存)不同,动态内存分配提供了更大的灵活性,特别是在处理大小不确定的数据结构时。

动态内存分配函数

C语言提供了几个用于动态内存分配的函数,主要包括malloccallocrealloc

  1. malloc函数malloc函数用于分配指定字节数的内存块,并返回一个指向该内存块起始地址的指针。如果分配失败,返回NULL
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    ptr = (int *)malloc(5 * sizeof(int)); // 分配5个整数大小的内存
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    // 使用ptr指向的内存
    for (int i = 0; i < 5; i++) {
        ptr[i] = i * 2;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    free(ptr); // 释放分配的内存
    return 0;
}

在上述代码中,我们使用malloc分配了足够存储5个整数的内存,并使用free函数释放了这块内存。需要注意的是,在使用malloc返回的指针之前,要检查是否为NULL,以避免空指针引用错误。

  1. calloc函数calloc函数用于分配指定数量的指定大小的内存块,并将所有字节初始化为0。它返回一个指向分配内存起始地址的指针,如果分配失败,返回NULL
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    ptr = (int *)calloc(5, sizeof(int)); // 分配5个整数大小的内存并初始化为0
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    // 使用ptr指向的内存
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    free(ptr); // 释放分配的内存
    return 0;
}

calloc的第一个参数是元素的数量,第二个参数是每个元素的大小。

  1. realloc函数realloc函数用于调整先前分配的内存块的大小。它可以增大或减小已分配内存块的大小。如果重新分配成功,返回一个指向新内存块的指针(可能与原来的指针相同,也可能不同);如果失败,返回NULL,并且原来的内存块保持不变。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    ptr = (int *)malloc(3 * sizeof(int)); // 初始分配3个整数大小的内存
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    // 使用ptr指向的内存
    for (int i = 0; i < 3; i++) {
        ptr[i] = i * 3;
    }
    int *newPtr;
    newPtr = (int *)realloc(ptr, 5 * sizeof(int)); // 重新分配为5个整数大小的内存
    if (newPtr == NULL) {
        printf("Memory re - allocation failed\n");
        free(ptr);
        return 1;
    }
    ptr = newPtr;
    // 使用扩展后的内存
    for (int i = 3; i < 5; i++) {
        ptr[i] = i * 3;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    free(ptr); // 释放分配的内存
    return 0;
}

在上述代码中,我们首先使用malloc分配了3个整数大小的内存,然后使用realloc将其扩展为5个整数大小的内存。

结构体与动态内存分配结合

当我们处理结构体时,有时结构体的实例数量是不确定的,或者结构体本身的大小在运行时才能确定。这时就需要将结构体与动态内存分配结合起来。

动态分配结构体数组

假设我们要管理一个班级的学生信息,学生数量在运行时才能确定。我们可以动态分配一个结构体数组来存储这些学生信息。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    int numStudents;
    printf("Enter the number of students: ");
    scanf("%d", &numStudents);

    struct Student *students = (struct Student *)malloc(numStudents * sizeof(struct Student));
    if (students == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    for (int i = 0; i < numStudents; i++) {
        printf("Enter details for student %d:\n", i + 1);
        printf("Name: ");
        scanf("%s", students[i].name);
        printf("Age: ");
        scanf("%d", &students[i].age);
        printf("Score: ");
        scanf("%f", &students[i].score);
    }

    printf("\nStudent details:\n");
    for (int i = 0; i < numStudents; i++) {
        printf("Student %d:\n", i + 1);
        printf("Name: %s\n", students[i].name);
        printf("Age: %d\n", students[i].age);
        printf("Score: %.2f\n", students[i].score);
    }

    free(students);
    return 0;
}

在上述代码中,我们首先从用户处获取学生的数量numStudents,然后使用malloc动态分配一个能存储numStudentsStudent结构体的内存块。接着,我们通过循环获取每个学生的详细信息,并在最后打印出来。最后,别忘了使用free释放分配的内存。

动态分配结构体指针成员

有时候,结构体中的某个成员本身也是一个需要动态分配内存的指针。例如,假设我们要优化学生姓名的存储,使用动态分配的字符串而不是固定大小的字符数组。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Student {
    char *name;
    int age;
    float score;
};

int main() {
    struct Student stu;
    char tempName[50];

    printf("Enter student name: ");
    scanf("%s", tempName);

    stu.name = (char *)malloc(strlen(tempName) + 1);
    if (stu.name == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    strcpy(stu.name, tempName);

    printf("Enter student age: ");
    scanf("%d", &stu.age);
    printf("Enter student score: ");
    scanf("%f", &stu.score);

    printf("\nStudent details:\n");
    printf("Name: %s\n", stu.name);
    printf("Age: %d\n", stu.age);
    printf("Score: %.2f\n", stu.score);

    free(stu.name);
    return 0;
}

在上述代码中,Student结构体的name成员是一个指针。我们首先从用户处获取学生姓名并存储在临时数组tempName中,然后使用mallocstu.name分配足够的内存来存储这个字符串,并使用strcpy将临时数组中的字符串复制到动态分配的内存中。在使用完后,通过free(stu.name)释放动态分配的字符串内存。

结构体链表中的动态内存分配

链表是一种常用的数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。在C语言中,结构体与动态内存分配的结合在链表的实现中起着关键作用。

单链表的实现

  1. 定义链表节点结构体
struct Node {
    int data;
    struct Node *next;
};

在上述代码中,Node结构体包含两个成员:data用于存储数据(这里是整数类型),next是一个指向Node类型的指针,用于指向下一个节点。

  1. 创建新节点
struct Node *createNode(int value) {
    struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        return NULL;
    }
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

createNode函数用于创建一个新的链表节点。它使用malloc为新节点分配内存,将传入的值赋给data成员,并将next指针初始化为NULL

  1. 插入节点到链表头部
struct Node *insertAtHead(struct Node *head, int value) {
    struct Node *newNode = createNode(value);
    if (newNode == NULL) {
        return head;
    }
    newNode->next = head;
    return newNode;
}

insertAtHead函数用于将一个新节点插入到链表的头部。它首先调用createNode创建新节点,然后将新节点的next指针指向当前的头部节点,并返回新的头部节点。

  1. 打印链表
void printList(struct Node *head) {
    struct Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

printList函数用于遍历并打印链表中的所有节点。它从头部节点开始,依次打印每个节点的数据,并移动到下一个节点,直到遇到NULL指针。

  1. 释放链表内存
void freeList(struct Node *head) {
    struct Node *current = head;
    struct Node *nextNode;
    while (current != NULL) {
        nextNode = current->next;
        free(current);
        current = nextNode;
    }
}

freeList函数用于释放链表中所有节点分配的内存。它从头部节点开始,依次释放每个节点的内存,并移动到下一个节点。

以下是完整的单链表实现代码:

#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

struct Node *createNode(int value) {
    struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        return NULL;
    }
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

struct Node *insertAtHead(struct Node *head, int value) {
    struct Node *newNode = createNode(value);
    if (newNode == NULL) {
        return head;
    }
    newNode->next = head;
    return newNode;
}

void printList(struct Node *head) {
    struct Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

void freeList(struct Node *head) {
    struct Node *current = head;
    struct Node *nextNode;
    while (current != NULL) {
        nextNode = current->next;
        free(current);
        current = nextNode;
    }
}

int main() {
    struct Node *head = NULL;
    head = insertAtHead(head, 10);
    head = insertAtHead(head, 20);
    head = insertAtHead(head, 30);

    printList(head);

    freeList(head);
    return 0;
}

在上述代码中,我们创建了一个简单的单链表,插入了几个节点,打印链表内容,最后释放链表占用的内存。

双链表的实现

双链表与单链表类似,但每个节点除了有一个指向下一个节点的指针外,还有一个指向前一个节点的指针。

  1. 定义双链表节点结构体
struct DNode {
    int data;
    struct DNode *prev;
    struct DNode *next;
};

DNode结构体包含三个成员:data用于存储数据,prev是指向前一个节点的指针,next是指向后一个节点的指针。

  1. 创建新双链表节点
struct DNode *createDNode(int value) {
    struct DNode *newNode = (struct DNode *)malloc(sizeof(struct DNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        return NULL;
    }
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

createDNode函数用于创建一个新的双链表节点,与单链表创建节点类似,只是多了对prev指针的初始化。

  1. 插入节点到双链表头部
struct DNode *insertDAtHead(struct DNode *head, int value) {
    struct DNode *newNode = createDNode(value);
    if (newNode == NULL) {
        return head;
    }
    if (head != NULL) {
        head->prev = newNode;
    }
    newNode->next = head;
    return newNode;
}

insertDAtHead函数将新节点插入到双链表的头部,除了更新next指针外,还需要更新当前头部节点的prev指针(如果头部节点不为NULL)。

  1. 打印双链表
void printDList(struct DNode *head) {
    struct DNode *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

printDList函数与单链表的打印函数类似,用于遍历并打印双链表的内容。

  1. 释放双链表内存
void freeDList(struct DNode *head) {
    struct DNode *current = head;
    struct DNode *nextNode;
    while (current != NULL) {
        nextNode = current->next;
        free(current);
        current = nextNode;
    }
}

freeDList函数用于释放双链表中所有节点的内存,与单链表释放内存的方式相同。

以下是完整的双链表实现代码:

#include <stdio.h>
#include <stdlib.h>

struct DNode {
    int data;
    struct DNode *prev;
    struct DNode *next;
};

struct DNode *createDNode(int value) {
    struct DNode *newNode = (struct DNode *)malloc(sizeof(struct DNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        return NULL;
    }
    newNode->data = value;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

struct DNode *insertDAtHead(struct DNode *head, int value) {
    struct DNode *newNode = createDNode(value);
    if (newNode == NULL) {
        return head;
    }
    if (head != NULL) {
        head->prev = newNode;
    }
    newNode->next = head;
    return newNode;
}

void printDList(struct DNode *head) {
    struct DNode *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

void freeDList(struct DNode *head) {
    struct DNode *current = head;
    struct DNode *nextNode;
    while (current != NULL) {
        nextNode = current->next;
        free(current);
        current = nextNode;
    }
}

int main() {
    struct DNode *head = NULL;
    head = insertDAtHead(head, 10);
    head = insertDAtHead(head, 20);
    head = insertDAtHead(head, 30);

    printDList(head);

    freeDList(head);
    return 0;
}

通过上述对单链表和双链表的实现,我们可以看到结构体与动态内存分配在链表这种数据结构中的紧密结合。在链表的操作中,动态内存分配为节点的创建和销毁提供了灵活性,而结构体则用于封装节点的数据和指针,使得链表的实现更加清晰和高效。

动态内存分配与结构体的注意事项

  1. 内存泄漏:在使用动态内存分配时,最常见的错误之一就是内存泄漏。如果分配了内存但没有释放,随着程序的运行,内存会不断被占用,最终导致系统资源耗尽。例如,在链表操作中,如果在插入节点时分配了内存,但在删除节点时没有释放该内存,就会发生内存泄漏。
// 错误示例:内存泄漏
struct Node *badInsertAtHead(struct Node *head, int value) {
    struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = head;
    return newNode;
}

在上述代码中,createNode函数没有检查malloc是否成功,并且在函数结束时没有释放可能分配失败的内存。正确的做法应该是在malloc后检查返回值,并在不需要内存时及时调用free释放。

  1. 悬空指针:当释放了一块内存,但指针仍然指向该内存地址时,就会产生悬空指针。如果后续继续使用这个指针,可能会导致程序崩溃或未定义行为。
struct Node *head = createNode(10);
free(head);
// 错误:head此时是悬空指针
printf("%d\n", head->data);

在上述代码中,释放head指向的内存后,head成为悬空指针,再访问head->data是错误的行为。为了避免悬空指针,可以在释放内存后将指针设置为NULL

struct Node *head = createNode(10);
free(head);
head = NULL;
  1. 内存碎片:频繁地分配和释放内存可能会导致内存碎片。内存碎片是指内存中存在许多小块的空闲内存,但这些空闲内存块不连续,无法满足较大的内存分配请求。为了减少内存碎片,可以尽量一次性分配较大的内存块,而不是频繁地分配和释放小块内存。例如,在管理大量结构体实例时,可以预先分配一个较大的内存块来存储所有结构体,而不是为每个结构体单独分配内存。

  2. 对齐问题:不同的系统和编译器对内存对齐有不同的要求。结构体成员的内存布局可能会受到对齐规则的影响。当动态分配结构体内存时,要确保分配的内存满足结构体成员的对齐要求。一般来说,malloccallocrealloc函数返回的内存地址是满足对齐要求的,但在一些特殊情况下,如自定义内存分配器时,需要特别注意对齐问题。

总结结构体与动态内存分配结合的优势

  1. 灵活性:结合结构体与动态内存分配,程序可以在运行时根据实际需求分配和释放内存,这对于处理大小不确定的数据结构(如链表、动态数组等)非常有用。例如,在管理学生信息时,如果学生数量在编译时不确定,通过动态分配结构体数组,可以灵活地适应不同数量的学生。

  2. 高效内存利用:动态内存分配允许程序在需要时获取内存,不需要时释放内存,避免了静态内存分配可能导致的内存浪费。例如,在链表中,每个节点按需分配内存,只有在节点存在时才占用内存,节点删除时即释放内存。

  3. 数据封装与组织:结构体提供了一种将相关数据封装在一起的方式,而动态内存分配则为结构体实例的创建和管理提供了动态性。这种结合使得程序可以更好地组织和管理复杂的数据结构,提高代码的可读性和可维护性。

  4. 扩展性:在程序开发过程中,需求可能会发生变化。结构体与动态内存分配的结合使得程序更容易扩展。例如,如果需要在链表节点中添加新的成员,只需要修改结构体定义,并在创建和使用节点的地方进行相应调整,而不需要重新分配固定大小的内存区域。

通过深入理解和合理运用C语言结构体与动态内存分配的结合,开发者可以编写出更加高效、灵活和健壮的程序,能够更好地应对各种复杂的编程需求。在实际编程中,要始终注意内存管理的正确性,避免内存泄漏、悬空指针等常见问题,以确保程序的稳定性和可靠性。