目录
- 程序的翻译环境
- 详解编译+链接
- 梳理:
- 程序的执行环境
- 预处理详解
- 预定义符号
- #define
- 一些你没使用过的五花八门的宏定义
- 错误示范
- 带参数的宏定义
- #和##
- 带副作用的宏参数
- 宏和函数的对比
- 预处理指令 #undef
- 条件编译
- 常见的条件编译指令:
- 1、
- 2.多个分支的条件编译
- 判断是否被定义3种写法
- 嵌套指令
- 文件包含
程序的翻译环境
-
程序的翻译环境和执行环境 在ANSI C的任何一种实现中,存在两个不同的环境。
-
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
-
第2种是执行环境,它用于实际执行代码。
-
假设你要处理一个2 + 3 的简单加法计算你的程序会经过预编译、编译、汇编、最终链接生成一个可执行程序,程序运行输出结果5
详解编译+链接
翻译环境:
一个c语言的源文件会经过编译器的处理(预编译、编译、汇编)生成目标文件(.obj),再将生成的目标文件通过链接库最终链接到一块生成可执行程序,在vs底下他的编译器是cl.exe,链接器是link.exe
梳理:
- 1、一个源文件要经过链接和编译生成一个可执行程序,最终就可以运行。
- 2、而编译又分为预编译 + 编译 + 汇编生成目标文件,最终经过链接器的处理再加上链接库将目标文件链接到一起形成可执行程序
何为链接库呢?
假设你想使用一个库函数,这个库函数是会被包含在库里头的,当你想要使用这个函数那么这些库会被链接到你的工程中去
程序预编译做了哪些事情?
预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。
链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
文本操作
- 1、#include头文件的包含
- 2、#define 定义符号的替换
- 3、删除注释
程序编译做了哪些事情?
将c语言代码转换为汇编代码
- 1、语法分析
- 2、词法分析
- 3、语义分析
- 4、符号汇总
汇编的过程做了哪些事
- 1、将汇编代码转换成了二级制指令(机器指令),
- 2、形成符号表
链接的过程做了哪些事
- 1、合并段表
- 2、符号表的合并和重定位
程序的执行环境
运行环境
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。
- 在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同 时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止
预处理详解
预定义符号
-
FILE //进行编译的源文件
-
LINE //文件当前的行号
-
DATE //文件被编译的日期
-
TIME //文件被编译的时间
-
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
以下简单地使下这几个预定义符号,给读者看看测试效果
一个测试代码
void functest()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for (int i = 0; i < 10; i++)
{
//按照格式化的形式将数据输出到文件当中
printf("file:%s line:%d arr[%d] = %d\n",
__FILE__, __LINE__,i,arr[i]);
}
}
在这里不仅可以看到程序在哪个文件下编译,还可以现在printf打印所处的这一行
多使用几组预定义符号
函数功能是将格式化的数据写入到文件当中
void functest()
{
FILE *pf = fopen("C:\\Users\\26961\\Desktop\\data.txt","w");
if (pf == NULL)
{
perror("fopen: file");
exit(-1);
}
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for (int i = 0; i < 10; i++)
{
//按照格式化的形式将数据输出到文件当中
fprintf(pf,"file:%s line:%d date: %s time : %s arr[%d] = %d\n", __FILE__, __LINE__, __DATE__, __TIME__,i,arr[i]);
}
}
呈现的效果
#define
#define 定义标识符
语法:
#define int INT
#define 定义INT,相当于给int取了一个小名叫INT,但是INT和int是同一个人是一回事,在预处理阶段只是将INT进行文本替换成int
一些你没使用过的五花八门的宏定义
#define reg register //为 register这个关键字,取别名reg
#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 CASE break;case
#define CASE break;case
void functest()
{
int input = 0;
scanf("%d",&input);
switch (input)
{
case 1:
//语句
CASE 2 :
//语句
//预处理展开后 break;case 2:
CASE 3 :
//语句
预处理展开后 break;case 3:
CASE 4 :
//语句
预处理展开后 break;case 4:
; //分号结束
}
}
在使用switch语句的时候 #define CASE break;case 是可行的,会将#define定义的符号替换成break;case,但是不建议这么写,可读性不好,但是确确实实是#define 的一个强大之处
解释#define do_forever for(; ; )
#define do_forever for(;;)
int main()
{
do_forever for(;;)
printf("hello c");
}
用更形象的符号来替换一种实现,实现的功能是循环打印hello c,并且程序不会终止
再来看一个
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,)\
__DATE__,__TIME__ )
后面的反斜杠表示的是一个续行符,只有在写#define 定义标识符的时候才会出现的语法,但是值得注意的是千万不要在续行符的后面加上空格、回车之类的,如果反斜杠跟空格回车组合在一起会成转义字符,那么你的程序可能会出问题,小心使用即可
错误示范
#define MAX 1000;
void functest()
{
int max = 0;
if (1)
max = MAX;
//宏展开后max = 1000;;
else
max = 0;
}
if语句并没有带{},所以他只针对一行语句有效,可是宏展开后max = 1000;; ,在c语言中以分号结束的可以看出是一条空语句,这样就会导致if和else没有匹配上的问题,除非用{}把 max = MAX;括起来
带参数的宏定义
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)
容易坑人的写法
#define SQUARE(x) x * x
void functest()
{
printf("%d \n", SQUARE(5));
printf("%d \n", SQUARE(4 + 1));
}
写成第一种确实不会有什么问题,但是当写成第二种的时候会被替换成
4 + 1 * 4 + 1 = 9,原因是在预处理阶段会将4 + 1直接替换x,x并不看作是一个整体
稍作修改
#define SQUARE(x) (x) * (x)
void functest()
{
printf("%d \n", SQUARE(5));
printf("%d \n", SQUARE(4 + 1));
}
(x) * (x)即使被替换了也是(4 + 1) * (4 + 1),并不会影响最后的结果,因为x已经被看作是一个整体了
#define 替换规则
-
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
-
1、 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
-
2、 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
-
3、 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
#和##
使用 # ,把一个宏参数变成对应的字符串
#define PRINTF(x) printf("the value of "#x" is %d\n",x);
void functest()
{
int a = 100;
PRINTF(a)
}
#的作用不将参数名替换成100,而是保留参数名,将这个参数名变成一个字符串
执行结果
看一个复杂的宏定义
#define PRINTF(FORMAT,x)\
printf("the value of "#x" is "FORMAT"\n",x);
#x :保留参数名
FORMAT:指定格式
void functest()
{
int a = 100;
int b = 200;
float f = 5.51;
PRINTF("%d",a)
PRINTF("%d",b)
PRINTF("%f",f)
}
这个宏的功能是按照指定格式进行输出
程序运行结果
##
- ##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符
#define CAT(x,y) x##y
void functest()
{
int class102 = 100;
printf("%d",CAT(class,102));
}
程序功能是将两个标识符合并成一个标识符,再对他进行宏替换,x##y替换的就是class102,打印结果就是100
程序运行结果
带副作用的宏参数
- 当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如:
x+1;//不带副作用
x++;//带有副作用
#define MAX(x,y) ((x) > (y) ? (x):(y))
void functest()
{
int a = 10;
int b = 11;
int ret = MAX(a++,b++);// ((a++) > (b++) ? (a++) : (b++))
printf("%d\n", ret);//12
printf("%d %d",a,b);//11,13
}
宏的两个参数是a++和b++,当它们传递过去后a++会替换x,b++会替换y在宏展开的时候就变成了 ((a++) > (b++) ? (a++):(b++)),由于是后置++,先使用在++,10 > 11 ? 执行的是后面的那个表达式所以b++会被执行两次,而a++只会在做判断的时候执行一次,
程序运行结果
宏和函数的对比
宏和函数对比:
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务? 原因有二:
- 1、用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序 的规模和速度方面更胜一筹。
- 2、更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可 以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
为了方便大家认清宏和函数的区别这里举例一个例子出来,请看下面代码
//从以下程序来看宏确实是要比函数好的,因为他写的更加的简洁
#define MAX(a, b) ((a)>(b)?(a):(b))
int Max(int x,int y)
{
return x > y ? x : y;
}
int main()
{
//举例一:
printf("max = %d ",Max(1, 3));//函数
printf("max = %d",MAX(1,3));//宏
//举例二:
printf("max = %f ",Max(1.5, 3.6));//函数
printf("max = %f",MAX(1.5,3.6));//宏
return 0;
}
第一个例子看得出宏确实要比函数好,好在哪里好在够简洁,即使是一个简单得计算可能花费的时间就需要1毫秒,但是如果使用函数的话,函数的调用以及返回都是需要占用时间的这个过程会花费2秒的时间,而同样是计算一个max的值带参数的宏定义只需要花费1秒(从汇编代码的角度观察)
但是从例子二来看的话并不是如此,预处理期间会将宏进行文本替换,他还是一个浮点数,但是Max函数就不一样了,他的两个参数类型是int,浮点型数据传递过去会丢失小数从而变成1和3,即使返回结果却也发了变化
当然和宏相比函数也有劣势的地方:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
解释:
1、宏会大幅度增加程序的长度? 使用宏的时候在预处理阶段会将宏替换成文本,代码长度增加可想而知
2、宏是没法调试的?预处理阶段排在程序执行前,预处理阶段完成的替换工作后,才会将代码插入到程序中去,我们调试的时候其实用的是编译后的代码,所以宏没法调试
3、宏由于类型无关,也就不够严谨?在上一个例子里面已经解释过
4、宏可能会带来运算符优先级的问题?SQUARE(x) x * x 这个例子x并不看作整体
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
//num :要开辟几个type大小的空间
//type:类型
#define Malloc(num,type) (type*)malloc(num * sizeof(type));
//预处理替换后:(int *)malloc(10 * sizeof(int));
int main()
{
//使用宏定义开辟40个字节的空间
int *p = Malloc(10,int);
return 0;
}
宏和函数的区别
命名约定
宏名全部大写 ,函数名不要全部大写
预处理指令 #undef
功能:这条指令用于移除一个宏定义。
#define MAX 100
int main()
{
printf("%d\n",MAX);//MAX被替换成100,正常打印
#undef MAX //取消MAX的宏定义
printf("%d\n",MAX);//err,MAX未定义
return 0;
}
条件编译
条件编译
- 在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说: 调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
#include <stdio.h>
#define __PRINTR__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __PRINTR__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
#ifdef __PRINTR__这句预处理指令的意思是如果定义了__PRINTR__这个符号那么就对printf("%d\n", arr[i]);这行代码进行编译,在防止头文件重复包含经常用的是条件编译
常见的条件编译指令:
1、
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
//使用场景
void functest()
{
#if 1
printf("1\n");
#elif 2
printf("2\n");
#else
printf("3\n");
#endif
}
多个分支的条件编译如果一个条件成立就只对这个条件的下面这行语句进行编译,其他分支结构的语句不参与编译,这里可以看到只有第一条语句参与编译,后面的语句都变灰色
判断是否被定义3种写法
3.
//判断是否定义了symbol
#if defined(symbol)
//需要编译的的语句
#endif
//判断是否定义了symbol
#ifdef symbol
//需要编译的的语句
#endif
//判断是否定义了symbol
#if !defined(symbol)
//需要编译的的语句
#endif
//判断是否定义了symbol
#ifndef symbol
//需要编译的的语句
#endif
各种形式判读宏定义的条件语句,读者觉得哪个适合自己就用哪个
嵌套指令
4.
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif //结束OPTION1这个条件判断
#ifdef OPTION2
unix_version_option2();
#endif //结束OPTION2这个条件判断
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif //结束OPTION2这个条件判断
#endif//结束整个条件判断
类似于if else的语句
文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
要想一个头文件只需要包含一次,以供自己的需求,重复包含多分头文件会造成重复代码太多,因为预处理阶段都会被替换,使用条件编译是一个不错的方法在linux环境下可以细节查看被重复包含的文件的展开形式
假设A程序员写了两份代码,B和C程序员都需要使用,,最终D程序员在拼接这B和C的两个模块的时候就会被会重复包含两次
解决方案:条件编译
#ifndef __TEST_H__ //判断需不需要包含头文件
#define __TEST_H__
//包含头文件
#include"comm.h"
#endif //__TEST_H__
理解#ifndef TEST_H ,如果没有定义__TEST_H__ 判断条件为真,那么就会执行#define TEST_H, 定义__TEST_H__ ,最后包含头文件
现代的一种写法
#pragma once
#include"comm.h"
总结:
头文件中的 ifndef/define/endif是防止头文件重复包含
头文件被包含的方式:
他的查找策略会分两种,先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误
库文件包含
#include <filename.h>
本地文件包含
#include "filename.h"
对于库文件也可以使用 “” 的形式包含? 答案是肯定的,可以。 但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了
完