LinuxC语言编程03:输入输出

标准I/O

标准I/O是指标准C中定义的一组输入和输出的API,与操作系统无关,具备可移植性.

标准I/O将打开文件的信息抽象成为流(stream),使用FILE结构体代表流.标准I/O的所有操作都是围绕FILE结构体进行的.

文本流与二进制流

Windows操作系统区分文本流与二进制流,两者间最显著的区别在文本流中\n会被替换成\r\n,而二进制流不会做这种转换.而Linux操作系统不区分文本流与二进制流,不会做这种转换.

流的缓冲类型

流有三种缓冲类型:

  1. 全缓冲: 当流的缓冲区无数据或无空间时才执行实际I/O操作.使用标准I/O打开文件时默认是全缓冲.
  2. 行缓冲: 当在输入输出中遇到换行符\n时才执行实际I/O操作.当流与终端交互时(输入或输出)是行缓冲.
  3. 无缓冲: 直接与文件进行交互,不进行缓存.向标准错误流输出时是无缓冲.

预定义的流

标准I/O中预定义了3个流,程序运行时自动打开这三个流:

预定义的流文件描述符关键字
标准输入流0stdin
标准输出流1stdout
标准错误流2stderr

除了上面3个预定义的的流以外,一个C程序最多打开1021个流.

流的打开与关闭

流的打开

使用fopen()函数打开一个流,其函数原型如下:

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

成功打开流时返回流的指针,出错时返回NULL,并设置错误号errno.

pathname参数表示要打开的文件路径,mode参数表示文件的打开方式,可选值如下:

文件打开方式意义
"r""rb"以只读方式打开文件.若文件不存在则报错.
"r+""rb+"以读写方式打开文件.若文件不存在则报错.
"w""wb"以只写方式打开文件.若文件存在则清空原文件;若文件不存在则创建文件.
"w+""wb+"以读写方式打开文件.若文件存在则清空原文件;若文件不存在则创建文件.
"a""ab"以只写方式打开文件.若文件存在则将数据追加到文件末尾;若文件不存在则创建文件.
"a+""ab+"以读写方式打开文件.若文件存在则将数据追加到文件末尾;若文件不存在则创建文件.

上面的"b"参数表示以二进制方式打开文件,Linux下不区分文本流与二进制流,该参数可忽略.

下面表格对比了各种打开方式的区别:

要求rwar+w+a+
文件必须存在
清空原文件中的内容
流可以读
流可以写
流只能在尾端写

使用fopen()函数创建的文件的访问权限为0666(rw-rw-rw-),但系统的umask设定会影响文件的访问权限,实际权限的表达式为实际权限 = 默认权限&~umask.默认的umask值为022,因此使用fopen()函数创建的文件的实际访问权限为0644(rw-r--r--).

流的关闭

使用fclose()函数打开一个流,其函数原型如下:

int fclose(FILE *stream);

成功时关闭流时返回0,出错时返回EOF,并设置错误号errno.

流关闭时自动刷新缓冲区中的数据并释放缓冲区,流关闭后不能对其执行任何操作.

当一个程序正常终止时,所有打开的流都会被关闭.

处理错误信息

当打开或关闭流的过程中发生错误时,程序会将错误号存放在变量errno中,该变量被定义在头文件errno.h中.使用perror()strerror()可以得到错误的详细信息,两个函数的函数原型如下:

void perror(const char *s);
char *strerror(int errno);

perror()函数先输出字符串s,再输出错误的详细信息.strerror()函数根据错误号errno返回对应的错误的详细信息.

下面例子演示了一个常见的打开文件并处理错误的方式:

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

int main() {
    FILE *fp;
    if ((fp = fopen("file.txt", "r+")) == NULL) {
        // 下面两种写法是等价的
        perror("打开流的过程中发生错误");
        printf("打开流的过程中发生错误: %s\n", strerror(errno));
        return -1;
    }
    
    if ((fclose(fp)) == EOF) {
    	// 下面两种写法是等价的
        perror("关闭流的过程中发生错误");
        printf("关闭流的过程中发生错误: %s\n", strerror(errno));
        return -1;
    }
    
    return 0;
}

流的读写

流支持三种不同的读写方式:

  • 按字符读写: fgetc()/fputc()每调用一次读写一个字符.
  • 按行读写: fgets()/fputs()每调用一次读写一行.
  • 按对象读写: fread()/fread()每调用一次读写若干个具有相同长度的对象.

按对象读写最灵活,最常被使用.

按字符读写

按字符输入

下列函数用来读取一个字符:

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

