C语言语法基础

关于C语言基础语法的一些整理。该部分内容是曾在2018年发布的。

当前博客显示的发布时间非真实时间,而是这些内容在当时发布时的最后发布时间。

程序和程序设计

源程序指的是高级语言编写的程序,目标程序是由二进制代码表示的程序。机器并不能看得懂我们的代码,所以软件工作者们就写了一种叫做编译程序的东西,它可以把我们的源程序翻译成二进制的机器指令,这样机器就能听得懂。

C源程序经过C的编译程序编译后生成一个.obj的二进制文件,我们称为目标文件,然后由称为“连接程序”的软件,把.obj文件与C语言提供的各种库函数连接起来,生成一个.exe,这个是可执行文件。

#include并不是一个语句。

标识符、常量、变量、整型数据、实型数据相关

C语言的标识符分为关键字、预定义标识符和用户标识符。关键字是C语言预先规定的,不可以另作他用。预定义标识符是C语言中预先定义好并表示一定含义的标识符。比如库函数和预编译处理命令。C语言允许把这类标识符重定义,但是这样做它将失去原来的含义。但是强烈不建议去尝试这么做。用户标识符允许用户自定义标识符,但是不能与关键字相同,如果与预定义标识符相同,系统不会报错,但是原来的预编译标识符会失去原来的含义。

整型常量和实型常量又称为数值型常量,有正负的区分。实型常量必须带小数点,整型常量不带。C编译程序根据字面形式确定常量类型。

使用#define命令行(这个也不是语句)定义某个符号常量。在程序中出现这个符号的时候,一律替换至所定义的值。

变量的名字由用户自定义,该名字必须符合规则。在定义变量时说明类型,系统在编译时就能根据它的类型分配相应的存储单元。

十六进制数可以是0X开头(平时用0x比较多),进制中的六个字母大小写均可。在C程序中,只有十进制可以是负数,八进制和十六进制只能是整数(没错,你没看错,不是正数,是整数)。

不同的编译系统int变量开辟内存单元大小不同。VC会为int变量开辟4个字节的内存单元,我们知道,一个字节是八位二进制数,那么int就是32位的二进制数。它的取值范围是-2147482648到2147483647。

无论是短整型数还是长整型数,都会被识别为有符号整数。无符号整数在数的末尾应该加上后缀u或者U,如果是长整型无符号常量,可以加lu或者LU。短整型无符号常量取值是0到65535,长整型无符号常量取值在0到4294967295。

bit与byte并不是一回事。bit是计算机中最小的存储单位,只能存放0或1,称为二进制位。大多数的计算机把八个二进制位组成一个字节,这个就是byte,每个字节都有自己的地址。若干的字节会组成一个“字”,这个“字”用来存放机器指令或者数据。机器不同。“字”的长度也就不同。我们常说32位的计算机(当然现在已经out了),它就是以32个二进制位作为一个“字”的计算机,这一个“字”就是4byte。

对于有符号整数,它的最高位是存放符号的。最高位是0位正,1为负。

负数以补码形式存放。补码=反码+1。

在内存中存放的补码,怎么显示出来?首先对除符号位的所有位按位取反,把所得二进制数转化为十进制数后-1。

C语言中以两种形式表示一个实型常量。第一种形式是小数形式,第二种是指数形式。和数学中的指数形式很像,在C语言中以一个字母e或者E后跟一个整数来表示以10位底的幂数。C语言规定,字母e或者E之前必须要有数字,后面的指数必须是整数,这三部分中间没有空格。

在一般的计算机系统中,为float类型的变量分配4个字节的存储单元,为double类型的存储变量分配8个字节的存储单元,但是它并不是按照实型数的存储方式存放数据。在VC中,所有的float类型数据运算都自动转换成double。实型数值的范围非常大,而且是存在一定误差的。

算术表达式

双目运算符两边运算符类型一致,所得类型一致,如果类型不一致,系统会进行转换到一致后进行计算。转换规律如下:

短整型 运算 长整型 短整型会变成长整型

整型 运算 长整型 整型会变成长整型

字符型 运算 整型 字符型会变成整型

有符号整型 运算 无符号整型 有符号整形会变成无符号整型

