1. 程式人生 > 實用技巧 >記一次奇妙的 spring-boot + spark debug 經歷

記一次奇妙的 spring-boot + spark debug 經歷

什麼是檔案

  磁碟上的檔案就是檔案,分為普通檔案和目錄檔案(資料夾);

  在程式設計中,有兩種檔案:

    程式檔案:包括源程式檔案(字尾為.c),目標檔案(windows環境字尾為.obj),可執行程式(windows環境字尾 為.exe);

    資料檔案:檔案的內容不一定是程式,而是程式執行時讀寫的資料,比如程式執行需要從中讀取資料的檔案,或者輸出 內容的檔案;

  在以前學習的操作中,對資料的操作都是以終端為物件來操作的,從終端的鍵盤輸入資料,執行結果顯示到顯示器上;不過這樣的操作難以將資料儲存下來,例如我前面部落格中寫的一個小專案——通訊錄,你在執行這個程式的時候,當你把聯絡人的資訊輸入之後,你可以檢視相應的資料,但是當你將程式關閉之後再開啟,你之前錄入的聯絡人資訊就全部消失不見了;

  現在我們可以把資訊輸出到磁碟上,當需要的時候再從磁碟上把資料讀取到記憶體中使用,這裡處理的就是磁碟上面的檔案。通過這樣的方式我們就可以將資料有效的儲存較長的時間。

檔名

  一個檔案要有一個唯一的標識——檔名,檔名由三部分構成:檔案路徑+檔名主幹+檔案字尾;  

    檔案路徑是為了能找到檔案在計算機中的位置,後續程式碼想要針對檔案進行操作,就必須通過路徑來找需要的檔案。檔案路徑分為絕對路徑和相對路徑;

      絕對路徑:就是從開啟資料夾開始,到找到你需要找的檔案為止,這個路徑就是絕對路徑,例如:E:\git\c-\TheAddressBook\Debug\TheAddressBook.exe,這就是我的通訊錄可執行程式的絕對路徑;

      相對路徑:就是選定一個參考系,告訴系統從這個目錄出發,然後查詢檔案,例如:選定E:\git\c-\TheAddressBook\Debug作為參考系,此時通訊錄可執行程式就是.\TheAddressBook.exe;

      在上面兩種查詢檔案的方法中,我們可以看到在路徑中出現"\"這個符號,其實在Windows下,路徑中不止"\"這可以作為分割符,還可以使用"/"作為分割符,在Linux/Mac下,只能使用"/"作為分割符;那麼在實際開發中,更推薦使用"/"這個,因為在程式中,"\"這個符號也有轉義字元的意思,所以會搞混淆;

    檔名就是為了在找到的路徑中確定唯一的檔案;

    檔案字尾是為了區分檔案的種類,告訴作業系統使用哪個程式來預設開啟這個檔案;

檔案型別

  根據資料的組織形式,可以把檔案分成兩類:文字檔案和二進位制檔案;

    資料在記憶體中以二進位制的形式儲存,如果不加轉換的輸出到外存,就是二進位制檔案

    如果要求在外存上以ASCII碼的形式儲存,則需要在儲存前轉換。以ASCII字元的形式儲存的檔案就是文字檔案

  其實有一種非常簡單的方式就能分辨出到底是文字檔案還是二進位制檔案,先開啟系統軟體——記事本,然後將你需要檢視的檔案移入記事本,展示出來的結果你要是能看得懂,那就是文字檔案,否則就是二進位制檔案。

檔案緩衝區

  ANSIC 標準採用“緩衝檔案系統”處理資料檔案的,所謂緩衝檔案系統是指系統自動地在記憶體中為程式的每一個正在使用的檔案開闢一塊“檔案緩衝區”。從記憶體向磁碟輸出資料會先送到記憶體中的緩衝區,裝滿緩衝區後才一起送到磁碟上。如果從磁碟向計算機讀入資料,則從磁碟檔案中讀取資料輸入到記憶體緩衝區(充滿緩衝區),然後再從緩衝區逐個地將資料送到程式資料區(程式變數等)。緩衝區的大小根據C編譯系統決定。

  使用檔案緩衝區的目的就是為了提高讀寫檔案的效率,因為訪問記憶體的效率遠遠高於訪問外存,所以在需要操作外存時,就先搬運一定量的資料到緩衝區中(將緩衝區填滿了後),然後再一起送到外存中。

檔案指標

  每個被使用的檔案都會在記憶體中開闢一塊相應的檔案資訊區,用來存放檔案的相關資訊,這些檔案是儲存在一個結構體中,結構體的名字叫做FILE。至於結構體內部是怎樣排列的,我們不必關心,我們只需要定義一個FILE*型別的指標變數來指向某個檔案的檔案資訊區

