阅读:0

指针,C语言指针完全攻略

与其他高级编程语言相比,C 语言可以更高效地对计算机硬件进行操作,而计算机硬件的操作指令,在很大程度上依赖于地址。

指针提供了对地址操作的一种方法,因此,使用指针可使得 C 语言能够更高效地实现对计算机底层硬件的操作。另外,通过指针可以更便捷地操作数组。在一定意义上可以说,指针是 C 语言的精髓。

内存与地址

在计算机中,数据是存放在内存单元中的,一般把内存中的一个字节称为一个内存单元。为了更方便地访问这些内存单元,可预先给内存中的所有内存单元进行地址编号,根据地址编号,可准确找到其对应的内存单元。由于每一个地址编号均对应一个内存单元,因此可以形象地说一个地址编号就指向一个内存单元。C 语言中把地址形象地称作指针。

C语言中的每个变量均对应内存中的一块内存空间,而内存中每个内存单元均是有地址编号的。在 C 语言中,可以使用运算符 & 求某个变量的地址。

例如,在如下代码中,定义了字符型变量 c 和整型变量 a,并分别赋初值 'A' 和 100。
#include <stdio.h>
int main(void)
{
    char c='A';
    int a=100;
    printf("a=%d\n",a);//输出变量a的值
    printf("&a=%x\n",&a);//输出变量a的地址
    printf("c=%c\n",c);
    printf("&c=%x\n",&c);
    return 0;
}
程序某次的运行结果为:
a=100
&a=12ff40
c=A
&c=12ff44

分析:
在 C 语言中,字符型变量占一个字节的内存空间,而整型变量所占字节数与系统有关。例如,在 32 位系统中,VC++6.0 开发环境中,int 型占 4 个字节。假设程序在某次运行时,变量 a 和 c 在内存中的分配情况如图 1 所示:内存单元(每个字节)的地址编号分别为十六进制表示的 ...12ff40、12ff41、12ff42、12ff43、12ff44...,每个地址编号均为对应字节单元的起始地址。


图 1 变量的值及内存地址

由图 1 可知,变量 a 对应于从地址 12ff40 开始的 4 个字节(12ff40、12ff41、12ff42、12ff43)的内存空间,存储的是整数 100 的 32 位二进制形式(为直观表示,本例并没有转换成二进制形式)。字符型变量 c,对应地址为 12ff44,该地址内存储的是字母对应 ASCII 值的 8 位二进制形式。

语句 printf("a=%d\n",a); 输出:a=100。

语句 printf("&a=%x\n",&a); 是按十六进制形式输出变量 a 的地址(a 在内存中的起始地址值)为 &a=12ff40。

在上例中,变量 a 和 c 的起始地址 12ff40 和 12ff44 均为指针,分别指向变量 a 和变量 c。

区分变量的地址值和变量的值。如上例中,变量 a 的地址值(指针值)为12ff40,而变量 a 的值为 100。

指针变量的定义

可以保存地址值(指针)的变量称为指针变量,因为指针变量中保存的是地址值,故可以把指针变量形象地比喻成地址箱

指针变量的定义形式如下。

类型 * 变量名;

例如:
int *pa;
定义了一个整型指针变量 pa,该指针变量只能指向基类型为 int 的整型变量,即只能保存整型变量的地址。

说明:
1) *号标识该变量为指针类型,当定义多个指针变量时,在每个指针变量名前面均需要加一个 *,不能省略,否则为非指针变量。例如:
int *pa,*pb;
表示定义了两个指针变量 pa、pb。而:

int *pa,pb;

则仅有 pa 是指针变量,而 pb 是整型变量。
int *pi,a,b; //等价于inta,b,*pi;
表示定义了一个整型指针变量 pi 和两个整型变量 a 和 b。

2) 在使用已定义好的指针变量时,在变量名前面不能加 *。例如:
int *p,a;
*p=&a; //错误,指针变量是p而不是*p
而如下语句是正确的。
int a,*p=&a; //正确
该语句貌似把 &a 赋给了 *p,而实际上 p 前的 * 仅是定义指针变量 p 的标识,仍然是把 &a 赋给了 p,故是正确的赋值语句。

3) 类型为该指针变量所指向的基类型,可以为 int、char、float 等基本数据类型,也可以为自定义数据类型。

该指针变量中只能保存该基类型变量的地址。

假设有如下变量定义语句:
int a,b,*pa,*pb;
char *pc,c;
则:
pa=&a;//正确。pa基类型为int,a为int型变量,类型一致
pb=&c;//错误。pb基类型为int,c为char型变量,类型不一致
pc=&c;//正确。pc基类型为char,c为char型变量,类型一致
*pa=&a;//错误。指针变量是pa而非*pa
4) 变量名是一合法标识符,为与普通变量相区分,一般指针变量名以字母 p(pointer)开头,如 pa、pb 等。

5) 由于是变量,故指针变量的值可以改变,也即可以改变指针变量的指向。
char c1,*pc,c2;//定义了字符变量c1、c2和字符指针变量pc
则如下对指针变量的赋值语句均是正确的。
pc=&c1; //pc指向c1
pc=&c2; //pc不再指向c1,而指向c2
6) 同类型的指针变量可以相互赋值。
int a,*p1,*p2,b;//定义了两个整型变量a,b;两个整型指针变量为p1,p2
float *pf;
以下赋值语句均是正确的。
p1=&a; //地址箱p1中保存a的地址,即p1指向a
p2=p1; //p2也指向a,即p1和p2均指向a
上述最后一条赋值语句相当于把地址箱 p1 中的值赋给地址箱 p2,即 p2 中也保存 a 的地址,即和 p1 —样,p2 也指向变量 a。

