当前位置:文档之家› 015 TC操作手册之---指针与内存操作

015 TC操作手册之---指针与内存操作

TC操作手册之---指针与内存操作

下面来介绍C语言功能最强大的特点,同时也是相对而言比较难掌握的概念之一——指针。

一、指针的基本概念
如同其它基本类型的变量一样,指针也是一种变量,但它是一种把内存地址作为其值的变量。因为指针通常包含的是一个拥有具体值的变量的地址,所以它可以间接地引用一个值。

二、指针变量的声明、初始化和运算符
声明语句

int *ptra, a;

声明了一个整型变量a与一个指向整数值的指针ptra,也就是说,在声明语句中使用*(称为“间接引用运算符”)即表示被声明的变量是一个指针。指针可被声明为指向任何数据类型。需要强调的是,在此语句中变量a只被声明为一个整型变量,这是因为间接引用运算符*并不针对一条声明语句中的所有变量,所以每一个指针都必须在其名字前面用前缀*声明。指针应该用声明语句或赋值语句初始化,可以把指针初始化为0、NULL或某个地址,具有值0或NULL的指针不指向任何值,而要想把某个变量地址赋给指针,需使用单目运算符&(称为“地址运算符”)。

例如程序已用声明语句

int *ptra, a=3;

声明了整型变量a(值为3)与指向整数值的指针a,那么通过赋值语句

ptra=&a;

就可以把变量a的地址赋给指针变量ptra。需要注意的是不可将运算符&用于常量、表达式或存储类别被声明为register的变量上。被赋值后的指针可以通过运算符*获得它所指向的对象的值,这叫做“指针的复引用”,例如打印语句

printf("%d", *ptra);

就会打印出指针变量ptra所指向的对象的值(也就是a的值)3。如果被复引用的指针没有被正确的初始化或没有指向具体的内存单元都会导致致命的执行错误或意外地修改重要的数据。printf的转换说明符%p以十六进制整数形式输出内存地址,例如在以上的赋值后,打印语句

printf("%p", &a);
printf("%p", ptra);

都会打印出变量a的地址。

三、指针表达式和算术运算以及数组、字符串和指针的关系
在算术表达式、赋值表达式和比较表达式中,指针是合法的操作数,但是并非所有的运算符在与指针变量一起使用时都是合法的,可以对指针进行的有限的算术运算包括自增运算(++)、自减运算(--)、加上一个整数(+、+=)、减去一个整数(-或-=)以及减去另一个指针。

数组的各元素在内存中是连续存放的,这是指针运算的基础。现在假设在一台整数占4个字节的机器上,指针ptr被初始化指向整型数组a(共有三个元素)的元素a[0],而a[0]的地址是40000,那么各个变量的地址就会如下表所示:

表达式 ptra &a[0] &a[1] &a[2]
表达式的意义 指针ptra的值

元素a[0]的地址 元素a[1]的地址 元素a[2]的地址
表达式的值 40000 40000 40004 40008

必须注意,指针运算不同于常规的算术运算,一般地,40000+2的结果是40002,但当一个指针加上或减去一个整数时,指针并非简单地加上或减去该整数值,而是加上该整数与指针引用对象大小的乘积,而对象的大小则和机器与对象的数据类型有关。例如在上述情况下,语句

ptra+=2;

的结果是40000+4*2=40008, ptra也随之指向元素a[2],同理,诸如语句

ptra-=2;
ptra++;
++ptra;
ptra--;
ptra--;

等的运算原理也都与此相同,至于指针与指针相减,则会得到在两个地址之间所包含的数组元素的个数,例如ptra1包含存储单元40008,ptra2包含存储单元40000,那么语句

x = ptra1 - ptra2;

得到的结果就是2(仍假设整数在内存中占4个字节)。因为除了数组元素外,我们不能认为两个相同类型的变量是在内存中连续存储的,所以指针算数运算除了用于数组外没有什么意义。

如果两个指针类型相同,那么可以把一个指针赋给另一个指针,否则必须用强制类型转换运算符把赋值运算符右边的指针的类型转换为赋值运算符左边指针的类型。例如ptr1是指向整数的指针,而ptr2是指向浮点数的指针,那么要把ptr2的值赋给ptr1, 则须用语句

