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

C 语言函数的参数传递

2021-10-221.7k 阅读

C 语言函数参数传递基础

在 C 语言中,函数是模块化编程的核心。函数允许我们将一个复杂的任务分解为多个较小的、可管理的部分。当我们调用一个函数时,常常需要向它传递一些数据,这些数据就是函数的参数。函数参数传递是 C 语言编程中非常重要的一个概念,理解它对于编写高效、正确的代码至关重要。

函数参数的定义

在定义函数时,我们在函数名后面的括号内指定参数。例如:

int add(int a, int b) {
    return a + b;
}

在这个例子中,add 函数有两个参数 ab,它们的类型都是 int。这些参数就像是函数内部的局部变量,在函数被调用时会被初始化。

函数调用时的参数传递

当我们调用一个函数时,会为函数的参数分配内存,并将实际的值传递给这些参数。例如:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int result;
    result = add(3, 5);
    printf("The result of 3 + 5 is %d\n", result);
    return 0;
}

main 函数中,我们调用 add 函数,并传递了两个值 35。这两个值被分别赋给了 add 函数的参数 ab

值传递

值传递的原理

C 语言中默认的参数传递方式是值传递。在值传递中,函数接收的是实参值的一个副本。也就是说,函数内部对参数的任何修改都不会影响到函数外部的实参。

考虑下面的代码:

#include <stdio.h>

void changeValue(int num) {
    num = num + 10;
    printf("Inside changeValue, num is %d\n", num);
}

int main() {
    int value = 20;
    printf("Before calling changeValue, value is %d\n", value);
    changeValue(value);
    printf("After calling changeValue, value is %d\n", value);
    return 0;
}

在这个例子中,changeValue 函数接收 value 的一个副本 num。当我们在 changeValue 函数内部修改 num 时,并不会影响到 main 函数中的 value。输出结果为:

Before calling changeValue, value is 20
Inside changeValue, num is 30
After calling changeValue, value is 20

值传递的优点

  1. 简单易懂:值传递的概念很直观,函数接收到的就是实参的值,就像局部变量的初始化一样。这使得代码的理解和调试相对容易。
  2. 安全性:由于函数操作的是实参的副本,不会直接修改实参的值,这在一定程度上保护了调用者的数据。例如,在一个复杂的程序中,我们不希望某个函数意外地修改了重要的全局变量或其他函数中的关键数据。

值传递的缺点

  1. 资源消耗:当传递大型数据结构(如结构体或数组)时,值传递需要复制整个数据结构,这会消耗大量的内存和时间。例如,如果我们有一个非常大的结构体,每次函数调用都要复制它,会降低程序的性能。
  2. 无法修改实参:在某些情况下,我们希望函数能够直接修改调用者提供的实参值。如果使用值传递,就无法实现这一点,需要采用其他的参数传递方式。

指针传递

指针传递的原理

指针传递是一种通过传递变量的地址来实现参数传递的方式。通过传递指针,函数可以直接访问和修改调用者提供的变量。

下面是一个使用指针传递的例子:

#include <stdio.h>

void changeValue(int *ptr) {
    *ptr = *ptr + 10;
    printf("Inside changeValue, value is %d\n", *ptr);
}

int main() {
    int value = 20;
    printf("Before calling changeValue, value is %d\n", value);
    changeValue(&value);
    printf("After calling changeValue, value is %d\n", value);
    return 0;
}

在这个例子中,changeValue 函数的参数是一个 int 类型的指针 ptr。在 main 函数中,我们通过 & 运算符获取 value 的地址,并将其传递给 changeValue 函数。在 changeValue 函数内部,我们通过 *ptr 来访问和修改 value 的值。输出结果为:

Before calling changeValue, value is 20
Inside changeValue, value is 30
After calling changeValue, value is 30

指针传递的优点

  1. 高效性:当传递大型数据结构时,指针传递只需要传递一个指针(通常是 4 字节或 8 字节,取决于系统架构),而不是整个数据结构的副本。这大大减少了内存和时间的消耗。例如,对于一个包含大量成员的结构体,如果使用值传递,每次调用函数都要复制整个结构体,而使用指针传递只需要传递结构体的地址。
  2. 可修改实参:指针传递允许函数直接修改调用者提供的变量的值。这在很多场景下非常有用,比如在排序函数中,我们希望函数能够直接修改数组中的元素顺序。

指针传递的缺点

  1. 复杂性:指针的概念相对复杂,尤其是对于初学者来说。使用指针时容易出现错误,如空指针引用、野指针等问题。这些错误可能会导致程序崩溃或产生未定义行为。
  2. 安全性:由于函数可以直接修改实参的值,如果使用不当,可能会意外地修改调用者不希望修改的数据。这就需要程序员在编写代码时更加小心,确保函数的正确性和安全性。