其中fgetc()getc()是等价的,它们从stream中读取一个字符,getchar()从标准输入stdin中读取一个字符,等同于fgetc(stdin).

成功读取时返回读取到的字符,读到文件末尾或出错时返回EOF.

按字符输出

下列函数用来输出一个字符:

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

其中fgetc()getc()是等价的,它们将c转化为unsigned int类型变量并将其写入到stream中,getchar()向标准输入stdin中写入一个字符,等同于fputc(c, stdout).

成功写入时返回写入的字符,出错时返回EOF并设置错误号errno.

下面的例子使用fgetcfputc实现文件的复制:

#include<stdio.h>

int main(const int argc, const char *argv[]) {
    
    // 若传入参数不够则提醒用户输入完整参数
    if (argc < 3) {
        printf("Usage: %s <src_file> <dst_file>\n", argv[0]);
        return -1;
    }
    
    // 打开输入流与输出流
    FILE *fps, *fpd;
    if ((fps = fopen(argv[1], "r")) == NULL) {
        perror("打开src_file的过程中发生错误");
        return -1;
    }
    
    if ((fpd = fopen(argv[2], "w")) == NULL) {
        perror("创建dst_file的过程中发生错误");
        return -1;
    }
    
    // 进行复制
    char ch;
    while((ch = fgetc(fps)) != EOF) {
        fputc(ch, fpd);
    }
    
    // 关闭两个流
    fclose(fps);
    fclose(fpd);
    
    return 0;
}	

按行读写

按行输入

下列函数用于读取一行:

char *gets(char *s);
char *fgets(char *s, int size, FILE* stream);

gets()从标准输入stdin中读取一行数据,不建议使用,容易造成缓冲区溢出.fget()从流stream中读取数据,遇到\n或读到size-1个字符时返回,因此独到的字符串总是包含\0.

成功读取时返回读到的缓冲区字符串指针s,读到文件末尾或出错时返回NULL.

按行输出

下列函数用于输出一行:

int puts(const char *s);
int fputs(const char *s, FILE *stream);

puts()将缓冲区s中的字符串写入到标准输出流stdout中,并追加\n.fputs()将缓冲区s中的字符串写入到流stream中,并追加\n.

成功写入时返回输出的字符个数,出错时返回EOF并设置错误号errno.

尽管我们称其为按行输出,但puts()fputs()仅仅是将字符串s中的内容输出,不会受到s中可能存在的换行符\n影响.

下面程序用于统计传入的文件的行数:

#include<stdio.h>
#include<string.h>

#define N 20	// 指定缓冲区大小

int main(const int argc, const char *argv[]) {
    
    // 若传入参数不够则提醒用户输入完整参数
    if (argc < 2) {
        printf("Usage: %s <filename>\n", argv[0]);
        return -1;
    }
    
    // 打开文件
    FILE *fp;
    if ((fp = fopen(argv[0], "a+")) == NULL) {
        perror("打开文件的过程中发生错误");
        return -1;
    }
    
    // 读取到文件末尾,计算行数
    int line = 0;
    char buff[N];
    while (fgets(buff, N, fp) != NULL) {
        // 若缓冲区内容以\n结尾,才表示读完一整行
        if (buff[strlen(buff)-1] == '\n')	line++;
    }
   
    return 0;
}	

按对象读写

下列函数用于从流中读写若干个对象:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

fread()fwrite()用于对流stream读写最多nmemb个大小为size字节的对象,并存入缓冲区*ptr中.

读写成功时返回读写的对象个数,出错时返回EOF并设置错误号errno.

下面的例子使用fread()fwrite()实现文件的复制:

#include<stdio.h>

#define N 20	// 指定缓冲区大小

int main(const int argc, const char *argv[]) {
    
    // 若传入参数不够则提醒用户输入完整参数
    if (argc < 3) {
        printf("Usage: %s <src_file> <dst_file>\n", argv[0]);
        return -1;
    }
    
    // 打开输入流与输出流
    FILE *fps, *fpd;
    if ((fps = fopen(argv[1], "r")) == NULL) {
        perror("打开src_file的过程中发生错误");
        return -1;
    }
    
    if ((fpd = fopen(argv[2], "w")) == NULL) {
        perror("创建dst_file的过程中发生错误");
        return -1;
    }
    
    // 进行复制
    char buff[N];
    int nmemb;
    while((nmemb = fread(buff, 1, N, fps)) > 0) {
        fwrite(buff, 1, nmemb, fpd);
    }
    
    // 关闭两个流
    fclose(fps);
    fclose(fpd);
    
    return 0;
}	

流的刷新

