一.程序的翻译和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境,它用于实际执行代码。

二.c语言程序的编译和链接

从总体结构上来讲,编译器会将每一个源文件进行单独编译,最终形成一个目标文件(后缀为.obj)
然后,链接器将多个目标文件和所需要的链接库进行链接形成可执行程序(后缀为.exe)

编译又可分为三个阶段

1.预编译阶段 :

预编译的目的是处理预编译指令

具体规则如下 :

(1).处理 “#include"预编译指令,将”#include"所包含的文件插入到该预编译指令的位置
(2).删除所有的注释
(3).将 #define所定义的符号进行替换
(4).处理所有的条件预编译指令,如 “#if” “#ifdef” “#elif” “#else” “#endif”
(5).保留所有的 #pragma编译器指令,因为编译器要使用它们.

经过预编译阶段,生成后缀名为 .i 的文件

2 .编译阶段 :

编译阶段的目的是将 c语言代码转换成汇编代码

程序在编译阶段所做的事情主要有以下几件 :

(1).词法分析

源代码会被输入到扫描器,扫描器扫描过后会产生一些记号,这些记号为 关键字 ,标识符 , 字面量以及一些特殊符号,与此同时,扫描器也会完成其他工作,如将标识符放到符号表中,字面量放到文字表中.

(2).语法分析

语法分析器对扫描器扫描所产生的记号进行语法分析,检查是否有语法错误

(3).语义分析

前面的语法分析器仅仅对语法进行了检查,但对语句是否有意义并未做检查,语义分析器对语句是否有意义进行检查

(4).符号汇总

对全局变量和函数进行汇总

经过编译阶段,生成后缀名为 .s 的文件

3.汇编阶段 :

汇编阶段的目的是将汇编代码转换成二进制指令,即最终得到的目标文件(后缀为.obj)

在汇编阶段将汇总的符号形成符号表

链接阶段

链接阶段的目的是将目标文件及链接库通过链接器形成可执行程序

程序在链接阶段所做的事情 :
(1).合并段表

目标文件是按照 elf 文件格式进行组织的,elf 文件结构由多个段组成 ,合并段表就是将目标文件的相同段进行合并

(2).符号表的合并和重定位

三.预定义符号介绍

__FILE__     //进行编译的源文件 
__LINE__     //文件当前的行号 
__DATE__     //文件被编译的日期 
__TIME__     //文件被编译的时间
 __STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

用例 :

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
}

四.预处理指令 #define

(1). #define定义标识符

#define name stuff

用例 :

#define MAX 100
#define DEBUG_PRINT printf("%s\t%d\t\
							%s\t%s\t\
							__FILE__,__LINE__\
							__DATE__,__TIME__");

(2). #define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏

#define name(parament-list) stuff 

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。 注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部 分。

宏使用时的注意事项 :

(1).

#include<stdio.h>
#define SQUARE( x )  x * x
int main()
{
	int a = 5;
 	printf("%d\n" ,SQUARE(a + 1) );
}

打印结果并不是我们所想要的 36 ,这是因为宏替换到文本中变成了 5 + 1 * 5 + 1 = 11

(2).

#include<stdio.h>
#define DOUBLE(x) (x) + (x)
int main() 
{
	int a = 5;
	printf("%d\n" ,10 * DOUBLE(a)); 
}

打印结果并不是我们所想要的 100 ,宏替换到文本中变成了 10 * (5) + (5) = 55

因此我们在写宏的时候 , 记得要加上括号,否则可能会因为操作符优先级的问题导致没有得到我们想要的结果

(3).

#include<stdio.h>
#define MAX(a, b)  ( (a) > (b) ? (a) : (b) ) 
int main()
{
	int x = 5,y = 8; 
	int z = MAX(x++, y++); 
	printf("x=%d y=%d z=%d\n", x, y, z);
	//输出的结果是什么? 
}

替换之后为 ( (x++) > (y++) ? (x++) : (y++) ) ,因为为后置++ , 5 > 8为假,执行 (y++) ,因此最终x = 6,y = 10,z = 9

五. # 和 ## 的作用

使用 # ,把一个宏参数变成对应的字符串

#include<stdio.h>
#define print(data,format) printf("the value of " #data " is "format,data)
int main()
{
	int a = 10;
	print(a, "%d");
}
// 打印结果为 the value of a is 10

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

#include<stdio.h>
#define ADD_TO_SUM(num,value) sum##num += value
int main()
{
	int sum5 = 10;
	ADD_TO_SUM(5, 10);
	printf("%d\n", sum5);
}
// 打印结果为 20

六.宏和函数的对比

属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执 行 速 度更快存在函数的调用和返回的额外开销, 所以相对慢一些
操 作 符 优 先 级宏参数的求值是在所有周围表达式的上下文环境里,除非加 上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一 次,它的结果值传递给函数。表达式 的求值结果更容易预测
带 有 副 作 用 的 参 数参数可能被替换到宏体中的多个位置,所以带有副作用的参 数求值可能会产生不可预料的结果函数参数只在传参的时候求值一次, 结果更容易控制。
参 数 类 型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数, 即使他们执行的任务是相同的
调 试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

七.条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

(1).
#if 常量表达式
//…
#endif

#include<stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
#if 1
		printf("hehe\n");
#endif
	}
}

(2).
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif

#include<stdio.h>
int main()
{
#if 2 + 3
	printf("5\n");
#elif 3 - 4
	printf("-1\n");
#elif 5 - 2
	printf("3\n");
#else 5 -5 
	printf("0\n");
#endif
}

(3). 判断是否被定义

#ifdef symbol
//…
#endif

#include<stdio.h>
#define PRINT
int main()
{
#ifdef PRINT
	printf("hehe\n");
#endif
}

#ifndef symbol
//…
#endif

#include<stdio.h>
int main()
{
#ifdef PRINT
	printf("hehe\n");
#endif
}

#if defined(symbol)
//…
#endif

#include<stdio.h>
#define PRINT
int main()
{
#if defined(PRINT)
	printf("hehe\n");
#endif
}

#if !defined(symbol)
//…
#endif

#include<stdio.h>
int main()
{
#if !defined(symbol)
	printf("hehe\n");
#endif
}

八.文件包含

头文件包含的两种方式 :

(1). 库文件包含

#include <filename.h> 

查找头文件直接去标准路径下查找,若找不到提示编译错误

(2). 本地文件包含

#include "filename.h" 

查找头文件首先去当前工程的目录下去查找,若查不到再去标准路径下去查找

防止头文件重复包含的两种方式 :

(1).

#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif

(2).

#pragma once 
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