关于我的编程语言——C/C++——第七篇(深入4)

时间:2024-11-09 10:24:05

(叠甲:如有侵权请联系,内容都是自己学习的总结,一定不全面,仅当互相交流(轻点骂)我也只是站在巨人肩膀上的一个小卡拉米,已老实,求放过)

什么是文件?

磁盘上的文件是文件,在设计程序中,一般谈论的文件有两种,程序文件、数据文件

程序文件

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

数据文件

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

一下内容主要介绍数据文件

文件名

一个文件要有唯一的文件标识,以便用户识别和引用,文件名包括3部分:文件路径+文件名主干+文件后缀,比如:C\code\test.txt 。

文件类型

根据数据的组织形式,数据文件被称为文本文件或者二进制文件,数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCll码的形式存储,则需要在存储前转换,以ASCll字符形式存储文件就是文本文件。

数据在内存中的存储形式

字符一律以ASCll形式存储,数值型数据即可以用ASCll形式存储,也可以使用二进制形式存储。如有整数10000,如果以ASCll码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。

 例

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

文件缓冲区

ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”,从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送出磁盘上,如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个的将数据送到程序数据区(程序变量等),缓冲区的大小根据C编译系统决定的。

文件指针

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

struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
       };
typedef struct _iobuf FILE;//vs2008环境提供的stdio.h文件中的文件类型声明

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异,每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

例子:

FILE* pf;//文件指针变量

定义pf是一个指向FILE类型的指针变量,可以使pf指向某个文件的文件信息(是一个结构体变量)。通过该文件信息区就可以访问该文件,通过文件指针变量能够找到与它关联的文件。

文件的打开和关闭

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

语法

FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );

打开方式如下

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

#include <stdio.h>
int main()
{
	FILE* pFile;
	pFile = fopen("myfile.txt", "w");
	if (pFile != NULL)
	{
		fputs("fopen example", pFile);
		fclose(pFile);
	}
	return 0;
}

文件的顺序读写

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

文件的随机读写

fseek

根据文件指针的位置和偏移量来定位指针文件

语法

int fseek ( FILE * stream, long int offset, int origin );
#include <stdio.h>
int main()
{
	FILE* pFile;
	pFile = fopen("example.txt", "wb");
	fputs("This is an apple.", pFile);
	fseek(pFile, 9, SEEK_SET);
	fputs(" sam", pFile);
	fclose(pFile);
	return 0;
}
ftell

返回文件指针相对于起始位置的偏移量

语法:

long int ftell ( FILE * stream );

#include <stdio.h>
int main()
{
	FILE* pFile;
	long size;
	pFile = fopen("myfile.txt", "rb");
	if (pFile == NULL) perror("Error opening file");
	else
	{
		fseek(pFile, 0, SEEK_END);// non-portable
		size = ftell(pFile);
		fclose(pFile);
		printf("Size of myfile.txt: %ld bytes.\n", size);
	}
	return 0;
}
rewind

让文件指针的位置回到文件的起始位置

语法

void rewind ( FILE * stream );

#include <stdio.h>
int main()
{
	int n;
	FILE* pFile;
	char buffer[27];
	pFile = fopen("myfile.txt", "w+");
	for (n = 'A'; n <= 'Z'; n++)
		fputc(n, pFile);
	rewind(pFile);
	fread(buffer, 1, 26, pFile);
	fclose(pFile);
	buffer[26] = '\0';
	puts(buffer);
	return 0;
}

文件结束判断

在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束,而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

1)文本文件读取是否结束,判断返回值是否尾EOF(fgetc),或者NULL(fgets)

例如:

        fgetc判断是否为EOF;

        fgets判断返回值是否为NULL;

1)二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

例如:

        fread判断返回值是否小于实际要读的个数。

文本文件的例子:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
	int c; // 注意:int,非char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if (!fp) {
		perror("File opening failed");
		return EXIT_FAILURE;
	}
	//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);
}

二进制文件的例子