整型 运算 浮点型 整型会变成浮点型

赋值表达式

赋值运算符的优先级别只比逗号运算符高,比其他的运算符都低。

复合赋值运算符的优先级和赋值运算符相同。

赋值时如果赋值号左右边类型相同,那么好,没事。如果不相同,分成下面几种情况:(以下情况把变量名和表达式的类型反过来同样适用)

短整型 = 长整型,短整型会变成长整型。

有符号整型 = 无符号整型,有符号整型会变成无符号整型。

同时应当注意:

当短整型 = 长整型的情况出现时,短整型变量只能接受长整型数位上两个字节里的数据,高位上的数据将舍弃(丢失)。

当无符号整型 = 有符号整型时,会发生什么?

哦抱歉,什么也不会发生。内存中的内容会全部被复制进去。你发现了什么问题吗?

没错,负数会变成一个巨大的正数(想一想,为什么)。

同理,如果是有符号整型 = 无符号整型,这个无符号整型在足够大时会变成一个负数。

(我发现luogu上有好多人吐槽我这个“想一想,为什么”[笑抽],这话颇具刘汝佳大佬的写作风格23333其实真不是有意而为之,我就是想把这句加上,没别的意思[笑抽])

逗号表达式

结合性从左到右,最后一个表达式的值就是此逗号表达式的值。逗号运算符优先级最低。

格式字符

%c 输出一个字符。

%d 输出带符号的十进制整数(最常用),还有%ld长整型,%hd短整型,%I64d(%lld)为64位长整数(long long类型)。

%o 八进制 %#o 加先导0的八进制

%x 十六进制 %#x 同上

%u 无符号十进制整数

%f 以带小数点的数学形式输出浮点数

%e 以指数形式输出浮点数

%g 由系统决定是采用%f还是%e,目标是输出宽度最小

%s 输出字符串,直到遇到\0。

%p 输出内存地址

% % 输出一个”%”

%* 跳过此处数据,输入但不处理。

关系运算和逻辑运算

<,<=,>,>=的优先级别相同,=,!=优先级别相同,前四种运算符优先级高于后两种。

算术运算符的优先级别最高,关系运算符次之,赋值运算符最低。

若x和y都是浮点数,像x==y这样的关系表达式尽量要少用,因为浮点数存在误差,两者可能会因为误差而导致不相等。

逻辑非级别最高,逻辑与次之,逻辑或最低(指优先级)。

条件表达式构成的选择结构

条件运算符是C语言提供的唯一的三目运算符。它可以被当作一个简单的if使用。有三个表达式,先求出表达式1的真假,真则执行表达式2,假则执行表达式3。条件运算符优先于赋值运算符,但是低于关系运算符和算术运算符。

字符型常量

字符常量在内存中仅占一个字节,它实际上存放的是ASCII的代码值。

‘A’ 65 ‘a’ 97 ‘0’ 48 ‘ ‘ 32

各种转义字符:

\n 换行,很常用(有的系统中写作\r,我的旧博客中讲快读的部分使用过\r)。

\t 敲一个“Tab”,我喜欢用Tab进行缩进。

\v 竖向跳格 (还有这种操作?)

\f 换页符 (没用过。。。)

\b 敲一个“Backspace”,退格。

\ 敲一个“\”。

‘ 敲一个’, (想一想,为什么把这个单独摘出来,连带着下边那个)

“ 敲一个”,双引号 (233333)

\0 就是一个空值。

\后面加一个 八进制 数字,代表的是ASCII码的符号,会显示对应符号。

\x后面加一个 十六进制 数字,也是一样的意思。

字符串常量要占用一个字节存放末尾的\0,它代表串的结束。

指针变量相关

指针移动的最小单位是一个存储单元而不是一个字节。因此,对于基类型不同的指针变量,地址值的增1减1跨越的字节数是不同的。基类型不同的指针变量不可以混合使用。

求地址运算符只能应用于变量和数据元素,不可以用于表达暗示、常量或者register变量。

可以通过调用库函数malloc和calloc在内存中开辟动态存储单元。

可以作为间接访问运算符,它通过指针引用某个地址的存储单元。这里和“p”中的星号意思不同。

