標籤:

程序邏輯錯誤的調試--以獎學金計算題目為例

程序邏輯錯誤的調試--以獎學金計算題目為例

來自專欄 Sun Flowers C World

很多同學都會遇到一個這樣的問題,程序沒有語法錯誤,但是運行的結果不對。這種情況下就需要對程序進行調試,查找出現問題的位置,並進行修改。今天以Codeblocks工具介紹下C語言程序的調試方法。程序調試方法主要有兩個:(1)利用printf語句列印中間運算結果,分析程序運行的過程與預計的都哪些不同,進而查找到相應的問題;(2)在Debug模式下運用斷點或者Run to cursor/next line等工具,利用watches對話框查看在程序運行的過程以及運行到不同位置時各變數的值,以此來判斷程序中邏輯錯誤的位置並改正。今天重點介紹第一種方法。先說題目:

題目:誰拿了最高獎學金

題目描述:某校的慣例是在每學期的期末考試之後發放獎學金。發放的獎學金共有五種,獲取的條件各自不同:

  1) 院士獎學金,每人8000元,期末平均成績高於80分(>80),並且在本學期內發表1篇或1篇以上論文的學生均可獲得;

  2) 五四獎學金,每人4000元,期末平均成績高於85分(>85),並且班級評議成績高於80分(>80)的學生均可獲得;

  3) 成績優秀獎,每人2000元,期末平均成績高於90分(>90)的學生均可獲得;

  4) 西部獎學金,每人1000元,期末平均成績高於85分(>85)的西部省份學生均可獲得;

  5) 班級貢獻獎,每人850元,班級評議成績高於80分(>80)的學生幹部均可獲得;

  只要符合條件就可以得獎,每項獎學金的獲獎人數沒有限制,每名學生也可以同時獲得多項獎學金。例如姚林的期末平均成績是87分,班級評議成績82分,同時他還是一位學生幹部,那麼他可以同時獲得五四獎學金和班級貢獻獎,獎金總數是4850元。

  現在給出若干學生的相關數據,請計算哪些同學獲得的獎金總數最高(假設總有同學能滿足獲得獎學金的條件)。

輸入:

題目中的輸入數據為多組!每組數據組成如下:

 輸入的第一行是一個整數N(1 <= N <= 100),表示學生的總數。接下來的N行每行是一位學生的數據,從左向右依次是姓名,期末平均成績,班級評議成績,是否是學生幹部,是否是西部省份學生,以及發表的論文數。

姓名是由大小寫英文字母組成的長度不超過20的字元串(不含空格);期末平均成績和班級評議成績都是0到100之間的整數(包括0和100);是否是學生幹部和是否是西部省份學生分別用一個字元表示,Y表示是,N表示不是;發表的論文數是0到10的整數(包括0和10)。

每兩個相鄰數據項之間用一個空格分隔。

輸出:

輸出包括三行,第一行是獲得最多獎金的學生的姓名,第二行是這名學生獲得的獎金總數。

如果有兩位或兩位以上的學生獲得的獎金最多,輸出他們之中在輸入文件中出現最早的學生的姓名。第三行是這N個學生獲得的獎學金的總數。

input example:

4

YaoLin 87 82 Y N 0

ChenRuiyi 88 78 N Y 1

LiXin 92 88 N N 0

ZhangQin 83 87 Y N 1

output example:

ChenRuiyi

9000

28700

我們首先來分析一下程序的設計與實現過程,然後再調試同學的錯誤代碼。

一、程序分析

1.數據類型

(1)由於存儲的學生的信息類型多種多樣,因此,我們最好定義個學生類型的結構體,把學生的姓名、平均成績、評議成績、是否幹部、是否省份和論文數定義為結構體的成員,這樣n個同學的信息就可以同一個學生結構體數組來進行存儲了。各個學生獎學金的總數可以存儲在一個整型數組中,也可以把獎學金總數作為一個成員來進行進行存儲。結構體中姓名類型應該是長度為20的字元數組,平均成績、評議成績、論文數量、獎學金數量用int類型變數即可,是否幹部和是否省份的處理有兩種方式,用字元數組和字元型變數來進行存儲。正常來講,這兩個變數的值只有『Y和『N,也就是一個字元的長度,應該用char類型變數來進行存儲,但是有的同學在處理char類型變數值的接受時總是忘記處理兩個數據之間的空格,導致數據的接受不正確,後面會進行單獨介紹,因此,採用長度為2的字元數組來存儲,這樣用scanf("%s")就可以接收了,不需要處理空格。這種方法雖然帶來了一時之便,但是卻為後面數據的值的判斷帶來了麻煩,判斷字元型變數c的值是否為Y可以直接用c==Y 來進行判斷,但是如果用字元數組c來進行存儲,判斷值是否為Y時,則必須用strcmp(c,"Y")==0來進行判斷。因此,建議用char類型變數來存儲。

