程序环境和预处理
前言
本文将学习进阶阶段的最后一些内容了,程序环境和预处理,主要内容包括:
- 程序的翻译环境和执行环境
- 详解C语言程序的编译和链接
- 预定义符号介绍
- 预处理指令 #define
- 宏和函数的对比
- 预处理操作符#和##的介绍
- 命令定义
- 预处理指令 #include
- 预处理指令 #undef
- 条件编译
1、程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境:
- 翻译环境,在这个环境中源代码被转换为可执行的机器指令
- 执行环境,它用于实际执行代码
2、详解C语言程序的编译和链接
2.1 翻译环境
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数
- 而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
2.2 编译具体阶段
//sum.c
int g_val = 2022;
void print(const char *str)
{
printf("%s\n", str);
}
//test.c
int main()
{
extern void print(char *str);
extern int g_val;
printf("%d\n", g_val);
print("hello bit.\n");
return 0;
}
2.3 查看编译每一步
test.c
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", i);
}
return 0;
}
使用linux gcc查看:
- 预处理 选项 gcc -E test.c -o test.i
- 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中
- 编译 选项 gcc -S test.c
- 编译完成之后就停下来,结果保存在test.s中
- 汇编 gcc -c test.c
- 汇编完成之后就停下来,结果保存在test.o中
2.4 程序执行的过程
- 程序必须载入内存中
- 在有操作系统的环境中:一般这个由操作系统完成
- 在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
- 程序的执行便开始,接着便调用main函数
- 开始执行程序代码
- 这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址
- 程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
- 终止程序,正常终止main函数;也有可能是意外终止
3、预处理
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__ )
3.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中:
- 参数列表的左括号必须与name紧邻
- 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用:
#define SQUARE(x) (x) * (x) //需要注意多加括号
printf ("%d\n",(a + 1) * (a + 1) );
3.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
- 替换文本随后被插入到程序中原来文本的位置
- 对于宏,参数名被他们的值所替换
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号
- 如果是,就重复上述处理过程
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号
- 但是对于宏,不能出现递归
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
3.2.4 #和##
char* p = "hello ""bit\n";
printf("hello"," bit\n");
printf("%s", p);
#define PRINT(n) printf("the value is "#n" is %d\n", n)
int main()
{
int a = 10;
PRINT(a);
//相当于printf("the value of ""a"" is %d\n", a)
return 0;
}
#define PRINT(N, format) printf("the value of "#N" is "format"\n", N)
int main()
{
int a = 20;
double pai = 3.14;
PRINT(a, "%d");
PRINT(pai, "%lf");
return 0;
}
#define cat(name,num) name##num
int main()
{
int class105 = 105;
printf("%d\n", cat(class, 105));
return 0;
}
3.2.5 带副作用的宏参数
具有副作用的参数所引起的问题:
x+1;//不带副作用
x++;//带有副作用
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 5;
int b = 8;
int c = MAX(a++, b++);
//int c = ((a++) > (b++) ? (a++) : (b++));
printf("%d\n", c);//
printf("%d\n", a);//
printf("%d\n", b);//
return 0;
}
3.2.6 宏和函数对比
宏:
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 0;
int b = 20;
int c = 0;
c = MAX(a, b);
return 0;
}
函数:
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 0;
int b = 20;
int c = 0;
c = MAX(a, b);
return 0;
}
#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));
宏和函数的对比:
#define定义宏 | 函数 | |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
3.2.7 命名约定
- 把宏名全部大写
- 函数名不要全部大写
3.3 #undef
这条指令用于移除一个宏定义
#undef NAME
#define MALLOC(num ,type) (type*)malloc(num*sizeof(type))
int main()
{
int*p = (int*)malloc(10 * sizeof(int));
int*p2 = MALLOC(10, int);
//int *p2 = (int*)malloc(10*sizeof(int));
#undef MALLOC
MALLOC(20, char);
return 0;
}
3.4 命令行定义
允许在命令行中定义符号。用于启动编译过程
3.5 条件编译
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 OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
#define NUM 8
int main()
{
#if NUM==1
printf("hehe\n");
#elif NUM == 2
printf("haha\n");
#else
printf("heihei\n");
#endif
return 0;
}
3.6 文件包含
#include 指令可以使另外一个文件被编译,替换方式:
- 预处理器先删除这条指令,并用包含文件的内容替换
- 一个源文件被包含10次,那就实际被编译10次
3.6.1 头文件被包含的方式
本地头文件,或者自定义头文件
#include "filename
查找策略:
- 先在源文件所在目录下查找
- 如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
- 如果找不到就提示编译错误
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
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
就可以避免头文件的重复引入。
4、其他预处理指令
#error
#pragma
#line
...
#pragma pack()在结构体部分介绍
总结
本文的内容大家可以参考下面几本书:
- 《C语言深度解剖》
- 《高质量C/C++编程指南》
- 《程序员的自我修养》
经过基础阶段和进阶阶段的学习,推荐大家看 《C程序设计语言》(第2版.新版) :
- 这本书不到300页,内容非常精炼,不适合没学过C的直接看
- 有一定的基础再看,就能更加深刻的理解书中的知识点
到目前位置,C语言阶段的知识学习基本结束了。但是学过不代表会了,要经常复习,温故而知新。还要练习题目,巩固所学知识。推荐牛客网的在线编程的题目:牛客在线编程—基本语法。
将自己不熟悉的知识点记录下来,写成博客分享到 CSDN,形成一个良性循环,坚持总会有收获的。
下一个阶段开始学习新的内容了:初阶数据结构和算法,是用C语言实现。