赞
踩
·1)编译(Compile)会将源文件(
.c文件)转换为目标文件。对于 VC/VS,目标文件后缀为
.obj;对于GCC,目标文件后缀为
.o`。
编译是针对单个源文件的,一次编译操作只能编译一个源文件,如果程序中有多个源文件,就需要多次编译操作。
`2) 链接(Link)是针对多个文件的,它会将编译生成的多个目标文件以及系统中的库、组件等合并成一个可执行程序。
在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。例如,我们希望自己的程序在 Windows 和 Linux 下都能够运行,那么就要在 Windows 下使用 VS 编译一遍,然后在 Linux 下使用 GCC 编译一遍。但是现在有个问题,程序中要实现的某个功能在 VS 和 GCC 下使用的函数不同(假设 VS 下使用 a(),GCC 下使用 b()),VS 下的函数在 GCC 下不能编译通过,GCC 下的函数在 VS 下也不能编译通过,怎么办呢?
这就需要在编译之前先对源文件进行处理:如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。
这些在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)
。
预处理主要是处理以#
开头的命令,例如#include <stdio.h>
等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
预处理是C语言的一个重要功能,由预处理程序
完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
编译器会将预处理的结果保存到和源文件同名的.i
文件中,例如 main.c 的预处理结果在 main.i 中。和.c
一样,.i
也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
实例
下面我们举个例子来说明预处理命令的实际用途。假如现在要开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下的暂停函数和头文件都不一样:
Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds)
(注意 S 是大写的),参数的单位是“毫秒”,位于 <windows.h>
头文件。
Linux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds)
,参数的单位是“秒”,位于 <unistd.h>
头文件。
不同的平台下必须调用不同的函数,并引入不同的头文件,否则就会导致编译错误,因为 Windows 平台下没有 sleep() 函数,也没有 <unistd.h> 头文件,反之亦然。这就要求我们在编译之前,也就是预处理阶段来解决这个问题。请看下面的代码:
#include <stdio.h> //不同的平台下引入不同的头文件 #if _WIN32 //识别windows平台 #include <windows.h> #elif __linux__ //识别linux平台 #include <unistd.h> #endif int main() { //不同的平台下调用不同的函数 #if _WIN32 //识别windows平台 Sleep(5000); #elif __linux__ //识别linux平台 sleep(5); #endif puts("http://c.biancheng.net/"); return 0; }
#if、#elif、#endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。这里我们不讨论细节,只从整体上来理解。
对于 Windows 平台,预处理以后的代码变成:
#include <stdio.h>
#include <windows.h>
int main() {
Sleep(5000);
puts("http://c.biancheng.net/");
return 0;
}
#include <stdio.h>
#include <unistd.h>
int main() {
sleep(5);
puts("http://c.biancheng.net/");
return 0;
}
#include
叫做文件包含命令
,用来引入对应的头文件(.h
文件)。#include 也是C语言预处理命令的一种。
#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
#include 的用法有两种,如下所示:
#include <stdHeader.h>
#include "myHeader.h"
< >
和双引号" "
的区别在于头文件的搜索路径不同:< >
,编译器会到系统路径下查找头文件;" "
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
前面我们一直使用尖括号来引入标准头文件,现在我们也可以使用双引号了,如下所示:
#include "stdio.h"
#include "stdlib.h"
stdio.h
和 stdlib.h
都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。
当然,你也可以把当前项目所在的目录添加到系统路径,这样就可以使用尖括号了,但是一般没人这么做,纯粹多此一举,费力不讨好。
在以后的编程中,大家既可以使用尖括号来引入标准头文件,也可以使用双引号来引入标准头文件;不过,我个人的习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。
关于 #include 用法的注意事项:
我们早就学会使用 #include
引入标准头文件了,但是如何使用 #include 引入自定义的头文件呢?下面我们就通过一个例子来简单地演示一下。
本例中需要创建三个文件,分别是 main.c
、my.c
和 my.h
,如下图所示:
演示#include的用法
my.c
所包含的代码:
//计算从m加到n的和
int sum(int m, int n) {
int i, sum = 0;
for (i = m; i <= n; i++) {
sum += i;
}
return sum;
}
my.h
所包含的代码:
//声明函数
int sum(int m, int n);
main.c
所包含的代码:
#include <stdio.h>
#include "my.h"
int main() {
printf("%d\n", sum(1, 100));
return 0;
}
我们在 my.c 中定义了 sum() 函数,在 my.h 中声明了 sum() 函数,这可能与很多初学者的认知发生了冲突:函数不是在头文件中定义的吗?为什么头文件中只有声明?
「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。
#define
叫做宏定义命令
,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
我们先通过一个例子来看一下 #define 的用法:
#include <stdio.h>
#define N 100
int main(){
int sum = 20 + N;
printf("%d\n", sum);
return 0;
}
运行结果:
120
注意第 6 行代码int sum = 20 + N,N被100代替了。
#define N 100
就是宏定义,N为宏名
,100
是宏的内容(宏所表示的字符串)`。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define
完成的,宏替换是由预处理程序完成的。
宏定义的一般形式为:
#define 宏名 字符串
#
表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名
是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。
程序中反复使用的表达式就可以使用宏定义
例如:
#define M (n*n+3*n)
它的作用是指定标识符M
来表示(y*y+3*y)
这个表达式。在编写代码时,所有出现 (yy+3y) 的地方都可以用 M 来表示,而对源程序编译时,将先由预处理程序进行宏代替,即用 (yy+3y) 去替换所有的宏名 M,然后再进行编译。
将上面的例子补充完整:
#include <stdio.h>
#define M (n*n+3*n)
int main(){
int sum, n;
printf("Input a number: ");
scanf("%d", &n);
sum = 3*M+4*M+5*M;
printf("sum=%d\n", sum);
return 0;
}
运行结果:
Input a number: 10↙
sum=1560
sum=3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);
#difine M n*n+3*n
s=3*n*n+3*n+4*n*n+3*n+5*n*n+3*n;
3n2+3n+4n2+3n+5n2+3n
`1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
`2) 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
`3) 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。例如:
#define PI 3.14159
int main(){
// Code
return 0;
}
#undef PI
void func(){
// Code
}
`4) 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include <stdio.h>
#define OK 100
int main(){
printf("OK\n");
return 0;
}
运行结果:
OK
`5) 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:
#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/
对语句:
printf("%f", S);
在宏代换后变为:
printf("%f", 3.1415926*y*y);
`6) 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
`7) 可用宏定义表示数据类型,使书写方便。例如:
#define UINT unsigned int
UINT a, b;
应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
请看下面的例子:
#define PIN1 int *
typedef int *PIN2; //也可以写作typedef int (*PIN2);
从形式上看这两者相似, 但在实际使用中却不相同。
下面用 PIN1,PIN2 说明变量时就可以看出它们的区别:
PIN1 a, b;
在宏代换后变成:
int * a, b;
PIN2 a,b;
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
#define 宏名(形参列表) 字符串
在字符串中可以含有各个形参。
带参宏调用的一般形式为:
宏名(实参列表);
#define M(y) y*y+3*y //宏定义
// TODO:
k=M(5); //宏调用
在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=55+35。
【示例】输出两个数中较大的数。
#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main(){
int x , y, max;
printf("input two numbers: ");
scanf("%d %d", &x, &y);
max = MAX(x, y);
printf("max=%d\n", max);
return 0;
}
运行结果:
input two numbers: 10 20
max=20
MAX
表示条件表达式(a>b) ? a : b
,形参 a、b 均出现在条件表达式中。程序第 7 行max = MAX(x, y)
为宏调用,实参 x、y 将用来代替形参 a、b。宏展开后该语句为:max=(x>y) ? x : y;
`1) 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。例如把:
#define MAX(a,b) (a>b)?a:b
写为:
#define MAX (a,b) (a>b)?a:b
(a,b) (a>b)?a:b
。宏展开时,宏调用语句:max = MAX(x,y);
将变为:
max = (a,b)(a>b)?a:b(x,y);
`2) 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
【示例】输入 n,输出 (n+1)^2 的值。
#include <stdio.h>
#define SQ(y) (y)*(y)
int main(){
int a, sq;
printf("input a number: ");
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
运行结果:
input a number: 9
sq=100
sq=(a+1)*(a+1);
`3) 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。例如上面的宏定义中 (y)*(y) 表达式的 y 都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:
#include <stdio.h>
#define SQ(y) y*y
int main(){
int a, sq;
printf("input a number: ");
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
运行结果为:
input a number: 9
sq=19
sq=a+1*a+1;
#include <stdio.h>
#define SQ(y) (y)*(y)
int main(){
int a,sq;
printf("input a number: ");
scanf("%d", &a);
sq = 200 / SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
sq = 200/SQ(a+1);
input a number: 9
sq=200
sq=200/(a+1)*(a+1);
a 为 9 时,由于“/”和“”运算符优先级和结合性相同,所以先计算 200/(9+1),结果为 20,再计算 20(9+1),最后得到 200。
为了得到正确答案,应该在宏定义中的整个字符串外加括号:
#include <stdio.h>
#define SQ(y) ((y)*(y))
int main(){
int a,sq;
printf("input a number: ");
scanf("%d", &a);
sq = 200 / SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
由此可见,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。
带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
【示例①】用函数计算平方值。
#include <stdio.h>
int SQ(int y){
return ((y)*(y));
}
int main(){
int i=1;
while(i<=5){
printf("%d^2 = %d\n", (i-1), SQ(i++));
}
return 0;
}
运行结果:
1^2 = 1
2^2 = 4
3^2 = 9
4^2 = 16
5^2 = 25
#include <stdio.h>
#define SQ(y) ((y)*(y))
int main(){
int i=1;
while(i<=5){
printf("%d^2 = %d\n", i, SQ(i++));
}
return 0;
}
在 Visual Studio 和 C-Free 下的运行结果(其它编译器的运行结果可能不同,这个++运算的顺序有关):
3^2 = 1
5^2 = 9
7^2 = 25
在示例①中,先把实参 i 传递给形参 y,然后再自增 1,这样每循环一次 i 的值增加 1,所以最终要循环 5 次。
在示例②中,宏调用只是简单的字符串替换,SQ(i++) 会被替换为 ((i++)*(i++)),这样每循环一次 i 的值增加 2,所以最终只循环 3 次。
由此可见,宏和函数只是在形式上相似,本质上是完全不同的。
带参数的宏也可以用来定义多个语句,在宏调用时,把这些语句又替换到源程序中,请看下面的例子:
#include <stdio.h>
#define SSSV(s1, s2, s3, v) s1 = length * width; s2 = length * height; s3 = width * height; v = width * length * height;
int main(){
int length = 3, width = 4, height = 5, sa, sb, sc, vv;
SSSV(sa, sb, sc, vv);
printf("sa=%d, sb=%d, sc=%d, vv=%d\n", sa, sb, sc, vv);
return 0;
}
运行结果:
sa=12, sb=15, sc=20, vv=60
#ifdef 宏名
程序段1
#else
程序段2
#endif
它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
也可以省略 #else:
#ifdef 宏名
程序段
#endif
VS/VC 有两种编译模式,Debug 和 Release。在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。
为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:
#include <stdio.h>
#include <stdlib.h>
int main(){
#ifdef _DEBUG
printf("正在使用 Debug 模式编译程序...\n");
#else
printf("正在使用 Release 模式编译程序...\n");
#endif
system("pause");
return 0;
}
#ifndef 宏名
程序段1
#else
程序段2
#endif
最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
例如,下面的形式只能用于 #if:
#include <stdio.h>
#define NUM 10
int main(){
#if NUM == 10 || NUM == 20
printf("NUM: %d\n", NUM);
#else
printf("NUM Error\n");
#endif
return 0;
}
运行结果:
NUM: 10
#include <stdio.h>
#define NUM1 10
#define NUM2 20
int main(){
#if (defined NUM1 && defined NUM2)
//代码A
printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
#else
//代码B
printf("Error\n");
#endif
return 0;
}
运行结果:
NUM1: 10, NUM2: 20
预处理指令是以#
号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换
。
下面是本章涉及到的部分预处理指令:
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。