FILE* pf;//檔案指標變數

  我們將這個指標稱為檔案指標,又稱為控制代碼,意思就是遙控器,我們後續對檔案進行任何操作都是通過這個遙控器。

開啟檔案

  既然知道了遙控器的概念,也知道了遙控器怎麼製造出來,但是如何將遙控器和這個檔案關聯起來呢?也就是說如何讓這個檔案指標指向你想要的檔案呢?

  我麼可以通過fopen()函式來開啟一個檔案

FILE * fopen ( const char * filename, const char * mode );

  可以看到,fopen()函式的返回值就是FILE*型別的,所以通過這個函式我麼就可以將檔案指標指向你所需的檔案。先來看看他的簡單使用:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include<string.h> int main() { FILE* fp = fopen("E:/test.txt", "r"); if(fp == NULL)//如果檔案 開啟失敗 {
printf("檔案開啟失敗!%s\n",strerror(errno)); perror(
"檔案開啟失敗!");//輸出錯誤提示 }
//對檔案進行操作...
fclose(fp);
return 0; }

  通過上面的例項,來總結一下使用的注意點:

    ①函式的第一個引數就是檔案路徑,使用時應要用雙引號引起來;

    ②函式的第二個引數是檔案的開啟方式,使用時帶上引號;關於開啟方式有很多種,有的開啟方式只支援讀取不支援寫入,有的開啟方式在沒有目標檔案的時候會自行建立或是刪除原有檔案中的內容。具體可以參考下面我畫出來的表格:

    ③檔案開啟的時候不總是成功的,也有失敗的時候,如果開啟檔案失敗,那麼返回NULL,所以在執行開啟檔案操作之後,一定要對返回值進行檢驗,否則將會對一個NULL進行操作,出現未定義行為。

    ④假如檔案開啟失敗,想要檢視失敗的原因,那麼可以使用下面兩種方式輸出錯誤資訊:

//包含<string.h><errno.h>標頭檔案
//使用strerror()函式打印出errno錯誤碼對應的錯誤資訊
 printf("檔案開啟失敗!%s\n",strerror(errno));
//不用包含標頭檔案,這一個函式就將文字打印出來
//並將錯誤碼對應的錯誤資訊打印出來,相當於是上面兩個的合體
 perror("檔案開啟失敗!");

關閉檔案

  我們把檔案開啟肯定要自己將檔案關閉,如果只開不關閉,那麼檔案的檔案資訊區就會不斷地佔據記憶體的空間,然後就會出現和記憶體洩漏一樣的問題,我們稱之為檔案資源洩露。那麼該怎麼關閉檔案呢?

  我麼使用fclose()函式來將開啟的檔案關閉

int fclose ( FILE * stream );

  在上一個程式碼中,我們看到在檔案進行操作完畢之後,我就使用了fclose()函式將檔案關閉了。

檔案操作

  在檔案開啟成功之後,我們需要對其進行操作:讀操作、寫操作,下面我們來學習一些進行這些操作的函式。

  fread()

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

    這個函式的功能用於把磁碟上的檔案讀到記憶體中;其中ptr-- 這是指向帶有最小尺寸size*count位元組的記憶體塊的指標。size-- 這是要讀取的每個元素的大小,以位元組為單位。count-- 這是元素的個數,每個元素的大小為 size 位元組。stream-- 這是指向 FILE 物件的指標,也就是檔案指標。

    成功讀取的元素總數會以 size_t 物件返回,size_t 物件是一個整型資料型別。如果總數小於 count 引數,則表示檔案讀取完畢。我們可以進行if語句判斷,看看是否完全讀出資料。    

    當我們將檔案內容讀取到記憶體之後,就可以使用printf()將內容列印到終端上;下面舉個例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“這是一個測試程式碼!”
    char ch[1024] = { 0 };
    size_t ret = fread(ch, 2, 1024, p);
