蛙蛙推薦:C語言入門之二——編寫第一個有意義的小程序

作者: 蛙蛙王子  來源: 博客園  發布時間: 2010-10-18 21:25  閱讀: 907 次  推薦: 0   原文鏈接   [收藏]  

  簡介

  上次配置好了linux+vim+gcc以及寫了一個HelloWorld級別的示例程序,這次寫一個稍微有意義的程序,在寫這個小程序的過程中,我們快速的對C語言有一個大致的了解,SICP里指出,要學一門語言,要注意3個方面,一是這個語言提供了哪些Primitive,如數據類型,表達式,語句;二是提供了哪些組合規則,三是提供了哪些抽象機制,我們學C的時候也有意識的留意一下。

  需求分析

  同事們中午一般都一起出去吃午飯,AA制,但每次吃飯都現場算錢的話,比較麻煩,不如一人付一次,輪換著付錢,最終付的錢還是均勻的。但有的時候今天吃的多,明天吃的少,而且有的人今天來了,明天沒來,所以要有個記賬的軟件,要記錄下哪天都有誰去吃飯了,花了多少錢,打了多少折扣,當天是誰付的款,然后程序能自動算出來,誰付款付的多,誰付款付的少,付款付的最少的今天就主動付款。(大家可以了解下www.5dfantuan.com)

  我定義了一個文件格式,每個字段用"|"分隔,從左到右每列一次是吃飯日期,總消費金額,折扣,吃飯的人,付款人和付款金額。其中吃飯的人用逗號分隔,付款記錄也用逗號分隔,每個付款記錄用冒號分隔開付款人和付款金額。

 
2010-9-10|83|0.8|a,b,c,d|a:100,b:100
2010-9-11|102|0.8|a,b,c,d,e|b:100,c:50

  比如以上的輸入文件input.txt,9月10日花了83塊錢,打了0.8折是66.4元,有4個人吃飯,分別是a,b,c,d,人均消費是66.4/4=16.6元,當天a和b各充了100元,那么今天a和b的余額就是100-16.6=83.4元,而c和d沒付錢,余額就是-16.6元,下次就應該讓他倆出錢。

  數據結構定義

  我們先進行數據結構的定義,在C里定義數據一般用struct來定義,c的struct不能定義函數(能定義函數指針),只能定義數據成員,而且不是原生支持的數據類型,使用類型的時候要加struct前綴。

  我們定義兩個常量,MAX_RECORD_COUNT定義input.txt里最大的記錄數(一行一個記錄),因為C里要自己管理內存,分配數據等要考慮個最大值,不像c#里有ArrayList這樣自動擴大的類,所以我們聲明列表類型的數據一般用數組,數組要給定一個最大長度。MAX_ARRAY_COUNT,這個定義普通字符串的最大長度,如輸入文件里各個字段的長度都不能超過這個長度。

data_structure.h
 
#define MAX_RECORD_COUNT 10
#define MAX_ARRAY_COUNT 15

struct people
{

char name[MAX_ARRAY_COUNT];
};

struct pay_record
{

struct people person;
double amount;
};

struct account_record
{

char date[MAX_ARRAY_COUNT];
double discount;
struct people person[MAX_ARRAY_COUNT];
int people_count;
struct pay_record payrecord[MAX_ARRAY_COUNT];
int pay_record_count;
int total_consumption;
};

struct account_record_list
{

struct account_record records[MAX_RECORD_COUNT];
int count;
};

struct person_consumption
{

char name[MAX_ARRAY_COUNT];
double consumption;
};

