第二十一章 指针 三 实例演练与提高   21.1 简单变量、数组、指针 21.2 小王成绩管理系统V2.0 的问题 ? 21.2.1 软件升级历史 21.3 指针的最常用用法 ? 21.3.1 分配内存 ? 21.3.2 访问指针指向的内存 21.4 小王成绩管理系统 V3.0 21.5 字符串指针 ? 21.5.1 为字符串分配指定大小的空间 ? 21.5.2 字符串常用函数 ??? 21.5.2.1 字符串比较 ??? 21.5.2.2 字符串复制 21.6 指针数组 ? 21.6.1 什么叫“指针数组”? ? 21.6.2 指针数组实例一 ? 21.6.3 指针数组实例二 ? 21.6.4 字符串指针数组?   21.1 简单变量、数组、指针 学习的知识点越来越多了…… 刚开始会觉得很兴奋啊,学得越多越好嘛。可慢慢的就会感到压力了,各种知识点在头脑里混在一起,每个都变得模糊了。 其实,每个知识点都有它存在,或出现的理由,只要我们多做对比,就会发现学习的知识点越多,反倒越容易理解每个知识点本质。 比如说,简单变量、数组、指针,三者都是C++中用于表达数据的工具,但在表达能力上,又各有不同。 如果用建筑上的房间来比喻:   简单变量是一间房屋。优点是占用空间少,建筑时间短,缺点是一间房子只适于住一个人; 数组是房间数固定的一排房子,每个房子里头同样只住一人,但由于它有多间,所以适于多人居住,优点是可以统一管理多人,缺点一来是占用空间大,二来房间数一旦确定,就不能改变了。先头盖了10间,如果如果来了11个人,就有一人住不下,如果来了9个人,就有一间浪费。 指针呢……它不是实际房子,而是设计纸上的房子。因此,它首先有一个特点:如果你想让指针存储数据,那一定得先为它分配内存。这就像光有一张设计蓝图是解决不了四代同堂的问题的,重要的是你还得根据这张蓝图,去找块地皮盖好房子。指针的优点是可以临时决定要盖多少间房子。 下面我们回顾一个例子,以理解三者的不同用处。   21.2 小王成绩管理系统V2.0的问题 先回顾一下该程序的升级过程,今天我们将对它做出两种不同方向的改进。 21.2.1 软件升级历史 V1.0 : 本版成绩管理系统实现让计算机自动统计6个班级的成绩总分和平均分。 V2.0 : 经过改进,本版可以实现多达5000个学生的成绩进行求总分和平均分,并且可以支持用户输入序号,查询任意一个学生的成绩!   在第一版,小王正在学习“循环流程”。通过在每次循环中,让用户输入一个成绩,然后保存在一个简单变量里,并累加到另一个简单变量,最终计算出总分和平均分。 第二版,由于段长要求不仅可以统计5000个学员的成绩,而且应实现成绩查询功能,这就要求程序必须同时记下5000个学生成绩。小王先是想用5000个简单变量来记下成绩——这显然太不实际了,后来学到数组,用数组轻松解决了这个问题,因为数组正是为“同时存储多个相同类型的变量”这一问题来设计的。 然而,第二版存在的不足也是显然易见的。那就是,它固定只能处理最多5000个学员的成绩。假想,这个软件要推广到全市300个学校,每个学校的学生总数都是不一样的,更惨的是每一年,一个学校的学生个数总是会有变化。难道就让我们的王老师时不时地改它的程序? 在没有指针时,惟一办法就是,浪费一点,比如定义数组元素个数为1万。目的是宁可浪费一点,也尽量不要出现不够的情况。显然,本办法只能算是一个无奈之举。难道就没有一个办法,即可以适应某个山区小学只有30名学员也情况,又可以轻松对付某大学高达2万名学员的情况?   锣声响起,锵锵锵……指针出场了。   指针是如何完成这一历史使命?带着问题,我们来学习下面的内容。 我们会在学习新内容之中,同时有选择地做一些旧知识点的复习工作。但如果你仍看不懂下面的一些代码,那得全面复习前两章的指针内容;或者,如果你连for都有些陌生,那你得重温一下小王成绩管理系统的前两个版本。   21.3 指针的最常用用法 21.3.1 分配内存 如何为指针分配和释放内存,上一章的内容中讲到了C++独用的new/delete、new[] / delete[] 和 C 使用的malloc, realloc/ free 方法。如果你忘了,请先复习。我们这里使用C++的方法演练。   new 只能为我们分配一个简单变量的内存,就是说new只盖了一间房子。new [] 才能为我们盖出一排的房子。   例子: int* p; //定义一个整型指针   p = new [10];? //new [] 为我们分配出10个int大小的内存。(盖了10间房,每间住一个整数)   21.3.2 访问指针指向的内存 前面:p = new [10]; 为我们分配了10个int,那么,我们该如何设置和访问这10个整数的值呢? 这一点完全和数组一致,我们来看数组是如何操作:   int a[10]; //以数组方式来定义10个int   //让第1个整数的值为100: a[0] = 100; //让第2个整数的值为80: a[1] = 80;   指针的操作方式如下:   int* p = new int[10]; //定义1个整型指针,并为它分配出10个int的空间   //让第1个整数的值为100: p[0] = 100; //让第2个整数的值为80: p[1] = 80;   对比以上两段代码,你可以发现,对指针分配出的元素操作,完全和对数组的元素操作一致。不过,指针还有另一种对其元素的操作方法:   int* p = new int[10]; //定义1个整型指针,并为它分配出10个int的空间   //让第1个整数的值为100: *(p+0) = 100; //让第2个整数的值为80: *(p+1) = 80;   请大家自己对比,并理解。如果觉得困难,请复习第19章关于*的用法,和指针偏移部分的内容。   21.4 小王成绩管理系统 V3.0   3.0 版的最重要的改进就是:用户可以事先指定本校的学生总数。   请仔细看好。   //定义一个指针,用于存入未知个数学生的成绩: int* pCj;   //总成绩,平均成绩: int zcj=0, pjcj;   //首先,要求用户输入本校学生总数: int xszs; //学生总数 cout << "请输入本校学生总数:"; cin >> xszs;   //万一有调皮用户输入不合法的总数,我们就不处理 if (xszs <= 0) { ??? cout << "喂,你想耍我啊?竟然输入一个是0或负数的学生总数.我不干了!" << endl; ??? return -1; //退出 }   pCj = new int[xszs]; //仍然可以用我们熟悉的循环来实现输入: for(int i=0; i < xszs; i++)? { ?? cout << "请输入第" << i+1 << "学员的成绩:"; ?? cin >> pCj[i];????? //输入数组中第i个元素 ?? ?? //不断累加总成绩: ?? zcj += pCj[i];??????????? }   //平均成绩: pjcj = zcj / xszs;   //输出: cout << "总成绩:" << zcj << endl; cout << "平均成绩:" << pjcj << endl;   //下面实现查询: int i;   do { ?? cout << "请输入您要查询的学生次序号(1 ~ " << xszs << "):" ; ?? cin >> i;   ?? if( i >= 1 && i <= xszs) ?? { ????? cout << cj[i-1] << endl; //问:为什么索引是 i-1,而不是 i ? ?? } ?? else if( i != 0) ?? { ????? cout << "您的输入有误!" << endl; ?? } } while(i != 0);? //用户输入数字0,表示结束。   //最后,要释放刚才分配出的内存: delete [] pCj; ......   请大家现在就动手,实现小王成绩管理3.0版。这是本章的第一个重点。通过该程序,你应该可以记住什么叫“动态分配内存”。   21.5 字符串指针 21.5.1 为字符串分配指定大小的空间   有必要的话,你应复习一下第16章之第6节:字符数组。   假设有个老外叫 "Mike",以前我们用字符数组来保存,需要指定是5个字符大小的数组:   char name[5] = "Mike";   "Mike"长4个字符,为什么要5个字符的空间来保存? 这是因为计算机还需要为字符串最后多保存一个零字符:'\0'。用来表示字符串结束了。   在学了指针以后,我们可以用字符串指针来表达一个人的姓名:   char* pname = "Mike";   此时,由系统自动为pname 分配5个字符的位置,并初始化为 "Mike"。 最后一个位置仍然是零字符:‘\0’。   采用字符串的好处,同样前面所说的,可以在程序中临时决定它的大小(长度)。 比如:   char* pname; pname = new char[9]; //临时分配9个字符的大小。   除了要记得额外为字符串的结束符'\0'分配一个位置以外,字符串指针并没有和其它指定有太多的不同。   既然讲到字符串,我们就顺带讲几个常用的字符串操作函数   21.5.2 字符串常用函数   字符串操作函数的声明都包含在该头文件: <string.h>   21.5.2.1 字符串比较   int strcmp(const char *s1, const char *s2);   比较s1 和 s2 两个字符串,返回看哪个字符串比较大。对于字母,该比较区分大小写 返回值: ? < 0?? : s1 < s2; ??? 0?? :? s1 == s2; ? > 0?? :? s1 > s2;   int strcmpi(const char *s1, const char *s2); 该函数类似于上一函数,只是对于字母,它不区分大小写,比如它认为'A'和'a' 是相等的。   要说两个字符串相等不相等,还好理解,比如: "Borland" 和 "Borlanb" 显然不相等。不过,字符串之间还有大小之分吗? 对于字母,采用ASCII值来一个个比较。谁先出现一个ASCII值比较大的字母,谁就是大者。比如:"ABCD" 比 "AACD"大。 如果一直相等,但有长短不一,那就长的大。比如:“ABCD” 比 “ABC”。 记住了,由于在ASCII表里,小写字母比大写字母靠后,所以小写的反倒比大写的大。比如:"aBCD"比"ABCD"大啊。   我这里写个例子,看如何比较字符串:   #include <string.h>? #include <iostream.h> ...   int reu = strcmp ("ABCD", "AACD");   if (reu > 0) ?? cout << "没错, ABCD > AACD" << endl; else ?? cout << "搞错了吧?" << endl;   请大家照此例,分别比较 "ABCD" 和 "ABC" 、 "aBCD" 和 "ABCD"。 如果你对如何用C++ Builder 建立一个控制台下的工程,请复习第二章第3节。   前面说的是英文字母,对于汉字字符串的比较,大小是如何确定的呢? 对于常用汉字,Windows按其拼音进行排序,比如“啊”是最小的,排在最前面,而“坐”之类的,则比较大,排在后面。 对于非常用的汉字,则按笔划来排序。有关常用不常的划分,是国家管的事,我们就不多说了。   我一直在网上叫“南郁”,大家可以拿你的名字和我做一下 strcmp,看看谁的名字比较大。(友情提醒:名字大没有什么好处,相反,名字大了,在各种场合里,一般是排名靠后的……) 21.5.2.2 字符串复制   char *strcpy(char *dest, const char *src);   该函数用于将字符串 src的内容,复制给 字符串dest。 注意,一定要保证 dest有足够的空间。 该函数最后返回dest.   比如: char name1[10]; char* name2 = "张三";   strcpy (name1, name2);   现在name1 的内容也是“张三”。   21.6 指针数组 学过数组,指针,二者结合起来,指什么? 21.6.1 什么叫“指针数组”? 一个数组用来存放整型数,我们就叫它 整型数组或整数数组; 一个数组用来存入字符,就叫字符数组; 同样,一个数组用来存入指针,那就叫指针数组。   比如: int* p; 这只是一个指针. 而 int* p[10]; 这是一个数组,里头存放了10个指针。   请大家区分: int* p = new int[10]; 和 int* pa[10]; 前者,是一个指针,并且该指针分配了10个元素的空间。 而后者,则是一个指针数组,用于存放10个指针(pa[0],pa[1]...pa[9]都是指针),这10个指针都可以分配10个元素(也可以不是10个,比如是8个或11个)。   仍然以建筑来比喻:   int* p = new int[10]; p 是 一张(是的一张而已)图纸,new int[10] 是 我们根据它建了10间房子。   int* pa[10]; pa[10] 是10张图纸。至于这10图纸上各准备建多少间房子,我们暂未定下。   我们可以通过一个循环,来为pa[10]中的每个指针分配8个元素的空间。 for (int i=0; i < 10; ++i) { ?? pa[i] = new int [8]; //为每个指针都分配8个int元素空间。 }   21.6.2 指针数组实例一   请打CB,并新建一个控制台工程(记得在出现的对话框中选中“C++选项”)。 然后输入以下代码(部分代码是CB自动生成的,你不必加入):   //--------------------------------------------------------------------------- #pragma argsused int main(int argc, char* argv[]) { ?? //定义一个指针数组,可以存放10个int 型指针 ?? int *p[10];   ?? //循环,为每个指针各分配空间。 ?? for(int i=0; i<10; ++i) ?? { ????? p[i] = new int [5];?? //分配5个元素的空间 ????? ????? //然后为当前指针中每个元素赋值: ????? for (int j = 0; j<5; ++j) ???????? p[i][j] =? j; ?? } ?? ? ? //输出每个指针中每个元素(用了两个“每”,所以需要两层循环) ? for(int i=0; i<10; ++i) ? { ????? for (int j = 0; j<5; ++j) ???????? cout << p[i][j] << ","; ???? cout << endl; ???? }   ? //重要!!!最后也要分别释放每个指针 ? for(int i=0; i<10; ++i) ??? delete [] p[i];   ? system("Pause"); ? return 0; ?? } //---------------------------------------------------------------------------   本例的输出为:    21.6.3 指针数组实例二 本例对上例做一些小小改动:   //--------------------------------------------------------------------------- #pragma argsused int main(int argc, char* argv[]) { ?? //定义一个指针数组,可以存放10个int 型指针 ?? int *p[10];   ?? //循环,为每个指针各分配空间。 ?? for(int i=0; i<10; ++i) ?? { ????? p[i] = new int [i+1];?? //分配i+1个元素的空间 ????? ????? //然后为当前指针中每个元素赋值: ????? for (int j = 0; j<i+1; ++j) ???????? p[i][j] = j; ?? } //输出每个指针中每个元素 ? for(int i=0; i<10; ++i) ? { ????? for (int j = 0; j<i+1; ++j) ???????? cout << p[i][j] << ","; ???? cout << endl; ???? }   ? //重要!!!最后也要分别释放每个指针 ? for(int i=0; i<10; ++i) ??? delete [] p[i];   ? system("Pause"); ? return 0; ?? } //---------------------------------------------------------------------------   本例的输出为:    21.6.4 字符串指针数组 假设我们想在程序中加入某个班级的花名册。让我们来想想如何实现。 由于一个人名由多个字符组成,所以人名就是一个数组。而全班人名,就是数组的数组,因此可以用二维数组来实现。 假设本班只有3个学员,每个学员的人名最长不超过4个汉字,每个汉字占2个字符,加上最后1个固定的结束符,共9个字符来表示一个人名。   char names[3][9] = { ?? {"郭靖"}, ?? {"李小龙"}, ?? {"施瓦辛格"}, };   这是个不错的解决方案。惟一稍有一点不足的是,我们为每个学员分配长9个字符,其实像1号郭靖,他只需5个字符就足矣。但我们使用“二维数组”的方案无法为不同长度的姓名分配不同的空间。   这时候,就可以用字符串指针数组了!“锵锵锵”,我们让字符串指针数组的方案出场。   char* names[3] = { ?? {"郭靖"}, ?? {"李小龙"}, ?? {"施瓦辛格"}, };   变化并不多,但是学杂费空间的问题却得到了解决。更为美妙的是,就算现在新来一位大侠叫“无敌鸳鸯腿”,我们也可以从容处理: char* names[4] = { ?? {"郭靖"}, ?? {"李小龙"}, ?? {"施瓦辛格"}, ?? {"无敌鸳鸯腿"}, };   接下来,我们来对“小王成绩管理V2.0”系统做另一种改进。   此次改进并不要求可以动态输入学校学生的总数。相反,作来一种“定制版”,我们希望专门为某个学校加入花名册功能,希望在录入成绩时,可以增加显示该学生的姓名。 假设这个学校叫“精武文武学校”,并暂定本期学员也只有上述那4位。   "精武馆定制版的小王成绩管理系统"如何实现?呵,我不写了,大家写吧。题目条件就是上面的黑体部分。最终录入界面应类似于:    快点开始做吧,把你的作业发在BBS里,让我评评你的成绩是多少 :))))