#include <stdio.h>
#include <stdlib.h>
enum { SIZE = 5 };
int main(void)
{
	double a[SIZE] = { 1.0,2.0,3.0,4.0,5.0 };
	double b = 0.0;
	size_t ret_code = 0;
	FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
	fwrite(a, sizeof(*a), SIZE, fp); // 写 double 的数组
	fclose(fp);
	fp = fopen("test.bin", "rb");
	// 读 double 的数组
	while ((ret_code = fread(&b, sizeof(double), 1, fp)) >= 1)
	{
		printf("%lf\n", b);
	}
	if (feof(fp))
		printf("Error reading test.bin: unexpected end of file\n");
	else if (ferror(fp)) {
		perror("Error reading test.bin");
	}
	fclose(fp);
	fp = NULL;
}

程序的翻译环境和执行环境

在ANSI的任何一种实现中,存在两个不同的环境

第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第二种是执行环境,它用于实际执行代码。

详解编译+链接

翻译环境

1)组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code);

2)每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序;

3)链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中;

 编译本来也分几个阶段

sum.c

int g_val = 2016;
void print(const char* str)
{
	printf("%s\n", str);
}

test.c

#include <stdio.h>
int main()
{
	extern void print(char* str);
	extern int g_val;
	printf("%d\n", g_val);
	print("hello bit.\n");
	return 0;
}

1)预处理选项gcc - E test.c -o test.i预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中;

2)编译选项gcc - S test.c 编译完成之后就停下来,结果保存在test.s中;

3)汇编gcc -c test.c 汇编完成之后就停下来,结果保存在test.o中;

运行环境

1)程序必须载入内存中,在有操作系统的环境中,一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成;程序的执行便开始,接着便调用main函数;

2)开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的执行过程一直保留它们的值;

3)终止程序,正常终止main函数;也有可能时意外终止;

预处理详解

预处理符号
__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的,举个例子:

#include<stdio.h>

int main()
{
	printf("file:%s line:%d\n", __FILE__, __LINE__);
	return 0;
}

 #define

语法:

#define name stuff

例子:

define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,       \
                          __DATE__,__TIME__ )

注:#define定义标识符的时候,在最后不能加分号,容易导致问题;

例:

#include<stdio.h>
#define MAX 1000;
//#define MAX 1000

int main()
{
	int a = 10;
	int max = 0;
	if (a)
		max = MAX;
	else
		max = 0;
	return 0;
}

 #define 机制包括了一个规定,运行把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)

宏的声明方式

#define name(parament-list)stuff其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff。

注:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分;

宏定义经典错误点
#define SQUARE( x ) x * x

这样的宏存在一个问题

例:

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

替换文本时,参数x被替代成了a+1,所以这条语句实际上变成了:printf("%d\n",a+1*a+1);由于替换产生的表达式并没有按照预想的次序进行求值;

在宏定义上加两个括号,这个问题就轻松解决了;

#define SQUARE(x) (x) * (x)

不过这样就出了新的问题

int a = 5; 
printf("%d\n" ,10 * SQUARE(a));

替换之后

printf ("%d\n",10 * (5) + (5));

这样就只有再加一对括号了

#define DOUBLE( x) ( ( x ) + ( x ) )

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用;

#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤

1)在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换;

2)替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值替换;

3)最后再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程;

注:

1)宏参数和#defne定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归;

2)当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索;

#和##

例:

#include<stdio.h>
int i = 10;
#define PRINT(FORMAT, VALUE)\ 
		printf("the value of "#VALUE "is "FORMAT "\n", VALUE)

int main()
{
	PRINT("%d", i + 3);//产生了什么效果?
	return 0;
}

"VALUE"最终的输出结果应是:

the value of i+3 is 13 
##的作用

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符;

例:

#define ADD_TO_SUM(num, value) 
 sum##num += value

ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
带副作用的宏参数

当宏参数在宏定义中出现超过一次的时候,那么这个在使用的宏就可能出现危险,导致不可预测的后果,副作用就是表达式求值的时候出现的永久性效果;

例:

x+1;//不带副作用
x++;//带有副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) 

x = 5; 
y = 8; 
z = MAX(x++, y++); 
z = ( (x++) > (y++) ? (x++) : (y++));
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
宏和函数对比

宏通常被应用与执行简单的运算,比如在两个数找出较大的值。

#define MAX(a, b) ((a)>(b)?(a):(b))
宏的优势

1)用于调用函数和从函数返回的代码可能比实际执行这个小型计算机工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹;