流的刷新指的是立即执行实际I/O操作并清空缓冲区,在以下三种情况下,流会刷新:

  1. 全缓冲情况下缓冲区满或行缓冲情况下遇到换行符.
  2. 流关闭时.
  3. 调用fflush()函数显式刷新缓冲区.

fflush()函数的函数原型如下:

int fflush(FILE *stream);

若刷新成功则返回0,出错则返回EOF.

下面例子演示刷新缓冲区的作用:若不刷新缓冲区,则不会进行实际I/O,文件的大小不会改变:

#include<stdio.h>

int main() {

    // 打开一个一直不关闭的输出流
    FILE *fp;
    if ((fp = fopen("test.txt", "w")) == NULL) {
        perror("打开文件的过程中发生错误");
        return -1;
    }
    
    fputc('a', fp);
    // fflush(fp)		// 若注释掉该句,则发现创建的文件一直为空
	while(1);
    
    return 0;
}	

运行上述程序,我们发现创建的文件的大小一直为0,说明不进行缓冲的情况下不会进行实际的I/O操作.

流的定位

下列函数与流的定位有关:

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
  • ftell()函数返回流stream的当前读写位置,出错时返回EOF并设置errno.

  • fseek()函数对流进行定位,成功时返回0,出错时返回EOF并设置errno.

    该函数将流定位到whence加上offset个字节的位置,whence的取值为下面三个常量之一:

    whence意义
    SEEK_SET文件开头
    SEEK_CUR文件的当前读写位置
    SEEK_END文件末尾
  • rewind()函数将流定位到文件开始位置,相当于fseek(fp, 0, SEEK_SET)

下面例子演示了fseek()函数的用法:

#include <stdio.h>

int main () {
   	FILE *fp = fopen("file.txt","w+");
    
   	fputs("This is a file", fp);
	fseek(fp, 7, SEEK_SET);
   	fputs(" C Programming Langauge", fp);
   	
    fclose(fp);
	
    return 0;
}

最终文件中的内容如下:

This is C Programming Langauge

判断流的出错与结束

  • 下面函数用于判断流是否出错:

    int ferror(FILE *stream);
    

    若流stream出错则返回非零数,否则返回0.

  • 下面函数用于判断流是否已读到文件末尾:

    int feof(FILE *stream);
    

    若已读到流stream末尾则返回非零数,否则返回0.

格式化输入输出

fscanf()fprintf()两个函数用于对流进行格式化输入输出,用法类似于scanf()printf().

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

读写成功时返回读写的字符个数,出错时返回EOF并设置errno.

下面例子以追加形式每隔一秒向文件log.txt中写入当前时间以及行号:

#include<stdio.h>
#include<string.h>
#include<unistd.h>	// sleep()函数的头文件
#include<time.h>	// time()和localtime()函数的头文件

#define N 20	// 指定缓冲区大小

