48 C 语言指针与函数核心详解:值传递、指针传递(变量地址、指针变量、数组名)、编程实战案例

1 值传递

1.1 概念与特点

值传递(Pass by Value) 是函数调用中一种常见的参数传递方式,具有以下特点:

  1. 复制实参的值: 在调用函数时,实参的值会被复制一份,然后这份复制的值被传递给形参。
  2. 形参独立: 形参在函数内部是一个独立的变量,与实参没有直接的关联。
  3. 不影响实参: 函数内部对形参的任何修改都不会影响到调用者处的实参。

1.2 案例演示

下面是一个简单的 C 程序,演示了值传递的工作原理:

cpp 复制代码
#include <stdio.h>

// 函数原型声明
void increment(int value);

int main()
{
    int num = 100;

    printf("调用函数前:num = %d\n", num); // 输出 100
    // 第一次调用函数,传递 num 的值
    increment(num);
    printf("第一次调用函数后:num = %d\n", num); // 输出 100

    // 第二次调用函数,传递 num 的值
    increment(num);
    printf("第二次调用函数后:num = %d\n", num); // 输出 100

    return 0;
}

// 函数定义
void increment(int value)
{
    value += 1;                              // 修改的是局部变量 value
    printf("函数内部:value = %d\n", value); // 输出 101
}

程序在 VS Code 中的运行结果如下所示:


2 指针传递

在一些编程语言中(如 C++、Java、Python 等),存在一种称为**引用传递(Pass by Reference)**的参数传递方式。在这种方式下,函数接收的是实际参数的别名或直接引用,因此在函数内部对形参的修改会直接影响到调用者处的原始数据。

虽然 C 语言本身并不支持引用传递这一机制,但可以通过传递变量的地址(即指针)来实现类似的效果。将指针作为参数传入函数后,函数可以访问和修改该地址所指向的数据,从而影响函数外部的原始变量。

这种方式常用于需要在函数内部修改实参值的场景,以下是几种常见的指针传递方式。

2.1 传递变量地址

通过传递变量的地址给函数,函数可以修改该变量的值。

  • 地址传递: 通过传递变量的地址,函数可以访问和修改原始数据。
  • 指针解引用: 在函数内部使用指针解引用操作符 * 来访问和修改指针指向的值
cpp 复制代码
#include <stdio.h>

// 函数原型声明
void increment(int *p);

int main()
{
    int num = 100;
    printf("num 的初始值 = %d\n\n", num); // 输出:100

    // 第一次调用 increment
    printf("开始第一次 increment 调用...\n");
    increment(&num);
    printf("第一次调用结束后,主函数中的 num = %d\n\n", num); // 输出:101

    // 第二次调用 increment
    printf("开始第二次 increment 调用...\n");
    increment(&num);
    printf("第二次调用结束后,主函数中的 num = %d\n\n", num); // 输出:102

    return 0;
}

// 函数定义,接受一个指向整型的指针作为参数
void increment(int *p)
{
    *p += 1; // 修改指针所指向的值

    // 打印当前操作的地址和值
    printf(" 函数内部:成功修改了地址 %p 中的值。\n", (void *)p);
    printf(" 当前地址中存储的数据为:*p = %d\n", *p);
}

程序在 VS Code 中的运行结果如下所示:

2.2 传递指针变量

传递指针变量给函数与直接传递地址效果相同,因为指针变量本身存储的是地址。通过传递指针变量,函数可以访问和修改该地址指向的数据。

  • 指针变量: 指针变量存储的是地址,通过传递指针变量,函数可以访问和修改该地址指向的数据。
  • 指针解引用: 在函数内部使用指针解引用操作符 * 来访问和修改指针指向的值
  • **灵活性:**使用指针变量可以提高代码的灵活性,因为它可以在程序中进行重新赋值和传递。
cpp 复制代码
#include <stdio.h>

// 函数原型声明
void increment(int *p);