struct person_consumption_list
{

struct person_consumption persons[MAX_ARRAY_COUNT];
int count;

};

  如上,我們用struct account_record來表示一天的記賬記錄,account_record_list表示多條這樣的記錄,我們的命名規則就是表示多條數據類的結構后綴名加_list,并有一個count的成員表示有效數據的長度。struct account_record里各個成員分別對應輸入文件里的各個字段,比如struct people其實就是一個長度為15的字符數組,person_consumption表示每個人的余額。這里盡量不用typeof是因為那樣有些亂。

  在這里我們用到了各種數據類型的定義,如單個值int,double,一維數組,結構定義等。

  接口設計

  定義好了數據,就該定義操作這些數據的函數了,我們先從上層來分析都需要哪些模塊,模塊之間的依賴關系,以及模塊里有哪些操作。首先因為我們定義了一個輸入文件,就應該有一個模塊來讀取這個文件,并構建成內存里的消費記錄,付款記錄等對象,該模塊就叫readinput吧。另外內存里有了消費記錄,付款記錄這些對象,就需要處理它們,計算出每個人的余額,某天的人均消費等,我們把這個模塊叫record_handler,最后我們要有個主模塊調用這兩個模塊,組合成最終的業務邏輯,并顯示給用戶,這個模塊就叫main吧。

readinput.h

 
struct account_record_list read_input();

  該模塊對外只提供一個方法read_input,返回一個消費記錄列表類,其內部實現的私有函數不需要寫在頭文件里,因為沒人用它,這也算起到了封裝的作用,因為具體該函數的實現類是readinput.c,該文件最終會編譯成一個.o文件,別人要想用該模塊的功能的話,只要有readinput.o和readinput.h就行了,一般會把.o放到lib目錄下,.h放到include目錄下。

record_handler.h
 
void edit_person_consumption(struct person_consumption_list *list,
const char *name,double money);
void print_person_consumption_list(const struct person_consumption_list list);
double calc_avg_consumption(double total, int person_count, double discount);

  該模塊定義了對消費記錄的處理,edit_person_consumption用來修改消費記錄,比如某人吃飯消費了多少錢,某人付了多少錢,都調用它來計算出各個人的余額。print_person_consumption_list用來打印出每個人的余額,誰是正的余額,誰是負的余額,calc_avg_consumption用來根據總金額,折扣數和吃飯的人數計算出人均消費數。

  我們在設計模塊時要盡量讓模塊的職責清晰,做到高內聚,盡量少的使用別的模塊的功能,并盡量讓很多的模塊使用自己,還要考慮清楚模塊之間的調用關系。

  Main模塊不需要.h頭,它是一個驅動模塊,用來調用其它兩個模塊,完成整體的功能,不對外提供接口,但要實現一個main的入口函數。

  主函數的實現

  每個可執行程序都要有一個main的方法,我們在main模塊里定義,在使用前,先要用include來聲明你都依賴哪些模塊,只需要包含該模塊的頭文件就可以,尖括號括的是系統的頭文件,會在/usr/include/下查找,引號括住的是自己的頭文件,會在當前目錄下查找。

代碼
 
#include <stdio.h>
#include "data_structure.h"
#include "readinput.h"
#include "record_handler.h"

void print_account_record_list(const struct account_record_list list);
struct person_consumption_list handler_account_record_list(
const struct account_record_list list);
int main()
{

struct account_record_list list ;
list
= read_input();
print_account_record_list(list);

struct person_consumption_list consumption_list =
handler_account_record_list(list);
print_person_consumption_list(consumption_list);

return 0;

}

  接下來我們聲明兩個main函數要用到的兩個私有函數,因為c里要使用函數要先聲明,否則你就只能用你這個函數上面定義的函數,我們在這里先聲明兩個私有函數的原型,print_account_record_list來打印出每條消費記錄的細節,handler_account_record_list用來處理整個記錄列表。在這里看到list參數有個const的修飾,該關鍵字可以保證調用的函數不會修改你的傳入的變量,因為這兩個方法一個用來打印,一個用來當作輸入源計算一些值,從語義上來說就不應該會去修改該參數,所以我們加了const。c里使用并深入理解const關鍵字是老鳥和新手的一個標志,大家可以查查相關資料。

  main主函數一般都返回int,其中函數定義里可以省略掉int,默認就是int,里面的邏輯也很簡單,讀取消費記錄,打印消費記錄,處理消費記錄得到每個人的余額狀況,打印每個人的余額狀況,邏輯非常清晰,下面就是每個子函數的具體實現了。

  下面這個私有函數用來處理消費記錄,遍歷每天的消費和充值記錄,并修改每人的余額記錄,邏輯也很清晰,很好的調用了record_handler模塊提供的功能,使該函數的簡單明了,職責明確。