int main() {
    
    // 打开log.txt文件
    FILE *fp;
    if ((fp = fopen("log.txt", "a+")) == NULL) {
        perror("打开log.txt的过程中发生错误");
        return -1;
    }
    
    // 读取到文件末尾,计算行数
    int line = 0;
    char buff[N];
    while (fgets(buff, N, fp) != NULL) {
        if (buff[strlen(buff)-1] == '\n')	line++;
    }
    
    // 不断循环向文件末尾追加内容
    time_t t;			// 存储以秒数表示的时间
    struct tm *tp;		// 存储以可读格式表示的时间
    while(1) {
        time(&t);
        tp = localtime(&t);
        fprintf(fp, "%02d, %04d-%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;
}	

运行上述程序一段时间后,按Ctrl+c终止程序,发现时间信息已经按照期望的格式存入log.txt文件中.再次运行该程序一段时间,发现时间信息被追加进log.txt文件中且行号能正确的按照顺序接上.

01, 2019-12-10 08:30:53
02, 2019-12-10 08:30:54
03, 2019-12-10 08:30:55
04, 2019-12-10 08:31:12
05, 2019-12-10 08:31:13
06, 2019-12-10 08:31:14
07, 2019-12-10 08:31:15

文件I/O

文件I/O是一套只适用于Linux系统的文件操作的API,不带有缓冲机制.下面表格对比标准I/O与文件I/O的区别:

标准I/O文件I/O
标准ANSI CPOSIX
是否带有缓冲机制有缓冲无缓冲
核心对象文件描述符

在linux操作系统下,标准I/O是基于文件I/O实现的.因此,标准I/O也被称为高级I/O,文件I/O也被称为低级I/O.

文件描述符

文件描述符是一个非负整数,Linux为程序中每个打开的文件分配一个文件描述符,文件I/O的所有操作都是围绕文件描述符进行的.

其中,预留的三个文件描述符0,1,2分别代表标准输入流stdin,标准输出流stdout和标准错误流stderr.

文件的打开与关闭

文件的打开

使用open()函数打开文件,被定义在头文件fcntl.h中,函数原型如下:

int open(const char *pathname, int flags);  
int open(const char *pathname, int flags, mode_t mode);  

成功打开文件时返回文件描述符,出错时返回EOF并设置错误号errno.

pathname参数表示文件名;flags参数表示文件的打开方式,是一系列基本常量相与|得到的结果;mode参数仅在创建新文件时有效,表示被打开文件的权限,同样受到umask影响.flags参数的取值及其作用如下:

取值作用
O_RDONLY以只读方式打开文件这三个参数互斥
O_WRONLY以只写方式打开文件
O_RDWR以读写方式打开文件
O_CREAT若该文件不存在则创建新文件,并设置其权限为mode参数
O_EXCL设置O_CREAT时该位有效,若文件存在则报错.
O_NOCTTY若欲打开的文件为终端机设备时,则不会将该终端机当成进程控制终端
O_TRUNC若文件存在且以可写的方式打开时则清空文件中原有数据
O_APPEND以追加方式打开文件

文件的关闭

使用close()函数关闭一个打开的文件,被定义在头文件unistd.h中,函数原型如下:

int close(int fd)

成功关闭文件时返回0,出错时返回EOF并设置错误号errno.

与标准I/O类似,当一个程序正常终止时,所有打开的文件都会被关闭.当文件被关闭后,对应的文件描述符就不再代表该文件.

文件I/O中发生错误时同样会设置错误号errno,并使用perror()strerror()得到错误的详细信息.

#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>

int main() {
    
    int fd;
    if ((fd = open("test.txt", O_RDWR|O_CREAT|O_EXCL, 0666) == EOF)) {
        perror("创建文件过程中出错");
        return -1;
    }
    
	if (close(fd) == EOF) {
        perror("关闭文件过程中出错");
        return -1;        
    }
    
    return 0;
}

文件的读写

read()函数和write()函数用于进行文件读写,被定义在头文件unistd.h中,函数原型如下:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

上述函数通过缓冲区buf向文件描述符fd表示的文件中读写最多count个字节的数据.读写成功时返回读写的字节数,出错时返回EOF并设置错误号errno.

下面程序使用文件I/O实现文件复制:

#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>

# define N 20

int main(const int argc, const char *argv[]) {
    
    // 若传入参数不够则提醒用户输入完整参数
    if (argc < 3) {
        printf("Usage: %s <src_file> <dst_file>\n", argv[0]);
        return -1;
    }
    
    // 打开原文件与目标文件
    int fds, fdt;
    if ((fds = open(argv[1], O_RDONLY)) == EOF) {
        perror("打开src_file的过程中发生错误");
        return -1;
    }
    
    if ((fdt = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0666)) == EOF) {
        perror("创建dst_file的过程中发生错误");
        return -1;
    }
    
    // 进行复制
    char buff[N];
    int n;
    while((n = read(fds, buff, N)) > 0) {
        write(fdt, buff, n);
    }
    
    // 关闭两个文件
    close(fds);
    close(fdt);
    
    return 0;
}	

文件的定位

lseek()函数用于对文件进行定位,被定义在头文件unistd.h中,函数原型如下:

off_t lseek(int fd, off_t offset, int whence);

fd参数表示被定位文件的文件描述符,offsetwhence参数与fseek()函数的用法完全相同.

目录的访问

使用opendir()函数打开一个目录文件,使用readdir()函数读取目录流中的内容,使用closedir()函数关闭一个目录文件,它们都被定义在头文件dirent.h中,函数原型如下:

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
  • opendir()函数成功打开目录文件时返回一个DIR结构体指针,出错时返回NULL并设置错误号errno.
  • readdir()函数成功读取到目录流中的下一个目录项时返回一个struct dirent结构体指针,读到文件流末尾时或出错时返回NULL并设置错误号errno.
  • closedir()函数成功关闭目录文件时返回0,出错时返回EOF并设置错误号errno.

下面程序打印给定目录的所有目录项:

#include<stdio.h>
#include<dirent.h>

int main(const int argc, const char *argv[]) {
    
    // 若传入参数不够则提醒用户输入完整参数
    if (argc < 2) {
        printf("Usage: %s <directory>\n", argv[0]);
        return -1;
    }
    
    // 打开目录流
    DIR *dirp;
    if ((dirp = opendir(argv[1])) == NULL) {
        perror("打开目录流出现错误");
        return -1;
    }
    
    // 遍历输出所有目录项
    struct dirent *dp;
    while ((dp = readdir(dirp)) != NULL) {
        printf("%s\n", dp->d_name);
    }
    
    return 0;
}	 

文件的访问权限

chmod()fchmod()函数用于修改文件的访问权限,它们都被定义在头文件sys/stat.h中,函数原型如下:

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

成功修改访问权限时返回0,出错时返回EOF并设置错误号errno.

文件的属性

stat(),lstat()fstat()函数用来获取文件属性,函数原型如下:

int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);

