多文件项目
简介
一个软件项目往往包含多个源码文件,编译时需要将这些文件一起编译,生成一个可执行文件。
假定一个项目有两个源码文件foo.c和bar.c,其中foo.c是主文件,bar.c是库文件。所谓“主文件”,就是包含了main()函数的项目入口文件,里面会引用库文件定义的各种函数。
123456// File foo.c#include <stdio.h>int main(void) { printf("%d\n", add(2, 3)); // 5!}
上面代码中,主文件foo.c调用了函数add(),这个函数是在库文件bar.c里面定义的。
12345// File bar.cint add(int x, int y) { return x + y;}
现在,将这两个文件一起编译。
1234$ gcc -o foo foo.c bar.c# 更省事的写法$ gcc -o foo *.c
上面命令中,gcc
的-o参数指定生成的二进制可执行文件的文件名,本例是foo。
这个命令运行后,编译器会发 ...
变量说明符
C
语言允许声明变量的时候,加上一些特定的说明符(specifier),为编译器提供变量行为的额外信息。它的主要作用是帮助编译器优化代码,有时会对程序行为产生影响。
const
const说明符表示变量是只读的,不得被修改。
12const double PI = 3.14159;PI = 3; // 报错
上面示例里面的const,表示变量PI的值不应改变。如果改变的话,编译器会报错。
对于数组,const表示数组成员不能修改。
12const int arr[] = {1, 2, 3, 4};arr[0] = 5; // 报错
上面示例中,const使得数组arr的成员无法修改。
对于指针变量,const有两种写法,含义是不一样的。如果const在*前面,表示指针指向的值不可修改。
1234// const 表示指向的值 *x 不能修改int const * x// 或者const int * x
下面示例中,对x指向的值进行修改导致报错。
1234int p = 1const int* x = &p;(*x)++; // 报错
如果 ...
文件操作
本章介绍 C 语言如何操作文件。
文件指针
C 语言提供了一个 FILE
数据结构,记录了操作一个文件所需要的信息。该结构定义在头文件stdio.h,所有文件操作函数都要通过这个数据结构,获取文件信息。
开始操作一个文件之前,就要定义一个指向该文件的 FILE
指针,相当于获取一块内存区域,用来保存文件信息。
1FILE* fp;
上面示例定义了一个 FILE 指针fp。
下面是一个读取文件的完整示例。
123456789101112131415161718#include <stdio.h>int main(void) { FILE* fp; char c; fp = fopen("hello.txt", "r"); if (fp == NULL) { return -1; } c = fgetc(fp); printf("%c\n", c); fclose(fp); return 0;}
上面示例中,新建文件指针fp以后,依次使用了下 ...
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
指针。它只有内存块的地址信息,没有类型信息,等到使用该块内存的时候,再向 ...