代碼
 
struct person_consumption_list handler_account_record_list(
const struct account_record_list list)
{

struct person_consumption_list consumption_list;
consumption_list.count
= 0;
int i = 0, j = 0;
for(i = 0; i < list.count; i++)
{

struct account_record record = list.records[i];
double average_consumption =
calc_avg_consumption(
record.total_consumption,
record.people_count,
record.discount);

for(j = 0; j < record.people_count; j++)
{
edit_person_consumption(
&consumption_list,
record.person[j].name,

-average_consumption);
int k =0;
}

for(j = 0; j< record.pay_record_count; j++)
{
edit_person_consumption(
&consumption_list,
record.payrecord[j].person.name,
record.payrecord[j].amount);
}
}

return consumption_list;
}

  讀取記賬文件

  我們會用到IO,字符串以及一些字符串和數值轉換的函數,所以先包含這些頭文件。

 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "data_structure.h"

  C的編譯器比較傻,有的時候你不包含頭文件也能編譯,但運行時會給個錯誤記錄,比如atof是在stdlib.h里定義的,你不包含它也能編譯,但你printf("%f",atof("0.8"));它會給你顯示0.0,你包含了就沒事了,這個太無語了,在c#里你不引用dll就使用人家的方法,編譯肯定出錯,在C里卻什么事都可能發生,所以最好把自己以前學的編程知識先扔到一邊,當個編程初學者來學習C,感覺c比javascript還詭異。

  struct account_record_list read_input()是一個比較大的函數,我們分開來看,先看變量定義部分,在C的函數里,變量定義要放在最前面,我們這里定義了fp一個文件類型指針,其中文件操作用c的標準庫函數fopen,fclose操作,大家看下c手冊就知道用法,這里是用只讀方式打開,如果不存在則拋錯。

代碼
 
FILE *fp;
if((fp=fopen("input.txt","rt")) == NULL)
{
printf(
"cannot open input.txt");
getchar();
exit(
1);
}


int i = 0;
enum read_state {
state_default,
state_date,
state_consumption,
state_discount,
state_person,
state_payrecord
} state;
state
= state_date;
struct account_record_list result;
result.count
= 0;
struct account_record *p_record = result.records;
char temp_buffer[512];
memset(temp_buffer, ’
\0’, 512);
char *p_temp_buffer = temp_buffer;
char ch = fgetc(fp);

  定義了一個read_state的枚舉,在定義枚舉的時候一般第一個成員定義成default,表示一種無效或者默認的狀態,c里的枚舉不能用xxx.yyy來訪問,只能用yyy來訪問,跟常量一樣,所以我們定義成員的時候加上一個state_前綴,這樣在使用的時候就知道是個枚舉了。

  下面還定義了要返回的account_record_list result,因為在棧上聲明的變量沒人給初始化,所以result.count我們要人工設置為0,p_record是指向result.records的指針,它是一個指向數組的指針,這樣可以用p_record++來依次對每個記錄賦值,而不需要像用下標訪問那樣得知道下標值,再一個就是指針可以提高一點性能。

  temp_buffer是定義的一個臨時緩沖區,因為我們解析輸入文件,肯定要對原文件進行一些分隔等,所以要用臨時緩存區保存臨時結果。同理,這里生成的字符數組也沒人給初始化,我們用memset來把每個字節都初始化成'\0'。最后也用一個p_temp_buffer指針來指向臨時緩沖區,指針我們就以p_做前綴,這樣能看出來。

  接下來是對輸入文件的解析,我們要盡量保證函數的短小,所以這里的邏輯只是按分隔符找出每個字段,具體每個字段的解析又調用了各個set_xxx的函數。

