C语言指针

你从网页复制一段文本,再到你想要的地方随意粘贴的时候;当你在Excel表格中插入几行数据的时候;当你不知怎的突然遭到被弹出“xx程序错误,单击确定立即关闭”的时候;当你用游戏修改软件锁定生命值,让游戏中的角色成为“金刚不死之躯”的时候……知道吗,这些都与指针息息相关!

指针可是程序设计的一个强大工具,使用指针,不仅可以表示很多重要的数据结构、高效地使用数组、方便地处理字符串、另类地调用函数……而且可以直接访问内存,赋予我们广大的自由度和“至高无上”的权利!编程高手们常说“无指针,不自由”,就是这个道理。因此指针也是学习C语言最重要的一环,可以说如果不会使用指针编程,就不是真正掌握了C语言。

有人说指针也是学习C语言最困难的一部分,但本书持相反意见。只要学习方法得当,实际上指针也并没有听起来那么难学!请读者不要畏惧,在本章我们会教给你很多技巧和方法,一步步带领大家攻克指针的壁垒。让我们现在就出发,一起体会这一次精彩、充满刺激挑战和富有乐趣的指针之旅!

内存里的门牌号——地址和指针的基本概念

“编号”,是人们常用的手段。例如,现在你翻到本书的这一页就有一个页码编号。编号的例子还有很多,如超市的存包箱有箱号,电影院座位有座次号,楼房的房间有房间号……。通过编号我们可以准确地找到位置。

计算机的内存是由一个个字节组成的,每个字节可以保存8个比特(8个0或1)。计算机内存的字节数可以有很多,例如一台有2GB内存的计算机就有多达2,147,483,648个字节(2×1024×1024×1024=2,147,483,648)! 那么多的字节,如果搞错搞乱,麻烦可就大了。如何有条不紊地管理这些字节,必须有个合适的手段。人们仿照生活中为事物编号的方式,也为计算机内存的每个字节编号。把第一个字节编为0号(从0开始,与数组下标有点像),第二个字节编为1号……,最后一个字节是2G-1即2,147,483,647号,如图8.1所示。

与我们把房间号称为地址类似,计算机内存中的字节编号也称为地址,地址也称指针(注:现在称地址为指针只是开场白,何谓指针这个概念,还没有讲完。不宜断章取义;且谭浩强先生的《C语言程序设计》也是这样介绍的)。

变量位于内存中,如定义变量int a; 则变量a要占用内存中的4个字节(在Visual C++ 6.0环境下)。变量a要占用哪4个字节呢?这是由计算机分配的,我们不能左右;而且在不同的计算机上运行程序或在同一计算机的不同时刻运行程序,变量被分配到的位置也不同。然而位置是可以假设一下的,假设变量a占据了内存中编号为1000~1003的4个字节,则这4个字节就被标记名称为"a"。用变量a保存一个整数,就是用这4个字节保存一个整数。例如执行语句a=1; 则1就被保存到这4个字节中(转换为二进制,前补0占满4个字节),如图8.1。

如果财务处位于办公楼的305,我们称财务处的地址是305;如果财务处比较大,占用了305-307三间房间,习惯上我们仍称财务处的地址为305,即取第一间房间号为地址。对于变量a,它占用了编号为1000~1003的4个字节,我们说变量a的地址为1000,也取它的第一个字节的编号作为变量的地址。注意说变量a的地址为1000,并不一定变量a只占用编号为1000的这一个字节。在分析程序时,我们不必像图8.1那样画出每个字节,而可采用图8.2的方式,在变量空间的左下角写出变量的地址就可以了。请注意"变量的地址"和"变量的值",这是两个完全不同的概念:

变量的地址:变量所位于存储空间的"门牌号"(写在变量的空间外、左下角),在整个程序运行期间,地址永久不变;

变量的值:变量空间中所保存的数据内容(写在变量的空间内),在程序运行期间,变量的值是可以变化的。

