文件操作函数---C语言版本

目录

为什么使用文件?

⽂件的分类

程序⽂件

数据⽂件

⽂件名

文本文件和二进制文件

数据在内存中的存储

文本文件和二进制文件的区别

文件的打开和关闭

标准流(一种特殊类型的流)

文件指针

​编辑文件打开和关闭函数

fopen函数

fclose函数

文件的读写顺序

顺序读写函数

fgetc函数

fputc函数 

fgets函数

​编辑fputs函数

fscanf函数

fprintf函数

​编辑

函数对比:

sscanf函数和sprintf函数

fread函数

fwrite函数

随机读写函数

fseek函数

ftell函数

rewind函数

文件读取结束的判定

feof函数

ferror函数

文件读取结束的可能原因

文本文件例子 

二进制文件例子  

文件缓冲区 

为什么使用文件?

数据存放在内存中:程序退出、掉电  =》数据丢失

数据存放在硬盘中:即存储在文件中,即使程序退出、掉电  =》数据不会丢失

硬盘与内存的区别主要有三点:

1、内存是计算机的工作场所,硬盘用来存放暂时不用的信息

2、内存中的信息会随掉电而丢失,硬盘中的信息可以长久保存

有关文件在内存和硬盘之间数据的输出和输入时可能会出现的概念混淆问题:

文件操作函数---C语言版本_第1张图片

⽂件的分类

从⽂件功能的⻆度来说,⽂件一般有两种:程序⽂件、数据⽂件

程序⽂件

程序⽂件包括:
  • 源程序⽂件(后缀为.c)
  • ⽬标⽂件(windows环境后缀为.obj)
  • 可执⾏程序(windows环境后缀为.exe)

数据⽂件

概念:程序可以从中读取和输出内容的文件叫做数据文件,该文件通常存储在硬盘上
以前数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运⾏结果显⽰到显⽰器上:
文件操作函数---C语言版本_第2张图片

        有时我们也会把一些数据信息输出到磁盘上,等需要的时候再从磁盘的数据文件中把数据读取到内存中使⽤:

文件操作函数---C语言版本_第3张图片

⽂件名

⽂件名包含3部分:⽂件路径+⽂件名主⼲+⽂件后缀
例如: c:\code\test.txt
其中,  c:\code\为文件路径, test为文件名主干, .txt为文件后缀

文本文件和二进制文件

数据文件又被分为文本文件(存储文本信息二进制文件(存储二进制信息的文件

数据在内存中以⼆进制的形式存储,如果不加转换的直接输出到文件中,该文件就是⼆进制⽂件

转换为以ASCII码的形式存储到文件中 ,该文件就是⽂本⽂件

数据在内存中的存储

字符⼀律以ASCII码形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储
比如整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节)
以⼆进制形式输出到磁盘,则磁盘上中只占用4个字节
~0的ASCII码值为48~
文件操作函数---C语言版本_第4张图片

文件操作函数---C语言版本_第5张图片

#define _CRT_SECURE_NO_WARNINGS
#include 
int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);//二进制形式写到文件中
	fclose(pf);//关闭文件
	pf = NULL;
	return 0;
}

 我们发现在项目文件夹中创建了test.txt文件,但是文件中的内容我们是无法查看的乱码:

文件操作函数---C语言版本_第6张图片

将该文件拉至源文件中右键文件选择打开方式为二进制编辑器,就会看到文件的二进制内容了:

文件操作函数---C语言版本_第7张图片

文本文件和二进制文件的区别

存储方式不同:文本文件以字符为单位进行存储,二进制文件是以字节为单位进行存储的

编码方式不同:文本文件一般用ASCII码等编码方式来表示字符,二进制文件没有编码规则

打开方式不同:文本文件可以用文本编辑器打开查看和编辑,二进制文件需要使用特定的软件或编程语言进行处理

文件的打开和关闭

        流(stream)是一个抽象概念,表示数据在输入和输出设备之间的传输通道。通过使用不同类型的输入流和输出流,可以实现对各种数据源和数据目标的读取和写入操作。

标准流(一种特殊类型的流)

        标准流(Standard streams)是计算机编程中常见的一组预定义的输入输出流。它们提供了与程序环境进行交互的标准方式,无需显式地打开或关闭文件。C语⾔程序执行时,默认打开3个标准流:

