本次将详细介绍指针,是C语言中的一 个重要部分。
在程序中,指针提供强大而灵活的方法来操纵数据。
本次将介绍以下内容:
●指针的定义
●指针的用途
●如何声明和初始化指针
●如何将指针用于简单变量和数组
●如何用指针给函数传递数组
使用指针有两方面的优势:
其一,用指针能更好地完成某些任务;
其二,有些任务只能用指针才能完成。
一.什么是指针:
在学习什么是指针之前,必须先了解计算机如何在内存中储存信息的基本知识。
下面,将简要地介绍计算机的存储器。
1.1 计算机的内存
计算机的内存(RAM) 由数百万个顺序存储位置组成,每个位置都有唯一的地址。
计算机的内存地址范围从0开始至最大值(取决于内存的数量)。
运行计算机时,操作系统要使用一些内存。
运行程序时,程序的代码(执行该程序中不同任务的机器语言指令)和数据(该程序使用的信息)也要使用一些内存。
这里讨论的是储存程序数据的内存。
在C程序中声明一个变量时,编译器会预留一个内存位置来储存该变量,此位置有唯一的地址。
编译器把该地址与变量名相关联。当程序使用该变量名时,将自动访问正确的内存位置。
虽然程序使用了该位置的地址,但是对用户是隐藏的,不必关心它。
下面用图来帮助你理解。程序声明了一个名为rate的变量,并将其初始化为100。
编译器已经在内存中将地址为1004的位置留给了该变量,并将变量名rate与地址1004 相关联。
1.2 创建指针
注意,rate变量或任何其他变量的地址都是一个数字(类似于C语言的其他数字)。
如果知道一个变量的地址,便可创建第2个变量来储存第1个变量的地址。
第一步,先声明一个变量(命名为p_rate)储存rate变量的地址。
此时,p_rate尚未初始化。编译器已经为p_rate 分配了储存空间,但是它的值待定,如图所示。
第二步,把rate变量的地址储存到p_rate 变量中。
由于现在p_rate 变量中储存的是rate变量的地址,因此p_rate 指明了rate被储存在内存中的具体位置。
用C语言的说法是: P_rate指向rate,或者p_rate是指向rate的指针。如图所示。
综上所述,指针是储存其他变量地址的变量。接下来,我们进一步学习如何在C程序中使用指针。
二.指针和简单变量:
在上面的示例中,指针变量指向一个简单(即,非数组)变量。
本次介绍如何创建并使用指向简单变量的指针。
2.1 声明指针:
指针是一个数值变量,和所有变量类似,必须先声明才能使用。
指针变量名遵循与其他变量名相同的命名规则,而且指针变量名必须唯一。
指针的声明形式如下:
类型名可以是任意C语言的变量类型,它指明该指针所指向变量的类型。
星号( * )是间接运算符,表明指针名是一个指向类型名类型的指针,不是类型名类型的变量。
下面是一些示例:
*号可用作间接运算符和乘法运算符。
不用担心编译器会混淆两者。编译器通过星号上下文提供的信息,完全清楚该星号是间接运算符还是乘法运算符。
2.2初始化指针:
现在,你已经声明了一个指针,可以用它来做什么?
它在指向某些内容之前什么也做不了。与普通变量类似,使用未初始化的指针会导致无法预料的结果和潜在的危险。
没有储存变量地址的指针是没用的。
变量的地址不会自动“变”进指针中,必须在程序中使用取址运算符(& )获得变量的地址,然后将其存入指针才行。
把取址运算符放在变量名前,便会返回该变量的地址。因此,以下面的形式初始化指针:
参看上图所示的例子,要让p_rate变量指向rate变量,应该这样写:
该语句把rate的地址赋值给p_rate 。初始化之前,p_rate未指向任何内容;初始化之后,p_rate是指向rate的指针。
2.3使用指针:
现在,你已经学会声明和初始化指针,一定很想知道如何使用它。
这里,又要用到间接运算符(* )。把*放在指针名前,该指针便引用它所指向的变量。
在上一个例子中,已经初始化了p_rate指针,使其指向rate变量。如果写*p_rate ,该指针变量则引用rate变量的内容。
因此,要打印rate的值(该例中,rate的值是100 ),可以这样写:
也可以这样写
在C语言中,以上两条语句是等价的。
通过变量名访问变量的内容,称为直接访问 ;
通过指向变量的指针访问变量的内容,称为间接访问或间接取值。
下面图解释了将间接运算符放在指针名前,引用的是指针所指向变量的值。
仔细思考一下上述内容。指针是C语言不可或缺的部分,必须要理解指针。许多人都不太明白指针,如果你理解起来也有困难,别担心。也许下面总结的内容会让你更好的进行理解。
假设声明一个名为ptr的指针,已将其初始化为指向var变量,以下的说法都正确:
●*ptr和var都引用var的内容(即,程序储存在该位置的任何值) ;
●*ptr和&var都引用var的地址。
因此,不带间接运算符的指针名访问指针本身储存的值(即,指针所指向的变量的地址)。
下面程序清单演示了指针的基本用法。请输入、编译并运行这个程序。
输入:
输出:
解析:
注意:要理解什么是指针以及指针工作的原理。
要把地址赋给指针之前,不要使用未初始化的指针否则可能会凉凉
三.指针和变量类型:
前面的讨论都没有考虑不同类型的变量占用不同数量的内存。
对于较常用的计算机操作系统,一个short类型的变量占2字节,一个float类型的变量占4字节,等等。
内存中的每个字节都有唯一 的地址,因此,多字节变量实际上占用了多个地址。
那么,指针如何储存多字节变量的地址?
实际上,变量的地址是它所占用字节的首地址(最低位的地址)。
下面声明并初始化3个变量来说明:
这些变量都储存在内存中,如下图所示。图中,short类型的变量占2字节,char 类型的变量占1字节,float类型的变量占4字节。
接下来,声明并初始化3个指针分别指向这3个变量:
指针中储存的是它所指向变量的第1个字节地址。
因此,p_vshort 的值是1000,
p_vchar的值是1003,
p_vfloat的值是1006。
因为每个指针都被声明指向某种类型的变量
因此编译器知道:指向short类型变量的指针指向2字节中第1个地址;
指向float类型变量的指针指向4字节中的第1个地址,等等。
如下图所示:
如图和上图所示,3个变量之间都有一些空的内存存储位置。
这样做是为了方便理解。实际操作时,大多数C编译器都会把这3个变量储存在相邻的内存位置,不会像图中所示那样。
四.指针和数组:
在C语言中,指针和数组之间的关系很特殊。
下面将详细讲解其中的原理。
4.1数组名
数组名(不带方括号)是指向数组第1个元素(即,首元素)的指针。
因此,如果声明一个数组data[],那么data中储存的是数组第1个元素的地址。
你可能会问:“等等, 不需要取址运算符来获取地址?”是的,不需要。
当然,通过&data[0]表达式来获取数组首元素的地址也没问题。在C语言中,(data == &data[0]) 为真。
数组名不仅是指向数组的指针,而且是指针常量,它在程序的执行期间保持不变且不能被改变。
这很好理解:如果能改变它的值,它就会指向别处,而不是指向原来的数组(该数组位于内存中某固定的位置)。
但是,可以声明一个指针变量并将其初始化以指向该数组。
例如,下面的代码声明并初始化指针变量p_array ,把array数组首元素的地址储存在p_array中
因为p_array 是一个指针变量,所以可修改它的值让其指向别处。
与数组名(array )不同,p_array 并未被锁定指向array[]的第1个元素。
因此,可以改变它的值,使其指向array[] 的其他元素。
如何做?
首先,要了解一下如何在内存中储存数组元素。
4.2:储存数组元素
在前面笔记中介绍过,数组元素按顺序被储存在内存位置上。
第1个元素在最低位地址上,随后的数组元素(那些数组下标大于0的元素)被依次储存在较高位地址上。
能储存到多高位,取决于数组的数据类型(char、int、float等)。
以short类型的数组为例。一个short 类型的变量占用2字节的内存。
因此,每个数组元素与它前一个元素的间隔是2字节,每个数组元素的地址都比它上一个元素的地址高2。
对于float类型而言,一个float类型的变量占用4字节的内存,每个元素与它前一个元素的间隔是4字节,其地址比它上一个元素的地址高4。
下面图解释了如何在内存中储存不同类型的数组(分别是,包含6个short类型元素的数组和包含3个float类型元素的数组),以及数组中各元素地址之间的关系。
从图7可知,下列关系为真:
第1行,不带数组方括号的x是数组首元素的地址(&x[0] )。
第2行,x[0]位于1000 的地址上,可以这样读:“ 数组x的首元素的地址是1000”。
第3行表示第2个元素(在数组中的下标是1 )的地址是1002,
如上图所示。实际上,第4、5、6行与第1、2、3行几乎分别相同。
区别在于,在short类型的数组x中,每个元素占2字节,而在float类型的数组expenses 中,每个元素占4字节。
如何使用指针访问这些连续的数组元素?
从上述例子可知,指针的值(即指针中储存的地址)以2递增就能访问short类型数组连续的元素,以4递增指针就能访问float类型数组连续的元素。可将其概括为:要访问某种数据类型数组连续的元素,必须以sizeof(数据类型)递增指针的值。第3节中学过sizeof()运算符以字节为单位返回C语言数据类型的大小。
程序清单中通过声明short、float、double类型的数组并依次显示数组元素的地址,演示了不同类型数组的元素和地址之间的关系。
输入:
输出:
解析:
4.3 指针算术:
假设有一个指向数组第1个元素的指针,该指针必须以该数组中储存的数据类型大小来递增。
如何通过指针表示法访问数组元素?
答案是:指针算术
指针算术非常简单。只需关注两种指针运算:递增和递减。
(1)指针递增
递增指针时,递增的是指针的值。
例如,将指针递增1,指针算术将自动地递增指针的值,使其指向数组的下一个元素。
也就是说,C编译器(查看指针声明)知道指针所指向的数据类型,并以数据类型的大小递增指针中储存的地址。
假设ptr_to_short 是指向short类型数组中某个元素的指针变量,如果执行下面的语句:
ptr_to_short的值将递增short类型的大小(通常是2字节),而且ptr_to_short现在指向该数组的下一个元素。
同理,如果ptr_to_float指向float类型数组中某个元素,执行下面的语句:
ptr_to_float的值将递增float类型的大小(通常是4字节)。
递增大于1的值也是如此。
如果给指针加上n,那么C编译器将递增该指针的值是n与相应数据类型大小的乘积(即,如果指针加上n,则该指针指向后续第n个元素)。因此执行下面的语句:
ptr_to_short 的值将递增8 ( 假设short是2字节),即该指针指向后续的第4个元素。同理,如果执行下面的语句:
ptr_to_float的值将递增40 (假设float是4字节),即该指针指向后续的第10个元素。
(2)指针递减:
指针递减的原理和指针递增类似。
递减实际上是递增的特殊情况,即增加的值为负。如果通过--或-=运算符递减指针,指针算术将自动根据数组元素的大小来调整。
输入:
程序ptr_math.c:使用指针算术和指针表示法访问数组元素
输出:
解析:
(3)其他指针运算:
使用指针时,除了递增和递减,还会用到求差,即两个指针相减。
如果有两个指针指向相同数组的不同元素,便可将两指针相减得出它们的间隔。
再次提醒注意,指针算术会根据指针所指向数组元素的个数自动伸缩。
因此,如果ptr1和ptr2指向(任意类型)数组的两个元素,下面的表达式可以得出两个元素相隔多远:
除此之外,当两个指针都指向相同数组时,可以对这两个指针进行比较操作。
注意,只有在这种情况下,==、!=、>、<、>=、<=这些关系运算符才能正常工作。
较低数组元素(即,其数组下标较小的元素)比较高数组元素的地址低。
因此,假设ptr1 和ptr2都指向相同数组的不同元素,下面的表达式:
如果ptr1指向的元素在ptr2指向的元素之前,以上关系成立(即,为真)。
许多对普通变量执行的算术运算( 如乘法、除法),都不能用在指针上。C编译器不允许对指针执行这些操作。
例如,假设ptr是一个指针,如果执行下面的语句,编译器会报错:
表格总结了可用于指针的所有操作。这些操作也都介绍过。
指针运算表
运算 | 描述 |
赋值 | 可以给指针赋值,这个值必须是通过取址运算符(&) 获得的地址或者是指针常量(数组名) 中储存的地址。 |
间接取值 | 间接运算符( * )返回储存在指针所指向位置上的值(这通常称为解引用) |
取址 | 可以使用取址运算符找到指针的地址,因此,有指向指针的指针。 |
递增 | 可以给指针加一个整数,使其指向不同的内存位置 |
递减 | 可以给指针减去一个整数,使其指向不同的内存位置 |
求差 | 将两个指针相减,得出两者的间距 |
比较 | 只有指向相同数组两个指针才能进行比较 |
五.指针的注意事项:
如果编写的程序中要用到指针,千万不要在赋值表达式语句的左侧使用未初始化的指针。
例如,下面声明 了一个指向int类型变量的指针:
该指针尚未被初始化,因此它未指向任何内容。更确切地说,该指针并未指向任何已知内容。
未初始化的指针中有某些值,你并不知道是什么。大多数情况下是零。如果在赋值表达式语句中使用未初始化的指针,
如:
12被储存在ptr指向的地址上。
该地址可以是内存中的任意位置一可能是储存操作系统或其他程序代码的地方。
将12储存于此会擦写某些重要的信息,这可能导致奇怪的程序错误,甚至整个系统崩溃。
在赋值表达式语句左侧使用未初始化的指针非常危险。
在程序的其他地方使用未初始化的指针也会导致其他错误(尽管这些错误没那么严重)。
必须自己多留心,不要奢望编译器能帮你检查出来。
记住,对指针做加法或减法时,编译器是根据指针指向的数据类型大小来改变指针的值,不是直接把指针的值与加上或减去的值做加法或减法(除非指针指向1字节的字符,那么指针加1,则是给指针的值加1)。
要熟悉你的计算机中变量类型的大小。在操纵指针和内存时必须要知道变量的大小。
不要用指针做乘法、除法运算。但是,可以用指针做加法(递增)和减法(递减)运算。
不要递增或递减数组变量。但是,可以将数组的首地址赋值给指针,然后递增该指针。
六.数组下标表示法和指针:
数组名是一个指向该数组首元素的指针。
因此,可以使用间接运算符访问数组的第1个元素。
如果声明了一个数组array[],那么,array表达式就是该数组的第1个元素, (array + 1)则是该数组的第2个元素,以此类推。
如果推广至整个数组,下面的关系都为真:
这说明了数组下标表示法与数组指针表示法等价,可以在程序中任意互换这两种表示法。
C编译器将其看作是使用指针访问数组数据的不同方式。
七.给函数传递数组:
本次已经讨论了C语言中指针和数组之间的特殊关系,在将数组传递给函数时会用得上。
只有用指针才能将数组传递给函数。
实参是主调函数(或程序)传递给被调函数的一个值。
这个值可以是int、float 或任意简单的数据类型,但必须是单独的数值——可以是单个数组元素,但不能是整个数组。
那么,如果要给函数传递整个数组怎么办?
别忘了指向数组的指针,该指针就是一个数值(即,数组首元素的地址)。
如果将该值传递给一个函数,该函数就知道了待传递数组的地址,便可用指针表示法访问该数组的其他元素。
考虑另一个问题。如果你想编写一个能处理不同大小数组的函数
例如,用该函数找出整型数组中最大的元素。这样的函数如果只能处理固定大小的数组就用处不大。
如果只把数组的地址传递给函数,该函数如何知道数组的大小?
记住,传递给函数的是指向数组首元素的指针。该指针的值可能是包含10个元素的数组首地址,也可能是包含10000个元素的数组首地址。
有两种方法可以让函数知道数组的大小。
可以在数组的最后一个元素中储存一个特殊的值作为数组末尾的标志。函数在处理数组时,会查看每个元素的值。当函数发现这个特殊的值时,就意味着到达数组的末尾。这个方法的缺点是,必须预留一个值作为数组末端的指示符,在储存实际数据时不太灵活。
另一个方法相对灵活和直接,也是我采用的方法:将数组大小作为实参传递给函数。数组大小就是一个简单的int值。因此,需要给函数传递两个实参:一个是指向数组首元素的指针,一个是指定该数组元素个数的整数。
下面程序清单接受用户提供的一系列值,并将其储存在数组中。然后调用largest()函数,并将数组(指向该数组的指针和数组大小)传递给它。该函数在数组中找出最大值并将其返回主调函数。
输入:
输出:
解析:
下面程序清单用另一种方式将数组传递给函数。
输出:
解析:
八:小结
本次介绍了C语言的重点内容一一指针。 指针是储存其他变量地址的变量。指针“指向”它所储存的地址上的变量。学习指针要用到两个运算符:取址运算符(& )和间接运算符(* )。把取址运算符放在变量名前,返回变量的地址。把间接运算符放在指针名前,返回该指针指向变量的内容。
指针和数组有特别的关系。数组名是指向该数组首元素的指针。通过指针的运算特性,可以很方便地使用指针来访问数组元素。实际上,数组下标表示法就是指针表示法的特殊形式。
本次还介绍了通过传递指向数组的指针来将数组作为参数传递给函数。函数一旦知道数组的地址和数组的元素个数,便可使用指针表示法或下标表示法访问数组元素。
问答题
1:为什么在C语言中,指针很重要?
通过指针能更好地控制数据。当使用函数时,指针能让你改变被传递变量的值(无论这些值在哪里)。
2:编译器如何知道*指的是乘法、解引用还是声明指针?
编译器根据星号出现的上下文来确定是哪一种用法。如果声明的开始是变量的类型,编译器就假定该星号用于声明指针。如果星号与已声明为指针的变量一起使用,却不在变量声明中,编译器则将该星号假定为解引用。如果星号出现在数学表达式中,但是没有和指针变量一起使用,编译器则将其假定为乘法运算符。
3:如果对指针使用取址运算符会怎样?
这样做得到的是指针变量的地址。记住,指针也是变量,只不过它储存的是它所指向变量的地址。
4:同一个变量是否都储存在相同的位置?
不是。每次运行程序时,其中的变量都储存在不同的地址上。千万不要把常量地址赋给指针。
5:确定变量的地址要使用什么运算符?
取址运算符是&
6:通过指针确定它所指向位置上的值,要使用什么运算符?
要使用间接运算符*。在指针名前写上*,引用的是该指针所指向的变量。
7:什么是指针?
指针是储存其他变量地址的变量。
8:什么是间接取值(indirection ) ?
间接取值指的是,用指针向变量指针访问变量的内容。
9:在内存中,如何储存数组的元素?
数组元素被顺序存储在内存中,下标越小的元素存储的地址位置越低。
10:用两种方式获得数组data[]的第1个元素的地址。
11:如果要给函数传递一个数组,有哪两种方式让函数知道已到达数组的末尾?
一种方法是,把数组的长度作为参数传递给函数。
另一种方法是,在数组中加入一个特定值(如,NULL),表面已达数组末尾。
12:本次介绍了哪6种可用于指针的运算?
赋值,间接取值,取址,递增,相减和比较
13:假设有两个指针,第1个指针指向int类型数组的第3个元素,第2个指针指向该数组的第4个元素。如果让第2个指针减去第1个指针,会得到多少?(假设整型的大小是2字节)
将两个指针相减得到他们之间之间的元素个数,在这种情况下答案为1,与数组元素实际大小无关。
14:假设上一题的数组类型是float。两个指针相减得到多少?(假设整型的大小是2字节)
还是1
练习题
1.声明一个指向char 类型变量的指针。指针名是char_ptr
2.假设有一个int类型的变量cost ,声明并初始化一个指向该变量的指针p_cost 。
下面声明了一个cost的指针,然后将cost的地址(&cost)赋值给该指针:
3.根据练习题2,使用直接访问和间接访问两种方式将100赋值给变量cost 。
直接访问:cost = 100;
间接访问:*p_cost = 100;
4.根据练习题3,打印指针的值和它所指向的值。
5.将float类型变量radius 的地址赋值给一个指针。
6.用两种方式将100赋值给数组data[]的第3个元素。
7.编写一个名为sumarrays()的函数,接受两个数组作为参数,将两个数组中所有的值相加,并将计算结果返回给主调函数。