int main()
{
    int num = 100;
    printf("num 的初始值 = %d\n\n", num); // 输出:100

    // 定义一个指针变量并让它指向 num
    int *ptr = &num;

    // 第一次调用 increment
    printf("开始第一次 increment 调用...\n");
    increment(ptr);  // 通过指针变量传入
    printf("第一次调用结束后,主函数中的 num = %d\n\n", num); // 输出:101

    // 第二次调用 increment
    printf("开始第二次 increment 调用...\n");
    increment(ptr);   // 再次通过指针变量传入
    printf("第二次调用结束后,主函数中的 num = %d\n\n", num); // 输出:102

    return 0;
}

// 函数定义,接受一个指向整型的指针作为参数
void increment(int *p)
{
    *p += 1; // 修改指针所指向的值

    // 打印当前操作的地址和值
    printf(" 函数内部:成功修改了地址 %p 中的值。\n", (void *)p);
    printf(" 当前地址中存储的数据为:*p = %d\n", *p);
}

程序在 VS Code 中的运行结果如下所示:

2.3 传递数组名

传递数组名给函数时,实际上是传递数组的首地址,函数可以通过指针访问和修改数组元素。

  • 数组作为指针: 在函数调用中,数组名退化为指向其首元素的指针
  • 数组大小传递: 通常需要显式传递数组大小 ,因为函数内部无法直接获取数组大小。
cpp 复制代码
#include <stdio.h>

// 函数原型声明
double getAverage(int *arr, int size);

int main()
{
    // 定义一个包含 5 个元素的整型数组
    int balance[5] = {1000, 2, 3, 17, 50};
    double avg; // 声明一个变量用于存储平均值

    // 打印调用函数前的数组内容
    printf("调用 getAverage 前的数组内容:\n");
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", balance[i]);
    }
    printf("\n\n");

    // 调用函数,传入数组名和数组大小
    // 数组名作为参数传递时,实际上传递的是数组首元素的地址
    // 所以需要显示地传递数组大小,以便函数知道要处理多少个元素
    avg = getAverage(balance, 5);

    // 打印调用函数后的数组内容
    printf("调用 getAverage 后的数组内容(每个元素已被乘以 2):\n");
    for (int i = 0; i < 5; ++i)
    {
        printf("%d ", balance[i]);
    }
    printf("\n\n");

    // 输出计算得到的平均值(是原始数据两倍后的平均值)
    printf("平均值为: %.2f\n", avg);

    return 0;
}

// 函数定义:将数组中每个元素乘以 2,并计算其总和与平均值
// 注意:此函数会修改原数组的内容
double getAverage(int *arr, int size)
{
    int sum = 0; // 用于累加数组元素的和
    double avg;  // 用于存储最终计算出的平均值

    // 遍历数组中的每个元素
    // 将其乘以 2,并将结果累加到 sum 中
    for (int i = 0; i < size; i++)
    {
        *(arr + i) *= 2;   // 修改数组元素的值(乘以 2)
        sum += *(arr + i); // 累加修改后的值
    }

    // 计算平均值
    avg = (double)sum / size;

    // 返回最终的平均值
    return avg;
}

程序在 VS Code 中的运行结果如下所示:

提示: 为什么需要显式传递数组大小?

在 C 语言中,数组名本质上是一个指向数组首元素的指针常量。当我们将数组名作为参数传递给函数时,实际上传递的是数组的起始地址,而不包含数组长度或其他元信息。因此,在函数内部:

  • 无法通过数组名直接获取数组的大小;
  • 无法判断数组的边界;
  • 如果不对数组长度进行额外传递,函数将不知道要处理多少个元素。

所以,在需要操作整个数组时,必须手动将数组大小作为额外参数传入函数

2.4 副作用与注意事项

在 C 语言中,将指针作为函数参数进行传递是一种强大的机制,它允许函数直接访问并修改调用者作用域内的数据。这种机制虽然提高了程序的效率,但也可能带来副作用(Side Effect),即函数外部的数据在未显式赋值的情况下被意外修改。这种副作用可能导致程序行为难以预测,增加调试难度。

