指针是C语言的核心概念,也是C语言的特色和精华所在,只有掌握了指针,才算的上真正地掌握了C语言。
0 指针简介
指针是一个值为内存地址的变量,其声明形式如下:
1 | int *pi; // pi is a pointer point to a value of type int |
可以总结指针的声明格式如下:
1 | Type *ptr; // ptr是指向type类型变量的指针 |
这里有几点需要声明:
- 指针是一个变量,这个变量存储的是一个内存地址,这个变量的类型就是指针,绝不是Type;
- Type指的是这个内存地址(指针变量)上存储的变量类型;
- 星号(*)表明声明的变量是一个指针,和解引用运算符、二元乘法运算符 有区别;
所以我认为可以用以下声明格式来更好地理解指针(个人理解),Pointer表明是指针类型;Type表示类型,如int、char;ptr就是变量。
1 | Pointer <Type> ptr; |
0.1 取址运算符(&)
取址运算符(&)用来取得其操作数的地址,如下,操作数a的类型为Type,则表达式&a的类型是Type类型的指针。取址运算符的操作数必须是在内存中可以寻址的变量。
1 | Type a; |
0.2 解引用运算符(*)
解引用运算符(*)用来取出指向地址上存储的值,其操作数必须是指针类型,如下,b的值和即为a,记住第三行的星号(*)为解引用运算符,第二行的星号(*)是声明指针。
1 | Type a = ...; |
0.2.1 不能解引用未初始化的指针!!!
如下,第二行的意思是把100这个数存储在pi指向的位置,但是pi没有被初始化,其值是一个随机地址(局部变量,在栈区,值为随机值),这是一件十分危险的事情,因为地址是随机的,所以这个操作可能会擦写数据或代码,导致程序崩溃。
1 | int *pi; |
切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配存储数据的内存,因此,在使用指针前,必须使用已分配的地址进行初始化,同理,同样不可以访问未初始化指针指向的结构体的成员。
0.3 指针运算
0.3.1 指针与整数相加减
可以用+
运算符把指针与整数相加,或整数与指针相加(加法符合交换律)。无论何种情况,整数都会和指针所指向的类型大小(字节为单位)相乘,然后把结果与初始指针(初始地址)相加,如果相加的结果超出了初始指针指向的有效范围,计算结果则是未定义的。
可以用-
从一个指针中减去一个整数(减法不符合交换律)。该整数将乘以指针所指向的类型大小(字节为单位),然后用初始化地址减去乘积。如果相减的结果超出了初始指针所指向的有效范围,计算结果则是未定义的。
在增减指针时还要注意一些问题,编译器不会检查指针是否越界,所以指针运算的时候应当注意指针是否越界。
0.3.2 指针求差
可以计算两个指针的差值,如下,p1和p2是指向同意数组不同元素的指针,注意:其相减的值表示两个元素相隔的是2个int,而不是2个字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1一个地址),C都能保证运算有效。如果指向两个不同数组,则求差可能会得到一个值,也可能会导致运行时错误。
1 |
|
结果如下:
1 | $ ./main |
0.4 NULL指针
NULL指针在C语言中表示空指针,如下第2行所示,NULL被定义为是void*类型指针,大小为0,由于C语言是弱类型语言,可以隐式转换,如int *p = NULL
或者char *p = NULL
都是合法的。
1 |
但是在C++中,NULL被定义为0,因为C++不能将void*类型指针隐式转换为其它类型的指针,为了兼容,C++中直接将NULL定义为0.
在C++中,如果继续使用NULL代替空指针,那么C++的重载特性会使得NULL存在这二义性的问题,为了防止此类问题出现,在C++11中特意引进了nullptr这一关键字以表征空指针。因此,在C++中,还是用nullptr代替NULL会更好。
1 指针与数组
数组和指针的关系十分密切,C语言实际上借助指针去描述数组的表示法,对于指向数组的指针,我们既可以使用数组描述法去使用指针,也可以使用指针描述的方式去使用数组,如下所示。
1 |
|
执行结果如下所示:
1 | $ ./main |
看以上的例子,看上去a就是一个指针,它指向了一个数组内存,但是他和指针是有区别的。指针是一个变量,其自身是有地址的,其值存的是a的地址;a是这个数组的标识,它并非声明出来的指针变量,所以其自身的地址就是其本身,也是整个数组的起始地址。但是对其本身取址,则和指针是不一样的概念了,如下。
1 |
|
执行结果如下:
1 | $ ./main |
可以看到,p和a的值都是一样的,这个地址也是数组的起始地址;&p指的是指针p这个变量的地址,而&a没有什么实际意义,但是大多数编译器将其设置为与a的值一样。
1.1 指针数组
我们将形如以下形式的定义称为指针数组,指针数组的每一个元素都是指针。
1 | Type *ptr[n]; |
在上述声明中,由于“[]”的优先级比“*”高,故ptr首先与[]结合,成为一个数组,再由Type指明这是一个Type类型的指针数组,数组中的元素都是Type类型的指针,可以看例子如下,声明一个char类型的指针数组,每个指针指向一个常量字符串,然后将其打印出来。
1 |
|
执行结果如下:
1 | $ ./main |
1.2 数组指针
数组指针是一个指针,它指向一个数组,声明的方式如下:
1 | Type (*ptr)[n]; |
ptr是一个指针,它指向一个Type类型的数组,这个数组的长度是n,这也是指针ptr的步长,即执行p+1时,会跨过n个Type类型数据的长度。数组指针和二维数组的联系密切,可以用数组指针指向一个二维数组,如下:
1 |
|
执行结果如下:
1 | $ ./main |
注意以上用法,(*p)指的是数组。
2 指针和结构体
结构体指针是指向结构体的指针,结构体访问成员时用的是.
,而指针访问成员用的是->
,如下:
1 |
|
结果如下:
1 | $ ./main |
3 指针和函数
3.1 指针传参
C语言所有的参数均是以“传值调用”的方式进行传递的,这意味者函数将获得参数值的一份拷贝。
指针传参并不违背以上原则,只是传递的参数变成了指针变量,而函数只不过拷贝了一份指针变量,但是指针变量的值是地址,所以函数操作的是这个值,即地址,从而对存储在此地址值的任何操作都会改变指针指向的参数值。所以如果我们需要被调函数回传一个值给调用函数,我们可以使用指针传参。
另外,指针传参也能大大提高函数执行的效率,在值传递时,需要把数据的一份拷贝传递入函数的形参表,并存储会栈中,函数返回后弹出栈,拷贝被删除,这就是深拷贝。而指针传递则不然,函数会在执行是直接去指针指向的地址中获取数据直接进行操作,这也就是浅拷贝。这对于参数是一些复杂的结构体的函数而言,会大大的提升效率。
指针传参的例子许多C语言的书上都会有,这里我就不赘言了,需要注意的是,对于数组而言,数组名是该数组首元素的地址,所以利用数组名传参等同于指针传参,如下例所示:
1 |
|
结果如下,值得注意的是,虽然在printb函数中,形参用的数组形式,但是其和使用指针形式的printc函数一样,都相当于利用指针传值将a的地址传递过来,但是这个指针却是在各自的函数中却是有地址的。主要记住这两种函数定义或声明的形式是等价的。
1 | $ ./main |
3.2 函数指针
在C语言中,函数指针指向函数的入口地址,声明一个函数指针的形式如下所示。函数指针在回调函数中比较常见。
1 | Type (*ptr)(Type1 *, Type2 *) |
以上声明了一个函数指针ptr,指向一个函数,函数具有两个参数为Type1*和Type2*的参数,且返回值类型为Type。
下面举一个例子,在我的博客《链表的C语言实现》里,曾经声明了一种函数指针的类型,具体如下:
1 | typedef int (*match_t)(void *, void *); |
我们实现如下的主函数:
1 |
|
可以看到,函数指针p指向函数match,其实将第37行的p指针直接替换为match传入函数亦可,和数组一样,函数名也可以理解为是指针。执行结果如下:
1 | $ ./main |
4 总结
有关指针的概念和用法还有很多,譬如多级指针等。初学C语言的时候,总会被指针这个概念搞懵,网上的很多解释也是停留在指针的用法上,很难找到一种统一的、从根本上的解释。
我觉得明白一个核心概念:指针变量也是变量,和一般的变量没有区别,只是此变量的值是其它变量的地址,甚至也可以是指针的地址(那就是二级指针咯!)。明白这个概念后,对指针的特性也就理解的更彻底,有些疑问也就迎刃而解了。
本文作者: IguoChan
本文链接: https://iguochan.github.io/2020/07/18/%E6%8C%87%E9%92%88/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!
![]()