C语言研究性学习的路线之号外:一些朋友所提问题的反馈及第9章 指针

C语言研究性学习的路线 专栏收录该内容
11 篇文章 0 订阅

 首先感谢CSDN的朋友们,写本书困惑时总能受到你或TA的启发和指导,在此再次表示诚挚的感谢。

*周老师,在56上看到视频教程出到第9章指针(3)。不知道有没有出新的视频呢?请问一下,有没有全套的视频?我想下载或者购买全套的视频教程呢!
如果有请通知我一下吧!谢谢
这是我邮箱:chuanwei2004.student@sina.com

-你好!开学了比较忙没有录视频,现在只有1-9章的视频,后面的自定义数据类型、文件和位运算暂时只能看课件或看C语言研究性学习的路线了。视频下载地址博客中有,我也会发到你邮箱中。学习C语言时有什么心得,欢迎来信交流。

*计算机中0.1+0.1并不等于0.2 这句话是什么意思呢? 求高手解答

-建议看一下视频C01预备知识二进制,本书的例2-8回答了这个问题(可看视频也可看样章)。“小数部分是由0.5 0.25 0.125 拼起来的”的形容很形象。

*谁用vc呀,gCC不爽?

-C语言与操作系统无关,也与编译系统无关。对于初学者,哪个编译系统容易上手,可能就是最佳的选择。

*其实讲清楚指针就很好啦...

-我也经常这么想,可惜我是个教师要面对N多学生.......(特意把指针一章附于后面,谢谢你给我的勇气)

*楼主的整理的确是一个重要的一环——基础。但是,我认为我认为还有一环——应用能力,更重要。试想,当我们熟知了C语言了的规则了以后,却还是连一个使用的程序都写不出来,甚至不能看懂别人写的一个播放器程序,那么这些关于c规则的学习又有什么意义呢。所以要用,一定要在用的过程中专研。所以不要动不动谈语言的特性和语法规则。比如,我们经常说c语言数组越界问题。这算是一种语法规则(或者漏洞)还是一种可以利用的特性。据我所知,有一种病毒就是利用这个特性来越过系统的密码的。所以我们千万不能单独谈语言本身及其特性。

-这会是一个系列,关于C语言的方方面面。我同意你的看法,算法或者说语言的应用是精髓。(我想请你到zeq126.56.com看一下“递归算法课件版” 并继续关注本系列中关于算法的部分)。

*研究型学习肯定离不开汇编的,堆、栈的理解等等。

-研究是分层次的,C语言是高级语言。这里的“研究”只是针对C语言的每个知识点提问“为什么”。

*纯广告贴! 当做少儿读物可以,太基础了。。。

-尴尬。确实如此。

 

 

 

 

 

 

 

 

 

 

 

本书的前四章内容可到http://wenku.baidu.com/view/fd0aa30879563c1ec5da71b6.html下载。

大家可能最感兴趣的指针一章首发(:-)很早就想发了,但总害怕处理图片)CSDN。(建议不喜欢看书的同学直接看视频)

其它章节只能到百度文库下载课件,通过课件了解内容了,还好课件还算详细。(也可到115下载课件http://115.com/file/clqui38l#
新编C语言程序设计教程ppt.rar)

第9章 指针

指针是C语言的精华,指针让C语言威力无穷,魅力四射。为什么scanf函数的实参有时要加一个&操作符,有时却不加?为什么数组作为参数时就可以改变实参的值?这些前面遗留的问题都与指针有关,这些问题的答案均可在本章中找到。

指针是C语言中特殊的数据类型。整型变量所标识的存储单元中存放整数,浮点型变量中存放浮点数,那么指针变量所标识的存储单元中存放的显然是指针,但是指针是什么呢?

9.1 指针类型

9.1.1 变量的左值和右值

变量用于标识存储单元。计算机中的内存以字节为单位编号。编号多为32位的二进制数,从0号开始,即0x0000 0000、0x0000 0001、……、0xffff ffff。计算机中只用内存编号(又称内存地址)标识内存单元。

如果定义并初始化了一个整型变量如int i = 5;,则计算机中的内存状态可能如图9-1所示。

 

图9-1 变量i的内存状态图

从图9-1可知,整型变量i所标识的存储单元共4个字节,通常以存储单元的首字节地址作为该存储单元的地址,即整型变量i所标识的存储单元的地址为0x0012 ff00,类型为整型。当取变量i的值时,计算机会把从0x0012 ff00处开始的4个字节作为一个整体,取出其中的内容,然后按整型解码最终得到变量i的值为5。

存储单元如宿舍,其地址像宿舍号(如408),其存储的内容如住宿者(如王五),相关变量名如宿舍的雅称(如liaozhai)。

由以上分析可知,变量既标识存储单元的地址又标识其存储的内容,因此变量比如整型变量i也有两个值。整型变量i的一个值是地址0x0012 ff00,另一个值是内容5。变量i在使用时表现为何值呢?

例9-1分析语句i = 5; j = i;中整型变量i的值。

分析:

语句i = 5;的操作结果是把5放入变量i所标识的存储单元中,也就是把5的补码存入地址为0x0012 ff00的存储单元中,变量i的值此时实为地址0x0012 ff00。

语句j = i;的操作结果是把变量j所标识的存储单元的状态设置成与变量i的相同,即把变量j的值也设置成5,变量i的值在此处实为内容5。

位于赋值操作符的左边时,变量的值通常表现为地址,由此称变量的地址为变量的左值;位于赋值操作符的右边时,变量的值通常表现为内容,由此称变量的内容为变量的右值。

变量的左值都为地址,而右值则可能为整数、浮点数、字符等。指针变量的特殊之处在于其右值也为某存储单元的地址,其左值和右值均为地址。如果认为指针变量存储的是指针,则这里的指针显然为某存储单元的地址。需强调:存储单元的地址只是其首字节地址,仅仅凭地址而不知道类型(存储单元的大小、编码格式等)是无法正确访问存储单元的。

9.1.2 指针变量定义和初始化

指针变量用“*”号定义,但仅用*号标明变量的类型为指针还不行,必须知道指针变量存储的是何种存储单元的地址。因此,指针变量的一般定义形式为:

类型 *标识符

其中,类型规定了与指针变量存储的地址相关的存储单元是何类型,也就是说,一个指针变量只能存储规定类型的存储单元的地址。类型可以是C语言中的整型、浮点型、数组等。

如语句int *pi;定义了一个指针变量pi,pi的内容(右值)应为一个int型存储单元的地址;语句double *pf;定义了一个指针变量pf,pf的右值应为一个double型存储单元的地址。

有int i=5, *pi;,且已知变量i的左值为0x0012 ff00。现设指针变量pi的内容为0x0012 ff00,则相关的内存状态可能如图9-2所示。

图9-2  变量i和变量pi的存储状态图

从图9-2可知,指针变量pi的左值为0x0012 ff80,右值为0x0012 ff00;整型变量i的左值为0x0012 ff00,右值为5。

如果指针变量所指向的存储单元为整型,则称之为整型指针变量;如果指针变量pi的右值为变量i的左值(如图9-2所示),则称指针变量pi指向变量i。

怎样把变量i的左值赋给指针变量pi呢?

第一种作法:pi = 0x0012 ff00;

分析:

假设变量i的左值为0x0012 ff00,在语句pi = 0x0012 ff00;中,0x0012 ff00仅被看做一个十六进制的整型字面量,指针变量要求用地址为其赋值,两者类型不匹配。

第二种作法:pi = i;

分析:

语句pi = i;中变量i表现为右值5,因此语句pi = i;相当于pi = 5;,同样类型不匹配。

类型不匹配时可以用强制类型转换,正确的作法为:pi = (int*)0x0012 ff00;。该语句在执行时,会先定义一个临时无名指针变量(假设为int *ptemp;),然后强制地把整型字面量0x0012 ff00作为地址赋值给临时无名指针变量(此时整型指针变量ptemp的右值为0x0012 ff00),最后再用临时无名指针变量给指针变量pi赋值(pi = ptemp;),从而使指针变量pi的右值变为0x0012 ff00。

由于编程时不可能知道某变量的左值,因此这种通过强制类型转换把变量的左值赋给指针变量的方法实际上是行不通的。

9.2 指针操作符和空指针

9.2.1 指针操作符

取地址操作符&和间接引用操作符*与指针相关,故称为指针操作符。

单目操作符&的操作对象是一个变量,操作结果是该变量的地址。使用&i就可以获得变量i的左值,即变量i的地址。

有int i = 5,*pi;,用语句pi = &i;就可把变量i的地址赋值给指针变量pi,从而使整型指针变量pi指向整型变量i。

间接引用操作符*也是单目操作符,操作对象是一个地址型的量(如指针变量),操作结果为该地址所标识的存储单元的内容。

例9-2 设有如图9-2所示的i和pi,则对于整型变量j,分析语句j = *pi;和语句j = *i;。

分析:

在语句j = *pi;中,指针变量pi表现为右值即地址0x0012 ff00,间接引用操作符*会获得此存储单元的内容,也就是变量i的右值5,故语句j = *pi;相当于j = 5;。

在语句j = *i;中,变量i也表现为右值整数5,间接引用操作符*的操作对象必须是一个地址型的量,显然此处类型不匹配,有语法错误。