常见问题

  1. **无意间的数据修改:**函数内部可能无意间修改了原始数据,导致调用者处的数据状态发生变化,而调用者可能并未预期到这种修改。
  2. **共享内存区域的数据状态混乱:**当多个函数共享同一块内存区域时,一个函数对该区域的修改可能会影响其他函数的执行结果,导致数据状态混乱。
  3. **代码可读性和调试难度增加:**指针传递使得数据的变化路径变得难以追踪,增加了代码的复杂性和调试的难度。

案例演示

cpp 复制代码
#include <stdio.h>

// 函数原型声明
void modifyData(int *p);

int main()
{
    int value = 10;

    printf("调用前 value = %d\n", value); // 输出 10

    modifyData(&value); // 传入地址

    printf("调用后 value = %d\n", value); // 输出 20

    return 0;
}

// 函数定义:通过指针修改数据
void modifyData(int *p)
{
    *p = 20; // 修改原始变量
}

程序在 VS Code 中的运行结果如下所示:

在这个例子中,函数 modifyData 接收一个指向整型的指针,并对其所指向的值进行了修改。虽然功能上没有错误,但如果没有注释或文档说明,阅读代码的人很难发现主函数中的 value 被改变了。

注意事项

  1. 明确函数意图: 如果函数需要修改实参,应在函数命名和注释中清楚表达这一点,以便其他开发者能够理解函数的预期行为。
  2. 避免不必要的修改: 仅在确实需要改变原始数据时才使用指针传递。如果函数只需要读取数据而不需要修改,应考虑使用值传递或其他方式。
  3. 合理使用 const 限定符: 为了防止误操作并增强代码的可读性和安全性,当函数不需要对指针所指向的数据进行写操作时,应将指针声明为 const 类型。

2.5 使用 const 保护原始数据

为了避免函数在不必要的情况下修改原始数据,C 语言提供了 const 关键字。当函数不需要对指针所指向的数据进行写操作时,应将指针声明为 const 类型,以限制其修改能力。

语法形式

cpp 复制代码
void func(const int *p);
  • 这表示函数不能通过 p 修改其所指向的内容。

案例演示

cpp 复制代码
#include <stdio.h>

// 函数原型声明
// 使用 const 修饰指针,表示指针指向的数据不允许修改
void printData(const int *p);

int main()
{
    int value = 100;

    printData(&value); // 传入地址

    return 0;
}

// 函数定义:只读取数据,不允许修改
void printData(const int *p)
{
    printf("Data: %d\n", *p);

    // 下面这行代码会导致编译错误
    // *p = 200;
}

程序在 VS Code 中的运行结果如下所示:

若尝试取消注释 *p = 200; ,大多数现代 C 编译器会报错,提示不能通过 const int * 类型的指针修改数据。


3 值传递 VS 指针传递

特性 值传递 指针传递
定义 在调用函数时,实参的值被复制一份给形参,形参拥有独立的副本 通过传递变量的地址(指针)给函数,函数可以访问和修改该地址所指向的数据
形参与实参的关系 形参是实参的副本,与实参无直接关联 形参是实参的指针,指向实参的内存地址
对实参的影响 函数内部对形参的修改不会影响到实参 函数内部可以通过指针修改实参的值
内存占用 形参会占用额外的内存空间来存储实参的副本 形参只是一个指针,不占用额外的内存空间来存储实参的值,只是存储了实参的地址
使用场景 当函数不需要修改实参的值时,或希望保持实参的独立性时 当函数需要修改实参的值时,或希望减少数据复制的开销时
代码可读性 对于不需要修改实参的函数,值传递可能使代码意图更清晰 对于需要修改实参的函数,指针传递可能更直观,但也可能增加代码的复杂性
示例 void increment(int value); 调用时 increment(num); void increment(int *p); 调用时 increment(&num);

4 编程练习

4.1 交换数值

