人生没有白走的路,每一步都算数。今天是C语言最后一篇内容了,坚持!
文章思路:
程序运行有两个环境,翻译环境和执行环境,我会先分别大体介绍翻译环境和执行环境。而翻译环境又分为编译和链接阶段,编译阶段又分为预编译、编译、汇编三个部分,然后我会精讲编译和链接阶段所进行的操作。再聚焦编译阶段中的预编译阶段中执行的预处理操作。
目录
1、程序的翻译环境和执行环境
在ANSI C的任意一种实现中,存在两个不同的环境
- 第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令
- 第二种是执行环境,它用于实际执行代码
2、详解编译+链接
2.1 翻译环境
组成一个程序的每个源文件通过编译过程分别转换成目标代码(.obj),每个目标文件由链接器连捆绑在一起,形成一个单一而完整的可执行程序。 链接器同时会引入ANSI C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2 剖析编译和链接
翻译环境中的编译本身也分为几个阶段
我通过一张图纸让大家搞明白:
- 预处理完成之后就停下来,预处理之后的结果都放在test.i中
- 编译完成之后也停下来,编译之后的结果都放在test.s中
- 汇编完成之后也停下来,汇编之后的结果放在test.o中
2.3 运行环境
程序执行的过程:
- 程序必须载入内存中,在有操作系统的环境:一般由操作系统将程序载入操作系统。独立的环境中:程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
- 程序的执行开始,接着便调用main函数。
- 开始执行程序代码,这个时候程序将使用一个运行时的堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,用于存储全局变量和静态局部变量,存储在静态内存中的变量在程序的整个执行过程中一直保留他们的值。
- 终止程序。正常终止或者意外终止main函数。
3、预定义符号
首先先介绍几个预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
前四个使用如下:
#include<stdio.h>
int main()
{
for (int i = 0; i < 5; i++)
{
printf("file:%s line=%d date:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
return 0;
}
除了__LINE__,其它打印出的都是字符串,用%s来输出。
还有一个__STDC__,我们可以用来判断编译器是否遵循ANSI C,只需要敲上一行这样的代码:
printf("%d",__STDC__);
如果输出的是1,则说明编译器遵循ANSI C;若编译器不认识__STDC__这个符号,出现error,则说明编译器不遵循ANSI C。VS就不认识__STDC__这个符号,不遵循ANSI C,但gcc符合,待我们能力稍微强一点后,学会使用gcc,在出现某一个代码在gcc和VS中运行结果不一样,我们应该倾向gcc的结果。
2、#define
2.1 #define 定义标识符
语法: #define name stuff
用name作为stuff的标识符
举个栗子:
#define MAX 100
#define reg register //为register这个关键字创建一个更简短的名字,用reg替换成register
注意:在define定义标识符的时候,最好不要在最后加上" ; "
2.2 #define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现称为宏或者定义宏。
宏的申明方式:
#define name( parament-list ) stuff
其中parament-list是一个由逗号隔开的符号表,它们可能出现在stuff里。
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
此外,我们习惯命名宏时,全部大写宏的名字,但是不全大写函数的名字
举个栗子:
#define SQUARE(x) x*x
这个宏接受一个参数x,把SQURE(5)置于程序中,预处理器就会用 5*5 替换SQURE(5);
但是我建议在定义宏的时候我们应该习惯写成以下形式
#define SQUARE(x) ((x)*(x))
以下有几个代码段可以论证这样写的好处:
代码段一:
int a=5;
printf("%d\n",SQUARE(a+1));
当a+1替代x后,x*x会变成 5+1*5+1;答案就不是36,而是11。
代码段二:
还有个宏定义: #define DOUBLE(x)(x)+(x)
我们在这里加了两个括号避免了代码段一出现的问题,但还是存在缺点。
int a=5;
printf("%d”,10*DOUBLE(a));
当a+1代替x后,10*DOUBLE(a)会变成10*(a)+(a),代入a=5,则结果为55,而非100。
所以用于对数值表达式进行求值的宏定义都应该用 #define DOUBLE(x) ((x)+(x))这种形式
2.4 #和##
在讲知识点前,我们先看段代码
#include<stdio.h>
int main()
{
char* p = "hello world\n";
char* p2 = "hello"" world\n";
printf("%s", p);
printf("%s", p2);
return 0;
}
我们可以发现字符串有自动连接的特点。那我们想在定义一个变量且给它赋值后,能输出“ the value of 变量名 is 变量本身的值 ”的字符串,这种功能是用函数实现不了的,我们此时得借助宏和#的功能。如下:
#define PRINT(FORMAT,VALUE) printf("the value of " #VALUE " is "FORMAT,VALUE)
#include<stdio.h>
int main()
{
int a = 10;
PRINT("%d",a);
return 0;
}
代码中的#VALUE会被预处理为 “VALUE” 。 把“%d”替代FORMAT,a替代VALUE后,PRINT("%d",a);就会变成printf(“the value of ”“a”“ is ”“%d”,a);根据字符串能自动连接的特点,最终变成了printf(“the value of a is %d”,a);最终输出 the value of a is 10。
学会这种方法后我们也可以输出字符串,输出浮点数。
#define PRINT(FORMAT,VALUE) printf("the value of " #VALUE " is "FORMAT,VALUE)
#include<stdio.h>
int main()
{
char * title="Today is Friday";
float f = 3.24;
PRINT("%s\n",title);
PRINT("%.2f", f);
return 0;
}
##的作用
##可以把位于它两边的符号合成一个符号。
#define ADD_TO_SUM(num,value) sum##num+=value
#define PRINT(format,value) printf("the value of " #value " is "format,value)
#include<stdio.h>
int main()
{
int sum1 = 10;
ADD_TO_SUM(1, 10);
PRINT("%d", sum1);
return 0;
}
注意:这样的连接必须产生一个合法的标识符,否则其结果是未定义的
2.5 #undef
我们会定义一个宏,那我们如果想要移除宏怎么办呢?
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先就要被移除
3、宏与函数对比
宏通常被用于简单计算,但是这些简单计算用函数也可以实现,比如说
#define MAX(a,b) ((a)>(b)?(a):(b))
原因如下:
- 用于调用函数和从函数返回的代码比执行宏所需要的时间更多,在速度方面,宏比函数更胜一筹
- 更为重要的是,宏不需要声明宏参数的类型,但函数必须要声明参数类型,比如说规定是int或者char或者float等可以比较大小的数据类型。
那宏是完美的吗?当然不是,宏也有缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏很短,否则可能大幅度增加程序的长度。
- 宏是没办法调试的,所以出问题了我们不好快速解决。
- 宏由于类型无关,所以不够严谨
- 宏可能会带来优先级的问题,导致程序容易出错误。
宏的功能实现就是通过替换来实现的,但在替换的过程中也会出现一些问题,宏参数会产生一些副作用。
#define MAX(a,b) ((a)>(b)? (a):(b))
#include<stdio.h>
int main()
{
int x = 5, y = 4;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d", x, y, z);
return 0;
}
最后输出的结果是多少呢?首先我们应该把第五行替换掉,变成
int z=((x++)>(y++)?(x++):(y++));
4、条件编译
条件编译可以用于什么时候呢?比如说调试性代码,删除可惜,保留又碍事,所以我们可以用选择性的编译。
#include<stdio.h>
#define _DEBUG_
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef _DEBUG_
printf("%d\n", 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(symbol1)
#ifdef option1
function1();
#endif
#ifdef option2
function2();
#endif
#elif defined(symbol2)
#ifdef option2
function3();
#endif
#endif
我们学会了条件编译后,就可以用这个去避免头文件的重复引入。
5、文件包含
5.1 防止头文件被重复包含
#ifndef __TEST_H__ //头文件的内容,头文件大写
#define __TEST_H__
#endif
或者:
#pragma once
这两个都可以避免头文件的重复引入。
5.2 头文件的包含
头文件的包含分为 <> 和 “ ”,这两个区别查找的策略不同。
< >查找策略:直接去库目录下查找
“ ”查找策略: 先去代码所在的路径下查找,然后再去库目录下查找
那库文件也可以“ ” 的形式包含? 答案是可以的,但是这样查找的效率就低了,这样也不容易分清到底是库文件还是本地文件。
本文到此结束,喜欢我的文章就给我点个赞吧,更新不易(o(╥﹏╥)o)