又如,定义变量double b=2.8; 变量b为double型,它在内存中占用8个字节。我们假设它占用了2000~2007的8个字节,则变量b的地址为2000,也表示如图8.2。

在学习本章之前,我们只能通过变量名来访问变量(访问就是存取变量的值)。现在有了变量的地址,访问变量就有了两种方式:(1)通过变量名;(2)通过变量的地址。这好比有了财务处的地址305,就既可通过"财务处"这个名字,也可直接去找305,都能找到财务处。

8.2 别把地址不当值–指针变量

地址本身也要被保存起来,就像把一个房间的门牌号写在纸上,把各章节的页码写在书的目录页中。在程序中地址应保存在哪里呢?对!也需要变量来保存。然而地址是不能被保存在普通变量中的,C语言提供有一种特殊的专有变量专门用来保存地址,这种变量称为指针变量,指针变量也可简称为指针。

8.2.1 找张字条记地址–定义指针变量

例如定义了整型变量a并为其赋初值1:

int a=1;

假设a占用1000~1003的4个字节,则a的地址为1000。为了将a的这个地址1000保存起来,需定义专用于保存地址的变量–指针变量。

定义指针变量与定义普通变量的形式类似,只需在变量名前加 * 号:

int *p;

  • 号是一个标志,有了 * 号才表示所定义的是指针变量,才能保存地址;如果没有 * 号,则p与a相同,都是int型的普通变量了,将只能保存普通的整型数据不能保存地址。注意指针变量的定义形式中:

变量名是p,不是*p;

变量p的类型是int *,不是int。

然后可通过如下语句将a的地址保存到p中:

p = &a;

其中&表示取地址,我们在scanf语句中也使用过这个符号。&a表示变量a的地址,p = &a;则表示将a的地址赋值给指针变量p,变量空间情况如图8.3所示。

下面的做法是不行的:

int x;

x = &a; /* 错误,x不能保存地址 */

x不是指针变量,不能保存地址。尽管内存中的字节编号是"整数"的,也不能用一个int型的变量来保存这种字节编号。反过来说,下面的做法也不行:

p = 1; /* 错误,p不能保存普通整数 */

C语言是很"讲究"的,必须做到"专变量专用":

◆ 由于变量a和变量x的定义语句分别是int a; int x; 都无 * 号,所以a、x都是普通变量,它们只能保存普通数据,不能保存地址;

◆ 由于变量p的定义语句是int *p; 有 * 号,所以p是指针变量,它只能保存地址,不能保存普通数据。

指针变量也是变量,普通变量的特性指针变量同样具有。指针变量的空间也要位于内存中,就像书的目录页与书的正文页同样都位于同一本书中。一个指针变量的空间有多大呢?规定指针变量一律都占用4个字节,这类似于各种书的目录页所占的页数一般是固定的。

指针变量p占用哪4个字节呢?它在内存中的位置也是由计算机分配的,但我们仍可为其假设一个位置:假设指针变量p占据30003003的4个字节;这样也可以说,指针变量p的地址是3000。30003003的4个字节将用于保存一个地址,而不是用来保存普通数据的。

因此,指针变量用于保存地址,但指针变量本身也有地址,请读者注意区分这"两个地址"。前者是指针变量里面所保存的内容,内容可变;后者是指针变量本身所在的"门牌号",是不变的。如图8.3对指针变量p空间的画法,前者画在方框内,后者画在方框左下角。如同一本书的目录页是指针变量,其内容是目录中所列出的各章节位于第几页,而目录页的地址一般是个罗马数字的页码如第II页。

就像射箭运动员将弓箭对准靶子,指针变量p保存了普通变量a的地址,就是"对准了"普通变量a,因为将可通过p中所保存的这个地址来访问变量a(就是存取变量a)。我们称:

指针变量p指向了变量a

或称

p是指向变量a的指针变量