改正这个错误有两种方法。

第一种,用取地址操作符&获得变量i的地址,把原语句改为j = *(&i);。在语句j = *(&i);中,&i的操作结果为变量i的左值0x0012 ff00,间接引用操作会获得此存储单元的内容5,故此时j = *(&i);相当于j = *pi;或j = i;。

第二种,用强制类型转换操作把语句j = *i;中的变量i的右值整数5强制转换为一个地址,原语句变为j = *(int*)i;,实际上为j = *(int*)5;。在语句j = *(int*)5;中,强制类型转换操作把整数5强制转换成编号为5的地址。此时虽然没有了语法错误,但是地址为0x0000 0005的存储单元通常不会分配给应用程序且不允许应用程序读写,因此在执行该语句时会因为读写不允许读写的存储单元而出现非法内存访问错误。

例9-3 如有int i = 5, *pi = &i;,则一般情况下*pi可以与变量i互换使用。

分析:

设指针变量pi与整型变量i的内存状态如图9-2所示。

由例9-2可知,*pi在赋值操作符右边时,其值就为变量i的内容5,即j = *pi;与j = i;相同,因此,当变量i位于赋值操作符右边表现为内容时,*pi与变量i可以互换使用。

当变量i为左值时,如语句i = 3;中变量i的值为地址0x0012 ff00。在语句*pi = 3;中*pi的值为多少呢?此时指针变量pi也表现为左值即它本身的地址0x0012 ff80,间接引用操作符*将取此地址处的内容,即*pi的结果为0x0012 ff00,语句*pi=3;的作用也是将整数3存储到地址为0x0012 ff00处的存储单元中,与语句i = 3;相同。

综上所述,当整型指针变量pi指向整型变量i时,*pi与变量i在一般情况下可以互换使用。

例9-4 分析下面程序的运行情况。

#include <stdio.h>

#define PR(x, y) printf("%3.1f, %3.1f\n", x, y)

void main( )

{

    float fa = 2.3, fb = 3.2, *pf1 = &fa, *pf2 = &fb;

    float *pt, f;

    PR(*pf1, *pf2);

    pt = pf1;

    pf1 = pf2;

    pf2 = pt;

    PR(*pf1, *pf2);

    PR(fa, fb);

    f = *pf1;

    *pf1 = *pf2;

    *pf2 = f;

    PR(*pf1, *pf2);

    PR(fa, fb);

}

 

 

图9-3 例9-4中变量的关系图

 

分析:

程序执行时变量fa,fb,pf1与pf2的关系如图9-3(1)所示,故此时*pf1和*pf2的输出值为2.3和3.2。语句pt = pf1;执行后,变量的关系图如图9-3(2)所示。语句pf1 = pf2;执行后,变量的关系图如图9-3(3)所示。语句pf2 = pt;执行后,变量关系图如图9-3(4)所示。因此,*pf1和*pf2的输出值为3.2和2.3,而fa和fb的值仍为2.3和3.2。

对于下面的三条语句,因为*pf1与pf1所指向的变量fb可互换使用,*pf2与变量fa可互换使用,所以,最后输出时*pf1和*pf2的值为2.3和3.2,而fa与fb的值为3.2和2.3。

9.2.2 空指针

例9-5 下面这段程序有问题吗?

#include <stdio.h>

void main( )

{

   int i, *pi;

   *pi = 5;

   printf("%d,%d\n", i, *pi);

}

分析:

语句*pi = 5;中,*pi是指针变量pi的内容,即某存储单元的地址,整数5将存储在此存储单元中,但是指针变量pi的值是多少呢?

指针变量pi和变量i都没有初始化。C语言中变量没有初始化时,全局变量的值默认为0,局部变量的值则没有规定,因此指针变量pi的值在此处通常不可预知。语句*pi = 5;执行时将向一个位置不可预知的存储单元写入数据,这样的操作可能导致非法内存访问错误或程序逻辑上的错误。

类似本程序中的指针变量pi,右值为不属于程序所拥有存储单元的地址的指针变量称为“野指针”。间接引用野指针的右值将导致非法内存访问错误或其他不可预知的错误。预防由野指针引发的错误的常用办法是把没有明确指向的指针变量设置成“空指针”,而在使用指针变量时检测它是否为空指针。

所谓空指针是指右值为0的指针变量。地址为0的存储单元不可能分配给应用程序使用,因此约定指向此处的指针变量为空指针。空指针指向明确的存储单元不再是野指针。此外,0是一个特殊的整数,它是唯一不用类型转换就能直接赋值给指针变量的整数。如语句int *pi = 0; double *p = 0;等均合法。

直接用整数0给指针变量赋值容易让人误解,为此在stdio.h中定义了一个值为0的NULL宏来取代0。如int *pi = NULL; 和float *pf = NULL;语句定义了两个指针变量pi和pf,并且它们被初始化成空指针。

把没有明确赋值的指针变量设置成空指针(p = NULL),在使用时检测指针是否为空指针(if(p == NULL))可以有效地避免野指针带来的问题。

9.3 指针与函数

9.3.1 指针作为函数参数

函数的参数可以是指针类型,当参数为指针这种特殊的数据类型时,函数的调用过程呈现出了“新”的特点。

例9-6 分析下面的函数,并编程测试。

void swap(int *px, int *py)

{

int temp;

temp = *px;

*px = *py;

*py = temp;

}

分析:

swap函数用于交换两个整型变量的右值,它的形参是两个整型指针。用swap函数交换整型变量i(int i = 2;)和j(int j = 3;)的值时,用语句swap(i,j);调用函数可以吗?

C语言是传值调用,因此函数调用swap(i, j)相当于swap(2, 3)。被调用的函数执行时会用实参的值给形参赋值,即有px = 2;和py = 3;,此处显然有语法错误,整数2和3是不能作为地址直接赋值给指针变量的。使用swap函数时,应该把变量i和j的地址传值给形参,正确地调用形式为swap(&i, &j)。

测试程序如下:

#include <stdio.h>

void main( )

{

   int i=2, j=3;

   printf("%d,%d\n", i, j);

   swap(&i, &j);

   printf("%d,%d\n", i,j);

}

设main函数执行时,变量i和j的内存状态可能如图9-4所示。

图9-4 变量i和j的内存状态图

下面详细分析函数调用swap(&i, &j)的执行过程。

由图9-4可知,&i和&j的值分别为0x0012 ff00和0x0012 ff04,则swap函数调用执行时,形参px和py的右值分别为0x0012 ff00和0x0012 ff04。

在语句temp = *px;中,px的右值为0x0012 ff00,故*px的值为2!这里怎么使用了main函数中的变量i,得到了i的值2?swap函数中没有使用变量i,因为根本就没有出现变量i。*px的值为2是通过把地址为0x0012 ff00类型为整型的存储单元的内容取出得到的。只要存储单元为程序所拥有,间接引用操作符*就可以通过地址得到其储存内容。程序拥有地址为0x0012 ff00的存储单元的时间实际上是变量i的生存期,在swap函数执行期间,变量i一直存在,即相关存储单元一直为程序所拥有,虽然在swap函数中由于变量作用域的限制不能通过变量i访问该存储单元。现在,利用指针变量通过地址在swap函数中可以访问该存储单元了!

语句*px = *py;把地址0x00012 ff04处的内容复制到地址为0x0012 ff00的存储单元中。由于是直接写入存储单元,因此,在main函数中与此存储单元相关的变量i的值自然变为3了!

语句*px = temp;把变量temp的右值2复制到了地址为0x0012 ff04的存储单元中。

测试程序的输出:

2,3

3,2

注意:

1. 指针变量通过地址实现了对存储单元的直接读写,没有了利用变量访问相关存储单元时作用域的限制。

2. 间接引用操作符*的操作结果正确与否取决于相关存储单元是否为程序所拥有。

3. 形参为指针时,由于获得了特殊的值——实参相关存储单元的地址,所以在函数中可以通过指针变量对存储单元的直接写入来改变实参的值。有时也称形参为指针时函数的参数传递方式为“地址传递”,而形参为其它数据类型时称为“值传递”。准确地说函数的参数只传递“值”,只是当形参的类型为指针时值为地址。

例9-7 分析下面的程序

#include <stdio.h>

void add(int x, int y, int *pr)

{

   *pr = x + y;

}

void main( )

{

   int i = 23, j = 32, k = 0;

   add(i, j, &k);

   printf("%d + %d = %d\n", i, j, k);

}

分析:

函数add中参数pr用于保存函数的返回值。

9.3.2 指针作为函数返回值

函数的返回值类型也可以是指针。

例9-8分析下面的程序

#include <stdio.h>

int *test( )

{

   int i = 5, *pi = &i;

   return pi;

}

void main( )

{

   int *pj;

   pj = test( );

   printf("%d\n", *pj);

}

程序的输出结果:

5

分析:

test函数返回一个整型指针,也就是一个类型为整型的存储单元的地址。其实际的返回值为变量i所标识的存储单元的地址。

在main函数中,test函数返回的地址会被赋值给指针变量pj,此时指针变量pj指向了变量i,接着输出了指针变量pj所指向的变量i的值5。