数组作为函数参数

数组名作为参数

在 C 语言中,当我们将数组名作为函数参数传递时,实际上传递的是数组的首地址,这类似于指针传递。

例如:

#include <stdio.h>

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    printArray(numbers, size);
    return 0;
}

在这个例子中,printArray 函数的第一个参数 arr 实际上是一个指向 int 类型的指针,它指向数组 numbers 的首元素。虽然我们在函数定义中写成 int arr[],但编译器会将其视为 int *arr

多维数组作为参数

多维数组作为函数参数传递时,也遵循类似的规则。例如,对于二维数组:

#include <stdio.h>

void print2DArray(int arr[][3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
    print2DArray(matrix, 2);
    return 0;
}

print2DArray 函数中,arr 是一个指向包含 3 个 int 类型元素的数组的指针。我们必须指定第二维的大小,因为编译器需要知道如何计算数组元素的地址。

数组参数的优缺点

  1. 优点
    • 高效性:与指针传递类似,传递数组名实际上传递的是地址,避免了复制整个数组的开销。这对于大型数组非常重要,可以显著提高程序的性能。
    • 灵活性:函数可以方便地对数组进行操作,如遍历、排序等。通过传递数组名和数组大小,函数可以处理不同大小的数组,提高了代码的复用性。
  2. 缺点
    • 边界检查问题:在 C 语言中,函数无法直接获取传递进来的数组的实际大小,需要额外传递数组大小参数。如果在调用函数时不小心传递了错误的大小参数,可能会导致数组越界访问,引发未定义行为。
    • 数组退化:当数组作为函数参数传递时,数组会退化为指针。这意味着我们无法在函数内部使用 sizeof 运算符获取数组的实际大小,因为 sizeof 作用于指针时,返回的是指针本身的大小(通常是 4 字节或 8 字节)。

结构体作为函数参数

结构体值传递

我们可以像传递基本数据类型一样,以值传递的方式将结构体传递给函数。例如:

#include <stdio.h>

struct Point {
    int x;
    int y;
};

void printPoint(struct Point p) {
    printf("Point: (%d, %d)\n", p.x, p.y);
}

int main() {
    struct Point myPoint = {10, 20};
    printPoint(myPoint);
    return 0;
}

在这个例子中,printPoint 函数接收一个 struct Point 类型的结构体变量 p。函数内部对 p 的操作不会影响到 main 函数中的 myPoint

结构体指针传递

为了避免结构体值传递时的复制开销,并且能够在函数内部修改结构体的成员,我们可以传递结构体指针。例如:

#include <stdio.h>

struct Point {
    int x;
    int y;
};

void movePoint(struct Point *p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

int main() {
    struct Point myPoint = {10, 20};
    printf("Before moving, Point: (%d, %d)\n", myPoint.x, myPoint.y);
    movePoint(&myPoint, 5, 10);
    printf("After moving, Point: (%d, %d)\n", myPoint.x, myPoint.y);
    return 0;
}

movePoint 函数中,我们通过结构体指针 p 来访问和修改 myPoint 的成员。这样既提高了效率,又实现了对结构体成员的修改。

结构体参数传递的选择

  1. 值传递:当结构体较小且不需要在函数内部修改结构体内容时,值传递是一个简单的选择。它的优点是代码简单易懂,函数内部对参数的操作不会影响到外部的结构体。
  2. 指针传递:当结构体较大或者需要在函数内部修改结构体内容时,指针传递更为合适。它可以减少内存复制开销,并且提供了修改结构体成员的能力。但使用指针传递需要注意指针的正确性,避免空指针引用等问题。

函数指针作为参数

函数指针的定义和使用

函数指针是指向函数的指针变量。我们可以将函数指针作为参数传递给其他函数,这样可以实现更加灵活的编程。

例如,我们有一个计算两个数之和的函数和一个计算两个数之积的函数,然后通过一个通用的计算函数,根据传入的函数指针来决定执行哪种计算:

#include <stdio.h>

// 加法函数
int add(int a, int b) {
    return a + b;
}

// 乘法函数
int multiply(int a, int b) {
    return a * b;
}

// 通用计算函数,接收函数指针作为参数
int calculate(int (*func)(int, int), int a, int b) {
    return func(a, b);
}

int main() {
    int result1 = calculate(add, 3, 5);
    int result2 = calculate(multiply, 3, 5);
    printf("3 + 5 = %d\n", result1);
    printf("3 * 5 = %d\n", result2);
    return 0;
}

在这个例子中,calculate 函数的第一个参数 func 是一个函数指针,它指向一个接收两个 int 类型参数并返回 int 类型结果的函数。我们可以将 addmultiply 函数的地址传递给 calculate 函数,从而实现不同的计算逻辑。

函数指针参数的应用场景

  1. 回调函数:在很多库函数中,经常使用函数指针作为参数来实现回调机制。例如,在 qsort 函数中,我们需要传递一个比较函数的指针,qsort 函数会根据这个比较函数来对数组进行排序。这样可以根据不同的需求定义不同的比较逻辑,提高了库函数的通用性。
  2. 状态机:在实现状态机时,函数指针可以用来表示不同状态下的处理逻辑。通过将不同的函数指针传递给状态机的执行函数,可以根据当前状态执行相应的处理逻辑。

可变参数函数

可变参数函数的定义

C 语言允许我们定义接受可变数量参数的函数。可变参数函数的定义需要使用 <stdarg.h> 头文件中的宏。

例如,下面是一个简单的可变参数函数,用于计算传入参数的总和:

#include <stdio.h>
#include <stdarg.h>

int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

int main() {
    int result = sum(3, 1, 2, 3);
    printf("The sum is %d\n", result);
    return 0;
}

sum 函数中,... 表示可变参数部分。va_list 是一个类型,用于存储可变参数的信息。va_start 宏用于初始化 va_listva_arg 宏用于获取下一个可变参数的值,va_end 宏用于清理 va_list

可变参数函数的注意事项

  1. 固定参数:可变参数函数通常需要至少一个固定参数,用于指定可变参数的数量或提供其他必要的信息。在上面的 sum 函数中,count 参数用于指定后续可变参数的数量。
  2. 类型安全:由于可变参数函数在编译时无法确定参数的类型和数量,所以需要程序员在调用函数时确保参数的正确性。如果传递了错误的参数类型或数量,可能会导致未定义行为。

不同参数传递方式的性能分析

值传递的性能

值传递在传递小型数据类型(如 intchar 等)时,性能开销较小,因为复制这些数据的时间和空间成本都很低。然而,当传递大型数据结构(如大型结构体或数组)时,由于需要复制整个数据结构,会消耗大量的内存和时间,从而显著降低程序的性能。

指针传递的性能

指针传递在传递大型数据结构时具有明显的性能优势。因为只需要传递一个指针(通常是 4 字节或 8 字节),而不是整个数据结构的副本,大大减少了内存和时间的消耗。但指针传递也有一些额外的开销,如解引用指针的操作,不过这种开销相对复制大型数据结构来说通常较小。

数组传递的性能

数组传递实际上是通过传递数组首地址(类似于指针传递),因此在性能上与指针传递类似。对于大型数组,这种方式避免了复制整个数组的开销,提高了效率。但需要注意的是,在函数内部访问数组元素时,可能会因为缓存未命中等问题影响性能,尤其是对于多维数组。

结构体传递的性能

结构体值传递的性能取决于结构体的大小。如果结构体较小,值传递的性能开销可以接受;但对于大型结构体,值传递会带来较大的性能损失。结构体指针传递则可以避免这种情况,通过传递结构体的地址,提高了效率,特别是在需要在函数内部修改结构体成员的场景下。

实际编程中的参数传递选择

根据需求选择传递方式

  1. 如果不需要修改实参:当函数不需要修改调用者提供的实参值,并且传递的数据量较小(如基本数据类型)时,值传递是一个简单且安全的选择。例如,一个用于计算两个整数之和的函数,使用值传递就很合适。
  2. 如果需要修改实参:当函数需要直接修改调用者提供的实参值时,指针传递或数组传递(对于数组类型)是必须的。比如,一个用于交换两个整数的函数,就需要通过指针传递来实现对实参的修改。
  3. 大型数据结构:对于大型数据结构(如大型结构体或数组),为了避免值传递带来的性能开销,应优先考虑指针传递或数组传递(对于数组)。这样可以显著提高程序的运行效率。

代码可读性和维护性

在选择参数传递方式时,也要考虑代码的可读性和维护性。值传递通常更直观,容易理解,对于简单的函数和小型数据类型,使用值传递可以使代码更加清晰。而指针传递虽然高效,但由于指针概念相对复杂,对于初学者或不太熟悉指针操作的程序员来说,可能会增加代码的理解和维护难度。因此,在保证性能的前提下,应尽量选择使代码更易读、易维护的参数传递方式。

在实际编程中,我们需要综合考虑性能、功能需求以及代码的可读性和维护性等因素,来选择最合适的函数参数传递方式。通过合理地运用不同的参数传递方式,可以编写出高效、健壮且易于理解的 C 语言程序。