c语言进阶文件操作(上)

Posted 小参宿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c语言进阶文件操作(上)相关的知识,希望对你有一定的参考价值。

🚀write in front🚀
📜所属专栏:> c语言学习
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!


文章目录

👻前言👻:

  在前面的通讯录学习中,我们发现,当程序结束以后,内存空间就会被释放并回收,我们所存的通讯录人的消息就全部消失了。如此,我们无法将数据真正保留下来。
  所以,我们的目的便是希望将数据保留在本地,只有当我们进行删除操作时,才将对应的数据删除掉,即尝试实现数据的持久化。而我们实现数据持久化的方式一般有两种:使用 数据库 或将数据以 文件操作 存放至硬盘中。今天我们就来学习一下文件操作相关知识。

一、文件概述:

1.什么是文件:

  我们通常所说的文件,一般是指存放在我们计算机本地硬盘上的文件。但是在我们的程序设计中,则指的是 程序文件数据文件 两种文件(根据文件功能分类)。

1.1.程序文件

   包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

1.2数据文件

  • 文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章讨论的是数据文件。
  在以前各章所处理数据的输入输出都是以终端为对象的,即从终端加粗样式的键盘输入数据,运行结果显示到显示器上。而今天我们的目的则是将数据信息输入至我们的本地磁盘上,而当我们想要对数据进行操作时,再从本地硬盘进行读取

1.3文件名

1.3.1绝对路径:

  一个文件要有一个唯一的文件标识,以便用户识别和引用。绝对路径的文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\\code\\test.txt
为了方便起见,文件标识常被称为文件名。

1.3.2相对路径:

  如果打开所在文件路径的文件,则不用前面的文件路径
例如: test.txt
  如果要打开所在文件的上一层文件,则在前面+一个 . ./ ;
例如: . ./test.txt
同理,如果是上上一层文件,则+ . ./. ./

二、文件的打开和关闭:

  我们在使用或操作我们的文件之前,首先需要在我们的程序中及将其打开,于是我们就来研究一下文件的打开和关闭方式。

1.文件指针

  缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
  每个文件在被使用时,都会在内存中开辟了一个相应的文件信息区,用来存放文件相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.

例如,VS2022编译环境提供的 stdio.h 头文件中有以下的文件类型申明

此时结构体内的指针指向保存文件信息的成员

  每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量并填充其中的信息 ,使用者不必关心细节。
  一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:

FILE* p;
//定义一个文件指针p

  像这样,我们就能创建出一个文件指针,而接下来就可以使这个文件指针 p 指向某个文件信息区(FILE 类型的结构体变量),并通过该文件信息区中所保存的信息来访问本地硬盘内的文件了。这样,我们通过使用文件指针就可以找到与其相关联的文件了

2.文件的打开与关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件.

fopen 函数的使用方式:

FILE* p = fopen(const char* filename, const char* mod);
  • 其中“ const char* filename ”指文件名(是字符串,文件名即文件标识符**)。

  • 其中“ const char* mod ”指文件打开模式(也是字符串,后面会为大家列出)。

  • 返回值:若成功,返回一个文件指针,若文件打开失败,则返回一个空指针

fclose 函数的使用方式为:

fopen(FILE* strname);
  • 参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流;
  • 返回值:若文件关闭成功,则返回0;
        若文件关闭失败,则返回EOF;
    举个栗子🌰
int main()

	//D:\\\\code\\\\test.txt - 绝对路径
	//
	FILE* pf = fopen("test2.txt", "w");
	if (pf == NULL)
	
		perror("fopen");
		return 1;
	
	else
	
		printf("打开文件成功\\n");
	
	//读文件
	//....

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;

3.文件的打开方式

文件使用方式含义如果指定文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件(会清除文件里的消息)建立一个新的文件
“a”(追加)向文本文件尾添加数据建立一个新的文件
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向一个二进制文件尾添加数据出错
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建议一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

三、文件的顺序读写:

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

1.字符的读与写 fgetc 与 fputc:

字符的读取函数 fputc :

  • 描述 :把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动;
  • 参数 int char :这是要被写入的字符。该字符以其ASCII 值进行传递
  • 参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流;
  • 返回值:该函数以无符号 char 强制转换为 int 的形式(即字符的ASCII值)返回写入的字符,如果发生错误则返回 EOF.
int main()

	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//写文件
	/*fputc('a', pf);
	fputc('b', pf);
	fputc('c', pf);
	fputc('d', pf);*/

	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)
	
		fputc(ch, pf);
	

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;

运行结果:

  这就是字符的写入,大家会在该项目找到这个文件。

字符的读取函数 fgetc :

  • 参数 FILE *stream :这是指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流;
  • 返回值:该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误则返回 EOF。

  我们在上面代码的基础下展示文件的读取。

int main()

	FILE* pf = fopen("test.txt", "r");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//读文件
	int ch = fgetc(pf);
	printf("%c\\n", ch);

	ch = fgetc(pf);
	printf("%c\\n", ch);

	ch = fgetc(pf);
	printf("%c\\n", ch);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;

运行结果:

注意:

  • getc函数对于字符的读取,每读取一个字符,该函数内的指针会自动将指向的位置向后移动,指向下一个字符;

组合应用:

int main()

	FILE* pf = fopen("test.txt", "w");  //以只写的方式打开文件
	if (pf == NULL)
	
		perror("fopen");
		return 0;
	
	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)   //向文件写入26个小写英文字母
	
		fputc(ch, pf);
	
	fclose(pf);
	pf = NULL;
	pf= fopen("test.txt", "r");  //以只读的方式打开文件
	if (pf== NULL)
	
		perror("fopen");
		return 0;
	
	while (ch != EOF)   //判断是否读取到文件末尾
	
		ch = fgetc(pf);   //从文件中读取字符
		printf("%c", ch);   //打印读取字符
	
	fclose(pf);
	pf = NULL;
	return 0;

代码结果:

2.文本行的读与写 fgets 与 fputs

fputs读取字符串:

  • 参数 const char *str : 这是一个字符串,有’\\0’结尾。
  • 参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流;
  • 返回值:该函数返回一个非负值,如果发生错误则返回 EOF。
int main()

	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//写文件
	//测试写一行数据
	fputs("hello world\\n", pf);
	fputs("hello bit\\n", pf);

	return 0;

运行结果:

fgets读取字符串:

  • 描述: 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。
  • 参数 char *str :这是指向一个指针,该指针指向了要读取的字符串;
  • 参数 int n : 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度;
  • 参数 FILE *stream :这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流;
  • 返回值:如果成功,该函数返回相同的 str 参数;如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针;如果发生错误,返回一个空指针;

特殊情况:

  • 读取 (n-1) 个字符时,会自动在第n个位置补’\\0’
  • 读取到换行符时,会把’\\n’之前的字符读取出来然后补’\\0’,但是不会读到’\\n’,具体情况在后面的缓冲区部分知识里会提到。此时的’\\n’会在保存在下一个fgets函数的第一个字符
  • 到达文件末尾时,它会停止,具体视情况而定.
int main()

	FILE* pf = fopen("test.txt", "r");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//读文件
	//测试一行数据
	char buf[20] = 0;
	fgets(buf, 20, pf);
	printf("%s", buf);
	fgets(buf, 20, pf);
	printf("%s", buf);
 	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

代码结果:

3.格式化输出函数 fscanf和fprintf:

fprintf的格式化写入:


  其实该函数和printf函数一模一样,只是前面多了一个指向文件的指针! 这里的意思其实是将后面的数据以不同的格式写入指针指向的文件信息中。

struct S

	char name[20];
	int age;
	float score;
;

int main()

	struct S s =  "zhangsan", 20, 95.5 ;
	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//格式化的写入文件
	fprintf(pf, "%s %d %f\\n", s.name, s.age, s.score);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

运行结果:

fscanf的格式化读取:


  一样的,都是多了一个文件指针而已,其他都是一模一样的!这里的意思是将文件指向的文件消息不同格式读出并且保存在不同类型的变量中。