*p++和++p的意思并不相同,间接访问运算符的优先级和++是一样的,计算时从左到右计算。p++会先取p指向的地址的值,然后让值++,但是++*p会让p这个指针的地址+1,它里面所指向的值并没有改变,总之,应该先取再改才是满足我们的要求的。

数组相关

可以通过赋初值来确定数组的大小。可以认为数组名是一个地址常量。当调用函数时,数组元素可以作为一个实参传送给形参,但是这样做并不能改变原数组当中的值。数组名也可以作为一个实参传送,但是数组名本身是一个地址值,所以传入的实际上应该是一个指针变量。

二维数组名也是一个地址常量,二维数组名应该理解为一个行指针,对于二维数组名,并不支持a++,a+=i这样的运算。

[]的优先级高于*,所以可以建立指针数组来用做各种用途。

要注意,(*p)[2]并不表示指针数组。它是 一个 指针,它的基类型是一个包含两个int元素的数组。

当二维数组名作为实参时,对应的形参必须是一个行指针变量。当指针数组名作为实参时,对应的形参应该是一个指向指针的指针(对就是有这种操作)。像int
**p,它就是让p指向一个指针,基类型是指针型,所以它是指针的指针。

字符串相关

\0标志作为字符串的结束标志,它实际上要占用物理存储空间,但是在计算串长度时不计入。定义字符串时并不需要手动添加\0,系统会帮助你完成。每一个字符串常量分别占用内存中一连串的存储空间,可以理解为字符型的一维数组。这些数组没有名字,但是地址是存在的。

但是,以下代码是不合法的:

1
2
char s[10];
s = "Hello!";

这是为什么呢?字符串常量在赋值过程中给出的是这个字符串在内存中所占的一串连续存储单元的首地址,而s是一个不可以重新赋值的数组名,这样的赋值就是不合法的。

不过稍加修改就没有问题了。

1
2
char *s;
s = "Hello!";

这个赋值语句使用了指针,它并不是把串的内容放到*s中,而是把s这个指向字符的指针指到了字符串在内存中所占的首地址,我们说,该指针指向该字符串。

字符数组的每个元素中课存放一个字符,但它并不限定最后一个字符必须是什么。因为有关字符串的大量操作都与串结束标志\0有关,因此,在字符数组中的有效字符后面加上\0这一特定情况下,可以把这种一维字符数组看作字符串变量,但是又不同于一般的字符串变量。可以说,字符串是字符数组的一种具体应用。

所以,在使用字符数组存放字符串时,不要把数组大小开到正好存下整个串,至少要多一个位置来存放\0。虽然计算机在刚才的操作中会自动在末尾添加\0,但是若用下面的操作初始化则不会:

1
char str[] = { &aposh&apos,&apose&apos,&aposl&apos,&aposl&apos,&aposo&apos };

它就是一个字符数组,因为它没有\0,系统并不会认为它是字符串。如果此时你把它误用作字符串,系统就会在内存池中上翻下找,直到找到一个\0,乱套了那就。这个时候,要把它当作字符串使用,必须在末尾手动的加上一个\0。

不过实际上没多少人会用这种方式初始化字符串的……

真要是想用,可以这么写:

1
char str[10] = { "hello" };

花括号可以去掉。

1
char str[10] = "hello";

但是这样写就是不太合理的:

1
char str[5] = "hello";

为啥呢?\0没有地方放了。但是这样做有可能会正常运行,但是实质上是不正确的,这个添加的\0有可能会破坏掉其他数据。

如果对字符串开辟的内存长度没有明确要求,数组大小其实是可以不写的。

1
char str[] = "hello";

它的长度系统会算出来。

对于字符串的复制,我们知道直接使用赋值语句是不可以的。有两种方法可以实现这个操作。一是用for循环按位复制再手动加\0,还有一种方式是用strcpy()。

字符串的格式说明是%s。用%s格式输入字符串时,空格和回车符都被作为输入数据的分隔符而不能读入。但这并不是说我把一个字符串用空格分割为两部分用scanf也能输入到一个串中,实际上,它只能输入空格前的部分。当字符串长度越界时,系统并不报错,这可能会导致一些隐性错误。

