c-programming-chap8
If two people write exactly the same program, each should be put in micro-code and then they certainly won’t be the same.
[TOC]
指针
零 前言
指针是 C 语言最重要——也是最常被误解——的特性之一。本节重点介绍指针的基础内容。
一 指针变量
现代大多数计算机将内存分割为字节(byte),每个字节可以存储 8 位的信息:0000 0001
。
每个字节都有唯一的地址(address),用来和内存中的其他字节相区别。如果内存中有 n 个字节,那么可以把地址看作 0 ~ n - 1的数。
可执行程序由代码(原始 C 程序中于语句对应的机器指令)和 数据(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节,把第一个字节的地址称为是变量的地址。
上图中,i 占有的字节是 2000 ~ 2003 4 个字节,2000 就是 i 的地址。
我们可以用特殊的指针变量(pointer variable)存储地址。在用指针变量存储 p 存储变量 i 的地址时,我们说 p “指向” i 。换句话说,指针就是地址,而指针变量就是存储地址的变量。
1. 指针变量的声明
1 |
|
上述声明说明p 是指向 int 类型对象的指针变量。
左端的int是在定义指针变量时必须指定的“基类型”。指针变量的基类型用来指定此指针变量可以指向的变量的类型。
前面介绍过基本的数据类型(如int,char,float等),既然有这些类型的变量,就可以有指向这些类型变量的指针,因此,指针变量是基本数据类型派生出来的类型,它不能离开基本类型而独立存在。
指针变量可以与其他变量一起出现在声明中:
1 |
|
C 语言要求每个指针变量只能指向一种特定类型(引用类型)的对象。
1 |
|
关于指针变量声明中 * 与谁挨着的问题:
请看下面的声明:
1 |
|
请问,上面的声明中 p 和 q 都是指针变量吗?
小黄:我觉得是,如果你写成这样:
1 |
|
那就是只有 p 是指针变量了。
程序圆:你这样想就大错特错啦,上面这两种写法是等价的。都是声明 p 为指针变量而 q 是一个普通的 int 类型变量。
小黄:哦~那我们平时应该选择那种写法呢?
程序圆:通常情况下我们都是选择第一种写法,即:int* p
。但是这样确实容易造成误解,所以我们通常一行只声明一个指针变量就可以了。
二 如何应用指针变量
1. 取地址运算符【指针变量初始化】
声明指针变量时我们没有将它指向任何对象:
1 |
|
在使用之前初始化 p 是至关重要的。使用取地址运算符&
把某个变量的地址赋值给它。
1 |
|
现在 p 就指向了整型变量 i
我们也可以声明的同时初始化:
1 |
|
甚至可以这样:
1 |
|
但是需要先声明 i
2. 间接寻址运算符
间接寻址运算符 *
1 |
|
指针变量 p 指向 i,使用*
运算符可以访问存储在对象中的内容
(即访问存储在指针变量指向的地址上的内容)。
1 |
|
“*
和&
互为逆运算”:
1 |
|
只要 p 指向 i,*p 就是 i 的别名。*p 不仅拥有和 i 相同的值,而且 *p 的改变也会改变 i 的值。
1 |
|
注意:
未初始化的指针变量会导致未定义行为:
1 |
|
3. 指针赋值
C 语言允许相同类型的指针变量进行赋值。
1 |
|
或者直接初始化并赋值:
1 |
|
现在可以通过改变 *p 的值来改变 i :
1 |
|
不要将 *q = *p
和 q = p
搞混,前者是将 p 指向的对象的值(变量 i 的值)赋值给 q 指向的对象(变量 j)中。
4.指针作为参数
函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法。
如果想通过函数调用得到n个要改变的值,可以这样做:
- 在主调函数中设n个变量,用n个指针变量指向它们;
- 设计一个函数(自定义),有n个指针形参。在这个函数中改变这n个形参的值;
- 在主调函数中调用这个函数,在调用时将这n个指针变量作实参,将它们的值,也就是相关变量的地址传给该函数的形参;
- 在执行该函数的过程中,通过形参指针变量,改变它们所指向的n个变量的值;
- 主调函数中就可以使用这些改变了值的变量。
程序1:对输入的两个整数按大小顺序输出。
1 |
|
程序2:找出数组中的最大元素和最小元素
与程序的交互如下:
1 |
|
参考程序:
1 |
|
指针应用小结
① 给指针变量赋值。
② 引用指针变量指向的变量。
③引用指针变量的值。
1 |
|
要熟练掌握两个有关的运算符:
(1) &取地址运算符。&a是变量a的地址。
(2) * 指针运算符(或称“间接访问”运算符),*p代表指针变量p指向的对象。
(3) 注意区分printf("%d",*p)
和printf("%o",p)
的区别
例题
若有int *p,a=10;和p=&a;下面( )均代表地址。
A) a,p,*&a
B) &*a,&a,*p
C) *&p,*p,&a
D) &a,&*p,p ✅
扩展:请用至少两种不同的方式输出变量a的值
三 指针与数组
用指针引用数组元素
一个变量有地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。 可以用一个指针变量指向一个数组元素。
1 |
|
引用数组元素可以用下标法,也可以用指针法,即通过指向数组元素的指针找到所需的元素。
初始化数组指针
即将指针变量指向数组首个元素
1 |
|
在定义指针变量时可以对它初始化:
1 |
|
在引用数组元素时指针的运算
在指针已指向一个数组元素时,可以对指针进行以下运算:
- 加一个整数(用+或+=),如p+1,表示指向同一数组中的下一个元素;
- 减一个整数(用-或-=),如p-1,表示指向同一数组中的上一个元素;
- 自加运算,如p++,++p;
- 自减运算,如p–,–p。
1 |
|
两个指针相减,如p1-p2(只有p1和p2都指向同一数组中的元素时才有意义),结果是两个地址之差除以数组元素的长度。注意: 两个地址不能相加,如p1+p2是无实际意义的。
如果p的初值为&a[0],则p+i和a+i就是数组元素a[i]的地址,或者说,它们指向a数组序号为i的元素。
*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。[]实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。
例题:有一个整型数组a,有10个元素,要求输出数组中的全部元素
- 方法1:下标法
1 |
|
- 方法2:通过数组名计算数组元素地址,找出元素的值
1 |
|
- 方法3:用指针变量指向数组元素
1 |
|
三种方法的比较
第(1)和第(2)种方法执行效率是相同的。C编译系统是将a[i]转换为*(a+i)处理的,即先计算元素地址。因此用第(1)和第(2)种方法找数组元素费时较多。
第(3)种方法比第(1)、第(2)种方法快,用指针变量直接指向元素,不必每次都重新计算地址,像p++这样的自加操作是比较快的。这种有规律地改变地址值(p++)能大大提高执行效率。
例题
若有定义 int a[5],*p=a;则对 a 数组元素的正确引用是( )。
A) *&a[5]
B) a+2
C) *(p+5)
D) *(a+2) ✅
注意事项
用下标法比较直观,能直接知道是第几个元素。适合初学者使用。
用地址法或指针变量的方法不直观,难以很快地判断出当前处理的是哪一个元素。单用指针变量的方法进行控制,可使程序简洁、高效。
在使用指针变量指向数组元素时,有以下几个问题要注意:
(1) 可以通过改变指针变量的值指向不同的元素。
如果不用p变化的方法而用数组名a变化的方法(例如,用a++)行不行呢?
1 |
|
因为数组名a代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。既然a是常量,所以a++是无法实现的。
(2) 要注意指针变量的当前值。
例题
通过指针变量输出整型数组a的10个元素。
判断下面哪一个代码正确??
1 |
|
1 |
|
例题小结
- 从此例题可以看到,虽然定义数组时指定它包含10个元素,并用指针变量p指向某一数组元素,但是实际上指针变量p可以指向数组以后的存储单元。
- 指向数组元素的指针变量也可以带下标,如p[i]。p[i]被处理成*(p+i),如果p是指向一个整型数组元素a[0],则p[i]代表a[i]。但是必须弄清楚p当前指向的是哪一个元素?如果当前p指向a[3],则p[2]并不代表a[2],而是a[3+2],即a[5]。
- 利用指针引用数组元素,比较方便灵活,有不少技巧。请分析下面几种情况:
- 设p开始时指向数组a的首元素(即p=a):
1
2
3
4
5
6p++; //使p指向下一元素a[1]
*p; //得到下一个元素a[1]的值
*(p++); //先取*p值,然后使p加1
*(++p); //先使p加1,再取*p
*p++; /*由于++和*同优先级,结合方向自右而左,因此它等价于*(p++)。先引用p的值,实现*p的运算,然后再使p自增1*/
++(*p); /*表示p所指向的元素值加1,如果p=a, 则相当于++a[0],若a[0]的值为3,则a[0]的值为4。注意: 是元素a[0]的值加1,而不是指针p的值加1*/ - 如果p当前指向a数组中第i个元素a[i],则:
1
2
3*(p--) //相当于a[i--],先对p进行“*”运算,再使p自减
*(++p) //相当于a[++i],先使p自加,再进行“*”运算
*(--p) //相当于a[--i],先使p自减,再进行“*”运算
用数组名作函数参数
如果有一个实参数组,要想在函数中改变此数组中的元素的值,实参与形参的对应关系有以下4种情况。
① 形参和实参都用数组名
② 实参用数组名,形参用指针变量。
③ 实参形参都用指针变量。
④ 实参为指针变量,形参为数组名。
例题 【例8.10】用指针方法对10个整数按由大到小顺序排序。
四 指针与字符串【重要!!】
如何用数组定义字符串??
忘记的同学请自行查阅!
字符串的引用方式
- 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明
%s
输出该字符串。 - 用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
字符型指针声明、赋值
1 |
|
string被定义为一个指针变量,基类型为字符型。它只能指向一个字符类型数据,而不能同时指向多个字符数据,更不是把″I love China!″这些字符存放到string中(指针变量只能存放地址),也不是把字符串赋给*string。只是把″I love China!″的第1个字符的地址赋给指针变量string。
可以对指针变量进行再赋值,如:
1 |
|
可以通过字符指针变量输出它所指向的字符串,如:
1 |
|
%s是输出字符串时所用的格式符,在输出项中给出字符指针变量名string,则系统会输出string所指向的字符串第1个字符,然后自动使string加1,使之指向下一个字符,再输出该字符……如此直到遇到字符串结束标志′\0′为止。注意,在内存中,字符串的最后被自动加了一个′\0′。
例题
【例8.18】将字符串a复制为字符串b,然后输出字符串b(用指针变量来处理);
1 |
|
字符指针作函数参数
字符指针作为函数参数时,实参与形参的类型有以下几种对应关系:
【例8.20】
使用字符指针变量和字符数组的总结
- 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第1个字符的地址),绝不是将字符串放到字符指针变量中。
- 赋值方式。可以对字符指针变量赋值,但不能对数组名赋值。(数组名是常量)
- 初始化的含义。
-
存储单元的内容。编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元(Visual C++为指针变量分配4个字节)
-
指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。
-
引用数组元素。对字符数组可以用下标法(用数组名和下标)引用一个数组元素(如a[5]),也可以用地址法(如*(a+5))引用数组元素a[5]。如果定义了字符指针变量p,并使它指向数组a的首元素,则可以用指针变量带下标的形式引用数组元素(如p[5]),同样,可以用地址法(如*(p+5))引用数组元素a[5]。
-
字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。
- 用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串。
1 |
|
例题
改变指针变量的值
1 |
|
1 |
|
指针变量a的值是可以变化的。printf函数输出字符串时,从指针变量a当时所指向的元素开始,逐个输出各个字符,直到遇’\0’为止。而数组名虽然代表地址,但它是常量,它的值是不能改变的。
参考资料:《C语言程序设计:现代方法》、《C语言程序设计(第五版)》
- 如果两个人用低级语言写同一个程序,它们显然不会相同。Epigrams on Programming 编程警句 ↩
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!