公佈欄

2016年10月2日 星期日

程式筆記 (一) --- 指標 + 常數、strtok、fgets + feof

回憶許久沒有打 code 了,不過開學時就知道是逃不掉的宿命了(哭)
果然開學一個禮拜後就有可愛的程式作業,也和自己預期的一樣,遇到了許多問題,在此將學習到的經驗貼出來和大家分享。

常數指標 ?  指標常數 ?


我們先來看看下列的程式碼宣告:
  1. const int a = 1;
  2.           a = 2;       // error

很明顯的 B 行會發生錯誤,因為整數 a 的值不能改變 (常數)。
那如果 a 的型態是指標呢?   那就有點意思了 我們來看看以下三種宣告:
  1. const int* const a;   // const pointer to constant
  2. const int* a;        // pointer to constant
  3. int const* a;        // pointer to constant
  4. int* const a;        // constant to integer

是不是看的頭暈了?   回憶的一位朋友提供了一個簡單的辦法:
看 const 位於什麼型態的後面,唯一的例外是 B 行

我們以比較特殊的 B 行做舉例:
由於 const 沒有在類型的後面,自動將他往後丟一個(等價於 C 行)
可以看到 const 位於 int 後面,因此該指標指向的是常數 (不可變)
不是指標不可變

說實話這個問題用中文記憶很容易搞混,建議使用英文理解 (如同 code 區的註解)

strtok


strtok 函數在字串處理相關的程式很常見
但相信很多人和回憶一樣覺得這函數的用法很難記
每次要打的時候都要上網再查詢一次用法
這次回憶想到一個比較好記憶的方式,就從理解他的參數下手
我們先來看看函數宣告
   char * strtok ( char * str, const char * delimiters );
                           //from http://www.cplusplus.com/reference/cstring/strtok/
 

以下列出幾個要點
  1. 此函數尋找目標為 delimiters 中任意一字元
  2. 忽略起始符合字元(不顯示),直至發現不符合字元後,將第一個發現的符合字元改成 '\0' ,請看下方分析
  3. 第一個參數 str 可以指向一個字串或是空指標,若為空指標,則延續上一次找到的地方往下尋找
  4. delimiters 參數可改變

在這裡我們用以下程式碼來做測試
  1. #include <stdio.h>
  2. #include <string.h>

  3. int main(void){

  4.     int i,j = 0;
  5.   //char* test = "testascaddava";     // error
  6.     char test[] = "testascaddava";    // no special meaning, only for demonstration
  7.     char* pch;
  8.     char* delimiters ="abcdefg";      // used for altering delimiters
  9.     pch = strtok (test, "t");
  10.     while(pch != NULL){
  11.         printf ("%s\n",pch);
  12.         pch = strtok (NULL, delimiters+j);
  13.         j ++;
  14.         }

  15.     printf("\n");
  16.     for(i = 0; i < 13; i++)
  17.         printf("%c",test[i]);         // print the whole string

  18.     printf("\n");
  19.     for(i = 0; i < 13; i++)
  20.         printf("%d ",test[i]);        // print the whole string in number format

  21.     return 0;
  22. }

執行結果: es
s
a
ava

tes as a dava
116 101 115 0 97 115 0 97 0 100 97 118 97
Process returned 0 (0x0)   execution time : 0.016 s
Press any key to continue.

我們來做個簡單的分析:                                                  
字串狀態 (test)比對字串 (delimiters)附註
第一次testascaddavattest[3]='\0'
第二次tes ascaddavaabcdefgtest[6]='\0'
第三次tes as addavabcdefgtest[8]='\0'
第四次tes as a davacdefg讀到'\0'

我們可以看到第一次操作過程中,第一個符合字元 't' 並未被修改成'\0' ,只是不顯示在輸出中

  請問G行會報錯的原因是因為它等同於下列哪一個宣告?
  1. const char
  2. const char*
  3. char* const
  4. char const*                                                            ANS: B or D

到底甚麼時候 eof ?