实际上在使用时,数组的长度大多数时候都是开的比较大的。

函数进阶

正常情况下,在写main函数时,里面的参数是空的。其实,main函数是可以有参数的。这样写。

1
2
3
int main(int argc, char **argv) {
//do something...
}

其中argc和argv可以由用户自己修改名称,但是类型是固定的。

这样写出来有什么用呢?我们可以通过命令行中执行程序。假如这个程序叫test,这时候argc的值为1,argv[0]中讲存放字符串“test”的首地址。

argc存的是命令行中字符串的个数。为了执行程序,argv[0]必不可少,argc的值至少为1。另外,按照标准规定,argv[argc]由系统置为\0。

在C语言中函数名代表函数的入口地址,因此可以定义一种指向函数的指针来存放这种地址。

函数名或指向函数的指针变量可以作为实参传送给函数。

用户标识符的作用域和存储类

C语言中有两种存储类别,一种是自动类,一种是静态类。局部变量既可以说明成自动类,也可以说明成静态类。但是全局变量只能是静态类。

当在函数内部或复合语句内定义变量时,如果没有指定存储类,或使用了auto说明符,系统就认为该变量具有自动类别。自动变量被分配在动态存储区,每当进入函数体时,系统自动为auto变量分配内存单元,退出时自动释放。使用这类局部变量的优点是可造成信息隔离,不同函数中使用了同名变量也不会造成影响。

用register声明的变量叫做寄存器变量。它并不占用内存单元,而是“建议”程序把它放在CPU的寄存器里。对寄存器的访问要比内存快得多,当程序在该部分对性能有较高要求时,可以使用它。

那我们为什么不在所有地方都是用寄存器变量呢?

刚才有个细节可能有读者忽略了,那就是“建议”这个词。实际上,使用register声明的变量 不一定
会被分配到寄存器里。CPU中寄存器的数量是有限的,我们只能同时使用很少的寄存器变量。如果寄存器满了,它就会变成auto类型放在内存里。寄存器变量的数目与运行程序的CPU有关,也与所使用的编译程序有关。所以说,register只是一个“建议”,而不是强制。

此外,register变量如果被放在了寄存器里的话,它就是没有地址的(想一想,为什么),所以这时候不能对它进行取地址运算,与地址相关的操作均不适用。register变量在使用时要注意尽量靠近它使用的地方,做到开辟完马上使用,避免浪费。

静态局部变量使用static来说明。静态局部变量的作用域与auto和regster一样,但是static与它们有一些本质性的区别。在整个程序运行期间,静态局部变量仍然是在使用原来的存储单元,也就是说它并不会被释放。静态局部变量的生存期将一直延长到程序运行时结束。