好像一切正常,但这个程序有问题。

程序拥有变量i所标识的存储单元仅限test函数执行期间,因为变量i的生存期仅限于此。在main函数中,指针变量pj虽然获得了变量i的地址,但由于此时相关存储单元不再为程序所拥有,故指针变量pj变成了野指针。

可是程序正确地输出了变量i的值5,这里有两个疑问:

第一个疑问,程序为什么没有出现非法内存访问错误?

只有程序访问了无权访问的存储单元时才会出现非法内存访问的错误。本例中,变量i所标识的存储单元不再为程序所拥有,但是程序可以访问该存储单元,因此不会出现非法内存访问的错误。应用程序无权访问的存储单元多为那些地址编号较小仅能由系统软件使用的存储单元。

第二个疑问,变量i的生存期结束了,为何还能得到正确的内容5?

变量i 的生存期结束了仅表明相关存储单元可以再分配给其他变量使用,相关存储单元的内容不会被改变,因此,即使该存储单元已经不属于程序所拥有,读取其内容时依然可以得到整数5。

强调:

当存储单元不再属于程序所拥有时,即使在程序中可以通过指针访问该存储单元,也不应该再使用该存储单元了,以免出现不可预知的错误。

例9-9 分析下面的程序

#include <stdio.h>

int *test( )

{

   int i = 5, *pi = &i;

   return pi;

}

void test2( )

{

   int j = 3;

}

void main( )

{

   int *pj;

   pj = test( );

   test2( );

   printf("%d\n", *pj);

}

分析:

本程序的输出结果为3,不是预期的i的值5。

在main函数中,指针变量pj获得了变量i所标识的存储单元的地址,但与此同时,该存储单元随着变量i生存期的结束而变成了可分配存储单元。当main函数中调用test2函数时,该存储单元又被分配给了变量j且被初始化为3。虽然test2函数返回后,该存储单元再次成为了“空闲”的存储单元,但是其内容已经变成了3,所以在main函数中通过地址输出该存储单元的内容时值为3。

再次强调:

一定要保证指针变量指向程序所拥有的存储单元,以免出现野指针。

9.4 指针变量的运算

9.4.1 指针支持的运算

指针变量可以与一个整数做加法或减法运算。

如果一个整型指针变量pi的右值为0x0012 ff00,则pi + 1的值是多少呢?

做加法运算时变量pi应表现为右值0x0012 ff00,但是pi + 1的结果不是0x0012 ff01!

指针变量指向的是存储单元,其值0x0012 ff00仅是存储单元的首地址,pi + 1的结果也为指针变量且指向后面相邻的整型存储单元,因此新的指针变量的右值为0x0012 ff04,其类型与指针变量pi的相同。

例9-10 分析下面程序的输出

#include <stdio.h>

void main( )

{

   double a[ ] = {1.1, 2.2, 3.3}, *p;

   int i;

   p = &a[0];

   for(i=0; i<3; ++i)

      printf("%5.1lf", *(p+i));

}

分析:

设程序运行时变量的内存状态如图9-5所示。

图9-5 例9-10的内存状态图

当i = 0时,p + i的值为0x0012 ff00,*(p + i)的值为1.1;

当i = 1时,p + i的值为0x0012 ff08,*(p + i)的值为2.2;

当i = 2时,p + i的值为0x0012 ff10,*(p + i)的值为3.3。

例9-11 设指针变量p的右值为0x0012 ff00,求其定义如下时p+1的右值。

1. char *p;    2. char (*p)[5]

分析:

1. 指针变量p所指向的存储单元为字符型,故p+1的右值为0x0012 ff01,相关存储单元的类型也为字符型。

2. char (*p)[5]定义中先解释*p,故变量p为指针变量,其类型为char [5],即长度为5的字符型数组,因此,p + 1的右值为0x0012 ff05,相关存储单元的类型也是长度为5的字符型数组。

注意:

char *p [5]相当于char *(p [5] ),先解释p [5],定义一个长度为5的数组p,数组元素的类型为char *即字符型指针。

指针变量的减法与加法相同,p - 1的结果为指向前面相邻的存储单元的指针变量,其右值为相关存储单元的首地址,类型也与变量p的相同。

两个同类型的指针变量可以相减,结果为整数,表示两个变量之间相差几个同样的存储单元。对于图9-5的指针变量p,(p + 2) – (p + 0)的值为2。p + 2的值为a[2]的地址0x0012 ff10,p + 0的值为0x0012 ff00,两者相差2个存储单元(double类型共16个字节)。

对于指针变量p,p = p + 1;同样可简写为p += 1;或++p;、p++;。p = p – 1;与此类似。

两个同类型的指针变量可以进行等于(==)或不等于(!=)比较运算,如果两个指针变量相等,则它们指向同一块存储单元。两个同类型的指针变量也可以进行>、<、>=和<=比较运算。

指针变量的运算通常与数组相关时才有实际意义。

9.4.2 表达式的左值性和右值性

有int i = 5, *pi = &i;,且变量i和pi 的内存状态如图9-6(1)所示。

图9-6 变量i和pi 的内存状态图

由上节可知,pi + 1的结果为指向后面相邻的一个类型相同的存储单元的指针变量,其右值为0x0012 ff04,但其左值是多少呢?

pi + 1的计算在运算器中进行,计算结果也位于运算器中。运算器中的存储单元仅供运算器使用,应用程序只能读其中的内容而不能写入数据。表示pi + 1的结果的指针变量可理解为一个无名临时变量。与无名临时变量相关的存储单元多位于运算器中。强制类型转换的结果、C语言中许多表达式的结果均可理解为无名临时变量。无名临时变量的特点是程序只能读取其值而不能写入数据。以赋值语句pi + 1 = 3;为例,该语句先进行pi + 1的操作,此时变量pi虽然在赋值操作符的左边,但要进行加法运算,变量pi表现为右值,运算结果为图9-6(2)所示的无名临时变量。应用程序不能使用无名临时变量的左值向其写入数据,因此语句pi + 1 = 3;不可能把整数3存入相关存储单元,也就是说该语句非法!

C语言中每个表达式还有一个属性:左值性或右值性。具有左值性的表达式可以位于赋值操作符的左边,而具有右值性的表达式只能位于赋值操作符的右边。表达式pi + 1就具有右值性而不具有左值性。C语言中许多表达式的结果均为无名临时变量,因此可以简单地把表达式的左值性问题归结为与无名临时变量相关的地址是否可以被应用程序写入的问题。如果与无名临时变量相关的地址不允许程序写入,则相关的表达式就表现为右值性。显然大部分C语言表达式表现为右值性。

例9-12 相关变量的内存状态如图9-6(1)所示,判断表达式*(pi + 1)和*(&i)是否表现为左值性。

分析:

pi + 1的结果为如图9-6(2)所示的无名临时变量,其左值不知,右值为0x0012 ff04。*(pi + 1)在赋值操作符左边时表现为无名临时变量的存储内容,其值为地址0x0012 ff04,注意,无名临时变量可以读取不能写入。应用程序有权存取与地址0x0012 ff04相关的存储单元,因此,类似*(pi + 1) = 3;的语句可以正常执行,表达式*(pi + 1)为左值性。

&i的结果也可理解为如图9-6(2)所示的无名临时变量,其左值不知,右值为0x0012 ff00。*(&i)在赋值操作符左边时表现为无名临时变量的存储内容,其值为地址0x0012 ff00。应用程序有权存取与地址0x0012 ff00相关的存储单元,因此,类似*(&i ) = 3;的语句可以正常执行,表达式*(&i)为左值性。

注意:

本节只是简单地讨论了C语言中表达式的左值性问题。

9.5 指针与数组

在C语言中,指针与数组有着密切的关系,这种关系表现在对数组元素的引用可以改写成对指针的间接引用。有数组a,则a[e1]与*(a+e1)等价;有指针变量p,则*(p+e1)与p[e1]等价。C语言规定:数组变量(a)的右值为数组首元素的地址(&a[0])。

9.5.1 指针与一维数组

例9-13 已知int a[3] = {1, 2, 3}; int *pi;。

1. 分析表达式sizeof(a)、a = 3、sizeof(a+1)、*a+1、*(a+1)和pi = a;

2. pi = a后,分析表达式pi++、*pi++、*++pi和pi[2]。

分析:

设数组变量a在内存中的状态如图9-7所示。

图9-7 一维数组a的内存状态

从图9-7可知,数组变量a没有专属于自己的存储单元,并不是一个真正的变量。数组变量a的右值并非在某个存储单元中存储,而是人为规定的。从这个角度分析,数组变量a 是一个符号常量。与其它简单的符号常量不同,数组变量a是整个数组元素的代表,从这个角度分析,所有数组元素的存储单元也是数组变量a的存储单元。因此,数组变量a有存储单元但并不真正属于自己,数组变量a的右值只是规定的,数组变量只能算作虚拟的变量。

1. sizeof操作符返回与数据类型相关存储单元的长度,与数组变量a相关的存储单元是3个整型变量所占的空间,故sizeof(a) 的值为12(个字节)。

a = 3是把整数3存储到变量a相关的存储单元中。数组变量a仅仅是一个虚拟的变量,没有真正属于自己的储存空间,故此赋值操作不可能完成,因此这个赋值表达式有语法错误。

