1 值传递
1.1 概念与特点
值传递(Pass by Value) 是函数调用中一种常见的参数传递方式,具有以下特点:
- 复制实参的值: 在调用函数时,实参的值会被复制一份,然后这份复制的值被传递给形参。
- 形参独立: 形参在函数内部是一个独立的变量,与实参没有直接的关联。
- 不影响实参: 函数内部对形参的任何修改都不会影响到调用者处的实参。
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 = #
// 第一次调用 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),即函数外部的数据在未显式赋值的情况下被意外修改。这种副作用可能导致程序行为难以预测,增加调试难度。
常见问题
- **无意间的数据修改:**函数内部可能无意间修改了原始数据,导致调用者处的数据状态发生变化,而调用者可能并未预期到这种修改。
- **共享内存区域的数据状态混乱:**当多个函数共享同一块内存区域时,一个函数对该区域的修改可能会影响其他函数的执行结果,导致数据状态混乱。
- **代码可读性和调试难度增加:**指针传递使得数据的变化路径变得难以追踪,增加了代码的复杂性和调试的难度。
案例演示
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 被改变了。
注意事项
- 明确函数意图: 如果函数需要修改实参,应在函数命名和注释中清楚表达这一点,以便其他开发者能够理解函数的预期行为。
- 避免不必要的修改: 仅在确实需要改变原始数据时才使用指针传递。如果函数只需要读取数据而不需要修改,应考虑使用值传递或其他方式。
- 合理使用 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 中的运行结果如下所示:
