指针(pointer)很多人理解成地址(address)。其实不然,指针就是地址,地址就是指针;指针变量是一个变量,它保存了基本类型变量的地址。就好比你和别人说你家地址是:XX省XX市XX区XX镇XX小区XX单元XX号,你可能觉得很准确,但是,地址更加准确:XX省XX市XX区XX镇XX小区XX单元XX号+只有这一间,XX省XX市XX区XX镇XX小区XX单元XX号+连排几家。所以本质山指针描述的信息更多,只是其中包含了地址信息而已。

指针变量

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。

定义指针变量:

1
datatype *name;

或者

1
datatype *name = value;

指针变量的运算

数组指针

一个指针指向了数组,我们就称它为数组指针(Array Pointer)。

二维数组指针

字符串指针

指针变量作为函数参数

指针作为函数返回值

二级指针(指向指针的指针)详解

空指针NULL以及void指针

空指针 NULL

NULL 是“零值、等于零”的意思,在C语言中表示空指针。

NULL 是在stdio.h中定义的一个宏,它的具体内容为:

#define NULL ((void *)0)

(void _)0表示把数值 0 强制转换为void _类型,最外层的( )把宏定义的内容括起来,防止发生歧义。从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。

void 指针

对于空指针 NULL 的宏定义内容,上面只是对((void _)0)作了粗略的介绍,这里重点说一下void _的含义。void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。

也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。

数组与指针并不等价

数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针,前面我们已经强调过了,这里不妨再来演示一下:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int a[6] = {0, 1, 2, 3, 4, 5};
int *p = a;
int len_a = sizeof(a) / sizeof(int);
int len_p = sizeof(p) / sizeof(int);
printf("len_a = %d, len_p = %d\n", len_a, len_p);
return 0;
}

运行结果:

1
len_a = 6, len_p = 1

数组到底在什么时候会转换为指针

数组名的本意是表示一组数据的集合,它和普通变量一样,都用来指代一块内存,但在使用过程中,数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身。

数组作函数参数

C语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。

这种隐式转换意味着下面三种形式的函数定义是完全等价的:

1
2
3
void func(int *parr){ ...... }
void func(int arr[]){ ...... }
void func(int arr[5]){ ...... }

在函数内部,arr 会被转换成一个指针变量,编译器为 arr 分配 4 个字节的内存,用 sizeof(arr) 求得的是指针变量的长度,而不是数组长度。

把作为形参的数组和指针等同起来是出于效率方面的考虑。数组是若干类型相同的数据的集合,数据的数目没有限制,可能只有几个,也可能成千上万,如果要传递整个数组,无论在时间还是内存空间上的开销都可能非常大。而且绝大部分情况下,我们其实并不需要整个数组的拷贝,我们只想告诉函数在那一时刻对哪个特定的数组感兴趣。

指针数组

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

1
dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为:

1
dataType *(arrayName[length]);

括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

函数指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

1
returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerName 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

注意( )的优先级高于_,第一个括号不能省略,如果写作returnType _pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *。

用指针来实现对函数的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
return a>b ? a : b;
}
int main(){
int x, y, maxval;
//定义函数指针
int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
printf("Input two numbers:");
scanf("%d %d", &x, &y);
maxval = (*pmax)(x, y);
printf("Max value: %d\n", maxval);
return 0;
}

运行结果:

1
2
Input two numbers:10 50
Max value: 50

攻克C语言指针

指针数组、二维数组指针、函数指针等几种较为复杂的指针,它们的定义形式分别是:

1
2
3
4
int *p1[6];  //指针数组
int *(p2[6]); //指针数组,和上面的形式等价
int (*p3)[6]; //二维数组指针
int (*p4)(int, int); //函数指针

C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!

对于初学者,有几种运算符的优先级非常容易混淆,它们的优先级从高到低依次是:

  • 定义中被括号( )括起来的那部分。
  • 后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
  • 前缀操作符:星号*表示“指向xxx的指针”。

int *p1[6];

从 p1 开始理解,它的左边是 _,右边是 [ ],[ ] 的优先级高于 _,所以编译器先解析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int _,它用来说明数组元素的类型。从整体上讲,p1 是一个拥有 6 个 int _ 元素的数组,也即指针数组。

int (*p3)[6];

从 p3 开始理解,( ) 的优先级最高,编译器先解析(*p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是一个拥有 6 个元素的一维数组。从整体上讲,p3 是一个指向拥有 6 个 int 元素数组的指针,也即二维数组指针。

为了能够通过指针来遍历数组元素,在定义数组指针时需要进行降维处理,例如三维数组指针实际指向的数据类型是二维数组,二维数组指针实际指向的数据类型是一维数组,一维数组指针实际指向的是一个基本类型;在表达式中,数组名也会进行同样的转换(下降一维)。

int (*p4)(int, int);

从 p4 开始理解,( ) 的优先级最高,编译器先解析(*p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针。

char ( c[10])(int **p);

c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。

int (((_pfunc)(int _))[5])(int *);

pfunc 是一个函数指针(蓝色部分),该函数的返回值是一个指针,它指向一个指针数组(红色部分),指针数组中的指针指向原型为int func(int *);的函数(橘黄色部分)。

C语言野指针

如果一个指针指向的内存没有访问权限,或者指向一块已经释放掉的内存,那么就无法对该指针进行操作,这样的指针称为野指针(Wild Pointer)

指向没有访问权限的内存

指向释放掉的内存

规避野指针

要想规避野指针,就要养成良好的编程习惯:

1.
指针变量如果暂时不需要赋值,一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

2.
当指针指向的内存被释放掉时,要将指针的值设置为 NULL,因为 free() 只是释放掉了内存,并为改变指针的值。

总结

|
定  义 |

含  义 |
| — | — |
|
int *p; |
p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。 |
|
int **p; |
p 为二级指针,指向 int * 类型的数据。 |
|
int *p[n]; |
p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]); |
|
int (*p)[n]; |
p 为二维数组指针。 |
|
int *p(); |
p 是一个函数,它的返回值类型为 int *。 |
|
int (*p)(); |
p 是一个函数指针,指向原型为 int func() 的函数。 |