由于数组变量a的右值是数组首元素a[0]的地址0x0012 ff00,故*a与a[0]可以互换。在*a+1中,*a即a[0]表现为右值1,所以*a+1的值是整数2。

*(a+1)中,a表现为右值即a[0]的地址0x0012 ff00,该存储单元的类型为整型,故a+1的值为0x0012 ff04,为整型存储单元的地址,也就是a[1]的地址,因此,*(a+1)与a[1]可以互换。

pi = a是把数组变量a的右值存储到指针变量pi相关的储存单元中。数组变量a的右值为a[0]的地址,故pi = a相当于pi = &a[0],即pi指向了整型变量a[0]。

2. 由1可知,pi = a后,pi指向了整型变量a[0]。

pi++的值为pi,pi指向了变量a[0],故表达式pi++的值为a[0]的地址0x0012 ff00,与该地址相关的存储单元的类型为整型。需注意pi++会使pi的值加1,也就是说pi++之后,pi的右值变成了a[1]的地址0x0012 ff04,即pi指向了a[1]。

*pi++为*(pi++),pi++的值为pi,故*(pi++)的值为*pi,由于pi指向a[0],故*pi++的值为a[0]的值。需注意,表达式*pi++求值后,pi的值自增了1,pi指向了a[1]。

*++pi为*(++pi),++pi的值为pi加1后的新值,pi原先指向了a[0],加1之后pi指向了a[1],故表达式*++pi的值为a[1]的值。

pi[2]可理解为*(pi+2),由pi指向a[0]可知pi+2指向了a[2],故*(pi+2)可与a[2]互换,即pi[2]与a[2]可以互换。

例9-14 分析程序

#include <stdio.h>

void main( )

{

   int a[5], i, *pi;

   pi = a;

   for(i = 0; i <= 4; ++i)

       scanf("%d", pi + i);

   for (i = 0; i <= 4; ++i)

       printf("%d\t", *(a + i));

}

分析:

程序把用户输入的5个整数放入数组a中并输出了这5个整数。在语句scanf("%d", p + i);中,pi+i相当于&a[i]。在语句printf("%d\t", *(a+i));中,*(a+i)相当于a[i]。

例9-15分析程序

#include <stdio.h>

#define N 5

int a[N] = {23, 32, 25, 52, 21};

void main( )

{

   int i, j, min, temp, *pi;

pi = a;

   for(i = 0; i < N; ++i)

   {

      min = i;

      for(j = i + 1; j < N; ++j)

         if(*(pi + min) > *(pi + j))

             min = j;

      temp = pi[i];

      pi[i] = pi[min];

      pi[min] = temp;

   }

   pi = a;

   for(i = 0; i < N; ++i)

      printf("%-3d", *pi++);

}

分析:

程序把数组元素按升序排序。排序时先从待排序的数组元素中找出最小的,再把此数组元素与待排序数组元素中的第一个交换。重复这个过程直到整个数组有序。

例9-16 分析例7-18

#include <stdio.h>

void swap(int a[])

{

   int temp;

   temp = a[0];

   a[0] = a[1];

   a[1] = temp;

}

void main( )

{

   int b[2] = {2, 3};

   swap(b);

   printf("b[0]=%d,b[1]=%d\n", b[0], b[1]);    

}

分析:

C语言规定,形参的类型为数组时,数组类型会退化为相应的指针类型。对于一维数组a,由于它的值为首元素a[0]的地址,即整型存储单元的首地址,所以函数的形参int a[2]或int a[ ]退化为指向整型变量的指针,也就是说swap(int a[ ])应理解为swap(int *a),在这里形参a不再是一个数组变量,而仅是一个普通的指针变量。在main函数中,函数调用swap(b)执行时,实参b的值为b[0]的地址,故此时参数a的值为b[0]的地址,指针变量a指向了b[0]。

9.5.2 指针和二维数组

例9-17 如何理解二维数组?

分析:

以二维数组int a[3][2] = {{1, 2}, {21, 22}, {31, 32}}为例,设它的内存状态如图9-8(1)所示。

图9-8 二维数组a的内存状态

二维数组a有3个数组元素a[0]、a[1]、a[2],这三个数组元素的类型均是长度为2的一维整型数组。

如果把二维数组变量a看作一个虚拟变量,则它的存储单元长度为24个字节(sizeof(a)的值为24),它存储的内容是首元素a[0]的地址0x0012 ff00,相关存储单元的类型为有2个数组元素的一维整型数组,因此,二维数组变量a的右值所标识的存储单元长度为8个字节(sizeof(*a)的值为8)。

如果把一维数组a[0]看作一个虚拟变量,则它的存储单元长度为8个字节(sizeof(a[0])的值为8),它存储的内容是其首元素a[0][0]的地址0x0012 ff00,相关存储单元的类型为整型,因此,一维数组变量a[0]的右值所标识的存储单元的长度为4个字节(sizeof(*a[0])的值为4)。

二维数组变量a,一维数组变量a[0],整型变量a[0][0]三者的关系如图9-8(2)所示。

下面分析一些表达式以帮助理解上面的概念。

1. a, a + 1, a + 2

分析:

变量a为虚拟的二维数组变量,其存储单元的长度为24但不可写入。a的右值为首元素a[0]的地址0x0012 ff00,类型为有2个元素的一维整型数组。

a + 1中二维数组变量a表现为右值,故a + 1的值为地址0x0012 ff08,相关存储单元的类型为有2个元素的一维整型数组,即a[1]的地址。

同理a + 2是a[2]的地址。

2. *a, *a + 1,*(a+1) + 1和**a

分析:

*a与a[0]等价,二维数组a中a[0]为虚拟的一维数组变量,其存储单元不能写入,故不能赋值给*a(如赋值语句*a=23;有语法错误)。*a的右值为a[0]的内容,即首元素a[0][0]的地址0x0012 ff00,相关存储单元的类型为整型。

*a + 1中*a(a[0])表现为的右值,即类型为整型的存储单元(a[0][0])的地址0x0012 ff00,故表达式的结果为另一个整型存储单元的地址0x0012 ff04,即a[0][1]的地址。

*(a+1) + 1中*(a+1)与a[1]等价,且表现为右值即a[1]的内容。a[1]为一维数组变量,其值为首元素a[1][0]的地址0x0012 ff08,类型为整型,故*(a+1) + 1的值为0x0012 ff0c,相关存储单元的类型为整型,即a[1][1]的地址。

**a就是*(*a),也为*(a[0]),a[0]的值为其首元素a[0][0]的地址,故*(a[0])与a[0][0]等价,即**a与a[0][0]可以互换。

3. 有int *p = (int *)0x0012 ff00,则表达式p == a的值是否为真。

分析:

整型指针变量p和二维数组变量a的右值均为0x0012 ff00,但是整型指针变量p指向存储单元的类型为整型,而二维数组变量a指向的存储单元的类型为有2个元素的一维整型数组,两者类型不一致,因此两者的比较操作没有实际意义,也就是说表达式p == a有问题。

同理有int *p时,用二维数组变量a给整型指针变量p赋值即p = a也是有问题的。

只有指向有两个元素的一维整型数组的指针变量才能用二维数组变量a赋值。相关指针变量理想的定义方式为int[2] *p,而C语言中实际的定义方式为int (*p)[2](注意不用int *p[2]定义)。定义此类型的指针变量p后,就可以用p = a或p = &a[0]把二维数组变量a的右值保存在指针变量中。此时指针变量p、二维数组变量a、一维数组变量a[0]及a[0][0]的关系如图9-9所示。

图9-9 二维数组与指针

例9-18分析下面的程序

#include <stdio.h>

void main( )

{

   int a[3][2] = {{1, 2}, {21, 22}, {31, 32}};

   int i, j, (*p)[2], *pi;

   for(i=0; i<=2; ++i)

   {

      for(j=0; j<=1; ++j)

         printf("%3d", *(*(a + i) + j));

      printf("\n");

   }

   printf("\n");

   p = a;

   for(i=0; i<=2; ++i)

   {

      for(j=0; j<=1; ++j)

         printf("%3d", p[i][j]); //按*(*(p+i)+j)分析

      printf("\n");

   }

   printf("\n");

   for(i=0; i<=2; ++i)

   {

      pi = a[i];

      for(j=0; j<=1; ++j)

         printf("%3d", *pi++);

     printf("\n");

   }

}

分析:

程序用三种方式输出了二维数组a。

二维数组作参数时同样退化为相应的指针。对于上面的二维整型数组a,由于二维数组变量a的右值为a[0]的地址0x0012 ff00,相关存储单元的类型为有2个元素的一维整型数组,因此,形参int a[3][2]实际上相当于int (*a)[2],即二维整型数组变量a退化为指向有两个元素的一维整型数组的指针变量。

例9-19 用函数输出二维数组a。

#include <stdio.h>

void printA(int (*p)[2], int m)

{

   int i, j;

   for(i=0; i<m; ++i)

   {

       for(j=0; j<=1; ++j)

         printf("%3d", *(*(p + i) + j));

       printf("\n");

   }

}

void main( )

