数据结构课程设计实验报告
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
青岛理工大学
数据结构课程设计报告
题目一:魔王语言解释
题目二:文学研究助手
院(系):计算机工程学院
学生姓名: 龚剑勇
班级:计算机科学与技术092
学号:200907079
起迄日期: 2011/6/20——2011/6/30
指导教师: 张艳
2010—2011年度第 2 学期
题目一:魔王语言解释
一、需求分析
[问题描述]
有一个魔王总是使用自己的一种非常精练而又抽象的语言讲话,没有人能听得懂,但他的语言是可以逐步解释成人能听懂的语言,因为他的语言是由以下两种形式的规则由人的语言逐步抽象上去的:
(1)α-> β1β2…βm
(2)(θδ1δ2…δn)->θδnθδn-1…θδ1θ
在这两种形式中,从左到右均表示解释。
试写一个魔王语言的解释系统,把他的话解释成人能听得懂的话。
[基本要求]
用下述两条具体规则和上述规则形式(2)实现。
设大写字母表示魔王语言的词汇;小写字母表示人的语言词汇;希腊字母表示可以用大写字母或小写字母代换的变量。
魔王语言可含人的词汇。
(1)B -> tAdA
(2)A -> sae
[测试数据]
B(ehnxgz)B解释成tsaedsaeezegexenehetsaedsae
若将小写字母与汉字建立下表所示的对应关系,则魔王说的话是:“天上一只鹅地上一只鹅鹅追鹅赶鹅下鹅蛋鹅恨鹅天上一只鹅地上一只鹅”。
t d s a e z g x n h
天地上一只鹅追赶下蛋恨
二、概要设计
1,程序设计思路
(1)以一维数组demon[ i ]表示魔王语言.
(2)魔王语言由用户输入,初始保存在demon[ i ]中.
(3)魔王语言与人类语言对应关系固化在程序中.
(4)实现过程:
A:初始,魔王语言接收后存放在demon[ i ]中.
B:初次遍历数组,将数组中括号内的元素入栈,同时插入相应首字母;
C:再次遍历数组,将数组元素依次入队。
(小写字母直接入队;大写字母经翻译成相应字符后入队;遇到括号,将栈中保存的元素依次出栈入队)在翻译过程中,如果依旧包含大写字母,则置flag为1,否则为0。
D:将队列中元素赋值给demon[ i ]。
如果此时flag=1,则再次重复C过程。
直至所有元素为人类语言。
E:输出demon[ i ]。
此时数组中元素为对应的人类语言。
注:如果程序中没有相应的对应关系,则翻译成“”。
2.数据结构设计:
1:设定栈的抽象数据类型定义:
ADT stack{
数据对象:D={ai|ai∈CharSet,i=1,2,…,n,n>=0}
数据关系:R1={<ai-1,ai>|ai-1,ai∈D,i=2,…,n}
基本操作:
initstack (&s)
操作结果:构造一个空栈s.
push (&s,e)
初始条件:栈s已存在.
操作结果:在栈s的栈顶插入新的栈顶元素e.
pop(&s,&e)
初始条件:栈s已存在.
操作结果:删除s的栈顶元素,并以e返回其值.
}ADT stack
2:设定队列的抽象数据类型:
ADT queue{
数据对象:D={ai|ai∈Elemset,i=1,2,…,n,n>=0}
数据关系:R1={<ai-1,ai>|ai-1,ai∈D,i=2,…,n}
基本操作:
initqueue(&q)
操作结果: 构造一个空队列q.
enqueue(&q, e)
初始条件: 队列q已存在.
操作结果: 插入元素e为q的新队尾元素.
dequeue(&q,&e)
初始条件: q为非空队列.
操作结果: 删除q的队头元素,并用e返回其值. }ADT queue
3:本程序包含四个模块:
1)主函数模块.其中主函数为:
status main()
{
初始化栈;
初始化队列;
接收魔王语言输入到数组demon[i ];
遍历数组将括号中元素进栈;
while(数组demon[i ]中元素有大写字母)
{ 翻译排序处理后入队列;
将对列元素保存在数组demon[i ];
}
输出人类语言(数组demon[ i]);
}
2)括号内元素入栈处理模块.
tempstack(&temps)
将括号内元素入栈,依次插入首字符.
举例:(abcd)->adacaba.
3)排序入队列模块.
sort(&s,&q)
{
遍历数组;
{
遇到小写字母,直接入队列;
遇到大写字母,翻译大写后入队列;
遇到括号,将栈中保存的元素依次出栈入队列; }
}
4)翻译大写处理模块.
spenqueue(&*q,key)
{
switch(key)
{
找到各个大写字母对应的字符串.
没有相应的则解释为’***’
}
}
#各模块之间调用关系:
三、详细设计
1.定义全局变量
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define NULL 0
#define OVERFLOW -2
#define MAXSIZE 100
#define stack_init_size 100
#define stackincrement 10
typedef char selemtype;
typedef char qelemtype;
typedef char elemtype;
typedef int status;
char e;
char demon[MAXSIZE];
2.栈类型及其基本操作
typedef struct
{
selemtype *base; //栈底指针
selemtype *top; //栈顶指针
int stacksize; //栈存储空间的大小
}sqstack;
status initstack (sqstack *s)
{
s->base=(selemtype *)malloc(stack_init_size*sizeof(selemtype)); if(!s->base) exit (OVERFLOW);
s->top=s->base;
s->stacksize=stack_init_size;
return OK;
}/*创建栈*/
status push (sqstack *s,selemtype e)
{
if(s->top-s->base>=s->stacksize)
{
s->base=(elemtype *)
realloc(s->base,(s->stacksize+stackincrement)*sizeof(elemtype)); if(!s->base) exit(OVERFLOW);
s->top=s->base+s->stacksize;
s->stacksize+=stackincrement;
}
*(s->top++)=e;
return OK;
}/*入栈*/
status pop(sqstack *s,selemtype *e)
{
if(s->top==s->base) return ERROR;
*e=*(--(s->top));
return OK;
}/*出栈*/
3.队列类型及其基本操作
typedef struct qnode
{
qelemtype data;
struct qnode *next;
}qnode,*queueptr;
typedef struct
{
queueptr front; //队头指针
queueptr rear; //队尾指针
}linkqueue;
status initqueue(linkqueue *q)
{
q->front=q->rear=(queueptr)malloc(sizeof(qnode)); if(!q->front) exit(OVERFLOW);
q->front->next=NULL;
return OK;
}/*创建队列*/
status enqueue(linkqueue *q,qelemtype e)
{
queueptr p;
p=(queueptr)malloc(sizeof(qnode));
if(!p) exit(OVERFLOW);
p->data=e;
p->next=NULL;
q->rear->next=p;
q->rear=p;
return OK;
}/*入队*/
status dequeue(linkqueue *q,qelemtype *e)
{
queueptr p;
if(q->front==q->rear) return ERROR;
p=q->front->next;
*e=p->data;
q->front->next=p->next;
if(q->rear==p)
{
q->rear=q->front;
}
free(p);
return OK;
}/*出队*/
4.括号内元素入栈处理函数
void tempstack(sqstack *temps)
{
int i=0;
char t;
char c;
c=demon[i ];
for(i=0;c!='#';i++)/*遍历数组*/
{
c=demon[i ];
if(c=='(')/*遇到开括号*/
{
t=demon[i+1];/*取括号中的首字母*/
push(temps,t);/*入栈*/
i++;/*指向首字母*/
do
{
i++;
c=demon[i ];
push(temps,c)/*第一次循环将次字母入栈*/; push(temps,t);/*再将首字母进栈*/
}
while(c!=')');/*直到括号中元素全部进栈*/ pop(temps,&t);/*将多余进栈的首字母t出栈*/ pop(temps,&t); /*将多余进栈的’)’出栈*/ }
}
}/*临时栈*/
5.特殊入队函数
void spenqueue(linkqueue *q,char key)
{
int j=0;
char a[5];
switch(key) /*判断大写字母对应的字符串*/
{
case'A':strcpy(a,"sae");break;
case'B':strcpy(a,"tAdA");break;
case'C':strcpy(a,"abc");break;
case'D':strcpy(a,"def");break;
case'E':strcpy(a,"ghi");break;
case'F':strcpy(a,"klm");break;
case'G':strcpy(a,"mop");break;
case'H':strcpy(a,"jkk");break;
default:strcpy(a,""); /*不能翻译的魔王语言以””输出*/
}
while(a[j]!='\0') /*如果数组还有字母*/
{
enqueue(q,a[j]);/*进队*/
j++;
}
}/*特殊入队*/
6.排序入队处理函数
status sort(sqstack *s,linkqueue *q)
{
qnode b;
int flag=0;/*大写字母监视哨置零*/
int i;
for(i=0;demon[ i]!='#';i++)/*遍历数组*/
{
b.data=demon[ i];
if( ('a'<=b.data&&b.data<='z') || b.data=='?') /*如果是小写字母或者’?’则直接入队*/
{
enqueue(q,b.data);
}
else
{
if('A'<=b.data&&b.data<='Z') /*如果是大写字母,则调用特殊入队函数,*/
{
spenqueue(q,b.data);
flag=1; /*发现大写字母监视哨置1*/
}
else
{
if(b.data=='(')/*如果是括号*/
{
do
{
pop(s,&e);
enqueue(q,e);
}
while(!(s->top==s->base)); /*只要栈不为空,则出栈进队*/
while (b.data!=')') /*只要还指向括号内元素,就继续往后移,保证原括号内的元素不再进栈*/
{
i++;
b.data=demon[ i];
}
}
}
}
}
return flag;
}/*排序*/
status main()
{
sqstack s1;
linkqueue q1;
int k=0;
int flag=1;
printf("\n\n\n\t\t\t欢迎使用\n");
printf("\t***************************************\n");
printf("\t请输入魔王语言:\n\t");
printf("!: 以'#'结束: )\n\t");
printf("***************************************\n\t");
printf("魔王语言:\n\t");
scanf("%s",demon);
printf("\n\t***************************************");
initstack(&s1); /*创建栈*/
initqueue(&q1); /*创建队*/
tempstack(&s1); /*调用函数*/
while (flag==1) /*如果有大写字母*/
{
k=0;
flag=sort(&s1,&q1);
while(q1.front!=q1.rear) /*重写demon[i ]*/
{
dequeue(&q1,&e);
demon[k]=e;
k++;
}
demon[k]='#';
}
demon[k]='\0';
printf("\n\t翻译后的人类语言为:\n\t%s",demon);
printf("\n\n\t***************************************");
printf("\n\t\t\tTHANK YOU!\n\t");
printf("Please press any key to exit...");
} /*主函数*/
7.函数之间的调用关系
四、调试分析
1. 函数调用比较多,因而得仔细对待数值和地址的传递.
2. 由于魔王语言中’B’中仍然包含着大写字母(tAdA).所以考虑设置flag.
3. 函数数组遍历.进栈出栈.入队出队中都要牵扯指针的移动,所以要仔细考虑一循环的条件以及进栈元素的个数.
4.程序只适用于字符的翻译,对其它类型数据不适用。
五、测试结果
1,B(ehnxgz)B#->tsaedsaeezegexenehetsaedsae ;
2,gongjy#->gongjy ;
3,485jhkdjg#->jhkdjg;
六、用户手册
1. 本程序运行环境为DOS/WINDOWS操作系统,执行文件为:魔王语言解
释.exe。
2. 进入程序即显示提示信息:
WELCOME!
***************************************
请输入魔王语言:
!: 以 '#'结束: )
***************************************
魔王语言:
等待用户输入.用户输入字符以’#’回车结束
*************************************** 翻译后的人类语言为:
(翻译后的语言)
*************************************** THANK YOU!
例如:
题目二:文学研究助手
一、需求分析
[问题描述]
文学研究人员需要统计某篇英文小说中某些形容词的出现次数和位置,试写一个实现这一目标的文字统计系统,称为“文学研究助手”。
[基本要求]
1、英文小说存于一个文本文件中。
待统计的词汇集合要一次输入完毕,即统计
工作必须在程序的一次运行之后全部完成,程序的输出结果是每个词的出现次数和出现位置所在行的行号,格式自行定义。
2、待统计的“单词”在文本串中不跨行出现,它或者从行首开始,或者前置若干
空格字符。
3、“单词”定义:由字母构成的字符序列,中间不含空格字符且区分大小写。
4、文本串非空且以文件形式存放,统计匹配的词集非空。
文件名和词集均由用
户从键盘输入。
[测试数据]
1、测试文件为“文学研究助手.txt”,从中任意选取“单词”作为测试的词集。
二、概要设计
1,数据结构设计:
1)串的抽象数据类型
ADT String{
数据对象:D={ai|ai∈CharacterSet, i = 1,2,3,…….,n,n ≥0}
数据关系:R1={<ai-1,ai>} | ai-1,ai ∈D,i = 1,2,3,…..,n}
基本操作:
StrAssign(&T,chars)
//初始条件:chars是字符串常量。
//操作结果:生成一个其值等于chars的串T。
StrLength(S)
//初始条件:串S存在。
//操作结果:返回S的元素个数,称为串的长度
2)线性链表的抽象数据类型
ADT list{
数据对象:D={ai|ai∈CharacterSet, i = 1,2,3,…….,n,n ≥0}
数据关系:R1={<ai-1,ai>} | ai-1,ai ∈D,i = 1,2,3,…..,n}
基本操作:
InitList(&L)
//操作结果:构造一个空的线性链表。
CreateNode(p,e)
//初始条件:线性链表存在。
//操作结果:分配由p指向的值为e的结点,并返回OK。
Append(LinkList &L,Link s)
//将指针s所指的串结点链接在线性表L的最后一个结点之后,并改变链表L的尾指针指向新的尾结点。
2,模块设计:
1)主函数模块,其中主函数为
void main( ){
输入信息和文件初始化
生成测试目标词汇表
统计文件中每个待测单词出现的次数和位置
输出测试结果
};
2)字符串单元模块——实现串类型;
3)行数据链表单元模块——实现行数据链表类型;
4)串数据链表单元模块——实现串数据链表类型;
#各模块之间的调用关系
三、详细设计
1,定义全局变量
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
2,串的堆分配存储表示
typedef struct{
char *ch; //若是非空串,则按串长分配存储区,否则ch为NULL int length; //串长度
}HString;
串的基本操作的算法实现:
Status StrInit(HString &T){
//初始化串
T.ch=NULL; T.length=0;
return OK;
}
Status StrAssign(HString &T, char *chars){
//生成一个其值等于串常量chars的串T
//if(T.ch) free(T.ch); //释放原有的串空间
int len; char *c;
for(len=0, c=chars;*c;++len,++c); //求chars的长度i;
if(!len) {T.ch=NULL; T.length=0;}
else{
if( !(T.ch = (char *) malloc(len*sizeof(char) ) ))
exit(OVERFLOW);
for(int i=0;i<len;i++) T.ch[i] = chars[i];
T.ch[i]='\0'; //结尾加上\0
T.length = len;
}
return OK;
}
int StrLength(HString s){
//返回串长度
return s.length;
}
void StrPrint(HString s){
//打印串
for(int i=0;i<s.length;i++)
printf("%c",s.ch[i]);
//printf("\t");
}
int Index(HString s,HString t, int pos){
//返回子串t在主串s中第pos个字符之后的位置。
如不存在,则返回0 int i=pos,j=1;
while(i<=s.length && j<=t.length){
if(s.ch[i-1]==t.ch[j-1]) i++,j++;
else i=i-j+2,j=1;
}
if(j>t.length) return i-t.length;
else return 0;
}
int Next(HString s,int j){
//KMP模式匹配的next函数
if(j==1) return 0;
for(int k=j-1;k>1;k--){
for(int i=1;i<k;i++){
if(s.ch[i-1] != s.ch[j-k+i-1])
break;
}
if(i==k) break;
}
return k;
}
int Index_KMP(HString s,HString t, int pos){
//KMP算法
int i=pos,j=1;
while(i<=s.length && j<=t.length){
if(j==0 || s.ch[i-1]==t.ch[j-1]) i++,j++;
else j=Next(t,j);
}
if(j>t.length) return i-t.length;
else return 0;
}
int IsNotCharactor(char ch){
//判断该字符是不是字母
return !(ch>='a'&&ch<='z' || ch>='A'&&ch<='Z');
}
int StrCount_KMP(HString s,HString t,int pos){
//求串t在s中出现的次数
int i=pos,j=1;
int count=0;
while(i<=s.length ){
if(j==0 || s.ch[i-1]==t.ch[j-1]) i++,j++;
else j=Next(t,j);
if(j>t.length) {
//if(s.ch[i-1]==' '||s.ch[i-1]=='\0') //如果下一个字符为' ',或者为串的结尾
// if(s.ch[i-t.length-2]==' ' || i-t.length-2<0) //并且前一个字符为' ',或者为串的开头
if(IsNotCharactor(s.ch[i-1]) &&
IsNotCharactor(s.ch[i-t.length-2])) //如果该字符串的前一和后一字符不是字母,说明找到了
count++; //次数++
j=1;
}
}
return count;
}
2,行数据链表
typedef struct LRowDataElem{
int row; //所在行
int count; //次数
}LRowDataElem;
typedef struct LRowNode{
LRowDataElem data;
struct LRowNode *next;
}LRowNode,*RowLink;
typedef struct{
RowLink head,tail;
int len;
}RowLinkList; //行数据链表
行数据链表的基本操作的算法实现:
Status InitList(RowLinkList &L){
L.head = (RowLink)malloc(sizeof(RowLink));
L.tail = (RowLink)malloc(sizeof(RowLink));
if(!L.head || !L.tail ) exit(OVERFLOW);
L.head->next = L.tail->next = NULL;
L.len =0;
return OK;
}
Status CopyElem(LRowDataElem &e1,LRowDataElem e2){
e1.count = e2.count; e1.row = e2.row;
return OK;
}
Status CreateNode(RowLink &rl, LRowDataElem elem){
//创建节点
rl = (RowLink) malloc(sizeof(RowLink));
if(!rl) exit(OVERFLOW);
CopyElem(rl->data,elem);
rl->next = NULL;
return OK;
}
Status Append(RowLinkList &ls, RowLink link){
//附加节点
if(ls.head->next == NULL) ls.head->next = link;
else ls.tail->next->next = link;
ls.tail->next = link;
ls.len ++;
return OK;
}
Status PrintList(RowLinkList L){
//打印列表
RowLink h = L.head->next;
while(h ){
printf("%d : %d ",h->data.row, h->data.count);
h = h->next;
}
printf(" ");
return OK;
}
3,串数据链表
typedef struct StringNode{
HString str; //要查找的串
RowLinkList rowlist; //串匹配的行数据列表,每个串对应一个 StringNode *next;
}StringNode,*StringLink;
typedef struct{
StringLink head,tail;
int len;
}StringLinkList;
串数据链表的基本操作的算法实现:
Status InitList(StringLinkList &L){
L.head = (StringLink)malloc(sizeof(StringLink));
L.tail = (StringLink)malloc(sizeof(StringLink));
if(!L.head || !L.tail ) exit(OVERFLOW);
L.head->next = L.tail->next = NULL;
L.len =0;
return OK;
}
Status MakeNode(StringLink &sl, HString str){
sl = (StringLink) malloc(sizeof(StringLink));
if(!sl) exit(OVERFLOW);
StrInit(sl->str);
StrAssign(sl->str, str.ch);
InitList(sl->rowlist);
sl->next = NULL;
return OK;
}
Status Append(StringLinkList &ls, StringLink link){
if(ls.head->next == NULL) ls.head->next = link;
else ls.tail->next->next = link;
ls.tail->next = link;
ls.len ++;
return OK;
}
Status StrListCount(StringLinkList &stringLinkList, HString hstrLine,int row){
//在串数据链表stringLinkList,读出查找的串strkey,与传入的串hstrLine匹配
//如果成功将匹配的次数与行数row,写入相对应的行行数据链表
StringLink stringLink = stringLinkList.head->next; //找出第一个串
while(stringLink){
HString strkey = stringLink->str;
int count=StrCount_KMP(hstrLine, strkey,1); //求匹配的次数
if(count>0) {
RowLink rLink;
LRowDataElem data; data.count = count;data.row=row;
CreateNode(rLink,data);
Append(stringLink->rowlist,rLink); //写入相对应的行行数据链表
}
stringLink = stringLink->next; //找下一个 }
return OK;
}
Status PrintList(StringLinkList L){
//打印串数据链表的信息
StringLink h = L.head->next;
while(h ){
printf("字符串:");
StrPrint(h->str);
printf(" 出现的位置和次数是: ");
PrintList(h->rowlist);
h = h->next;
}
printf(" ");
return OK;
}
4,主函数代码
void main()
{
FILE *fp;
char filename[100];
printf("请输入文件名: ");
gets(filename);
if( !(fp=fopen(filename,"r")) ){
printf("打开文件失败!! ");
exit(0);
}
StringLinkList s;
InitList(s);
HString hstrkey;
StrInit(hstrkey);
StringLink stringLink;
char key[100];
printf("请输入要统计的词汇:\n ");
gets(key);
while( strcmp(key,"#") ){
StrAssign(hstrkey,key);
MakeNode(stringLink,hstrkey);
Append(s,stringLink);
gets(key);
}
//
char line[LEN];
int row=0;
while( fgets(line,LEN,fp) ){
row++;
HString hstrLine;
StrAssign(hstrLine,line);
StrListCount(s,hstrLine,row);
}
PrintList(s);
printf("\n");
}
5,函数调用关系
四、调试分析
1.
2.
3.程序的扩展方向:
程序还不能完全处理汉字串的情况,可以向着类似offic和一般的文本编辑工具所提供的全字匹配查找与一般查找方向拓展,真正做到“一查即得,一查即准”。
同时,针对统计延时,可以在参考KMP算法的同时,加以优化改良。
五、测试结果
六.用户手册
1. 本程序运行环境为DOS/WINDOWS操作系统,执行文件为:文学研究助手.exe。
2.进入程序后即显示提示信息:请输入文件名:以等待用户输入待统计的文本文件名(一个以回车为结束标志的字符串),如果该文件不存在,则显示信息:打开文件失败。
3.在找到用户需要统计的文件后,显示提示信息:请输入要统计的词汇:以等待用户输入要统计的词汇(没输入一个词汇按回车结束,输完要统计的词汇后以“quit”结束),最后按回车键开始统计。
4.输入结束后,程序即开始统计。
随后输入统计信息:
字符串:“待统计词汇”出现的位置和次数是:
直到所有词汇统计完毕。
体会与自我评价
理解分析问题的能力得到提高。
设计一个应用程序关键是对要求做最准确的把握,也就是说弄清楚需求分析是很重要的。
本程序要求我从文件中读取单词的
位置,就是在文件中检索字符串,这样一抽象,问题的脉络就清晰了。
接下来,如何读取,读取后如何映射,映射的字符串又怎么和待查字符串关联,这就构成了解决问题的几大关键模块。
逐个解析,整个程序的框架就了然于胸了。
特别要指出的是,对整个程序的把握,随着编程工作的深入,是越来越深刻,而且新的思路也是层出不穷。
创新意识和创新能力的提高。
本程序的设计,我最开始的想法是按照习题集上所指导的参考KMP算法,后来,我放弃了KMP算法,原因有两个,其一是KMP算法的缺陷,就我的了解来看,getlength 中含有一部分是length,若用KMP算法来检索length,肯定会认为getlength也算作length的出处。
这显然不合单词的定义要求。
其二是我想自己设计一种新的算法来挑战自己,挑战自己的数据结构与算法设计能力。
这也是最重要的原因。
于是,在这种动力的催动下,我设计出了新的算法,如源程序中的locate()和count ()所示的定位和统计方法。
撇开算法的性能来看,本人的创新能力是值得肯定的。
而分析到算法的执行性能,本人认为也是可以得到肯定的。
通过这个项目的课程设计,我真正提高了创新意识和创新能力。
对程序设计语言的细微之处又了更深刻的理解。
由于字符串的操作是很原始的几于原子的操作,所以更能看清楚平时我们所用的字符串操作函数在底层的实现机制,尽管我的实现和标准字符串的实现原理和实施手段会又不同,但是根本认识也差的不远。
同时,对指针也有了新的认识,譬如程序中有一处检验指针操作是否到了结束处,我惯用的就是
while(p),j结果陷入了死循环,非得用while(*p)来检测,这就让我对new 和指针的本质有了更深刻的认识。
再就是有时候p == NULL 和!p其实不是一回事。
这些平时不在意的问题,这次统统暴露了。