编写一个 C 语言程序,使用指针交换两个整数的值。

cpp 复制代码
#include <stdio.h>

// 函数原型声明
void swap(int *a, int *b);

int main()
{
    int num1 = 5, num2 = 10;

    printf("交换前: num1 = %d, num2 = %d\n", num1, num2);

    swap(&num1, &num2); // 调用交换函数,传递 num1 和 num2 的地址

    printf("交换后: num1 = %d, num2 = %d\n", num1, num2);

    return 0;
}

// 函数定义:交换两个整数的值
void swap(int *a, int *b)
{
    // 使用 * 操作符访问指针指向的值
    int temp = *a;
    *a = *b;
    *b = temp;
}

程序在 VS Code 中的运行结果如下所示:

4.2 求数组均值

编写一个 C 语言程序,使用指针计算一个整数数组的平均值。

cpp 复制代码
#include <stdio.h>

// 函数原型声明
double calculateAverage(int *arr, int size);

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

    double avg = calculateAverage(arr, size); // 调用计算平均值函数

    printf("数组的平均值: %.2f\n", avg);

    return 0;
}

// 函数定义:计算整数数组的平均值
// 注意:需要传递数组指针和数组大小作为参数
double calculateAverage(int *arr, int size)
{
    double sum = 0; // 用于存储数组元素的总和

    for (int i = 0; i < size; i++)
    {
        sum += arr[i]; // 累加数组元素
    }

    return sum / size; // 返回平均值
}

程序在 VS Code 中的运行结果如下所示:

4.3 找数组最大值

编写一个 C 语言程序,使用指针查找一个整数数组中的最大值。

cpp 复制代码
#include <stdio.h>

// 函数原型声明
int findMax(int *arr, int size);

int main()
{
    int arr[] = {3, 7, 1, 9, 4};
    int size = sizeof(arr) / sizeof(arr[0]);

    int max = findMax(arr, size); // 调用查找最大值函数

    printf("数组中的最大值: %d\n", max);

    return 0;
}

// 函数定义:查找整数数组中的最大值
// 参数:arr - 整数数组的指针,size - 数组的大小
int findMax(int *arr, int size)
{
    int max = arr[0]; // 初始化最大值为数组的第一个元素

    for (int i = 1; i < size; i++)
    {
        if (arr[i] > max)
        {
            max = arr[i]; // 如果当前元素大于最大值,则更新最大值
        }
    }

    return max; // 返回最大值
}

程序在 VS Code 中的运行结果如下所示:

4.4 反转字符串

编写一个 C 语言程序,使用指针反转一个字符串。

cpp 复制代码
#include <stdio.h>
#include <string.h>

// 函数原型声明
void reverseString(char *str);

int main()
{
    char str[] = "Hello, World!";

    printf("原始字符串: %s\n", str);

    reverseString(str); // 调用反转字符串函数

    printf("反转后的字符串: %s\n", str);

    return 0;
}

// 函数定义:反转字符串
void reverseString(char *str)
{
    int length = strlen(str); // 获取字符串长度

    // 遍历字符串的前半部分,并交换字符
    for (int i = 0; i < length / 2; i++)
    {
        // 可以当做数组来使用,直接使用下标访问
        char temp = str[i];
        str[i] = str[length - i - 1];
        str[length - i - 1] = temp;
    }
}

程序在 VS Code 中的运行结果如下所示:

4.5 统计字符数

编写一个 C 语言程序,使用指针统计一个字符串中某个特定字符的出现次数。

cpp 复制代码
#include <stdio.h>
#include <string.h>

// 函数原型声明:统计字符串中某字符的出现次数
int countCharInStr(char *str, char ch);

int main()
{
    char str[] = "Hello, welcome to C programming!"; // 字符串
    char ch = 'o';                                   // 要统计的字符

    int count = countCharInStr(str, ch); // 调用统计字符出现次数的函数
    printf("字符 '%c' 出现的次数: %d\n", ch, count);

    return 0;
}