{

   int a[3][2] = {{1, 2}, 21, 22, 31, 32};

   printA(a, 3);

}

注意:

printA的首部也可改为void printA(int p[3][2],int m)或void printA(int p[ ][2],int m)。

9.5.3 指针与字符串

9.5.3.1 字符串常量

C语言中字符串常用字符数组存储。一维字符数组变量的值为首元素(字符变量)的地址,因此,一维字符数组作函数参数时退化为一个指向字符型变量的指针变量,同时可以把字符数组变量赋值给字符型指针变量。

例9-20 分析下面的程序。

#include <stdio.h>

void main( )

{

char ch[] = "Hello!";

char *str;

char *str1 = "Hello!";

str = ch;

puts(str);

*(str + 1) = 'i';

str[2] = '!';

*(str + 3) = '\0';

puts(ch);

puts(str1);

*str1 = 'A';

}

程序的运行结果为:

Hello!

Hi!

Hello!

分析:

字符型指针变量str指向字符数组ch的首元素ch[0],puts(str)与puts(ch)输出结果相同。当利用指针变量str修改了数组元素的值后,再次输出时字符串也随之改变。

语句char *str1 = "hello! ";定义了一个字符型指针变量str1,它初始化后指向了字符串常量"hello!"。C语言中字符串常量是指用一对双撇号括起来一串字符型字面量,通常以字符数组的形式存储于内存中一个“只读”的特殊内存区域,并且整个字符常量表现为与首字符相关的存储单元的首地址,因此,它可以赋值给一个字符型指针变量。语句put(str1);输出了整个字符串常量,而语句putchar(*(strl+1));输出了字符e。语句*strl = 'A';将使字符串常量的首字符变为字符'A',不过由于与字符串常量相关的存储单元只能读出不能写入,故此语句会引发内存访问错误导致程序出错。指针变量strl与字符串常量"hello!"的内存状态可能如图9-10所示。

图9-10 字符串常量

字符串常量的生存期为整个程序运行期间,因此程序中两个完全相同的字符串常量可能分享相同的存储单元。

9.5.3.2 指针数组和指向指针的指针

指针数组是数组元素为指针类型的数组,如语句char *a[3] = {"C", "C++", "Java"};就定义并初始化了一个一维的字符型指针数组,数组a的三个数组元素a[0]、a[1]和a[2]的类型为字符型指针。使用一维的字符型指针数组常见的错误为char *p; p = a;,即用一维的字符型指针数组变量a给字符型指针赋值。一维的字符型指针数组a可能的存储状态如图9-11所示。

 

图9-11 一维的字符型指针数组a的存储状态图

由图9-11可知,数组变量a的右值为a[0]的地址0x0012 ff00,相关存储单元的类型为字符型指针,也就是说0x0012 ff00存储单元的内容为字符型指针存储单元的地址。字符型指针变量p中存放的内容应为字符型存储单元的地址,而数组变量a的右值为字符型指针存储单元的地址,两者类型不匹配。可以用数组元素a[0]给字符型指针变量p赋值,如p = a[0];或p = *a;。

可以用语句char *(*pp);或char **pp;定义一个指向存储单元类型为字符型指针的指针。如果一个指针指向的变量为指针类型,则称该指针为指向指针的指针。可以用数组变量a给指向指针的指针变量pp赋值(pp = a;),赋值后指向指针的指针变量pp,数组变量a,和数组元素a[0]的关系如图9-12(1)所示。也可用指针变量p的地址给指向指针的指针变量pp赋值(pp = &p),此时指向指针的指针变量pp,指针变量p,数组元素a[0]的关系如图9-12(2)所示,设变量p的地址为0x0013 0000。

 

图9-12 指向指针的指针

例9-21 分析程序的输出

#include <stdio.h>

void main( )

{

char *a[3] = {"C", "C++", "Java"};

char *p, **pp;

puts(a[1]);

printf("%c\n", *(a[2] + 2));

pp = a;

puts(*(pp + 1));

printf("%c\n", *(*(pp + 1) + 1));

puts(*(pp + 1) + 1);

p = a[1];

puts(p + 1);

pp = &p;

puts(*pp);

puts(*(pp - 1));

}

程序的输出为:

C++

v

C++

+

++

++

C++

(

注意:

语句puts(*(pp – 1));的输出为乱码,从图9-12(2)中可知,pp – 1的结果为地址0x0013 fefc,与此地址相关的存储单元不属于程序所有,其为野指针。

一维指针数组作为函数参数时退化为指向指针的指针。

例9-22 分析程序的输出

#include <stdio.h>

void printA(int n, char **pp)

{

   int i;

   for(i=0; i<n; ++i)

      puts(*pp++);

}

void main( )

{

   char *a[ ] = {"C", "C++", "Java", "C#"};

   printA(sizeof(a)/sizeof(*a), a);

}

程序的输出为:

C

C++

Java

C#

注意:

函数printA的首部可以改写为void printA(int n, char *pp[])。

9.6 main函数和命令行参数

为了简便在前面章节中把main函数的返回值都定义为空,实际上main函数标准的定义形式为:int main(void) {……}或int main(int argc, char *argv[ ]) {……}。当程序不需要使用命行参数时使用第一种形式,需要命令行参数时使用第二种形式。

命令行参数是指以命令行方式运行程序时所带的参数。设工程编译后得到了一个名为test.exe的可执行文件,则以命令行方式运行程序的方法为:先启动“DOS”窗口(开始→运行→输入cmd回车或开始→程序→附件→命令提示符),再把当前目录转到工程所在目录的debug子目录(如E:\csample\test\debug),然后输入test a b cd回车运行程序。输入被空格分成了四个字符串,系统会把这些字符串传给main函数。如果main 函数用第二种形式定义,则当程序运行时参数argc的值是命令行中字符串的个数,此时argc的值为4。命令行中的每个字符串都被存储到内存中,并且*argv(即argv[0])指向第一个字符串,即文件名("text"),*(argv+1)(即argv[1])指向第二个字符串("a"),以此类推。除文件名之外的字符串"a"、"b"、"cd"就是所谓的命令行参数。

例9-23 请分析下面的程序

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char *argv[ ])

{

   int i, j, sum = 0;

   if(argc > 1)

   {

       while(argc-->1)

       {

          printf("%s + ", *(argv + argc));

          sum += atoi(argv[argc]);

       }

       printf("\b\b= %d\n", sum);

   }

   else

   {

       printf("请输入两个整数:\n");

       scanf("%d%d", &i, &j);

       printf("%d + %d = %d\n", i, j, i+j);

   }

   return 0;

}

分析:

1. 库函数atoi可以将由数字构成的字符串转换为相应的整数。

2. 带命令行参数执行该程序时,程序将计算命令行参数的和;不带命令行参数时程序会输出用户输入的两个整数之和。

3. 用VC6.0执行程序之前,可以单击【工程(Project)】|【设置(Setting…)】命令,在弹出的对话框中选择“调试(Debug)”标签找到“程序变量(Program arguments)”,在此可以设置运行程序时使用的命令行参数。如果输入23 32 52后运行程序(Ctrl+F5),程序将输出52+32+23=107。

注意:

1. 操作系统会获得main函数的返回值,main函数的返回值为0时表示程序运行顺利,正常退出。

2. 在main函数的第二种定义形式中,参数类型固定,但参数名可变。如也可以用如下形式定义main函数。

int main(int n, char **pp){……}

9.7 指向函数的指针变量

存放函数体中相关指令的存储单元通常位于内存中称为代码段的部分。与数组名类似,函数名的值在C语言中同样被规定为与该函数相关的存储单元的首地址。调用执行函数实际上就是执行从函数名标识的首地址开始的相关存储单元中的指令。如果一个指针变量可以用函数名赋值,则称该指针变量为指向函数的指针变量。利用指向函数的指针变量也可以获得与函数相关的存储单元的地址,有了这个地址就能够执行相关指令,也这就是说利用指向函数的指针变量也可以调用执行函数,与使用函数名调用执行函数类似。

如何定义指向函数的指针变量呢?

指针变量的定义用*号,函数的定义用一对圆括号。函数的作用是完成从输入到输出的转换,编译系统检查函数调用正确与否的关键在于实参的个数、类型是否匹配、返回值类型是否匹配。综上所述,在定义指向函数的指针变量时需体现以上诸多要素。简单地说,定义时只需在函数的首部中把函数名部分改为(*指针变量名),并省略形参名即可。如求两个整数和的函数的说明为int add(int m, int n);,则指向此函数的指针变量pf的定义为int (*pf)(int, int);。

注意:

1. 语句int *pf(int, int);为函数pf的声明,该函数有两个整型的形参,返回值类型为指向整型变量的指针。

2. 指向函数的指针变量pf的值可以是一类函数的首地址,这类函数的特点是有两个整型形参,返回值类型也为整型。

例9-24 使用指向函数的指针变量调用函数

#include <stdio.h>

double func(double x)

{

   return (x * (x - 3) + 2);

}

int main( )

{

   double (*pf)(double);

   pf = func;

   printf("func(3)=%.0lf\n", (*pf)(3));

   return 0;

}

分析:

函数func的功能是求f(x)=x2-3x+2的值,而指针变量pf指向了该函数,程序中使用指针变量pf求出了f(3)的值。

