C语言 指针
指针(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 |
|
运行结果:
1 | len_a = 6, len_p = 1 |
数组到底在什么时候会转换为指针
数组名的本意是表示一组数据的集合,它和普通变量一样,都用来指代一块内存,但在使用过程中,数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身。
数组作函数参数
C语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。
这种隐式转换意味着下面三种形式的函数定义是完全等价的:
1 | void func(int *parr){ ...... } |
在函数内部,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 |
|
运行结果:
1 | Input two numbers:10 50↙ |
攻克C语言指针
指针数组、二维数组指针、函数指针等几种较为复杂的指针,它们的定义形式分别是:
1 | int *p1[6]; //指针数组 |
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() 的函数。 |