(2)定義了學生類型的結構體後,就可以定義一個結構體類型的數組來存儲學生的信息了,由於題目中學生數量n的值的範圍為1=<n<=100,因此,結構體數組的大小定義為100.

2.程序設計

程序處理的思路如下:

(1)首先接受學生的數量,存入變數n。由於是多組數據處理,因此要設計為一個循環,循環的條件是scanf語句返回的值是否為-1,如果不是,則說明還沒有遇到文件結束,後面有一組的數據需要處理,因此循環體要進行執行,如果是,則跳出循環。

(2)用循環來接受n個同學的信息,注意學生信息的格式是:姓名 期末平均成績 班級評議成績 是否班幹部 是否西部學生 論文數量,再是否為班幹部和是否為西部學生的字元前有一個空格,對於char類型變數來講,空格是合法的輸入字元,因此如果不處理,將會把空格存入對應的變數中。故這裡要再%c前加上一個空格,來進行處理。因此在完成數據輸入時,scanf語句的格式控制字元串應該為"%s%d%d %c %c%d"。

(3)計算n個同學的獎學金。這裡可以講計算一個學生的獎學金寫成一個函數,函數接受一個結構體變數s,然後計算s的獎學金數量並作為函數返回值進行返回。也可以接受一個結構體指針,直接把計算的獎學金數量存儲指向的結構體空間中。或者,接受一個結構體數組地址和數組中學生的個數n,在函數中計算每個同學的獎學金數額並存入各自的獎學金總數數據成員。函數原型可以為:int salary(struct student s); 或者void salary(struct student *p) 或者 void salary(struct student *p,int n).

(4)查找獎學金數量最多的同學的姓名,並累計n名同學的獎學金總數。由於本題目中明確說明如果有兩個或兩個以上同學獎學金最多,就只輸出在輸入文件中最早出現的學生的姓名,因此,這裡查找到的獎學金同學的信息可以用一個結構體變數帶回。因此,這裡可以用一個函數來完成查找獎學金數量最多的同學的姓名和累計n名同學的獎學金總數這兩個功能,函數接受一個結構體數組的地址,學生人數,用於返回獎學金最高的學生信息的結構體指針,返回一個int值表示所有人的獎學金總數。函數的原型可以為: int stat(struct student *p,int n, struct student *pmax).

(5)列印輸出結果。因此,實現代碼如下:

#include <stdio.h>#include <stdlib.h>typedef struct{ char name[20];//姓名 int score;//期末考試成績 int grade;//評議成績 char isLeader;//是否班級幹部 char isWest;//是否西部學生 int paper;//論文數量 int sum;//獎學金總數}student;void input(student *p,int n){ int i; for(i=0;i<n;i++) scanf("%s%d%d %c %c%d",(p+i)->name,&(p+i)->score,&(p+i)->grade,&(p+i)->isLeader,&(p+i)->isWest,&(p+i)->paper); }void output(student *p,int n){ int i; for(i=0;i<n;i++) printf("%s %d %d %c %c %d
",(p+i)->name,(p+i)->score,(p+i)->grade,(p+i)->isLeader,(p+i)->isWest,(p+i)->paper);}int salary(student *p,int n){ int i,sum; for(i=0;i<n;i++) { sum=0;//很重要!!!!必須放在這裡,如果放在for外則不可以。為每一個同學計算總數時都要先清0。否則上一個同學的計算結果為累計到下一個同學上。。 //院士獎學金 if(p[i].score>80 && p[i].paper>=1 ) sum+=8000; //五四獎學金 if(p[i].score>85 && p[i].grade>80 ) sum+=4000; //成績優秀獎 if(p[i].score>90) sum+=2000; //西部獎學金 if(p[i].score>85 && p[i].isWest==Y) sum+=1000; //班級貢獻獎 if(p[i].grade>80 && p[i].isLeader==Y) sum+=850; p[i].sum=sum; }} int stat(student *p,int n, student *pmax) { int i,sum,max,index; //初始值賦初值很重要!!!! sum=p[0].sum;//很多同學這裡都賦值為0,這樣for循環中i的初始值就必須為0,不能是1 max=p[0].sum;//最大值的查找過程中,需要記錄最大值和最大值元素的下標。也可以只記錄下標 index=0; for(i=1;i<n;i++) { if(max<p[i].sum)// 如果不使用max變數,還可以使用if(p[max].sum<p[i].sum都可以 { max=p[i].sum; index=i; } sum+=p[i].sum; } *pmax=p[index]; //注意,這裡用pmax=p+index; 不可以。原因是函數中對形參pmax的修改不會帶回主調函數 //必須通過對pmax進行間接訪問,直接修改main中對應變數的內存空間 return sum; }int main(){ student a[100],max; student *p; int n,sum; while(scanf("%d",&n)!=EOF) { input(a,n); salary(a,n); sum=stat(a,n,&max);//注意這裡需要用&max做實參,max不行 // 上面的代碼中,&max用p也不行,雖然沒有語法錯誤,但是p沒有賦值,是野指針,也不可以 printf("%s
%d
\%d
",max.name,max.sum,sum); //output(a,n); } return 0;}

二、錯誤代碼分析與調試

