标准IO与文件IO的区别

IO概念

1> IO:顾名思义就是输入输出,程序与外部设备进行信息交换的过程称为IO操作

2> 最先接触的IO:#include<stdio.h>标准的缓冲输入输出头文件

IO的分类

1> 标准IO:使用系统提供的库函数实现

2> 文件IO:基于系统调用完成,每进行一次系统调用,进程会从用户空间内核空间进行一次切换,当用户空间内核空间进行切换时,进程就会进入一次挂起状态,从而导致进程执行效率低

3> 文件IO与标准IO的区别:标准IO相比于文件IO而言,提供了缓冲区,用户可以将数据先放入缓冲区中,等到缓冲区时机到了后,统一进行一次系统调用,将数据刷入内核空间

  • 标准IO接口printf/scanffopen/fclosefgetc/fputcfgets/futsfprintf/fscaffread/fwritefseek/ftell/rewind

  • 文件IO接口open/closeread/writelseek

标准IO

实现原理

FILE结构体

1> FILE结构体是系统提供的用于描述一个文件全部信息的结构体

2> FILE结构体的原型

1
2
3
4
5
6
struct FILE
{
char * _IO_buf_base; //缓冲区的起始地址
char * _IO_buf_end; //缓冲区终止地址
int _fileno; //文件描述符,用于进行系统调用
};

3> 特殊的FILE指针,这三个指针,全部都是针对于终端文件而言的,当程序启动后,系统默认打开的三个特殊文件指针

  • stderr: 标准出错指针
  • stdin: 标准输入指针
  • stdout: 标准输出指针

打开文件:fopen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h> //该函数所在的头文件
FILE *fopen(const char *path, const char *mode);
功能:打开一个文件,并返回当前文件的文件指针
参数1:表示文件路径,是一个字符串
参数2:打开文件的方式,也是一个字符串,字符串必须以以下的字符开头
r 以只读的形式打开文件,文件光标定位在开头部分
r+ 以读写的形式打开文件,文件光标定位在开头部分
w 以只写的形式打开文件,如果文件不存在,就创建文件,如果文件存在,就清空,光标定位在开头
w+ 以读写的形式打开文件,如果文件不存在,就创建文件,如果文件存在,就清空,光标定位在开头
a 以追加(结尾写)的形式打开文件,如果文件不存在就创建文件,光标定位在文件末尾
a+ 以读写的形式打开文件,如果文件不存在就创建文件,如果文件存在,读取操作光标定位在开头,写操作光标定位在结尾
#英文原版
r Open text file for reading. The stream is positioned at the beginning of the file.
r+ Open for reading and writing. The stream is positioned at the beginning of the file.
w Truncate file to zero length or create text file for writing. The stream is positioned at the beginning of the file.
w+ Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file). The file is created if it does not exist. The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.
返回值:成功返回打开的文件指针,失败返回NULL并置位错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include"stdio.h" //标准的输入输出头文件
int main(int argc, const char *argv[])
{
//1、定义一个文件指针
FILE *fp = NULL;
//以只读的形式打开一个不存在的文件,并将返回结果存入到fp指针中
//fp = fopen("./file.txt", "r");
//此时会报错,原因是以只读的形式打开一个不存在的文件,是不允许的
//以只写的形式打开一个不存在的文件,如果文件不存在就创建一个空的文件,如果文件存在就清空
fp = fopen("./file.txt", "w");
if(fp == NULL)
{
printf("fopen error\n");
return -1;
}
printf("fopen success\n");

fclose(fp);
return 0;
}

关闭文件:fclose

1
2
3
4
5
#include <stdio.h>
int fclose(FILE *fp);
功能:关闭指定的文件
参数:要关闭的文件指针(由fopen返回的结果)
返回值:成功执行返回0,失败返回EOF并置位错误码

错误码

1> 概念:当内核提供的函数出错后,内核空间会向用户空间反馈一个错误信息,由于错误信息比较多也比较复杂,系统就给每种不同的错误信息起了一个编号,用一个整数表示,这个整数就是错误码

2> 错误码都是大于或等于0的数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define EPERM   1   /* Operation not permitted */ 操作受限
#define ENOENT 2 /* No such file or directory */ 没有当前文件或目录
#define ESRCH 3 /* No such process */ 没有进程
#define EINTR 4 /* Interrupted system call */ 中断系统调用
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */

3> 关于错误码的处理函数(strerror、perror

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <errno.h> //错误码所在的头文件,里面定义了一个全局变量
int errno; //是线程局部变量:在多线程程序中,每个线程有自己独立的 errno,不会互相干扰。

//将错误码对应的错误信息转换处理
#include <string.h>
char *strerror(int errnum);
功能:将错误码转换为错误信息描述
参数:错误码
返回值:错误信息字符串描述

//只打印错误码对应的错误信息的函数
#include <stdio.h>
void perror(const char *s);
功能:输出当前错误码对应的错误信息
参数:提示符号,会原样打印出来,并且会在提示数据后面加上冒号,并输出完后,自动换行
返回值:无
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include"stdio.h" //标准的输入输出头文件
#include<errno.h> //错误码所在的头文件
#include<string.h> //字符串处理的头文件
int main(int argc, const char *argv[])
{
//1、定义一个文件指针
FILE *fp = NULL;
fp = fopen("./file.txt", "r");
if(fp == NULL)
{
//printf("fopen error: %d, errmsg:%s\n", errno,strerror(errno)); //打印错误信息,并输出错误码 2
perror("fopen error"); //打印当前错误码对应的错误信息
return -1;
}
printf("fopen success\n");
//2、关闭文件
fclose(fp);
return 0;
}

单字符读写:fputc/fgetc

原型:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int fgetc(FILE *stream);
功能:从指定文件中读取一个字符数据,并以无符号整数的形式返回
参数:文件指针
返回值:成功返回读取的字符对应的无符号整数,失败返回EOF并置位错误码
#include <stdio.h>
int fputc(int c, FILE *stream);
功能:将指定的字符c写入到stream指向的文件中
参数1:要写入的字符对应的无符号整数
参数2:文件指针
返回值:成功返回写入字符对的无符号整数,失败返回EOF并置位错误码

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<stdio.h>
int main(int argc, const char *argv[])
{
//定义文件指针
FILE *fp = NULL;
if((fp = fopen("./file.txt", "w")) == NULL) //以只写的形式打开一个文件
{
perror("fopen error");
return -1;
}
//使用程序对文件进行读写操作(单个字符进行操作)
//本次操作的结果是在外部文件中显示Hello,说明每写入一次,文件光标都会向后偏移
fputc('H', fp);
fputc('e', fp);
fputc('l', fp);
fputc('l', fp);
fputc('o', fp);
//能否从该处读取数据?不可以,为什么?因为光标此时在文件结尾处,没有数据可以被读取
//关闭文件
fclose(fp);
//再次以只读的形式重新打开上一个文件
if((fp = fopen("./file.txt", "r")) == NULL) //以只写的形式打开一个文件
{
perror("fopen error");
return -1;
}
char ch = 0; //字符的搬运工,将文件中的字符,搬运到终端上
while(1)
{
ch = fgetc(fp); //从fp指向的文件中光标位置处读取一个字符,并返回
if(ch == EOF)
{
break; //说明文件已经全部被读取了,此时就可以退出循环了
}
printf("%c ", ch); //在终端上打印读取的字符
}
//关闭文件
fclose(fp);
return 0;
}

拷贝文件案例

使用fgetc和fputc完成两个文件的拷贝工作,实现指令cp的功能: cp srcfile destfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<stdio.h>
int main(int argc, const char *argv[])
{
//判断外部传参的个数是否为 3
if(argc != 3)
{
printf("input file error\n");
printf("usage:./a.out srcfile destfile\n");
return -1;
}
//以只读的形式打开源文件,以只写的形式打开目标文件
FILE *srcfp = NULL; //源文件文件指针
FILE *destfp = NULL; //目标文件文件指针
if((srcfp = fopen(argv[1], "r")) ==NULL)
{
perror("srcfile open error");
return -1;
}
if((destfp = fopen(argv[2], "w")) ==NULL)
{
perror("destfile open error");
return -1;
}
//将源文件中的内容搬运到目标文件中
char ch = 0; //搬运工
while(1)
{
ch = fgetc(srcfp); //从源文件中读取一个字符
if(ch == EOF)
{
break; //文件全部读取结束
}
fputc(ch, destfp); //将读取的字符写入到目标文件中
}
//关闭两个文件
fclose(srcfp);
fclose(destfp);
printf("拷贝成功\n");
return 0;
}

字符串读写:fgets/fputs

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
int fputs(const char *s, FILE *stream);
功能:将指定的字符串,写入到指定的文件中
参数1:要被写入的字符串
参数2:文件指针
返回值:成功返回本次写入字符的个数,失败返回EOF


char *fgets(char *s, int size, FILE *stream);
功能:从stream指向的文件中最多读取size-1个字符到s容器中,遇到回车或文件结束,会结束一次读取,并且会将回车放入容器,最后自动加上一个字符串结束标识'\0'(之所以读取size-1是因为要留位置给结束符)
参数1:字符数组容器起始地址
参数2:要读取的字符个数,最多读取size-1个字符
参数3:文件指针
返回值:成功返回容器s的起始地址,失败返回NULL

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<stdio.h>
#include<iostream>
#include<string.h>
int main(int argc, const char *argv[])
{
//打开文件
FILE *fp = NULL;
if((fp = fopen("./file.txt", "w")) == NULL)
{
perror("fopen eror");
return -1;
}
//向文件中写入多个字符
char wbuf[128] = ""; //定义一个字符数组
while(1)
{
std::cin >> wbuf; //从终端输入一个字符串
//将上述字符串写入文件中
fputs(wbuf, fp);
fputc('\n', fp);
if(strcmp(wbuf, "quit") == 0) //strcmp:比较两个字符串是否一致,如果相等则返回0
{
break;
}
}
//关闭文件
fclose(fp);
//以只读的形式再次打开文件
if((fp = fopen("./file.txt", "r")) == NULL)
{
perror("fopen eror");
return -1;
}
char rbuf[128] = "";
while(1)
{
//从文件中读取数据到rbuf容器中
char *res = fgets(rbuf, sizeof(rbuf), fp);
if(res == NULL)
{
break; //说明文件读取结束
}
printf("rbuf = %s\n", rbuf); //将数据打印到终端上
}
//关闭文件
fclose(fp);
return 0;
}

拷贝文件案例

使用fgets和fputs完成两个文件的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
int main(int argc, const char *argv[])
{
//判断外部是否传入两个文件
if(argc != 3)
{
printf("inut file error\n");
printf("usage:./a.out srcfile destfile\n");
return -1;
}
//以只读的形式打开源文件,以只写的形式打开目标文件
FILE *srcfp = NULL;
FILE *destfp = NULL;
if((srcfp = fopen(argv[1], "r")) == NULL)
{
perror("fopen error");
return -1;
}
if((destfp = fopen(argv[2], "w")) == NULL)
{
perror("fopen error");
return -1;
}
//定义搬运工
char buf[128] = "";
while(1)
{
char *ptr = fgets(buf, sizeof(buf), srcfp); //将将数据从源文件中读取下来
if(ptr == NULL)
{
break; //读取结束
}
fputs(buf, destfp); //将从源文件中读取的数据写入到目标文件中
}
//关闭文件
fclose(srcfp);
fclose(destfp);
printf("拷贝成功\n");
return 0;
}

标准IO的缓冲区问题

缓冲区分类及大小

  • 行缓存:和终端文件相关的缓冲区叫做行缓存,行缓冲区的大小为1024字节,对应的文件指针:stdinstdout
  • 全缓存:和外界文件相关的缓冲区叫做全缓存,全缓冲区的大小为4096字节,对应的文件指针:fp
  • 不缓存:和标准出错相关的缓冲区叫不缓存,不缓存的大小为0字节,对应的文件指针:stderr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
//如果缓冲区没有被使用时,求出大小为0,只有被至少使用了一次后,缓冲区的大小就被分配了
printf("行缓存的大小为:%d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
//行缓存的大小
printf("行缓存的大小为:%d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
//1024


printf("行缓存的大小为:%d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
//0 没有使用标准输入
int num = 0;
//scanf("%d", &num); //从终端输入一个整数让如num变量中,类似于cin >> num
cin >> num; //使用了标准的输入缓冲区
printf("行缓存的大小为:%d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
//1024


//验证全缓存的大小为4096
FILE *fp = NULL;
if((fp = fopen("./aa.txt", "r")) == NULL)
{
perror("fopen error");
return -1;
}
//未使用全缓存时,大小为0
printf("全缓存的大小为:%d\n", fp->_IO_buf_end - fp->_IO_buf_base); //0
//使用一次全缓存
fgetc(fp); //从文件中读取一个字符
printf("全缓存的大小为:%d\n", fp->_IO_buf_end - fp->_IO_buf_base); //4096
//关闭文件
fclose(fp);


//验证不缓存的大小为0
//未使用不缓存时大小为0
printf("不缓存的大小为:%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);
//0


//使用一次不缓存
perror("error");
printf("不缓存的大小为:%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);
//0
return 0;
}

结果图:

缓冲区刷新时机/fflush函数

缓冲区刷新函数:fflush

原型:

1
2
3
4
5
#include <stdio.h>
int fflush(FILE *stream);
功能:刷新给定的文件指针对应的缓冲区
参数:文件指针
返回值:成功返回0,失败返回EOF并置位错误码

行缓存刷新时机

触发刷新(输出)的条件 具体说明
遇到换行符 \n 最核心规则:只要输出内容里有 \n,不管缓冲区有没有满,都会立即把缓冲区内容刷到屏幕 / 终端。
缓冲区被填满 备用规则:如果一直不输出 \n,当写入的内容达到缓冲区容量(通常为 1024 字节)时,缓冲区会自动刷新。
主动调用刷新函数 手动控制:调用 fflush(stdout) 可以强制刷新,不管有没有 \n、缓冲区满没满。
程序正常结束 程序执行到 return 0 / exit() 时,会自动刷新所有缓冲区,确保数据全部输出。
输入输出切换时 当从输出(如 printf)切换到输入(如 scanfgetchar)时,为了让用户看到提示,行缓冲区会自动刷新。
关闭行缓存对应的文件指针 调用 fclose(stdout) 或程序结束时,会先刷新缓冲区,再关闭文件指针,防止数据丢失。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
/*1、验证缓冲区如果没有达到刷新时机,就不会将数据进行刷新
printf("hello world"); //在终端上打印输出一个hello world,没有到缓冲区的
刷新时机,就不会输出数据

while(1); //阻塞程序不让进程结束
*/

/*2、当程序结束后,会刷新行缓冲区
printf("hello world");
*/

/*3、当遇到换行时,会刷新行缓存
printf("hello world\n");
while(1);
*/

/*4、当输入输出发生切换时,也会刷新行缓存
int num = 0;
printf("请输入>>>"); //向标准输出缓冲区中写入一组数据,没有换行符号
scanf("%d", &num); //cin >> num
*/

/*5、当关闭行缓存对应的文件指针时,也会刷新行缓存
printf("hello world"); //向标准输出缓冲区中写入数据,没有换行
fclose(stdout); //关闭标准输出指针
while(1);
*/

/*6、使用fflush函数手动刷新缓冲区时,行缓存会被刷新
printf("hello world");
fflush(stdout);
while(1);
*/

//7、当缓冲区满了后,会刷新行缓存,行缓存大小:1024字节
for(int i=0; i<1025; i++)
{
printf("A");
}

while(1); //防止程序结束
return 0;
}

全缓存刷新时机

触发刷新(输出)的条件 具体说明
缓冲区被填满 当写入的数据量达到缓冲区容量(通常为 4096 字节)后,再写入数据时,缓冲区会自动刷新。
主动调用刷新函数 调用 fflush(fp) 可以强制刷新缓冲区,与缓冲区是否已满无关。
程序正常结束 程序执行到 return 0 / exit() 时,会自动刷新所有缓冲区,确保数据全部写入文件。
输入输出切换时 当对同一个文件指针进行读写切换时(如先 fputs 输出,后 fgetc 输入),会自动刷新缓冲区。
关闭文件指针时 调用 fclose(fp) 关闭文件时,会先刷新缓冲区,再关闭文件指针,防止数据丢失。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
//打开一个文件
FILE *fp = NULL;
if((fp = fopen("./aa.txt", "r+")) == NULL) //以读写的形式打开文件
{
perror("fopen error");
return -1;
}

/*1、当缓冲区刷新时机未到时,不会刷新全缓存
fputs("hello world", fp); //将字符串写入到文件中
while(1);
*/

/*2、当遇到换行时,也不会刷新全缓存
fputs("hello world\n", fp); //将字符串写入到文件中
while(1);
*/

/*3、当程序结束后,会刷新全缓存
fputs("hello world 你好 星球!\n", fp);
*/

/*4、当输入输出发生切换时,会刷新全缓存
fputs("I love China\n", fp); //向文件中输出一个字符串
fgetc(fp); //从文件中读取一个字符,主要是让输入输出发生切换
while(1);
*/

/*5、当关闭缓冲区对应的文件指针时,也会刷新全缓存
fputs("上海欢迎你", fp);
fclose(fp); //刷新文件指针
while(1);
*/

/*6、当手动刷新缓冲区对应的文件指针时,也会刷新全缓存
fputs("上海欢迎你", fp);
fflush(fp); //刷新文件指针
while(1);
*/

//7、当缓冲区满了后,再向缓冲区中存放数据时会刷新全缓存
for(int i=0; i<4097; i++)
{
fputc('A', fp); //循环将单字符放入文件中
}
while(1);
return 0;
}

全缓冲与行缓冲对比

特性 全缓冲(Full Buffer) 行缓冲(Line Buffer)
适用场景 磁盘文件 IO(fopen 终端 IO(stdout/stdin
对换行符 \n 的响应 完全忽略,不会触发刷新 核心触发条件,遇到 \n 立即刷新
输入输出切换 对同一文件指针读写切换时,会刷新 从输出切换到输入时,会刷新
关闭文件指针 会刷新 主要用于终端,一般不涉及关闭文件指针
主动刷新 fflush(fp) fflush(stdout)
程序结束 自动刷新 自动刷新
缓冲区满 自动刷新 自动刷新(备用规则)

不缓存直接刷新

对于不缓存的刷新时机,只要放入数据,立马进行刷新

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
//perror("a"); //向标准出错中放入数据
fputs("A", stderr); //向标准出错缓冲区中写入一个字符A
while(1); //阻塞
return 0;
}

格式化读写:fprintf/fscanf

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
int fprintf(FILE *stream, const char *format, ...);
功能:向指定的文件中输出一个格式串
参数1:文件指针
参数2:格式串,可以包含格式控制符,%d(整数)、%s(字符串)、%f(小数)、%lf(小数)
参数3:可变参数,输出项列表,参数个数由参数2中的格式控制符的个数决定
返回值:成功返回输出的字符个数,失败返回一个负数

int fscanf(FILE *stream, const char *format, ...);
功能:从指定的文件中以指定的格式读取数据,放入程序中
参数1:文件指针
参数2:格式串,可以包含格式控制符,%d(整数)、%s(字符串)、%f(小数)、%lf(小数)
参数3:可变参数,输入项地址列表,参数个数由参数2中的格式控制符的个数决定
返回值:成功返回读入的项数,失败返回EOF并置位错误码

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
//向标准输出文件指针中写入数据
fprintf(stdout, "%d %lf %s\n", 520, 3.14, "I Love Xingqiu");
//从标准输入中读取数据到程序中来
int num = 0;
fscanf(stdin, "%d", &num); //从标准输入缓冲区中以%d的格式读取一个整数
printf("num = %d\n", num); //1314
//对外部文件进行格式化读写
FILE *fp = NULL;
if((fp = fopen("./usr.txt", "w")) == NULL) //以只写的形式打开外部文件
{
perror("fopen error");
return -1;
}
//向文件中写入数据
fprintf(fp, "%s %d", "admin", 123456); //将两个不同的数据存放到文件中
//关闭文件
fclose(fp);
//再次以只读的形式打开文件
if((fp = fopen("./usr.txt", "r")) == NULL) //以只写的形式打开外部文件
{
perror("fopen error");
return -1;
}
char usrName[20] = ""; //存放用户名
int pwd = 0; //存放密码
fscanf(fp, "%s %d", usrName, &pwd); //从外部文件中分别读取一个字符串和一
个整数放入程序中的
//usrName和pwd中
//关闭
fclose(fp);
printf("usrName = %s, pwd = %d\n", usrName, pwd);
return 0;
}

案例:

​ 使用fprintffscanf完成注册和登录功能,要求做个小菜单,三个功能,功能1是注册,用户输入账号和密码后,将其存放到外部文件中;功能2是登录,用户输入登录账号和密码后,如果跟文件中匹配,则提示登陆成功,如果全部不匹配,则提示登录失败;功能0,表示退出。菜单可以循环调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
using namespace std;
//定义注册函数
int do_register()
{
//定义存储注册账户和密码的容器
char reg_name[20] = "";
char reg_pwd[20] = "";
//输入注册账户和密码
printf("请输入注册账户:");
fgets(reg_name, sizeof(reg_name), stdin);
reg_name[strlen(reg_name)-1] = '\0'; //将字符串最后的换行符换成结束符号
printf("请输入注册密码:");
fgets(reg_pwd, sizeof(reg_pwd), stdin);
reg_pwd[strlen(reg_pwd)-1] = '\0';
//将账户和密码写入文件
FILE *wfp = NULL;
if((wfp = fopen("./usr.txt", "a")) == NULL) //以追加的方式打开文件
{
perror("fopen error");
return -1;
}
//将账户和密码写入文件
fprintf(wfp, "%s %s\n", reg_name, reg_pwd);
//关闭文件
fclose(wfp);
printf("注册成功\n");
return 0;
}

//定义登录功能
int do_login()
{
//定义容器存储登录账户和密码
char log_name[20] = "";
char log_pwd[20] = "";
char name[20] = ""; //存储从文件中读取下来的账户
char pwd[20] = ""; //存储从文件中读取下来的密码
//提示并输入登录账户和密码
printf("请输入登录账户:");
fgets(log_name, sizeof(log_name), stdin);
log_name[strlen(log_name)-1] = '\0'; //将字符串最后的换行符换成结束符号
printf("请输入登录密码:");
fgets(log_pwd, sizeof(log_pwd), stdin);
log_pwd[strlen(log_pwd)-1] = '\0';
//打开文件
FILE *rfp = NULL;
if((rfp = fopen("./usr.txt", "r")) == NULL)
{
perror("fopen error");
return -1;
}
//将登录账户和密码跟文件中所有账户和密码进行匹配
while(1)
{
//将每一行的账户和密码读取出来
int res = fscanf(rfp, "%s %s", name, pwd);
if(res == EOF) //说明全部都试一遍了,他也没有成功
{
break;
}
//将读取出来的账户和密码跟登录账户和密码进行匹配
if(strcmp(name, log_name)==0 && strcmp(pwd, log_pwd)==0)
{
printf("登录成功\n");
//关闭文件
fclose(rfp);
return 1;
}
}
//关闭文件
fclose(rfp);
printf("登录失败, 请重新登录\n");
return 0;
}

/*****************主程序************/
int main(int argc, const char *argv[])
{
int menu = 0; //存储菜单选项变量
while(1)
{
system("clear"); //创建子进程调用终端指令
printf("\t\t======XXX 登录界面=========\n");
printf("\t\t======1、注册==========\n");
printf("\t\t======2、登录==========\n");
printf("\t\t======0、退出==========\n");
printf("请输入功能选项:");
scanf("%d", &menu);
getchar(); //吸收一下scanf留下的回车
switch(menu)
{
case 1:
{
//注册
do_register();
}
break;
case 2:
{
//登录
int res = do_login();
if(res == 1)
{
printf("登录成功后的界面\n");
//此处省略一万行程序代码
}
}
break;
case 0:exit(EXIT_SUCCESS); //退出进程
default:printf("您输入的功能有误,请重新输入!!!!!\n");
}
printf("请输入任意键按回车清屏\n");
while(getchar() != '\n');
}
return 0;
}

格式串转字符串:sprintf/snprintf

printf是向终端打印一个格式串,fprintf是向外部文件中打印一个格式串。有时候,想要将多个不同数据类型的数据,组成一个字符串放入字符数组中,此时就可以使用sprintfsnprintf

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sprintf(char *str, const char *format, ...);
功能:将指定的格式串转换为字符串,放入字符数组中
参数1:字符数组的起始地址
参数2:格式串,可以包含多个格式控制符
参数3:可变参数
返回值:成功返回转换的字符个数,失败返回EOF
对于上述函数而言,用一个小的容器去存储一个大的转换后的字符时,会出现指针越界的段错误,为了安全起见,引入了snprintf

int snprintf(char *str, size_t size, const char *format, ...);
功能:将格式串中最多size-1个字符转换为字符串,存放到字符数组中
参数1:字符数组的起始地址
参数2:要转换的字符个数,最多为size-1
参数3:格式串,可以包含多个格式控制符
参数4:可变参数
返回值:如果转换的字符小于size时,返回值就是成功转换字符的个数,如果大于size则只转换
size-1个字符,返回值就是size,失败返回EOF

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
char buf[10] = ""; //定义一个字符串容器
//需求:将多种数据类型的数据连接成一个字符串,放入字符数组中
//sprintf(buf, "%d %s %lf", 1001, "zhangsanfeng", 99.5);
//安全起见,我们使用snprintf
snprintf(buf, sizeof(buf), "%d %s %lf", 1001, "zhangsanfeng", 99.5);
//输出转换后的字符串
printf("buf = %s\n", buf); //1001 zhangsanfeng 99.500000
return 0;
}

模块化读写:fread/fwrite

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:从stream指向的文件中,读取nmemb项数据,每一项的大小为size,将整个结果放入ptr指
向的容器中
参数1:容器指针,是一个void*类型,表示可以存储任意类型的数据
参数2:要读取数据每一项的大小
参数3:要读取数据的项数
参数4:文件指针
返回值:成功返回nmemb,就是成功读取的项数,失败返回小于项数的值,或者是0

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:向stream指向的文件中写入nmemb项数据,每一项的大小为size,数据的起始地址为ptr
参数1:要写入数据的起始地址
参数2:每一项的大小
参数3:要写入的总项数
参数4:文件指针
返回值:成功返回写入的项数,失败返回小于项数的值,或者是0

字符串的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
int main(int argc, const char *argv[])
{
// 定义文件指针,以只写的形式打开文件
FILE *fp = NULL;
if ((fp = fopen("./test.txt", "w")) == NULL)
{
perror("fopen error");
return -1;
}
// 定义要存储的字符串
char wbuf[128] = "";
while (1)
{
// 提示从终端上输入字符串
printf("请输入>>>");
fgets(wbuf, sizeof(wbuf), stdin); // 从标准输入中读取数据
// wbuf[strlen(wbuf)-1] = 0; //将换行换成字符串结束标志
// 判断输入的是否为 quit
if (strcmp(wbuf, "quit\n") == 0)
{
break;
}
// 将字符串写入文件项
fwrite(wbuf, strlen(wbuf), 1, fp); // 将字符串以每个字符串为单位写入文件,每次写一项
// fwrite("\n", strlen("\n"), 1, fp); //每输入一个字符串加上换行
// 刷新缓冲区
fflush(fp);
printf("录入成功\n");
}
// 关闭文件
fclose(fp);

// 再次以只读的形式打开文件
if ((fp = fopen("./test.txt", "r")) == NULL)
{
perror("fopen error");
return -1;
}
char rbuf[10] = "";
int res = fread(rbuf, 1, sizeof(rbuf), fp); // 从文件中读取一个字符串
fwrite(rbuf, 1, sizeof(rbuf), stdout); // 向标准输出中写入数据
// 关闭文件
fclose(fp);
return 0;
}

整数的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
int main(int argc, const char *argv[])
{
// 定义文件指针,以只写的形式打开文件
FILE *fp = NULL;
if ((fp = fopen("./test.txt", "w")) == NULL)
{
perror("fopen error");
return -1;
}
// 定义要写入文件中的数据
int num = 16;
// 将数据写入文件中
fwrite(&num, sizeof(num), 1, fp); // 将num中的数据写入fp指向的文件中
// 关闭文件
fclose(fp);
// 再次以只读的形式打开文件
if ((fp = fopen("./test.txt", "r")) == NULL)
{
perror("fopen error");
return -1;
}
// 定义接收数据的变量
int key = 0;
// 从文件中读取数据
fread(&key, sizeof(key), 1, fp);
// 关闭文件
fclose(fp);
printf("key = %d\n", key);
return 0;
}

结构体数据的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
// 定义一个学生信息
class Stu
{
public:
char name[20]; // 姓名
int age; // 年龄
double score; // 成绩
};
int main(int argc, const char *argv[])
{
// 定义文件指针,以只写的形式打开文件
FILE *fp = NULL;
if ((fp = fopen("./test.txt", "w")) == NULL)
{
perror("fopen error");
return -1;
}
// 定义三个学生
Stu s[3] = {{"张三", 18, 98},
{"李四", 20, 88},
{"王五", 16, 95}};
// 将三个学生信息写入文件中
fwrite(s, sizeof(Stu), 3, fp);
// 关闭文件
fclose(fp);
// 再次以只读的形式打开文件
if ((fp = fopen("./test.txt", "r")) == NULL)
{
perror("fopen error");
return -1;
}
// 定义一个对象,接收读取的结果
Stu temp;
// 从文件中读取一个学生的信息
fread(&temp, sizeof(Stu), 1, fp);
// 将读取的数据展示出来
printf("name:%s, age:%d, score:%.2lf\n", temp.name, temp.age, temp.score);
// 关闭文件
fclose(fp);
return 0;
}

文件光标操作:fseek/ftell/rewind

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int fseek(FILE *stream, long offset, int whence);
功能:移动文件光标位置,将光标从指定位置处进行前后偏移
参数1:文件指针
参数2:偏移量
>0:表示从指定位置向后偏移n个字节
<0:表示从指定位置向前偏移n个字节
=0:在指定位置处不偏移
参数3:偏移的起始位置
SEEK_SET:文件起始位置
SEEK_CUR:文件指针当前位置
SEEK_END:文件结束位置
返回值:成功返回0,失败返回-1并置位错误码

long ftell(FILE *stream);
功能:获取文件指针当前的偏移量(字节数)
参数:文件指针
返回值:成功返回文件指针所在的位置,失败返回-1并置位错误码
eg:
fseek(fp, 0, SEEK_END); //将光标定位在结尾
ftell(fp); //该函数的返回值,就是文件大小

void rewind(FILE *stream);
功能:将文件光标定位在开头:fseek(fp, 0, SEEK_SET);
参数:文件指针
返回值:无

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
// 定义一个学生信息
class Stu
{
public:
char name[20]; // 姓名
int age; // 年龄
double score; // 成绩
};
int main(int argc, const char *argv[])
{
// 定义文件指针,以只写的形式打开文件
FILE *fp = NULL;
if ((fp = fopen("./test.txt", "w+")) == NULL)
{
perror("fopen error");
return -1;
}
// 定义三个学生
Stu s[3] = {{"张三", 18, 98},
{"李四", 20, 88},
{"王五", 16, 95}};
// 将三个学生信息写入文件中
fwrite(s, sizeof(Stu), 3, fp);
// 求出文件的大小
printf("此时文件的大小为:%ld\n", ftell(fp)); // 文件大小
// 将光标移动到开头位置
// fseek(fp, 0, SEEK_SET);
// 将光标直接定位到第二个学生信息前,但是此时光标在最后
// fseek(fp, sizeof(Stu), SEEK_SET); //将光标从开头后移一个学生空间的内容
fseek(fp, -sizeof(Stu) * 2, SEEK_CUR); // 将光标从当前位置向前偏移两个学生空间的内容
// 定义一个对象,接收读取的结果
Stu temp;
// 从文件中读取一个学生的信息
fread(&temp, sizeof(Stu), 1, fp);
// 将读取的数据展示出来
printf("name:%s, age:%d, score:%.2lf\n", temp.name, temp.age, temp.score);
// 关闭文件
fclose(fp);
return 0;
}

文件IO

​ 通过系统调用(内核提供的函数)实现,只要使用的文件IO接口,那么进程就会从用户空间内核空间进行一次切换。标准IO的实现中,也是在内部调用了文件IO操作。该操作效率较低,因为没有缓冲区的概念。文件IO常用的操作:openclosereadwritelseek

文件描述符(File Descriptor,FD)

1> 文件描述符(FILE结构体里有提到)的本质是一个大于等于0的整数,在使用open函数打开文件时,就会产生一个用于操作文件的句柄,。文件描述符是操作系统给打开的文件 / 设备分配的「整数编号」,程序通过这个编号来间接操作文件 / 设备,而非直接操作硬件。

2> 在一个进程中,能够打开的文件描述符是有限制的,一般是1024个即[0,1023],可以通过指令 ulimit -a进行查看,如果要更改这个限制,可以通过指令 ulimit -n 数字,进行更改

3> 文件描述符的使用原则一般是最小未分配原则

4> 特殊的文件描述符:0、1、2,这三个文件描述符在一个进程启动时就默认被打开了,分别表示标准输入、标准输出、标准错误

5> 文件描述符不是只针对磁盘文件,而是操作系统统一的 IO 抽象.这也是 Unix/Linux 「一切皆文件」思想的核心 ——所有 IO 操作都通过文件描述符统一处理

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[])
{
//分别输出标准输入、标准输出、标准出错文件指针对应的文件描述符
printf("stdin->_fileno = %d\n", stdin->_fileno); //0
printf("stdout->_fileno = %d\n", stdout->_fileno); //1
printf("stderr->_fileno = %d\n", stderr->_fileno); //2
return 0;
}

效果图:

打开文件:open

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:打开或创建一个文件,并返回该文件的文件描述符,返回文件描述符的规则是最小未分配原则
参数1:要打开的文件文件路径
参数2:打开模式
以下三种方式必须选其一:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)
以下的打开方式可以有零个或多个,跟上述的方式一起用位或运算连接到一起
O_CREAT:用于创建文件,如果文件不存在,则创建文件,如果文件存在,则打开文件,如果flag中包含了该模式,则函数的第三个参数必须要加上
O_APPEND:以追加的形式打开文件,光标定位在结尾
O_TRUNC:清空文件
O_EXCL:常跟O_CREAT一起使用,确保本次要创建一个新文件,如果文件已经存在,则open函数报错
//windows下,需要加上 | O_BINARY(二进制打开),核心作用是「禁用文本模式的自动转换」
eg:
"w": O_WRONLY|O_CREAT|O_TRUNC
"w+": O_RDWR|O_CREAT|O_TRUNC
"r": O_RDONLY
"r+": O_RDWRx
"a": O_WRONLY|O_CREAT|O_APPEND
"a+": O_RDWR|O_CREAT|O_APPEND
参数3
如果参数2中的flag中有O_CREAT时,表示创建新文件,参数3就必须给定,表示新创建的文件的权限
如果当前参数给定了创建的文件权限,最终的结果也不一定是参数3的值,系统会用你给定的参数3的值,与系统的umask取反的值进行位与运算后,才是最终创建文件的权限(mode & ~umask)
当前终端的umask的值,可以通过指令 umask 来查看,一般默认为为 0022,表示当前进程所在的组中对该文件没有写权限,其他用户对该文件也没有写权限
当前终端的umask的值是可以更改的,通过指令:umask 数字进行更改,这种方式只对当前终端有效
普通文件的权限一般为:0644,表示当前用户没有可执行权限,当前组中其他用户和其他组中的用户都只有读权限
目录文件的权限一般为:0755,表示当前用户具有可读、可写、可执行,当前组中其他用户和其他组中的用户都没有可写权限
注意:如果不给权限,那么当前创建的权限会是一个随机值
返回值:成功返回打开文件的文件描述符,失败返回-1并置位错误码

​ 用 O_BINARY 打开文本文件,不会损坏文件,只是改变了系统对「换行符」的处理规则:

打开方式 对文本文件的影响 示例(写入 \n
不加 O_BINARY(文本模式) Windows 自动把 \n(0x0A)转换成 \r\n(0x0D+0x0A),适配 Windows 记事本的换行规则 写入 \n → 实际存储 \r\n
O_BINARY(二进制模式) 禁用换行符转换,写入什么字节,就存储什么字节,完全原样保存 写入 \n → 实际存储 \n

文件权限

四位数字的格式是:[特殊权限位][所有者权限][所属组权限][其他人权限]

  • 第 1 位:特殊权限位(取值 0-7,对应 SUID/SGID/ 粘滞位的组合)
  • 第 2 位:所有者(u)基础权限(rwx,取值 0-7)
  • 第 3 位:所属组(g)基础权限(rwx,取值 0-7)
  • 第 4 位:其他人(o)基础权限(rwx,取值 0-7)

特殊权限位:

数字 二进制 对应特殊权限 含义
0 000 无特殊权限(最常见,所以第一位是 0)
1 001 粘滞位(Sticky) 仅目录所有者可删除目录内文件(如 /tmp
2 010 SGID 执行文件时继承所属组权限;目录下新建文件继承目录所属组
4 100 SUID 执行文件时继承文件所有者权限(如 passwd 命令)
3 011 SGID + 粘滞位 组合权限
5 101 SUID + 粘滞位 组合权限
6 110 SUID + SGID 组合权限
7 111 三者都有 组合权限

「所有者 / 所属组 / 其他人」基础权限:

权限符号 二进制 八进制数字 含义
000 0 无权限
–x 001 1 仅执行
-w- 010 2 仅写入
-wx 011 3 写入 + 执行
r– 100 4 仅读取
r-x 101 5 读取 + 执行
rw- 110 6 读取 + 写入
rwx 111 7 读 + 写 + 执行(全权限)

文件关闭:close

原型

1
2
3
4
5
#include <unistd.h>
int close(int fd);
功能:关闭文件描述符对应的文件
参数:文件描述符
返回值:成功返回0,失败返回-1并置位错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<myhead.h>
int main(int argc, const char *argv[])
{
//1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
int fd = -1;
//以只读的形式创建文件,如果文件不存在则创建文件
//如果创建文件时没有给权限,则该文件的权限是随机权限
//如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
if((fd = open("./tt.txt", O_WRONLY|O_CREAT, 0644)) == -1)
{
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //3,由于0、1、2已经被使用,所以该数为3
//关闭文件
close(fd); //关闭fd引用的文件
return 0;
}

读写操作:read、write

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:从fd文件描述符引用的文件中读取count的字符放入buf对应的容器中
参数1:已经打开文件对应的文件描述符
参数2:容器的起始地址
参数3:要读取的字符个数
返回值:成功返回读取的字符个数,这个个数可能会小于count的值,失败返回-1并置位错误码


#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
功能:将buf容器中的count个数据,写入到fd引用的文件中
参数1:已经打开的文件的文件描述符
参数2:要写入的数据的起始地址
参数3:要写入数据的个数
返回值:成功返回写入的字符个数,这个个数可能小于count,失败返回-1并置位错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<myhead.h>
int main(int argc, const char *argv[])
{
//1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
int fd = -1;
//以只读的形式创建文件,如果文件不存在则创建文件
//如果创建文件时没有给权限,则该文件的权限是随机权限
//如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
if((fd = open("./tt.txt", O_WRONLY|O_CREAT, 0644)) == -1)
{
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //3,由于0、1、2已经被使用,所以该数为3
//对数据进行读写操作
char wbuf[128] = "hello world";
//将上述字符串写入文件中
write(fd, wbuf, strlen(wbuf));
//关闭文件
close(fd); //关闭fd引用的文件
//再次以只读的形式打开文件,此时参数2中不需要使用O_CREAT,那么第三个参数也不需要了
if((fd = open("./tt.txt", O_RDONLY)) == -1)
{
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //3
//定义接收数据容器
char rbuf[5] = "";
int res = read(fd, rbuf, sizeof(rbuf)); //从文件中读取数据放入rbuf中
write(1, rbuf, res); //向1号文件描述符中写入数据,之前读取多少,现在写入多少
//关闭文件
close(fd);
return 0;
}

关于光标的操作:lseek

原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能:移动光标的位置,并返回光标现在所在的位置
参数1:文件描述符
参数2:偏移量
>0:表示从指定位置向后偏移n个字节
<0:表示从指定位置向前偏移n个字节
=0:在指定位置处不偏移
参数3:偏移的起始位置
SEEK_SET:文件起始位置
SEEK_CUR:文件指针当前位置
SEEK_END:文件结束位置
返回值:光标现在所在的位置
注意: lseek = fseek+ftell

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<myhead.h>
int main(int argc, const char *argv[])
{
//1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
int fd = -1;
//以读写的形式创建文件,如果文件不存在则创建文件,如果文件存在则清空文件
//如果创建文件时没有给权限,则该文件的权限是随机权限
//如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
if((fd = open("./tt.txt", O_RDWR|O_CREAT|O_TRUNC, 0644)) == -1)
{
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //3,由于0、1、2已经被使用,所以该数为3
//对数据进行读写操作
char wbuf[128] = "hello world";
//将上述字符串写入文件中
write(fd, wbuf, strlen(wbuf));
//此时文件光标是在文件的末尾位置
//需求是:读取文件中的 world
//lseek(fd, 6, SEEK_SET); //从文件开头位置向后偏移6个字节
lseek(fd, -5, SEEK_END); //从文件结束位置向前偏移5个字节
//定义接收数据容器
char rbuf[5] = "";
int res = read(fd, rbuf, sizeof(rbuf)); //从文件中读取数据放入rbuf中
write(1, rbuf, res); //向1号文件描述符中写入数据,之前读取多少,现在写入多少
//关闭文件
close(fd);
return 0;
}

改图案例

​ 将一个bmp格式的图片信息读取出来,并将该图片的相关内容(颜色)进行更改

​ 关于bmp图像格式可以参考:https://upimg.baike.so.com/doc/5386615-5623077.html

​ 如何将windows下的文件,复制到linux系统的Centos下

1
2
3
1、在windows下调出cmd窗口
2、输入指令:
scp 要拷贝的文件路径 linux下的用户名@ip地址:linux下存放的地址路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<myhead.h>
int main(int argc, const char *argv[])
{
//以读写的形式打开文件
int fd = -1;
if((fd = open("./wukong.bmp", O_RDWR)) == -1)
{
perror("open error");
return -1;
}
//获取文件的大小
printf("文件大小为:%ld\n", lseek(fd, 0, SEEK_END)); //输出的就是文件大小
//定义变量存储文件大小
int pic_size = 0;
lseek(fd, 2, SEEK_SET); //将文件从起始位置向后偏移两个字节,跳过文件类型
read(fd, &pic_size, 4); //将文件头的 3--6字节的内容读取出来
printf("pic_size = %d\n", pic_size); //文件的大小
//将文件光标向后偏移54字节,跳过文件头和信息头
lseek(fd, 54, SEEK_SET);
//定义一像素的颜色:颜色规律是蓝绿红
unsigned char color[3] = {0, 0, 255}; //定义一个绿色
for(int i=0; i<100; i++) //以像素为单位遍历行数,遍历前100行
{
for(int j=0; j<684; j++) //遍历所有列数
{
//此时的 (i,j) 定位的就是一个像素点,是一个三字节为单位的颜色点
write(fd, color, sizeof(color)); //将当前光标所在位置的像素点变成绿色
}
}
//关闭文件
close(fd);
return 0;
}

拷贝案例

​ 使用文件IO来完成两个文件的拷贝,可以完成两个图像的拷贝工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#define BUF_SIZE 4096

int main(int argc, const char *argv[])
{
int input_fd = -1;
//注意必须二进制打开文件,不然会无法复制成功,具体影响见下表
if ((input_fd = open("cat.bmp", O_RDONLY | O_BINARY)) == -1)
{
perror("open input file failed");
return -1;
}
int output_fd = -1;
if ((output_fd = open("cat_copy2.bmp", O_RDWR | O_CREAT | O_TRUNC | O_BINARY, 0644)) == -1)
{
perror("open output file error");
close(input_fd); // 失败时关闭已打开的源文件,避免泄漏
return -1;
}
char buf[BUF_SIZE] = "";
int read_bytes = 0;
while ((read_bytes = read(input_fd, buf, sizeof(buf))) > 0)
{
int write_len = 0;
if ((write_len = write(output_fd, buf, read_bytes)) == -1 || write_len != read_bytes)
{
perror("write error");
break;
}
}

if (read_bytes == -1)
{
perror("read input file error");
return -1;
}

close(input_fd);
close(output_fd);
printf("拷贝成功!");
return 0;
}
场景 关键差异 对 BMP 的影响 视觉结果
都用 O_BINARY 无任何数据转换 / 截断 完整保留二进制结构 和原图一致 ✅
output 文本模式 写入时插入 0x0D,增加字节数 像素偏移、图像撕裂 整体偏移 📏
input 文本模式 读取时遇到 0x1A 提前终止 数据截断,丢失上部内容 大面积全透明/黑屏

补充操作建议

追代码工具ctags

  1. 安装ctags:sudo yum install ctags
  2. 进入 usr/incldue 目录(或其他需要查询的目录)
  3. 执行sudo ctags -R指令,会在当前目录生成一个名为 tags 的文件,里面记录了代码中所有「函数、变量、类、宏」的定义位置(文件名 + 行号)。
  4. 追寻相关内容:vi/vim -t 内容
  5. 编辑器中快速跳转(Vim 中最常用):
    • 生成 tags 文件后,在 Vim 中打开代码,光标移到函数名上,按 Ctrl + ] 直接跳转到定义处;
    • Ctrl + t 跳回原来的位置;
    • 输入 :tag 函数名 直接定位到指定函数的定义。

vsc中自定义代码片段设置

  1. 在vs code中使用组合键ctrl+sift+p调出控制面板

  2. 输入user snippets

  3. 输入自定义片段文件名称打开自定义文件

  4. 输入程序内容

    案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"Simple C++ Program": {
"scope": "cpp",
"prefix": "main",
"body": [
"#include <iostream>",
"using namespace std;",
"",
"int main(int argc, char* argv[])",
"{",
"",
"\treturn 0;",
"}"
],
"description": "A simple C++ program with a main function that returns 0."
}

建立myhead.h(Linux)

  1. 创建一个头文件,例如 myhead.h
  2. 将能用到的所有头文件都放入该文件中
  3. myhead.h文件,移动到根目录下的usr目录中的include目录下sudo mv myhead.h /usr/include
  4. 常用的头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream> //C++标准输入输出流

#include<string> //C++字符串类对象

#include<iomanip> //C++中的格式化输入输出

#include<stdio.h> //C语言的输入输出头文件

#include<math.h> //C语言的数据函数头文件

#include<string.h> //C语言字符串处理头文件

#include<stdlib.h> //C语言库头文件
  1. 重新设置用户的自定义片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"Simple C++ Program": {
"scope": "cpp",
"prefix": "main",
"body": [
"#include <myhead.h>",
"using namespace std;",
"",
"int main(int argc, char* argv[])",
"{",
"",
"\treturn 0;",
"}"
],
"description": "A simple C++ program with a main function that returns 0."
}