上述函数读取文件属性并将其存入参数statbuf指向的结构体中.成功读取到文件参数时并返回0,出错时返回EOF并设置错误号errno.

stat()函数和lstat()函数在大多数情况下是等价的,区别在于:若传入的文件是符号链接,则stat()函数获取的是目标文件的属性,而lstat()函数获取的是链接文件本身的属性.

结构体struct stat存储文件的属性,包含的成员参数如下:

struct stat {
    dev_t     st_dev;         /* ID of device containing file */
    ino_t     st_ino;         /* Inode number */
    mode_t    st_mode;        /* File type and mode */
    nlink_t   st_nlink;       /* Number of hard links */
    uid_t     st_uid;         /* User ID of owner */
    gid_t     st_gid;         /* Group ID of owner */
    dev_t     st_rdev;        /* Device ID (if special file) */
    off_t     st_size;        /* Total size, in bytes */
    blksize_t st_blksize;     /* Block size for filesystem I/O */
    blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */
    struct timespec st_atim;  /* Time of last access */
    struct timespec st_mtim;  /* Time of last modification */
    struct timespec st_ctim;  /* Time of last status change */
};

其中st_mode成员存储了文件类型和访问权限信息.

  • 可以根据判断表达式st_mode&0170000的运算结果来确定文件类型:

    表达式st_mode&0170000的运算结果文件类型
    0140000套接字文件
    0120000符号链接文件
    0100000普通文件
    0060000块设备文件
    0040000目录文件
    0020000目录文件
    0010000管道文件
  • 可以根据st_mode与以下掩码相与的结果判断用户的访问权限:

    掩码意义
    S_IRWXU00700拥有者是否具有读写执行权限
    S_IRUSR00400拥有者是否具有读权限
    S_IWUSR00200拥有者是否具有写权限
    S_IXUSR00100拥有者是否具有执行权限
    S_IRWXG00070拥有组其他成员是否具有读写执行权限
    S_IRGRP00040拥有组其他成员是否具有读权限
    S_IWGRP00020拥有组其他成员是否具有写权限
    S_IXGRP00010拥有组其他成员是否具有执行权限
    S_IRWXO00007其他用户是否具有读写执行权限
    S_IROTH00004其他用户是否具有读权限
    S_IWOTH00002其他用户是否具有写权限
    S_IXOTH00001其他用户是否具有执行权限

下面程序仿照ls -l的格式输出传入文件的属性信息:

#include<stdio.h>
#include<time.h>
#include<sys/stat.h>

int main(const int argc, const char *argv[]) {
    
    // 若传入参数不够则提醒用户输入完整参数
    if (argc < 2) {
        printf("Usage: %s <filename>\n", argv[0]);
        return -1;
    }

    // 获取文件的属性
    struct stat statbuf;
    if (lstat(argv[1], &statbuf) == EOF) {
        perror("获取文件属性失败");
        return -1;
    }
    
    // 判断文件类型并输出
    switch (statbuf.st_mode & S_IFMT) {
        case S_IFREG:
            printf("-");
            break;
        case S_IFDIR:
            printf("d");
            break;
		// 省略其他文件类型的判断过程
    }
    
    // 判断文件权限并输出
    for (int n=8; n>=0; n--) {
        if (statbuf.st_mode & (1<<n)) {
            switch (n%3) {
                case 2:
                    printf("r");
                    break;
                case 1:
                    printf("w");
                    break;
                case 0:
                    printf("x");
                    break;
            }
        } else {
            printf("-");
        }
    }
    
    //输出文件大小
    printf("\t%lu", statbuf.st_size);
    
    // 输出文件最后修改时间
    struct tm *tp = localtime(&statbuf.st_mtime);
    printf("\t%04d-%02d-%02d %02d:%02d:%02d",  
               tp->tm_year+1900, tp->tm_mon+1, tp->tm_mday,
               tp->tm_hour, tp->tm_min, tp->tm_sec);
    
    // 输出文件名
    printf("%s\n", argv[1]);
    
    return 0;
}	 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值