int main()

	struct S s =  0 ;
	FILE* pf = fopen("test.txt", "r");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//格式化的读取文件
	fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
	
	//打印看数据
	printf("%s %d %f\\n", s.name, s.age, s.score);
	
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

运行结果:

4.二进制的输入与输出fwead和fwrite:

二进制的输出fwrite:

  • 描述:把 ptr 所指向的数组中的数据写入到给定流 stream 中;
  • 参数 void (*)ptr :这是指向带有最小尺寸 size*nmemb 字节的内存块的指针;
  • 参数 size_t size :这是要读取的每个元素的大小,以字节为单位;
  • 参数 size_t count :这是元素的个数;
  • 参数 FILE(*)stream : 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流;
  • 返回值:如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。
int main()

	struct S s =  "张三", 20, 98.5;
	FILE* pf = fopen("test.txt", "wb");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//写文件
	fwrite(&s, sizeof(struct S), 1, pf);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;


文本形式是这样的:

但是我们转出2进制看就是这样的:

二进制的输入fread:

  • 描述:从给定流 stream 读取数据到 ptr 所指向的空间中;
  • 参数 void (*)ptr :这是指向带有最小尺寸 size*nmemb 字节的内存块的指针;
  • 参数 size_t size :这是要读取的每个元素的大小,以字节为单位;
  • 参数 size_t nmemb :这是元素的个数;
  • 参数 FILE(*)stream : 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流;
  • 返回值 :成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果返回值小于元素个数 ,则发生了错误或读到文件末尾。
int main()

	struct S s =  0;
	FILE* pf = fopen("test.txt", "rb");
	if (NULL == pf)
	
		perror("fopen");
		return 1;
	
	//读文件
	fread(&s, sizeof(struct S), 1, pf);
	printf("%s %d %f\\n", s.name, s.age, s.score);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

代码结果:

5.流的概念:

  小伙伴对于上面所提到的输入流输出流肯定非常迷惑。可以看到流其实流是一个极其抽象的概念,我们可以把它理解成C程序与外部设备进行交流的一个媒介,文件是一个流,我们的键盘屏幕也是一个流,并且他们有各自专门的文件指针的名称。
其实我们也可以用上面的fprintffscanf来实现printf和scanf

struct S

	char name[20];
	int age;
	float score;
;

int main()

	struct S s =  0 ;
	fscanf(stdin, "%s %d %f", s.name, &(s.age), &(s.score));
	fprintf(stdout, "%s %d %f\\n", s.name, s.age, s.score);

	//int ch = fgetc(stdin);
	//fputc(ch, stdout);

	return 0;

6.辨析输入与输出:

  在这里,为了方便辨析是输入还是输出。我们可以把自己当作内存以内存为主体判断是输出还是输入。比如键盘上打字对于内存来说是输入/读取,在屏幕上打印是输出/写入。写文件是将内存的东西放入文件是输出/写入,从文件获取信息是输入/读取。

7.辨析三组函数:

7.1.scanf和printf:

  • scanf —从键盘上读取格式化的数据 stdin
  • printf —把数据写到(输出)屏幕上 stdout

7.2.fscanf和fprintf:

  • fscanf —针对所有输入流的格式化的输入函数:stdin,打开的文件
  • fsprintf —针对所有输出流的格式化的输出函数:stdout,打开的文件

7.3.sscanf和sprintf

  • sscanf —从一个字符串中,还原一个格式化的数据
  • sprintf —把格式化的数据,存放在(转化成)一个字符串

  相信大家看到前面的s就知道肯定是和字符串有关,其实这两个函数就是将格式化数据和字符串数据相互转化罢了,看一个栗子🌰就够了!

int main()

	struct S s = "zhangsan", 20, 98.5;
	char buf[100] =  0 ;
	//转化成字符串形式
	sprintf(buf, "%s %d %f", s.name, s.age, s.score);
	//按照字符串打印的
	printf("%s\\n", buf);

	struct S tmp =  0 ;
	//转化为格式化数据
	sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
	//打印结构体数据
	printf("%s %d %f\\n", tmp.name, tmp.age, tmp.score);

	return 0;

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件