stdin:标准输⼊流,⼤多数环境中会从键盘输⼊
stdout:标准输出流,⼤多数环境中会输出⾄显⽰器界⾯
stderr:标准错误流,⼤多数环境中会输出到显⽰器界⾯
#define _CRT_SECURE_NO_WARNINGS
#include 
//标准输出流的使用
int main()
{
	fputc('a', stdout);
	fputc('b', stdout);
	return 0;
}

以上三个标准流的类型是: FILE* ,通常称为⽂件指针,可以用它来维护流的各种操作......  

文件指针

文件操作函数---C语言版本_第8张图片文件打开和关闭函数

        ⽂件在读写前后需要进行文件的打开和关闭操作,在打开⽂件时,会返回⼀个FILE*的指针变量指向该⽂件,相当于建⽴了指针和⽂件的关系。 ANSI C 规定了使⽤ fopen 函数 来打开⽂件, fclose函数 来关闭⽂件:

fopen函数

函数原型:FILE * fopen ( const char * filename, const char * mode );  

filename表示字符组成的文件名,mode表⽰⽂件的打开模式,下⾯是mode的取值内容:

文件使用方式 含义 指定文件不存在
"r"    (只读) 打开已经存在的文本文件,输入数据 报错
"w"   (只写) 打开文本文件,输出数据 建立新文件
"a"    (追加) 向文本文件尾添加数据 建立新文件
"rb"   (只读) 打开二进制文件,输入数据 报错
"wb"  (只写) 打开二进制文件,输出数据 建立新文件
"ab"  (追加) 向二进制文件尾添加数据 建立新文件
"r+"   (读写) 打开文本文件,输入输出数据 报错
"w+" (读写) 建立新文件,输入输出数据 建立新文件
"a+"  (读写) 在文件尾进行读写 建立新文件
"rb+" (读写) 打开二进制文件,输入输出数据 报错
"wb+"(读写) 建立新二进制文件,输入输出数据 建立新文件
"ab+" (读写) 在二进制文件尾输入输出数据 建立新文件

返回值:文件成功打开,返回指向该文件的指针,文件打开失败,返回 null 指针

fclose函数

函数原型:int fclose ( FILE * stream );
stream表示的就是指向文件的文件指针变量
返回值:文件成功关闭,则返回零。文件关闭失败,返回EOF

实际案例: 

文件操作函数---C语言版本_第9张图片

文件操作函数---C语言版本_第10张图片

#define _CRT_SECURE_NO_WARNINGS
#include 
int main()
{
	FILE* pf = fopen(".\\..\\..\\test.txt", "w");
	int a = 10;
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fclose(pf);//关闭文件
	pf = NULL;
	return 0;
}

文件的读写顺序

顺序读写函数

函数名 功能 适用于
fgetc 字符输入函数(一次读取一个字符) 所有输入流
fputc 字符输出函数(一次写一个字符) 所有输出流
fgets 文本行输入函数(一次读取一行数据) 所有输入流
fputs 文本行输出函数(一次写一行数据) 所有输出流
fscanf 格式化输入函数 所有输入流
fprintf 格式化输出函数 所有输出流
fread 二进制输入 文件
fwrite 二进制输出 文件

fgetc函数

函数原型:int fgetc ( FILE * stream );

返回值:执行成功时返回读取的字符,如果到达文件末尾或发生错误,则返回常量EOF

原型解释:返回整型(文件指针)

每次读取后指针都会自动指向下一个字符

#define _CRT_SECURE_NO_WARNINGS
#include 
//使用fgetc读取文件中某个字符的个数(当文件中为ABCGAWFA时)
int main()
{
	int c;
	int n = 0;
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		do
		{
			c = fgetc(pf);
			if (c == 'A')
				n++;
		} while (c != EOF);
		fclose(pf);
		printf("文件中一共有%d个A字符",n);
	}
	return 0;
}

//使用fgetc从标准输出流中读取数据
int main()
{
int ch = fgetc(stdin);
printf("%c\n",ch);
}

//fgetc的简单使用(当文件中为abc时)
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		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);
	ch = fgetc(pf);
	printf("%d\n", ch);  //读取到文件末尾时返回EOF,值为1
	fclose(pf);
	pf = NULL;
	return 0;
}

fputc函数 

函数原型:int fputc(int character,FILE* stream);

返回值:执行成功时返回写入的字符,如果发生错误,则返回常量EOF

原型解释:返回整型(写入单个字符,文件指针)

#define _CRT_SECURE_NO_WARNINGS
#include 
//单次输入单个字符
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//写文件
	fputc('x', pf);
	fputc('y', pf);
	fputc('z', pf);
	fputc('m', pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

//循环写入单个字符
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//写文件
	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

//fputc和fgetc的结合使用拷贝文件data1至data2中,其中data1中文件为hello,data2中文件为空,(该拷贝具有覆盖效果)
int main()
{
	//创建写文件指针
	FILE* pfread = fopen("data1.txt", "r");
	if (pfread == NULL)
	{
		perror("fopen-1");   //这里是杠一的意思而非减一
		return 1;
	}
	//创建读文件指针
	FILE* pfwrite = fopen("data2.txt", "w");
	if (pfwrite == NULL)
	{
		perror("fopen-2");   //这里是杠二的意思而非减二
		fclose(pfread);   //既然写的指针为空了,那么读取也就没有意义了直接关闭文件
		pfread = NULL;
		return 1;
	}

	//读写文件
	int ch = 0;
	while ((ch = fgetc(pfread)) != EOF)  //当不等于EOF的时候就证明它没有读取到文件末尾可以继续循环
	{
		fputc(ch, pfwrite);
	}

	//关闭文件
	fclose(pfread);
	pfread = NULL;
	fclose(pfwrite);
	pfwrite = NULL;
	return 0;
}

文件操作函数---C语言版本_第11张图片

fgets函数

函数原型:char * fgets ( char * str, int num, FILE * stream );

返回值:执行成功时返回读取的字符串的指针,如果到达文件末尾或发生错误,则返回NULL

原型解释:返回char*类型(字符数组,读取num个字符直至到达换行符或文件末尾,文件指针)

换行符会使 fgets 停止读取,但它仍被函数视为有效字符,并包含在复制到 str 的字符串中,同时读取结束后会自动添加\0

#define _CRT_SECURE_NO_WARNINGS
#include 
//fgets函数的使用(文件中第一行为hello world,第二行为hehe)
int main(){
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char arr[20] = "xxxxxxxxxxxxxxxxxx";
	//第一次读取
    fgets(arr, 20, pf);
	printf("%s\n", arr);
    //第二次读取
	fgets(arr, 20, pf);
	printf("%s\n", arr);
	fclose(pf);
	pf = NULL;
	return 0;
}

文件操作函数---C语言版本_第12张图片fputs函数

函数原型:int fputs ( const char * str, FILE * stream );

返回值:执行成功时返回非负值,如果发生错误,则返回EOF

原型解释:返回整型(将字符字符数组中的内容拷贝给文件,文件指针)

#define _CRT_SECURE_NO_WARNINGS
#include 
int main()
{
	FILE* pFile;
	char sentence[256];
	printf("Enter sentence to append: ");
	fgets(sentence, 256, stdin);
	pFile = fopen("test.txt", "w");
	fputs(sentence, pFile);
	fclose(pFile);
	return 0;
}

文件操作函数---C语言版本_第13张图片

每种函数与不同的文件打开方式搭配都能产生不同的效果 

fscanf函数

函数原型:int fscanf ( FILE * stream, const char * format, ... );

返回值:返回成功匹配并赋值的参数个数,如果发生读取错误或到达文件末尾,则返回EOF

原型解释:返回整型(文件指针,格式说明符......)

允许从指定文件流中按照指定格式读取数据,并将其赋值给相应变量,通俗来讲就是:每次调用 fscanf 函数都会尝试从文件中读取一个数据项,并根据提供的格式进行解析和赋值如果希望实现循环逐个读取文件中的多个数据项,需要结合循环语句来重复调用 fscanf 函数。

#define _CRT_SECURE_NO_WARNINGS
#include 
struct S
{
	int n;
	float f;
	char arr[20];
};

int main()
{
	struct S s = {0};
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;  
	}
	//读文件
	fscanf(pf, "%d %f %s", &(s.n), &(s.f), s.arr);
	printf("%d %f %s\n", s.n, s.f, s.arr);
	fclose(pf);
	pf = NULL;
	return 0;
}

fprintf函数

函数原型:int fprintf ( FILE * stream, const char * format, ... );

返回值:返回成功写入的字符数,如果发生错误,则返回负值

原型解释:返回整型(文件指针,格式说明符......)

允许将格式化数据写入到指定的文件流中,并根据提供的格式字符串进行转换和排列。

#define _CRT_SECURE_NO_WARNINGS
#include 
//在文件中显示
struct S
{
	int n;
	float f;
	char arr[20];
};

int main()
{
	struct S s = { 100,3.14,"zhangsan" };
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fprintf(pf, "%d %f %s", s.n, s.f, s.arr);
	fclose(pf);
	pf = NULL;
	return 0;
}

//在显示器上显示
struct S
{
	int n;
	float f;
	char arr[20];
};

int main()
{
	struct S s = { 100,3.14,"zhangsan" };
	fprintf(stdout, "%d %f %s", s.n, s.f, s.arr);
	return 0;
}

函数对比:

scanf/fscanf/sscanf

printf/fprintf/sprintf

scanf   -   针对标准输入流(stdin)的格式化输入函数

printf   -   针对标准输出流(stdout)的格式化输出函数

fscanf   -   针对所有输入流的格式化输出函数

fprintf   -   针对所有输出流的格式化输入函数

sscanf   -   从字符串中读取格式化的数据

sprintf   -   把格式化的数据转换成字符串

sscanf函数和sprintf函数

函数原型:int sscanf / sprintf( char * str, const char * format, ... );

原型解释:返回整型(文件指针,格式说明符......)

#define _CRT_SECURE_NO_WARNINGS
#include 
struct S
{
	int n;
	float f;
	char arr[20];
};

int main()
{
	struct S s = { 100,3.14,"zhangsan" };
	char arr[30] = { 0 };
	sprintf(arr, "%d %f %s", s.n, s.f, s.arr);
	printf("%s\n", arr);

	//从arr这个字符串中提取出格式化数据
	struct S t = { 0 };
	sscanf(arr, "%d %f %s",&(t.n),&(t.f),&t.arr);
	printf("%d %f %s\n", t.n, t.f, t.arr);
	return 0;
}

fread函数

函数原型:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

返回值:返回成功读取的元素总数,当到达文件尾或出现错误时,返回小于请求读取个数的数字

原型解释:返回无符号类型(在二进制文件中读取count个大小为size的数据返回至ptr中去)

与fwrite函数结合读取fwrite函数写入的二进制文件内容

