程序的翻译环境和执行环境 ( 在ANSI C的任何一种实现中,存在两个不同的环境)
第1种是翻译环境:在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境:它用于实际执行代 码。
一.生成一个程序的过程可以分为四个步骤:预处理----->编译----->汇编----->链接
-
预处理:gcc -E test.c -o test.i( 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件 中)
①:展开头文件
②:宏替换
③:条件编译
④:去掉注释 -
编译:gcc -s test. i -o test.s(编译完成之后就停下来,结果保存在test.s中)
①:检查语法
②:生成汇编代码 -
汇编:gcc -c test.i -o test.o(汇编完成之后就停下来,结果保存在test.o中。)
①:将汇编代码生成二进制代码 -
链接:gcc test.o -o test
①:链接生成可执行程序(注:在windows下生成为:xx.exe;在unix下生成为:xx.out)
二、预处理部分详解
1.预定义符号
—FILE— //进行编译的源文件
—LINE— //文件当前的行号
—DATE— //文件被编译的日期
—TIME— //文件被编译的时间 (注意:该时间指的是编译时的时间)
—STDC— //如果编译器遵循ANSI C,其值为1,否则未定
举个例子:
2.#define(命名约定:把宏名全部大写 函数名不要全部大写)
(1)#define定义的标识符(语法:define name stuff)
举个例子:
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n",\
__FILE__, __LINE__,\
__DATE__, __TIME__)
// 如果定义的 stuff过长,可以分成几行写,除了后一行外,每行的后面都加一个反斜杠(续行符)。
还要注意的一个问题:续行符后面不能跟任何东西;如果分成几行写,不加续行符,那么只能替换一行;续行符后面不允许有空格!!!
例外,在define定义标识符的时候,建议不要在后面加上分号(既 ;),加上可能会引起语法问题。
(2)#define 定义宏
#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现 在stuff中。 注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部 分。
举个容易出错的例子:
上面这个例子看似计算出来了,其实算出的答案是不是我们想要的,我们知道真正想要的答案应该是(5+1)(8+1)=54;而按照上面这个例子程序计算结果为5+18+1=14。在宏定义上加上括号就可以解决,如下得出正确结果:
但是还有一种情况也会产生错误,举个例子:
我们其实想要的答案是(5+8)* 2=26,但是程序计算出结果为5+ 8 * 2=21,在宏定义上加上括号的基础上再给宏定义表达式加上括号就可以解决了。如下得出正确结果:
综上可以得出:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符 或邻近操作符之间不可预料的相互作用
(3)define替换
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
- 后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过 程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define SQUARE(a,b) ((a) + (b))
int main()
{
char* p = "hello""world";
printf("%s\n", p);//输出为helloworld
system("pause");
return 0;
}
程序输出为"helloworld",可以得出字符串是有自动拼接的特点。则可以这样子写代码:
①:有当字符串作为宏参数的时候才可以把字符串放在字符串中
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define PRINT(FORMAT,VALUE) printf("the value is "FORMAT" \n",VALUE)
int main()
{
PRINT("%d", 20);
system("pause");
return 0;
}
②: 使用 # ,把一个宏参数变成对应的字符串。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define PRINT(FORMAT,VALUE) printf("the value of "#VALUE" is "FORMAT" \n",VALUE)
int main()
{
int i = 20;
PRINT("%d", i+20);
system("pause");
return 0;
}
③:使用##,##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define SWAP(type,name,a,b)\
type c##name = a;\
a = b;\
b = c##name;
int main()
{
int x1 = 2;
int x2 = 5;
printf("%d %d\n", x1, x2);
SWAP(int, 1, x1, x2);
printf("%d %d\n", x1, x2);
SWAP(int, 2, x1, x2);
printf("%d %d\n", x1, x2);
system("pause");
return 0;
}
是将 c 与 1 拼为 c1 ; c 与 2 拼为 c2(比较特殊)
(4)宏和函数的比较
宏通常被应用于执行简单的运算,优点有二:
①:用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程 序的规模和速度方面更胜一筹
②:更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎 可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的
例外,宏相比函数也有劣势的地方:
①: 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
②: 宏是没法调试的。
③:宏由于类型无关,也就不够严谨。 4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到,举个例子,比如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define MALLOC(type,num)\
(type *)malloc(num * sizeof(type))
int main()
{
int* p = MALLOC(int,100);//预处理替换之后MALLOC(int,100)相当于(int *)malloc(100 * sizeof(int));
if (p != NULL)
{
printf("malloc成功\n");
}
system("pause");
return 0;
}
(5)带副作用的宏参数
X+1;//不带副作用
X++;//带副作用
举个X++带副作用的例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int x1 = 5;
int x2 = 8;
int max1 = MAX(x1++, x2++);
printf("%d %d %d\n",x1,x2,max1);//6,10,9
// MAX(x1++, x2++)预处理后为:max1=((x1++)>(x2++)?(x1):(x2) 其中x1只被加了一次,x2被加了两次
system("pause");
return 0;
}
MAX(x1++, x2++)预处理后为:max1=((x1++)>(x2++)?(x1):(x2) ,其中x1只被加了一次,x2被加了两次。为了改变宏的这种结果,我们可以将 x1++换成X1+1, x2++换成X2+1:
这个才是我们真正想要得到的结果。
(6)#undef(这条指令用于移除一个宏定义)
例子: #undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
(7)条件编译( 注意:条件编译是在预处理时候处理)
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。常见的条件编译指令:
①:
#define _DEBUG_
#ifdef _DEBUG_
//.......
#endif
上下对比:
②:
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//.. 。
#endif
③多个分支的条件编译:
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
④判断是否被定义:
#if defined(symbol)
#ifdef symbol
#if !defined(sumbol)
#ifndef symbol
除了以上列举,and so on…
(8)文件包含
1.头文件被包含的方式:
①:本地文件包含(比如:#include" func.h ")
先在源文件所在目录下查找,如果未找到——>然后再到系统目录下去查找。如果在找不到就提示编译错误。
②:库文件包含(比如:#include< stdio.h >)
直接到系统目录下查找。,如果找不到就提示编译错误。
注意: 其实对于库文件也可以使用 “ ” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
2.嵌套文件包含
如何解决嵌套文件造成的文件内容重复?——>可以用 条件编译 来解决这个问题
每个头文件的开头写:
#ifndef __TEST_H__ //ifndef是判断是否有define
#define __TEST_H__ //define定义一个宏
//头文件的内容
#endif //__TEST_H__ //结束条件编译
或者
#pragma once
这样就可以避免头文件的重复引入