软件开发 2007-04-24 14:25:49 阅读100 评论0 字号:大中小 订阅
第6单元 指针
本单元教学目标
介绍C++中指针的基本概念。
学习要求
指针是C++中最重要的基本概念之一。要求同学们充分理解和掌握:
1.什么是地址? 什么是指针?
2.指针类型变量的声明方法和怎样将一个变量或数组的地址赋给指针类型的变量;
3.怎样通过指针类型的变量去访问某个变量或数组元素的值。
授课内容
6.1 地址与指针
我们知道, 从拓扑结构上来说, 计算机的内存储器就象一个巨大的一维数组, 每个数组元素就是一个存储单元(在微型计算机中其大小通常为一个字节)。就象数组中的每个元素都有一个下标一样, 每个内存单元都有一个编号, 又称地址。
内存储器是程序活动的基本舞台。在运行一个程序时, 程序本身及其所用到的数据都要放在内存储器中:程序、函数、变量、常数、数组和对象等, 无不在内存储器中占有一席之地。
凡是存放在内存储器中的程序和数据都有一个地址, 用它们占用的那片存储单元中的第一个存储单元的地址表示。在C++中,为某个变量或者函数分配存储器的工作由编译程序完成[1]。在编写程序时, 通常是通过名字来使用一个变量或者调用某个函数。这样做既直观, 又方便。而变量和函数的名字与其实际存储地址之间的变换由编译程序自动完成。但是, C++也允许直接通过地址处理数据, 在很多情况下这样做可以提高程序的运行效率。
那么怎样才能知道某个变量、数组或者函数的地址呢?
C++规定:
(1) 变量的地址可以使用地址运算符&求得。例如, &x表示变量x的地址;
(2) 数组的地址, 即数组第一个元素的地址, 可以直接用数组名表示;
(3) 函数的地址用函数名表示。
前面已经介绍过,所谓地址就是存储单元在整个内存构成的数组中的下标,实际上可用一个无符号整数(无符号长整数)表示[2]。因此,可以用一个无符号整型变量将地址存放起来。这种用来存放地址的变量就叫作指针型变量, 简称指针。假定ptr 为一指针, 则语句
ptr = &x;
就将变量x的地址存入了指针ptr中。当然, 也可以通过指针得到变量x的值,或者改变变量x的值:
*ptr = 2;
y = *ptr;
其中的运算符“*”用于一个地址(例如指针变量值)之前,表示该地址上的变量、数组或者函数本身。因此, 语句 *ptr = 2; 的作用相当于 x = 2; ,因为指针ptr中存放者变量x的地址。由于通过指针可以对一个其他类型的变量进行操作, 所以有时也把“变量x的地址存放在指针ptr中”简称为“指针ptr指向变量x”。
6.2 指针型变量的声明
如上所述, 指针也是一个变量, 因此也必须遵循先声明, 后使用的原则。在C++中, 并没有一个专用的指针类型说明符, 声明指针类型的变量是通过声明该指针指向的变量类型进行的。如果要声明一个用于存储int类型变量地址的指针ptr, 则可以使用如下语句:
int *ptr;
注意这时运算符“*”的含义是说明指针变量ptr用于指向一个int类型的变量。
[例6-1] 编写用于交换两个整型变量的值的函数。
算 法: 交换两个变量的值必须使用第3个变量。例如,要交换变量x和y的内容,可以使用临时变量tmp:
int x, y, tmp;
tmp = x;
x = y;
y = tmp;
但在例5-6中直接利用上法将函数swap()编写为
// 函数swap(): 交换两个整形变量的值(不成功)
void swap(int x,int y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
}
却是有问题的。试用以下主函数验证:
// 试验函数swap()用的主函数
void main()
{
int x = 2, y = 3;
cout << "x = “ << x << “, y = “ << y << endl;
swap(x,y);
cout << "After exchange x & y:” << endl;
cout << “x = “ << x << “, y = “ << y << endl;
}
其输出为
x = 2, y = 3
After exchange x & y:
x = 2, y = 3
从结果可以看出, 上面的swap()函数根本没有完成交换变量x和y的任务。为什么? 请看图6-1中的内存分配情况:
图6-1 验证交换变量值的函数swap()时的内存分配示意图[3]
图6-1描述的是程序运行到swap()函数中时的内存分配。由图中可以看出,主函数中声明的变量x和y与函数swap()的参数x和y在内存中分别占有各自的存储区, 它们之间唯一的联系只是在主函数中调用函数swap()时将主函数中变量x和y的值分别传送给了函数swap()的两个参数x和y。参数x和y在函数swap()运行期间相当于两个局部变量。因此, 事情很明显, 在函数swap()执行完毕以后, 其参变量x和y的值确实已被交换, 如图6-2所示。
然而, 事情到此为止了。从图6-2中可以看出,虽然swap()函数的两个参数变量x和y的值已被交换, 但原来主函数的变量x和y的值却没有发生变化。而且, 随着函数swap()运行结束返回主函数后, swap()中为局部工作变量申请的内存空间, 包括其参数x、y和局部变量tmp占用的内存单元,都将被释放。函数swap()所做的一切工作都付之东流了。
那么, 怎样才能编写一个真正可以交换参数值的swap()函数呢? 这就要用到指针了。
下面将函数swap()的参数声明为指向int类型的指针, 重新编写该函数:
程 序:
// Example 6-1:函数 swap(): 交换两个整形变量的值
void swap(int *xp,int *yp)
{
int tmp;
tmp = *xp;
*xp = *yp;
*yp = tmp;
}
注意到这次函数swap()的参数变量xp和yp是两个指向整型变量的指针, 因此在调用该函数时只能使用变量的地址作为实参。使用下列语句取代测试主函数中的函数调用语句:
swap(&x,&y);
此时的内存分配如图6-3所示。
于是, 在新的swap()函数中, 语句
tmp = *xp;
*xp = *yp;
*yp = tmp;
通过地址直接对主函数中原来的变量x和y进行操作, 完成了交换这两个变量的值的任务。在函数swap()执行完毕后,即使释放其局部变量tmp和指针参数xp、yp占用的存储也不会影响到主函数中变量x和y的新内容。
一般来说, 函数可以用返回值的形式为调用程序提供一个计算结果。在前面的各单元中出现的函数返回值类型大都是int、double之类的简单类型。其实,也可以将一个地址数据(如变量、数组和函数的地址, 指针变量的值等)作为函数的返回值。在声明返回值为地址的函数时, 要使用指针类型说明符, 例如
char *strchr(char *string, int c);
char *strstr(char *string1, char *string2);
这是两个用于字符串处理的库函数,其返回值均为地址。前者的功能为在字符串string中查找字符c,如果字符串string中有字符c出现, 则返回字符c的地址, 否则返回NULL。后者的功能为在字符串string1中查找子字符串string2,如果字符串string1中包含有子字符串string2, 则返回string2在string1中的地址(即string2中第一个字符的地址), 否则返回空指针值NULL。
[例6-2] 将表示月份的数值(1-12)转换成对应的英文月份名称。
算 法:首先声明一个字符串数组month,用来存放月份的英文名称。在转换时只须按下标值返回一个字符串的地址即可。
程 序:
// Example 6-2:将月份数值转换为相应的英文名称
char *month_name(int n)
{
static char *month[]=
{
"Illegal month", // 月份值错
"January", // 一月
"February", // 二月
"March", // 三月
"April", // 四月
"May", // 五月
"June", // 六月
"July", // 七月
"August", // 八月
"September", // 九月
"October", // 十月
"November", // 十一月
"December" // 十二月
};
return (n>=1 && n<=12)?month[n]:month[0];
}
分 析:我们知道, 只有全局数组和静态局部数组可以赋初值, 而随意使用全局数组和全局变量又会破坏程序的模块化结构, 降低程序的可读性。因此, 在上述函数中选用静态局部指针数组存放表示月份的英文名称的各字符串的地址。该数组中的第一个字符串表示输入错误的月份值时的提示。
6.3 指针与数组
指针是一个变量, 因此也应该可以参加运算。那么, 对指针进行运算的意义是什么?
设有指针ptr, qtr以及字符型数组string:
char *ptr,*qtr;
char string[6];
其关系如图6-4所示。
从图6-4中可以看出, 指针ptr指向数组string的第一个元素, 其内容就是该元素的地址1000。现在, 如果执行运算
ptr++;
即在指针变量ptr原来的值上再加1, 使其变为1001。可以看出, 这正是数组中第二个元素的地址, 也就是说, 指针现在改为指向数组中的第二个元素了。如果执行运算
ptr += 3;
则ptr的值由1000变为1003,即指向数组string的第四个元素。由此可以看出在指针变量上加上一个常数, 相当于改变了其中存储的地址值, 即改变了指针指向的数组元素。同样, 也可以从指针变量存储的地址值上减去一个常数, 此时指针向前移动若干个元素。
在图6-4中还有一个指针变量qtr, 指向字符串的结束标志0(即数组string的第四个元素)。如果求出这两个指针的差:
len = qtr-ptr;
可以看出, 这正是字符串string的长度(不含字符串结束符)。
由于在C++中每个变量、数组和函数的具体地址和相对顺序是由连接程序确定的(局部变量是动态分配的), 在编写程序时无法知道其确切地址和相对顺序, 所以对于指向单个变量和函数的指针进行这样的运算是没有意义的。但是无论怎样分配, 一个数组内的各元素的相对位置总是固定的, 所以对数组元素的引用除了使用下标以外, 还可以通过使用指针运算来实现, 这是C++程序设计的一大特点。
[例6-3] 编写一个字符串复制函数mystrcpy()。
算 法: 我们知道,字符串的定义是以0为结束符的字符序列。因此复制字符串也只需复制到0为止。设指针source中存放着原来的字符串的地址,指针destin中存放着新的字符型数组的地址, 则伪代码算法为
while(*source!=0) // 如果*source==0则表示原字符串结束
{
*destin = *source; // 复制字符
source ++; // source移向原字符串中的下一个字符
destin ++; // destin移向新字符数组的下一位置
}
*destin = 0; // 在新字符串尾部添写一个结束符0
由于这个算法非常直观, 所以上面的伪代码其实已经是最终的程序段了。由于C++的表达式应用非常灵活, 所以这段程序也可以写成:
while((*destin++ = *source++) != 0);
改写后的程序段只使用了半个语句(while语句的后半部分被省略掉了)!但是功能依然不变,甚至新字符串尾部的0也已经被复制。C++的这种特点, 是其程序比较精练的原因, 受到程序员们的偏爱。但是过分的精练也会使程序难以理解, 有悖于结构化程序设计的基本原则[4]。
程 序:
// Example 6-3:复制字符串
mystrcpy(char *destin, char *source)
{
while(*source!=0)
{
*destin = *source;
source ++;
destin ++;
}
*destin = 0;
}
分 析: 函数mystrcpy()的功能为将一个字符串的内容复制到另一个字符型数组中去。在复制字符串时要注意, 一定要保证目标数组确实可以放得下整个字符串。初学者最易犯的一个错误是混淆指针与数组的概念, 写出如下的语句:
char *string1 = "This is a sample.";
char *string2;
... ...
strcpy(string2, string1);
这时确实可以将字符串string1中的内容复制到从存放于指针string2中的地址开始的一段内存中去。但问题是指针string2 中存放的究竟是谁的地址? 由于没有对string2赋值,所以它可能指向任何地方, 包括已经分配给其他变量、数组甚至函数的区域。向string2复制字符串会覆盖这些地方原来的内容,造成各种运行错误, 包括突然死机;即使幸而指针string2指向一片未被使用的存储区,成功地复制了字符串, 但由于没有合法的授权, 也不能保证其后程序不再将这片存储区域分配给其他的变量或数组, 从而造成刚刚复制的内容又被其他数据所覆盖。
上面的例子使用了字符类型的数组, 其特点是每个数组元素的大小正好是一个字节。如果使用其他类型的数组, 其指针的运算规则要不要进行修改呢?
假设有指针ptr, qtr以及整型数组array:
int *ptr,*qtr;
int array[3];
其关系如图6-5所示。
从图6-5中容易看出, 如果
ptr++;
的结果仍是在指针变量ptr原来的值上加1, 即ptr的值由1000变为1001,则其作为地址来说已经没有意义, 因为地址为1001的存储单元并不是某个数组元素或变量的第一个存储单元, 而是存放着数组array的第一个元素的后半部分。同样,如果表达式
len = qtr-ptr;
的结果为将1006-1000 = 6送入变量len, 则len也没有正确地反映出数组array中数组元素的数目。为了解决这一问题, C++规定, 对于指针变量来说, 其运算的基本单位为其指向的数据类型的变量占用的字节数。因此,如果某指针是指向int类型变量的, 由于int类型的变量占用两个字节, 所以该指针运算时的基本单位为2; 如果某指针是指向float类型的, 由于float类型变量的长度为4个字节,则该指针运算时的基本单位为4。这样可以保证对指针的操作能正确地反映地址的变化。因此, 图6-5中的
ptr++;
实际上是在ptr原来的值上加2, 正好使ptr指向array的第二个数组元素; 同样, 语句
len = qtr-ptr;
的结果是变量len的值为3, 正好为该数组中的元素个数。
[例6-4] 编写一个函数用于将一个float类型的数组清零(即将其所有元素全部置为0)。
算 法: 通过引用下标变量很容易实现数组清零的功能。但在本例中, 我们采用指针编写该函数。因为数组元素的类型为float, 所以必须使用指向float类型的指针。
程 序:
// Example 6-4:数组清零
void clear_array(float *ptr, int len)
{
float *qtr = ptr+len;
while(ptr<qtr)
{
*ptr = 0.0;
ptr++;
}
}
分 析: 由程序中可以看出, 由于C++规定指针运算的基本单位为其指向的类型变量的长度, 所以使程序设计变得相当简单。在编写程序时, 如果要使指针指向下一个数组元素, 不必知道一个数组元素实际占用几个存储单元, 只要简单地在指针指上加1即可。而且如果要将该函数改为对double类型的数组清零,只要将指针类型由float *改为double *即可, 其他不必改动。
有时也用下标表示法引用指针指向的数组元素。例如在声明了指针变量
double x, a[100], *ptr = a;
之后, 也可以使用
x = ptr[10];
这样的用法, 并不表示ptr是一个数组, 而只是
x = *(ptr+10);
的另一种写法。
6.4 动态存储分配
一般来说, 程序中使用的变量和数组的类型、数目和大小是在编写程序时由程序员确定下来的, 因此在程序运行时这些数据占用的存储空间数也是一定的。这种存储分配方法被称为静态存储分配。静态存储分配的缺点是程序无法在运行时根据具体情况(如用户的输入)灵活调整存储分配情况。例如, 无法根据用户的输入决定程序能够处理的矩阵的规模。
C++的动态存储分配机制为克服这种不便提供了手段。动态存储分配要使用指针、运算符new和delete。
运算符new用来申请所需的内存。其用法为
<指针> = new <类型>;
或者
<指针> = new <类型>(<初值>);
new运算符从堆(管理内存中的空闲存储块)中分配一块与<类型>相适应的存储,如果分配成功,将其首地址存入<指针>,否则置<指针>的值为NULL(空指针值,即0)。<初值>用于为分配好的变量置初值。
new运算符也可以为数组分配内存。用法为:
<指针> = new <类型>[<元素数>];
运算符delete用于释放先前申请到的存储块,并将其归还到堆中。其用法为
delete <指针>;
其中<指针>中应为先前分配的存储块的地址。
动态存储分配的变量和数组通过指针来访问。例如:
int x, *ptr = new int(5);
x = *ptr;
使用动态存储分配时要注意几个问题。一是要确认分配成功后才能使用,否则可能造成严重后果;二是在分配成功后不宜变动<指针>的值,否则在释放这片存储时会引起系统内存管理混乱;三是动态分配的存储不会自动释放,只能通过delete释放。因此要注意适时释放动态分配的存储。例如:
void func()
{
int *ptr = new int(5);
… …
}
由于ptr是局部变量,在退出func()函数后自动失效。如果在函数func()中没有及时释放动态分配的存储单元,则在退出func()函数后再也找不到这些单元的地址了。
6.5 引用
C++提供了一个与指针密切相关的特性,即引用。引用提供了另一种访问变量的手段,特别是为函数间传递数据提供了方便。
引用运算符“&”用来声明一个引用。例如
int i, &refi = i;
这里,引用refi被声明为对变量的引用,其引用类型为int。经这样声明后,变量i与引用refi代表的是同一变量。实际上,refi可看作是变量i的一个别名。因此对引用的操作就是对原变量的操作。例如
int i = 100, &refi = i;
refi += 100;
其结果是变量i的值增加为200。
C++引入引用的主要目的是为了方便函数间数据的传递,在应用中主要是作为函数的参数。
[例6-5] 利用引用编写用于交换函数swap()。
程 序:
// Example 6-5:交换两个整形变量的值
void swap(int &x,int &y)
{
int tmp = x;
x = y;
y = tmp;
}
使用语句swap(x, y);取代例6-1中测试主函数中的函数调用语句,可以发现对实参x, y内容的交换成功。这是因为函数的参数是引用,所以在函数中的操作直接对引用指向的变量进行。可以看出,使用引用作函数的参数比使用指针方便。
除了作为函数的参数外,引用也用作函数的返回值。例如
int &func(void)
{
static int count = 0;
return ++count;
}
该函数的返回值用于统计调用该函数的次数。注意其中的返回语句返回的不是变量count的值,而是对它的引用,即返回变量count本身。要注意的是,函数不应返回对局部自动变量的引用,原因是退出函数后该变量已不存在。
返回值为引用的函数可以用作赋值运算符的左操作数,其含义是为该引用对应的变量赋值。例如
func( ) = 100;
实际上是对局部静态变量count的赋值。
自学内容
6.6 指针的数组
指针是变量, 当然也可以用其组成数组。指针数组的声明方式和普通数组的声明方式类似, 在数组名后加上维长说明即可。例如, 声明一个一维指针数组, 其中包括10个数组元素, 均为指向字符类型的指针:
char *ptr[10];
当然也可以声明二维以至多维指针数组, 例如:
int *index[10][2];
[例6-6] 编写一个查字典的函数。字典以词条为单位, 词条的格式为:
knowledge: n. 知识, 学问, 认识.
每个词条使用一个字符型数组存放, 整个词典使用一个指向字符类型的指针数组表示, 其中每个指针指向一个词条。其拓扑结构如图6-6所示。
算 法: 在一个线性表(如数组)中进行查找是程序设计的经典题目, 不同的查找算法的效率(可以从查找速度和需用的存储单元数目两个方面考查)相差很大。对于无序表来说, 一般只能采用顺序查找法, 其速度最慢。如果设表中元素数目为n个, 则平均查找长度(查找长度是为了找到指定元素所进行的比较次数)为n/2。如果所查找的内容不在表中, 则必须将整个表中所有元素都浏览一遍后才能知道, 即最大查找长度为n。
对于有序表来说, 则可以采用二分法进行查找。二分法查找的算法为:首先将关键字(即查找内容)与位于表正中的元素进行比较, 此时不外四种情况(设表按升序排列, 下同):
1) 关键字等于该中点元素: 查找成功, 结束查找过程;
2) 关键字小于该中点元素: 说明关键字在表的前半部分, 因此可以将表的前半部分作为一个新表(表长小于原来表的一半), 继续应用本算法在新表中进行查找;
3) 关键字大于该中点元素: 说明关键字在表的后半部分, 因此可以将表的后半部分作为一个新表, 继续应用本算法在新表中进行查找;
4) 表长已等于0: 说明表中没有要查找的内容, 查找失败。
容易看出, 这是一个递归算法, 其最大查找长度为O(log2n)。 在n比较大的场合, 二分法查找明显优于顺序查找。例如, 若表长为1024, 则顺序查找的平均查找长度为512, 最大查找长度为1024; 而二分法的最大查找长度不过为10。表越长,二分法查找的优点越明显。
二分法查找的缺点是事先要将表排序。排序的代价很高, 因此为了一、二次查找就对表进行排序是不经济的。只有在查找次数频繁, 而表中内容不经常变动时采用排序+二分法查找才是划算的。
词典的排列是有序的, 而且其长度通常都比较大, 内容相对固定, 采用二分法查找正合适。设词典存放在长度为n的一维表dict中,表中元素为指向词条字符串的指针; 待查单词为word; 并有三个工作变量low、high和mid, 分别用于记录待查表的低端、高端和中项元素的下标。使用伪代码将上述二分法算法细化如下:
low = 0; // 设置工作变量的值
high = n-1;
do
{
mid = (low+high)/2; // 算出表中点元素的下标
if(word等于dict[mid])
则dict[mid]即为所要查找的词条, 查找成功, 结束查找;
else if(word在dict[mid]之前)
high = mid-1; // 把表缩小为原表的前一半
else if(word在dict[mid]之后)
low = mid+1; // 把表缩小为原表的后一半
}while(表长度大于0);
// 如果循环正常结束, 说明查找失败
程 序:
// Example 6-6:二分法查词典
char *search_word(char *word, char *dict[],int n)
{
int low = 0, high = n-1, mid, searchpos, wordlen = strlen(word);
do
{
mid = (low+high)/2; // 算出表中点元素的下标
searchpos = strnicmp(word, dict[mid], wordlen);
if(searchpos==0) // 查找成功
return dict[mid];
else if(searchpos<0)
high = mid-1; // 把表缩小为原表的前一半
else
low = mid+1; // 把表缩小为原表的后一半
}while(high>low);
return NULL; // 查找失败
}
分 析: 如果查找的关键字是数值, 则可以简单地使用等于、大于和小于等比较运算符判断进一步查找的路线。但对于字符串查找来说, 问题要稍微复杂些。字符串的大小是按ASCII码字典序确定的, 排在前面的单词小,排在后面的单词大。我们知道, C++的库函数strcmp()正好可以用来比较两个字符串(见第4单元的自学内容)。但在本例的词典查找任务中,还有几个问题需要认真考虑。一是在真正的词典中, 大小写字母被认为是排在一起的,而在ASCII码表中所有的大写字母排在所有的小写字母之前。考虑到这种差别, 如果不准备打乱通常词典中的词序重新排列, 就必须在比较时忽略大、小写字母的差别。二是在上述查找词典算法中是将待查单词和词典中的词条直接进行比较, 如果直接使用strcmp()之类的比较函数, 就会发现后者的长度大于前者的长度, 从而认为这两个字符串不相等。解决的办法之一是在比较时只比较待查单词和词条的前几个字符, 如果相等就认为比较成功。符合上述要求的字符串比较库函数为strnicmp()。当然, 即使没有这样一个现成的函数, 也可以自己编写一个, 见例[6-8]。
6.7 指针和指针数组的初始化
指向字符类型的指针可以用字符串进行初始化, 例如
char *string = "This is a sample.";
局部静态指针数组和全局指针数组在声明的同时可以进行初始化。例如
char *func_namelist[] =
{
"strcat", "strchr", "strcmp", "strcpy", "strlwr", "strstr", "strupr"
};
6.8 指向函数的指针
程序只有装入内存储器以后才能运行。函数本身作为一段程序(子程序), 也要在内存中占有一片存储区域, 因此也可以声明一个指针变量指向这个函数, 即存放该函数的调用地址。
指向函数的指针变量的声明格式为:
<函数返回值类型说明符> (*<指针变量名>)(<参数声明表>);
或者
<函数返回值类型说明符> (*<指针变量名>)( );
即参数声明表可以省略。但我们建议在写指向函数的指针变量的声明时尽量保留对参数的声明, 这样就可以利用编译的查错功能检查可能的函数调用错误。例如:
int(*p)(); // p为指向返回值为整型的函数的指针
float(*q)(float,int); // q为指向返回值为浮点型函数的指针
函数名与数组名类似, 表示该函数的入口地址, 因此可以直接把函数名赋给指向函数的指针变量。
请注意, 在声明指向函数的指针变量时, 变量名外的括号不能缺少。试比较:
int *func(); // 返回地址的函数
int (*func)(); // 指向函数的指针
前者声明了一个函数, 其返回值为指向整型的指针; 而后者声明了一个指向返回值为整型的函数的指针变量, 意义完全不同。
如果已经将某函数的地址赋给了一个指向函数的指针变量, 就可通过该指针变量引用函数。例如:
double (*func)() = sin; // 声明一个指向函数的指针
double y, x; // 声明两个双精度类型的变量
x = ...; // 计算自变量x的值
y = (*func)(x); // 通过指针调用库函数求x的正弦
[例6-7] 将第一单元的上机题目第4题中的数值积分程序改编为一个通用数值积分函数。
算 法:我们仍然采用梯形积分公式(见第1单元的上机题目第4题的说明),但将被积函数作为积分函数的参数设计。
程 序:
// Example 6-7:用梯形积分法求解定积分的通用积分函数
double integral(double a, double b, double (*fun)(double), int n)
{
double h = (b-a)/n;
double sum = ((*fun)(a)+(*fun)(b))/2;
int i;
for(i=1;i<n;i++)
sum += (*fun)(a+i*h);
sum *= h;
return sum;
}
分 析:这个程序本身很简单。为了能够计算不同被积函数的定积分, 将被积函数也设计为一个参数, 实际上是将被积函数的调用地址传递给积分函数。在上述积分函数内部, 通过指向函数的指针调用被积函数来计算相应的函数值。例如要计算
可以这样调用函数integral():
double s;
s = integral(0.0,1.0,sin,1000); // 积分区间等分为1000分
6.9 void和const类型的指针
在以前各单元中, 曾遇到过用类型说明符void声明函数的参数和返回值时的情况。void用于函数的参数表, 是明确声明该函数不使用参数; 而使用void声明函数的返回值则是说明该函数不提供任何返回值。例如
void func(void);
说明函数func()既不需要参数, 也不提供返回值。
C++规定, 可以声明指向void类型的指针, 但其含义有所不同。指向void类型的指针是通用型的指针, 可以指向任何类型的变量。可以直接对void型指针赋值或将其与NULL作比较, 但是在求指针的对象变量的内容, 或者进行指针运算之前必需对其进行强制类型转换。例如
int x, y;
void *ptr;
ptr = &x; // 任何类型变量的地址均可存入指向void类型的指针
y = *((int *)ptr); // 通过void型指针求值时要用强制类型转换
用关键字const修饰一个指针时,根据其位置的不同有不同的含义。例如
const char *ptr = “Point to constant string”;
表示声明了一个指针ptr,它指向一个常数字符串。因此,运算
*ptr = ‘Q’;
是非法的,因该字符串为常量。但指针ptr本身为变量,可以修改。例如
ptr ++;
合法。而
char *const qtr = “A constant pointer”;
声明了一个常指针。在这种情况下,指针本身不能修改,但其指向的对象并非常量,允许修改。
实际上,修饰符const多用于修饰函数的指针或引用参数,以防止在编程中无意识地改变其值。例如
double func1(const double *x);
double func2(const double &x);
如果在编写函数func()的代码时改变了指针对象或引用对象的值,则会引起编译错误。
在函数声明
double &func3(double x) const;
中的const修饰符用于说明函数func3()的返回值是一常引用,即禁止如下赋值
func3()= 3.14;
实用编程
6.10 Visual C++的帮助功能
MSDN (Microsoft Developer Network) 是使用 Microsoft 开发工具或是以 Windows 和 Internet 为开发平台的开发人员的基本参考资料。MSDN信息库包含超过 1.1 GB 的编程指南信息,其中包括示例代码、开发人员知识库、Visual Studio 文档、SDK 文档、技术文章、会议及技术讲座的论文、以及技术规范等,其中所包括的信息的完整性是可以让每一个使用者都感到吃惊的。通过MSDN所提供的Help资料,我们可以对Visuan C++和Windows的工作机制有更全面的了解,可以帮助解决开发者遇到的大多数问题。
由于MSDN库为包括Visual C++在内的所有的Visual studio 6.0开发环境工具提供在线帮助,所以它在系统中作为一个应用程序独立运行,并没有同任何单个开发环境结合在一起。要从Visual C++中访问 MSDN,一种方法是从Visual C++的Help菜单中选择 Contents、Search或Index命令,另一种方法是在Visual C++开发环境中直接按下 F1键,系统都会自动运行 MSDN帮助程序。
一旦运行了MSDN之后,就会出现如图6-8所示的界面。MSDN界面分为三个窗格,顶端的窗格包含有工具栏。左侧的窗格包含有各种信息定位方法,通过单击列表中的主题,即可浏览或查找所需的各种信息;右侧的窗格则显示所选择的主题的具体内容,这些内容是以超文本(一种INTERNET文本格式)形式存在的,其中的相关内容可以通过超文本链接(它是文本中一些特殊的词或句子,其下有下划线或以突出的颜色显示,当鼠标的光标移动该键接上时,光标就会变成小手的形状)连接到其他的相关主题。
左边窗口中有四个选项卡:目录、索引、搜索和书签,用于提供四种不同的在线帮助浏览方式。
1.目录。单击“目录”选项卡可浏览主题的标题。该目录是依照标题和副标题的排列方式形成一个包含了 MSDN中所有可用信息的可扩充目录表。默认情况下,目录列表概括了MSDN的全部文章。双击列表中的标题,就可在MSDN窗口的右栏中打开该文章。
2.索引。单击“索引”选项卡可查看索引项的列表,然后可通过该栏左边的滚动条翻阅整个索引列表。要找到一个索引条目,在对话框顶部的编辑框中键人关键字即可。在键人关键字的同时,列表框中的索引自动滚动到该关键字所在的位置。找到所要的索引条目后双击,如果该条目仅对应一篇文章,MSDN就会立刻在其窗口右栏显示其内容;否则会出现“已找到的主题”对话框,其中列出了该条目可能指向的所有文章,这时你可通过双击列表中所需要主题,打开相应的一篇文章。
3.搜索。单击“搜索”选项卡可查找到包含在某个主题中的所有词组或短语。它是一个全文本搜索引擎,允许你寻找包含指定词或短语的主题。MSDN搜索引擎是非常优秀的,它能理解词的派生、通配符、布尔逻辑组合及NEAR运算符。和MSDN的“索引”选项比起来,全文本搜索所能覆盖范围更宽,它可以提供更多的文章以供选择。搜索结果的好坏很大程度上取决于你所使用的搜索字符串及搜索运算符。
4.书签。单击“书签”选项卡可创建或访问书签的列表。用户只需简单地标记书签中的某些主题,即可重新访问它们。
程序设计举例
[例6-8] 编写一个字符串比较函数, 仅比较两个字符串的前面若干个字符, 且在比较时不区分大小写字母。
算 法: 例5-11介绍了字符串比较函数mystrcmp()的编写方法,但那时我们是通过下标对数组进行操作。其实, 使用指针处理这类操作会更加方便。为了达到在比较时不区分大小写字母的目的, 可以使用库函数toupper(), 其原型为:
int toupper(int c);
其中参数c为待转换的ASCII代码, 如果c是一个小写字母,则该函数返回与其对应的大写字母。
程 序:
// Example 6-8:不区分大小写字母的部分字符串比较
int mystrnicmp(char *str1, char *str2, int n)
{
while(toupper(*str1)==toupper(*str2) && *str1!=0 && *str2!=0 && n>0)
{
str1++;
str2++;
n--;
}
return *str2-*str1;
}
分 析: 该函数的核心是while语句中的条件。我们规定, 只有以下3个条件同时满足, 循环继续执行:
1) 不计大小写字母的区别, 两个字符串中对应位置上的字符相等;
2) 两个字符串均未结束;
3) 已经比较过的字符尚未达到指定的数目。
因此循环结束时的状况必然是以上条件中有一条或多条已不满足。通常, 应对这些条件逐一进行判断, 分别处理。但本函数是一个特例, 无论因为什么原因结束循环, 均返回表达式*str2 -*str1的值。如果返回值为0, 表示两个字符串相同(在忽略大小写字母的区别和仅比较两个字符串的前n个字符的前提下, 下同);如果返回值大于0, 表示字符串str2大于str1; 否则表示str2等于str1。
单元上机练习题目
1. 编写一个函数, 用于生成一个空字符串, 其原型为:
char *mystrspc(char *string, int n);
其中参数string为字符串, n为空白字符串的长度 (空格符的个数)。返回值为指向string的指针。
2. 编写一个函数, 用于去掉字符串尾部的空格符, 其原型为:
char *mytrim(char *string);
其中参数string为字符串, 返回值为指向string的指针。
3. 编写一个函数, 用于去掉字符串前面的空格, 其原型为:
char *myltrim(char *string);
其中参数string为字符串, 返回值为指向string的指针。
[1] 实际上, 为变量和函数分配存储的工作是分几步完成的。在编译和连接的过程中, 只给程序中的各个变量分配了相对地址。每个变量的实际存储位置要到程序运行时才能确定。如果是局部变量, 其存储分配更晚, 要到该局部变量所属的函数被调用时才进行。因此, 同一个变量, 在程序的各次运行中可能被分配在不同的存储地址上。这也是为什么通常只需要知道某变量确有一个地址, 而不必关心该地址值具体是多少的原因。
[2] 这只是一个简化了的说法, 在不同的计算机中可能有更复杂的情况。
[3] 由于地址分配问题相当复杂,超出本课程的范围。因此图中的地址值只是示意。以下各图同此。
[4] 可能有人会说精练的程序效率高, 其实这是一种误解。影响程序效率的因素很多, 但源代码长度对程序效率几乎没有直接影响。事实上, 程序的效率表现在两个方面, 一是程序运行速度, 一是程序使用的数据占用的存储空间大小。通常用相对于程序处理的数据量所执行的机器指令条数的数量级来表示程序的运行效率 (时间) 。例如O(n)表示程序运行时间与数据量之间成比例; O(n2)表示程序运行时间与数据量的平方成比例, 运行时间的增长速度大于数据量的增长速度。显然, 对于完成同样功能的两个程序来说, 如果一个程序的效率为O(n), 另一个程序的效率为O(n2),则前者的效率高于后者。对程序的空间效率也可以做如此分析。
对于上面的两个程序段来说, 可以使用字符串长度表示数据量, 通过简单的分析就可以得出结论, 它们的运行速度均为O(n), 使用的数据占用的存储空间长度均为2n, 所以效率基本相同。至于程序代码本身的大小, 应以编译后生成的目标代码长度为准, 实践表明这两段程序所生成的目标代码长度亦相差无几。