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

C语言指针在函数参数传递中的妙用

2021-02-242.5k 阅读

C语言指针在函数参数传递中的妙用

指针基础回顾

在深入探讨指针在函数参数传递中的妙用之前,我们先来回顾一下C语言指针的基础知识。指针是一种特殊的变量,它存储的是内存地址。例如,我们定义一个整型变量 int num = 10;,如果我们想获取 num 的地址,可以使用取地址运算符 &,即 &num 就是 num 在内存中的地址。

#include <stdio.h>
int main() {
    int num = 10;
    printf("The address of num is: %p\n", &num);
    return 0;
}

在上述代码中,%p 是用于输出地址的格式说明符。运行这段代码,你会看到 num 在内存中的具体地址。

指针变量的声明需要指定它所指向的数据类型。例如,要声明一个指向整型的指针,可以这样写:int *ptr;。这里的 * 表示 ptr 是一个指针变量,它指向 int 类型的数据。

值传递方式及其局限

在C语言中,函数参数传递最常见的方式是值传递。当我们以值传递的方式将参数传递给函数时,函数会在其栈空间中创建参数的副本。例如:

#include <stdio.h>
void increment(int num) {
    num = num + 1;
}
int main() {
    int value = 5;
    increment(value);
    printf("The value after increment is: %d\n", value);
    return 0;
}

在上述代码中,increment 函数接收一个 int 类型的参数 num。在函数内部,numvalue 的副本。对 num 的修改不会影响到 main 函数中的 value。运行结果会输出 The value after increment is: 5,而不是我们期望的 6。这就是值传递的局限性,它无法直接修改调用函数中的原始变量。

指针传递的优势

  1. 直接修改原始变量 通过传递指针,我们可以在函数内部直接修改调用函数中的原始变量。因为指针传递的是变量的地址,函数内部通过地址可以访问和修改原始数据。例如:
#include <stdio.h>
void increment(int *ptr) {
    (*ptr) = (*ptr) + 1;
}
int main() {
    int value = 5;
    increment(&value);
    printf("The value after increment is: %d\n", value);
    return 0;
}

在这个例子中,increment 函数接收一个指向 int 类型的指针 ptr&valuevalue 的地址传递给函数。在函数内部,*ptr 表示指针所指向的变量,即 value。通过 (*ptr) = (*ptr) + 1; 语句,我们成功地修改了 main 函数中的 value。运行这段代码,输出将是 The value after increment is: 6

  1. 节省内存开销 当传递大型数据结构(如结构体)时,值传递会复制整个数据结构,这会消耗大量的内存和时间。而传递指针只需要复制一个地址值(通常在32位系统上是4字节,在64位系统上是8字节)。例如,假设有一个大型结构体:
#include <stdio.h>
#include <string.h>
struct BigStruct {
    char data[1000];
    int num;
};
void modifyStruct(struct BigStruct *ptr) {
    strcpy(ptr->data, "New data");
    ptr->num = 100;
}
int main() {
    struct BigStruct myStruct;
    strcpy(myStruct.data, "Old data");
    myStruct.num = 50;
    modifyStruct(&myStruct);
    printf("Data: %s, Num: %d\n", myStruct.data, myStruct.num);
    return 0;
}

在上述代码中,struct BigStruct 是一个包含1000字节字符数组和一个 int 类型变量的结构体。如果使用值传递,每次调用函数时都要复制整个 BigStruct 结构体,开销巨大。而通过指针传递,只传递结构体的地址,大大节省了内存和时间。

指针在数组参数传递中的应用

  1. 数组名与指针的关系 在C语言中,数组名在很多情况下会被隐式转换为指向数组首元素的指针。例如,定义一个数组 int arr[5] = {1, 2, 3, 4, 5};arr 就可以看作是一个指向 arr[0] 的指针。

  2. 传递数组到函数 当我们将数组作为参数传递给函数时,实际上传递的是数组首元素的地址,也就是一个指针。例如:

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

printArray 函数中,int *arr 接收的是数组 arr 的首地址。我们可以通过指针偏移来访问数组的各个元素,arr[i] 等价于 *(arr + i)。这种方式使得我们可以在函数内部对数组进行操作,并且修改会反映到调用函数中的原始数组上。

  1. 多维数组的传递 对于多维数组,情况稍微复杂一些,但本质上仍然是传递指针。以二维数组为例:
#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 arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
    print2DArray(arr, 2);
    return 0;
}