ptr1 = (int *) ptr2;

来实现。唯一例外的是指向void类型的指针(即void *),因为它可以表示任何类型的指针。任何类型的指针都可以赋给指向void类型的指针,指向void类型的指针也可以赋给任何类型的指针,这两种情况都不需要使用强制类型转换。

但是由于编译器不能根据类型确定void *类型的指针到底要引用多少个字节数,所以复引用void *指针是一种语法错误。

可以用相等测试运算符和关系运算符比较两个指针,但是除非他们指向同一个数组中的元素,否则这种比较一般没有意义。相等测试运算符则一般用来判断某个指针是否是NULL,这在本文后面提到内存操作中有一定的用途。

C语言中的数组和指针有着密切的关系,他们几乎可以互换,实际上数组名可以被认为是一个常量指针,假设a是有五个元素的整数数组,又已用赋值语句

ptra = a;

将第一个元素的地址赋给了指向整数的指针ptra,那么如下的一组表达式是等价的:

a[3] ptra[3] *(ptra+3) *(a+3)

它们都表示数组中第四个元素的值。

又因为C语言中的字符串是用空字符('\0')结束的字符数组,所以事实上,字符串就是指向其第一个字符的指针。但是还是要提醒大家,数组名和字符串名都是常量指针,他们的值是不可被改变的,例如程序段

char s[]="this is a test.";
for (;*s='\0';s++)
printf("%c", *s);


错误的,因为它试图在循环中改变s的值,而s实际上是一个常量指针。

在本部分的最后,说一说指向函数的指针。指向函数的指针包含了该函数在内存中的地址,函数名实际上就是完成函数任务的代码在内存中的起始地址。函数指针常用在菜单驱动系统中。

四、指针的应用、内存操作和简单的数据结构
其实初学者在学习指针时,其困难之处往往不在于理解其中的基本概念,而在于不知道指针究竟有何用处,什么时候应该用指针去解决实际问题。以下我就将指针的主要功能作一个简单的介绍。

A——调用函数时能修改两个或两个以上的值并将其返回调用函数
我们在没有学习指针之前便涉及了函数的使用,在函数中使用return语句可以把一个值从被调用的函数返回给调用函数(或从被调用函数返回控制权而不返回一个值),但是在实际应用中常常需要能够修改调用函数中的多个值,这就必须用到指针。

给函数传递参数的方式有两种,即传值和传引用,C语言中的所有函数调用都是传值调用,但可以用指针和间接引用运算符模拟传引用调用。在调用某个函数时,如果需要在被调用的函数中修改参数值,应该给函数传递参数的地址。当把变量的地址传递给函数时,可以在函数中用间接引用运算符*修改调用函数中内存单元中的该变量的值。比如我们无法在只用传值调用的情况下在函数中完成两个数的交换,因为它要求修改两个参数的值,但通过传地址模拟传引用调用即可轻松地实现这一点,例如函数:

void swap(int *a, int *b)
{
int temp;
temp=*a;
*a=*b;
*b=temp;
}

就实现了两个整数的交换,在调用函数swap时,需要用两个地址(或两个指向整数的指针)作为参数,比如

swap(&num1, &num2);

其中num1和num2是两个整型变量,这样他们的值通过调用swap函数便得到了交换。需要指出的是,传递数组不需要使用运算符&, 因为数组名实际上是一个常量指针,然而传递数组元素就不同了,仍需要以元素的地址作为参数才能在函数中修改元素的值并返回调用函数。

B——能更加方便地处理字符串与数组
如前文所述,数组名与字符串名都是常量指针,指针和数组名有时候是可以替代的。用指针编写数组下标标达式可节省编译时间,而且有时表示相对偏移量会更加方便,这方面的技巧本文就不多介绍了。

C——能动态地分配空间并直接处理内存地址
不像BASIC等一些其他高级语言,C语言要求同一层次中声明语句不能被置于任何执行语句之后,这决定了C中的数组是完全静态的,因为数组的长度必须在声明的时候确定,也就是说不能通过输入语句之类的方

