【Linux】Linux下的标准I/O和文件I/O


I/O其实就是input和output的简称,它代表着输入和输出。当然,输入输出包括在控制台进行输入输出,也包括着对文件进行输入输出。

标准I/O

标准IO是由ANSI C标准定义的,直观感受就是在引入头文件时,使用的是#include<stdio.h>
可以通过缓冲机制来减少系统调用,实现更高的效率(系统调用:对文件的操作,都是先通过编程语言调用系统的接口,再由系统接口对底层硬件进行操作。)。同时,标准IO是有缓存机制的(首先会通过系统调用读取一批数据到缓冲区,应用先是从缓冲区中进行数据读取的,只有当缓冲区已空时,才会进行系统调用来再读取一批数据)

文件类型

在linux系统下,文件类型可以被分为
常规文件			->				r
普通文件 		->				-
目录文件			->				d
字符设备文件		->				c
块设备文件		->				b
管道文件			->				p
套接字文件		->				s
符号链接文件		->				|

标准IO-流

FILE 是一个结构体类型,用来存放打开的文件相关信息,标准IO的所有操作都是基于FILE进行的。

流(stream),FILE又被称为流,可以分为文本流和二进制流。

流的缓冲类型
全缓冲:缓冲区中无数据或无空间或者关闭流时才执行实际的IO操作(默认打开的文件都属于此类)
行缓冲:输入输出中遇到换行符时,进行IO操作(流与终端关联时就是典型的行缓冲)
无缓冲:不进行缓冲,数据直接读写。
当我们打开一个FILE流指针时,标准IO会预定义并且打开3个流:标准输入流,标准输出流,标准错误流。

流的打开和关闭

FILE* fp;
if((fp = fopen("filepath", "打开方式"))==NULL);//打开失败会返回NULL,因此要进行判断
{
	perror("fopen");//由perror对打开文件失败进行说明
	return -1;
}
//文件操作
fclose(fp);//当流关闭时,会自动刷新缓冲区中的数据,并且释放缓冲区

错误信息处理

extern int errno;//存放错误编号
char* strerror(int errno);//根据错误编号返回对应的错误信息
void perror(const char*s);//先输出字符串,再输出错误号对应的信息
//错误信息处理1
FILE* fp
if((fp = fopen("filepath", "打开方式"))==NULL);//打开失败会返回NULL,因此要进行判断
{
	perror("fopen");//由perror对打开文件失败进行说明
	return -1;
}
//文件操作
fclose(fp);

//错误信息处理2
FILE* fp
if((fp = fopen("filepath", "打开方式"))==NULL);//打开失败会返回NULL,因此要进行判断
{
	printf("fopen:%s\n", strerror(errno));
	return -1;
}
//文件操作
fclose(fp);

按字符输入输出

读写一个字符:fgetc,fputc, getc,putc
读写一个标准输入输出字符:getchar, puchar

利用fgetc和fputc实现文件复制

#include<stdio.h>

int main()
{
	FILE* fps, *fpd;
	int ch;
	if((fps = fopen("source.txt", "r"))==NULL)//打开源文件
	{	
		perror("fopen fps");
		return -1;
	}
	if((fpd = fopen("dest.txt", "r+"))==NULL)//打开目标文件
	{
		perror("fopen fpd");
		return -1;
	}
	while((ch = fgetc(fps)) != EOF)//判断是否到文件末尾
	{
		fputc(ch, fpd);
	}
	fclose(fps);
	fclose(fpd);
	return 0;
}

按行输入输出

s为缓冲区,size一般为缓冲区的大小,从流中读取数据时,若遇到换行符,会在换行符后追加’\0’,不再向后读取。若一直没有遇到换行符,缓冲区中会存储size-1个字符,最后一个位置追加’\0’.

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

统计文本文件包含多少行

