C/C++ 学习笔记六(文件系统、预处理)

文件系统

文件是指存储在外部存储介质的数据集合(如磁盘光盘等)。操作系统中使用由文件路径和文件名两部分组成的文件标识符来标识文件。

而在C、C++语言中无论是何种文件,都将其看做是一段字节序列。在对其进行读写操作时,以字节流(stream)的方式进行处理。以字节流的方式进行文件操作,很方便的将具体的文件格式都归一为相同流式操作,增强了处理文件的灵活性。

文件缓冲区

因为文件存储在外存介质中,对于CPU而言,访问内存的效率要比访问访问内存的效率更加快。为提高计算机的运行效率,操作系统会分配一块称为文件缓冲区的内存区域,对文件的读写操作会首先在对文件缓冲区进行操作,再在适当的时机,将缓冲区的数据一次写入到外存中。

使用scanf操作时需要注意缓冲区

虽然有文件缓冲区的存在,但在一般的读写文件时并不需要十分理会对于最后缓冲区写入外存的时机。但在使用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

读写文件操作

文件结构体FILE

在使用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);
}

inline、typedef、define区别与使用场景

c、c++中可以使用内联函数inline、typedef、define去方便的定义一小段代码。之前在将变量时有提及到typedef和define的区别。

C/C++学习笔记二(变量、表达式)

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三者的区别与使用场景。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IMWeb前端团队

json-schema入门

json-schema(一) 相关知识点 它是什么 描述json的数据格式 有什么优点 描述自定义的数据格式 清晰,对人和机器友好 完整的结构校验 自动化测试 ...

3290
来自专栏吴柯的运维笔记

48个Shell脚本小技巧(一)

<转载>这篇文章主要介绍了收集的48个Shell脚本小技巧,涵盖方方面面,比如获取CPU信息、VI技巧、正则表达式等等,需要的朋友可以参考下 1. shell...

3244
来自专栏黄Java的地盘

旧项目TypeScript改造问题与解决方案记

由于本次改造的项目为一个通过NPM进行发布的基础服务包,因此本次采用TypeScript进行改造的目标是移除Babel全家桶,减小包体积,同时增加强类型约束从而...

1961
来自专栏编程微刊

如何把传统写法改成框架形式 es6http://www.expressjs.com.cn/

1282
来自专栏数据之美

shell 学习笔记(16)

转载请注明出处: https://cloud.tencent.com/developer/user/1177713/activities 注:以前的1-15连载...

22510
来自专栏Jerry的SAP技术分享

webpack最简单的入门教程里bundle.js之运行单步调试的原理解析

您可以阅读我前一篇文章:Webpack 10分钟入门 来在本地运行一个Webpack的hello world项目。https://www.toutiao.com...

1004
来自专栏我和PYTHON有个约会

29.企业级开发进阶1:文件输入输出流[IO操作]

农历五月初一 宜声明变量"a",提交代码;忌打DOTA,提交BUG 适宜方位:坐西朝东 多饮水、鲜奶,女神亲近指数较高

612
来自专栏开发 & 算法杂谈

Linux常用shell语法和命令

判断/home/oicq/script/get_random_shm_key.sh是否存在

822
来自专栏木子昭的博客

详解Django自定义过滤器

过滤器与函数 django过滤器的本质是函数,但"函数"太多了,为了显示自己的与众不同,设计者们想了个名字"过滤器"... django有一些内置的过滤器,但和...

3437
来自专栏拂晓风起

Mysql Workbench建模导入MySQL中

942

扫码关注云+社区