print2DArray 函数中,int (*arr)[3] 表示 arr 是一个指针,它指向一个包含3个 int 类型元素的数组。这里的 3 表示二维数组的列数,必须明确指定。通过这种方式,我们可以正确地访问和操作二维数组的元素。

指针在字符串处理函数中的应用

  1. 字符串的表示与传递 在C语言中,字符串通常是以 '\0' 结尾的字符数组。当我们将字符串传递给函数时,实际上传递的是字符串首字符的地址,也就是一个字符指针。例如:
#include <stdio.h>
void printString(char *str) {
    while (*str != '\0') {
        printf("%c", *str);
        str++;
    }
    printf("\n");
}
int main() {
    char str[] = "Hello, World!";
    printString(str);
    return 0;
}

printString 函数中,char *str 接收字符串的首地址。通过 while 循环,我们逐个输出字符串的字符,直到遇到 '\0' 结束符。

  1. 字符串操作函数实现 许多C语言标准库中的字符串操作函数都是基于指针实现的。例如,strcpy 函数用于复制字符串:
#include <stdio.h>
void myStrcpy(char *dest, const char *src) {
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
}
int main() {
    char source[] = "Hello";
    char destination[10];
    myStrcpy(destination, source);
    printf("Copied string: %s\n", destination);
    return 0;
}

myStrcpy 函数中,const char *src 表示源字符串指针,char *dest 表示目标字符串指针。通过指针操作,我们将源字符串的字符逐个复制到目标字符串,直到遇到 '\0',并在目标字符串末尾添加 '\0'

指针传递中的注意事项

  1. 空指针检查 在使用指针作为函数参数时,一定要进行空指针检查。否则,当传递一个空指针时,程序可能会崩溃。例如:
#include <stdio.h>
void printValue(int *ptr) {
    if (ptr != NULL) {
        printf("The value is: %d\n", *ptr);
    } else {
        printf("The pointer is NULL.\n");
    }
}
int main() {
    int num = 10;
    int *ptr = &num;
    printValue(ptr);
    ptr = NULL;
    printValue(ptr);
    return 0;
}

printValue 函数中,我们首先检查 ptr 是否为 NULL。如果不是,才进行解引用操作输出值;否则,输出提示信息。

  1. 指针类型匹配 传递给函数的指针类型必须与函数参数期望的指针类型匹配。例如,如果函数期望一个 int * 类型的指针,传递一个 char * 类型的指针会导致未定义行为。例如:
#include <stdio.h>
void incrementInt(int *ptr) {
    (*ptr)++;
}
int main() {
    char ch = 'a';
    // 以下代码会导致未定义行为,因为类型不匹配
    // incrementInt((int *)&ch); 
    return 0;
}

在上述代码中,如果我们尝试将 char 类型变量的地址强制转换为 int * 并传递给 incrementInt 函数,由于 intchar 在内存中的存储方式和大小不同,会导致未定义行为。

  1. 内存管理 当传递指针涉及动态内存分配时,要特别注意内存管理。例如,如果在函数内部动态分配内存并通过指针返回,调用函数有责任释放该内存,否则会导致内存泄漏。例如:
#include <stdio.h>
#include <stdlib.h>
int *allocateMemory() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 10;
    }
    return ptr;
}
int main() {
    int *result = allocateMemory();
    if (result != NULL) {
        printf("The value is: %d\n", *result);
        free(result);
    }
    return 0;
}

allocateMemory 函数中,我们使用 malloc 动态分配内存。在 main 函数中,我们接收返回的指针并使用完后通过 free 释放内存,以避免内存泄漏。

指针与函数指针的结合应用

  1. 函数指针基础 函数指针是指向函数的指针变量。函数在内存中也有自己的地址,我们可以通过函数指针来调用函数。例如:
#include <stdio.h>
int add(int a, int b) {
    return a + b;
}
int main() {
    int (*funcPtr)(int, int);
    funcPtr = add;
    int result = funcPtr(3, 5);
    printf("The result of addition is: %d\n", result);
    return 0;
}

在上述代码中,int (*funcPtr)(int, int) 声明了一个函数指针 funcPtr,它指向一个返回 int 类型、接收两个 int 类型参数的函数。funcPtr = add;funcPtr 指向 add 函数。然后我们通过 funcPtr 调用 add 函数。

  1. 函数指针作为参数传递 函数指针可以作为参数传递给其他函数,这在实现回调函数等功能时非常有用。例如,我们有一个通用的函数,它可以根据不同的操作函数对两个数进行操作:
#include <stdio.h>
int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}
void operate(int a, int b, int (*func)(int, int)) {
    int result = func(a, b);
    printf("The result is: %d\n", result);
}
int main() {
    int num1 = 10, num2 = 5;
    operate(num1, num2, add);
    operate(num1, num2, subtract);
    return 0;
}

operate 函数中,int (*func)(int, int) 是一个函数指针参数。通过传递不同的函数指针(如 addsubtract),operate 函数可以执行不同的操作。

指针在链表操作中的应用

  1. 链表基础 链表是一种动态数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。在C语言中,我们可以通过结构体和指针来实现链表。例如,定义一个简单的链表节点:
#include <stdio.h>
#include <stdlib.h>
struct Node {
    int data;
    struct Node *next;
};
  1. 链表操作函数中的指针传递 在链表的插入、删除等操作函数中,指针传递起着关键作用。例如,向链表头部插入节点的函数:
struct Node* insertAtHead(struct Node **head, int value) {
    struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = *head;
    *head = newNode;
    return newNode;
}

insertAtHead 函数中,struct Node **head 是一个指向指针的指针。因为我们需要在函数内部修改链表头指针 head,所以需要传递指针的地址。如果只传递 struct Node *head,对 head 的修改不会影响到调用函数中的链表头指针。

  1. 遍历链表 遍历链表也依赖指针操作:
void printList(struct Node *head) {
    struct Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

printList 函数中,我们通过 struct Node *current 指针遍历链表,逐个输出节点的数据。

指针在结构体嵌套与递归中的应用

  1. 结构体嵌套中的指针 在结构体嵌套中,指针可以用于表示复杂的数据关系。例如,假设有一个包含学生信息和课程信息的结构体:
#include <stdio.h>
#include <string.h>
struct Course {
    char name[50];
    int credits;
};
struct Student {
    char name[50];
    int age;
    struct Course *course;
};

struct Student 中,struct Course *course 是一个指针,它指向学生所选课程的信息。通过这种方式,我们可以在 Student 结构体中灵活地关联不同的 Course 结构体,而不需要重复存储课程信息。

  1. 递归结构体与指针 递归结构体是指结构体中包含自身类型的指针。最典型的例子就是链表节点。但在更复杂的场景中,递归结构体可以用于表示树状结构等。例如,定义一个二叉树节点:
struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
};

在二叉树的各种操作(如遍历、插入、删除等)中,指针传递和操作是实现这些功能的基础。例如,前序遍历二叉树的函数:

void preOrder(struct TreeNode *root) {
    if (root != NULL) {
        printf("%d ", root->data);
        preOrder(root->left);
        preOrder(root->right);
    }
}

preOrder 函数中,通过指针 root 来递归地访问二叉树的每个节点,实现前序遍历。

指针在内存映射与硬件交互中的潜在应用

  1. 内存映射基础 在操作系统和嵌入式系统中,内存映射是将文件或设备的内容映射到进程的地址空间的技术。指针在内存映射中起着关键作用。例如,在Linux系统中,可以使用 mmap 函数将文件映射到内存:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    off_t fileSize = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);
    char *map = (char *)mmap(NULL, fileSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(1);
    }
    // 现在可以通过指针map访问文件内容
    // 例如修改文件内容
    map[0] = 'A';
    if (munmap(map, fileSize) == -1) {
        perror("munmap");
        close(fd);
        exit(1);
    }
    close(fd);
    return 0;
}

在上述代码中,mmap 函数返回一个指向映射内存区域的指针 map。我们可以像操作普通内存一样通过这个指针来读写文件内容。

  1. 硬件交互中的指针 在嵌入式系统中,指针常用于与硬件寄存器进行交互。硬件寄存器在内存中都有特定的地址,我们可以通过指针来访问和修改这些寄存器的值,从而控制硬件设备。例如,假设某个微控制器的GPIO控制寄存器地址为 0x40000000
// 假设这是一个模拟的硬件交互代码
volatile unsigned int *gpioReg = (volatile unsigned int *)0x40000000;
// 设置GPIO输出值
*gpioReg = 0x01;

在上述代码中,volatile 关键字用于告诉编译器不要对该变量进行优化,因为它的值可能会被硬件异步修改。通过将硬件寄存器地址强制转换为指针类型,我们可以像操作普通变量一样读写硬件寄存器的值,实现对硬件设备的控制。

通过以上对C语言指针在函数参数传递以及各种应用场景中的深入探讨,我们可以看到指针在C语言编程中具有强大而灵活的功能,合理运用指针可以使程序更加高效、灵活地处理各种数据和操作。