#include<stdio.h>
int main()
{	
	char buf[6];
	FILE* fp;
	int line = 0;
	int ch;
	if((fp = fopen("filename", "r")) == NULL)
	{
		perror("fopen");
		return -1;
	}
	while(fgets(buf, 6, fp) != NULL)
	{
		if(buf[strlen(buf)-1] == '\n')
		{
			line++;
		}
	}
	printf("%d ", line);
	fclose(fp);
}

按对象读写

fread,fwrite

FILE* fp;
fp = fopen("student.txt", "w");
struct student
{
	char name[10];
	int age;
	float sorce;
}s[] = {{zhangsan, 18, 89.9},{lisi, 19, 90.2}};

fwrite(s, sizeof(struct student), 2, fp);
fclose(fp);

实现文件复制

#include<stdio.h>
#define N 64
int main()
{
	FILE* fps,*fpd;
	char buf[N];
	int n;
	if((fps = fopen("source.txt", "r")) == NULL)
	{
		perror("fopen fps");
		return -1;
	}
		if((fpd = fopen("dest.txt", "r")) == NULL)
	{
		perror("fopen fpd");
		return -1;
	}
	while((n = fread(buf, 1, N, fps)) > 0)//fread读取的返回值为读取元素个数
	{
		fwrite(buf, 1, n, fpd);//将读取的元素个数写入到fpd流中
	}
	fclose(fps);
	fclose(fpd);
	
	return 0;
}

流的刷新和定位

流的刷新
当缓冲区满时,或者遇到换行符时,流的缓冲区会刷新->写到实际的文件中
当关闭流时,缓冲区会刷新->写到实际的文件中
通过fflush函数,强制刷新缓冲区
int fflush(FILE* fp);成功返回0,失败返回-1,在Linux下,只能刷新输出缓冲区

流的定位
当一个流打开的时候,内部有一个读写位置pos,我们在对流进行定位时,实际上就是在定位这个pos
*打开流时,pos=0
*每当读写一个位置,pos会自动加1

ftell(FILE* stream);//成功时,返回流当前的读写位置
fseek(FILE* stream, long offset, int whence);//定位到流的某个位置
rewind(FILE* stream);//定位到流的起始位置
//whence为基准位置,参数有:SEEK_SET/SEEK_CUR/SEEK_END(分别代表文件起始,当前,末尾)
//offset为偏移量。

格式化输出

例:以指定格式年-月-日写入文件和缓冲区

int year, month, day;
FILE* fp;
char buf[64];

year = 2023;month = 1;day = 19;
fp = fopen("test.txt", "a+");
fprintf(fp, "%d-%d-%d\n", year, month, day);
sprintf(buf, "%d-%d-%d\n", year, month,day);

例:每隔1秒向文件test.txt写入系统当前时间
1,2022-12-11 15:21:31
2, 2022-12-11 15:21:32
无线循环,直到按ctrl+C结束,每次执行程序,系统时间追加到文件末尾,序号递增

#include<stdio.h>
#include<unistd.h> //sleep
#include<time.h>   //time localtime
#include<string.h>

int main()
{
  FILE* fp;
  int line = 0;
  char buf[64];
  time_t t;
  struct tm* tp;

  if((fp = fopen("timetofile.txt", "a+"))==NULL)//打开文件fp
  {
    perror("fopen");
    return -1;
  }
  while(fgets(buf, 64, 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);//按规定格式将tp写入fp流
    fflush(fp);//强制刷新缓冲区
    sleep(1);//延时1秒
  }
  return 0;
}

文件I/O

标准IO与文件IO的区别

上面我们说到,标准IO遵循的是ANSI C的标准,明显的特点是头文件#include<stdio.h>而文件IO遵循的是POSIX标准,头文件为#include<unistd.h>,除此之外,文件IO并没有缓冲机制,并且是通过一个文件描述符来表示一个打开的文件。可以访问linux中所有类型的文件。

文件描述符
每打开一个文件,都有一个对应的文件描述符,它是一个非负整数,从0开始,依次分配,各个程序的文件描述符互不影响。

open函数

