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

C语言结构体作为函数参数的值传递

2023-08-206.2k 阅读

C 语言结构体作为函数参数的值传递

结构体基础回顾

在深入探讨结构体作为函数参数的值传递之前,我们先来回顾一下 C 语言中结构体的基本概念。结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个单一的实体。通过结构体,我们可以将相关的数据组织起来,使得程序的逻辑更加清晰和易于管理。

例如,我们定义一个表示学生信息的结构体:

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

在上述代码中,struct Student 定义了一个新的数据类型,它包含了三个成员:一个字符数组 name 用于存储学生姓名,一个整数 age 表示学生年龄,以及一个浮点数 grade 表示学生成绩。

函数参数传递方式概述

在 C 语言中,函数参数的传递方式主要有两种:值传递和指针传递。

值传递

值传递是指在调用函数时,将实际参数的值复制一份传递给函数的形式参数。在函数内部对形式参数的修改不会影响到实际参数的值。例如:

#include <stdio.h>

void increment(int num) {
    num = num + 1;
    printf("函数内部 num 的值: %d\n", num);
}

int main() {
    int number = 5;
    printf("调用函数前 number 的值: %d\n", number);
    increment(number);
    printf("调用函数后 number 的值: %d\n", number);
    return 0;
}

在上述代码中,increment 函数接收一个整数参数 num,在函数内部对 num 进行加 1 操作。但是,由于是值传递,函数内部对 num 的修改并不会影响到 main 函数中的 number 变量。程序输出如下:

调用函数前 number 的值: 5
函数内部 num 的值: 6
调用函数后 number 的值: 5

指针传递

指针传递是将实际参数的地址传递给函数的形式参数。这样,在函数内部可以通过指针来访问和修改实际参数的值。例如:

#include <stdio.h>

void increment(int *num) {
    (*num) = (*num) + 1;
    printf("函数内部 *num 的值: %d\n", *num);
}

int main() {
    int number = 5;
    printf("调用函数前 number 的值: %d\n", number);
    increment(&number);
    printf("调用函数后 number 的值: %d\n", number);
    return 0;
}

在这个例子中,increment 函数接收一个指向整数的指针 num。通过解引用指针 *num,我们可以在函数内部修改 main 函数中的 number 变量的值。程序输出如下:

调用函数前 number 的值: 5
函数内部 *num 的值: 6
调用函数后 number 的值: 6

结构体作为函数参数的值传递

当我们将结构体作为函数参数传递时,也可以采用值传递的方式。这意味着,在调用函数时,会将结构体变量的所有成员的值复制一份传递给函数的形式参数。

简单结构体值传递示例

以之前定义的 struct Student 结构体为例,我们定义一个函数来打印学生的信息:

#include <stdio.h>

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

void printStudent(struct Student student) {
    printf("姓名: %s\n", student.name);
    printf("年龄: %d\n", student.age);
    printf("成绩: %.2f\n", student.grade);
}

int main() {
    struct Student john = {"John", 20, 85.5};
    printStudent(john);
    return 0;
}

在上述代码中,printStudent 函数接收一个 struct Student 类型的结构体变量 student。在函数内部,通过结构体成员访问运算符 . 来访问结构体的各个成员,并将其打印出来。在 main 函数中,我们创建了一个 struct Student 类型的变量 john,并将其作为参数传递给 printStudent 函数。程序输出如下:

姓名: John
年龄: 20
成绩: 85.50

结构体值传递的本质

从本质上讲,结构体值传递和普通变量的值传递类似,都是将实际参数的值复制一份传递给形式参数。只不过,结构体是一个包含多个成员的复合数据类型,所以在复制时会将结构体的所有成员的值都进行复制。

当我们将结构体变量作为参数传递给函数时,系统会在函数的栈帧中为形式参数分配一块内存空间,其大小与实际传递的结构体变量的大小相同。然后,将实际参数结构体变量的各个成员的值依次复制到这块新分配的内存空间中。

例如,对于 struct Student 结构体,假设 char 类型占 1 个字节,int 类型占 4 个字节,float 类型占 4 个字节,并且考虑到内存对齐的因素,struct Student 结构体的大小可能为 56 个字节(50 + 4 + 4,并且可能存在 4 字节的对齐填充)。当我们调用 printStudent(john) 时,系统会在 printStudent 函数的栈帧中为 student 形式参数分配 56 个字节的内存空间,并将 john 结构体变量的所有成员的值复制到这 56 个字节的空间中。

结构体值传递的优缺点

优点

  1. 代码简洁直观:在函数调用时,直接传递结构体变量,不需要像指针传递那样使用取地址符 & 和解引用符 *,使得代码更加简洁易懂。例如,在上述 printStudent 函数的调用中,printStudent(john) 非常直观,很容易理解是将 john 这个学生的信息传递给函数进行打印。
  2. 安全性较高:由于传递的是结构体的副本,函数内部对形式参数的修改不会影响到实际参数。这在一些情况下可以避免意外地修改外部数据,提高程序的安全性。比如,如果 printStudent 函数内部不小心对 student 进行了修改,也不会影响到 main 函数中的 john 结构体变量。