一般遇到讀檔問題時可能會使用 int feof(FILE*) 來判斷結束
可是此函數在 C / C++ 使用時,必須是讀到 EOF 才會等於 1
我們來看看下列的程式碼
  1. #include <stdio.h>
  2. #include <string.h>

  3. int main(void){
  4.     char mem[1000] = {'\0'};
  5.     FILE* file = fopen("test.txt", "r");

  6.     if(!file){
  7.         puts("cannot open file!");
  8.         return 0;
  9.         }

  10.     while(!feof(file)){
  11.         memset(mem, 0, sizeof(mem));
  12.         fgets(mem, sizeof(mem), file);
  13.         printf("%s", mem);
  14.         }
  15.     return 0;
  16. }

執行結果:
This is a test string.
Process returned 0 (0x0)   execution time : 0.085 s
Press any key to continue.



看起來似乎沒有什麼問題?
有些人會提出 feof 不應該放在 while 迴圈裡
但是目前在回憶的測試中是正常的
所以問題出在哪?
我們來看看以下4個檔案:


黑底的是在 linux (freeBSD 10.3 - release) 底下使用 vim 編輯的(test1.txt, test2.txt)
白底的是在 windows 7 底下使用記事本編輯的(test3.txt, test4.txt)
第一直行沒有空行,而第二直行多了一行空行
執行結果:


貌似 linux 建立的檔案會多讀一行?
我們先來看看 fgets 的函式說明:

   char * fgets ( char * str, int num, FILE * stream );
                           //from http://www.cplusplus.com/reference/cstdio/fgets/
 

重點整理:
  1. 遇到下列任意情況時停止讀取:
    1. 讀到EOF
    2. 讀到'\n'
    3. 讀滿 (num - 1) 個字元 (剩下的一個位置須補上空字元 '\0')
  2. '\n' 也會讀進 str
  3. 讀到 EOF 後, feof(file) = 1
  4. 若僅讀到 EOF(無其他字元),回傳空指標,str 內容不變
    若讀取錯誤亦回傳空指標, ferror(file) = 1 ,str 內容可能改變
    若無以上兩種狀況,則回傳 str 指標

再來看看4個檔案用16進位表示時的狀況:


可以看到除了換行字元的差異外,vim 建立的檔案結尾時會自動補上 0a ,查閱ASCII表得知其為換行鍵,就是 '\n'
如果需要濾掉空行,可以這樣寫
  1. #include <stdio.h>
  2. #include <string.h>

  3. char* getstring(FILE* file, char* mem){

  4.     int i;
  5.     memset(mem, 0, 1000);
  6.     while(mem[0] == '\0'){          // to test whether the line contains only '\n'
  7.         if(!fgets(mem, 1000, file))
  8.             return 0;
  9.         else
  10.             for(i = 0; i < 999; i++)
  11.                 if(mem[i] == '\n'){
  12.                     mem[i] = '\0';  // remove new line character
  13.                     break;
  14.                     }
  15.         }
  16.     return mem;
  17. }

  18. int main(void){
  19.     char mem[1000] = {'\0'};
  20.     FILE* file = fopen("test.txt", "r");

  21.     if(!file){
  22.         puts("cannot open file!");
  23.         return 0;
  24.         }

  25.     if(getstring(file,mem))          // first line
  26.         printf("%s",mem);

  27.     while(getstring(file,mem)){
  28.         printf("\n");
  29.         printf("%s",mem);
  30.         }

  31.     return 0;
  32. }

如果只是要避免讀到空指標(在做字串處理時可能會出錯 Eg. strcmp)
將 fgets 放進 while 迴圈即可
程式碼如下
  1. #include <stdio.h>
  2. #include <string.h>

  3. int main(void){
  4.     char mem[1000] = {'\0'};
  5.     FILE* file = fopen("test.txt", "r");

  6.     if(!file){
  7.         puts("cannot open file!");
  8.         return 0;
  9.         }

  10.     while(fgets(mem, sizeof(mem), file)){
  11.         printf("%s", mem);
  12.         memset(mem, 0, sizeof(mem));
  13.         }
  14.     return 0;
  15. }

沒有留言:

張貼留言