#define _CRT_SECURE_NO_WARNINGS
#include 
int main()
{
	int arr[10] = { 0};
	FILE* pf = fopen("test.txt", "rb"); //二进制的读wb
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	fread(arr, sizeof(int), 7, pf);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", arr[i]);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

fwrite函数

函数原型:size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

返回值: 返回成功写入的元素总数,当到达文件尾或出现错误时,返回小于请求读取个数的数字

原型解释:返回无符号类型(从ptr中读取count个大小为size的数据返回至二进制文件中)

#define _CRT_SECURE_NO_WARNINGS
#include 
int main()
{
	int arr[] = { 1,2,3,4,5,6,7 };
	FILE* pf = fopen("test.txt", "wb"); //二进制的写wb
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	fwrite(arr,sizeof(int),7,pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

随机读写函数

fseek函数

函数原型:int fseek ( FILE * stream, long int offset, int origin );

返回值:当成功执行定位操作时,返回0。当出现错误时,返回非零值

原型解释:返回整型(文件指针,偏移量,起始位置)

//偏移量取决于int origin的取值,关于int origin的取值有三种:

SEEK_END:文件指针的末尾

SEEK_CUR:文件指针的当前位置

SEEK_SET:文件开始位置

#define _CRT_SECURE_NO_WARNINGS
#include 
//fseek函数的使用(文件中的内容为abcdefghijk)
int main()
{
	int arr[10] = { 0};
	FILE* pf = fopen("test.txt", "r"); //二进制的读wb
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件:定位文件指针
	fseek(pf, -3, SEEK_END);  //从文件末尾开始向前移动三个字符
	//获取此时指向的数据
	//fgetc函数的返回类型为整型
	int ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

ftell函数

函数原型:long int ftell ( FILE * stream );

返回值:当成功获取当前文件位置时,返回当前文件位置的字节偏移量。当出现错误时,返回-1

原型解释:返回⽂件指针相对于起始位置的偏移量(文件指针)

#define _CRT_SECURE_NO_WARNINGS
#include 
//ftell函数的使用(文件中的内容为abcdefghijk)
int main()
{
	int arr[10] = { 0};
	FILE* pf = fopen("test.txt", "r"); //二进制的读wb
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fseek(pf, -3, SEEK_END);  //此时文件指针指向i
	int ch = ftell(pf);//i相比文件起始位置的偏移量为8
    rewind()
	fclose(pf);
	pf = NULL;
	return 0;
}

rewind函数

函数原型:void rewind ( FILE * stream );

返回值: 该函数没有返回值,它用于将文件位置指针重新设置为文件的起始位置

原型解释:让文件指针位置返回到文件的起始位置

#define _CRT_SECURE_NO_WARNINGS
#include 
//rewind函数的使用(文件中的内容为abcdefghijk)
int main()
{
	int arr[10] = { 0 };
	FILE* pf = fopen("test.txt", "r"); 
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fseek(pf, -3, SEEK_END);  
	int ch = ftell(pf);
	rewind(pf);   //返回起始地址
	int c = fgetc(pf);  //此时指针应该指向a
	printf("%c", c);
	fclose(pf);
	pf = NULL;
	return 0;
}

文件读取结束的判定

feof函数

函数原型:int feof ( FILE * stream );

返回值:当到达文件尾时,返回非零值。否则,返回0

ferror函数

函数原型:int ferror ( FILE * stream );

返回值:当发生文件操作错误时,返回非零值。否则,返回0

文件读取结束的可能原因

1、到达文件末尾

2、文件读取错误

⽂本⽂件判断读取是否结束,是根据返回值是否为 EOF或 NULL,比如:
fgetc函数  判断返回值是否为 EOF .
fgets函数  判断返回值是否为 NULL 
 ⼆进制⽂件判断读取是否结束,是根据返回值是否⼩于实际要读的个数,比如:
fread函数的返回值是成功读取的元素个数,在发生错误或到达文件末尾时返回一个小于请求元素个数的值

文本文件例子

#define _CRT_SECURE_NO_WARNINGS
#include 
int main(void)
{
	int c; // 注意:int,⾮char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if (!fp) 
    {
		perror("File opening failed");
		return -1;
	}
	//fgetc函数当文件读取失败时或遇到⽂件末尾时,都会返回EOF
	while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
	{
		putchar(c);
	}
	//判断是什么原因结束的
	if (ferror(fp))
		puts("I/O error when reading");//读取时遇到错误
	else if (feof(fp))
		puts("End of file reached successfully");//读取时遇到文件末尾
	fclose(fp);
}

二进制文件例子

#define _CRT_SECURE_NO_WARNINGS
#include 
enum { SIZE = 5 };
int main(void)
{
	double a[SIZE] = { 1.,2.,3.,4.,5. };
	FILE* fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式
	fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
	fclose(fp);
	double b[SIZE];
	fp = fopen("test.bin", "rb");
	size_t ret_code = fread(b, sizeof * b, SIZE, fp);//从fp指向的文件中读取SIZE个大小的数据到b
	if (ret_code == SIZE) 
    {
		puts("Array read successfully, contents: ");
		for (int n = 0; n < SIZE; ++n)
         printf("%f ", b[n]);
		putchar('\n');
	}
	else { 
		if (feof(fp))   //如果feof(fp)为真
        {
			printf("Error reading test.bin: unexpected end of file\n");
		}
        else if (ferror(fp)) //如果ferror(fp)为真
        {
			perror("Error reading test.bin");
		}
	}
	fclose(fp);
}

文件缓冲区 

文件缓冲区的运行过程:
①从内存向磁盘输出数据会先输出到内存中的输出缓冲区,装满缓冲区后⼀起送到磁盘上。
②从磁盘向计算机输入数据会先输入到内存的输入缓冲区,装满缓冲区后逐个将数据送到程
序数据区,缓冲区的⼤⼩根据C编译系统决定。
文件操作函数---C语言版本_第14张图片
#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 
//VS2019 WIN11环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");  //当系统睡眠十秒后打印刷新缓冲区
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}

文件操作函数---C语言版本_第15张图片

文件操作函数---C语言版本_第16张图片

结论: 因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件,如果不做,可能导致读写⽂件的问题。

~over~

你可能感兴趣的:(初阶C语言,c语言,开发语言,数据结构,算法,c++)