文章目录
第七章 程序的编译
程序翻译环境和执行环境
翻译环境
翻译环境
编译分为几个阶段
运行环境
- 预处理详解
预定义符号
#define
#undef
命令行定义
条件编译
文件包含
- 其他预处理指令
1.程序翻译环境和执行环境
翻译环境:在该环境中源代码被”翻译“成可执行的机器指令(二进制代码)。
执行环境:应用于实际执行代码的环境。
2.编译和链接
2.1 翻译环境
组成一个程序的每个源文件要通过编译过程分别转换成目标代码。
每个目标文件由链接器捆绑在一起,形成一个单一完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,它可以搜索程序员个人的程序库,将需要的函数也链接到程序当中。
2.2 编译分为几个阶段:
prt.c:
unsigned int g_val = 10;
void print(const char* str)
{
printf("%s", str);
}
test.c
#include <stdio.h>
int main()
{
extern g_val;
extern print(const char* str);
printf("%d\n", g_val);
printf("hello\n");
return 0;
}
test.c
#include <stdio.h>
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", i);
}
return 0;
}
- 预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
- 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。
- 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
2.3 运行环境
程序执行过程:
程序必须要载入内存中。在操作系统的环境中:一般由操作系统完成。在独立的环境中,程序载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
程序的执行就开始。接着便调用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__ )
建议在define定义标识符的时候,最后不要加上;
不然容易导致问题。
#include <stdio.h>
#define MAX 10;
#define MAX 10
int main()
{
int max = 0;
if (1)
{
max = MAX;
}
else
{
max = 0;
}
return 0;
}
3.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的申明方式:
#define Add(a,b) (a + b)
在上述声明后,Add(1,2)
预处理器就会用以下表达式替换以上表达式
1 + 2
该宏存在一个问题:
int a = 1;
int b = 3;
printf("%d\n", Add(a+4, b+2);
一看,你可能觉得该代码打印25。
其实,编译后打印的是15。
在替换文本时,参数a被替换成a+4,参数b被替换成b+2,该语句实际变成了
a + 4 * b + 2;
由于替换所产生的表达式没按照预想的顺序进行求值。
在宏定义中加上两个括号(),就能解决该问题了。
#define Mul(a,b) ((a) * (b))
还有一个宏定义:
#define Add(a,b) (a) + (b)
int a = 2;
int b = 3;
printf("%d\n", 5*Add(a, b));
看上去好像打印的是25。其实打印的是13
解决这个问题,只需要在宏定义表达式两边加上俩括号就行了。
#define Add(a,b) ((a) + (b))
总结:
在用于对数值表达式进行求值的宏定义都该用该方式加上括号,避免使用时参数中的操作符或者是邻近操作符之间进行不可预料的相互作用。
3.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要几个步骤:
1.调用宏时,首先要对参数进行检查,查看是否包含了由#define定义的符号。如果有,那么它们首先被替换。
2.替换文本后被插入到程序中原来文本的位置。宏和参数名会被它们的值所替换。
3.最后,再对结果文件扫描,查看是否包含任何由#define定义的符号。如果由,就会继续重复上述的处理过程。
注意:
1.宏参数和#define定义中可出现其他#define定义的变量。但对于宏,不能出现递归。
2.预处理器搜索#define定义的符号时,字符串常量的内容不被搜索。
3.2.4 #和##
如何把参数插入字符串:
#include <stdio.h>
int main()
{
char* p = "Hello ""World";
printf("Hello" " World\n");
printf("%s\n", p);
return 0;
}
这里输出的是:Hello World
可以发现字符串有自动连接的特点。
那可以这么写代码:
#include <stdio.h>
int main()
{
#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE);
PRINT("%d", 10);
}
只有当字符串当作宏参数的时候才可把字符串放字符串中。
#:把一个宏参数变成对应的字符串
#include <stdio.h>
int main()
{
int i = 5;
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
PRINT("%d", i + 1);
return 0;
}
在预处理器处理中#VALUE会被处理为“VALUE”.
输出结果:the value of i + 1 is 6
##:它可以把位于它两边的符号合为一个符号。还允许宏定义从分离的文本片段创建标识符。
#define ADD_SUM(num, value) sum##num += value;
...
ADD_SUM(3, 2);//作用:给sum3增加2.
这样的连接需要产生一个合法的标识符,不然其结果是未定义的。
3.2.5 带副作用的宏参数
宏参数在宏定义中出现超过一次时,如果参数带有副作用,那么在使用该宏时可能会出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
如:
n+1;//无副作用
n++;//带副作用
MAX宏可证具有副作用的参数所引起的问题:
#include <stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
预处理器处理后的结果:z = ( (x++) > (y++) ? (x++) : (y++));
输出结果:x=6 y=10 z=9
3.2.6 宏和函数两者的对比
宏常被应用于执行简单的运算。
为什么不用函数来完成?
原因:
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需时间更多。所以宏比函数在程序的规模和速度方面更好一些。
更重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上去使用。宏就可适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的。
宏与函数函数也有劣势的地方:
每次使用宏时,一份宏定义的代码将插入到程序中,除非宏较短,不然可能大幅度增加程序长度。
宏没法调试。
宏由于类型无关,所以不够严谨。
宏可能会带来运算符优先级的问题,导致程序容易出现错误。
宏有时可做函数做不到的事。如:宏参数可以出现类型,但函数做不到。
#include <stdio.h>
#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
int main()
{
MALLOC(10, int);
//预处理器替换后:
(int *)malloc(10 * sizeof(int));
}
宏和函数的一个对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了很小的宏之外,程序的长度可能会大幅度增长 | 函数代码只出现在一个地方,每次使用该函数时,都会调用那个地方的同一份代码 |
执行速度 | 更加快速 | 存在函数的调用和返回的额外开销,相对慢一些 |
操作符优先级 | 宏参数求值是在所有周围表达式的环境里,除非加上括号,不然邻近操作符的优先级可能会产生不可预料的后果。 | 函数参数只在函数调用的时候求值一次,它的结果值直接传递给函数。求值的结果更好预测些。 |
带副作用的参数 | 参数可能会被替换到宏体中的多个位置,带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更加可控。 |
参数类型 | 宏参数与类型无关,只要对参数的操作合法,它就可使用于任何参数类型。 | 函数参数与类型有关,如果参数类型不同,就要不同的函数,即使执行的任务是不同的。 |
调试 | 宏不方便调试 | 函数可以逐语句调试 |
递归 | 宏不能递归 | 函数可以递归 |
- 命名约定
一般来说函数和宏的使用语法比较相像,语言没法帮我们区分。
所以养成这么个习惯:
宏名全部大写
函数名不要全部大写
3.3 #undef
作用:移除一个宏定义。
#undef NAME
//如果现存的一个名字要被重新定义,那么它的旧名字首先会被移除。
3.4 命令行定义
许多C编译器提供了该功能,允许在命令行中可以定义符号。用来启动编译过程。
当我们根据同一个源文件要编译不同的程序的不同版本时,这个功能就有点用。(假如在某个程序中声明了一个某长度的数组,机器的内存有限,我们需要一个较小的数组,但是另外个机器内存大一点,我们需要一个数组能够大一点。)
#include <stdio.h>
int main()
{
int arr[SIZE];
int i = 0;
for(i = 0 ; i < SIZE; i++)
{
arr[i] = i;
}
for(i = 0 ; i < SIZE; i++)
{
printf("%d ", arr[i]);
}
printf("\");
return 0;
}
编译指令:
gcc -D SIZE=10 test.c
3.5 条件编译
在编译一个程序时我们如果要将一条语句编译或者放弃可以使用条件编译指令。
有时我们会面临一段代码,删除可惜,保留着又碍事,那我们可以选择性编译。
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0;i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d ", 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 OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
3.6 文件包含
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 嵌套文件包含
test.c和test.h是公共模块。
test1.c和test1.h使用了公共模块。
test2.c和test2.h使用了公共模块。
test3.c和test3.h使用了test1模块和test2模块。
最终程序会出现两份test.h的内容,造成了文件内容的重复。
解决:
可以用条件编译来解决该问题。
每个头文件的开头写上:
#ifndef __TEST_H__
#define __TEST_H__
//头文件内容
//endif //__TEST__H
或者:
#pragma once
笔试题:
1. 头文件中的 ifndef/define/endif是干什么用的?
2. #include <filename.h> 和 #include "filename.h"有什么区别?
4.其他预处理指令
#error
#pragma
#line
---
参考《C语言深度解剖》学习
练习:
1.写一个宏,可以将一个整数的二进制数的奇数位和偶数位交换。
#include <stdio.h>
//偶位数右移一位 + 奇数位左移一位
//获得偶数位 获得奇数位
//10101010101010101010101010101010 //01010101010101010101010101010101
#define SWAP(N) ((N & 0xaaaaaaaa) >>1) + ((N & 0x55555555) <<1)
int main()
{
int N = 10;
int ret = SWAP(N);
printf("%d\n", ret);
return 0;
}
2.写一个宏,计算结构体中变量相对于首地址的偏移
考察:offsetof宏的实现
#include <stdio.h>
#define OFFSETOF(struct_name,mem_name) (int)&(((struct_name*)0)->mem_name)
struct A
{
int a;
short b;
int c;
char d;
};
int main()
{
printf("%d\n", OFFSETOF(struct A, a));
printf("%d\n", OFFSETOF(struct A, b));
printf("%d\n", OFFSETOF(struct A, c));
printf("%d\n", OFFSETOF(struct A, d));
return 0;
}
上一章:C语言深入学习 — 6.文件操作
配套练习:
C语言练习题110例(一)
C语言练习题110例(二)
C语言练习题110例(三)
C语言练习题110例(四)
C语言练习题110例(五)
C语言练习题110例(六)
C语言练习题110例(七)
C语言练习题110例(八)
C语言练习题110例(九)
C语言练习题110例(十)
C语言练习题110例(十一)