缺点

  1. 性能开销较大:当结构体比较大时,复制结构体的所有成员值会消耗较多的时间和内存。例如,如果结构体中包含大量的数据,如大型数组或其他复杂数据类型,每次传递结构体副本都会占用较大的栈空间,并且复制操作也会花费一定的时间,可能会影响程序的性能。
  2. 无法修改实际参数:如果我们希望在函数内部修改结构体的成员值,并让这些修改反映到调用函数的实际参数中,使用值传递就无法实现。例如,如果我们有一个函数需要更新学生的成绩,使用值传递的方式就不能直接修改传入的学生结构体变量的成绩。

结构体值传递与内存管理

在使用结构体作为函数参数的值传递时,需要注意内存管理方面的问题。特别是当结构体中包含动态分配的内存时,更要谨慎处理。

结构体包含动态分配内存的情况

假设我们修改 struct Student 结构体,使其 name 成员使用动态分配的内存:

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

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

void printStudent(struct Student student) {
    printf("姓名: %s\n", student.name);
    printf("年龄: %d\n", student.age);
    printf("成绩: %.2f\n", student.grade);
}

int main() {
    struct Student john;
    john.name = (char *)malloc(50 * sizeof(char));
    strcpy(john.name, "John");
    john.age = 20;
    john.grade = 85.5;
    printStudent(john);
    free(john.name);
    return 0;
}

在上述代码中,struct Studentname 成员是一个指向字符的指针,我们在 main 函数中使用 malloc 动态分配了 50 个字节的内存来存储学生姓名,并通过 strcpy 函数将字符串 "John" 复制到这块内存中。在 printStudent 函数调用后,我们在 main 函数中使用 free 释放了 john.name 所指向的内存。

这里需要注意的是,在值传递过程中,虽然结构体的 name 指针会被复制到函数的形式参数中,但两个指针指向的是同一块动态分配的内存。如果在函数内部对 name 指针进行了释放操作,就会导致在 main 函数中再次释放时出现内存错误(双重释放)。

避免内存错误的方法

为了避免在结构体值传递过程中出现内存管理错误,有以下几种方法:

  1. 不在函数内部释放动态分配的内存:这是最直接的方法,即在函数内部只读取结构体中动态分配内存的数据,而不进行释放操作。例如,在 printStudent 函数中,只进行打印操作,不释放 student.name 所指向的内存。释放操作仍然在调用函数的地方进行,如上述 main 函数中的 free(john.name)
  2. 在函数内部复制动态分配的内存:如果需要在函数内部对动态分配的内存进行独立处理,可以在函数内部重新分配内存并复制数据。例如:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

void printStudent(struct Student student) {
    char *newName = (char *)malloc(strlen(student.name) + 1);
    strcpy(newName, student.name);
    printf("姓名: %s\n", newName);
    printf("年龄: %d\n", student.age);
    printf("成绩: %.2f\n", student.grade);
    free(newName);
}

int main() {
    struct Student john;
    john.name = (char *)malloc(50 * sizeof(char));
    strcpy(john.name, "John");
    john.age = 20;
    john.grade = 85.5;
    printStudent(john);
    free(john.name);
    return 0;
}

在这个改进的 printStudent 函数中,我们为 newName 重新分配了内存,并将 student.name 的内容复制过去。这样,函数内部对 newName 的操作就不会影响到 main 函数中的 john.name。在函数结束前,释放 newName 所指向的内存。

结构体嵌套时的值传递

结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。当结构体嵌套时,值传递的过程会稍微复杂一些,但本质上仍然是将所有成员的值进行复制。

结构体嵌套示例

例如,我们定义一个表示地址的结构体 struct Address,并将其作为 struct Student 的一个成员:

#include <stdio.h>

struct Address {
    char street[100];
    char city[50];
    char zipCode[10];
};

struct Student {
    char name[50];
    int age;
    float grade;
    struct Address address;
};

void printStudent(struct Student student) {
    printf("姓名: %s\n", student.name);
    printf("年龄: %d\n", student.age);
    printf("成绩: %.2f\n", student.grade);
    printf("地址: %s, %s, %s\n", student.address.street, student.address.city, student.address.zipCode);
}

int main() {
    struct Student john = {"John", 20, 85.5, {"123 Main St", "Anytown", "12345"}};
    printStudent(john);
    return 0;
}

在上述代码中,struct Student 结构体包含了一个 struct Address 类型的成员 address。在 printStudent 函数中,我们通过嵌套的成员访问运算符来访问和打印 address 结构体的各个成员。

嵌套结构体值传递的本质

当进行值传递时,系统会按照结构体的定义顺序,依次复制每个成员的值。对于嵌套的结构体成员,会递归地复制其所有成员的值。例如,在上述 printStudent(john) 的调用中,首先会复制 john 结构体的 nameagegrade 成员的值,然后会复制 address 结构体的 streetcityzipCode 成员的值。整个过程就像是将 john 结构体的所有数据按顺序“平铺”到函数的形式参数所对应的内存空间中。