(emmmmm,长寿变量,苟……)(打住

此外,静态局部变量的初值是在编译时已经赋好的,并不是执行到复制语句时才会赋值。对于未赋初值的静态变量,系统会自动赋予初值0。静态变量的上述特点对于编写那些在函数调用之间必须保留局部变量值的独立函数是很有用的。

若全局变量和某个函数的局部变量重名,则在该函数中全局变量会被屏蔽。其实,除十分必要外,一般不提倡使用全局变量(我在这方面做的不够好)。第一,不论是否需要,全局变量在整个程序运行期间都占用内存空间。第二,全局变量在函数以外定义,它会降低函数的通用性,影响函数的独立性。第三,使用全局变量容易因疏忽或使用不当导致全局变量中的值意外改变,引起副作用。

那还有一个extern是干啥的呢?

想一下下面的一个情况。在程序的前半部分有一个函数,使用了一个全局变量,但是这个全局变量是在函数之后定义的,系统会报错。但是从语法上讲这样做并没有什么问题,
这是为啥?

系统是个250,它不知道你定义了这个全局变量……

如果出现这种情况,我们需要在函数引用全局变量时加入extern进行说明(但是要注意,定义的时候不能使用)。这个玩意就是用来告诉那个函数:这个变量已经被定义了,你直接用就行,出了事我担着。

extern还有一个用途,是在多文件连接编译时出现的问题。我们把每一个可以单独编译的源文件叫“编译单位”,一个程序实际上是可以由许多编译单位组成的(本博客中目前展示的代码编译时都是由一个文件进行编译的)。当一个程序由多个编译单位组成,但是这些编译单位中都出现一个同名的全局变量,在连接时就会有“重复定义”的错误。一般解决这个问题的办法就是在其中一个文件中定义所有全局变量,在其它用到这些全局变量的文件中用extern进行说明,说明这些变量已经被定义,我不再去定义它。

比如有一个程序前面是这么写的:

1
2
3
4
5
#include <stdio.h>  
int x;
int main() {
//... c
}

然后还有一个程序这样写:

1
2
3
4
5
#include <stdio.h>  
extern int x;
void f1() {
//...
}

它俩连接起来编译就可以通过。

对于第二个程序,extern int x;并不是定义语句,而是说明语句,去说明我要用这个变量,这个变量在另一个程序里已经定义好了。

当使用说明符static说明全局变量时,它就是静态全局变量,它仅限于本编译单位使用,其他的编译单位是不可以用的。它能够起到一个很好的信息隐蔽作用。

当定义函数时用extern说明时,这个函数就被称为外部函数。一般的函数都隐含说明为extern,这个说明其实是可以省略的。外部函数可以被其他编译单位中的函数调用。通常,当函数调用语句与被调用函数不在同一编译单位,且函数的返回值为非整型时,extern不能省略。

同样的,加上static就是静态函数了。它可以理解为是一个内部函数,其他编译单位不能对他进行调用。使用静态函数可以避免不同编译单位因函数同名而引起混乱。

编译预处理和动态存储分配

在C语言中,凡是以#开头的行,都称为编译预处理命令行。在C编译程序对C源程序进行编译之前,由编译预处理程序对这些编译预处理命令行进行处理。目前常用的有这些:

1
2
3
4
5
6
7
8
9
10
11
#include   
#define
#if
#else
#elif
#ifdef
#ifndef
#undef
#line
#pragma
#error

#define可以用作不带参数的宏定义。它可以用来替换文本,也可以用来仅定义一个符号。如果在一行中写不下,要在下一行继续时,只需要在最后一个字符后紧接着加一个\就可以。替换文本不能替换字符串中与宏名相同的字符串。替换文本不替换用户标识符中与宏名相同的部分,用作宏名的标识符一般用全大写表示,这不是规定,只是一种通用的习惯。

带参数的宏定义中,形参表必须和宏名挨着。它和函数调用很像,但是在宏替换中,对参数没有类型要求。宏替换在编译时完成,并不占用运行时间。

#undef提前解除宏定义的作用域。

在使用C语言开发程序时,我们可以把一些宏定义按照功能分别存入不同的文件中。当我们需要使用某个宏定义时,只需要#include
一下它所在的文件就可以。这里可以使用#include “文件名”而不是#include
<文件名>,用前者的写法会让程序先在源程序所在目录下寻找包含文件。头文件名可以由用户指定,后缀不一定是.h。

关于动态存储分配。malloc函数分配size个字节的存储区,返回一个指向存储区首地址的基类型为void的地址,如果没有足够的内存单元,返回NULL。在ANSI
C中malloc函数返回的指针为void *(不是返回void啊,那样就算没有返回值了),在调用函数时,必须利用强制类型转换转成我们需要的类型。

可是我如果突然忘了某个类型占多少字节怎么办?不慌,用sizeof()就好。

1
pi = (int *)malloc(sizeof(int));

像这样写就好啦。这样写还有一个好,由系统计算字节数,这样是有利于程序的移植的。(因为跨系统时不同类型所占的字节数可能是不一样的)。

使用malloc申请的内存 必须
进行释放。free函数将指针所指向的存储空间进行释放,它只能释放由malloc或者calloc分配的地址。free函数没有返回值。

ANSI C规定calloc函数返回值类型为void *,和malloc一样。它可以这样调用:

1
calloc(n, size);

要求n和size的类型都是unsigned
int。它用来给n个同一类型的数据项分配连续的存储空间,每个数据项的长度是size字节。分配成功时返回存储空间的首地址,否则返回NULL。通过calloc函数分配的存储单元,系统自动把初值设为0。

显然,使用calloc开辟的存储单元相当于开辟了一个一维数组。它也可以用free函数释放,释放指向首地址的指针就好。

结构体、共用体、用户自定义类型

ANSI
C标准规定结构体至少允许嵌套15层。并且允许内嵌结构体成员的名字与外层成员的名字相同。结构体变量中的各个成员在内存中按说明的顺序依次排列。对于多层嵌套的结构体,引用方式是按照从外层到最内层的顺序逐层引用,每一层之间用点号隔开。

在调用函数时,可以将结构体变量中的成员作为实参单独传递,也可以将结构体变量作为实参进行整体传送。向函数传递结构体变量时,传递的是实参结构体变量中各成员的值,函数中形参结构体变量的改变不会影响到实参结构体变量,利用这一指针变量对数组进行任何的操作都将直接影响到实参结构体数组。

利用结构体可以实现链表。其中的一个成员指向本结构体类型,这种结构体也叫引用自身的结构体。每一个结构体变量作为一个结点。

我 引 用 我 自 己

共用体的类型说明和变量的定义方式与结构体的类型说明和变量定义的方式完全相同,不同的是,结构体变量中的成员各自占有自己的存储空间,而共用体变量中的所有成员占有同一个存储空间。关键字是union。

共用体变量在定义的同时只能用第一个成员的类型的值进行初始化。结构体变量中的每个成员分别占有独立的存储空间,因此结构体变量所占内存字节数是其成员所占字节数的总和。但是共用体变量中的所有成员共享一段公共存储区,所以共用体变量所占内存字节数与其成员中所占字节数最多的那个成员相等。

ANSI C允许在两个类型相同的共用体变量之间进行赋值操作。同结构体变量一样,共用体类型的变量可以作为实参进行传递,也可以传送共用体变量的地址。

位运算

C语言中,位运算的对象只能是整型或字符型数据,不能是其它类型数据。C语言提供了六种位运算符,我在以前的文章中也有使用。现在列出。

运算符~。按位求反,优先级最高。

运算符<<,左移,二进制数整体左移一位,也就是乘2,左移两位就是乘4……优先级次于~。

运算符>>是右移,相当于除以2,优先级和<<相同。

运算符&,按位与。按照1&1 = 1,其他的情况都是0的规则处理,它比左移和右移的优先级低。

运算符^,按位异或。两个值不相同时为1,相同时为0。它比按位与的优先级低。

运算符|,按位或,只有在0|0时才会是0,其余情况都是1。它的优先级最低。

还有一些扩展运算符诸如<<=,>>=,&=,^-,|=,用法和之前说过的类似。

位运算的对象可以是整型和字符型数据。当两个运算数类型不同时位数也会不同。遇到这种情况时,系统将自动进行如下处理:

1.将两个运算数右端对齐。

2.将位数短的一个运算数往高位补位,无符号数和正整数左侧补0,负数左侧用1补全,然后再进行计算。

位运算的速度非常快,快到单次位运算的时间很难被计算机测量出来。

文件相关

C语言中,对于输入、输出的数据都按照“数据流”的形式进行处理,也就是说,输出时,系统不添加任何信息,输入时,逐一读入数据,直到遇到EOF或者文件结束标志。C程序中的输入、输出文件都以数据流的形式存储在介质上。C语言支持顺序存取和直接存取。

顺序存取文件的特点是:每当“打开”这类文件,进行读写操作时,总是从文件的头开始,从头到尾顺序地读写。

直接存取文件又称随机存取文件,其特点是:可以通过调用C语言的库函数去指定开始读或写的字节号,然后直接对此位置上的数据进行读写或者把数据写在此位置。数据可以按文本形式或者二进制形式存放在介质上,因此文件可以按数据的存放形式分为文本文件和二进制文件,这两种文件都可以用上述两种方法进行存取。但是对二进制文件的操作要比对文本文件的操作要快一些,因为省去了一步转化的操作。

ANSI C规定,在对文件进行输入或输出的时候,系统将为输入或输出文件开辟缓冲区。它是系统在内存中为各个文件开辟的一片存储区,当对某个文件进行输出时,系统首先把输出的数据填入为该文件开辟的缓冲区内,每当缓冲区被填满时,就把缓冲区的内容一次性地输出到对应的文件中。当从某文件输入数据时,首先将从输入文件中输入一批数据放入到该文件的内存缓冲区中,输入语句将从该缓冲区依次读取出局,当该缓冲区中的数据被读完时,将再从输入文件中输入一批数据放入。这种方式使得读、写操作不必频繁地访问外设,从而提高了读写操作的速度。

文件指针实际上是一个指向结构体类型的指针,这个结构体中包含诸如缓冲区的地址,在缓冲区中当前存取字符的位置,对文件是读还是写,是否出错,是否文件结束等一些列信息。一般称之为FILE。

在C语言中,使用fopen打开文件就可以把程序要读写的文件和磁盘中的文件联系起来。格式是:

1
2
FILE *f;
f = fopen("文件名", "文件打开方式,不仅仅是r和w");

当打开文件错误时,fopen将返回NULL。可以利用这个性质设置文件读错误的报错信息。

文件打开方式比较常用的有这些:

1.”r”,以读方式打开文本。只能读,如果文件不存在会报错,如果文件不可读会报错。

2.”rb”,以只读方式打开二进制文本。其他的和上边那个一样。

3.”w”,以只写方式打开文本。如果指定的文件不存在,则系统会以指定的文件名新建一个文件。

如果指定的文件已存在,则会把原来的内容全部覆盖,千万要注意!!!

4.”wb”,以只写方式打开二进制文本。其他的和上边那个一个样。

5.”a”,为在后面添加数据而打开文本文件。如果指定的文件不存在,则系统会以指定的文件名新建一个文件。如果指定的文件已存在,则会在原来文件的末尾继续写。

6.”ab”,为在后面添加数据而打开二进制文本文件。其他的和上边那个一个样。

7.”r+”,为读和写打开文本文件,用这种方式时,指定的文件应该存在。既可以读又可以写,中间无需关闭。只是对于文件来说,读和写总是从该文件的起始位置开始。在写新的数据时,只覆盖新数据所占的空间,原来的数据不会丢失。

8.”rb+”,为读和写而打开二进制文本文件。除了它可以在任意位置开始读写,其他的和上边那个一个样。

9.”w+”,建立一个新文件,进行写操作,然后可以从头开始读。

如果指定的文件已存在,则会把原来的内容全部覆盖,千万要注意!!!

10.”wb+”,建立一个新二进制文件,进行写操作,然后可以从头开始读。除了它可以在任意位置开始读,其他的和上边那个一个样。

11.”a+”,功能与”a”相同,但是在文件尾部添加新数据后,可以从头读。

12.”ab+”,功能与”a+”相同,但是在文件尾部添加新数据后,可以从任意位置开始读。

(上面的任意位置开始读都是由一个叫做位置函数的东西完成的操作。)

当开始运行一个程序时,系统将负责打开三个文件,它们是标准输入文件、标准输出文件、标准错误文件。它们相应的文件指针叫做stdin,stdout,stderr。通常情况下,stdin与键盘连接,stdout和stderr与终端屏幕连接。注意:这些指针是常量,不可以重新赋值。

(相信OIer对此并不陌生,我们常用的freopen就少不了它们)

当对文件的读写操作完成后,必须将它关闭。关闭文件可以调用库函数fclose来完成。格式是:

1
fclose(文件指针);

在完成了对文件的操作后,应该关闭文件,否则文件缓冲区的剩余数据就会丢失。

使用putc/fputc输出字符。

1
putc(待输出的字符,可以是字符常量可以是字符变量, 文件指针);

如果输出成功,该函数返回输出字符,失败返回EOF。

(tip:EOF的值是-1。)

fputc和它完全一样。。

同样的,getc/fgetc是输入字符的函数。和之前在游戏设计中使用到的getch一样,输入的字符是作为返回值的。

1
ch = getc(文件指针);

它从文件指针指定的文件中读入一个字符,对,就读入一个。

fgetc和它完全一样。。

EOF用来判断文本的结束,因为ASCII中没有-1所对应的字符,所以它可以用作文本结束的标志,但是对于二进制文件来说,可能会出现-1,这个时候EOF就不管用了。还好,ANSI
C 提供了一个feof函数,判断文件是否结束。它接收一个参数,文件指针。返回值为1时文件结束,否则返回0。

fscanf函数只能从文本文件中按照格式读入,它与scanf唯一的区别就是参数表的最前面加了个文件指针。

fprintf函数按格式将内存中的数据转换成对应字符,并以ASCII代码形式输出到文件中。它也是多一个文件指针,其他的和printf一样。

fgets用来读入字符串。形式如下:

1
fgets(存放字符串的起始地址, 向后读多少位(并不是读多少位), 文件指针);

这里第二个参数要注意说明。假设写了一个n,那么它会从字符串的起始地址向后读n-1个字符。如果未读满字符时就读到了换行符或者EOF,则结束本次读操作。读入结束后系统自动加\0,返回值是读到的字符串。

fputs函数输出字符串到文件中。

1
fputs(等待输出的字符串, 文件指针);

等待输出的字符串可以是字符串常量,可以是指向字符串的指针,或者字符数组名。该函数输出并不会输出\0,也不换行。输出成功函数值为一个正整数,否则是EOF。

根据它的特点,在调用fputs输出字符串时,文件中各字符串将首尾相接,它们之间不存在任何间隔符。为了便于读入,在输出字符串时,应当注意人为加入\n进行分分隔。

fread和fwrite分别用来读、写二进制文件。它们调用形式如下:

1
2
fread(指向数据块的指针(内存块的首地址), 每个数据块的字节数, 每读写一次输入或输出的数据块个数, 文件指针);
fwrite(准备输出的数据块的起始地址, 每个数据块的字节数, 每读写一次输入或输出的数据块个数, 文件指针);

还有一个文件定位函数。在介绍文件定位函数之前,要先介绍一下文件位置指针。

文件指针和文件位置指针是不同的概念。文件指针是指在程序中定义的FILE类型的变量,通过fopen函数调用给文件指针赋值,使文件指针和某个文件建立联系,C程序中通过文件指针实现对文件的各种操作。

文件位置指针是一个形象化的概念。我们将用文件位置指针来表示当前读或写的数据在文件中的位置。当通过fopen函数打开文件时,可以认为文件位置指针总是指向文件的开头、第一个数据之前。当文件位置指针指向文件末尾时,表示文件结束。当进行读操作时,总是从文件位置指针所指位置开始,去读其后的数据,然后位置指针移动到尚未读的数据之前,以备指示下一次的读或写操作。当进行写操作时,总是从文件位置指针所指位置开始去写,然后移到刚写入的数据之后,以备指示下一次输出的起始位置。

fseek函数用来移动文件位置指针到指定的位置上,接着的读或写操作将从此位置开始。它的调用形式如下:

1
fseek(文件指针, 以字节为单位的偏移量(长整型), 起始地点(可以是标识符也可以是数字,代表偏移操作以哪个地方为基准));

起始地点标识符有三个。

SEEK_SET,也是数字0,代表文件开始。

SEEK_END,也是数字2,代表文件末尾。

SEEK_CUR,也是数字1,代表文件当前位置。

对于二进制文件,偏移量为正时向文件尾部移动,偏移量为负时向文件首部移动。

对于文本文件,偏移量必须是0。

ftell函数用来获得文件当前位置指针的位置。函数给出当前位置指针相对于文件开头的字节数。

1
2
long int t;
t = ftell(文件指针);

当函数调用出错时,返回-1L。

当打开一个文件时,通常不知道它的长度,可以先通过fseek把位置指针移到末尾,然后用ftell求出文件长度。

如果是二进制文件,里面存放的是struct test中的数据,还可以拿求出来的长度除以sizeof(struct
test)来获得以该结构体为单位的数据块个数。

rewind函数又称反绕函数,让文件的位置指针返回文件开头,它没有返回值。

1
rewind(文件指针);

完结撒花~

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2018-2021 Shawn Zhou
  • Hexo 框架强力驱动 | 主题 - Ayer
  • 访问人数: | 浏览次数:

感谢打赏~

支付宝
微信