代碼
 

while (ch != EOF)
{

if(result.count > MAX_RECORD_COUNT)
{
printf(
"max record count");
break;
}

if(ch != '|' && ch != '\n'){
*(p_temp_buffer++) = ch;
}

else{
*(p_temp_buffer++) = '\0';
switch(state)
{

case state_date:
set_date(p_record,temp_buffer);
state
= state_consumption;
break;
case state_consumption:
set_consumption(p_record,temp_buffer);
state
= state_discount;
break;
case state_discount:
set_discount(p_record,temp_buffer);
state
= state_person;
break;
case state_person:
set_person(p_record,temp_buffer);
state
= state_payrecord;
break;
case state_payrecord:
set_payrecord(p_record,temp_buffer);
state
= state_default;
break;

default:
printf(
"state is error");
break;
}
memset(temp_buffer,
0, 512);
p_temp_buffer
= temp_buffer;

}

if(ch == '\n'){
result.count
++;
p_record
++;
memset(temp_buffer,
0, 512);
p_temp_buffer
= temp_buffer;
state
= state_date;
}
putchar(ch);
ch
= fgetc(fp);
}
fclose(fp);

return result;

  這些邏輯性的東西就沒什么說的了,逐個讀取每個字符,如果遇到分隔符|或者\n就把這段字符放入緩沖區,并傳給set_xxx來處理,注意每次set_xxx后要重置緩沖區的內容,以及讓緩沖區指針指向起始位置。這里讀取完某個字段后要把讀取狀態修改成下一個狀態,這也是簡單的狀態機的應用,在字符串解析方面用的很廣。

  最后記著要fclose文件,否則會資源泄漏,像那些成對出現的api要時刻記著配平資源,比如foepn,fclose,malloc,free這種,少半拉的話,一般就會引起資源泄漏問題。  

  我們在看一個set_xxx方法,對付款記錄的解析是最復雜的,我們就看這個,付款記錄字段格式是先用逗號分隔每個人的付款記錄,再用冒號分隔付款人和付款金額。在c里有個strtok的函數,類似split,可以把一個字符串分隔成多個子串,這里也用到了臨時緩沖區,把傳入的只讀字符串用strncpy拷貝到臨時緩沖區里再做處理,strncpy比strcpy安全,因為后者拷貝時會一直拷貝,直到遇到\0為止,前者可以指定最多拷貝多少個字符。

代碼
 
void set_payrecord(struct account_record *record, const char *buff){
char temp_buffer[512];
memset(temp_buffer,
0, 512);
strncpy(temp_buffer, buff,
512*sizeof(char));

char c[MAX_ARRAY_COUNT][2*MAX_ARRAY_COUNT] = {{'\0'}};
char (*pc)[2*MAX_ARRAY_COUNT] = c;

char *p = strtok(temp_buffer,",");
int paycount= 0;
while(p != NULL)
{
strncpy(
*pc++, p, 2*MAX_ARRAY_COUNT*sizeof(char));
p
= strtok(NULL,",");
paycount
++;
}


struct pay_record *payrecord = record -> payrecord;
int i = 0;
for(i = 0; i < paycount; i++)
{

char *p2 = strtok(c[i],":");
if(p2 == NULL)
{
printf(
"error:parse payrecord error");
return;
}

struct people person;
strncpy(person.name, p2, MAX_ARRAY_COUNT
*sizeof(char));
p2
= strtok(NULL,":");
if(p2 == NULL)
{
printf(
"error:parse payrecord error");
return;
}

double amount = atof(p2);

payrecord
-> person = person;
payrecord
-> amount = amount;
payrecord
++;
record
-> pay_record_count++;
}
}

  這里需要一個兩維數組,聲明兩維數組就用char [3][4] 就行,c99里只是聲明數組時直接初始化,用={{'\0'}}就可以把數組都初始化成'\0',然后雖然這是一個兩位的數組,但要用一維的數組指針去指,如char (*pc)[4],然后用*pc就能訪問二維數組的每一行了,每一行是個字符數組,可以用strncpy等函數操作。注意strtok不能嵌套使用,所以先用它把逗號分隔的子串放入到二維數組里,然后便利二維數組的每一行,對每一行按冒號分隔取出付款人和付款金額,最后放到內存對象里。

  處理記賬記錄

  這個模塊比較小,edit_person_consumption用來處理每一筆消費和付款記錄,先看list里有沒有這個人,如果有這個人就直接把金額修改掉,如果沒有,就在list里添加一個人機器消費記錄。這里有個問題折騰了半天,就是我把strcmp寫成strcpy了,編譯也沒問題,但輸出結果讓人很詭異,賦值都亂了,看來這種編譯不出錯,運行時給個錯誤值的問題是最難排查的,拼寫錯誤真是程序員最常見的錯誤呀。剩下兩個函數比較簡單,打印沒人余額記錄和計算人均消費。