法临时决定数组的大小,要想建立和维护动态的数据结构就需要实现动态的内存分配并运用指针进行操作。

现在将关于内存操作的几个重要函数列表如下:

函数
作用

void *calloc(size_t nmemb, size_t size); 为含nmemb个对象的数组分配空间,每个对象的大小为size.所分配的空间初始化成全零。函数calloc返回空指针或一个指向所分配空间的指针。
void free(void *ptr); 回收ptr所指向的空间,即使该空间可用于再分配。
void malloc(size_t size); 分配由size指定大小的对象空间。函数返回一个空指针或指向所分配空间的指针。
void *realloc(void *ptr, size_t size); 将ptr所指对象的大小改为size指定的大小。在新旧空间的较小空间中的对象的内容不被改变。如果新空间大,该对象新分配部分的值是不确定的。若ptr是空指针,函数realloc的行为类似指定空间的函数malloc。如果size为零且ptr不是空指针,其释放所指向的对象。函数realloc返回一个空指针或指向可能移动了的分配空间的指针。
注:这些函数的原型在通用工具头文件中。

运用这些函数我们可以实现动态数据结构的处理,举个简单的例子,我们要模拟建立变长数组,只需使用如下语句:

int n, *ptr;
scanf("%d", &n);
ptr=malloc(n*sizeof(int));

这样,系统就分配了存放有n个元素的数组所需要的空间,ptr存储了分配内存的首地址,之后通过指针的复引用即可进行赋值等操作。需要提醒的是,如果没有可用的内存,在调用malloc函数时就返回NULL指针,所以这时一般应判断一下指针是否为NULL再进行下一步操作。

由此可见,尽管指针通常以变量的地址作为其值,但有时候它指向的地址根本就没有变量名,这时候只能通过复引用指针进行一系列的操作。

D——能有效地表示复杂的数据结构(本部分内容建议初学者在弄清一切基本概念之前可以先不看)
在初学者接触令人费解的数据结构之前,必须先了解两个新的概念——“指向指针的指针”(也称二级指针)和“自引用结构”。

一级指针包含一个地址,该地址中存放具体的值,而二级指针包含的地址中存放的是另一个地址,因此也可以说二级指针是指向指针的指针。

声明一个二级指针要使用**,例如:

int **ptr;

声明了一个指向整数的二级指针。二级指针到底有什么用呢?其实就像通过给函数传递地址,我们就可以修改具体的值一样,给一个函数传递二级指针,就可以将修改地址并将其返回调用函数,这在数据结构的管理上相当重要。

一般的结构包含若干成员,而自引用结构必定包含一个指针成员,该指针指向与自身同一个类型的结构。例如,如

下结构定义就定义了一个自引用结构:

struct node{
int data;
struct node *nextptr;
};

通过链节nextptr可以把一个struct node类型的结构与另一个同类型的结构链在一起。我们可以通过自引用结构建立有用的数据结构,如链表、队列、堆栈和树。考虑到篇幅问题与难度关系,笔者仅就比较基本的单向链表给出机械工业出版社《C程序设计教程》(第二版)中的源程序。我不推荐初学者过早接触数据结构,因为其中涉及了二级指针和自引用结构的大量应用,在打牢基础之前基本不可能完全读懂程序就更不要说运用了。对数据结构有兴趣的同学可以再和我进行交流。

五、学习指针与内存的一点建议
很多人在刚接触指针的时候可能会觉得比较抽象,所以在练习的时候总是回避指针的应用,尽量用原有的方法解决问题,越是这样就越难进步。我建议大家即使面对不用指针也能解决的问题,也应该尝试使用指针,尤其是模拟函数的传引用调用等,只要多多练习,其实掌握基本概念还是很轻松的。再接下来就是要多思考什么时候用指针显得更方便和实用,逐渐掌握怎么用指针的真谛。毫不夸张地说指针是C的核心,学好指针是驾驭C语言的必由之路,大家加油干吧。

相关主题
文本预览
相关文档 最新文档