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

C 语言指针作为函数参数

2021-01-096.8k 阅读

C 语言指针作为函数参数的基础概念

在 C 语言中,函数是模块化编程的核心,而指针作为一种强大的数据类型,当它作为函数参数时,能为函数带来独特的功能和灵活性。

指针的基本概念回顾

指针是一个变量,其值为另一个变量的地址。例如,假设有一个整型变量 int num = 10;,可以通过定义一个指针变量来存储 num 的地址:int *ptr = #。这里 ptr 就是一个指向 int 类型变量 num 的指针。指针变量的声明中,* 用于表明该变量是一个指针,而取地址符 & 用于获取变量的地址。

函数参数传递的常规方式

在 C 语言中,函数参数传递默认是值传递。这意味着当调用函数时,会为函数的形参分配新的内存空间,并将实参的值复制到形参中。例如:

#include <stdio.h>

void increment(int num) {
    num = num + 1;
}

int main() {
    int value = 5;
    increment(value);
    printf("The value is: %d\n", value);
    return 0;
}

在上述代码中,increment 函数的形参 num 是实参 value 的一个副本。在 increment 函数内部对 num 的修改不会影响到 main 函数中的 value。程序输出结果为 The value is: 5

指针作为函数参数的引入

当我们希望函数能够修改调用者传递进来的变量的值时,值传递就无法满足需求了。这时候就可以使用指针作为函数参数。通过传递变量的地址(即指针),函数可以直接访问和修改调用者提供的变量。例如:

#include <stdio.h>

void increment(int *ptr) {
    (*ptr) = (*ptr) + 1;
}

int main() {
    int value = 5;
    increment(&value);
    printf("The value is: %d\n", value);
    return 0;
}

在这段代码中,increment 函数的参数是一个 int 类型的指针 ptr。在 main 函数中,我们将 value 的地址传递给 increment 函数。在 increment 函数内部,通过解引用指针 *ptr 来访问并修改 main 函数中的 value 变量。程序输出结果为 The value is: 6

指针作为函数参数的不同应用场景

对单个变量的操作

  1. 修改变量值 如前面提到的 increment 函数示例,通过指针作为参数,函数可以直接修改调用者提供的变量的值。这种方式在很多场景下都非常有用,比如实现一个交换两个数的函数。
#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int num1 = 10;
    int num2 = 20;
    printf("Before swap: num1 = %d, num2 = %d\n", num1, num2);
    swap(&num1, &num2);
    printf("After swap: num1 = %d, num2 = %d\n", num1, num2);
    return 0;
}

swap 函数中,通过解引用指针 *a*b 来交换 num1num2 的值。程序输出结果为:

Before swap: num1 = 10, num2 = 20
After swap: num1 = 20, num2 = 10
  1. 获取变量状态 有时候函数不仅需要返回一个结果,还需要返回一些关于输入变量的状态信息。例如,实现一个函数来计算整数的平方根,并返回计算是否成功的状态。
#include <stdio.h>
#include <math.h>

int calculateSqrt(int num, double *result) {
    if (num < 0) {
        return 0; // 表示失败
    }
    *result = sqrt(num);
    return 1; // 表示成功
}

int main() {
    int number = 25;
    double squareRoot;
    int status = calculateSqrt(number, &squareRoot);
    if (status) {
        printf("The square root of %d is %lf\n", number, squareRoot);
    } else {
        printf("Cannot calculate square root of negative number.\n");
    }
    return 0;
}

calculateSqrt 函数中,通过指针 result 返回平方根的值,通过返回值表示计算是否成功。如果 number 为 25,程序输出 The square root of 25 is 5.000000

对数组的操作

  1. 传递一维数组 在 C 语言中,当把数组作为函数参数传递时,实际上传递的是数组首元素的地址,也就是一个指针。例如,实现一个计算数组元素之和的函数。
#include <stdio.h>

int sumArray(int *arr, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int array[] = {1, 2, 3, 4, 5};
    int size = sizeof(array) / sizeof(array[0]);
    int total = sumArray(array, size);
    printf("The sum of array elements is: %d\n", total);
    return 0;
}

sumArray 函数中,形参 arr 实际上是一个指向 int 类型的指针,它指向数组的首元素。我们可以通过指针 arr 来访问数组的各个元素。程序输出 The sum of array elements is: 15。 2. 传递二维数组 传递二维数组时,同样传递的是首元素的地址。但是二维数组的指针表示相对复杂一些。例如,假设有一个二维数组 int matrix[3][4],其首元素地址就是 &matrix[0][0]。下面是一个计算二维数组所有元素之和的函数。

#include <stdio.h>