一、fopen简介

在C语言中,操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立连接的过程。

打开文件之后,程序可以得到文件的相关信息,例如大小、类型、权限、创建者、更新时间等。在后续读写文件的过程中,程序还可以记录当前读写到了哪个位置,下次可以在此基础上继续操作。

标准输入文件 stdin(表示键盘)、标准输出文件 stdout(表示显示器)、标准错误文件 stderr(表示显示器)是由系统打开的,可直接使用。

1.1 fopen函数

使用 <stdio.h> 头文件中的 fopen() 函数即可打开文件,它的用法为:

FILE *fopen(char *filename, char *mode);

filename为文件名(包括文件路径),mode为打开方式,它们都是字符串。

1.2 fopen() 函数的返回值

fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。

FILE<stdio.h> 头文件中的一个结构体,它专门用来保存文件信息。我们不用关心 FILE 的具体结构,只需要知道它的用法就行。

如果希望接收 fopen() 的返回值,就需要定义一个 FILE 类型的指针。例如:

FILE *fp = fopen("demo.txt", "r");

表示以“只读”方式打开当前目录下的 demo.txt 文件,并使 fp 指向该文件,这样就可以通过 fp 来操作 demo.txt 了。fp 通常被称为文件指针

再来看一个例子:

FILE *fp = fopen("D:\\\\\\\\demo.txt","rb+");

表示以二进制方式打开 D 盘下的 demo.txt 文件,允许读和写

1.2.1 判断文件是否打开成功

打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,我们可以利用这一点来判断文件是否打开成功,请看下面的代码:

FILE *fp;
if( (fp=fopen("D:\\\\demo.txt","rb")) == NULL )
    printf("Fail to open file!\\n");
    exit(0);  //退出程序(结束程序)

我们通过判断 fopen() 的返回值是否和 NULL 相等来判断是否打开失败:
如果 fopen() 的返回值为 NULL,那么 fp 的值也为 NULL,此时 if 的判断条件成立,表示文件打开失败。

以上代码是文件操作的规范写法,读者在打开文件时一定要判断文件是否打开成功,因为一旦打开失败,后续操作就都没法进行了,往往以“结束程序”告终。

1.3 fopen() 函数的打开方式

不同的操作需要不同的文件权限。例如,只想读取文件中的数据的话,“只读”权限就够了;既想读取又想写入数据的话,“读写”权限就是必须的了。

另外,文件也有不同的类型,按照数据的存储方式可以分为二进制文件和文本文件,它们的操作细节是不同的。

在调用 fopen() 函数时,这些信息都必须提供,称为“文件打开方式”。最基本的文件打开方式有以下几种:

  • 控制读写权限的字符串(必须指明)
打开方式说明
“r”以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。
“w”以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
“a”以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
“r+”以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。
“w+”以“写入/更新”方式打开文件,相当于w和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
“a+”以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
  • 控制读写方式的字符串(可以不写)
打开方式说明
“t”文本文件。如果不写,默认为"t"。
“b”二进制文件。

调用 fopen() 函数时必须指明读写权限,但是可以不指明读写方式(此时默认为"t")。

读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)。例如:

  • 将读写方式放在读写权限的末尾:“rb”、“wt”、“ab”、“r+b”、“w+t”、“a+t”
  • 将读写方式放在读写权限的中间:“rb+”、“wt+”、“ab+”

整体来说,文件打开方式由 r、w、a、t、b、+ 六个字符拼成,各字符的含义是:

  • r(read):读
  • w(write):写
  • a(append):追加
  • t(text):文本文件
  • b(binary):二进制文件
  • +:读和写

1.4 关闭文件

文件一旦使用完毕,应该用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。fclose() 的用法为:

int fclose(FILE *fp);

fclose(fp)函数关闭fp指定的文件,必要时刷新缓冲区。
对于较正式的程序,应该检查是否成功关闭。
如果成功关闭,fclose()返回值为0,否则返回EOF

if(fclose(fp) != 0)
	printf("Error in closing file %s \\n", argv[1]);