int open(const char* path, int oflag, ...)//oflag为打开方式,
//新建文件会使用第三个参数,指定文件权限,
//打开成功返回的是对应的文件描述符,失败返回EOF

在这里插入图片描述
例:只写方式打开1.txt,如果不存在则创建,如果存在则清空

int fd;
if((fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 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函数
与fclose相同,int close(int fd);成功返回0,失败返回-1

read函数

ssize_t read(int fd, void *buf, size_t count);//从fd中读取数据,放到buf中,放count个

读取成功返回读取的字节数,出错返回EOF,读到文件末尾返回0

用read从指定文件中读取内容,并统计大小

    1 #include<unistd.h>
    2 #include<stdio.h>
    3 #include<fcntl.h>
    4 int main()
    5 {
    6   int fd, total,n;
    7   char buf[64];
    8   if((fd = open("1.txt", O_RDONLY )) < 0)                                                    
    9   {
   10     perror("open");
   11     return -1;
   12   }
   13 
   14   while((n = read(fd, buf, 64)) > 0)
   15   {
W> 16     total += n;
   17   }
   18   printf("total = %d\n", total);
   19   close(fd);
   20   return 0;
   21 }

write函数

ssize_t write(int fd, void *buf, size_t count);//向fd文件中写入,通过buf写入count个字节
//成功写入返回写入字节数,出错返回EOF
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
int main()
{
  int fd;
  char buf[20];
  if((fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC)) < 0)
  {
    perror("open");
    return -1;
  }
  while(fgets(buf, 20, stdin)!=NULL)
  {
    if(strcmp(buf, "quit\n")==0) break;
    write(fd, buf, strlen(buf));
  }
  close(fd);
  return 0;
}

lseek函数
lseek函数与fseek函数非常相似,仅仅是第一个变量由FILE流换为文件描述符

用文件IO实现文件的复制

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

#define N 64
int main()
{
  int fds, fdt;
  int n;
  char buf[N];
  if((fds = open("1.txt", O_RDONLY)) == -1)
  {
    fprintf(stderr, "fds open %s\n", strerror(errno));
  }

  if((fdt = open("2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666)) == -1)
  {
    fprintf(stderr, "fdt open %s\n", strerror(errno));
  }
  while((n = read(fds, buf, N)) > 0)
  {
    write(fdt, buf, n);
  }
  close(fds);
  close(fdt); 

  return 0;
}

读取目录

打印指定目录下的所有文件名称

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

int main()
{
  DIR* dirp;
  struct dirent* dp;
  if((dirp = opendir("opendir")) == NULL)
  {
    perror("opendir");
    return -1;
  }
  while((dp = readdir(dirp)) != NULL)
  {
    printf("%s\n" , dp->d_name);
  }
  closedir(dirp);
  return 0;
}

修改文件权限chmod

首先,我们输入ll可以显示当前目录下文件的详细信息,如下图所示:
在这里插入图片描述

第一个红色的框表示当前文件类型,-表示普通文件
在这里插入图片描述
第二个框表示当前文件的读写权限(针对文件所有者来说),rwx分别表示可读权限,可写权限,可执行权限。-表示没有权限

第三个框表示相同意思(但是针对文件所属组的其他用户来说的)

第四个框表示也是相同意思(针对组外用户来说的),第四和第五个框之间的1表示文件的硬链接计数

第五个框和第六个框表示文件所属用户和所属组

第七个框表示文件大小(k为单位)

最后两个框表示的是文件最后修改的时间和文件名称

那么主要针对文件权限来说,
在这里插入图片描述

我们可以这样来看,比如针对文件所有者来说,看第一个框,如果r的位置为-,那么对应二进制为0,r位置不为-,对应二进制为1,以此类推,可以产生3位二进制位,而这样的表示刚好可以由一个8进制数组代替,从而也叫8进制表现形式,因此三个框框,共有3个8进制数字来表示其所有的权限。

那么对于文件的权限我们是怎样进行修改的呢?
chmod函数
chmod u+r 1.txt 表示给文件1.txt的所有者增加可读权限
chmod g-w 1.txt 表示给文件1.txt的组内用户减去可写权限
chmod o+x 1.txt 表示给文件1.txt的其他用户增加可执行权限
这样就可以修改文件的权限了。
但是为了防止由于某个用户拥有了当前目录的可写权限后,可以删除目录下由其他用户所创建的文件我们引入粘滞位(注:拥有目录的可写权限后就可以对目录下的文件进行增加和删除

粘滞位

为了解决上述的问题,我们使用chmod +t <filename>这样的操作,使得即使拥有目录的可写权限后,也无法删除由其他用户创建的文件。(注:要在root用户下才能对文件增加或者删除沾滞位
在这里插入图片描述
对于这个ds文件而言,它的有可写权限(针对其他用户而言),这样会导致我在这个目录下创建的文件可以被其他用户删除修改,这样显然是不科学的。因此我们需要设置沾滞位。

chmod +t ds

在这里插入图片描述
可以看到已经修改成功了。

有些友友们可能在输入chmod +t <filename>时出现下面的提示。

在这里插入图片描述
这是因为我们修改权限的文件不属于我们这个用户,那么会报没有权限的错误,我们需要使用sodu来暂时拥有root用户的权限,这样就可以进行修改了。

sudo chmod +t tmp

库的创建和使用

库实际上是一个二进制文件,比如标准c库,线程库…
库一般被存储在lib 或usr/lib中
在linux中,库可以分为静态库和共享库(也被称为动态库)

静态链接库

静态库的特点是:在代码编译(链接)阶段会把静态库中的相关代码复制到可执行文件中。那么在运行时就无需静态库了。因此在程序运行时也无需加载库,运行的速度也会更快,同时也会占用更多的内存。
还有一个重要的点,当静态库发生更新之后,所有使用静态库的代码重新编译(链接)。

静态库的创建
(1)编写静态库源码

#include<stdio.h>
void hello(void)
{
	printf("hello world\n");
	return ;
}

(2)编译gcc -c hello.c
(3)创建静态库ar crs libhello.a hello.o
(4)可以查看库中包含的函数nm libhello.a

静态库的使用
(5)调用静态库

#include<stdio.h>
void hello(void);//需要进行声明
int main()
{
	hello();
	return 0;
}

(6)编译gcc test.c -o test -L. -lhello,注意这里需要增加-L. -lhello,L.表示要增加的搜索路径为当前路径(因为我们没有将库放到lib这样的默认路径下,系统是找不到这个库的),l后紧跟的是要链接的库的名称。
(7)执行代码./test
此时即使我们删除了libhello.a静态库,程序依然可以运行,就是因为库已经被复制一份了。

动态链接库

与静态库的区别
动态库仅仅记录了需要用到哪个函数,并不会复制库中的相关代码,因此占用内存较少,并且可以同时被多个代码调用,并且更新库后也无需重新编译程序。

动态库的创建
(1)编写库的源码

#include<stdio.h>
void hello(void)
{
	printf("hello world\n");
	return ;
}

(2)编译生成目标文件gcc -c -fPIC hello.c -Wall,这样就生成了hello.o
(3)生成hello动态库文件gcc -shared -o libhello.so.1 hello.o
(4)为动态库文件创建符号链接文件ln -s libhello.so.1 libhello.so

动态库的使用
(5)调用动态库(为了省去调用函数时需要进行函数声明,我们可以编写一个头文件,从而调引入头文件)

#include<stdio.h>
#include"hello.h"

int main()
{
	hello();
	return 0;
}

//hello.h
void hello(void);

(6)编译test.c 并链接动态库gcc test.c -o -L. -lhello,这里的hello为动态库名,需要和创建的头文件hello.h同名
(7)运行代码./test
若在运行时提示加载动态库错误,我们可以去系统目录下增加一个配置文件

sudo vim /etc/ld.so.conf.d/my_conf

打开之后,将目前动态库的所在路径添加到文件中,即可

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值