苏子伟
指针简介
指针是C语言的一个最重要的特征,它提供了一种统一的方法,使其能访问远程的数据结构。但对C语言初学者而言,在编程过程中熟练的使用指针并不能像使用int型变量一样地轻松愉快,容易上手,往往是不得其精髓。我们知道,不论什么时候,运行一个程序A,首先都是操作系统自身的加载器把A装入内存,然后CPU才能执行。所以A程序的所有要素都会驻留在内存的某个位置。
下面我们看一段示例程序。
#include
intcmp(int first, int second)
{
return ( first > second ? first : second );
}
int main(intargc, char **argv)
{
inti = 5;
int j = 9;
returncmp(i, j);
}
首先,编译器会为变量i和j开辟内存空间,用来存储i和j的值。同时也会为函数cmp开辟空间来存放其代码。这样使得最终的可执行程序就变为了跟内存一一对应的序列。操作系统的加载器把这个可执行程序载入内存后,cpu就可以按一条条的语句顺序执行了。
既然内存空间同程序的所有要素是一一对应的,那么怎么区分各要素的存放位置呢?内存使用不同的地址存放不同的要素,如下所示。
由于变量都存放于内存地址空间,并且与地址之间是一一对应的,那么利用地址能做些什么呢?我们可以把地址存放到别的变量中,以便我们可以在以后程序的某个地方使用它。C语言有一个专门用来存储内存地址的变量,这就是指针变量,通常我们称之为指针(pointer)。它是一种变量类型,这种变量方便我们把需要操控的内存地址记忆起来。
定义指针
定义指针的运算符同乘法运算符是一样的,都用“*”表示。定义一个指针变量在语法上是简单的,同我们定义其他变量的区别是:首先规定它指向的变量类型,然后并不是立即就给出其变量的标识符,而是在变量类型同变量标识符之间插入指针运算符(星号),这样就告诉编译器这是一个指针变量。
C语言中指针可以指向任何的数据类型,包括函数。函数指针的定义是:函数返回值+(* + 函数指针变量标识符)+(函数的参数列表)。函数指针能构建出更加清晰的程序结构。编程中经常使用的指针定义就是这两种,当然有些定义可能只是语法上面有意义,但是语义上面不一定有具体的意义。例如,int *(*(*(*f)())[])()声明f是一个函数指针,该函数返回一个指针,该指针指向数组,该数组元素是指针,那些指针指向返回值类型为整型指针的函数。这样的声明可能永远也不能应用到实际的代码中。
指针和数组
数组是内存中一段连续相同类型的内存数据,这组数据的首地址以数组名字来标识。所有数组对其数据的操控都可以使用指针来实现,同理,指针指向一段内存数据时,也可以使用数组下标的方式来实现操作。
数组与指针在使用上的某些地方是非常相似的,但是数组与指针又有一些细小的区别。数组名表现为一个静态指针,也可以直接把它赋值给指针变量,但它的大小与指针通常是不同的。数组名的内涵在于其指代的实体是一种数据结构,这种数据结构就是数组。数组名可以作为参数传入一个接受参数为指针的函数内部,但是此时数组完全丢失了数组的本义,变成了完全的指针类型,其常量特性(可以作自增、自减等操作)可以被修改。并且,数组名不能再重新赋值为其他的数组名字,而指针变量是可以被重新赋值并指向一段新的内存地址的。
指针的运算
指针的运算指的是指针的--、++、-和+运算,一个指针可以加上或者减去一个整数。两个指针相减得到的是指针之间相隔的元素个数。不同的指针变量之间进行相加运算尽管在语法上是合理的,但是从语义上来讲是没有意义的。除了void型指针和函数指针以外,所有其他类型的指针都可以进行指针运算。通过指针变量的增加或减少,指针变量会指向新的内存地址。
一般来说,指针变量自身的大小在理论上是指机器的字长,但是指针变量的运算并不是按照指针变量自身的大小进行内存偏移的,而是按照指针变量指向的变量类型大小进行内存偏移的。比如,声明一个整形的指针p,假定p的地址是0x4323672,那么++p后p的值变为0x43236726。偏移的内存大小等于整形变量的内存大小4(sizeof(int))。同理,double型指针进行++运算后偏移值就是8(sizeof(double))。
指针强转
如同整形变量可以强转为浮点型变量一样,指针类型也可以通过强转变成新的指针类型,比如我们可以把整形指针强转为字符型指针。指针强转最诱人的地方就在于对内存数据进行操控就够了。指针强转使得指针对数据的操控更具有针对性,而且通过指针的默认强转可以使得函数的参数更简单,且传递的信息量是不变的。比如,void*作为参数时可以把任意的指针变量传递到函数内部进行相关的操作。
下面我们来看一个具体的例子。数据的内存布局如下图所示,首先是一个字符型数据,紧接着的是两个整形数据,最后面是三个结构体A型数据。我们需要做的就是把这些数据读出来。
我们先声明一个字符型的指针p,使其指向第一个数据的内存地址。取完第一个字符型数据后,通过p++,然后强转指针为整形指针,就可以很方便地取出整形数据,同理可取出三个结构体数据。
指针作为参数
先看一个例子,我们有两个整形变量,x的值为777,y的值为888,现在想构建一个函数用来交换两个整形变量的值,使得x的值为888,y的值为777。首先我们以传值的方式构建
voidswap_value(int Param1,int Param2)
{
int Temp = Param1;
Param1 = Param2;
Param2 = Temp;
}
我们调用函数swap_value(x,y)后,发现x、y的值并没有被交换。造成这种结果的原因是由于函数调用时,首先对传入的实参进行变量的拷贝,交换的值是形参的值,并不是实参的值。而原来的实参与拷贝后的形参变量所处的内存也不同,所以并没有交换成功。
要想实现函数内部对这两个值的交换,必须使得实参与拷贝后的形参变量所处的内存是相同的。我们知道了原理后,修正函数参数列表,以指针的方式重新构建函数如下:
voidswap_value(int*Param1,int*Param2)
{
int Temp=*Param1;
*Param1=*Param2;
*Param2=Temp;
}
这时候我们发现x、y的值被交换了。通过上面的例子可以看出,使用指针作为参数可以修改原来的变量值,使得函数实现的机能更加模块化,方便了程序的设计。
野指针
前面我们已经讨论过指针变量同内存的关系,了解了指针变量里面存放的是某个变量的内存地址,该地址可以在程序的某个位置使用,以方便我们更改或取得该变量的值。指针使得我们拥有了操控内存的利器,但同时指针也是一把双刃剑。我们必须时刻确保指针变量的值是我们意图操控的内存地址。如果指针变量的值被不受控的更改或者初始化不正确,那么我们就使用了错误的地址,从而导致程序错误,通常我们称这个导致程序错误的指针变量为野指针。由于使用了野指针而产生的程序错误大多时候是隐蔽的,难于跟踪的。野指针的产生主要是由于以下几种情况。
(1)声明了指针变量,但是没有正确的初始化就使用了该指针变量。
(2)使用指针变量之前没有对其进行安全检查。
(3)指针指向的内存变为了无效值,但没有及时对指针清零,导致程序某处引用了该指针。
(4)多个指针同时指向同一内存区域,程序某处通过某个指针释放了该内存,但是没有及时对其他的指针清零,导致程序某处进行了错误的引用。
(5)多线程时,对全局的指针变量没有进行锁处理。
多级指针
定义一级指针我们使用一个‘*,在定义多级指针时,是几级指针我们就使用几个‘*。例如,声明一个整型的二级指针(int ** ppVar;)。下面以这个二级指针为例说明一下二级指针的意义。
二级指针变量同样是保存了一个地址,这个地址就是某个一级指针变量的地址,而一级指针变量里面保存了最终需要操作的变量的地址,如下所示。
0x4323640 0x4323668
二级指针变量的值为0x4323640,就是一级指针变量pVar的地址,变量pVar的值为0x4323668,就是变量Var的地址。如果需要修改变量Var的值,我们可以直接修正**ppVar的值就可以了。
三级指针或者更多级指针的原理与二级指针的原理是相同的,只是需要索引的内存空间的深度增加了。在程序设计中,引入多级指针更多的时候并不仅仅是为了关注最后一级指针所能取得的变量,而更多的是为了使用和操控其中间的级数的内存值。比如利用二级指针作为函数的参数在某个函数内部对其分配内存,我们更想利用的是一级指针变量自身。当然,在进行程序设计时,有时我们要在可读性与语法有效性之间做出选择,在实现代码的过程中能用低级指针实现的尽量不要使用多级指针实现,这样的代码更利于维护。
小结
在C语言中指针的使用非常的广泛,有时指针是实现某个计算的唯一方法。同样的机能使用指针通常也可以获得更加高效、紧凑的代码。指针使得函数构建的机能更加的模块化,使得函数参数栈更加的短小。同时在操纵字符串的运算中,指针更加简单直观。
在大项目构建时,把函数指针同数据封装在一起能够使得代码编程面向对象的结构,使得后期代码的维护成本大大降低,代码的表现也更加具有现实意义。
当然,使指针具有这些优点的前提是能够熟练地使用它。粗心大意地使用指针变量,更容易引入程序错误。因此,合理正确地使用指针也就成为了C语言爱好者和使用者的一门必修课。endprint