函数和指针在 C/C++ 中常常一起使用,指针可以用于传递函数参数、返回函数结果或者作为函数的返回值。这样做可以实现更灵活和高效的程序设计。

下面是一些常见的函数和指针的使用方式:

1. 指针做函数参数

1.1 参数为变量

可以将指针作为函数的参数,从而在函数内部直接访问并修改指针所指向的变量。这样可以避免在函数调用时进行变量的拷贝,提高程序的运行效率。

需求:编写一个函数实现两个变量之间的值的交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>

void swap1(int x, int y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d\n", x, y);
}

void swap2(int* x, int* y)
{
int tmp;
tmp = *x;
*x = *y;
*y = tmp;
}

int main()
{
int a = 3;
int b = 5;
swap1(a, b); // 值传递
printf("a = %d, b = %d\n", a, b);

a = 3;
b = 5;
swap2(&a, &b); // 地址传递
printf("a2 = %d, b2 = %d\n", a, b);

return 0;
}

程序输出的结果如下:

1
2
3
x = 5, y = 3
a = 3, b = 5
a2 = 5, b2 = 3

在上面代码中一共编写了两个交换函数swap1swap2,参数个数相同但是类型不同,通过输出的结果可以看出swap2函数完成了变量之间的数据交换,swap1没有,其主要原因是这样的:

  • swap1函数的参数是数值,参数传递的是值,在传递过程中实参会发生拷贝,此时形参和实参变量对应的内存地址是不同的,在函数体内部交换的是参数的值,而不是外部实参的值
  • swap2函数的参数是指针,参数传递的是地址,形参指针指向的地址和实参变量的地址是相同的,因此在函数体内部通过形参指针就可以交换实参变量内部的数值了。

1.2 参数为数组

数组名也可以做函数参数,但是数组类型的形参最终会退化为指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//void printArrary(int a[10], int n)
//void printArrary(int a[], int n)
void printArrary(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d, ", a[i]);
}
printf("\n");
}

int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int n = sizeof(a) / sizeof(a[0]);

//数组名做函数参数
printArrary(a, n);
return 0;
}

通过上面的示例代码可以得知,数组做函数参数的时候,对于形参而言有三种写法:

  • 指定为数组类型,并且指定了数组的最大容量,必须和实参容量保持一致
    • void printArrary(int a[10], int n)
  • 指定为数组类型,没有指定数组的最大容量,需要通过实参进行推导
    • void printArrary(int a[], int n)
  • 指定为指针类型,通过形参指针指向实参数组的地址
    • void printArrary(int* a, int n)

由于数组类型的形参最终会退化为指针,所以有一个细节需要额外注意:在函数体内部不能通过运算符 sizeof(形参) 的方式对数组进行内存大小的计算,因为这样计算出的是指针自身所占用的内存大小(得到的结果是4字节或者8字节),而我们想要知道的是指针指向的内存的大小。所以在进行数组传递的时候,都会给函数添加一个额外的整形参数用于标记当前这个数组的容量。

2. 指针做函数返回值

函数可以返回指针,这样可以在函数外部获取指针所指向的数据。需要额外注意返回的指针指向的内存的生命周期。

2.1 返回局部数据

局部数据是在函数内部定义的变量,它们的作用范围被限制在所在的函数块内。局部数据只在函数执行期间存在,并且在函数执行结束时会自动被销毁,释放相关的内存空间。

以下是一个示例,展示了局部数据的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int* getValue(int num)
{
int number = num * 2 - 66;
return &number;
}

int main()
{
int *val = getValue(66);
printf("value = %d\n", *val);

return 0;
}

执行上面的程序打印出的结果有两种:正确或者错误。上面的程序有一个隐藏的Bug:getValue 函数内部的 number 是一个局部变量,函数调用结束之后,这个变量对应的存储空间也就被释放了,此时函数调用者通过函数返回值拿到的地址就是非法的,这块地址随时都可能被分配给其它的变量并且被写入新的值,所以我们通过指针取出的数据可能正确也可能错误。

2.2 返回全局数据

全局数据是在函数外部定义的变量,它们在整个程序的执行期间都是有效的,其作用范围跨越了多个函数的调用。

以下是一个示例,展示了全局数据的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int a = 10;
int* getA(int num)
{
a = num * 3 % 9;
return &a;
}

int main()
{
int *val = getA(16);
printf("val = %d\n", *val);

return 0;
}

上面的代码中通过使用全局变量,解决了变量内存生命周期问题,但仔细思考一下会发现这么写没有任何意义可言,全局部变量可以直接使用,完全不需要通过返回值的方式将其传递给函数调用者,在后面的章节中会给大家详细讲解如何动态申请存储空间,这样函数返回指针就变得有意义了。

3. 函数指针

函数指针是指向函数的指针变量。它们可以用于动态地调用不同的函数,实现程序的灵活性和可扩展性。

以下是一个示例,展示了函数指针的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

int main()
{
// 定义一个函数指针,指向有两个整型参数和整型返回值的函数
int (*funcPtr)(int, int);
// 将函数指针指向 add 函数
funcPtr = &add;
// 通过函数指针调用 add 函数
int result = funcPtr(3, 4);
printf("Result of addition: %d\n", result);

// 将函数指针指向 subtract 函数
funcPtr = &subtract;
// 通过函数指针调用 subtract 函数
result = funcPtr(5, 2);
printf("Result of subtraction: %d\n", result);

return 0;
}

在上述示例中,我们首先定义了一个函数指针 funcPtr,它指向一个具有两个整型参数和整型返回值的函数。然后,我们将 funcPtr 分别指向 addsubtract 函数,并使用函数指针调用相应的函数。

需要注意的是,对于函数指针变量,我们需要显式地使用 & 取得函数的地址。然而,在大多数情况下,可以直接将函数名赋给函数指针,编译器会自动进行地址获取。

函数指针可以用于实现回调机制,使函数能够动态地选择要执行的代码段。此外,函数指针还可以在实现函数库、处理函数指针数组等方面发挥重要作用。需要注意的是,函数指针的类型必须与所指向的函数的类型保持一致,包括参数类型和返回值类型。

函数指针的灵活性使得我们能够编写更加通用和可扩展的代码,动态地适应不同的应用场景。