int sumMatrix(int (*arr)[4], int rows) {
    int sum = 0;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            sum += arr[i][j];
        }
    }
    return sum;
}

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int rows = sizeof(matrix) / sizeof(matrix[0]);
    int total = sumMatrix(matrix, rows);
    printf("The sum of matrix elements is: %d\n", total);
    return 0;
}

sumMatrix 函数中,形参 arr 是一个指向含有 4 个 int 类型元素的数组的指针。这是因为二维数组在内存中是按行存储的,arr 指向每一行的首地址。程序输出 The sum of matrix elements is: 78

对字符串的操作

  1. 字符串处理函数 C 语言中的字符串实际上是以 '\0' 结尾的字符数组。当把字符串作为函数参数传递时,传递的是字符串首字符的地址,即一个 char 类型的指针。例如,实现一个计算字符串长度的函数。
#include <stdio.h>

int stringLength(const char *str) {
    int length = 0;
    while (*str != '\0') {
        length++;
        str++;
    }
    return length;
}

int main() {
    const char *message = "Hello, World!";
    int len = stringLength(message);
    printf("The length of the string is: %d\n", len);
    return 0;
}

stringLength 函数中,通过指针 str 遍历字符串,直到遇到 '\0' 字符,从而计算出字符串的长度。程序输出 The length of the string is: 13。 2. 字符串修改函数 也可以通过指针作为函数参数来修改字符串。例如,实现一个将字符串中的小写字母转换为大写字母的函数。

#include <stdio.h>
#include <ctype.h>

void convertToUpper(char *str) {
    while (*str != '\0') {
        if (islower(*str)) {
            *str = toupper(*str);
        }
        str++;
    }
}

int main() {
    char message[] = "Hello, World!";
    printf("Before conversion: %s\n", message);
    convertToUpper(message);
    printf("After conversion: %s\n", message);
    return 0;
}

convertToUpper 函数中,通过指针 str 遍历字符串,使用 islowertoupper 函数来判断并转换小写字母为大写字母。程序输出:

Before conversion: Hello, World!
After conversion: HELLO, WORLD!

指针作为函数参数的优势

提高效率

  1. 减少数据复制开销 当传递大型数据结构(如数组或结构体)时,如果使用值传递,会为形参分配大量的内存空间来复制实参的数据,这会消耗大量的时间和内存。而使用指针作为函数参数,只传递数据的地址,大大减少了数据复制的开销。例如,假设有一个包含大量元素的数组,如果使用值传递,每次函数调用都要复制整个数组,而使用指针传递只需要传递一个地址(通常在 32 位系统中为 4 字节,64 位系统中为 8 字节)。
  2. 直接访问内存 指针可以直接访问内存中的数据,避免了多次数据传递和复制的中间过程。这使得函数对数据的操作更加直接和高效。比如在对数组进行遍历和修改时,通过指针直接访问数组元素,比通过数组下标访问在某些情况下效率更高,因为编译器在处理指针运算时可以生成更优化的机器码。

增强函数功能

  1. 实现双向数据传递 普通的函数参数传递是单向的,即从调用者到被调用函数。而通过指针作为函数参数,函数可以修改调用者传递进来的变量的值,实现双向数据传递。例如前面提到的 swap 函数和 calculateSqrt 函数,不仅可以返回计算结果,还可以修改调用者提供的变量。
  2. 灵活处理不同数据类型 指针的灵活性使得同一个函数可以处理不同数据类型的数据,只要这些数据类型的内存布局和操作方式兼容。例如,一个通用的排序函数可以通过传递不同类型数组的指针来对不同类型的数组进行排序。下面是一个简单的通用交换函数示例,可以用于交换 intfloat 等类型的数据。
#include <stdio.h>

void swapGeneric(void *a, void *b, size_t size) {
    char temp[size];
    char *ptrA = (char *)a;
    char *ptrB = (char *)b;
    for (size_t i = 0; i < size; i++) {
        temp[i] = ptrA[i];
        ptrA[i] = ptrB[i];
        ptrB[i] = temp[i];
    }
}

int main() {
    int num1 = 10;
    int num2 = 20;
    printf("Before swap: num1 = %d, num2 = %d\n", num1, num2);
    swapGeneric(&num1, &num2, sizeof(int));
    printf("After swap: num1 = %d, num2 = %d\n", num1, num2);

    float f1 = 1.5f;
    float f2 = 2.5f;
    printf("Before swap: f1 = %f, f2 = %f\n", f1, f2);
    swapGeneric(&f1, &f2, sizeof(float));
    printf("After swap: f1 = %f, f2 = %f\n", f1, f2);

    return 0;
}

swapGeneric 函数中,使用 void * 指针来接受不同类型的数据指针,通过 size_t 类型的参数 size 来确定数据的大小,从而实现对不同类型数据的交换。

指针作为函数参数的注意事项