由于指针变量还可简称为指针,后一句还可称为"p是指向变量a的指针"。

在一条定义语句中可同时定义多个指针变量,也可同时定义普通变量与指针变量,例如:

double *m, *n;

int *x, y, *z;

则m、n、x、z都是指针变量,因为它们前面都有 * 号;y不是指针变量,它是普通的int型变量,因为它前面没有 * 号。变量空间如图8.4所示。虽然还未给各变量赋值,在图8.4中,已在所有指针变量的方框内画上了一对[ ],这样提醒我们这些变量中只能存放地址不能存放普通数据。只有变量y是存放普通整数的,不能存放地址。

8.2.2 这可不是说我–指针变量的基类型

我们知道,如果定义了指针变量int *p; 则变量p的类型是int * 类型的。"int *类型"是什么含义呢?它表示指针变量p所指向的数据的类型是int型,也就是说将来p要保存一个地址,但这个地址有讲究,必须是一个int型的数据的地址才能被保存进来。如有:

int a, b;

double c, d;

int p; / p中只能保存int型数据的地址 */

则下面都是正确的语句:

p = &a; /* 正确,将a的地址保存到p中,a是int型变量 */

p = &b; /* 正确,将b的地址保存到p中,b是int型变量 */

而下面都是错误的语句:

p = &c; /* 错误,p不能保存c的地址,因c是double型不是int型 */

p = &d; /* 错误,p不能保存d的地址,因d是double型不是int型 */

而要想保存c、d的地址,要定义:

double q; / q中只能保存double型数据的地址 */

这样,才可以把c或d的地址保存到q中;但却不可把a或b的地址保存到q中:

q = &c; /* 正确,将c的地址保存到q中,c是double型变量 */

q = &d; /* 正确,将d的地址保存到q中,d是double型变量 */

q = &a; /* 错误,q不能保存a的地址,因a是int型不是double型 */

q = &b; /* 错误,q不能保存b的地址,因b是int型不是double型 */

可以看到,C语言是很讲究的,不仅用专门的变量–指针变量保存地址,而且不同类型的数据还专用不同类型的指针变量。如果某人准备了几本不同的通讯录,一本只记家人的联系方式,一本只记同学的联系方式,一本只记朋友的联系方式,不同类的人用不同的本儿,坚决不记在同一本上,那么这个人确实很讲究!C语言的指针变量就是如此。

在定义指针变量时,* 号之前的类型如int * 中的int是表示该指针变量将保存何种类型数据的地址,换句话说,就是指针变量所能指向的数据的类型,该类型称指针变量的基类型。指针变量要保存的地址必须是基类型这种类型数据的地址,指针变量只能指向同基类型类型的数据。

我们在打扑克时,有"超级"牌–大小王,如图8.5,它们能代替任意普通牌。指针变量里是否也有"大小王"呢?有的!这就是void基类型的指针。如有定义:

void *pt;

则pt可以保存任意类型数据的地址,而无论数据是什么类型的。下面语句均正确:

pt = &a; pt = &b; pt = &c; pt = &d;

8.2.3 把地址记下来–为指针变量赋值

为指针变量赋值,既可通过赋值语句的方式,也可在变量定义时赋初值。

① 通过赋值语句为指针变量赋值。

int a=1;

int *p;

p=&a; /* 不能写为 *p=&a,变量名是p不是 *p */

下面的做法是错误的,

p = a; /* 错误,p不能保存普通整数 */

它表示把a的值1赋值给变量p,但p中只能保存地址不能保存普通数据1故错误。

同普通变量类似,在为指针变量赋值之前,指针变量的值也是不确定的,也是随机数。指针变量是保存地址的,这也就是说,它里面保存的是个随机地址。如上例在执行p=&a;之前,p中所保存的地址是个随机地址。

② 定义指针变量时赋初值(定义时初始化)。

int *p = &a;

