作者:xuelangZF
为了说清楚什么是指针,必须弄清楚数据在内存中是如何存储的,又是如何读取的。
如果在程序中定义了一个变量,在编译时就给这个变量分配内存单元。系统根据程序中定义的变量类型,分配一定长度的空间。例如,C++编译系统一般为整型变量分配4个字节,为单精度浮点型变量分配4个字节,为字符型变量分配1个字节。内存区的每一个字节有一个编号,就是所谓的“地址”,如下图:
直接存取
方式,或直接访问方式。
间接存取(间接访问)
的方式。可以在程序中定义这样一种特殊的变量,它是专门用来存放地址的。
下图是直接访问和间接访问的示意图。
为了将数值3送到变量中,可以有两种方法:
- 直接将数3送到整型变量i所标识的单元中,如a。
- 将3送到指针变量i_pointer所指向的单元(这就是变量i所标识的单元)中,如b。
所谓指向,就是通过地址来体现的。由于通过地址能找到所需的变量单元,因此可以说,地址指向该变量单元。因此将地址形象化地称为“指针”,一个变量的地址称为该变量的指针。
指针变量
如果有一个变量是专门用来存放另一变量地址(即指针)的,则它称为指针变量。指针变量的值(即指针变量中存放的值)是地址(即指针)。
*
符号(解引用符号)表示指向。例如,i_pointer是一个指针变量,而*i_pointer
表示i_pointer所指向的变量。编译
时按变量类型分配存储空间。对指针变量来说,必须将它定义为指针类型,在32位机器上,指针类型占4个字节(因为存放的是 32 位的地址值)。基类型
,用来指定该指针变量可以指向的变量的类型。下面例子中int 指明指针变量指向整型数据,而不是指向浮点型数据。
此外,要使一个指针变量指向另一个变量,只需要把被指向的变量的地址赋给指针变量即可。
pointer_1=&a
- &*pointer_1与&a相同,即变量a的地址。(因为 & 和 * 两个运算符的优先级别相同,但按自右至左方向结合)
- *&a 和 *pointer_1 的作用是一样的,它们等价于变量a
p=NULL
; 实际上NULL代表整数0,也就是使p指向地址为0的单元,这样可以使指针不指向任何有效的单元。看下面的程序:
这里 pint指针指向 0 地址处,如果改为 int *p=1,则会报错。
指针运算
p+i
两个指针变量可以相减
:如果两个指针变量指向同一个数组的元素,则两个指针变量值之差是两个指针之间的元素个数。假如p1指向 a[0],p2指向a4,则p2-p1=(a+4)-(a)=4-0=4,但p1+p2并无实际意义。两个指针变量比较
:若两个指针指向同一个数组的元素,则可以进行比较。指向前面的元素的指针变量小于指向后面元素的指针变量。“野指针”
不是NULL指针,是指向“垃圾”内存的指针。“野指针”的成因主要有三种:
- 指针变量没有被初始化。指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
- 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
- 指针操作超越了变量的作用域范围。
指向指针的指针
指针变量也是变量,因此我们可以定义指向指针变量的指针变量,简称指向指针的指针。
单级间址
,见下图a。指向指针的指针用的是二级间址
方法,见下图b。从理论上说,间址方法可以延伸到更多的级,见下图c,但实际上在程序中很少有超过二级间址的。
如下例子:
指针与引用
指针与引用区别如下:
- 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用只不过是变量的一个别名而已。
- 指针的值可以为空,也可能指向一个不确定的内存空间,但是引用的值不能为空,并且引用在定义的时候必须初始化为特定对象;
- 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不可以改变引用对象了;
- 指针可以有多级,但是引用只能是一级;
- sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小;
指针和const
pointer to const
)不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。
const pointer
)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
也可以定义一个指向常量的常量指针(const pointer to const)。
const只对它左边的东西起作用,当const本身就是最左边的修饰符时,它才会对右边的东西起作用。有时候,情况可能会比较复杂,比如:
怎么去理解呢?先从一级指针说起吧:
- const char p: 限定变量p为只读。这样如p=2这样的赋值操作就是错误的。
- const char *p: p为一个指向char类型的指针,const只限定p指向的对象为只读。这样,p=&a或 p++等操作都是合法的,但如*p=4这样的操作就错了,因为企图改写这个已经被限定为只读属性的对象。
- char *const p: 限定此指针为只读,这样p=&a或p++等操作都是不合法的。而*p=3这样的操作合法,因为并没有限定其最终对象为只读。
- const char *const p: 两者皆限定为只读,不能改写。
有了以上的对比,再来看二级指针问题:
- const char **p:p为一个指向指针的指针,const限定其最终对象为只读,显然这最终对象也是为char类型的变量。故像**p=3这样的赋值是错误的,而像
*p=?,p++
- 这样的操作合法。
- const char * const *p:限定最终对象和p指向的指针为只读。这样
*p=?
- 的操作也是错的。
- const char * const * const p:全部限定为只读,都不可以改写。
指针和数组
通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此,像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:
一维数组:
二维数组:
常量指针
,不能对其进行赋值、++ 等操作。
关于指针数组和数组指针:
指针数组
- (array of pointers):即用于存储指针的数组,也就是数组元素都是指针
数组指针
- (a pointer to an array):即指向数组的指针
还要注意的是他们用法的区别,下面举例说明。
复杂例子分析
下面看一个比较复杂的例子,来理解指针数组和指向指针的指针:
首先理清楚c, cp, cpp 之间的指向关系。
然后用以下规则可以清晰知道指针之间的转换关系:
- 和 ++ 是属于同一个优先级的,它的结合方式是右结合。所以 *++cpp 等价于 *(++cpp)
- [] 运算符与解引用 * 可以相互转换:c[i]=*(c+i)
- 一个指针变量加/减一个整数是将该指针变量的原值(是一个地址)和它指向的变量所占用的内存单元字节数相加或相减。
所以可以得到下面的指针指向图:
[多级指针引用]
函数指针
是函数而非对象
,和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如
bool (const string&, const string&)
,要声明一个可以指向函数的指针,只需要用指针替换函数名即可。
从声明的名字开始观察,pf前面有 *,因此pf是指针,右侧是形参列表,表示 pf 指向的是函数,再观察左侧,发现函数的返回类型是 bool。
当我们把函数名作为一个值使用时,该函数自动地转换为指针。
还可以直接使用指向函数的指针调用该函数,无需提前解引用指针:
指向不同函数类型的指针间不存在转换规则,但是可以为函数指针赋一个 nullptr 或者值为 0 的整型常量表达式。
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数类型,实际上却是当成指针使用。
自动转换为指针
:
decltype
返回指向函数的指针
必须把返回类型写成指针形式
,编译器不会自动地将函数返回类型当成对应的指针类型来处理。
要声明一个返回函数指针的函数,最简单的方法是使用类型别名:
当然也可以使用下面的方式直接声明 f1:
由内向外
的顺序阅读这条声明语句。看到 f1 有 形参列表,所以 f1 是个函数;f1 前面有*, 所以 f1 返回一个指针;进一步观察,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是 int。
[指针传递]
重载函数的指针
如果定义了指向重载函数的指针,指针类型必须与重载函数中的某一个精确匹配。
类成员函数指针
具体看下面例子: