C 进阶 — 程序环境和预处理
主要内容
程序的编译和执行环境
C 程序编译和链接
预定义符号
预处理指令 #define
预处理指令 #include
预处理指令 #undef
预处理操作符 # 和 ##
宏和函数对比
命令行定义
条件编译
一 程序的编译和执行环境
ANSI C 存在两个不同环境
1、编译环境,将源代码转换为可执行的机器指令
2、执行环境,实际执行代码
二 C 程序编译和链接
2.1 编译
程序的每个源文件通过编译过程转换为目标文件(object code),并由链接器(linker)捆绑在一起,形成一个完整的可执行程序
链接器会引入程序用到的 C 库函数,及个人函数库,将所需函数链接到程序中
查看编译过程
#include <stdio.h>
int main()
{int i = 0;for(i=0; i<10; i++){printf("%d ", i);}return 0;
}
1、预处理 gcc -E print_arr.c -o print_arr.i
预处理完成后停止,结果保存在 print_arr.i
文件中
2、编译 gcc -S print_arr.c
编译完成后停止,结果保存在 print_arr.s
中
3、汇编 gcc -c print_arr.c
汇编完成后停止,结果保存在 print_arr.o
中
示例代码
//sum.c
int g_val = 2016;
void print(const char *str)
{printf("%s\n", str);
}//test.c
#include <stdio.h>
int main()
{extern void print(char *str);extern int g_val;printf("%d\n", g_val);print("hello bit.\n");return 0;
}
2.2 运行环境
程序执行过程
1、程序必须载入内存中,在有操作系统的环境中,一般由操作系统完成。在独立环境中,程序载入通过可执行代码置入只读内存完成
2、程序开始执行,调用 main 函数
3、执行程序代码,程序使用一个运行栈(stack),存储函数局部变量和返回地址。也可使用静态(static)内存,存储于静态内存中的变量生命周期是整个程序的执行过程
4、终止程序,main 函数正常终止,或意外终止
三 预处理
3.1 预定义符号
__FILE__ //编译的源文件名
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循 ANSI C, 其值为 1,否则未定义
预定义符号是语言内置的,如下例
printf("FILE %s LINE %d\n", __FILE__, __LINE__);
3.2 #define
3.2.1 #define 定义标识符
//语法:
#define name stuff
#define MAX 1000
#define reg register //为 register 关键字, 创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //写 case 语句时自动把 break 补上// 如果定义的 stuff 过长, 可分成几行写, 除最后一行外, 每行后面加一个反斜杠 ( 续行符 )
#define DEBUG_PRINT printf("FILE:%s\tLINE:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
在 define 定义标识符时,能不能在最后加上 ;
如下例
#define MAX 1000;
#define MAX 1000//建议不要加上 ; 容易导致问题, 比如下面场景
if(condition)max = MAX;
elsemax = 0;
3.2.2 #define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
//语法
#define name( parament-list ) stuff
// parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中
注意 参数列表左括号必须与 name 紧邻,两者间有任何空白存在,参数列表就会被解释为 stuff 的一部分
宏示例
#define SQUARE(x) x * xint a = 5;
printf("%d\n" ,SQUARE( a + 1) ); //打印 11
预处理器文本替换时,参数 x 被替换成 a + 1,所以上述语句实际上变成了 printf ("%d\n",a + 1 * a + 1 );
#define SQUARE(x) (x) * (x)int a = 5;
printf("%d\n" ,10 * DOUBLE(a)); //打印 55
替换后 printf ("%d\n",10 * (5) + (5));
因此 #define DOUBLE( x) ( ( x ) + ( x ) )
总结:用于对数值表达式进行求值的宏定义,都应该用上述方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符间不可预料的相互作用
3.2.3 #define 替换规则
#define 定义符号和宏时涉及几个步骤
1、在调用宏时,首先对参数检查,是否包含任何由 #define 定义的符号,如果是它们首先被替换
2、替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换
3、再次对结果文件进行扫描,看它是否包含任何由 #define 定义的符号,如果是则重复上述过程
注意:
1、宏参数和 #define 定义中可以出现其它 #define 定义的符号,但宏不能递归
2、预处理器搜索 #define 定义的符号时,字符串常量内容并不被搜索
3.2.4 # 和
如何把参数插入到字符串中 ?
char* p = "Hello ""World\n";
printf("Hello"" World\n"); //输出 Hello World
printf("%s", p); //输出 Hello World
字符串有自动连接的特点
#define PRINT(FORMAT, VALUE)\printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);
//这里只有当字符串作为宏参数时, 才可以把字符串放在字符串中
使用 # 把一个宏参数变成对应的字符串
int i = 10;
#define PRINT(FORMAT, VALUE)\printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3); //the value of i+3 is 13
代码中的 #VALUE
会预处理器处理为 "VALUE"
## 的作用
## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(num, value) \sum##num += value;
...
int sum5 = 0;
ADD_TO_SUM(5, 10); //作用是:给 sum5 增加 10
注:连接结果必须是合法标识符,否则结果是未定义的
3.2.5 带副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么在使用宏时就可能出现不可预测的后果(副作用指表达式求值时出现的永久性效果)
x+1; //不带副作用
x++; //带有副作用#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//z = ( (x++) > (y++) ? (x++) : (y++));
//x=6 y=10 z=9
3.2.6 宏和函数对比
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个
#define MAX(a, b) ((a)>(b)?(a):(b))
为什么不使用用函数
1、调用函数有开销,宏比函数效率更高
2、函数参数必须声明特定类型,宏是类型无关的
宏的缺点
1、除非宏比较短,否则可能大幅度增加程序长度
2、宏没法调试
3、宏由于类型无关,不够严谨
4、宏可能会带来运算符优先级问题
宏有时可以做函数做不到的事,比如:宏参数可以使用类型,但函数不行
#define MALLOC(num, type)\(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));
宏和函数的对比
一般的命名约定
宏名全部大写
函数名不要全部大写
3.3 #undef
//该指令用于移除一个宏定义
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
3.4 命令行定义
许多 C 编译器允许在命令行中定义符号,用于启动编译过程
例如:当同一个源文件要编译出一个程序的不同版本时(假定某程序中声明了一个某长度数组,如果机器内存有限,需要一个很小数组,但是另外一个机器内存大些,需要一个数组能够大些)
#include <stdio.h>
int main()
{int array [ARRAY_SIZE];int i = 0;for(i = 0; i< ARRAY_SIZE; i ++){array[i] = i;}for(i = 0; i< ARRAY_SIZE; i ++){printf("%d " ,array[i]);}printf("\n" );return 0;
}
编译指令
gcc -D ARRAY_SIZE=10 programe.c
3.5 条件编译
条件编译,在编译程序时将一条语句(一组语句)编译或者放弃编译
#include <stdio.h>
#define __DEBUG__
int main()
{int i = 0;int arr[10] = {0};for(i=0; i<10; i++){arr[i] = i;#ifdef __DEBUG__printf("%d\n", arr[i]); //观察数组是否赋值成功 #endif //__DEBUG__}return 0;
}
常见的条件编译指令
//1.
#if 常量表达式//...
#endif
//常量表达式由预处理器求值
如:
#define __DEBUG__ 1
#if __DEBUG__//..
#endif//2.多个分支的条件编译
#if 常量表达式//...
#elif 常量表达式//...
#else//...
#endif//3.判断是否被定义
#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol//4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
3.6 文件包含
#include 指令可以使另外一个文件被编译,就像它实际出现于 #include 指令的地方一样
替换的方式:
- 预处理器先删除这条指令,并用包含文件的内容替换
- 这样一个源文件被包含 10 次,那就实际被编译 10 次
3.6.1 头文件被包含的方式
本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误
//Linux 环境标准头文件路径
/usr/include//VS2013 环境标准头文件路径
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
3.6.2 嵌套文件包含
如下图 comm.h
和 comm.c
是公共模块。test1.h
和 test1.c
使用了公共模块,test2.h
和 test2.c
使用了公共模块,test.h
和 test.c
使用了 test1
模块和 test2
模块。这样最终程序中就会出现两份 comm.h
内容,造成了文件内容重复
使用条件编译解决上述问题
每个头文件的开头写
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
或者
#pragma once
避免头文件的重复引入
四 其他预处理指令
#error
#pragma
#line
可参考《C语言深度解剖》学习