结构体数组作为函数参数的值传递

结构体数组是一种包含多个结构体元素的数组。当将结构体数组作为函数参数传递时,同样可以采用值传递的方式。

结构体数组值传递示例

假设我们有一个班级,包含多个学生,我们可以使用结构体数组来表示:

#include <stdio.h>

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

void printClass(struct Student students[], int size) {
    for (int i = 0; i < size; i++) {
        printf("学生 %d:\n", i + 1);
        printf("姓名: %s\n", students[i].name);
        printf("年龄: %d\n", students[i].age);
        printf("成绩: %.2f\n", students[i].grade);
    }
}

int main() {
    struct Student class[] = {
        {"Alice", 21, 90.0},
        {"Bob", 20, 85.5},
        {"Charlie", 19, 88.0}
    };
    int size = sizeof(class) / sizeof(class[0]);
    printClass(class, size);
    return 0;
}

在上述代码中,printClass 函数接收一个 struct Student 类型的数组 students 和数组的大小 size。在函数内部,通过循环遍历数组,打印每个学生的信息。在 main 函数中,我们创建了一个包含三个学生信息的结构体数组 class,并将其传递给 printClass 函数。

结构体数组值传递的本质

从本质上讲,当将结构体数组作为函数参数值传递时,实际上是将数组的首地址传递给函数,这与普通数组作为函数参数传递的原理相同。但是,在函数内部,通过这个首地址可以访问到数组中的每个结构体元素,并且对每个结构体元素的访问和操作就如同对单个结构体变量进行值传递一样,会复制结构体元素的所有成员值。

例如,在 printClass 函数中,students[i] 实际上是通过首地址和偏移量计算得到的第 i 个结构体元素的位置,当访问 students[i].name 等成员时,会从该位置开始复制结构体元素的相应成员值。

优化结构体值传递的性能

由于结构体值传递可能会带来较大的性能开销,特别是当结构体较大时,我们可以采取一些措施来优化性能。

使用指针传递代替值传递

如前文所述,指针传递只传递结构体的地址,而不是整个结构体的副本,因此可以显著减少内存开销和复制时间。例如,我们将之前的 printStudent 函数修改为使用指针传递:

#include <stdio.h>

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

void printStudent(struct Student *student) {
    printf("姓名: %s\n", student->name);
    printf("年龄: %d\n", student->age);
    printf("成绩: %.2f\n", student->grade);
}

int main() {
    struct Student john = {"John", 20, 85.5};
    printStudent(&john);
    return 0;
}

在这个修改后的代码中,printStudent 函数接收一个指向 struct Student 结构体的指针 student。通过指针访问结构体成员时,使用 -> 运算符。在 main 函数中,我们通过 &johnjohn 结构体的地址传递给函数。这样,函数内部直接操作的是 john 结构体,而不是其副本,从而避免了结构体的复制开销。

使用 const 修饰指针参数

当使用指针传递结构体时,为了保证函数内部不会意外修改结构体的内容,可以使用 const 关键字修饰指针参数。例如:

#include <stdio.h>

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

void printStudent(const struct Student *student) {
    // 以下语句会导致编译错误,因为 student 是 const 指针
    // student->age = 21; 
    printf("姓名: %s\n", student->name);
    printf("年龄: %d\n", student->age);
    printf("成绩: %.2f\n", student->grade);
}

int main() {
    struct Student john = {"John", 20, 85.5};
    printStudent(&john);
    return 0;
}

在上述代码中,printStudent 函数的参数 student 被声明为 const struct Student *,这意味着函数内部不能通过 student 指针修改 struct Student 结构体的成员值。如果在函数内部尝试修改,如 student->age = 21;,会导致编译错误。这样可以在保证性能的同时,提高程序的安全性。

总结结构体作为函数参数的值传递要点

  1. 基本原理:结构体作为函数参数的值传递,本质上是将结构体变量的所有成员值复制一份传递给函数的形式参数。这与普通变量的值传递类似,只不过结构体是一个复合数据类型,包含多个成员。
  2. 优缺点:优点包括代码简洁直观和安全性较高;缺点主要是性能开销较大,特别是当结构体较大时,并且无法直接修改实际参数。
  3. 内存管理:当结构体中包含动态分配的内存时,要注意避免在函数内部和外部重复释放内存,可通过不在函数内部释放或在函数内部复制动态分配内存等方法来解决。
  4. 结构体嵌套与数组:结构体嵌套时,值传递会递归地复制嵌套结构体的所有成员值;结构体数组作为函数参数值传递时,实际传递的是数组首地址,函数内部对数组元素的操作如同对单个结构体变量进行值传递。
  5. 性能优化:为了优化结构体值传递的性能,可以使用指针传递代替值传递,并使用 const 修饰指针参数来保证安全性。

通过深入理解结构体作为函数参数的值传递,我们可以更加灵活和高效地使用结构体,编写出更健壮、性能更好的 C 语言程序。在实际编程中,应根据具体的需求和场景,选择合适的参数传递方式,以达到最佳的编程效果。