例9-25 利用梯形法求f(x)的定积分的公式为

 其中h=(b-a)/n

该公式可以用下面的函数实现。

double calDefInt(double (*pf)(double), double a, double b)

{

   double value, h;

   int i, n = 3000;

   value = ((*pf)(a) + (*pf)(b)) / 2.0;

   h = (b > a ? b - a : a - b) / n;

   for(i=1; i<n; ++i)

      value += (*pf)(a + i * h);

   value *= h;

   return value;

}

该函数可以用下面的程序测试。

#include <stdio.h>

#include <math.h>

/*此处省略了calDefInt函数和与例9-24相同的func函数的定义*/

int main( )

{

   printf("func(x)在[1,3]上的定积分为:%lf\n", calDefInt(func, 1, 3));

   printf("sin(x)在[0,3.1415926/2]上的定积分为:%lf\n", calDefInt(sin, 0, 3.1415926/2));

   return 0;

}

9.8 使用堆空间

存放程序中数据的内存通常分为两个区:静态存储区和动态存储区。与全局变量相关的存储单元位于静态存储区,它们在程序运行之前分配,在程序运行期间始终为程序所有。与局部变量相关的存储单元位于动态存储区的栈中,它们在程序运行期间定义时分配,超出作用域后释放。动态存储区中还有一种称为堆的存储空间,在程序运行期间可以根据需要利用库函数在其上分配一块内存。栈和堆是动态存储区中的两类存储空间。栈和堆中的存储单元都可以在程序运行期间分配或释放,但两者的管理方式不同。栈空间中存储单元由系统自动地分配和释放,而堆空间中的存储单元必须由程序员调用相关的库函数显式地分配和释放。如果程序中申请的位于堆空间中的存储单元在使用完毕后没有显式地释放,则它会一直为程序所拥有,直至程序运行结束。

库函数malloc用于在堆空间中申请一块存储空间,它的形参是一个无符号整型,指出需分配内存块的以字节为单位的长度。如果内存块分配成功,则malloc函数返回该内存块的首地址,否则它将返回空指针NULL。显然,需要使用指针变量接受malloc函数返回的地址,但是malloc函数分配的内存块是什么类型呢?或者要用什么类型的指针变量来保该地址呢?

malloc函数仅仅返回一个地址连续的内存块的首地址,该内存块在分配时没有指定类型。例如函数调用malloc(8)可能返回一个地址0x0044 02b0,但实际上与该地址相关的内存块从0x0044 02b0至0x0044 02b7共8个字节。如果此内存块用来存储整数,则它可以存储两个整数,此时内存块的类型像是一个有2个数组元素的整型数组。如果这块内存用来存储双精度的浮点数,则它可以存储一个双精度的浮点数,此时该内存块的类型是双精度的浮点型。malloc函数的返回值类型是void *。在C语言中void *表示指向void型存储单元的指针。void型的存储单元是“无类型”的存储单元,仅是一个地址连续的内存块。无类型的内存块可以强制转换为其它类型的存储单元。如果想用malloc函数分配的内存块存储整数,则使用方式为:int *pi = (int*)malloc(8);*pi = 2; *(pi+1)=3;。如果想用分配的内存块存储双精度的浮点数,则使用方式为:double *pf =(double *)malloc(8); *pf = 2.3;。

注意:

1. 可以定义一个void型的指针变量,其它类型的指针变量无需类型转换就可以直接给它赋值,但是,void型的指针变量在使用时必须强制转换为其它类型的指针变量。

2. malloc函数常见的使用方式为int *pi=(int*)malloc(2*sizeof(int));。

库函数free用于释放使用malloc函数申请的堆空间,free函数的首部为void free(void *memblock)。使用free函数释放堆空间时只需把内存块的首地址传给free函数即可,无需考虑它已被强制转换为何种类型了。

例9-26 分析下面的程序

#include <stdio.h>

#include <stdlib.h> //或<malloc.h>

int main( )

{

   int i, n, *pi, temp;

   printf("请输入整数的个数\n");

   scanf("%d", &n);

   pi = (int *)malloc(n * sizeof(int));

   if(pi != NULL)

   {

       printf("请输入%d个整数\n", n);

       for(i=0; i<n; ++i)

         scanf("%d", pi + i);

       for(i=0; i<n/2; ++i)

       {

         temp = *(pi + i);

         *(pi + i) = *(pi + n - i - 1);

         *(pi + n - i - 1) = temp;

       }

       printf("转置后的整数为:\n");

       for(i=0; i<n; ++i)

         printf(" %d ", *(pi + i));

       free(pi);

       pi = NULL;  //防止出现野指针

       return 0;

   }

   else

   {

       printf("申请空间失败!\n");

       return 21;  //程序运行失败!

   }

}

分析:

程序在堆空间中申请了可以存放n个整数的内存块,并且用整型指针变量pi指向了内存块的首地址,实际上,整型指针变量pi指向了一个有n个元素的整型数组的首地址。

注意:

1. 在VC6.0中使用库函数malloc和free时需要包含头文件stdlib.h或头文件malloc.h。

2. 堆空间使用完毕必须及时释放以防内存泄露,释放完毕后需把相关指针变量赋值成空指针以防止出现野指针。

内存泄露指由于疏忽或错误未能释放已经不再使用的堆空间内存。当程序中申请的一块堆空间内存没有指针变量指向它时,这块内存肯定会因无法释放而泄露了。为所有程序共享的堆空间容量有限,当一个程序因内存泄露占用了大量的堆空间时,其它程序可能会因申请不到堆空间而不能正常运行。

例9-27 分析下面的函数

void leak( )

{

   char *str;

   str = (char *)malloc(50 * sizeof(char));

   if(str != NULL)

   {

       printf("请输入一串字符:\n");

       gets(str);

       puts(str);

   }

}

分析:

该函数在堆空间中申请了一块内存,然后用它存储了用户输入的字符串,最后又输出了用户的输入。

该函数在调用执行时会发生内存泄露。指针变量str是局部变量,函数执行完毕其存储单元会自动释放,此时函数中在堆上申请的内存块将没有指针变量指向它,也就是说在程序中将无法释放此块内存,此内存块“泄露”了。每调用该函数一次,就泄露一块内存。

9.9 典型例题

例9-28 已知int a[3][2], i, j;,且有0 ≤ i < 3, 0 ≤ j < 2。

1. 写出几个表示数组元素a[i][j]所标示存储单元地址的表达式;

2. 写出几个表示数组元素a[i][j] 所标示存储单元的表达式。

分析:

1. 表示a[i][j]地址的常见形式为&a[i][i]。

由例9-17可知*(a+1)+1表示a[1][1]的地址,显然*(a+i)+j表示a[i][j]的地址。

又因*(a+i) 可与a[i]互换,故a[i]+ j也表示a[i][j]的地址。(当然通过分析也可得此结论)。

又由于a[i][j]与a[0][0]相距i*2+j个存储单位,所以a[i][j]的地址也可表示为&a[0][0]+i*2+j。

2. 表示数组元素a[i][j]的常见形式当然为a[i][j]。

由于*(a+i)可与a[i]互换,因此a[i][j]可用(*(a+1))[j]表示,也可用*(a[i]+j)表示,还可进一步替换为*(*(a+i)+j)。

如果a[i][j]的地址可表示为&a[0][0]+i*2+j,则a[i][j]自然可用*(&a[0][0]+i*2+j)表示。

例9-29 分析下面的程序

#include <stdio.h>

int main( )

{

   int a[2][3][4] = {{{100, 101, 102, 103}, {110, 111, 112, 113}, {120, 121, 122, 123}}, {{200, 201, 202, 203}, {210, 211, 212, 213}, {220, 221, 222, 223}}};

   int i, j, k;

   int (*p2)[3][4], (*p1)[4], *p0;

   printf("用指向二维数组的指针输出三维数组:\n");

   p2 = a;

   for(i=0; i<2; ++i)

   {

       for(j=0; j<3; ++j)

       {

          for(k=0; k<4; ++k)

             printf("%5d", p2[i][j][k]);

          printf("\n");

       }

       printf("\n\n");

   }

   printf("用指向一维数组的指针输出三维数组:\n");

   for(i=0; i<2; ++i)

   {

      p1 = a[i];

      for(j=0; j<3; ++j)

      {

         for(k=0; k<4; ++k)

             printf("%5d", p1[j][k]);

         printf("\n");

      }

      printf("\n\n");

   }

   printf("用指向整型的指针输出三维数组:\n");

   p0 = (int *)a;

   for(i=0; i<2; ++i)

   {

      for(j=0; j<3; ++j)

      {

         for(k=0; k<4; ++k)

             printf("%5d", *(p0+i*3*4+j*4+k));

         printf("\n");

      }

      printf("\n\n");

   }

}

分析:

三维数组a有两个数组元素a[0]和a[1],它们的类型为三行四列的二维数组,而三维数组变量a的右值为首元素的地址,所以可以把它赋值给指向三行四列的二维数组的指针变量p2。在输出数组元素时,p2[i][j][k]等价于*(*(*(p2+i)+j)+k)。可以用画图(类似图9-8和图9-9)的方式分析p2[i][j][k]的值。