代碼
 
#include "data_structure.h"

void edit_person_consumption(struct person_consumption_list *list,
const char *name,double money)
{

int i = 0;
int found = -1;
for(i = 0; i < list -> count; i++)
{

if(strcmp(list -> persons[i].name, name) == 0)
{
found
= i;
list
-> persons[i].consumption += money;
}
}


if(found == -1)
{

int count = list -> count;
strncpy(list
-> persons[count].name, name, MAX_ARRAY_COUNT);
list
-> persons[count].consumption = money;
list
-> count++;
}
}

void print_person_consumption_list(const struct person_consumption_list list)
{

int i;
printf(
"\n-----consumption details-------\n");
for(i = 0; i < list.count; i++)
{
printf(
"%s=%0.2f\n",list.persons[i].name,list.persons[i].consumption);
}
}

double calc_avg_consumption(double total,int person_count,double discount)
{

return total * discount / person_count;
}

  編譯及測試

  上篇帖子簡單介紹過makefile的編寫,以下是該程序的makefile文件,注意換行符和跳格鍵的使用。

代碼
 
book:readinput.o record_handler.o \
data_structure.h readinput.h record_handler.h\
main.c
gcc main.c
-o book readinput.o record_handler.o
readinput.o: data_structure.h readinput.h readinput.c
gcc
-c readinput.c
record_handler: data_structure.h record_handler.h record_handler.c
gcc
-c record_handler.c

  最后輸出一個book的可執行文件,執行./book,輸出以下結果,符合預期

  可以看到d負的最多,因為它吃了兩頓都沒付錢,下次吃飯就該他出錢了,而b正的最多,可以連續一周不用付款吃飯了。

代碼
 
2010-9-10|83|0.8|a,b,c,d|a:100,b:100
2010-9-11|102|0.8|a,b,c,d,e|b:100,c:50
2010-9-10
discount=0.80
consumption=83
person:
a,b,c,d,
pay_record
a:
100.00
b:100.00

2010-9-11
discount=0.80
consumption=102
person:
a,b,c,d,e,
pay_record
b:
100.00
c:50.00


-----consumption details-------
a=67.08
b=167.08
c=17.08
d=-32.92
e=-16.32

  小節

  其實最終的每人余額可以從小到大排個序,可以練習一下冒泡排序和函數指針的使用,不過這也算是一個比較有意義的下程序了,多寫代碼,C的入門也就快了。下次可能給大家分享下如何配置VIM能更快的編寫C程序,工具的熟練程度會大大影響開發效率。

  語言,工具等在編程里都是次要矛盾,編程的主要要解決的問題是業務邏輯本身的復雜性,所以要經常寫一些邏輯比較復雜的小程序來提高編程能力,可以迅速提高思維能力,減少出錯的能力,在寫代碼的過程中所犯的錯誤都積累起來,以后就可以一次編寫,直接執行就通過了,編譯和運行都沒有錯誤,推薦下我前段時間寫的練習作品:大家來找錯-自己寫個正則引擎

0
0
 
標簽:C語言 入門
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()