同學發來的錯誤代碼如下:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <ctype.h>struct Student{ char name[30]; int score_end; int score_cla; char stu_c[2]; char stu_w[2]; int num; int sum;};void input(struct Student *p,int n){ int i; for(i=0;i<n;i++) { scanf("%s%d%d%s%s%d",p->name,&(*p).score_end, &(*p).score_cla,p->stu_c,p->stu_w,&(*p).num); // printf("%s %d %d %s %s %d",p->name,p->score_end,p->score_cla, //p->stu_c,p->stu_w,p->num); }}void output(struct Student *p,int n){ int i; for(i=0;i<n;i++) printf("%s%d%d%s%s%d",p->name,p->score_end,p->score_cla, p->stu_c,p->stu_w,p->num);}int main(){ struct Student stu[30]; int i,j,k,t,n,max,sum; int b[30]; while(scanf("%d",&n)!=-1) { j++;//j++的目的是什麼??? input(stu,n);//輸入n個學生的信息 //output(stu,n); sum=0; max=0; t=0; for(i=0;i<n;i++) { j=0; if(stu[i].score_end>80&&stu[i].num!=0) b[j]+=8000; if(stu[i].score_end>85&&stu[i].score_cla>80) b[j]+=4000; if(stu[i].score_end>90) b[j]+=2000; if(strcmp(stu[i].stu_w,"Y")==0) { if(stu[i].score_end>85) b[j]+=1000; } if(strcmp(stu[i].stu_c,"Y")==0) { if(stu[i].score_cla>80) b[j]+=850; } printf("%d
",b[j]); j++; } for(j=0;j<n;j++) { if(b[max]<b[j]) {b[max]=b[j]; max=j;} sum=sum+b[j]; } printf("%s
",stu[max].name); printf("%d
",b[max]); printf("%d
",sum); } return 0;}

遇到程序運行結錯誤,可以用printf語句在程序中輸出中間運行結果和各種提示信息,依次來查看程序的運行結果。

(1)先在接收到n後,j++後輸出n,j的值,看看接收是否正確。從系統運行結果中可以看出,n接收正常,但是j的值是一個亂碼。結合上面可以看出j沒有賦初值。O(∩_∩)O~,我真實看不出來j++;在這裡是幹什麼的。不管因為什麼了,如果想用記得為j賦初值。這裡我就為其注釋掉了。

(2)在調用input函數完成n個學生信息輸入後,調用output函數輸出學生信息,查看輸入是否成功。一定要逐步查看,別覺得這部分一定不會出問題,不停的在後面調試,很多時候都是出現了低級錯誤,沒有意識到,後面怎麼調都調不對。通過查看運行結果就會發現現實的信息很亂。因此應該查看input函數和output函數是否有問題。

(3)先查看output函數,看看輸出的設置是否正確,然後再分析輸入是否有問題。學生的信息之間都沒有分割,也沒有換行。因此,輸出的信息很亂。

改為以下內容:

修改後運行界面如下所示,發現輸出的都是只有ZhangQin的信息。再次查看output函數,會發現for循環中每次輸出的都是p指向的學生的信息,而且p的值在循環中沒有更改。因此,應該在for語句後i++的後面,加上p++。

(4)修改後再次運行,效果如下所示:就能看出只有ZhangQin一個人的信息接收成功,後面輸出的都是亂碼,因此接收信息input函數應該有問題。

(5)查看如下input函數部分代碼就會發現,每次輸入都是只存入了p指向的內存區域,在完成一個同學信息後,並沒有移動指針p,這樣就導致所有學生的信息都只存入了p[0]元素的空間中。因此,程序應該把for循環中p改為p+i,或者for語句中i++的後面加上p++。

(6)修改後再次執行,界面如下圖,就能看出程序執行正常,輸入正確了。接下來需要在main函數中接著進行調試,以查找問題。本部分的輸出臨時信息可以注釋掉了。

(7)在main函數中,接下來的for語句用於計算n個學生的獎學金,可以通過輸出每個人的獎學金總數查看程序的執行過程。從後側的運行界面中可以看出i的值是正確的,而j的值每次執行都是0,這樣每次計算都是把獎學金存入b[0],同時b中存儲的數據顯然也都是錯誤的,查看for語句中會發現,for下第一句j=0,這樣就導致每次循環體執行j的值都改為0,這樣導致的問題。而這裡j是用來控制將第i個同學的獎學金總數存入b數組中的相應元素中的。那麼,第i個同學的獎學金總數應該放入那個元素呢?直接放入b[i]就完了,根本用不到j。這裡,顯然是用j來控制數組b的訪問過程了,那麼j=0就應該在for循環外賦值,而不應該在循環裡面賦值,這樣每次執行一次循環體,最後j++變為1,在下一次執行循環體時,它又悲催的變為0了!!關於b[0]元素中存儲的獎學金的總數問題,應該想到這樣的一個數就是一個亂碼,第一反應應該是忘記給b[0]元素賦初值了。往前看,確實沒有。因此,在使用b[j]元素存儲獎學金總數,每次都用+=的時候,一定要想到為其賦初值0。

(8)將代碼修改後,修改部分見灰色背景部分,通過右側的執行過程看,每個人的計算應該是對的。

(9)接下來main函數中的for語句用於查找最大值。可以在for語句的前後輸出數組b的值。通過右側的運行界面可以看出,在最大值的查找過程中,是不應該修改數組的各元素的值的,而只需要記錄最大值和最大值所在的下標。而程序中用max來表示最大值的下標,當b[max]<b[j]時,max=j;可以實現max記錄最大值下標的功能,這樣在for循環執行完畢後可以通過b[max]獲取到最大值,但是,另外一句b[max]=b[j];語句修改了max舊值表示的數組元素的值。如j=1時,max的值為0,b[max]=b[0]=4850,b[j]=b[0]=9000,b[max]<b[j],此時,執行b[max]=b[j]語句即b[0]=b[1]後,b[0]的值改為9000,然後執行max=j後,max的值改為1。然後將b[j]累加到sum中。因此,sum累加的值沒有計算錯誤,但是,修改了數組b的值,顯然存在邏輯錯誤。

(10)修改上面的代碼,如下:再次測試,正確。

(11)將修改後的代碼提交。提交後顯示如下的錯誤提示,一般出現這種問題的原因就是程序執行中內存訪問出現問題,導致這種問題出現的操作有數組越界訪問、對野指針進行間接訪問等。首先檢查input函數,發現沒有什麼問題,此時可以查看數組定義的長度。就會發現stu數組定義的長度為30,而題目描述中說明n的範圍為1=<n<=100,這樣就有可能伺服器端驗證示例中n的值大於30,導致發生了數組越界訪問的問題。將stu數組的大小改為100後,提交。

(12)提交後,顯示結果為下圖,可以看出,伺服器端驗證示例中n的值為100,真的發生了數組越界訪問的問題,導致了段錯誤。因此,提醒大家,在進行程序調試的過程中,認真審題,根據題目描述進行程序分析,遇到錯誤後耐心進行調試,總會發現錯誤。不要因為一個例子正確就認為程序正確,要用多個示例來測試程序正確與否。同時,在完成題目後,再多分析是否可以對程序進行優化,達到舉一反三的效果。

調試後正確的代碼為:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <ctype.h>struct Student{ char name[30];//姓名 int score_end;//期末成績 int score_cla;//班級測評成績 char stu_c[2];//是否班幹部 char stu_w[2];//是否西部學生 int num;//論文數量 int sum;//應該是用來存儲獎學金數量的,但是題目中沒有用到};void input(struct Student *p,int n){ int i; for(i=0;i<n;i++) { scanf("%s%d%d%s%s%d",(p+i)->name,&(p+i)->score_end, &(p+i)->score_cla,(p+i)->stu_c,(p+i)->stu_w,&(p+i)->num); // printf("%s %d %d %s %s %d",p->name,p->score_end,p->score_cla, //p->stu_c,p->stu_w,p->num); }}void output(struct Student *p,int n){ int i; for(i=0;i<n;i++,p++) printf("%s %d %d %s %s %d
",p->name,p->score_end,p->score_cla, p->stu_c,p->stu_w,p->num);//輸出時各信息之間沒有分隔,每個同學的信息沒有換行}int main(){ struct Student stu[100]; int i,j,k,t,n,max,sum; int b[30]; while(scanf("%d",&n)!=EOF) { //j++;看不出什麼用處 //printf("n=%d,j=%d
",n,j);
input(stu,n); //output(stu,n); sum=0; max=0; t=0; j=0; for(i=0;i<n;i++) { b[j]=0; if(stu[i].score_end>80&&stu[i].num!=0) b[j]=8000; if(stu[i].score_end>85&&stu[i].score_cla>80) b[j]+=4000; if(stu[i].score_end>90) b[j]+=2000; if(strcmp(stu[i].stu_w,"Y")==0) { if(stu[i].score_end>85) b[j]+=1000; } if(strcmp(stu[i].stu_c,"Y")==0) { if(stu[i].score_cla>80) b[j]+=850; } // printf("i=%d,j=%d,sum=%d
",i,j,b[j]);
j++; } //for(i=0;i<n;i++) // printf("%d%c",b[i],(i==n-1)?
: );
for(j=0;j<n;j++) { if(b[max]<b[j]) {//b[max]=b[j]; 錯誤 max=j;} sum=sum+b[j]; } // for(i=0;i<n;i++) // printf("%d%c",b[i],(i==n-1)?
: );
printf("%s
",stu[max].name); printf("%d
",b[max]); printf("%d
",sum); } return 0;}

三、程序反思

上面的程序雖然提交通過了,但是有不太合理的地方,比如結構體中定義的sum成員就可以存儲沒人的獎學金總數,為什麼不使用反而要使用數組b??小夥伴們可以自己優化。這裡要討論的是查找獎學金數量最多的同學的姓名,並累計n名同學的獎學金總數的部分。由於本題目中明確說明如果有兩個或兩個以上同學獎學金最多,就只輸出在輸入文件中最早出現的學生的姓名,因此,這裡查找到的獎學金同學的信息可以用一個結構體變數帶回。但是,如果本題目中的要求是:如果有兩個或兩個以上同學獎學金最多時,輸出所有學生的姓名呢?

(1)利用上面調試的代碼在main函數中完成。

在上面調試好的代碼的基礎上進行修改的話,就可以修改查找最大值的for循環即可。先查找最大值和統計總數,然後輸出學生姓名。代碼實現如下:

int main(){ struct Student stu[100]; int i,j,k,t,n,max,sum; int b[30]; while(scanf("%d",&n)!=EOF) { //j++;看不出什麼用處 //printf("n=%d,j=%d
",n,j);
input(stu,n); //output(stu,n); sum=0; max=0; t=0; j=0; for(i=0; i<n; i++) { b[j]=0; if(stu[i].score_end>80&&stu[i].num!=0) b[j]=8000; if(stu[i].score_end>85&&stu[i].score_cla>80) b[j]+=4000; if(stu[i].score_end>90) b[j]+=2000; if(strcmp(stu[i].stu_w,"Y")==0) { if(stu[i].score_end>85) b[j]+=1000; } if(strcmp(stu[i].stu_c,"Y")==0) { if(stu[i].score_cla>80) b[j]+=850; } // printf("i=%d,j=%d,sum=%d
",i,j,b[j]);
j++; } //for(i=0;i<n;i++) // printf("%d%c",b[i],(i==n-1)?
: );
//查找最大值 max=b[0]; sum=0; for(j=0; j<n; j++) { if(max<b[j]) max=b[j]; sum+=b[j]; } //輸出最大值對應的學生姓名 for(j=0; j<n; j++) if(b[j]==max) printf("%s ",stu[j].name); printf("
"); printf("
%s
",stu[max].name); printf("%d
",b[max]); printf("%d
",sum); } return 0;}

(2)利用第一部分設計的函數來優化

在第一部分,設計了一個函數stat來完成查找獎學金數額最高的學生和統計n個學生獎學金總數的功能。顯然,如果存在多個學生獎學金數額最高的情況,用一個結構體變數是無法返回所有獎學金最大的學生的信息了,可以用結構體數組來存儲,這樣的話,形參 struct student *pmax不需要修改,但是,返回主調函數後就不清pmax指向的數組中究竟存儲了多少個同學的信息,因此,函數還需要返回獎學金總數最多的學生的個數。因此函數的原型可以用:

int stat(struct student *p,int n, struct student *pmax,int *pnum)來實現,把pmax指向的數組中元素的個數通過pnum指向的變數來進行存儲。具體實現代碼為:

int stat(student *p,int n, student *pmax,int *pnum){ int i,sum,max; //初始值賦初值很重要!!!! sum=p[0].sum;//很多同學這裡都賦值為0,這樣for循環中i的初始值就必須為0,不能是1 max=p[0].sum;//最大值的查找過程中,需要記錄最大值和最大值元素的下標。也可以只記錄下標 //先查找最大值和統計總數 for(i=1; i<n; i++) { if(max<p[i].sum)// 如果不使用max變數,還可以使用if(p[max].sum<p[i].sum都可以 { max=p[i].sum; } sum+=p[i].sum; } //保存數額最大的學生信息 *pnum=0; for(i=0;i<n;i++) { if(p[i].sum==max) { *pmax=p[i]; pmax++; (*pnum)++; } } //注意,這裡用pmax=p+index; 不可以。原因是函數中對形參pmax的修改不會帶回主調函數 //必須通過對pmax進行間接訪問,直接修改main中對應變數的內存空間 return sum;}//對應的main函數實現為:int main(){ student a[100],max[100]; student *p; int n,sum,i; while(scanf("%d",&n)!=EOF) { input(a,n); salary(a,n); sum=stat(a,n,&max,&n);//注意這裡需要用&max做實參,max不行 for(i=0;i<n;i++) printf("%s%c",max[i].name,i==n-1 ?
: ); printf("%d
\%d
",max[0].sum,sum); //output(a,n); } return 0;}

同時,還可以用另外一個方法來實現,用三個函數,一個函數GetMax用來統計n個學生中獎學金數額的最大值,一個函數GetSum用來計算總額,一個函數show用來輸出n個學生中獎學金最大的學生信息。具體實現代碼為:

int GetSum(student *p,int n){ int i,s=p[0].sum; for(i=1; i<n; i++)sum+=p[i].sum; return sum;}int GetMax(student *p,int n){ int max=p[0].sum; for(i=1; i<n; i++) if(max<p[i].sum) max=p[i].sum; return max;}void show(student *p,int n){ int i,max=GetMax(p,n); for(i=0;i<n;i++) { if(p[i].sum==max) printf("%s ",p[i].name); } printf("
");}//main函數為:int main(){ student a[100],max[100]; student *p; int n,sum,i; while(scanf("%d",&n)!=EOF) { input(a,n); salary(a,n); sum=GetSum(a,n); max=GetMax(a,n); show(a,n); printf("%d
\%d
",max[0].sum,sum); //output(a,n); } return 0;}

推薦閱讀:

遠程線程注入代碼
ReactNative 知識小集(1)-深入理解 React Native Debugging
在windbg中細究函數調用
深入X64架構(翻譯轉載)(4)之參數獲取

TAG:軟體調試 |