如果磁盘已满、移动硬盘被移除或者出现I/O错误,都会导致fclose()函数失败。

1.5 实例演示

最后,我们通过一段完整的代码来演示 fopen 函数的用法,这个例子会一行一行地读取文本文件的所有内容:

#include <stdio.h>
#include <stdlib.h>

#define N 100

int main() 
    FILE *fp;
    char str[N + 1];

    //判断文件是否打开失败
    if ( (fp = fopen("d:\\\\demo.txt", "rt")) == NULL ) 
        puts("Fail to open file!");
        exit(0);
    

    //循环读取文件的每一行数据
    while( fgets(str, N, fp) != NULL ) 
        printf("%s", str);
    
   
    //操作结束后关闭文件
    fclose(fp);
    return 0;

二、 fopen实例

2.1 读取常规文件

如果您开发一个带有一些原生 C/C++ 层 (NDK) 的 Android 应用程序,并且
尝试通过执行

FILE *pFile = fopen ("myfile.txt" , "w" )

从其中打开文件,fopen 调用将失败。

原因是您的应用程序的当前工作目录是文件系统“ /”的根目录,您的应用程序自然无权访问它。

为了解决这个问题,您必须将文件的完整路径传递给fopen调用。
一种方法是将基本路径传递到您的应用程序具有读/写权限的应用程序数据文件夹。

例如,您可以使用上下文 API getFilesDir().getAbsolutePath()
http://developer.android.com/reference/android/content/Context.html#getFilesDir()
并将结果传递给本机函数调用。

这个特定的 API 当前解析为:“ /data/data/[app package]/文件/"

在您的本机代码中, fopen 调用应该成功

FILE *pFile = fopen (" /data/data/ [应用程序包]/文件/我的文件.txt" , "w" ); 

比如我写了下面这段代码来测试

我们读取一下/data/data/com.oyp.face2dsticker/files/HMS_MLKIT_FACE/version.txt 这个文件的内容

这个文件内容如下所示:

下面代码是处于cpp文件中的某个函数的代码,这个函数最终通过jni的方式被Java业务层调用。

  FILE *pFile = nullptr;
    // 这个路径可以在Java业务层通过 getFilesDir().getAbsolutePath() 来获取,然后传递到cpp层
    pFile = fopen ("/data/data/com.oyp.face2dsticker/files/HMS_MLKIT_FACE/version.txt" ,"r");
    if(nullptr == pFile)
        LOGD("File open fail!\\n")
    
    char tmp[100];
    fread(tmp, 1, 100, pFile);
    LOGD("File open success, content is :【%s】\\n", tmp)
    fclose(pFile);
    pFile = nullptr;

运行程序,成功读取到文件内容,打印出来的日志如下:

 File open success, content is :0.1.9.10q】

2.2 读取Asset目录文件

需要引入头文件

#include <android/asset_manager_jni.h>

typedef一下AAssetesFile

typedef AAsset esFile;

static JNIEnv *sEnv = nullptr;
static jobject sAssetManager = nullptr;

void GLUtils::setEnvAndAssetManager(JNIEnv *env, jobject assetManager) 
    sEnv = env;
    sAssetManager = assetManager;


static AAsset *loadAsset(const char *path) 
    AAssetManager *nativeManager = AAssetManager_fromJava(sEnv, sAssetManager);
    if (nativeManager == nullptr) 
        return nullptr;
    
    return AAssetManager_open(nativeManager, path, AASSET_MODE_UNKNOWN);


//
// File open
//
static esFile *esFileOpen(const char *fileName) 
    esFile *file;
    FUN_BEGIN_TIME("GLUtils::esFileOpen")
        AAssetManager *nativeManager = AAssetManager_fromJava(sEnv, sAssetManager);
        if (nativeManager == nullptr) 
            return nullptr;
        
        file = AAssetManager_open(nativeManager, fileName, AASSET_MODE_BUFFER);
    FUN_END_TIME("GLUtils::esFileOpen")
    return file;