if (ret!=9){
//如果返回值小於count值就表示讀取完畢
printf(
"%s\n%d", ch, ret);
}
//檔案不用了一定要記得關閉檔案 fclose(p); return 0; } //結果:這是一個測試程式碼! // 9

  fwrite()

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)

    這個函式的功能是把記憶體的資料寫入到磁碟中;其中ptr-- 這是指向要被寫入的元素陣列的指標。size-- 這是要被寫入的每個元素的大小,以位元組為單位。count-- 預期要讀的元素的個數,每個元素的大小為 size 位元組。stream-- 這是指向 FILE 物件的指標,也就是檔案指標。

    如果成功,該函式返回一個 size_t 物件,表示元素的總數,該物件是一個整型資料型別。如果該數字與 count 引數不同,則會顯示一個錯誤。可以使用if語句進行判斷;下面舉個例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“這是一個測試程式碼!”
    char* ch = "這是一個測試程式碼two!";
    size_t ret = fwrite(ch, 1, strlen(ch), p);
    if (ret != strlen(ch)) {
        perror("檔案寫入失敗!");
    }
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}
//最終文字檔案中打印出:這是一個測試程式碼two!

  上面這兩個函式的使用比較麻煩,下面再介紹幾個比較方便的函式:

  fprintf()

int fprintf(FILE *stream, const char *format, ...)

    這個函式的作用是格式化寫入,其實和printf()是等價的,只不過比printf()多了第一個引數-檔案指標,然後剩下的格式和我們平時向終端上寫入資料是一樣的;

    如果成功,該函式返回成功匹配和賦值的個數。如果到達檔案末尾或發生讀錯誤,則返回 EOF。這個和printf()一樣,但是在我以前寫的程式碼中,這個返回值並沒有使用到,所以瞭解一下就好;下面舉個例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“這是一個測試程式碼two!”
    char* ch = "three";
    fprintf(p, "這是一個測試程式碼%s!", ch);
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}

//結果:這是一個測試程式碼three!

  fscanf()

int fscanf(FILE *stream, const char *format, ...)

    這個函式的作用是將檔案內容讀出,並格式化輸入到變數中,如果成功,該函式返回成功匹配和賦值的個數。如果到達檔案末尾或發生讀錯誤,則返回 EOF。(如果讀取過程中,碰到空格就會輸入到下一個變數中或者結束,這取決於是否含有其他變數來接收資料,解釋的不太清楚,多擔待)下面舉個例子看看:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“這是一個 測試程式碼 three!”
        //注意,這串字元中間是有空格的,
    char ch1[1024] = { 0 };
    char ch2[1024] = { 0 };
    char ch3[1024] = { 0 };
    fscanf(p, "%s %s %s", ch1, ch2, ch3);
    printf("%s%s%s", ch1, ch2, ch3);
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}
//結果輸出:這是一個測試程式碼three!

  feof()

int feof(FILE *stream)

    C 庫函式int feof(FILE *stream)測試給定流 stream 的檔案結束識別符號。stream為一個檔案指標。當設定了與流關聯的檔案結束識別符號時,該函式返回一個非零值,否則返回零。

    在檔案讀取過程中,不能用feof函式的返回值直接用來判斷檔案的是否結束。而是應用於當檔案讀取結束的時候,判斷是讀取失敗結束,還是遇到檔案尾結束。

      1. 文字檔案讀取是否結束,判斷返回值是否為EOF (fgetc),或者NULL(fgets) 例如: fgetc判斷是否為EOF. fgets判斷返回值是否為NULL。

      2. 二進位制檔案的讀取結束判斷,判斷返回值是否小於實際要讀的個數。 例如: fread判斷返回值是否小於實際要讀的個數。

    本函式與下面的幾個函式要聯合使用,舉例請參考線面的例子。

  fgetc()

int fgetc(FILE *stream)

    C 庫函式int fgetc(FILE *stream)從指定的流 stream 獲取下一個字元(一個無符號字元),並把位置識別符號往前移動。stream-- 這是指向 FILE 物件的指標,該 FILE 物件標識了要在上面執行操作的流。

    該函式以無符號 char 強制轉換為 int 的形式返回讀取的字元,如果到達檔案末尾或發生讀錯誤,則返回 EOF。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“i love codeing!”
    char ch = 0;
    while (1) {
        if (feof(p)) {
            break;
        }
        ch = fgetc(p);
        printf("%c", ch);
    }
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}

//結果輸出:i love codeing!

  fputc()

int fputc(int char, FILE *stream)

    C 庫函式int fputc(int char, FILE *stream)把引數char指定的字元(一個無符號字元)寫入到指定的流 stream 中,並把位置識別符號往前移動。

    char-- 這是要被寫入的字元。該字元以其對應的 int 值進行傳遞。stream-- 這是指向 FILE 物件的指標,也就是一個檔案指標。如果沒有發生錯誤,則返回被寫入的字元。如果發生錯誤,則返回 EOF,並設定錯誤識別符號。

    下面舉個例子,是將ascll碼值為33到值為100的字元寫入到檔案中:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“i love codeing!”
    for (int i = 33; i <= 100; i++) {
        fputc(i, p);
    }
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}

//結果:!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcd

  fgets()

char *fgets(char *str, int n, FILE *stream)

    C 庫函式char *fgets(char *str, int n, FILE *stream)從指定的流 stream 讀取一行,並把它儲存在str所指向的字串內。當讀取(n-1)個字元時,或者讀取到換行符時,或者到達檔案末尾時,它會停止,具體視情況而定。

    str-- 這是指向一個字元陣列的指標,該陣列儲存了要讀取的字串。n-- 這是要讀取的最大字元數(包括最後的空字元)。通常是使用以 str 傳遞的陣列長度。stream-- 這是指向 FILE 物件的指標,該 FILE 物件標識了要從中讀取字元的流。

    如果成功,該函式返回相同的 str 引數。如果到達檔案末尾或者沒有讀取到任何字元,str 的內容保持不變,並返回一個空指標。如果發生錯誤,返回一個空指標。下面舉個例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“i love codeing!”
    char ch[1024] = { 0 };
    if (fgets(ch, 1024, p) != NULL) {
        printf("%s", ch);
    }
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}

//輸出結果:i love codeing!

  fputs()

int fputs(const char *str, FILE *stream)

    C 庫函式int fputs(const char *str, FILE *stream)把字串寫入到指定的流 stream 中,但不包括空字元。str-- 這是一個數組,包含了要寫入的以空字元終止的字元序列。stream-- 這是指向 FILE 物件的指標,該 FILE 物件標識了要被寫入字串的流。該函式返回一個非負值,如果發生錯誤則返回 EOF。下面舉個例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //檔案開啟時要進行判空檢驗
    if (p == NULL) {
        perror("檔案開啟失敗!");
        return 1;
    }
    //檔案的內容為“i love codeing!”
    fputs("這是一個測試程式碼four!", p);
    //檔案不用了一定要記得關閉檔案
    fclose(p);
    return 0;
}

//結果:這是一個測試程式碼four!

  下面再介紹兩個與檔案操作無關的函式,但是十分有用:

  sprintf()

int sprintf(char *str, const char *format, ...)

    C 庫函式int sprintf(char *str, const char *format, ...)傳送格式化輸出到str所指向的字串。有一個很重要的用法就是將整數轉換成字串格式輸入到str中;

  sscanf()

int sscanf(const char *str, const char *format, ...)

    C 庫函式int sscanf(const char *str, const char *format, ...)從字串讀取格式化輸入。有一個很重要的作用就是將字串轉換為整數;

  上面兩個函式的舉例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    char ch[1024] = { 0 };
    int num = 2048;
    sprintf(ch, "num的值為 %d", num);
    printf("%s\n", ch);
    //輸出結果:num的值為 2048

    int day, year;
    char weekday[20], month[20], dtm[100];
    strcpy(dtm, "Saturday March 25 1989");
    sscanf(dtm, "%s %s %d  %d", weekday, month, &day, &year);
    printf("%s %d, %d = %s\n", month, day, year, weekday);
    //輸出結果:March 25, 1989 = Saturday
    return 0;
}

隨機讀寫

  fseek()

int fseek ( FILE * stream, long int offset, int origin );

    根據檔案指標的位置和偏移量來定位檔案指標;

#include <stdio.h>
int main ()
{
  FILE * pFile;
  pFile = fopen ( "D:/test.txt" , "wb" );
  fputs ( "This is an apple." , pFile );
  fseek ( pFile , 9 , SEEK_SET );
  fputs ( " sam" , pFile );
  fclose ( pFile );
  return 0;
}

  ftell()

long int ftell ( FILE * stream );

    返回檔案指著相對於起始位置的偏移量;

#include <stdio.h>
int main ()
{
  FILE * pFile;
  long size;
  pFile = fopen ("D:/test.txt","rb");
  if (pFile==NULL) perror ("Error opening file");
  else
 {
    fseek (pFile, 0, SEEK_END);   // non-portable
    size=ftell (pFile);
    fclose (pFile);
    printf ("Size of myfile.txt: %ld bytes.\n",size);
 }
  return 0;
}

  rewind()

void rewind ( FILE * stream );

    讓檔案指標的位置回到檔案的起始位置;

#include <stdio.h>
int main ()
{
  int n;
  FILE * pFile;
  char buffer [27];
  pFile = fopen ("D:/test.txt","w+");
  for ( n='A' ; n<='Z' ; n++)
  fputc ( n, pFile);
  rewind (pFile);
  fread (buffer,1,26,pFile);
  fclose (pFile);
  buffer[26]='\0';
  puts (buffer);
  return 0;
}