以下赋值语句均是错误的。
pf=p1; //错误。p1,pf虽然都是指针变量,但类型不同,不能赋值
pf=&b; //错误。指针变量pf的基类型为float,b类型为int,不相同
由于指针变量是专门保存地址值(指针)的变量,故本节把指针变量形象地看成“地址箱”。

设有如下定义语句:
int a=3,*pa=&a; //pa保存变量a的地址,即指向a
char c='d',*pc=&c; //pc保存变量c的地址,即指向c
把整型变量 a 的地址赋给地址箱 pa,即 pa 指向变量 a,同理 pc 指向变量 c,如图 2 所示。


图 2 指针指向变量

指针变量的引用

访问内存空间,一般分为直接访问间接访问

如果知道内存空间的名字,可通过名字访问该空间,称为直接访问。由于变量即代表有名字的内存单元,故通。过变量名操作变量,也就是通过名字直接访问该变量对应的内存单元。

如果知道内存空间的地址,也可以通过该地址间接访问该空间。对内存空间的访问操作一般指的是存、取操作,即向内存空间中存入数据和从内存空间中读取数据。

在 C 语言中,可以使用间接访问符(取内容访问符)*来访问指针所指向的空间。

例如:
int *p,a=3;//p中保存变量a对应内存单元的地址
p=&a;
在该地址 p 前面加上间接访问符 *,即代表该地址对应的内存单元。而变量 a 也对应该内存单元,故 *p 就相当于 a。
printf("a=%d\n",a); //通过名字,直接访问变量a空间(读取)
printf("a=%d\n",*p); //通过地址,间接访问变量a空间(读取)
*p=6;//等价于a=6;间接访问a对应空间(存)

“野”指针

本节中,把没有合法指向的指针称为“野”指针。因为“野”指针随机指向一块空间,该空间中存储的可能是其他程序的数据甚至是系统数据,故不能对“野”指针所指向的空间进行存取操作,否则轻者会引起程序崩溃,严重的可能导致整个系统崩溃。

例如:
int *pi,a; //pi未初始化,无合法指向,为“野”指针
*pi=3; //运行时错误!不能对”野”指针指向的空间做存入操作。该语句试图把 3 存入“野”指针pi所指的随机空间中,会产生运行时错误。
a=*pi; //运行时错误!不能对”野”指针指向的空间取操作。该语句试图从“野”指针pi所指的空间中取出数据,然后赋给变量a同样会产生运行时错误。
正确的使用方法:
pi=&a;//让pi有合法的指向,pi指向a变量对应的空间
*pi=3;//把3间接存入pi所指向的变量a对应的空间

指针与数组

数组是一系列相同类型变量的集合,不管是一维数组还是多维数组其存储结构都是顺序存储形式,即数组中的元素是按一定顺序依次存放在内存中的一块连续的内存空间中(地址连续)。

指针变量类似于一个地址箱,让其初始化为某个数组元素的地址,以该地址值为基准,通过向前或向后改变地址箱中的地址值,即可让该指针变量指向不同的数组元素,从而达到通过指针变量便可以方便地访问数组中各元素的目的。

一维教组和指针

在 C 语言中,指针变量加 1 表示跳过该指针变量对应的基类型所占字节数大小的空间。指向数组元素的指针,其基类型为数组元素类型,指针加 1 表示跳过一个数组元素空间,指向下一个数组元素。

例如:
int *p,a[10];
p=a; //相当于 p=&a[0];
说明:数组名 a 相当于数组首元素 a[0] 的地址,即 a 等价于 &a[0]。

上述语句定义了整型指针变量 p 和整型数组 a,并使 p 初始指向数组首元素 a[0]。 当指针变量和数组元素建立联系后,可通过以下三种方式访问数组元素。

1) 直接访问:数组名[下标]; 的形式。如 a[3]。

2) 间接访问:*(数组名+i); 的形式。其中,i 为整数,其范围为:0<i<N,N 为数组大小。数组名 a 为首元素的地址,是地址常量,a+i 表示跳过 i 个数据元素的存储空间,即(a+i)表示 a[i] 元素的地址,从而 *(a+i) 表示 a[i]。

如果指针变量 p 被初始化为 a 之后,不再改变,那么也可以使用 *(p + i) 的形式访问 a[i],不过这样就失去了使用指针变量访问数组元素的意义。

3) 间接访问:*(指针变量);的形式。当执行语句 p=a; 后,可以通过改变 p 自身的值(可通过自增、自减运算),从而使得 p 中保存不同的数组元素的地址,进而通过 *p 访问该数组中不同的元素。这是使用指针访问数组元素较常用的形式。例如,如下代码通过使用指针变量的移动来遍历输出数组中的每个元素。
for (p=a;p<a+N;p++) //用p的移动范围控制循环次数
    printf ("%d\t",*p);
确定 p 指针移动的起止地址,即循环控制表达式的确定是使用指针访问数组元素的关键。

p 初始指向 a[0],即 p=&a[0]; 或 p=a;。
p 终止指向 a[N-1],即 p=&a[N-l]; 或 p=a+N-1;。

故可得 p 的移动范围为:p>=a && p<=a+N-1;,而 p<=a+N-1 通常写成 p<a+N;,由此可得循环条件为:for (p=a;p<a+N;p++)。

数组名 a 和指针变量 p 的使用说明如下。有如下代码:
int *p,a[10],i;
p=a;
1) 执行p=a; 后,*(a+i) 与 *(p+i) 等价,均表示 a[i]。