数组a[i]为三行四列的二维数组,数组变量a[i]的值为首元素的地址,相关存储单元的类型为有4个元素的一维数组,故可以用数组变量a[i]的右值给指向有4个元素的一维数组的指针变量p1赋值。

与三维数组变量a的右值相关的存储单元类型为三行四列的二维数组,但是可以把数组变量a强制类型转换为整型指针。数组变量只是虚拟变量,在强制类型转换之后三维数组变量a的右值等价于a[0][0][0]的地址,因此语句p0 =(int *)a;可以用p0 = &a[0][0][0];代替。当然这里的&a[0][0][0]也可为**a或*a[0]。又因为数组元素的地址连续,所以可以依次输出各数组元素。

例9-30 有n个人围坐一圈,用1,2,…,n按顺时针方向为每个人编号。从某个人起,按顺时针方向进行1至m(m>0)的报数,报到m的人出圈;接着从下一个人继续1至m的报数,报到m的人出圈;一直进行这样的报数,直到所有的人都出圈为止,试问他们出圈的次序。

分析:

用一维数组的数组元素表示参加报数的人,数组元素的值为报数人的编号,用指针变量p指向当前要报数的数组元素,整型变量k的值为上个人报的数。

如果报数人需出圈用把对应数组元素赋值为0表示,则整个报数活动的安排如下:

第一步,判断当前要报数的人是否已经出圈,如果没有出圈则先报数(用++k表示),再判断报的数是否为m,如果是,则输出报数人的编号后让其出圈(此时变量k的值需清零,表示出圈人数的变量g的值需加1)。

第二步,调整指针变量p指向下一个数组元素。

重复上面两步,直到变量g的值为n时停止。

#include <stdio.h>

#define MAX 100

int a[MAX];

int main( )

{

   int n, s, m, k, g = 0, *p;

   printf("请输入总人数,开始者编号和出圈者所报数字:\n");

   scanf("%d%d%d", &n, &s, &m);

   //初始化数组

   for(p=a; p<a+n; ++p)

      *p = ++g;

   //准备报数

   p = a + (s - 1) % n;

   k = g = 0;

   //重复报数

   while(g < n)

   {

      if(*p != 0)

      {

         ++k;

         if(k == m)

         {

             if(g % 10 == 0)

                 printf("\n");

             printf("%5d", *p);

             *p = 0;   //标识出圈者

             k = 0;

             ++g;

         }

      }

       //调整指针变量p指向下一个数组元素    

      if(p == a + n - 1)

         p = a;

      else

         ++p;

   }

   return 0;

}

例9-31 库函数qsort在头文件stdlib.h中声明为void qsort(void *base, unsigned n, unsigned size, int (*fcmp)(const void *, const void *))。库函数qsort采用快速排序算法对从base起始的n个元素base[0]到base[n-1]进行排序。排序后各元素将依照比较函数fcmp的定义升序排列。函数指针fcmp指向的是一个用来比较两个参数“大小”的函数。若第一个参数“小于”第二个参数,它通常返回一个小于0的整数;否则若二者相等,则通常返回0;否则返回通常一个大于0的整数。base中每个元素占用的存储单元为size个字节。

调用qsort函数对一个整型数组排序。

分析:

使用qsort函数需要定义一个比较整数大小的函数,这个比较函数的参数应符合函数指针fcmp的要求,定义时需注意形参的类型及void*型参数的使用。

#include <stdio.h>

#include <stdlib.h>

int intcmp(const void *p1, const void *p2)

{

   return *((int *)p1) - *((int *)p2);

}

 

int main( )

{

   int i, a[5] = {23, 32, 21, 52, 25};

   qsort(a, 5, 4, intcmp);

   for(i=0; i<5; ++i)

      printf("%3d", a[i]);

   return 0;

}

例9-32 调用qsort函数对下面数组中的字符串常量排序。

char *str[ ] = {"Henan", "Beijing", "Guangzhou"};

分析:

str数组中的元素值为地址,在排序时需注意不能让qsort函数根据元素值的大小排序,这就需要巧妙定义比较函数。

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int strcmp(const void *p1, const void *p2)

{

   char **pp1 = (char **)p1;

   char **pp2 = (char **)p2;

   return strcmp(*pp1, *pp2);

}

 

int main( )

{

   char *str[] = {"Henan", "Beijing", "Guangzhou"};

   int i;

   qsort(str, 3, 4, strcmp);

   for(i=0; i<3; ++i)

       puts(str[i]);

   return 0;

}

注意:

程序执行后str数组的状态变化可能如图9-13所示。

图9-13 str数组的状态变化

 

练习9

9.1 仅凭地址就可以访问储存单元吗?为什么访问储存单元时必须知道其类型?

9.2 讨论“指针就是内存地址”这个说法。

9.3 地址是整数,可为什么通常不能用整数给指针变量赋值?任意指针变量之间可以互相赋值吗?如有int *pi = (int*)23; float *pf;则pf = pi有问题吗?

9.4 double lf, *p = &lf;语句与double lf, *p; *p = &lf;等价吗?

9.5 有程序

#include <stdio.h>

void main( )

{

   int i = -5, *pi;

   pi = &i;

   printf("%p,%p\n", pi, &pi);

}

注:格式字符p以地址的格式(十六进制无符号整型)输出数据。

根据程序的输出,请参照图9-1画出变量i实际的内存状态和简化后的内存状态,再参照图9-2画出变量pi与变量i的关系。

9.6 指针变量的长度是多少?

有double f, *pf = &f;,则sizeof(pf)与sizeof(*pf)的值相等吗?

9.7 有程序

#include <stdio.h>

void main( )

{

   int i = 5, *pi;

   pi = i;

   printf("%p\n", pi);

}

问:

1. 程序有错误码?

2. 把pi = i;改为pi = (int *)i;后,程序的输出是多少?

9.8 分析下面程序的输出

#include <stdio.h>

void main( )

{

   int i = 3, *p1 = &i, *p2;

   p2 = p1;

   i = *p1 + *p2;

   printf("%d,%d,%d\n", *p1, *p2, i);

}

9.9 程序中通常利用变量存取相关存储单元。有了指针变量后,只要指针变量指向了某存储单元,就可以通过间接引用操作符*存取相关存储单元。指针变量的出现为在程序中使用相关存储单元提供了一种新的途径。讨论这种说法。

9.10 没有初始化的指针变量的值是空指针吗?查看下面程序的输出。

#include <stdio.h>

void main( )

{

   int *pi;

   if(pi == NULL)

      printf("pi是空指针!\n");

   else

      printf("pi不是空指针!\n");

   *pi = 5;

   printf("%d\n", *pi);

}

9.11 下列语句有问题吗?

1. int *p1, *p2; p1 = p2;

2. int i, *p1, *p2; p1 = p2 = &i;

3. int i = 3, *p1, *p2; p1 = &i; p2 = *p1;

4. int i = 3, *p1, *p2 = &i; *p1 = *p2;

5. int i = 3, *p1, *p2 = &i; p1 = p2;

6. int *p1, *p2; p1 = &p2;

9.12 分析下面程序的输出

#include <stdio.h>

void main( )

{

int a, b, m = 5, n = 3;

int *p1 = &m, *p2 = &n;

a = p1 == &m;

b = (++*p1)/(*p2) + 5;

n = *p1 + *p2;

printf("a=%d,b=%d\n", a, b);

printf("m=%d,n=%d\n", *p1, *p2);

}

9.13 已知整型指针变量pa,pb,pc分别指向整型变量a,b,c。按下面要求编程。

1. 使用指针,交换a,b,c的值使变量a,b,c按升序排列。(指针指向的对象不变,即pa一直指向a。)

2. 变量a,b,c的值不变,但pa,pb,pc指向的变量按升序排列(即pa指向值最小的变量。)

9.14 设例9-6 main函数中有int *p1 = &i; int *p2 = &j;分析函数调用swap(*p1, *p2)和swap(p1, p2)。

9.15 把例9-6的swap函数改为

void swap(int *px, int *py)

{

int *temp;

temp = px;

px = py;

py = temp;

}

设main函数执行时变量i、j的内存状态还如图9-4所示,分析函数调用swap(&i, &j)的执行过程。

9.16 指针作为函数参数时为什么可以改变实参的值?

9.17 在例9-­6中,swap函数执行时参数px指向了main函数中的变量i,在swap函数中,*px与i的等价吗?什么情况下*px与变量i可互换?

9.18 指针变量有作用域和生存期吗?

9.19 scanf函数为何可以改变实参的值?有int i,*pi = &i;用scanf函数把用户输入的整数存储到变量i中时,借助指针变量pi可以吗?

9.20 改正下面语句中的错误。

1. char *cp; scanf("%c", &cp);

2. int *pi; sacnf("%d", pi);

9.21 分析程序。

#include <stdio.h>

int *p;

void test(int x, int *pi)

{

   int c = 3;

   *p = *pi + c;

   x = *pi * c;

   printf("%d,%d,%d\n", x, *pi, *p);

}

void main( )

{

   int i = 5, j = 2, k = 3;

   p = &j;

   test(i, &j);

   printf("%d,%d,%d\n", i, j, *p);

}