指针的有效性检查

  1. 空指针检查 在函数内部使用指针之前,必须检查指针是否为空。如果传递了一个空指针给函数,并且函数在没有检查的情况下对空指针进行解引用操作,会导致程序崩溃。例如:
#include <stdio.h>

void printValue(int *ptr) {
    if (ptr == NULL) {
        printf("Error: NULL pointer passed.\n");
        return;
    }
    printf("The value is: %d\n", *ptr);
}

int main() {
    int num = 10;
    int *validPtr = &num;
    int *nullPtr = NULL;

    printValue(validPtr);
    printValue(nullPtr);

    return 0;
}

printValue 函数中,首先检查 ptr 是否为空指针。如果是,输出错误信息并返回。程序输出:

The value is: 10
Error: NULL pointer passed.
  1. 野指针避免 野指针是指向一个未分配内存或已释放内存的指针。避免产生野指针的关键在于正确地初始化指针和在内存释放后将指针设置为 NULL。例如,在动态内存分配中,如果分配内存失败,指针可能成为野指针。
#include <stdio.h>
#include <stdlib.h>

void allocateMemory(int **ptr) {
    *ptr = (int *)malloc(sizeof(int));
    if (*ptr == NULL) {
        // 处理内存分配失败
        printf("Memory allocation failed.\n");
        return;
    }
    **ptr = 10;
}

int main() {
    int *p = NULL;
    allocateMemory(&p);
    if (p != NULL) {
        printf("The value is: %d\n", *p);
        free(p);
        p = NULL;
    }
    return 0;
}

allocateMemory 函数中,检查内存分配是否成功。在 main 函数中,释放内存后将指针 p 设置为 NULL,避免 p 成为野指针。

指针类型匹配

  1. 函数声明与调用的一致性 函数声明中的指针类型必须与实际调用时传递的指针类型一致。否则,可能会导致未定义行为。例如,如果函数声明为接受 int * 类型的指针,但调用时传递了 char * 类型的指针,编译器可能不会报错,但运行时会出现错误。
#include <stdio.h>

void increment(int *ptr) {
    (*ptr)++;
}

int main() {
    int num = 5;
    char ch = 'a';

    increment(&num);
    // increment(&ch); // 这会导致未定义行为,因为类型不匹配
    printf("The incremented number is: %d\n", num);
    return 0;
}

在上述代码中,increment 函数期望接受 int * 类型的指针。如果取消注释 increment(&ch); 这一行,程序会出现未定义行为。 2. 指针运算的类型兼容性 当在函数内部对指针进行运算时,要确保指针运算的结果与指针类型兼容。例如,在处理数组指针时,指针的移动量要与数组元素的大小相匹配。如果有一个 int 类型的数组指针,每次指针移动应该是 sizeof(int) 个字节。

#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 array[] = {1, 2, 3, 4, 5};
    int size = sizeof(array) / sizeof(array[0]);
    printArray(array, size);
    return 0;
}

printArray 函数中,arr + i 表示移动到数组的第 i 个元素的位置,由于 arrint * 类型,arr + i 实际移动了 i * sizeof(int) 个字节,这与 int 数组元素的存储方式相匹配。

内存管理

  1. 动态内存分配与释放 如果在函数内部通过指针进行动态内存分配,要确保在适当的时候释放内存,以避免内存泄漏。例如,在一个函数中分配内存并返回指针给调用者,调用者负责释放内存。
#include <stdio.h>
#include <stdlib.h>

char *allocateString(const char *str) {
    int length = 0;
    while (str[length] != '\0') {
        length++;
    }
    char *newStr = (char *)malloc((length + 1) * sizeof(char));
    if (newStr == NULL) {
        printf("Memory allocation failed.\n");
        return NULL;
    }
    for (int i = 0; i <= length; i++) {
        newStr[i] = str[i];
    }
    return newStr;
}

int main() {
    const char *original = "Hello";
    char *copied = allocateString(original);
    if (copied != NULL) {
        printf("Copied string: %s\n", copied);
        free(copied);
    }
    return 0;
}

allocateString 函数中,分配内存并复制字符串。在 main 函数中,获取返回的指针并在使用后释放内存。 2. 避免重复释放 重复释放内存会导致未定义行为。要确保在释放内存后将指针设置为 NULL,并且在释放指针之前检查指针是否已经为 NULL。例如:

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr != NULL) {
        free(ptr);
        ptr = NULL;
        // 再次释放会导致错误,以下代码避免重复释放
        if (ptr != NULL) {
            free(ptr);
        }
    }
    return 0;
}

在上述代码中,释放内存后将指针 ptr 设置为 NULL,并且在再次尝试释放前检查 ptr 是否为 NULL,以避免重复释放。

通过深入理解指针作为函数参数的概念、应用场景、优势以及注意事项,开发者可以更加高效和安全地使用 C 语言进行编程,充分发挥指针的强大功能。