文件的概念和類型
概念:一組相關數據的有序集合
文件類型:
- 常規文件 r
- 目錄文件 d
- 字符設備文件 c
- 塊設備文件 b
- 管道文件 p, 進程間通信的機制
- 套接字文件 s, 進程間通信的機制
- 符號鏈接文件 l
如何理解標准IO
標准IO由ANSIC標准定義,就是用標准C語言定義好的一組用來輸入和輸出的API
主流操作系統(Linux,Windows)上都實現了C庫
標准IO通過緩沖機制減少系統調用,實現更高的效率
流(FILE)的含義
標准IO用一個結構體數據類型來存放打開的文件的相關信息
標准IO的所有操作都圍繞FILE來進行
FILE又被稱為流(stream)
流分為兩種流分別是
文本流:Windows系統中文本流的換行符占用兩個字節用“\r\n”表示,LInux中用‘\n’表示
二進制流:Windows系統二進制流的換行符占用一個字節用“\n”表示,LInux中用‘\n’表示
流的緩沖
全緩沖:當流的緩沖區無數據或無空間時才執行實際IO操作
行緩沖:當在輸入和輸出中遇到換行符“\n”時,進行IO操作;當流和一個終端關聯時,是典型的行緩沖
無緩沖:數據直接寫入文件,流不進行緩沖,一般在打印錯誤信息時使用
標准IO預定義3個流,程序運行時自動打開
標准輸入流 | 0 | STDIN_FILENO | stdin |
標准輸出流 | 1 | STDOUT_FILENO | stdout |
標准錯誤流 | 2 | STDERR_FILENO | stderr |
流的打開
下列函數可用於打開一個標准IO流
FILE *fopen(const char *path, const char *modle);
成功時返回流指針;出錯時返回NULL
model參數
模式 | 描述 |
---|---|
r或rb | 打開一個已有的文本文件,允許讀取文件。 |
w或wb | 打開一個文本文件,允許寫入文件。如果文件不存在,則會創建一個新文件。在這里,您的程序會從文件的開頭寫入內容。如果文件存在,則該會被截斷為零長度,重新寫入。 |
a或ab | 打開一個文本文件,以追加模式寫入文件。如果文件不存在,則會創建一個新文件。在這里,您的程序會在已有的文件內容中追加內容。 |
r+或r+b | 打開一個文本文件,允許讀寫文件。 |
w+或w+b | 打開一個文本文件,允許讀寫文件。如果文件已存在,則文件會被截斷為零長度,如果文件不存在,則會創建一個新文件。 |
a+或a+b | 打開一個文本文件,允許讀寫文件。如果文件不存在,則會創建一個新文件。讀取會從文件的開頭開始,寫入則只能是追加模式。 |
當給定b參數時,表示以二進制方式打來文件,但linux下忽略該參數
fopen新建文件權限
fopen()創建的文件訪問權限是0666(rw-rw-rw),0表是8進制數
Linux系統中umask設定會影響文件的訪問權限,其規則為(0666 & (~umask)),可以通過umask命令查看,默認為0022
0022 ----> 000 010 010
取反 ----> 111 101 101
0666 ----> 110 110 110
結果:---> 110 100 100 ---> 0644(rw-r--r--)
用戶可以通過umask函數修改相關設定,將umask設置為0時,umask不影響文件訪問權限
錯誤信息處理
extern int errno;//存放錯誤號
void perror(const char *s);//向輸出字符串s,再輸出錯誤號對應的錯誤信息
char *strerror(int errno);//根據錯誤號返回對應的錯誤信息
流的關閉
int fclose(FILE *stream)
fclose()調用成功返回0,失敗返回EOF,並設置errno
流關閉時自動刷新緩沖中的數據並釋放緩沖區
當一個程序正常終止時,所有打開的流都會被關閉,但是為了安全期間,程序員要主動關閉
流一旦關閉后就不能執行任何操作
程序中能夠打開的文件或流的個數有限制,寫程序測試:
程序結果是1021,因為啟動一個程序,默認打開stdin、stdout、stderr三個流,所以一個是1024;
#include <stdio.h> int main() { int count; while (1) { if (fopen("mycp.c", "r") == NULL) { break; } count++; } printf("%d\n", count);//1021 return 0; }
讀寫流
流支持不同的讀寫方式
讀寫一個字符:fgetc()/fputc()一次讀/寫一個字符
讀寫一行:fgets()/fputs()一次讀/寫一行數據,一般用於文本文件,一般不適用於二進制文件
讀寫若干個對象:fread()/fwrite()每次讀/寫若干個對象,而每個對象具有相同的長度,效率高,推薦使用
按字符輸入
下列函數用來輸入一個字符
#include <stdio.h>
int fgetc(FILE *stream);//與getc()函數功能完全相同,成功時返回讀取的字符,若到文件末尾或出錯時返回EOF
int getc(FILE *stream);
int getchar(void);//從標准輸入中獲取字符等同於fgetc(stdin)
按字符輸出
下列函數用來輸出一個字符
#include <stdio.h>
int fputc(int c, FILE *stream);//與putc()函數功能完全相同,第一個參數是輸出的字符,成功時返回寫入的字符;出錯時返回EOF
int putc(int c, FILE *stream);
int putchar(int c);//向標准輸出流寫入一個字符,等同於fputc(c, stdout);
利用fgetc()/fputc()實現文件復制
#include <stdio.h> #include <string.h> //提供strerror()函數 #include <errno.h> //提供errno變量 int main(int argc, char *argv[]) { FILE *fps, *fpd; //定義兩個流指針分別指向源文件和目標文件 int ch; //保存讀出的字符 if (argc < 3) //檢驗命令行參數 { printf("Usage : %s <src_file> <dst_file>\n", argv[0]); return -1; } /* 打開源文件 */ if ((fps = fopen(argv[1], "r")) == NULL) { //perror("fopen src file"); printf("fopen src file: %s\n",strerror(errno)); return -1; } /* 打開目標文件 */ if ((fpd = fopen(argv[2], "w")) == NULL) { perror("fopen dst file"); return -1; } while ((ch = fgetc(fps)) != EOF) { fputc(ch, fpd); } fclose(fps); fclose(fpd); return 0; }
按行輸入
下列函數用來輸入一行:
#include <stdio.h>
char *gets(char *s);//從標准輸入讀入一行數據,不推薦使用,因為沒有執行緩沖區的大小,容易造成緩沖區溢出
char *fgets(char *s, int size, FILE *stream);//成功時返回s,到文件末尾或出錯時返回NULL,遇到"\n"或已輸出size-1個字符時返回,總是包含"\0"
按行輸出
下列函數用來輸出一行字符串:
#include <stdio.h>
int puts(const char *s);//將緩沖區s中的字符串輸出到stdout,並追加"\n"
int fputs(const chars *s, FILE *stream);//將緩沖區s中的字符串輸出到stream中,不追加"\n"
成功時返回輸出的字符個數;出錯時返回EOF
寫程序統計一個文本中包含多少行?
#include <stdio.h> #include <string.h> #define SIZE 37 int main(int argc, const char *argv[]) { int count = 0;//統計文本行數 FILE *fp; char buf[SIZE] = {};//如果buf不指定大小會報棧溢出錯誤 if ((fp = fopen("b.txt", "r")) == NULL) { perror("fopen"); return -1; } while (fgets(buf, SIZE+1, fp) != NULL) { if (buf[strlen(buf) -1] == '\n') { count ++; } } fclose(fp); printf("lines:%d\n", count); return 0; }
按指定對象輸入/輸出
下列函數用來從流中讀寫若干個對象:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t n, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *fp);
參數說明:
ptr是緩沖區的首地址
size是對象數據的大小,對象為字符時值為1,對象為數字時值為4.
n是要讀寫的對象個數
fp為流指針
成功返回實際讀寫的對象個數;出錯時返回EOF
既可以讀寫文件,也可以讀寫數據文件
效率高
使用fread()/fwrite()函數實現文件拷貝
#include <stdio.h> #include <string.h> //提供strerror()函數 #include <errno.h> //提供errno變量 #define N 64 int main(int argc, char *argv[]) { FILE *fps, *fpd; //定義兩個流指針分別指向源文件和目標文件 char buf[N]; int n; if (argc < 3) //檢驗命令行參數 { printf("Usage : %s <src_file> <dst_file>\n", argv[0]); return -1; } /* 打開源文件 */ if ((fps = fopen(argv[1], "r")) == NULL) { //perror("fopen src file"); printf("fopen src file: %s\n",strerror(errno)); return -1; } /* 打開目標文件 */ if ((fpd = fopen(argv[2], "w")) == NULL) { perror("fopen dst file"); return -1; } while ((n = fread(buf, 1, N, fps)) > 0) { fwrite(buf, 1, n, fpd); } fclose(fps); fclose(fpd); return 0; }
流的刷新
自動刷新
全緩沖:當流的緩沖區滿的時候將自動刷新,打開文件時默認是全緩沖
行緩沖:流的緩沖區滿的時候或遇到換行符"\n"將自動刷新
關閉流的時候將會自動刷新
手動刷新
#include <stdio.h>
int fflush(FILE *fp);
成功返回0;出錯時返回EOF
將流緩沖區中的數據寫入到實際的文件
Linux下只能刷新輸出緩沖區
流的定位
#include <stdio.h>
long ftell(FILE (stream);//成功的時候返回當前讀寫位置,出錯時返回EOF
long fseek(FILE *stream, long offset, int whence);//定位一個流,成功返回0,出錯時返回EOF,whence參數:SEEK_SET(文件的開始)/SEEK_CUR(當前位置)/SEEK_END(文件的結尾)
void rewind(FILE *stream);//將流定位到文件的起始位置
檢測流結束和出錯
#include <stdio.h>
int ferror(FILE *stream);//返回1表示流出錯,否則返回0
int feof(FILE *stream);//返回1表示文件已到末尾;否則返回0
格式化輸出
#include <stdio.h>
int printf(const char *fmt,...);
int fprintf(FILE *stream, const char *fmt,...);//向指定流中輸出格式化后的數據
int sprintf(char *s, const char *fmt,...);//向指定緩沖區中輸出格式化后的數據
寫程序實現一下功能:
每個一秒向文件test.txt文件中寫入系統時間,格式如下:
1, 2020-02-19 15:30:20
2, 2020-02-19 15:30:21
該程序無限循環,直到ctrl+c結束程序,下次再執行該代碼時接着之前的格式接續寫入:3, 2020-02-19 15:31:21
#include <stdio.h> #include <string.h> //strlen() #include <time.h> //time()/localtime() #include <unistd.h> //sleep() #define SIZE 3 int main() { int line = 0;//用於記錄行號 FILE *fp; char buf[SIZE]; time_t t;//用於存放當前的時間 struct tm *tp;//存放格式化之后的時間 { }; if ((fp = fopen("test.txt", "a+")) == NULL) { perror("fopen"); return -1; } while (fgets(buf, SIZE, fp) !=NULL) { if (buf[strlen(buf) - 1] == '\n') line++; } while (1) { time(&t); tp = localtime(&t); fprintf(fp, "%02d, %d-%02d-%02d %02d:%02d:%02d\n", ++line, tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec); fflush(fp);//刷新緩沖區,將緩沖區的內容寫入到文件中 sleep(1); } return 0; }
文件IO
什么是文件IO
posix(可移植操作系統接口)定義的一組函數
不提供緩沖機制,每次讀寫操作都引起系統調用
核心概念是文件描述符
訪問各種類型文件(標准io一般只能訪問普通文件和終端文件)
Linux下,標准IO基於文件IO實現
文件描述符
每打開的文件都對應一個文件描述符
文件描述符是一個非負整數,Linux為程序中每個打開的文件分配一個文件描述符
文件描述符從0開始分配,依次遞增
每個程序打開的文件描述符都是相互獨立的
文件IO操作通過文件描述符來完成
0,1,2分別表示標准輸入,標准輸出,標准錯誤
open函數用來創建或打開一個文件
#include <fcntl.h>
int open(const char *path, int oflag, ...);
成功時返回文件描述符;出錯時返回EOF
打開文件時使用兩個參數
創建文件時第三個參數指定新文件的權限
只能打開設備文件
原型 | int open(const char *pathname, int flags, mode_t mode); | ||
參數 | pathname | 被打開的文件名(可包括路徑名) | |
flags | O_RDONLY:只讀方式打開文件 | 這三個參數互斥 | |
O_WRONLY:可寫方式打開文件 | |||
O_RDWR:讀寫方式打開文件 | |||
O_CREAT:如果文件不存在,就創建一個新的文件,並用第三個參數為其設置權限 | |||
O_EXCL:如果使用O_CREAT是文件存在,則可返回錯誤信息。這一參數可測試文件是否存在 | |||
O_NOCTTY:使用本參數時,如文件為終端,那么終端不可以作為調用open()系統調用的哪個進程的控制終端 | |||
O_TRUNC:如文件已經存在,那么打開文件時先刪除文件中原有數據 | |||
O_APPEND:以添加方式打開文件,所以對文件的寫操作都在文件的末尾進行 | |||
mode | 被打開文件的存儲權限,為8進制表示 |
以只寫方式打開文件1.txt。如果文件不存在則創建,如果文件存在則清空:
int fd;
if ((fd = open("1.txt", O_WRONLY| O_CREAT|O_TRINC, 0666)) < 0) {
perror("open");
return -1;
}
以讀寫方式打開文件1.txt,如果文件不存在則創建,如果文件存在則報錯
int fd;
if((fd = open("1.txt", O_RDWR|O_CREAT|O_EXCL, 0666)) < 0) {
if (errno == EEXIST){
perror("exist error");
}else{
perror("other error");
}
}
close 函數用來關閉一個打開的文件
#include <unistd.h>
int close(int fd);
成功時返回0;出錯時返回EOF
程序結束時自動關閉所有打開的文件
文件關閉后,文件描述符不再代表文件
讀取文件
read函數用來從文件中讀取數據
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t, count);
成功時返回實際讀取的字節數;出錯時返回EOF
讀到文件末尾時返回0
buf是接收數據的緩沖區
count不應超過buf大小
從指定的文件(文本文件)中讀取內容並統計大小
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(int argc, const char *argv[]) { int fd, n, total = 0; char buf[64]; if (argc < 2) { printf("Usage: %s <file>\n", argv[0]); return -1; } if ((fd = open(argv[1], O_RDONLY)) < 0 ) { perror("open"); return -1; } while ((n = read(fd, buf, 64)) > 0) { total += n; } close(fd); printf("total: %d\n", total); return 0; }
寫入文件
write函數用來向文件寫入數據
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t, count);
成功時返回實際寫入的字節數;出錯時返回EOF
buf是發送數據的緩沖區
count不應超過buf的大小
將鍵盤輸入的內容寫入文件,直到輸入quit
#include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define N 20 int main(int argc, char *argv[]) { int fd; char buf[N]; if (argc < 2) { printf("Usage: %s <file>\n", argv[0]); return -1; } if ((fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0 ) { perror("open"); return -1; } while (fgets(buf, N + 1, stdin) != NULL) { if(strcmp(buf, "quit\n") == 0) break; write(fd, buf, strlen(buf)); } close(fd); return 0; }
定位文件
lseek函數用來定位文件
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
成功時返回當前文件讀寫位置;出錯時返回EOF
參數offset和參數whence同fseek完全一樣
讀取目錄
opendir函數用來打來一個目錄文件
#include <dirent.h>
DIR *opendir(const char *name);
DIR是用來描述一個打開的目錄文件的結構體類型
成功時返回目錄流指針;出錯時返回NULL
readdir函數用來讀取目錄流中的內容
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent是用來描述目錄流中一個目錄項的結構體類型
包含成員char d_name[256] 參數幫助文檔
成功時返回目錄流dirp中下一個目錄項;出錯或到末尾時返回NULL
close函數用來關閉一個目錄文件
#include <dirent.h>
int closedir(DIR *drip);
成功返回0;失敗返回EOF
打印指定目錄下所有文件名稱(只會打印指定目錄下的文件和目錄,不會打印子目錄下的文件)
#include <dirent.h> #include <stdio.h> int main(int argc, char *argv[]) { DIR *dirp; struct dirent *dp; if (argc < 2) { printf("Usage: %s <directory>\n", argv[0]); return -1; } if ((dirp = opendir(argv[1])) == NULL) { perror("opendir"); return -1; } while ((dp = readdir(dirp)) != NULL) { printf("%s\n", dp->d_name); } closedir(dirp); return 0; }
修改文件訪問權限
chmod/fchmod函數用來修改文件的訪問權限
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
成功返回0;出錯返回EOF
root和文件所有者能修改文件的訪問權限
獲取文件屬性
stat/lstat/fstat函數用來獲取文件的屬性
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);//推薦使用
int fstat(int fd, struct stat *buf);
成功返回0;出錯返回EOF
如果path是符號連接stat獲取的是目標文件色的屬性;而lstat獲取的是連接文件的屬性
struct stat是存放文件屬性的結構體類型
mode_t st_mode: 類型和訪問權限
uid_t st_uid: 所有者id
uid_t st_gid: 用戶組id
off_t st_size: 文件大小
time_t st_mtime: 最后修改時間
st_mode
通過系統提供的宏來判斷文件類型:
st_mode & 0170000 (001 111 000 000 000 000,文件類型掩碼)
S_ISREG(st_mode) 0100000
S_ISDIR(st_mode) 0040000
S_ISCHR(st_mode) 0020000
S_ISBLK(st_mode) 0060000
S_ISFIFO(st_mode) 0010000
S_ISLNK(st_mode) 0120000
S_ISSOCK(st_mode) 0140000
通過系統提供的宏來獲取文件訪問權限
S_IRUSR 00400 8
S_IWUSR 00200 7
S_IXUSR 00100 6
S_IRGRP 00040 5
S_IWGRP 00020 4
S_IXGRP 00010 3
S_IROTH 00004 2
S_IWOTH 00002 1
S_IXOTH 00001 0
獲取並顯示文件屬性
#include <stdio.h> #include <sys/stat.h> #include <unistd.h> #include <time.h> #include <sys/types.h> int main(int argc, char *argv[]) { struct stat buf; int n; struct tm *tp; if (argc < 2) { printf("Usage %s <file>\n", argv[0]); return -1; } if (lstat(argv[1], &buf) < 0) { perror("lstat"); return -1; } switch (buf.st_mode & S_IFMT) { case S_IFREG: printf("-"); break; case S_IFDIR: printf("d"); break; case S_IFCHR: printf("c"); break; case S_IFBLK: printf("b"); break; case S_IFIFO: printf("p"); break; case S_IFLNK: printf("l"); break; case S_IFSOCK: printf("s"); break; } for (n=8; n>=0; n--) { if (buf.st_mode & (1 << n)) //1左移n位與一個數求&獲得的是該數第n位的值 { switch (n % 3) { case 2: printf("r"); break; case 1: printf("w"); break; case 0: printf("x"); break; } } else { printf("-"); } } printf(" %lu" , buf.st_size); tp = localtime(&buf.st_mtime); printf(" %d-%02d-%02d", tp->tm_year+1900, tp->tm_mon+1, tp->tm_mday); printf(" %s\n", argv[1]); return 0; }
庫
庫的概念
庫是一個二進制文件,包含的代碼可被程序調用
標准C庫、數學庫、線程庫......
庫有源碼,可下載后編譯;也可以直接安裝二進制包
庫默認的安裝路徑在/lib 或/usr/lib
庫是事先編譯好的,可以復用的代碼
在OS上運行的程序基本上都要使用庫。使用庫可以提高開發效率
Windows和Linux下庫文件的格式不兼容
Linux下包含靜態庫和共享庫
靜態庫
編譯(鏈接)時把靜態庫中相關代碼復制到可執行文件中
程序中已經包含代碼,運行時不再需要靜態庫
程序運行時無需加載庫,運行速度更快
占用更多磁盤和內存空間
靜態庫升級后,程序需要重新編譯鏈接
靜態庫的創建
確定庫中函數的功能、接口
編寫庫源碼hello.c
#include <stdio.h> void hello(void) { printf("hello world!\n"); }
編譯生成目標文件hello.o:gcc -c hello.c -Wall
創建靜態庫hello:ar crs libhello.a hello.o(庫文件名為libhell.a,庫名為hello)
查看庫中符號信息:
xdl@xdl-gj:~/C語言/lib$ nm libhello.a
hello.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T hello
U puts
鏈接靜態庫
編寫應用程序test.c
void hello(void); int main() { hello(); return 0; }
編譯test.c並鏈接靜態庫libhello.a
gcc -o test test.c -L. -lhello
-L:用來指定庫的搜索路徑,. 表示當前目錄
-l:指定要鏈接的庫名
共享庫
編譯(鏈接)時僅記錄用到哪個共享庫中的哪個符號(hs),不復制共享庫中相關代碼
程序不包含庫中代碼,尺寸小
多個程序可共享同一個庫
程序運行時需要加載庫
庫升級方便,無需重新編譯程序
使用更加廣泛
共享庫的創建
確定庫中函數功能、接口
編寫庫源碼hello.c bye.c
#include <stdio.h> void hello(void) { printf("hello world!\n"); } /**********************************/ #include <stdio.h> void bye(void) { printf("bye!\n"); }
編譯生成目標文件:gcc -c -fPIC hello.c by2.c -Wall;
-fPIC:告訴編譯器要生成.o文件可以被任何位置的程序調用
創建共享庫common:gcc -shared -o libcommon.so.1 hello.o bye.o ;.1表示版本
為共享庫文件創建鏈接文件:ln -s libcommon.so.1 libcommon.so
鏈接共享庫
編寫應用程序test.c
#include "common.h" int main() { hello(); bye(); return 0; } /*common.h*/ void hello(void); void bye(void);
編譯test.c並鏈接共享庫libcommon.so:
gcc -o test test.c -L. -lcommon (默認先找共享庫,其次才會尋找靜態庫,可以通過-static 參數直接連接靜態庫)
加載共享庫
執行程序:
xdl@xdl-gj:~/C語言/lib$ ./test
./test: error while loading shared libraries: libcommon.so: cannot open shared object file: No such file or directory
添加共享庫的加載路徑
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
xdl@xdl-gj:~/C語言/lib$ ./test
hello world!
bye!
如何找到共享庫
為了讓系統能找到要加載的共享庫,有三種方法:
1、把庫拷貝到/usr/lib 或 /lib目錄下
2、在LD_LIBRARY_PATH環境變量中添加庫所在路徑
3、添加/etc/ld.so.conf.d/*.conf文件,執行ldconfig刷新
sudo vim my.conf
/home/xdl/C語言/lib
sudo ldconfig