与为普通变量赋初值的做法类似,但要注意指针变量的初值必须为一个地址(这里是&a是变量a的地址)。另外尤其注意在这种赋值中,其含义仍是"p=&a;"而不是"p=&a;", 是与int结合的,变量的类型为int ;不要将 * 看做是与p结合的,变量名不为p。

③ 指针变量之间彼此互相赋值。所赋的值是其中保存的地址,这类似于把一张字条上所记录的朋友家的地址,抄一份誊在另一张字条上。赋值要求两个指针变量基类型必须相同。

int *q;

q = p; /* 则q、p均保存了变量a的地址,q、p均指向了变量a */

注意不要写为"q=&p;"、"*q=p;"等,这就是两个变量之间的赋值,没有那么麻烦!

float *r;

r = p; /* 错误,r与p的基类型不同,不能彼此赋值 */

④ 不允许把一个"数"当做地址直接赋值给指针变量,下面的赋值是错误的:

int *p;

p=1000; /* 错误,即使我们知道某个变量的地址是1000也不能这么干 */

但特殊地,允许把数值0直接赋值给指针变量,但仅此特例。下面的做法是正确的:

p=0; /* 正确,唯独可直接赋值为0 */

系统在stdio.h头文件中定义有符号常量NULL(#define NULL 0),因此也可写为:

p=NULL; /* 正确,NULL是0的代替符号,仍是p=0的意思 */

注意NULL 四个字母必须全部大写。

为什么可以给指针变量直接赋值为0呢?系统规定,如果一个指针变量里保存的地址为0,则说明这个指针变量不指向任何内容,叫做空指针,如图8.6。

指针变量未赋值和赋0值是不同的:指针变量未赋值时,其保存的地址是随机地址,是不能使用的。而将指针变量赋0值,其保存的地址是0,是确定的,它不指向任何内容。

在定义指针变量时,如果一时不能确定它要指向哪个位置,可先将其初始化为0或NULL。以免指针变量的随机指向。例如:

int *p = 0;

8.2.4 指针运算俩兄弟–两个运算符

(1) & 取地址运算符。获取变量的地址,写作:&变量名。& 运算符既可以取普通变量的地址,也可以取指针变量的地址。例如对图8.3中的变量,&p得到的地址是3000,&a得到的地址是1000。

(2) * 指针运算符(或称间接访问运算符)。获取或改写以p为地址的内存单元的内容,写作:*指针变量名。它就是"按图索骥",按照p中所保存的地址,找到数据的意思。

例如对图8.3中的变量执行:

printf("%d", *p);

将输出a的值1,因为p保存的是a的地址,*p就找到了变量a的内容。如果去掉 * 号写为printf("%d", p); 则将输出p本身的值(输出a的地址),而得不到a的值。

如果已定义int b; 还可执行:

b = *p;

是将a的内容1赋值给b。但如写为"b = p;"是错误的,因为它是把p中所保存的地址本身赋值给b,而变量b是不能保存地址的。

  • 像是一位快递员,可以帮我们收取物品,只要告诉他地址(如p)就可以了。显然,* 运算只能用于指针变量,如用于普通变量(如*a)是错误的。

以上是通过 * 获取数据,通过 * 还能改写数据。如同快递员除了可以帮我们收取物品外,还能帮我们寄送物品。同样只要把地址给他,他就能把物品送到我们指定地址的地方;在程序中就是将一个数据送到指定地址的变量中,从而改变变量的值。如执行语句:

*p = 2;

是将2送入p所指向的变量中,即a被赋值为2,它等价于语句a=2;。但如写为p=2;是错误的,因为后者是把变量p本身赋值为2,而变量p只能存地址,不能存普通整数2。

实际上p等价于a,用p或用a都能存取a这个变量;前者是通过地址的"门牌号"访问变量,后者是通过变量名访问变量。正因有此用法,赋值(=)的完整规则可归纳为:

赋值(=)左边必须为"变量",或"*指针变量"

& 和 * 都是单目运算符,结合方向"自右至左"。& 和 * 互为逆运算,即一个&和一个*可以相互抵消,如果有p=&a; 则:

&p <-> p <-> &a &a <-> a &&&p <-> p &&&*p <-> *p ? a

尤其注意的是:前面定义指针变量时变量名前的 *,与这里所讲的 * 是完全不同的,如图8.7。虽然"长相"一致,但是两种符号,它们之间也没有什么关系。在程序中,如何区分这两种符号呢?

◆ 在变量定义时写 * 就是指针变量的标志(前有int、double等类型说明符),如int *p; 的 *,它没有任何"取数据"或"改数据"的含义;

◆ 在执行语句中写 * (前没有int、double等类型说明符)才是"取数据"或"改数据"的含义。

int p=&a; / 正确*/

p=&a; / 错误 */

p=&a; /* 正确 */

以上程序第一句的 * 是指针变量定义的标志,是正确的;第二句的 * 是"改数据"的含义,*p表示变量a,它只能被赋值为普通数据,不能赋值为地址&a。

【程序例8.1】&和*运算符的用法。

main()

{ int a=1,x=2,*p;

p=&a;

x=p; / x=a; */

p=5; / a=5; */

printf("%d %d ", a, x);

printf("%d", p); / printf(" %d", a); */

}

程序的输出结果为:

5 1 5

讲到这里,不知读者是不是被指针里的这个星星 * 撞得有些晕呢?没关系,让我们总结一下,把指针程序中到底该不该写 * 捋顺:

◆ 定义指针变量时(前有int、double等类型说明符为定义),必须写 *,否则将定义普通变量而不是指针变量了。例如int *p;

◆ 使用指针变量时(前无int、double等类型说明符为使用),可以写 *,也可以不写 *,但二者是截然不同的:

√ 写 * 表示取指针变量所指向的内容或改写所指向的内容。例如printf("%d", *p); 或 *p=2;

√ 不写 * 表示指针变量本身所保存的地址。例如q=p;是赋值指针变量本身所保存的地址,就像把朋友家的地址另抄了一份誊在另一张字条上。

你能指出在图8.3中,执行语句中的p、&p、*p(前无int、double等类型说明符时)各表示什么?

p:表示指针变量p本身所保存的值(是个地址),是地址1000;

&p:表示指针变量p本身的地址,是p本身所在的位置,是地址3000;

*p:表示指针变量p所指向的数据,是变量a。

输入a和b两个整数,分别输出较大值和较小值。

main()

{ double *p1, *p2, *p, a, b;

p1=&a; p2=&b;

scanf("%lf%lf", &a, &b);

if (a<b) { p=p1; p1=p2; p2=p; }

printf(“max=%lf,min=%lf\n”,*p1,*p2);

}

程序的运行结果为:

2.4 3.4↓

max=3.400000,min=2.400000

程序中定义了3个指针变量p1、p2、p和2个普通变量a、b。起初让p1保存a的地址,p2保存b的地址。如果所输入的a值较小,就通过if语句交换p1和p2中所保存的地址(以p为临时变量),使p1总指向较大值。最后输出*p1就是较大值,*p2就是较小值。变量空间情况如图8.8所示,注意交换的是p1、p2,并未交换a、b。

以上程序中的scanf("%lf%lf", &a, &b); 还可以写为下面的形式吗:

① scanf("%lf%lf", &p1, &p2); ② scanf("%lf%lf", p1, p2); ③ scanf("%lf%lf", a, b);

答案:仅②正确,①③都不正确。scanf后面要求地址,③显然是错误的;①也不正确,p1、p2已经是a、b的地址了,直接写为p1、p2即可,就不要再取地址&p1、&p2。

发布了282 篇原创文章 · 获赞 4 · 访问量 5696
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览