//
// File close
//
static void esFileClose(esFile *pFile) 
    FUN_BEGIN_TIME("GLUtils::esFileClose")
        if (pFile != nullptr) 
            AAsset_close(pFile);
        
    FUN_END_TIME("GLUtils::esFileClose")


//
// File read
//
static int esFileRead(esFile *pFile, int bytesToRead, void *buffer) 
    int bytesRead = 0;
    FUN_BEGIN_TIME("GLUtils::esFileRead")
        if (pFile == nullptr) 
            return bytesRead;
        
        bytesRead = AAsset_read(pFile, buffer, bytesToRead);
    FUN_END_TIME("GLUtils::esFileRead")
    return bytesRead;


char *GLUtils::openTextFile(const char *path) 
    char *buffer;
    FUN_BEGIN_TIME("GLUtils::openTextFile")
        LOGI("GLUtils::openTextFile path [%s]", path)
        AAsset *asset = loadAsset(path);
        if (asset == nullptr) 
            LOGE("Couldn't load %s", path)
            return nullptr;
        
        off_t length = AAsset_getLength(asset);
        buffer = new char[length + 1];
        int num = AAsset_read(asset, buffer, length);
        AAsset_close(asset);
        if (num != length) 
            LOGE("Couldn't read %s", path)
            delete[] buffer;
            return nullptr;
        
        buffer[length] = '\\0';
    FUN_END_TIME("GLUtils::openTextFile")
    return buffer;

比如,我使用上面的openTextFile方法读取asset目录下的vertex/vertex_sticker_normal.glsl文件,代码如下:

char *VERTEX_SHADER  = GLUtils::openTextFile("vertex/vertex_sticker_normal.glsl");

但是在调用上面代码之前,需要先调用一下setEnvAndAssetManager 初始化一下assetManager

 // 初始化设置assetManager  一定要记得初始化,否则会报空指针异常
 GLUtils::setEnvAndAssetManager(env, assetManager);

这个assetManager是java层传入进来的,下面讲解一下怎么传下来。

  1. 业务层通过这样调用,传assetManager过来
 AssetManager assetManager = context.getAssets();
 OypCustomEffectRender.onSurfaceCreated(assetManager);
  1. onSurfaceCreated方法最终会调用nativeOnSurfaceCreated方法
 // 业务层方法
 public static void onSurfaceCreated(AssetManager assetManager) 
        nativeOnSurfaceCreated(assetManager);
 
 // JNI方法
 private static native void nativeOnSurfaceCreated(AssetManager assetManager);
  1. 而nativeOnSurfaceCreated方法最终的JNI实现如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_opengl_OypCustomEffectRender_nativeOnSurfaceCreated(JNIEnv *env, jclass clazz, jobject asset_manager) 
    // 初始化设置assetManager  一定要记得初始化,否则会报空指针异常
    GLUtils::setEnvAndAssetManager(env, assetManager);
    // 干其他的业务

这样通过调用GLUtils::setEnvAndAssetManager(env, assetManager);就将java层传进来的assetManager初始化了,那么在cpp层就可以使用去读取asset目录下的文件了。

运行结果如下:

2022-05-20 10:37:16.335 7627-7655/com.oyp.face2dsticker I/LOG_TAG_XtcCustomEffectRender: [GLUtils.cpp][openTextFile][309]: [GLUtils::openTextFile] func start
2022-05-20 10:37:16.335 7627-7655/com.oyp.face2dsticker I/LOG_TAG_XtcCustomEffectRender: [GLUtils.cpp][openTextFile][310]: GLUtils::openTextFile path [vertex/vertex_point.glsl]
2022-05-20 10:37:16.336 7627-7655/com.oyp.face2dsticker I/LOG_TAG_XtcCustomEffectRender: [GLUtils.cpp][openTextFile][326]: [GLUtils::openTextFile] func cost time 1ms
开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系

以上是关于c语言进阶文件操作(上)的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶—— 文件操作(详解)

C语言进阶文件数据操作详解(万字教你真正理解文件使用)

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件

超详细的C进阶教程!C语言文件操作

C语言进阶文本与二进制操作文件,优化通讯录。