知识点:
传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。这意味着只需要访问这个对象的函数才有访问权限,而且也不需要复制对象。
要在某个函数中修改数据,需要用指针传递数据。通过传递一个指向常量的指针,可以使用指针传递数据并禁止其被修改。当数据是需要被修改的指针时,我们就传递指针的指针。
传递参数(包括指针)时,传递的是它们的值。也就是说,传递给函数的是参数值的一个副本。当涉及大型数据结构时,传递参数的指针会更高效。比如说一个表示雇员的大型结构体,如果我们把整个结构体传递给函数,那么需要复制结构体的所有字节,这样会导致程序运行变慢,栈帧也会占用过多内存。传递对象的指针意味着不需要复制对象,但可以通过指针访问对象。
一、用指针传递数据
用指针来传递数据的一个主要原因是函数可以修改数据。下面的代码段实现了一个交换函数,可以交换其参数所引用的值。这是很多排序算法中常用操作。我们在这里用整数指针,通过解引用它们来实现交换。
#include <stdio.h> void swapWidthPointers(int *pnum1, int *pnum2) { int tmp; tmp = *pnum1; *pnum1 = *pnum2; *pnum2 = tmp; } int main(int argc, char **argv) { int n1 = 5; int n2 = 10; swapWidthPointers(&n1, &n2); printf("%d\n", n1); printf("%d\n", n2); return 0; }
输出:
10 5
指针 pnum1 和 pnum2 在交换操作中被解引用,结果是修改了 n1 和 n2 的值。图 3-3 说明了内存如何组织,左图表示 swap 函数开始时程序栈的状态,而右图则是函数返回前的状态。
二、用值传递数据
如果不通过指针传递参数,那么交换就不会发生。下面的函数通过值来传递两个整数:
#include <stdio.h> void swap(int num1, int num2) { int tmp; tmp = num1; num1 = num2; num2 = tmp; } int main(int argc, char **argv) { int n1 = 5; int n2 = 10; swap(n1, n2); printf("%d\n", n1); printf("%d\n", n2); return 0; }
输出:
5 10
然而,这样并没有实现交换,因为整数是通过值而不是指针来传递的。num1和num2中保存的只是实参的副本。修改num1,实参n1不会变化。修改形参不会影响实参。图 3-4 说明了形参的内存分配。
三、传递指向常量的指针
传递指向常量的指针是C中常用的技术,效率很高,因为我们只传了数据的地址,能避免某些情况下复制大量内存。不过,如果只是传递指针,数据就能被修改。如果不希望数据被修改,就要传递指向常量的指针。
在本例中,我们传递一个指向整数常量的指针和一个指向整数的指针。在函数内,我们不能修改通过指向常量的指针传进来的值:
#include <stdio.h> void passingAddressOfConstants(const int *num1, int *num2) { *num2 = *num1; } int main(int argc, char **argv) { const int limit = 100; int result = 5; passingAddressOfConstants(&limit, &result); printf("%d\n", limit); printf("%d\n", result); return 0; }
输出:
100 100
这样不会产生语法错误,函数会把 100 赋给 result 变量。在下面这个版本的函数中,我们试图修改两个被引用的整数:
void passingAddressOfConstants(const int *num1, int *num2) { *num1 = 100; *num2 = 200; }
如果我们把 limit 常量传递给函数的两个参数就会导致问题:
const int limit = 100; passingAddressOfConstants(&limit, &limit);
这样会产生一个语法错误,抱怨第二个形参和实参的类型不匹配。此外,它还会抱怨我们试图修改第一个参数所引用的常量。
/tmp/tmp.DGyJGxDCDv/src/test_1.c: 在函数‘passingAddressOfConstants’中: /tmp/tmp.DGyJGxDCDv/src/test_1.c:4: 错误:向只读位置‘*num1’赋值 /tmp/tmp.DGyJGxDCDv/src/test_1.c: 在函数‘main’中: /tmp/tmp.DGyJGxDCDv/src/test_1.c:12: 警告:传递‘passingAddressOfConstants’的第 2 个实参时丢弃了指针目标类型的限定 /tmp/tmp.DGyJGxDCDv/src/test_1.c:3: 附注:需要类型‘int *’,但实参的类型为‘const int *’
该函数期待一个整数指针,但是传进来的却是指向整数常量的指针。我们不能把一个整数常量的地址传递一个指向常量的指针,因为这样会允许修改常量。
像下面这样试图传递一个整数字面量的地址也会产生语法错误:
passingAddressOfConstants(&23, &23);
这种情况的错误信息会指出取地址操作符的操作数需要的是一个左值。
四、返回指针
返回指针很容易,只要返回的类型是某种数据类型的指针即可。从函数返回对象时经常用到以下两种技术:
- 使用 malloc 在函数内部分配内存并返回其地址。调用者负责释放返回的内存;
- 传递一个对象给函数并让函数修改它。这样分配和释放对象的内存都是调用者的责任;
首先,我们介绍用 malloc 这类函数来分配返回的内存,随后的示例中我们返回一个局部对象的指针(不推荐这种方法)。
在下面的例子中,我们定义一个函数,为其传递一个整数数组的长度和一个值来初始化每一个元素。函数为整数数组分配内存,用传入的值进行初始化,然后返回数组地址:
#include <stdio.h> #include <stdlib.h> int *allocateArray(int size, int value) { int *arr = malloc(size * sizeof(int)); for (int i = 0; i < size; i++) { arr[i] = value; } return arr; } int main(int argc, char **argv) { int *vector = allocateArray(5, 45); for (int i = 0; i < 5; i++) { printf("%d\n", vector[i]); } free(vector); return 0; }
图 3-5 说明了这个函数的内存分配。左图显示 return 语句执行前的程序状态,右图显示函数返回后的程序状态。vector 变量包含了函数内分配的内存的地址。当函数终止时,arr变量也会消失,但是指针所引用的内存还在,这部分内存最终需要释放。
尽管上例可以正常工作,但从函数返回指针时可能存在几个潜在的问题,包括:
- 返回未初始化的指针;
- 返回指向无效地址的指针;
- 返回局部变量的指针;
- 返回指针,但是没有释放内存;
最后一个问题的典型代表就是 allocateArray 函数。从函数返回动态分配的内存,意味着函数的调用者有责任释放内存。看一下这个例子:
int *vector = allocateArray(5, 45); ... free(vector);
最终,我们必须在用完后释放内存,否则就会产生内存泄漏。
五、局部数据指针
如果你不理解程序栈是如何工作的,就很容易犯返回指向局部数据的指针的错误。在下面的例子中,我们重写了 allocateArray 函数。这次我们不为数组动态分配内存,而是用了一个局部数组:
int *allocateArray(int size, int value) { int arr[size]; for (int i = 0; i < size; i++) { arr[i] = value; } return arr; }
不幸的是,一旦函数返回,返回的数组地址也就无效了,因为函数的栈帧从栈中弹出了。尽管每个数组元素仍然可能包含 45,但如果调用另一个函数,就可能覆写这些值。下面的代码段对此做了演示,重复调用 printf 函数导致数组损坏:
int *vector = allocateArray(5, 45); for (int i = 0; i < 5; i++) { printf("%d\n", vector[i]); }
图 3-6 说明了发生这种情况时内存的分配状态。虚线框表示其他的栈帧(比如 printf 函数用到的),可能会被推到程序栈上,从而损坏数组特有的内存。栈帧的实际内容取决于实现。
还有一种方法:把 arr 变量声明为 static 。这样会把变量的作用域限制在函数内部,但是分配在栈帧外面,避免其他函数覆写变量值。
int *allocateArray(int size, int value) { static int arr[5]; ... }
不过这种方法并不一定总是有用。每次调用 allocateArray 函数都会重复利用这个数组。这样相当于每次都把上一次调用的结果覆盖掉。此外,静态数组必须声明为固定长度,这样会限制函数处理变长数组的能力。
如果函数只是返回一个可能的值,而且共享这些值也不会有什么坏处,那么它可以维护一个这些值的列表,然后返回合适的值。如果我们需要返回状态类型的消息,比如不大可能修改的错误码,这么做就很有用。
六、传递空指针
下面这个版本的 allocateArray 函数传递了一个数组指针、数组长度和用来初始化数组元素的值。返回指针只是为了方便。这个版本的函数不会分配内存,但后面的版本会分配:
int *allocateArray(int *arr, int size, int value) { if (arr != NULL) { for (int i = 0; i < size; i++) { arr[i] = value; } } return arr; }
将指针传递给函数时,使用之前先判断它是否为空是个好习惯。
int *vector = malloc(5 * sizeof(int)); allocateArray(vector, 5, 45);
如果指针是 NULL,那么什么都不会发生,程序继续执行,不会非正常终止。
七、传递指针的指针
将指针传递给函数时,传递的是值。如果我们想修改原指针而不是指针的副本,就需要传递指针的指针。在下例中,我们传递了一个整数数组的指针,为该数组分配内存并将其初始化。函数会用第一个参数返回分配的内存。在函数中,我们先分配内存,然后初始化。所分配的内存地址应该被赋给一个整数指针。
#include <stdio.h> #include <stdlib.h> void allocateArray(int **arr, int size, int value) { *arr = (int *) malloc(size * sizeof(int)); if (*arr != NULL) { for (int i = 0; i < size; i++) { *(*arr + i) = value; } } } int main(int argc, char **argv) { int *vector = NULL; allocateArray(&vector, 5, 45); for (int i = 0; i < 5; i++) { printf("%d\n", vector[i]); } free(vector); return 0; }
allocateArray 的第一个参数以整数指针的指针的形式传递。当我们调用这个函数时,需要传递这种类型的值。这是通过传递 vector 地址做到的。malloc 返回的地址被赋给 arr。解引整数指针的指针得到的是整数指针。因为这是 vector 的地址,所以我们修改了 vector。
内存分配说明如图 3-7 所示。左图显示 malloc 返回且初始化数组后的栈状态。类似地,右图显示函数返回后的栈状态。
下面这个版本的函数,说明了为什么只传递一个指针不会起作用:
void allocateArray(int *arr, int size, int value) { arr = (int *) malloc(size * sizeof(int)); if (arr != NULL) { for (int i = 0; i < size; i++) { arr[i] = value; } } }
下面的代码段,说明了如何使用这个函数:
int *vector = NULL; allocateArray(&vector, 5, 45); printf("%p\n", vector);
运行后会看到程序打印出 0x0,因为将 vector 传递给函数时,它的值被复制到了参数 arr 中,修改 arr 对 vector 没有影响。但函数返回后,没有将存储在 arr 中的值复制到 vector 中。图 3-8 说明了内存分配情况:
- 左图显示 arr 被赋值之前的内存状态;
- 中图显示 allocateArray 函数中的 malloc 函数执行且初始化数组后的内存状态,arr 变量被修改为指向堆中的某个新位置;
- 右图显示函数返回后程序栈的状态。
此外,这里有内存泄漏,因为我们无法再访问地址 600 处的内存块了。
八、实现自己的 free 函数
由于 free 函数存在一些问题,因而某些程序员创建了自己的 free 函数。free 函数不会检查传入的指针是否是 NULL,也不会在返回前把指针置为 NULL。释放指针之后将其置为 NULL 是个好习惯。
我们给出下面这个 free 函数的实现,可以给指针赋 NULL。此处我们需要给它传递一个指针的指针:
void saferFree(void **pp) { if (pp != NULL && *pp != NULL) { free(*pp); *pp = NULL; } }
safeFree 函数调用实际释放内存的 free 函数,前者的参数声明为 void 指针的指针。使用指针的指针允许我们修改传入的指针,而使用 void 类型则可以传入所有类型的指针。不过,如果调用这个函数时,没有显式地把指针类型转换为 void 会产生警告,执行显式转换就不会有警告。
下面的这个 safeFree 宏调用 saferFree 函数,执行类型转换,并使用了取地址操作符,这样就省去了函数使用者类型转换和传递指针的地址:
#define safe_free(p) saferFree((void **)&(p))
应用举例:
int main(int argc, char **argv) { int *pi; pi = (int *) malloc(sizeof(int)); *pi = 5; printf("Before: %p\n", pi); safeFree(pi); printf("After: %p\n", pi); safeFree(pi); return 0; }
假设 malloc 返回的内存位于地址 1000,那么这段代码的输出是 1000 和 0。第二次调用 safeFree 宏给它传递 NULL 值不会导致程序终止,因为 saferFree 函数检测到这种情况并忽略了这个操作。
摘自:《深入理解C指针》陈晓亮 译