I/O 函数
C 语言提供了一些函数,用于与外部设备通信,称为输入输出函数,简称 I/O
函数。输入(import)指的是获取外部数据,输出(export)指的是向外部传递数据。
缓存和字节流
严格地说,输入输出函数并不是直接与外部设备通信,而是通过缓存(buffer)进行间接通信。这个小节介绍缓存是什么。
普通文件一般都保存在磁盘上面,跟 CPU
相比,磁盘读取或写入数据是一个很慢的操作。所以,程序直接读写磁盘是不可行的,可能每执行一行命令,都必须等半天。C
语言的解决方案,就是只要打开一个文件,就在内存里面为这个文件设置一个缓存区。
程序向文件写入数据时,程序先把数据放入缓存,等到缓存满了,再把里面的数据会一次性写入磁盘文件。这时,缓存区就空了,程序再把新的数据放入缓存,重复整个过程。
程序从文件读取数据时,文件先把一部分数据放到缓存里面,然后程序从缓存获取数据,等到缓存空了,磁盘文件再把新的数据放入缓存,重复整个过程。
内存的读写速度比磁盘快得多,缓存的设计减少了读写磁盘的次数,大大提高了程序的执行效率。另外,一次性移动大块数据,要比多次移动小块数据快得多。
这种读写模式,对于程 ...
预处理器(Preprocessor)
简介
C
语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。
预处理器首先会清理代码,进行删除注释、多行的语句合成一个逻辑行等等。然后,执行#开头的预处理指令。本章介绍
C 语言的预处理指令。
预处理指令可以出现在程序的任何地方,但是习惯上,往往放在代码的开头部分。
每个预处理指令都以#开头,放在一行的行首,指令前面可以有空白字符(比如空格或制表符)。#和指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。
所有预处理指令都是一行的,除非在行尾使用反斜杠,将其折行。指令结尾处不需要分号。
define
#define是最常见的预处理指令,用来将指定的词替换成另一个词。它的参数分成两个部分,第一个参数就是要被替换的部分,其余参数是替换后的内容。每条替换规则,称为一个宏(macro)。
1#define MAX 100
上面示例中,#define指定将源码里面的MAX,全部替换成100。MAX就称为一个宏。
宏的名称不允许有空格,而且必须遵守 C
语言的变量命名规则,只能使用字母、数字与下划线(_),且 ...
Enum 类型
如果一种数据类型的取值只有少数几种可能,并且每种取值都有自己的含义,为了提高代码的可读性,可以将它们定义为
Enum 类型,中文名为枚举。
12345enum colors {RED, GREEN, BLUE};printf("%d\n", RED); // 0printf("%d\n", GREEN); // 1printf("%d\n", BLUE); // 2
上面示例中,假定程序里面需要三种颜色,就可以使用enum命令,把这三种颜色定义成一种枚举类型colors,它只有三种取值可能RED、GREEN、BLUE。这时,这三个名字自动成为整数常量,编译器默认将它们的值设为数字0、1、2。相比之下,RED要比0的可读性好了许多。
注意,Enum
内部的常量名,遵守标识符的命名规范,但是通常都使用大写。
使用时,可以将变量声明为 Enum 类型。
1enum colors color;
上面代码将变量color声明为enum colors类型。这个变量的值就是常量RED、GREEN、 ...
Union 结构
有时需要一种数据结构,不同的场合表示不同的数据类型。比如,如果只用一种数据结构表示水果的“量”,这种结构就需要有时是整数(6个苹果),有时是浮点数(1.5公斤草莓)。
C 语言提供了 Union
结构,用来自定义可以灵活变更的数据结构。它内部可以包含各种属性,但同一时间只能有一个属性,因为所有属性都保存在同一个内存地址,后面写入的属性会覆盖前面的属性。这样做的最大好处是节省空间。
12345union quantity { short count; float weight; float volume;};
上面示例中,union命令定义了一个包含三个属性的数据类型quantity。虽然包含三个属性,但是同一时间只能取到一个属性。最后赋值的属性,就是可以取到值的那个属性。
使用时,声明一个该类型的变量。
123456789// 写法一union quantity q;q.count = 4;// 写法二union quantity q = {.count=4};// 写法三union quantity q = { ...
typedef 命令
简介
typedef命令用来为某个类型起别名。
1typedef type name;
上面代码中,type代表类型名,name代表别名。
123typedef unsigned char BYTE;BYTE c = 'z';
上面示例中,typedef命令为类型unsign char起别名BYTE,然后就可以使用BYTE声明变量。
typedef 可以一次指定多个别名。
1typedef int antelope, bagel, mushroom;
上面示例中,一次性为int类型起了三个别名。
typedef 可以为指针起别名。
1234typedef int* intptr;int a = 10;intptr x = &a;
上面示例中,intptr是int*的别名。不过,使用的时候要小心,这样不容易看出来,变量x是一个指针类型。
typedef 也可以用来为数组类型起别名。
123typedef int five_ints[5];five_ints x = {11, 22, 33, 44, 55};
...
struct 结构
简介
C
语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用中并不够用。
实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。
复杂的物体需要使用多个变量描述,这些变量都是相关的,最好有某种机制将它们联系起来。
某些函数需要传入多个参数,如果一个个按照顺序传入,非常麻烦,最好能组合成一个复合结构传入。
为了解决这些问题,C
语言提供了struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起。这样不仅为编程提供方便,也有利于增强代码的可读性。C
语言没有其他语言的对象(object)和类(class)的概念,struct
结构很大程度上提供了对象和类的功能。
下面是struct自定义数据类型的一个例子。
1234struct fraction { int numerator; int denominator;};
上面示例定义了一个分数的数据类型struct fraction,包含两个属性numerator和denominator。
注意,作为一个 ...
C 语言的内存管理
简介
C
语言的内存管理,分成两部分。一部分是系统管理的,另一部分是用户手动管理的。
系统管理的内存,主要是函数内部的变量(局部变量)。这部分变量在函数运行时进入内存,函数运行结束后自动从内存卸载。这些变量存放的区域称为”栈“(stack),”栈“所在的内存是系统自动管理的。
用户手动管理的内存,主要是程序运行的整个过程中都存在的变量(全局变量),这些变量需要用户手动从内存释放。如果使用后忘记释放,它就一直占用内存,直到程序退出,这种情况称为”内存泄漏“(memory
leak)。这些变量所在的内存称为”堆“(heap),”堆“所在的内存是用户手动管理的。
void 指针
前面章节已经说过了,每一块内存都有地址,通过指针变量可以获取指定地址的内存块。指针变量必须有类型,否则编译器无法知道,如何解读内存块保存的二进制数据。但是,向系统请求内存的时候,有时不确定会有什么样的数据写入内存,需要先获得内存块,稍后再确定写入的数据类型。
为了满足这种需求,C 语言提供了一种不定类型的指针,叫做 void
指针。它只有内存块的地址信息,没有类型信息,等到使用该块内存的时候,再向 ...
字符串
简介
C
语言没有单独的字符串类型,字符串被当作字符数组,即char类型的数组。比如,字符串“Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}处理的。
编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。在字符串结尾,C
语言会自动添加一个全是二进制0的字节,写作\0字符,表示字符串结束。字符\0不同于字符0,前者的
ASCII 码是0(二进制形式00000000),后者的 ASCII
码是48(二进制形式00110000)。所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}。
所有字符串的最后一个字符,都是\0。这样做的好处是,C
语言不需要知道字符串的长度,就可以读取内存里面的字符串,只要发现有一个字符是\0,那么就知道字符串结束了。
1char localString[10];
上面示例声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0,所以最多只能容纳9个字符的字符串。
字符串写成数组的形式,是非常麻烦的。C
语言提 ...
数组
简介
数组是一组相同类型的值,按照顺序储存在一起。数组通过变量名后加方括号表示,方括号里面是数组的成员数量。
1int scores[100];
上面示例声明了一个数组scores,里面包含100个成员,每个成员都是int类型。
注意,声明数组时,必须给出数组的大小。
数组的成员从0开始编号,所以数组scores[100]就是从第0号成员一直到第99号成员,最后一个成员的编号会比数组长度小1。
数组名后面使用方括号指定编号,就可以引用该成员。也可以通过该方式,对该位置进行赋值。
12scores[0] = 13;scores[99] = 42;
上面示例对数组scores的第一个位置和最后一个位置,进行了赋值。
注意,如果引用不存在的数组成员(即越界访问数组),并不会报错,所以必须非常小心。
123int scores[100];scores[100] = 51;
上面示例中,数组scores只有100个成员,因此scores[100]这个位置是不存在的。但是,引用这个位置并不会报错,会正常运行,使得紧跟在scores后面的那块内存区域被赋值,而那实际上是其他变量的区域,因 ...
函数
简介
函数是一段可以重复执行的代码。它可以接受不同的参数,完成对应的操作。下面的例子就是一个函数。
123int plus_one(int n) { return n + 1;}
上面的代码声明了一个函数plus_one()。
函数声明的语法有以下几点,需要注意。
(1)返回值类型。函数声明时,首先需要给出返回值的类型,上例是int,表示函数plus_one()返回一个整数。
(2)参数。函数名后面的圆括号里面,需要声明参数的类型和参数名,plus_one(int n)表示这个函数有一个整数参数n。
(3)函数体。函数体要写在大括号里面,后面(即大括号外面)不需要加分号。大括号的起始位置,可以跟函数名在同一行,也可以另起一行,本书采用同一行的写法。
(4)return语句。return语句给出函数的返回值,程序运行到这一行,就会跳出函数体,结束函数的调用。如果函数没有返回值,可以省略return语句,或者写成return;。
调用函数时,只要在函数名后面加上圆括号就可以了,实际的参数放在圆括号里面,就像下面这样。
12int a = plus_one(13 ...