// 函数定义:统计字符串中某字符的出现次数
int countCharInStr(char *str, char ch)
{
    int count = 0; // 计数器

    // 遍历字符串,统计字符出现次数
    // *str 取得当前字符
    while (*str != '\0')
    {
        if (*str == ch)
        {
            count++; // 计数器加 1
        }
        str++; // 指针后移,指向下一个字符
    }

    return count; // 返回字符出现次数
}

程序在 VS Code 中的运行结果如下所示:

4.6 复制字符串

编写一个 C 语言程序,使用指针将一个字符串复制到另一个字符串中。

cpp 复制代码
#include <stdio.h>

// 函数原型声明:将一个字符串复制到另一个字符串中
// 使用 const 修饰符来确保源字符串不会被修改
void copyStr(char *dest, const char *src);

int main()
{
    char src[] = "Source string"; // 源字符串
    char dest[50];                // 确保目标字符串有足够的空间

    copyStr(dest, src); // 调用复制字符串的函数
    printf("复制后的字符串: %s\n", dest);

    return 0;
}

// 函数定义:将一个字符串复制到另一个字符串中
// 使用 const 修饰符来确保源字符串不会被修改
void copyStr(char *dest, const char *src)
{
    // 使用 while 循环将源字符串中的字符复制到目标字符串中
    while (*src != '\0')
    {
        *dest = *src; // 将源字符串中的字符复制到目标字符串中
        dest++;       // 移动目标字符串的指针
        src++;        // 移动源字符串的指针
    }

    *dest = '\0'; // 添加字符串结束符
}

程序在 VS Code 中的运行结果如下所示:

4.7 查找子串

编写一个 C 语言程序,使用指针查找一个字符串中是否包含另一个子串,并返回子串首次出现的位置。

cpp 复制代码
#include <stdio.h>
#include <string.h>

// 函数原型声明:查找一个字符串中是否包含另一个子串,并返回子串首次出现的位置
// 使用 const 修饰符确保函数不会修改传入的字符串
int findSubStr(const char *str, const char *substr);

int main()
{
    char str[] = "This is a sample string."; // 源字符串
    char substr[] = "sample";                // 子串

    int pos = findSubStr(str, substr); // 调用查找子串的函数

    if (pos != -1)
    {
        printf("子串 '%s' 的位置: %d\n", substr, pos);
    }
    else
    {
        printf("未找到子串 '%s'\n", substr);
    }
    return 0;
}

// 函数定义:查找一个字符串中是否包含另一个子串,并返回子串首次出现的位置
// 使用 const 修饰符确保函数不会修改传入的字符串
int findSubStr(const char *str, const char *substr)
{
    int strLen = strlen(str);       // 获取源字符串的长度
    int substrLen = strlen(substr); // 获取子串的长度

    if (substrLen > strLen)
        return -1; // 子串比原字符串长,不可能包含

    // 遍历源字符串,查找子串
    // i 表示源字符串的当前位置,j 表示子串的当前位置
    // strLen - substrLen 表示源字符串中剩余的字符数,确保不会越界
    /* 如果 i 超过 strLen - substrLen,那么在比较子串时,
        源字符串的剩余部分将不足以容纳子串,导致数组越界访问。
        例如,如果源字符串长度为 10,子串长度为 3,那么 i 最多可以是 7,
        因为从位置 7 开始,子串需要 3 个字符,所以访问到位置 9,即字符串的末尾 */
    for (int i = 0; i <= strLen - substrLen; i++)
    {
        int j; // 子串的当前位置

        // 比较源字符串和子串的每个字符
        for (j = 0; j < substrLen; j++)
        {
            // 检查源字符串中从位置 i 开始的字符是否与子串中的字符逐一匹配
            if (str[i + j] != substr[j])
                break; // 如果不匹配,跳出内层循环
        }

        if (j == substrLen)
            return i; // 找到子串,返回位置
    }

    return -1; // 未找到子串
}

程序在 VS Code 中的运行结果如下所示: