目录
1 值传递
值传递(也称为按值传递)是函数调用中一种常见的参数传递方式。在这种方式下,当调用函数时,实参(实际传递给函数的值)会被复制一份,然后这份复制的值被传递给形参(函数定义中的参数),形参在函数内部是一个独立的变量,与实参没有直接的关联。这意味着函数内部对形参的任何修改都不会影响到调用者处的实参。
#include <stdio.h>
// 函数原型声明,告知编译器 func1 是一个接受一个 int 作为参数的函数
void func1(int value);
int main()
{
// 定义一个整型变量 num 并初始化为 100
int num = 100;
// 调用函数 func1,传入 num 的值
// 注意,这里的 num 不会受到 func1 内部操作的影响
func1(num);
// 打印 num 值,应该仍然是 100
printf("num = %d\n", num); // 100
// 再次调用函数 func1,传入 num 的值
func1(num);
// 再次打印 num 值,应该仍然是 100
printf("num = %d\n", num); // 100
return 0;
}
// 函数定义,接受一个 int 类型的参数
void func1(int value)
{
// 对传入的值加 1
value += 1;
// 打印修改后的值
printf("Inside func1: value = %d\n", value); // 101, 102
}
输出结果如下所示:
2 传递指针给函数
在 C 语言中,没有直接的 “引用传递” 概念,但可以通过指针实现类似的效果。C 语言中的函数参数传递默认是值传递(即传值),但通过传递指针,可以实现对原始数据的修改,从而达到类似引用传递的效果。
当函数的形参类型是指针类型时,调用该函数时需要传递指针、地址或数组给该形参。
2.1 传地址或指针给函数
当通过传地址或指针给函数时,函数可以访问和修改调用者处的原始数据。这种方式称为 “指针传递”。这种方式在需要修改调用者处的变量值时非常有用,尤其是在处理大型数据结构时,可以避免不必要的数据复制,提高程序的效率。
#include <stdio.h>
// 函数原型声明,告知编译器 func 是一个接受一个 int 指针作为参数的函数
void func(int *);
int main()
{
// 定义一个整型变量 num 并初始化为 100
int num = 100;
// 定义一个指向整型的指针变量 ptr,并使其指向 num
int *ptr = #
// 调用函数 func,传入 num 的地址
// 这将使 func 能够修改 num 的值
func(&num);
// 打印修改后的 num 值
printf("函数外面的num = %d\n", num); // 101
// 再次调用函数 func,这次传入的是指针变量 ptr
// 由于 ptr 指向 num,所以这与直接传入 &num 效果相同
func(ptr);
// 再次打印修改后的 num 值
printf("函数外面的num = %d\n", num); // 102
return 0;
}
// 函数定义,接受一个指向整型的指针作为参数
void func(int *p)
{
// 通过指针 *p 访问指针指向的值,并对该值加 1
*p += 1;
printf("\n我会修改传递过来的地址里面的值哦~\n");
printf("参数地址当前数据:*p = %d\n", *p); // 101
}
输出结果如下所示:
2.2 传数组给函数
数组作为函数参数传递时,实际上是传递数组的首地址,也就是说,传递的是一个指针。
传递数组给函数时,通常需要显式地传递数组的大小。这是因为当数组作为参数传递给函数时,实际上传递的是数组的首地址,而不是整个数组。在函数内部,数组退化为指针,因此函数无法直接获取数组的大小。如果尝试在函数内部使用 sizeof 操作符来获取数组的大小,实际上获取的是指针的大小,而不是数组的实际大小。如果不传递数组大小,函数内部可能会发生数组越界访问,导致未定义行为或程序崩溃。
#include <stdio.h>
/* 函数声明 */
double getAverage(int *arr, int size);
int main()
{
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* 传递一个指向数组的指针作为参数 */
avg = getAverage(balance, 5);
/* 输出返回值 */
printf("Average value is: %f\n", avg);
return 0;
}
// 函数定义
double getAverage(int *arr, int size)
{
int sum = 0;
double avg;
// 遍历数组,计算总和
for (int i = 0; i < size; i++)
{
// 将当前指针所指向的值加到 sum 上
sum += *(arr + i);
// 打印当前指针所指向的地址
printf("当前arr存放的值:%d,地址: %p \n", *(arr + i), (void *)(arr + i));
}
// 计算平均值
avg = (double)sum / size;
// 返回平均值
return avg;
}
输出结果如下所示:
问:如果在 getAverage() 函数中,通过指针修改了数组的值,那么 main 函数中的 balance 数组的值是否会相应变化?
答:如果在 getAverage 函数中通过指针修改了数组的值,那么 main 函数中的 balance 数组的值也会相应变化。这是因为传递给函数的是数组的首地址,函数内部对指针所指向的值的修改会影响到原始数组。简单来说: getVerage 函数中的指针,指向的就是 main 函数中的 balance 数组。如下代码所示:
#include <stdio.h>
/* 函数声明 */
double getAverage(int *arr, int size);
int main()
{
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* 打印调用前的数组 */
printf("Before getAverage:\n");
for (int i = 0; i < 5; ++i)
{
printf("%d ", balance[i]); // 1000 2 3 17 50
}
printf("\n");
/* 传递一个指向数组的指针和数组大小作为参数 */
avg = getAverage(balance, 5);
/* 打印调用后的数组 */
printf("After getAverage:\n");
for (int i = 0; i < 5; ++i)
{
printf("%d ", balance[i]); // 2000 4 6 34 100
}
printf("\n");
/* 输出返回值 */
printf("Average value is: %f\n", avg);
return 0;
}
// 函数定义
double getAverage(int *arr, int size)
{
int sum = 0;
double avg;
// 遍历数组,计算总和
for (int i = 0; i < size; i++)
{
// 修改数组的值
*(arr + i) *= 2;
// 将当前指针所指向的值加到 sum 上
sum += *(arr + i);
}
// 计算平均值
avg = (double)sum / size;
// 返回平均值
return avg;
}
输出结果如下所示:
3 指针函数(返回指针的函数)
3.1 语法格式
指针函数是指返回值类型为指针的函数。换句话说,这类函数的返回值是一个指针,指向某种数据类型(如 int、char、struct 等)。
指针函数的声明需要指定返回值的指针类型和参数列表。语法格式如下:
返回类型 *函数名(参数列表);
// 例如,声明一个返回 int 类型指针的函数:
int *getPointer();
// 声明一个返回 char 类型指针的函数
char *getString();
// 声明一个返回 char 类型指针的函数,并接收两个 char* 类型的参数
char *strlong(char *str1, char *str2);
3.2 案例:返回静态局部变量
函数运行结束后会销毁在其内部定义的所有局部数据,包括局部变量、局部数组和形式参数。因此,函数返回的指针不能指向这些数据。如果确实有这样的需求,需要将局部变量定义为静态局部变量。
静态局部变量的生命周期与整个程序相同,即使函数调用结束,静态局部变量仍然存在于内存中。因此,返回静态局部变量的地址是安全的。
#include <stdio.h>
// 函数声明
int *returnStaticVariable();
int main()
{
// 调用 returnStaticVariable 函数,获取返回的指针
int *p = returnStaticVariable();
// 获取返回的值
int n = *p;
// 打印返回的值
printf("函数返回的数据是:value=%d\n", n); // value=100
return 0;
}
// 函数定义
int *returnStaticVariable()
{
// 函数运行结束后会销毁在其内部定义的所有局部数据,不能返回局部变量的地址
// int n = 200; 错误,编译器会报错!!!
// 静态局部变量,存放在静态数据区,即使函数调用结束,静态局部变量仍然存在于内存中
static int n = 100;
// 返回静态局部变量的地址
return &n;
}
输出结果如下所示:
3.3 案例:返回字符串
在 C 语言中,字符串常量(如 "Hello, World!")存储在静态数据区,而不是栈上。因此,返回指向字符串常量的指针是安全的。
字符串常量的生命周期与整个程序相同,不会在函数调用结束后被销毁。
#include <stdio.h>
// 声明一个返回 char 类型指针的函数
char *getString();
int main()
{
char *str = getString(); // 调用函数,获取字符串
printf("函数返回的字符串是: %s\n", str); // 打印字符串,输出:String: Hello, World!
return 0;
}
// 定义指针函数
char *getString()
{
char *str = "Hello, World!"; // 返回一个字符串(常量)的首地址
return str; // 返回指针
}
输出结果如下所示:
注意:
函数内部定义的局部变量在函数返回后会被销毁,因此返回局部变量的地址是不安全的。然而,对于这个案例:在 getString 函数中定义的 char *str 并不是指向一个局部变量,而是指向一个字符串常量。字符串常量在 C 语言中是存储在静态数据区的,因此返回它的地址是安全的。
3.4 案例:返回较长的字符串
使用 strlen 函数可以获取字符串的长度。通过比较两个字符串的长度,可以确定哪个字符串较长。
#include <stdio.h>
#include <string.h>
// 函数声明
char *strlong(char *str1, char *str2);
int main()
{
// 定义两个字符串数组和一个指针
char str1[30], str2[30];
char *str;
// 提示用户输入第 1 个字符串
printf("请输入第1个字符串:");
scanf("%29s", str1); // 限制输入长度,防止缓冲区溢出
// 提示用户输入第 2 个字符串
printf("请输入第2个字符串:");
scanf("%29s", str2); // 限制输入长度,防止缓冲区溢出
// 调用 strlong 函数,获取较长的字符串
str = strlong(str1, str2);
// 打印较长的字符串
printf("\n较长的字符串是: %s\n", str);
return 0;
}
// 函数定义
char *strlong(char *str1, char *str2)
{
// 比较两个字符串的长度
// if (strlen(str1) >= strlen(str2))
// {
// return str1; // 返回较长的字符串
// }
// else
// {
// return str2; // 返回较长的字符串
// }
// 使用三元运算符
return strlen(str1) >= strlen(str2) ? str1 : str2;
}
输出结果如下所示:
为什么这个案例没有传递数组的长度呢?
strlen 函数会遍历字符串,直到遇到 \0,从而计算字符串的长度。例如,strlen(str1) 会从 str1 的首地址开始,逐个检查字符,直到遇到 \0,返回字符数。
每个字符串数组在初始化时,最后一个元素会被自动设置为 \0,除非你手动覆盖它。因此,str1 和 str2 都有明确的终止符,strlen 可以准确地计算字符串的长度。
注意:
函数内部定义的局部变量在函数返回后会被销毁,因此返回局部变量的地址是不安全的。然而,对于这个案例:在 main 函数中定义的字符串数组 str1 和 str2 存储在 main 函数的栈上。当调用 strlong 函数时,传递给 strlong 的参数 str1 和 str2 是 main 函数中数组的地址。这些参数在 strlong 函数调用结束后仍然有效,因为它们指向的是 main 函数中的数组,而不是 strlong 函数内部的局部变量。
3.5 案例:返回静态局部数组
编写一个函数 randArr,该函数生成 10 个随机数,范围是 [1,100],并使用数组名作为返回值。由于返回的数组需要在函数调用结束后仍然有效,因此必须将数组声明为静态局部变量。
#include <stdio.h>
#include <stdlib.h> // 用于 rand 和 srand 函数
#include <time.h> // 用于 time 函数
// 函数声明
int *randArr();
int main()
{
// 初始化随机数生成器
srand(time(NULL));
// 调用 randArr 函数,获取返回的指针
// p 指向是在 randArr 生成的数组的首地址(即第一个元素的地址)
int *p = randArr();
// 打印生成的随机数
for (int i = 0; i < 10; i++)
{
printf("第%d个[1,100]随机数:%d\n", i, *(p + i)); // 使用指针算术访问数组元素
}
return 0;
}
// 函数定义:生成 10 个随机数并返回数组
int *randArr()
{
// 随机数边界值
int min = 1;
int max = 100;
// 静态局部数组,存放在静态数据区
// 必须加上 static ,让 arr 的空间在静态数据区分配
static int arr[10];
// 生成 10 个随机数并存储到数组中
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % (max - min + 1) + min;
}
// 返回静态局部数组的地址
return arr;
}
输出结果如下所示:
3.6 返回值注意事项
返回局部变量:指针函数返回局部变量的地址是不安全的,原因在于局部变量是在栈上分配的,它们的生命周期仅限于定义它们的函数作用域内。当函数调用结束后,栈帧被销毁,局部变量所占用的内存也随之释放,因此返回的指针将指向不确定的内存区域,可能导致未定义行为。
返回字符串常量:指针函数返回指向字符串常量的指针通常是安全的,因为这些字符串常量通常存储在程序的只读数据段(静态数据区)中。其生命周期贯穿整个程序运行期间,因此指向这些常量的指针在程序运行时始终有效。然而,需要注意的是,不应尝试修改字符串常量内容,因为这可能会导致程序崩溃或未定义行为。
返回传递的参数:指针函数如果函数返回的是传递参数的地址(假设这些参数本身不是局部变量或临时变量),这通常是安全的。因为这些参数要么来自调用者的作用域(对于值传递的参数,其地址实际上指向调用者的变量),要么是通过指针或引用传递的,在调用者上下文中保持有效。不过,如果参数本身是局部变量且已被销毁(例如,如果参数是通过值传递的局部变量的地址),则返回这样的地址同样是不安全的。
返回静态局部变量或数组:指针函数返回静态局部变量或数组的地址是安全的,因为静态局部变量或数组存储在静态存储区,它们的生命周期与整个程序相同。静态局部变量或数组在程序的整个运行期间都存在,不会被函数调用结束所销毁,因此返回的指针在整个程序运行期间都是有效的。
4 函数指针(指向函数的指针)
4.1 函数的内存布局
在 C 和 C++ 编程中,函数总是占用一段连续的内存区域,并且函数名在表达式中有时会被编译器转换为该函数所在内存区域的首地址(即函数的入口地址)。这种特性使得函数名在某种程度上类似于数组名,因为数组名也代表数组首元素的地址。
利用这一特性,我们可以将函数的入口地址赋予一个指针变量,使该指针变量指向函数所在的内存区域。通过这个指针变量,我们可以间接地找到并调用相应的函数。这种指针被称为函数指针。
4.2 语法格式
返回类型 (*函数指针名)(参数列表);
- 括号 () 的优先级高于星号 *,因此第一个括号不能省略。如果省略,编译器会将其解析为返回指针的函数(即指针函数),而不是函数指针。
- 在函数指针声明中,参数名是可以省略的,只保留类型即可。
-
函数指针调用时可以省略解引用操作符 *,直接使用:函数指针名(参数列表)。
4.3 案例演示
下面我们实现用函数指针来实现对函数的调用,返回两个整数中的最大值。
#include <stdio.h>
// 函数原型声明,形参名可以省略,但是形参的数据类型不可以省略
int max(int, int);
int main()
{
int x, y, maxVal;
// 定义一个函数指针类型,它指向一个接受两个 int 参数并返回 int 的函数
// 这里我们直接在声明时进行了初始化,所以不需要在下一行单独赋值
// int (*pmax)(int x, int y) = max;
int (*pmax)(int, int) = max; // pmax 是一个指向 max 函数的指针
// 注意:在函数指针声明中,参数名是可以省略的,只保留类型即可
// 输入两个整数
printf("Input first number: ");
scanf("%d", &x);
printf("Input second number: ");
scanf("%d", &y);
// 通过函数指针 pmax 调用 max 函数,并将结果存储在 maxVal 中
// 使用(*pmax)(x, y),pmax 表示函数的地址(指针),*pmax 取地址里面的内容,即表示函数本身
// maxVal = (*pmax)(x, y);
// 函数指针调用时可以省略解引用操作符 *
maxVal = pmax(x, y); // 这里可以直接使用 pmax(x, y),效果相同
// 输出最大值
printf("Max value: %d\n", maxVal);
// 输出函数指针和函数地址的信息
printf("函数指针是(函数的入口地址): %p, 函数指针的本身的地址是: %p\n", (void *)pmax, (void *)&pmax);
printf("函数名代表的是函数的入口地址: %p, 对函数名取地址数值上也是入口地址: %p\n", max, &max); // 和指针数组类似
return 0;
}
// 返回两个整数中较大的一个
int max(int a, int b)
{
// 使用三元运算符来比较两个整数并返回较大的一个
return a > b ? a : b;
}
输出结果如下所示:
注意:
上述案例在打印函数地址时,使用了 (void*) 进行强制类型转换,以避免潜在的警告或错误。然而,需要强调的是,在标准 C 中,获取和打印函数地址的行为是未定义的(undefined behavior)。尽管许多编译器允许这样做,但并不意味着它是可移植的或安全的。在实际编程中,通常不需要这样做,这里只是为了更好地讲解知识点。
5 回调函数
5.1 概念
函数指针可以作为参数传递给其他函数,这种通过函数指针调用的函数被称为回调函数。简而言之,回调函数是你在调用另一个函数时传入,并由该函数在适当时候执行的函数。
5.2 案例:传入库函数
使用回调函数的方式,给一个整型数组 int arr[10] 赋 10 个随机数和其自定义内容。
#include <stdlib.h>
#include <stdio.h> // 使用 rand、srand 函数
#include <time.h> // 使用 time() 函数
// 函数原型声明
// void initArray(int *, int, int (*)()); 省略参数列表
void initArray(int *array, int arraySize, int (*f)());
int zero();
int one();
int main()
{
int myarray[10];
// 设置随机数种子
srand(time(NULL));
// 1. 调用 initArray 函数,传入 myarray、数组大小 10 和 rand 函数作为回调函数
initArray(myarray, 10, rand);
// 输出数组内容
for (int i = 0; i < 10; i++)
{
printf("myarray[%d] = %d\n", i, myarray[i]); // 随机初始化
}
printf("\n\n");
// 2. 调用 initArray 函数,传入 myarray、数组大小 10 和 zero 函数作为回调函数
initArray(myarray, 10, zero);
// 输出数组内容
for (int i = 0; i < 10; i++)
{
printf("myarray[%d] = %d\n", i, myarray[i]); // 全 1 初始化
}
printf("\n\n");
// 3. 调用 initArray 函数,传入 myarray、数组大小 10 和 one 函数作为回调函数
initArray(myarray, 10, one);
// 输出数组内容
for (int i = 0; i < 10; i++)
{
printf("myarray[%d] = %d\n", i, myarray[i]); // 全 0 初始化
}
return 0;
}
// 初始化数组,使用回调函数 f 来为数组的每个元素赋值
void initArray(int *array, int arraySize, int (*f)())
{
// 遍历数组
for (int i = 0; i < arraySize; i++)
{
// 调用回调函数 f,并将其返回值赋给数组元素
// array[i] = (*f)(); 指针函数在调用时间,可以省略引用符 *
array[i] = f();
}
}
int zero()
{
return 0;
}
int one()
{
return 1;
}
输出结果如下所示:
5.3 案例:传入自定义函数
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
// 函数原型声明
int generateRandomNumber();
int generateFixedValue();
void initArray(int *array, int arraySize, int (*f)());
int main()
{
int myarray[10];
// 设置随机数种子
srand(time(NULL));
// 调用 initArray 函数,传入 myarray、数组大小 10 和 generateRandomNumber 函数作为回调函数
initArray(myarray, 10, generateRandomNumber);
// 输出数组内容
printf("Array with random values:\n");
for (int i = 0; i < 10; i++)
{
printf("myarray[%d] = %d\n", i, myarray[i]);
}
// 再次调用 initArray 函数,传入 myarray、数组大小 10 和 generateFixedValue 函数作为回调函数
initArray(myarray, 10, generateFixedValue);
// 输出数组内容
printf("Array with fixed values:\n");
for (int i = 0; i < 10; i++)
{
printf("myarray[%d] = %d\n", i, myarray[i]);
}
return 0;
}
// 定义一个回调函数,用于生成随机数
int generateRandomNumber()
{
return rand() % 100; // 生成 0 到 99 之间的随机数
}
// 定义一个回调函数,用于生成固定值
int generateFixedValue()
{
return 42; // 返回固定值 42
}
// 初始化数组,使用回调函数 f 来为数组的每个元素赋值
void initArray(int *array, int arraySize, int (*f)())
{
// 遍历数组
for (int i = 0; i < arraySize; i++)
{
array[i] = f(); // 调用回调函数 f,并将其返回值赋给数组元素
}
}
输出结果如下所示:
6 测试题
1. 请写出下面程序的运行结果。
void func(int arg1, int *arg2)
{
arg1 += 1;
*arg2 += 1;
}
int main()
{
int num1 = 10, num2 = 10;
func(num1, &num2);
printf("%d %d", num1, num2);
return 0;
}
【答案】10 11
【解析】
(1)func 函数的形参 arg1 接收到的是变量 num1的值,接收完值后,两者不再有关系。
(2)func 函数的形参 arg2 接收到的是变量 num2 的地址,所以 arg2 仍然指向 num2。
2. 请写出下面程序的运行结果。
int num = 10;
int cb(int arg)
{
return num + arg;
}
void func(int (*f)())
{
int num = 20;
printf("%d", f(2));
}
int main()
{
int num = 30;
func(cb);
return 0;
}
【答案】12
【解析】
cb 函数中使用了自身作用域没有的变量 num,cb 函数的上层作用域是全局,所以使用全局作用域中定义的变量 num,值是 10。然后再加上传入过来的参数 2,最终结果为 12。