目錄
一、stdin、stdout 和 stderr 詳解
二、文件打開方式
三、C語言文件操作函數詳解
1、文件操作概述
2、文件操作函數分類表
1. 文件打開與關閉
2. 字符讀寫函數
3. 字符串讀寫函數
4. 格式化讀寫函數
5. 二進制讀寫函數
6. 文件定位函數
7. 文件狀態與錯誤檢測
8. 其他文件操作函數
3、注意事項
4、對文件進行寫入操作示例
5、對文件進行讀取操作示例
6、另一個讀文件的例子
代碼功能
代碼結構分析
1. 代碼功能
2. 代碼解析
(1) 參數檢查
(2) 打開文件
(3) 讀取文件內容
(4) 關閉文件
3. 關鍵點分析
(1)?fread?的返回值
(2)?buf[s] = '\0'?的作用
(3) 循環終止條件
四、輸出信息到顯示器的常用方法
1、代碼功能
2、代碼解析
(1)?fwrite?方式
(2)?printf?方式
(3)?fprintf?方式
3、輸出結果
4、關鍵區別
5、使用場景建議
6. 擴展:puts?和?fputs
7. 總結
五、?什么是當前路徑?
命令解析:ps -axj | head -1 && ps -axj | grep demol | grep -v grep
組合效果:
一、stdin、stdout 和 stderr 詳解
????????在Linux系統中,"一切皆文件"的理念意味著所有設備都可以被視為文件進行操作。顯示器作為輸出設備被抽象為文件,通過寫入數據實現內容顯示;鍵盤作為輸入設備同樣被抽象為文件,系統通過讀取該文件獲取用戶輸入的字符。
????????這些特殊設備文件之所以不需要手動打開,是因為每個進程在創建時都會自動打開三個標準I/O流:
- 標準輸入(stdin) - 對應鍵盤設備
- 標準輸出(stdout) - 對應顯示器設備
- 標準錯誤(stderr) - 同樣對應顯示器設備
????????根據man手冊的說明,這三個標準流在C語言中都是以FILE*指針形式存在的,與 fopen 返回的文件指針類型相同。這種設計使得進程可以直接使用這些預打開的文件描述符進行基本輸入輸出操作,無需額外的打開步驟。
????????當運行C程序時,操作系統會自動使用C語言接口打開這三個標準輸入輸出流,這樣我們才能調用scanf和printf等函數進行鍵盤輸入和屏幕輸出。
????????實際上,stdin、stdout和stderr與通過fopen獲取的文件指針本質相同。例如使用fputs函數時,若將第二個參數設為stdout,函數就會直接將數據輸出到顯示器上。
#include <stdio.h>
int main()
{fputs("hello stdin\n", stdout);fputs("hello stdout\n", stdout);fputs("hello stderr\n", stdout);return 0;
}
此時我們相當于使用fputs函數向“顯示器文件”寫入數據,也就是顯示到顯示器上:?
????????需要注意的是,標準輸入流、標準輸出流和標準錯誤流并非C語言獨有的概念。在C++中對應的是cin、cout和cerr,幾乎所有編程語言都具備類似的設計。實際上,這些特性并非源于特定編程語言,而是由操作系統底層提供的基礎支持。
二、文件打開方式
文件操作模式說明:
- r:以只讀方式打開文本文件。文件指針位于文件開頭。
- r+:以讀寫方式打開文件。文件指針位于文件開頭。
- w:以寫入方式打開文件。若文件存在則清空內容,不存在則創建新文件。文件指針位于文件開頭。
- w+:以讀寫方式打開文件。若文件不存在則創建,存在則清空內容。文件指針位于文件開頭。
- a:以追加方式打開文件(寫入內容到文件末尾)。若文件不存在則創建。文件指針位于文件末尾。
- a+:以讀寫和追加方式打開文件。若文件不存在則創建。讀取時文件指針位于開頭,寫入時始終追加到末尾。
我將會在另一篇博客中詳細驗證上面的每一種文件打開模式,挺多內容的其實!!!
三、C語言文件操作函數詳解
1、文件操作概述
????????在C語言中,文件操作主要通過標準庫中的文件I/O函數實現。這些函數定義在<stdio.h>
頭文件中。文件操作通常包括打開、讀寫、定位和關閉等基本操作。
2、文件操作函數分類表
1. 文件打開與關閉
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
FILE *fopen(const char *filename, const char *mode); | 打開文件 | filename: 文件名 mode: 打開模式 | 成功: 返回FILE指針 失敗: 返回NULL | FILE *fp = fopen("test.txt", "r"); |
int fclose(FILE *stream); | 關閉文件 | stream: 文件指針 | 成功: 返回0 失敗: 返回EOF | fclose(fp); |
文件打開模式:(同第六點)
-
"r": 只讀
-
"w": 只寫(創建新文件或清空已有文件)
-
"a": 追加
-
"r+": 讀寫(文件必須存在)
-
"w+": 讀寫(創建新文件或清空已有文件)
-
"a+": 讀寫(追加)
2. 字符讀寫函數
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
int fgetc(FILE *stream); | 從文件讀取一個字符 | stream: 文件指針 | 成功: 返回讀取的字符 失敗/EOF: 返回EOF | char c = fgetc(fp); |
int fputc(int c, FILE *stream); | 向文件寫入一個字符 | c: 要寫入的字符 stream: 文件指針 | 成功: 返回寫入的字符 失敗: 返回EOF | fputc('A', fp); |
int getc(FILE *stream); | 同fgetc,但可能實現為宏 | 同fgetc | 同fgetc | char c = getc(fp); |
int putc(int c, FILE *stream); | 同fputc,但可能實現為宏 | 同fputc | 同fputc | putc('B', fp); |
3. 字符串讀寫函數
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
char *fgets(char *s, int size, FILE *stream); | 從文件讀取一行 | s: 存儲緩沖區 size: 緩沖區大小 stream: 文件指針 | 成功: 返回s 失敗/EOF: 返回NULL | fgets(buf, 100, fp); |
int fputs(const char *s, FILE *stream); | 向文件寫入字符串 | s: 要寫入的字符串 stream: 文件指針 | 成功: 返回非負值 失敗: 返回EOF | fputs("Hello", fp); |
4. 格式化讀寫函數
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
int fprintf(FILE *stream, const char *format, ...); | 格式化輸出到文件 | stream: 文件指針 format: 格式字符串 ...: 可變參數 | 成功: 返回寫入字符數 失敗: 返回負值 | fprintf(fp, "%d %f", i, f); |
int fscanf(FILE *stream, const char *format, ...); | 從文件格式化輸入 | stream: 文件指針 format: 格式字符串 ...: 可變參數 | 成功: 返回匹配項數 失敗/EOF: 返回EOF | fscanf(fp, "%d %f", &i, &f); |
5. 二進制讀寫函數
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 從文件讀取二進制數據 | ptr: 數據存儲位置 size: 每個元素大小 nmemb: 元素數量 stream: 文件指針 | 返回成功讀取的元素數 | fread(&data, sizeof(data), 1, fp); |
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 向文件寫入二進制數據 | ptr: 要寫入的數據 size: 每個元素大小 nmemb: 元素數量 stream: 文件指針 | 返回成功寫入的元素數 | fwrite(&data, sizeof(data), 1, fp); |
6. 文件定位函數
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
int fseek(FILE *stream, long offset, int whence); | 移動文件位置指針 | stream: 文件指針 offset: 偏移量 whence: 起始位置(SEEK_SET/SEEK_CUR/SEEK_END) | 成功: 返回0 失敗: 返回非0 | fseek(fp, 0, SEEK_END); |
long ftell(FILE *stream); | 獲取文件當前位置 | stream: 文件指針 | 成功: 返回當前位置 失敗: 返回-1L | long pos = ftell(fp); |
void rewind(FILE *stream); | 重置文件位置到開頭 | stream: 文件指針 | 無 | rewind(fp); |
int fgetpos(FILE *stream, fpos_t *pos); | 獲取文件位置 | stream: 文件指針 pos: 存儲位置 | 成功: 返回0 失敗: 返回非0 | fgetpos(fp, &pos); |
int fsetpos(FILE *stream, const fpos_t *pos); | 設置文件位置 | stream: 文件指針 pos: 要設置的位置 | 成功: 返回0 失敗: 返回非0 | fsetpos(fp, &pos); |
7. 文件狀態與錯誤檢測
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
int feof(FILE *stream); | 檢測文件結束標志 | stream: 文件指針 | 到達文件尾: 返回非0 否則: 返回0 | while(!feof(fp)) {...} |
int ferror(FILE *stream); | 檢測文件錯誤 | stream: 文件指針 | 發生錯誤: 返回非0 否則: 返回0 | if(ferror(fp)) {...} |
void clearerr(FILE *stream); | 清除錯誤標志 | stream: 文件指針 | 無 | clearerr(fp); |
8. 其他文件操作函數
函數原型 | 功能描述 | 參數說明 | 返回值 | 示例 |
---|---|---|---|---|
int remove(const char *filename); | 刪除文件 | filename: 文件名 | 成功: 返回0 失敗: 返回-1 | remove("temp.txt"); |
int rename(const char *old, const char *new); | 重命名文件 | old: 原文件名 new: 新文件名 | 成功: 返回0 失敗: 返回-1 | rename("old.txt", "new.txt"); |
FILE *tmpfile(void); | 創建臨時文件 | 無 | 成功: 返回FILE指針 失敗: 返回NULL | FILE *tmp = tmpfile(); |
char *tmpnam(char *s); | 生成唯一文件名 | s: 存儲緩沖區(可為NULL) | 返回指向文件名的指針 | char name[L_tmpnam]; tmpnam(name); |
3、注意事項
-
使用文件操作函數時,應始終檢查返回值以確保操作成功
-
打開文件后,必須記得關閉文件(重要!!!)
-
二進制模式和文本模式在Windows平臺上有區別(換行符處理不同)
-
文件指針位置會影響讀寫操作的結果
-
緩沖區內容在文件關閉或調用fflush()之前可能不會實際寫入磁盤
4、對文件進行寫入操作示例
#include <stdio.h>
int main()
{FILE* fp = fopen("log.txt", "w");if (fp == NULL){perror("fopen");return 1;}int count = 5;while (count){fputs("hello world\n", fp);count--;}fclose(fp);return 0;
}
編譯并運行程序后,在當前路徑下就會生成對應文件,文件當中就是我們寫入的內容:
5、對文件進行讀取操作示例
#include <stdio.h>
int main()
{FILE* fp = fopen("log.txt", "r");if (fp == NULL){perror("fopen");return 1;}char buffer[64];for (int i = 0; i < 5; i++){fgets(buffer, sizeof(buffer), fp);printf("%s", buffer);}fclose(fp);return 0;
}
編譯并運行程序后,就會將我們剛才寫入文件的內容讀取出來,并打印在顯示器上:
6、另一個讀文件的例子
#include <stdio.h>
#include <string.h>int main() {FILE *fp = fopen("myfile", "r");if(!fp) {printf("fopen error!\n");return 1;}char buf[1024];const char *msg = "hello bit!\n";while(1) {ssize_t s = fread(buf, 1, strlen(msg), fp);if(s > 0) {buf[s] = '\0';printf("%s", buf);}if(feof(fp)) {break;}}fclose(fp);return 0;
}
代碼功能
-
打開名為"myfile"的文件用于讀取
-
如果文件打開失敗,打印錯誤信息并退出
-
每次讀取與字符串"hello bit!\n"長度相同的數據塊(11字節)
-
將讀取的內容打印到標準輸出
-
遇到文件結尾時停止讀取
-
關閉文件并退出
代碼結構分析
-
文件打開部分:
FILE *fp = fopen("myfile", "r"); if(!fp) {printf("fopen error!\n");return 1; }
-
嘗試以只讀模式打開"myfile"文件
-
如果打開失敗(fp為NULL),打印錯誤信息并返回1
-
-
變量定義:
char buf[1024]; const char *msg = "hello bit!\n";
-
buf
是1024字節的緩沖區 -
msg
是字符串常量"hello bit!\n",用于確定每次讀取的長度
-
-
讀取循環:
while(1) {ssize_t s = fread(buf, 1, strlen(msg), fp);if(s > 0) {buf[s] = '\0';printf("%s", buf);}if(feof(fp)) {break;} }
-
無限循環讀取文件內容
-
每次讀取
strlen(msg)
(11)個字節到buf
中 -
如果讀取到數據(s>0),在末尾添加'\0'使其成為字符串并打印
-
檢查文件結束標志,如果到達文件末尾則退出循環
-
-
清理部分:
fclose(fp); return 0;
-
關閉文件
-
程序正常退出
-
編譯并運行輸出的結果:
稍作修改,實現簡單cat命令:
#include <stdio.h>
#include <string.h>int main(int argc, char* argv[]) {if (argc != 2) {printf("Usage: %s <filename>\n", argv[0]);return 1;}FILE *fp = fopen(argv[1], "r");if(!fp) {printf("fopen error!\n");return 2;}char buf[1024];while(1) {int s = fread(buf, 1, sizeof(buf), fp);if(s > 0) {buf[s] = '\0';printf("%s", buf);}if(feof(fp)) {break;}}fclose(fp);return 0;
}
1. 代碼功能
-
從命令行接收一個文件名,并嘗試打開該文件。
-
逐塊讀取文件內容(每次最多?
1024
?字節),并打印到標準輸出(stdout
)。 -
檢測文件結束(EOF),并在讀取完成后關閉文件。
2. 代碼解析
(1) 參數檢查
if (argc != 2) {printf("Usage: %s <filename>\n", argv[0]);return 1;
}
-
argc
?表示命令行參數的個數,argv[0]
?是程序名,argv[1]
?是傳入的文件名。 -
如果用戶未提供文件名(
argc != 2
),打印用法提示并返回?1
(表示錯誤)。
(2) 打開文件
FILE *fp = fopen(argv[1], "r");
if(!fp) {printf("fopen error!\n");return 2;
}
-
嘗試以?只讀模式(
"r"
)?打開文件?argv[1]
。 -
如果打開失敗(
fp == NULL
),打印錯誤信息并返回?2
。
(3) 讀取文件內容
char buf[1024];
while(1) {int s = fread(buf, 1, sizeof(buf), fp);if(s > 0) {buf[s] = '\0'; // 添加字符串結束符printf("%s", buf);}if(feof(fp)) {break;}
}
-
fread
?讀取數據:-
buf
:存儲數據的緩沖區。 -
1
:每個元素的大小(字節)。 -
sizeof(buf)
:每次最多讀取?1024
?字節。 -
fp
:文件指針。 -
返回值?
s
:實際讀取的字節數(可能小于?1024
)。
-
-
printf
?打印數據:-
buf[s] = '\0'
:確保?buf
?是一個合法的 C 字符串(printf
?需要?\0
?結尾)。 -
printf("%s", buf)
:打印讀取的內容。
-
-
feof(fp)
?檢測文件結束:-
如果?
fread
?返回?0
?且?feof(fp)
?為真,說明文件已經讀完,退出循環。
-
(4) 關閉文件
fclose(fp);
return 0;
-
關閉文件,防止資源泄漏。
-
返回?
0
?表示程序正常結束。
3. 關鍵點分析
(1)?fread
?的返回值
-
s > 0
:成功讀取數據,存入?buf
?并打印。 -
s == 0
:-
如果?
feof(fp)
?為?true
,說明文件已讀完,正常結束。 -
如果?
feof(fp)
?為?false
,可能是?讀取錯誤(但代碼未處理)。
-
(2)?buf[s] = '\0'
?的作用
-
fread
?不會自動添加?'\0'
,但?printf("%s", buf)
?需要?'\0'
?作為字符串結束符。 -
如果文件是?二進制數據,可能會因為?
'\0'
?導致?printf
?提前終止輸出。
(3) 循環終止條件
-
feof(fp)
?為?true
:文件讀完,正常退出。 -
如果?
fread
?返回?0
?但?feof(fp)
?仍為?false
:-
可能是?讀取錯誤(如文件損壞)。
-
當前代碼?不會檢測這種情況,可能導致?無限循環。
-
編譯并運行輸出結果:
四、輸出信息到顯示器的常用方法
#include <stdio.h>
#include <string.h>int main() {const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}
1、代碼功能
-
使用?
fwrite
?直接寫入二進制數據。 -
使用?
printf
?格式化輸出字符串。 -
使用?
fprintf
?向指定流(這里是?stdout
)輸出格式化字符串。
2、代碼解析
(1)?fwrite
?方式
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
-
fwrite
?是?二進制寫入函數,適用于任何數據(包括字符串、二進制數據等)。 -
參數解析:
-
msg
:要寫入的數據("hello fwrite\n"
)。 -
strlen(msg)
:寫入的?每個元素的大小(這里是字符串長度,不包括?'\0'
)。 -
1
:寫入的元素個數(這里是?1
?個字符串)。 -
stdout
:輸出流(標準輸出)。
-
-
特點:
-
不會自動添加?
'\0'
,因為它?不關心數據內容,只是按字節寫入。 -
適合?二進制寫入(如文件 I/O),但也可以用于字符串。
-
(2)?printf
?方式
printf("hello printf\n");
-
printf
?是?格式化輸出函數,默認輸出到?stdout
。 -
特點:
-
自動處理?
'\0'
?結尾,適合?文本輸出。 -
支持格式化字符串(如?
%d
,?%s
?等)。
-
(3)?fprintf
?方式
fprintf(stdout, "hello fprintf\n");
-
fprintf
?是?指定流的格式化輸出,可以輸出到任意文件流(如?stdout
、文件等)。 -
參數解析:
-
stdout
:指定輸出流(這里仍然是標準輸出)。 -
"hello fprintf\n"
:要輸出的字符串。
-
-
特點:
-
和?
printf
?類似,但可以指定輸出目標(如文件、標準錯誤?stderr
?等)。 -
適合需要?靈活控制輸出目標?的場景。
-
3、輸出結果
編譯并運行程序后,輸出如下:
-
fwrite
?和?printf
/fprintf
?都能正確輸出字符串。 -
但它們的?底層機制不同:
-
fwrite
?是?二進制寫入,不關心字符串格式。 -
printf
?和?fprintf
?是?格式化輸出,更適合文本。
-
4、關鍵區別
函數 | 類型 | 是否處理?'\0' | 適用場景 | 示例 |
---|---|---|---|---|
fwrite | 二進制寫入 | ? 不處理 | 文件 I/O、二進制數據 | fwrite(data, 1, size, fp); |
printf | 格式化輸出 | ? 自動處理 | 標準輸出、格式化文本 | printf("num=%d", 123); |
fprintf | 流格式化輸出 | ? 自動處理 | 任意流(文件、stdout 、stderr ) | fprintf(stderr, "Error: %s", errmsg); |
5、使用場景建議
-
如果要寫入字符串到?
stdout
,優先用?printf
?或?fprintf
:-
它們會自動處理?
'\0'
,更安全。 -
支持格式化(如?
%d
,?%f
)。
-
-
如果要寫入二進制數據(如圖片、結構體),用?
fwrite
:它不會修改數據,適合原始字節流。 -
如果要輸出到文件或?
stderr
,用?fprintf
:可以靈活指定輸出目標。
6. 擴展:puts
?和?fputs
如果只是簡單輸出字符串,還可以用:
puts("hello puts"); // 自動加換行
fputs("hello fputs", stdout); // 不加換行
-
puts
?會自動在末尾加?'\n'
,而?fputs
?不會。
7. 總結
-
fwrite
:底層二進制寫入,適合非文本數據。 -
printf
:標準格式化輸出,默認到?stdout
。 -
fprintf
:可指定輸出流的格式化寫入(如文件、stderr
)。 -
puts
/fputs
:更簡單的字符串輸出方式。
這段代碼展示了三種不同的輸出方式,實際使用時可以根據需求選擇最合適的函數。?
五、?什么是當前路徑?
????????當使用fopen
以寫入模式打開文件時,若文件不存在,系統會在當前工作目錄自動創建該文件。這里的"當前路徑"具體指程序運行時的工作目錄。
????????例如上面的例子中,在當前工作目錄(~目錄)下運行可執行程序demo1
時,程序創建的log.txt
文件默認會生成在~目錄中:
????????我們可以理解為:"當前路徑"指的是可執行程序運行時所在的路徑。為了驗證這一點,我們可以進行以下測試:
- 首先刪除之前生成的log.txt文件
- 在當前工作目錄中,創建一個新目錄,將可執行程序demo1拷貝到這個新目錄中
- cd到這個新目錄中重新運行該程序,觀察log.txt生成在哪里
????????運行該可執行程序后,我們發現log.txt文件并未在~目錄下生成,而是出現在當前工作目錄newdir中!!!這與結論相符!!!
????????當程序運行成為進程后,我們可以通過獲取其PID來查看相關信息。該進程的詳細信息存儲在根目錄下的proc目錄中,可通過對應PID進行訪問。
? ? ? ? 為了可以查看相關信息,我們把demo1.c改為每寫入一句字符串就休眠5秒,不至于查看信息的時候看不到現象:
#include <stdio.h>
#include <unistd.h>int main()
{FILE* fp = fopen("log.txt", "w");if (fp == NULL){perror("fopen");return 1;}int count = 5;while (count){fputs("hello world\n", fp);sleep(5);count--;}fclose(fp);return 0;
}
命令解析:ps -axj | head -1 && ps -axj | grep demol | grep -v grep
-
ps -axj | head -1
-
ps -axj
:顯示所有用戶的進程(a
),包括無控制終端的進程(x
),并以作業格式(j
)顯示 -
head -1
:只保留第一行輸出(即列標題) -
這部分的作用是顯示進程列表的標題行
-
-
ps -axj | grep demo1 | grep -v grep
-
ps -axj
:同上,獲取所有進程的完整列表 -
grep demo1
:過濾出包含"demo1"的行 -
grep -v grep
:排除掉grep自身的進程(因為grep命令本身也會出現在進程列表中)
-
組合效果:
-
先顯示進程列表的標題行
-
然后顯示所有包含"demo1"的進程信息(不包括grep自身的進程)
其中:
cwd
:指向當前進程運行目錄的符號鏈接exe
:指向啟動當前進程的可執行文件(含完整路徑)的符號鏈接
????????當打開文件時,實際上是進程在執行文件的操作。由于進程知道自身所在位置,即便文件不帶路徑信息,進程也能準確定位。因此,操作系統能夠明確新創建文件的存儲位置。
????????需要強調的是,這里所說的"當前路徑"并非指可執行程序的存儲位置,而是指程序運行成為進程時所在的工作目錄路徑。