9.22 例9-7中的函数可定义为int add(int x, int y){return x + y;} 。利用指针把例7-1改写为没有返回值的函数,并改写例7­-5以使用改写过的函数。

9.23 用指针把例7-20中的fac函数改写为没有返回值的函数。

9.24 指针作参数时,无论指针向何种类型的存储单元,实参与形参之间只传递4个字节的地址。双精度浮点型的实参与形参之间需传递8个字节的数据,因此,有时使用指针作为参数的目的仅仅是为了提高函数的执行效率。为了避免不小心使用指针改变实参的值,可以使用C语言的关键字const来限制指针参数。

关键字const用于定义“只读”变量,如有const int i = 23或int const i = 23;则变量i就成了一个只读变量,顾名思义它只能读,不能改变(写入),因此程序中的i = i * i; i = 5;等等语句都将引发语法错误。

当const修饰指针变量时,情况变得复杂。如有double m = 2.3,n = 3.2; const double *p = &m;则指针变量p为只读变量,但是此时是变量p的值不能改变(p=&n;出错)呢,还是不能通过变量p修改其指向变量的值呢?即p = &n 合法,而*p = 5.2非法。

编程测试下列语句中const的作用,并编写一个形参为指针但是不能通过其改变相应实参值的函数。

const double *pf = &m;

double const *pf = &m;

double * const pf = &m;

const double * const pf = &m;

9.25 请选择合适的语句。

int a[5], *p = &a[0],i;

for(i=0; i<5; ++i)

   scanf("%d",       );

A. *(p+i)        B. p++        C. ++p         D. p+i

9.26 已知int a[3] = {1, 2, 3}, *p = &a[1];,求*p++和*--p的值。

9.27 如何理解一维数组变量?讨论一维整型数组变量a与整型指针变量pi的异同点。

9.28 用指针实现练习6.5、6.6和6.7。

9.29 编写函数利用指针求字符串的长度。

9.30 实现一个将字符串形式存储的整数转换为一个整数的函数toInt,并用数据-5678和6789测试函数的输出,测试程序如下。

#include <stdio.h>

void main( )

{

   char str[11];

   gets(str);

   printf("%s=%d\n", str, toInt(str));

}

9.31 参考例9-15利用指针对用户输入字符串中的字符按升序排序。

9.32 swap(int a[3])与swap(int a[ ])中形参a的类型相同吗?它与一维整型数组变量a有何异同?数组作参数时退化为相应的指针对函数的调用执行有何影响?

9.33 分析下面的函数。

void reverse(char *ps)

{

   if(*ps == '\0')

       return;

   reverse(ps + 1);

   putchar(*ps);

}

有同学认为reveres函数中参数char *ps 应改为const char *ps以防止在函数中不小心修改实参,请讨论使用const修饰指针参数的时机。

9.34 分析并测试下面的函数。

void test(int *a, int n, int i, int *p)

{

   if(i < n)

   {

       if(a[i] > a[*p])

         *p = i;

       test(a, n, i + 1, p);

   }

}

9.35 分析下面的程序。

#include <stdio.h>

void test(int i, int a[3])

{

   printf("%d,%d\n", i, sizeof(a));

}

void main( )

{

   int a[5] = {1, 2, 3, 4, 5};

   printf("%d\n", sizeof(a));

   test(sizeof(a), a);

   printf("%d\n", sizeof(a) / sizeof(*a));

}

9.36 语句scanf("%d", &a[0][1]);可以把用户输入的整数赋值给变量a[0][1],使用指针变量替换掉&a[0][1]以完成同样的操作。如果用scanf函数赋值给变量a[i][j]呢?

9.37 利用指针完成练习6.16和6.17。

9.38 利用指针把一个二维数组分别按行和按列输出。

9.39 利用指针完成例6-8。

9.40 分析下面的程序。

#include <stdio.h>

#include <string.h>

char *str1 = "Hello!";

void main( )

{

char *str2 = "Hello!";

char *str3 = "Hello!";

printf("%p,%p,%p\n", str1, str2, str3);

printf("%d,%d,%d\n", strlen(str1), strlen(str2), strlen(str3));

printf("%d,%d,%d\n", sizeof(*str1), sizeof(*str2), sizeof(*str3));

}

9.41 讨论 char str[ ] = "Hi!";与char *str1 = "Hi!" ;的异同。语句char *str; scanf("%s", str);有什么问题?

9.42 已知int *a[ ];其中数组a的元素分别指向整型变量a,b,c。利用整型指针数组变量a重做练习9.13。

9.43 分析程序,并画出数组变量a与指针变量pi的内存状态图。

#include <stdio.h>

void main( )

{

   int a[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

   int i, j, *pi[3];

   for(i=0; i<3; ++i)

       pi[i] = &a[i * 3];

   for(i=0; i<3; ++i)

   {

       for(j=0; j<3; ++j)

         printf("%3d", *(pi[i] + j));

       printf("\n");

   }

}

9.44 编程对下面数组中的字符串常量按升序排列。

char *str[3] = {"Henan", "Beijing", "Guangzhou"};

9.45 补充语句,并画图分析程序中指针变量与数组变量的关系。

#include <stdio.h>

void main( )

{

   int a = 5, b = 2, c = 3;

   int *pa[3] = {&a, &b, &c};

   int *pi, i;                       

   for(i=0; i<3; ++i)

   {

         printf("%2d", *pi);     

   }

   printf("\n");

   p = pa;

   //使用指针变量p输出的a, b, c值

   for(i=0; i<3; ++i)

   {

       printf("%2d",  **p);             

   }

}

9.46 用一对双撇号把命令行参数括起来,可以对命令行参数产生什么影响(如test "hello C! "回车与test hello C!回车)?编程测试。

9.47命令行参数可以为/s、/p、/a和/?四种选项中的一种(如test.exe /s 回车),程序输出用户选用的命令行参数(如输出您用/s选项执行了程序)。实现此程序。

9.48 用例9-25中的calDefInt函数求2x2+sin(x)在[0,π/2]上的定积分。

9.49 如何理解void型指针变量?为什么不能用间接引用操作符*获取void型指针变量向的内容?void型变量和void型指针变量在内存分配上有何不同,为什么定义void型变量(如void a;)没有意义?

9.50 下面是一个交换函数,借助指针它可以交换两个整型或浮点型变量的值,甚至是两个数组变量所有元素的值,请分析并测试该函数。

void swap(void *px, void *py, unsigned size)

{

   char temp, *pa, *pb;

   pa = (char *)px;

   pb = (char *)py;

   if(pa != pb)

      while (size--)

      {

         temp = *pa;

         *pa++ = *pb;

         *pb++ = temp;

      }

}

9.51 分析下面的程序。

#include <stdio.h>

#include <stdlib.h>

int main( )

{

   int i, j;

   int **ppa = (int **)malloc(3 * sizeof(int *));

   if(ppa != NULL)

   {

       for(i=0; i<3; ++i)

         if((*(ppa + i) = (int *)malloc(2 * sizeof(int))) == NULL)

         {

             printf("分配内存空间失败!\n");

             return 2;

         }

       for(i=0; i<3; ++i)

         for(j=0; j<2; ++j)

             scanf("%d", ppa[i] + j);

 

       for(i=0; i<3; ++i)

       {

         for(j=0; j<2; ++j)

             printf("%5d", ppa[i][j]);

         printf("\n");

       }

   }

   else

   {

       printf("分配内存空间失败!\n");

       return 2;

   }

   //释放堆空间

   for(i=0; i<3; ++i)

      free(ppa[i]);

   free(ppa);

   ppa = NULL;

   return 0;

}

9.52 请在堆空间上构造一个3行2列的二维字符型指针数组。

9.53 分析三维数组变量。

9.54 函数printA的首部为void printA(int *p, int i, int j),它用于输出一个i行j列的二维整型数组。实现并测试该函数。

9.55 分析下面的程序。

#include <stdio.h>

#define MAX 100

int a[MAX];

int main( )

{

   int i, n, s, m, k, g;

   printf("请输入总人数,开始者编号和出圈者所报数字:\n");

   scanf("%d%d%d", &n, &s, &m);

   //初始化数组

   for(i=0; i<n; ++i)

      a[i] = i + 1;

   //准备报数

   s = (s - 1) % n;

   //重复报数

   for(g=n; g>1; --g)

   {

      s = (s + m - 1) % g;

      k = a[s];

      for(i=s; i<g-1; ++i)

         a[i] = a[i + 1];

      a[g-1] = k;

   }

   for(i=n-1; i>=0; --i)

   {

      printf("%5d", a[i]);

      if((n - i) % 10 == 0)

         printf("\n");

   }

   return 0;

}

9.56 库函数bsearch在stdlib.h中声明为void * bsearch(const void *key, const void *base, unsigned n, unsigned size, int (*fcmp)(const void *, const void *));。它采用二分搜索算法查找从base起始的已有序的n个元素(base[0]到base[n-1])中是否有与指针变量key指向的变量的值(*key)相同元素,若有则返回指向这个元素的指针,否则返回空指针(NULL)。函数的其它参数同qsort函数的。编程测试该函数。(n个元素的顺序跟fcmp相关)

 

 

  • 4
    点赞
  • 6
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 6 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

stunt

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值