目录
1. 程序的翻译环境和执行环境
在ANSIC 的任何一种环境中都存在两个不同的环境。
第一种是翻译环境,在这个环境中,源代码被翻译成可执行的机器指令。
第二种是执行环境,用于实际执行代码。
2. 详解编译和链接
21. 编译环境
我们先通过一个图,简单了解一下程序编译过程
组成一个程序的每一个源文件都通过编译器,转换为目标代码。
每个目标文件由链接器捆绑在一起,形成一个独立而完整的可执行程序。
链接器会引入标准C函数库中任何被该程序用到的函数,同时它也可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2 拆分编译的每个阶段
上面的图中,我列出了,编译的每个阶段,以及各个阶段中较为重要的操作。真实的编译过程是及其复杂的,这张图只是列出了我们需要了解的部分知识。
本人平时写C语言程序多用的是 VS2019,它的环境属于集成开发环境,我们并不能很好的去观察编译的每一个阶段,后面我会单独写一篇博客来使用linux gcc来演示编译和链接。
2.3 运行环境
程序指行的过程:
1. 程序必须载入内存中,在操作系统的环境中,一般执行的操作由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能通过可执行代码置入只读内存实现。
2. 程序的执行开始,调用main()函数
3. 开始执行程序代码。这是程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储在静态内存中的变量在程序运行的整个过程中一直保留值。
4. 终止程序。正常终止main()函数,或者发生意外终止。
3. 预处理详解
3.1 预处理符号
_FILE_ // 进行编译的源文件
_LINE_ // 文件当前的行号
_DATE_ // 文件被编译的日期
_TIME_ // 文件被编译的时间
_STDC_ // 如果编译器遵循ANSIC ,其值为1 ,否则未定义
这些预定义符号为语言内置的。
代码实例:
#include<stdio.h>
int main()
{
printf("file:%s", __FILE__);
return 0;
}
屏幕就会打印我们的源文件信息
3.2 #define
3.2.1 #define 定义标识符
语法: #define name stuff
用法实例:
#define a 10000
#define reg register // 为register 这个关键字定义一个简单的名字 reg
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break ;case // 在写case 语句时,case语句后自动break
// 如果stuff过长,我们可以在每一行后面加 \ (续行符)
使用时有一些需要注意的细节。我们在使用#define 时,语句后 不需要加 ; 加了反而容易导致错误。
#define a 10;
#define b 10
int main()
{
int a1 = a;
int b1 = b;
}
右键text.c属性,打开预处理器,选择预处理到文件,在源文件的路径下找到text.i
text.i, .i 是预处理文件后缀 ,打开文件,拉到最后,我们可以看到下面的信息
我们可以看到,我们多写的 ; 也被替换了过来,就容易导致错误。
查看完毕后记得将预处理到文件操作改回否,否则后续运行不了程序。
3.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常叫做宏(macro)或定义宏(#define macro)。
申明方式:
#define name( parament-list) stuff
注意:左括号必须与name紧邻,不允许出现空格,不然括号的内容会被解释为stuff 的内容。
代码实例:
#define squ(x) x*x
打印结果为 25.
但是这样的宏存在问题。
#define squ(x) x*x
int main()
{
int a = 5;
printf("%d", squ(a+1));
}
大家可以算一下,程序运行的结果
结果可能有些出乎意料,由于#define 会在预编译阶段完成替换,实际的代码应该是这样
a +1 * a+1
此时我们将 5代入 ,得到的结果为 11 ,并不是我们预期的36。
此时我们只需稍微改造一下宏
#define squ(x) (x)*(x)
加上俩个括号。
还有一个例子:
#define doub(x) (x)+(x)
int main()
{
int a = 10;
printf("%d", 10 * doub(a));
return 0;
}
同样我们算一下程序运行结果,200?
不对。
10 * 10+10 = 110
乘法优先级高于宏的加法,解决方案就是再加上括号
#define doub(x) ((x)+(x))
我们在设计宏的时候需要提前想好这些可能存在的情况。
用于对数值表达式进行求值的宏,都应该用这种方式加上括号,避免在使用宏时,由于参数中的操作符或者邻近的操作符之间难以预料的相互作用。
3.2.3 #define 替换规则
替换时分步骤进行
1. 在调用宏时,首先对参数进行检查,如果参数包含#define 定义的符号,他们首先被替换。
2.替换文本随后插入到程序中原来文本的位。对于宏,参数名被它们的值替换。
3. 再次对文件进行检查,看他是否包含由#define 定义的符号,如果有重复上述过程。
注意:宏参数和#define 定义中可以出现其他#define 定义的变量,但是宏不允许递归。
预处理器搜索#define 定义的符号时,字符串的常量内容不能被搜索。
3.2.4 #和##
我们先运行一段有意思的代码
#include<stdio.h>
int main()
{
char* p = "hello world";
printf("hello " "world\n");
printf("%s\n", p);
return 0;
}
代码运行的结果为:
一样的效果,因此,我们可以断言,字符串有自动连接的功能。
这时,我们可以用到一个技巧,使用# ,把一个宏参数变成对应的字符串
代码实例:
#include<stdio.h>
#define PRINT(format,value) \
printf("the value of "#value" is "format"\n",value)
int main()
{
int a = 10;
PRINT("%d", a + 2);
}
输出的结果为:
这种设置的好处在于,当我们需要打印多个不同格式的数据,会特别的方便。
##的作用在于,将它两边的符号合成一个符号,它允许宏定义从分离的文本中创建标识符。
代码实例:
#include<stdio.h>
#define ADD_SUM(num,value) sum##num += value
int main()
{
int sum5 = 10;
ADD_SUM(5, 10);
printf("%d", sum5);
}
给sum5增加10,打印的结果为20,但是sum5必须提前声明,因为这个宏产生的必须是一个合法的标识符,否则结果未定义。
3.2.5带副作用的宏参数
当宏参数在宏的定义中出现超过一次时,如果参数带有副作用,那么你使用这个宏时就有可能出现危险。
例如:x +1 就是不带副作用,x++ 带有副作用
代码实例:
#include<stdio.h>
#define MAX(a,b) ((a)>(b) ?(a):(b))
int main()
{
int x = 2;
int y = 3;
int z = MAX(x++, y++);
printf("%d %d %d", x, y, z);
}
z = ((2++)>(3++)?(2++):(3++)
代码运行的结果为:3,5,4
3.2.6 宏和函数的对比
宏通常用于执行简单的运算,如比较较大值。
那函数也可以实现相同的功能,为啥不要函数?
1.用于调用函数和从函数返回值代码可能比实际执行这个小型计算做的工作所需的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2. 宏和类型无关。函数的参数必须声明为特点的类型。
宏的缺点:
1.如果宏太长,会增加程序的长度
2. 宏无法调试
3. 宏与类型无关,也就不够严谨
4. 宏可能带有运算符优先级的问题,导致容易出现错误
3.2.7 良好的代码风格
通常 我们把宏名全部大写
函数名不要全部大写
3.3 #undef
用于移除宏定义
3.4 命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
需要用到编译指令,暂且不做分析 了解即可。
3.5 条件编译
在编译一个程序时,我们如果要编译或者放弃编译一条语句是很简单的,因为我们有条件编译指令。
代码实例:
#include<stdio.h>
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#if 1
printf("%d ", arr[i]);
#endif
}
return 0;
}
常见的条件编译指令:
1.
#if 常量表达式
#endif
//常量表达式由预处理器求值。
2.多分支的条件编译
#if 常量表达式
#elif 常量表达式
#else
#endif
3. 判断是否被定义
#ifdef symbol
#ifndef synbol
4.嵌套指令
3.6 文件包含
3.6.1 头文件的包含方式
本地文件包含
#include "filename"
查找策略:先在源文件下查找,如果该头文件没有找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
库文件包含:
#include <filename .h>
查找策略:直接在标准位置查找,如果找不到提示编译错误。
我们可以考虑一下,库文件,我们可不可以用 " " 来包含,
可以,但是会查找效率,且容易混淆库文件和本地文件。
3.6.2 嵌套文件包含
如果出现一个复杂的场景
test.h 中,会出现两份 comm.h 造成的文件的重复。
如何解决这个问题?
条件编译
每个头文件的开头写
#ifndef __TEST.H__
#define __TEST.H__
//头文件内容
#endif
或者
#pragma once
这样可以避免头文件的重复引入。
本篇博客到此结束,评论区欢迎讨论,有问必答。
谢谢观看!