2)更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,宏可以适用于整型、长整型、浮点数等可以用于>来比较的类型,宏是类型无关的;

宏的劣势

1)每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。

2)宏是没法调试;宏由于类型无关,也就不够严谨;宏可能会带来运算符优先级的问题,导致容易出错;

宏可以做到函数做不到的事情,比如:宏的参数可以出现类型,但是函数做不到

#define MALLOC(num, type)\ 
 (type *)malloc(num * sizeof(type)) 
... 
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));
宏和函数的对比
属性 #define定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅度增加 函数代码只出现于一个地方,每次使用这个函数时,都调用哪个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级 宏参数的求值在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 函数参数只在函数调用的时候哦求值一次,它的结果值传递给函数,表达式的求值结果更容易预测
带副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能产生不可预测的结果 函数参数只在传参的时候求值一次,结果更容易控制
参数类型 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用任何参数类型 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

命名约定

一般来说函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的一个习惯是:把宏名全部大写,函数名不要全部大写;

#undef

这条指令用于移除一个宏定义

例:

#undef NAME 
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

命名行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编程用例。例:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性就很有用,(假定某个程序中声明了一个某长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要的数组也可以大些。)

#include <stdio.h> 
int main()
{
	int array[ARRAY_SIZE];
	int i = 0;
	for (i = 0; i < ARRAY_SIZE; i++)
	{
		array[i] = i;
	}
	for (i = 0; i < ARRAY_SIZE; i++)
	{
		printf("%d ", array[i]);
	}
	printf("\n");
	return 0;
}

编译指令:

gcc -D ARRAY_SIZE=10 programe.c

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令。

例:
调试的代码,删除很可惜,保留又碍事,所以我们可以选择性的编译

#include <stdio.h>
#define __DEBUG__ 
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__ 
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
#endif //__DEBUG__ 
	}
	return 0;
}
常见的条件编译指令:
1.
#if 常量表达式
//... 
#endif 
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1 
#if __DEBUG__ 
//.. 
#endif 
2.多个分支的条件编译
#if 常量表达式
//... 
#elif 常量表达式
//... 
#else 
//... 
#endif 
3.判断是否被定义
#if defined(symbol) 
#ifdef symbol 
#if !defined(symbol) 
#ifndef symbol 
4.嵌套指令
#if defined(OS_UNIX) 
#ifdef OPTION1 
unix_version_option1();
#endif 
#ifdef OPTION2 
unix_version_option2();
#endif 
#elif defined(OS_MSDOS) 
#ifdef OPTION2 
msdos_version_option2();
#endif 
#endif

文件包含

我们已经知道,#inclulde指令可以使用另外一个文件被编译,就像它实际出现于#include指令的地方一样,这种替换的方式很简单,预处理器先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含10次,那就实际被编译10次;

头文件被包含的方式

1)本地包含
#include "filename"

查找策略:现在源文本所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误,linux环境的标准头文件的路径;

/usr/include

vs环境的标准头文件的路径

C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include
2)库文件包含
#include <filename.h>

查找头文件直接去标准路径下查找,如果找不到就提示编译错误,对于库文件也可以使用" "的形式包含,但是这样做查找的效率就低一些,并且这样也不容易区分是库文件还是本地文件了。

comm.h和comm.c是公共模板,test1.h和test2.c使用了公共模板,test2.c和test.h使用了公共模板,test.h和test.c使用了test1和模板test2模板,这样最终程序就会出现两份comm.h的内容,这样就造成了文件内容的重复,使用条件编译就可以解决这个问题 

#ifndef __TEST_H__ 
#define __TEST_H__ 
//头文件的内容
#endif //__TEST_H__
或者
#pragma once

这样就可以避免头文件的重复引用

over ,see you next time!

最近在实习,更新的比较慢,溜溜溜……