文件是指存储在外部存储介质的数据集合(如磁盘光盘等)。操作系统中使用由文件路径和文件名两部分组成的文件标识符来标识文件。
而在C、C++语言中无论是何种文件,都将其看做是一段字节序列。在对其进行读写操作时,以字节流(stream)的方式进行处理。以字节流的方式进行文件操作,很方便的将具体的文件格式都归一为相同流式操作,增强了处理文件的灵活性。
因为文件存储在外存介质中,对于CPU而言,访问内存的效率要比访问访问内存的效率更加快。为提高计算机的运行效率,操作系统会分配一块称为文件缓冲区的内存区域,对文件的读写操作会首先在对文件缓冲区进行操作,再在适当的时机,将缓冲区的数据一次写入到外存中。
虽然有文件缓冲区的存在,但在一般的读写文件时并不需要十分理会对于最后缓冲区写入外存的时机。但在使用scanf函数时,需要注意缓冲区问题。
下面的一个例子,我们期望依次输入hello中的每个字符后,将输入的字符串数组打印出来。
char c[6] = { '\0' , '\0' , '\0' , '\0' , '\0', '\0' };
int i = 0;
printf("输入(hello) :\n");
for (size_t i = 0; i < 5; i++)
{
scanf("%c", &c[i]);
}
printf("输入的内容:%s \n", c);
但当依次输入h ,回车,e,回车,l,回车时,程序便直接执行最后一个printf函数。而且得到的也并不是我们期望的hello。
输入(hello)
h
e
l
输出的内容;h
e
l
出现这种结果的原因是scanf的实际作用是取标准输入缓冲区中最前的字符,而且换行符也是输入也包含了一个字符,所以在得到的字符串数组具体的值为{ ‘h’,0x0A, ‘e’,0x0A,’l’,0x0A,’\0’} (0x0A为换行符)
同样的代码,直接输入hello + 回车时可以达到目的
输入(hello)
hello
输出内容: hello
同理,这次因为没有换行符的影响,从标准输入缓冲区得到的是我们需要的hello
在使用fopen函数打开文件时,返回一个FILE * 指针,这个FILE结构体称为文件结构,这个结构体存放了一些和本文件中有关的一些信息。但对于不同的操作系统而言,该结构体会有所区别,所以在使用该函数进行跨平台开发时,就需要在理会其中的具体成员。
//打开文件
FILE * fp = fopen("I:\\xx.txt", "r");
在windows平台下,通过查看visual studio 安装目录下VC\include\stdio.h可以看到其具体的结构
// WINDOWS
#ifndef _FILE_DEFINED
struct _iobuf {
char *_ptr; //当前文件指针
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif
在mac os平台下FILE结构体如下
typedef struct __sFILE {
unsigned char *_p; /* current position in (some) buffer */
int _r; /* read space left for getc() */
int _w; /* write space left for putc() */
short _flags; /* flags, below; this FILE is free if 0 */
short _file; /* fileno, if Unix descriptor, else -1 */
struct __sbuf _bf; /* the buffer (at least 1 byte, if !NULL) */
int _lbfsize; /* 0 or -_bf._size, for inline putc */
/* operations */
void *_cookie; /* cookie passed to io functions */
int (* _Nullable _close)(void *);
int (* _Nullable _read) (void *, char *, int);
fpos_t (* _Nullable _seek) (void *, fpos_t, int);
int (* _Nullable _write)(void *, const char *, int);
/* separate buffer for long sequences of ungetc() */
struct __sbuf _ub; /* ungetc buffer */
struct __sFILEX *_extra; /* additions to FILE to not break ABI */
int _ur; /* saved _r when _r is counting ungetc data */
/* tricks to meet minimum requirements even when malloc() fails */
unsigned char _ubuf[3]; /* guarantee an ungetc() buffer */
unsigned char _nbuf[1]; /* guarantee a getc() buffer */
/* separate buffer for fgetln() when line crosses buffer boundary */
struct __sbuf _lb; /* buffer for fgetln() */
/* Unix stdio files get aligned to block boundaries on fseek() */
int _blksize; /* stat.st_blksize (may be != _bf._size) */
fpos_t _offset; /* current lseek offset (see WARNING) */
} FILE;
可以看到不同平台下的FILE结构体不尽相同,但是彼此又有相似之处,如指向当前位置的指针。(windows 下char _ptr,mac os下unsigned char _p;)
正是因为将文件看做一个数据块,为方便随机读写数据块的某一个区域,FILE结构体中会有一个指向文件位置的指针,用于读写文件。而在C中,可以使用fseek函数设定文件的当前读写位置。从而可以实现任意顺序访问文件的不同位置。
fseek函数中,与三个参数,第一个为FILE结构体,第二个offset为相对于第三个参数from的偏移量。 from的取值为0,1,2。枚举值分别为SEEK_SET、SEEK_CUR、SEEK_END,分别代表文件首,当前位置,文件尾。
int fseek(FILE *fp,long offset,int from);
fseek(fp,0,SEEK_SET);//文件头
fseek(fs,-10L,SEEK_END);//将读写指针回退到距离文件尾10字节处
另外还有一个rewind函数也可以用来将位置指针重新指向起始位置,但此函数的与fseek相比无法获知函数是否执行成功。
rewind(fp);
if(fseek(fp,0,SEEK_SET)!= 0)
{
//移动指针成功
}
我们知道源代码编译成可执行程序一般需要经过几个阶段:源代码->预处理->汇编->链接。 而在预处理阶段,我们使用宏来对源代码进行初步的处理。
宏又被称为宏替换。其作用是在程序的预处理阶段进行预定格式的字符替换。但由于宏替换缺少必要的类型检查,它只是简单的字符替换,不做计算与表达式的求解,因此在使用宏替换时需要格外小心。
下面为一个简单求平方的宏
#define SQUARE(x) x*x
为方便处理一些有用的信息,系统定义了一些预处理标识符,也就是预定义宏。预定义宏的名称是以下划线开头与结尾的,若有两个单词组成,中间以下划线 进行连接。下面是标准的C语言提供了一些标准的预定义宏:
__DATE__ 编译的时间
__FILE__ 源文件文件名
__LINE__ 源文件中的行数
__TIME__ 源文件最新编译时间
__STDC__ 是否符合ISO标准,符合时为1
__STDC_VERSION__ 如果符合C89标准,定义为199409L,如果符合C99定义为1999901L。其他情况下未定义
__STDC_HOST__ 如果是宿主系统,则为1,如果使独立系统则为0
__STDC_IEC_559__ 如果浮点数设计符合IEC559标准,则为1
__STDC_IEC_559_COMPLEX__ 如果复数设计符合IEC559,则为1
__STDC_IEC_ISO_10646__ 长整型常量,yyyymml表示wchar_t值遵循ISO10646标准及其年月的修订标准,其他为未定义
除开C语言本身的提供的标准宏外,各种编译器也提供了自己的预定义宏。如下是常见的几种编译器以及查看预定义宏的命令
编译器 | C宏指令 | C++宏指令 |
---|---|---|
Clang/LLVM | clang -dM -E -x c /dev/null | clang++ -dM -E -x c++ /dev/null |
GNU GCC/G++ | gcc -dM -E -x c /dev/null | g++ -dM -E -x c++ /dev/null |
Intel ICC/ICPC | icc -dM -E -x c /dev/null | ICPC -dM -E -x c++ /dev/null |
再以上面为例子,当传入的x+2,sqX所得结果并不是想象中的16 ,而是8,这是因为宏展时,出现了歧义。
int x = 2;
int sqX = SQUARE(x+2); //宏展开为 x+2*x+2 = 2+2*2+2 = 8
为解决这个歧义,在此基础上修改宏定义为
#define SQUARE(x) (x)+(x)
再换一个例子,这是一个数加上自己宏
#define DOUBLE(x) (x)+(x)
但当出现下面操作时又会出现问题
int x =2;
int b = 10* DOUBLE(x+2); // 宏展开 10*(x+2)+(x+2);
所以对于这种情况,必须外围加上括号
#define DOUBLE(x) ((x)+(x))
#define SQUARE(x) ((x)*(x))
正因为宏仅仅是字符替换,并没有像函数一样有函数的传参操作,在使用有参数的宏时,如果对参数进行自增减时,会产生副作用。
如下例子,因宏展开导致的歧义,y并不是想象的9,而是12。
int x = 2;
int y = SQUARE(++x); //宏展开 ((++x)*(++x)) = 3*4 = 12
这种情况是因为宏是简单的字符替换,而且宏无类型约束造成的,有时候可以通过语法巧妙的绕过,有些则很难或者使用其他复杂的方法进行描述,但这也同时违反了声明宏当初简单规则。
上面也说到,宏可能会有无可避免的副作用,但有时又无可避免的需要使用一小段代码量较小但使用频繁的代码,这时候可以使用内联函数。内联函数的优点是省了函数调用的开销,也避免了宏的副作用,且也有函数对于参数的严格约束,但其缺点便是若增加目标代码尺寸。
static inline int imax(int a,int b)
{
int m = a>b?a:b;
return m;
}
int main(int argc,char** argv)
{
int a = imax(5, 2);
int b = imax(3, 3);
}
c、c++中可以使用内联函数inline、typedef、define去方便的定义一小段代码。之前在将变量时有提及到typedef和define的区别。
typedef 是一种类型的新别名, 而宏是简单字符串替换。 宏定义的作用时间是在预处理阶段 typedef 的作用时间是在编译阶段 inline的作用时间也在编译阶段,
typedef的作用是将一种类型以另外一个名字命名,即为一种类型有两种名字。其应用的范围比较窄,仅可以修饰变量。
//将char *声明的别名为PCHAR1
typedef char * PCHAR1;
define是简单的字符替换,可以作为一种简单的字符替换,可以用于在预处理阶段修改源代码达到适应不同操作系统的作用。
#ifdef __IOS__
//do ios define ...
#elif (defined __WINDOWS__)
//do windows define ...
#elif (defined __ANDROID__)
//do andrid define ...
#else
#end
inline则一段嵌入执行体中代码段。
1.对于文件系统,需注意读写文件时的指针。 2.对于预处理,需注意宏定义需多使用括号包含其中。也需要注意宏的副作用。 3.注意define,tpyedef,inline三者的区别与使用场景。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。