目录
程序的翻译环境和执行环境
详解编译+链接
翻译环境
编译本身也分为几个阶段
运行环境
预处理(预编译)详解
预定义符号
#define
#define 定义标识符
#define 定义宏
#define 替换规则
#和##
## 的作用
带副作用的宏参数
宏和函数对比
命名约定
#undef
命令行定义
条件编译
文件包含
头文件被包含的方式
本地文件包含
库文件包含
嵌套文件包含
总结
程序的翻译环境和执行环境
第 1 种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第 2 种是执行环境,它用于实际执行代码
详解编译+链接
翻译环境
组成一个程序的每个源文件通过编译过程分别转换成目标代码( object code )。每个目标文件由链接器( linker )捆绑在一起,形成一个单一而完整的可执行程序。链接器同时也会引入标准 C 函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
编译本身也分为几个阶段
注意 :我们这里使用Linux系统上的gcc编译器
编译本身也分为几个阶段,测试代码如下
sum.c
int g_val = 2016;
void print(const char *str)
{printf("%s\n", str);
}
test.c
#include <stdio.h>
int main()
{extern void print(char *str);extern int g_val;printf("%d\n", g_val);print("hello bit.\n");return 0;
}
预处理阶段:
在gcc编辑器上进行的操作:gcc -E test.c -o test.i,·-E 是进行预处理操作,-o指定输出到test.i文件(不会自动生成,需要指定输出),该阶段进行的操作为:1.#include头文件的包含;2.#define定义的符号的替换和删除操作;3.注释的删除(1、2为预编译指令操作;3为文本操作)。
编译阶段:
在gcc编辑器上进行的操作:gcc -S test.i (进行编译操作,执行后自动生成test.s文件,也可以进行指定),该阶段进行的操作为:把C语言代码翻译成汇编代码、进行语义分析、词法分析、语义分析、符号汇总
汇编阶段:
在gcc编辑器上进行的操作:gcc -c test.c(进行汇编操作后,会自动生成test.o文件,也可以进行指定);该阶段进行的操作为:把汇编代码翻译成了二进制指令(存放在目标文件中)、形成符号表。
链接阶段:
1.合成段表
2.符号表的合并和符号表的重定位
关于符号汇总还可结合下图进行观看
那么我们如何查看编译期间的每一步发生了什么呢?
比如我们有以下这个代码
test.c
#include <stdio.h>
int main()
{int i = 0;for(i=0; i<10; i++){printf("%d ", i);}return 0;
}
查看步骤如下
1. 预处理 选项 gcc - E test.c - o test.i预处理完成之后就停下来,预处理之后产生的结果都放在 test.i 文件中。2. 编译 选项 gcc - S test.c编译完成之后就停下来,结果保存在 test.s 中。3. 汇编 gcc - c test.c汇编完成之后就停下来,结果保存在 test.o 中。
当我们编译完成后了,就会生成我们的可执行程序
运行环境
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。2. 程序的执行便开始。接着便调用 main 函数。3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static )内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。4. 终止程序。正常终止 main 函数;也有可能是意外终止。
预处理(预编译)详解
预定义符号
__FILE__ // 进行编译的源文件__LINE__ // 文件当前的行号__DATE__ // 文件被编译的日期__TIME__ // 文件被编译的时间__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
// printf("%s\n", __FILE__);
// printf("%d\n", __LINE__);
用我们vs编译器进行编译后 ,结果如下
我们再用gcc进行预编译,当我们打开我们的预编译
我们发现test.i的文件里已经变为我们所需要输出的内容
我们再举个例子:
int main()
{printf("%s\n", __FILE__);printf("%d\n", __LINE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);//printf("%d\n", __STDC__);//当前VS是不支持ANSI Creturn 0;
}
运行结果如下:
#define
#define 定义标识符
语法:#define name stuff
举个例子:
//#include <stdio.h>
//#define M 100
//#define STR "abc"
//#define FOR for(;;)
//int main()
//{
// printf("%d\n", M);
// printf("%s\n", STR);
// FOR;
// return 0;
//}
运行结果如下:
我们再转入gcc编译器看一下预编译
注意: 在define定义标识符的时候,不要再最后加上 ';'
例如:
#define MAX 1000;
#define MAX 1000
当这行代码遇上下列代码
if(condition)max = MAX;
elsemax = 0;
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏( macro )或定义宏(define macro )。
#define name( parament-list ) stuff
//#define MAX(x, y) ((x)>(y)?(x):(y))
//int main()
//{
// int a = 5;
// int b = 6;
// int c = MAX(a, b);
// printf("%d",c);
//}
运行结果如下
我们再用gcc进行预编译,结果如下
这里已经进行了替换
注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
例如:
#define SQUARE( x ) x * x
SQUARE ( 5 );
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
替换文本时,参数 x 被替换成 a + 1, 所以这条语句实际上变成了:printf ("%d\n",a + 1 * a + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。 在宏定义上加上两个括号,这个问题便轻松的解决了。
我们再看一个例子:
#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
这将打印什么值呢?
printf ( "%d\n" , 10 * ( 5 ) + ( 5 ));
#define DOUBLE( x) ( ( x ) + ( x ) )
总结:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define 替换规则
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。
#和##
如何把参数插入到字符串中?
我们先看这样一段代码
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);
运行结果如下
我们发现字符串是有自动连接的特点的。
那我们是不是就可以这样写代码
#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE)
int main()
{PRINT("%d", 10);
}
运行结果如下:
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is "FORMAT "\n", VALUE)
int main()
{int i = 10;PRINT("%d", i + 3);
}
## 的作用
## 可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符
例如:
//#define CAT(x,y) x##y
//int main()
//{
// int Class110 = 2024;
// printf("%d\n", CAT(Class, 110));
// printf("%d\n", Class110);
// return 0;
//}
运行结果如下:
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作用
x++;//带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。例如
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
z = ( ( x ++ ) > ( y ++ ) ? ( x ++ ) : ( y ++ ));
逻辑为:
最后的输出结果为
x = 6 y = 10 z = 9
宏和函数对比
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹 。2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于> 来比较的类型。 宏是类型无关的 。
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。2. 宏是没法调试的。3. 宏由于类型无关,也就不够严谨。4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。例如:
#define MALLOC(num, type)\(type *)malloc(num * sizeof(type))...// 使用MALLOC ( 10 , int ); // 类型作为参数// 预处理器替换之后:( int * ) malloc ( 10 * sizeof ( int ));
宏和函数的对比
命名约定
把宏名全部大写函数名不要全部大写
#undef
#undef NAME// 如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
命令行定义
许多 C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程
#include <stdio.h>
int main()
{int array [sz];int i = 0;for(i = 0; i< sz; i ++){array[i] = i;}for(i = 0; i< sz; i ++){printf("%d " ,array[i]);}printf("\n" );return 0;
}
注意:该代码演示的环境为linux
gcc - D sz = 10 programe . c
编译后结果如下
条件编译
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
常见的条件编译指令:
1.#if 常量表达式//...#endif
使用如下:
//#define M 0
//int main()
//{
//#if M==1
// printf("hehe\n");
//#endif
// return 0;
//}
注意:这里以if else语句是有区别的,if else语句会进行编译,只是不满足条件不进入,而#if与#endif是不会进行编译,相当于删除了本行代码。我们在linux进行预编译验证结果如下:
2. 多个分支的条件编译#if 常量表达式//...#elif 常量表达式//...#else//...#endif
3. 判断是否被定义#if defined(symbol)#ifdef symbol#if !defined(symbol)#ifndef symbol
举个例子:
//#define WIN 0
//
//int main()
//{
//#if defined(WIN)
// printf("windows");
//#endif
// return 0;
//}
当我们用linux进行预处理后发现,检测被定义
4. 嵌套指令#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1 ();#endif#ifdef OPTION2unix_version_option2 ();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2 ();#endif#endif
文件包含
头文件被包含的方式
本地文件包含
#include "filename"
/ usr / include
VS环境的标准头文件的路径:
注意按照自己的安装路径去找。
库文件包含
#include <filename.h>
嵌套文件包含
comm.h 和 comm.c 是公共模块。test1.h 和 test1.c 使用了公共模块。test2.h 和 test2.c 使用了公共模块。test.h 和 test.c 使用了 test1 模块和 test2 模块。这样最终程序中就会出现两份 comm.h 的内容。这样就造成了文件内容的重复
#ifndef __TEST_H__#define __TEST_H__// 头文件的内容#endif //__TEST_H__
或者
#pragma once
就可以避免头文件的重复引入。
总结
关于程序环境和预处理就讲解到这儿,至此为止C语言的基础知识就讲解完毕了,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下。