(一)程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
| test.c ——————————————> test.exe |
| 翻译环境 VS2019集成开发环境 || 运行环境 || 运行结果 |
| 编译(编译器) || 链接(链接器) |
|预编译(预处理)|| 编译 ||汇编|
(二)详解编译 + 链接
组成一个程序的每个源文件通过编译过程分别转换成目标代码
test.c |编| test.obj | |
contact.c |译| contact.obj |链|
common.c |器| common.obj |接| ———> 可执行程序
|器|
链接库(.LIB) | |
一、编译阶段
(1)预处理阶段(文本操作)
gcc test.c -E
预处理之后就停止
gcc test.c -E > test.i
1. #include <> 完成了头文件的包含
2. #define定义的符号和宏的替换
3. 注释删除
详解预处理
3.1 预定义符号
int main()
{
printf("%s\n", __FILE__); //给出编译的源文件的名字及路径
printf("%d\n", __LINE__); //打印行号(这条代码所在的行)
printf("%s\n", __DATE__); //打印日期
printf("%s\n", __TIME__); //打印时间
printf("%s\n", __FUNCTION__); //打印所在函数的函数名
return 0;
}
int main()
{
int i = 0;
FILE* pf = fopen("log.txt", "a+");
if (!pf)
{
perror("fopen");
return 1;
}
for (i = 0; i < 10; i++)
{
fprintf(pf, "%s %d %s %s %d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
}
3.2 #define
3.2.1 #define定义标识符
#define M 1000
#define reg register
#define do_forever for(;;)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n",\
__FILE__,__LINE__,__DATE__,__TIME__); //'\'是续航符,处理替换过长
int main()
{
reg int num = 0;
do_forever;
int m = M;
printf("%d\n", m);
return 0;
}
#define CASE break;case
int main()
{
int n = 0;
//switch (n)
//{
//case 1:
// break;
//case 2:
// break;
//case 3:
// break;
//}
switch (n)
{
case 1:
CASE 2:
CASE 3:
}
return 0;
}
尽量不要在符号后面加';'
#define M 1000;
int main()
{
int a = 10;
int b = 0;
if (a > 10)
b = M; //b = 1000;; 相当于两条语句,因此else不知道和谁匹配
else
b = -M;
return 0;
}
3.2.2 #define定义宏
#define机制包括了一个规定:允许把参数替换到文本中,这种实现通常称为宏
#define name( parament-list ) stuff
其中parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意
1.参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
2.括号很重要!
#define SQUARE(X) X*X //- SQUARE是一个宏
//#define SQUARE (X) X*X - SQUARE是一个符号,替换为(X) X*X
#define SQUARE2(X) ((X)*(X))
#define DOUBLE(X) (X)+(X)
#define DOUBLE2(X) ((X)+(X))
int main()
{
printf("%d\n", SQUARE(3)); //9
//printf("%d\n", 3 * 3);
printf("%d\n", SQUARE(3 + 1)); //7
//printf("%d\n", 3+1*3+1); - 预处理阶段已经替换宏为X*X,因此先替换后计算!
printf("%d\n", SQUARE2(3 + 1)); //16
//printf("%d\n", ((3+1)*(3+1)) );
printf("%d\n", 10 * DOUBLE(4)); //44
//printf("%d\n", 8 * (4)+(4));
printf("%d\n", 10*DOUBLE2(4)); //80
//printf("%d\n", 8 * ((4)+(4)));
return 0;
}
#define M 100
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int max = MAX(101, M);
//int max = MAX(101,100);
//int max = ((101)>(100)?(101):(100))
printf("%d\n", max);
return 0;
}
3.2.3
1.注意,宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容不被搜索
int main()
{
int max = MAX(101, M);
printf("M = %d\n", M); //字符串里的M不被搜索
}
3.2.4 #和##
如何把参数插入字符串?
void printf(int x)
{
printf("the value of ? is %d\n", x); //无论?是谁都不对
}
#define PRINT(X) printf("the value of "#X" is %d\n",X)
#X被替换为X名字所对应的字符串
#define PRINT(X,FORMAT) printf("the value of "#X" is "FORMAT"\n",X)
int main()
{
//printf("hello world\n");
//printf("hello" "world\n"); //两种形式相同
int a = 10;
PRINT(a, "%d");
//printf("the value of ""a"" is %d\n", a);
//the value of a is 10
int b = 20;
PRINT(b, "%d");
//printf("the value of ""b"" is %d\n", b);
//the value of b is 20
int c = 30;
//PRINT(c);
PRINT(c, "%d");
//printf("the value of ""c"" is %d\n", c);
//the value of a is 30
float f = 5.5f;
//PRINT(f);
//printf("the value of ""f"" is %d\n", f);
PRINT(f, "%f");
printf("the value of ""f"" is ""%f""\n", f);
return 0;
}
#define CAT(X,Y,Z) X##Y##Z
##将位于它两边的符号组合成一个符号
它允许宏定义从分离的文本片段创建标识符
int main()
{
int class101yes = 100;
printf("%d\n", CAT(class, 101,yes));
// printf("%d\n", class101yes)
return 0;
}
3.2.5 带副作用的宏参数
当宏参数的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏就有可能出现危险,导致不可预测的后果
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 5;
int b = 8;
int m = MAX(a++, b++);
//int m = (a++)>(b++)?(a++):(b++);
printf("m = %d\n", m); //9
printf("a = %d,b = %d\n", a, b); //6,10
}
3.2.6 宏和函数对比
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int Max(int x, int y)
{
return x > y ? x : y;
}
宏通常被应用于执行简单的运算。比如在两个数中找比较大的一个。
宏的优势?
1.宏比函数在程序的规模和速度方面更胜一筹 - 用于调用函数和函数返回的代码可能比实际的小型计算工作耗时更多
宏:运算
函数:调用-运算-返回
2.更为重要的是函数的参数必须声明为特定的类型,而宏与类型无关
宏的劣势?
1.每次使用宏的时候,一份宏定义的代码插入程序,可能大幅增加程序长度
2.宏是没法调试的 - 编译-链接-可执行程序(才可调试),宏在预处理阶段已经替换,和所看到的宏不一样,无法调试
3.宏由于类型无关,也就不够严谨
4.宏可能会带来运算符优先级的问题,导致程序出错
宏有时候可以完成函数完不成的工作,例如类型作为参数
#define MALLOC(num,type) (type*)malloc((num)*sizeof(type))
int main()
{
//malloc(10 * sizeof(int));
int* p = MALLOC(10, int);
//int* p = (int*)malloc((10)*sizeof(int))
return 0;
}
命名约定:把宏名全大写,函数名不要全部大写
3.3 #undef - 移除一个#define定义
#define M 100
int main()
{
int a = M;
#undef M
printf("%d", M);
return 0;
}
3.4 命令行定义
int main()
{
int arr[M] = 0;
int i = 0;
for (i = 0; i < M; i++)
{
arr[i] = i;
}
for (i = 0; i < M; i++)
{
printf("%d ", i);
}
return 0;
}
3.5 条件编译
在编译一个程序的时候我们要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
#define PRINT
int main()
{
#ifdef PRINT
printf("hehe"); //PRINT定义了预处理阶段不删除这条语句,因此会执行;没定义则在预处理阶段删除掉这条语句
#endif
return 0;
}
1.单分支的条件编译 #if 常量表达式
#endif
#define PRINT 1
int main()
{
#if 0
printf("hehe");
...
#endif
return 0;
}
2.多分支的条件编译 #if - #elif - ... - #else - #endif
int main()
{
#if 1==2
printf("hehe\n");
#elif 2==3
printf("haha\n");
#else
printf("heihei\n");
#endif
return 0;
}
3.判断是否被定义
#define TEST 0
#define HEHE 0
int main()
{
//如果test定义了,无论是真是假,下面参与编译
1.
#ifdef TEST
printf("test1\n");
#endif
2.
#if defined(TEST)
printf("test2\n");
#endif
//如果hehe不定义,下面参与编译
3.
#ifndef HEHE
printf("hehe1\n");
#endif
4.
#if !defined(HEHE)
printf("hehe2\n");
#endif
return 0;
}
#define HAPPY 1
int main()
{
#if defined(HAPPY) && !defined(SAD)
{
printf("feels good");
}
#endif
}
4.嵌套指令
int main()
{
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(US_MSDOS)
#ifdef OPTION2
msdos_vertion_option2();
#endif
#endif
}
(2)编译阶段
gcc test.i -s
gcc test.i > test.s
把C语言代码转化成汇编代码
1.语法分析
2.词法分析
3.语义分析
4.符号汇总
将test.c(main,Add) , test.add(Add) 分别汇总符号
(3)汇编阶段
gcc test.s -c
——>test.o(test.obj)
把汇编代码转换成了机器指令(二进制指令)
1.生成符号表
将汇总的符号生成符号表
例:
对test.c
(Add | 0x0000)
(main | 0x1004)
对Add.c
(Add | 0x1008)
test.o是elf格式
readelf - 工具
2.把汇编代码转换成机器指令
声明外部符号
extern int Add(int, int);
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
return 0;
}
3.6.1 头文件被包含的形式//详解预处理
3.1 预定义符号
int main()
{
printf("%s\n", __FILE__); //给出编译的源文件的名字及路径
printf("%d\n", __LINE__); //打印行号(这条代码所在的行)
printf("%s\n", __DATE__); //打印日期
printf("%s\n", __TIME__); //打印时间
printf("%s\n", __FUNCTION__); //打印所在函数的函数名
return 0;
}
int main()
{
int i = 0;
FILE* pf = fopen("log.txt", "a+");
if (!pf)
{
perror("fopen");
return 1;
}
for (i = 0; i < 10; i++)
{
fprintf(pf, "%s %d %s %s %d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
}
3.2 #define
3.2.1 #define定义标识符
#define M 1000
#define reg register
#define do_forever for(;;)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n",\
__FILE__,__LINE__,__DATE__,__TIME__); //'\'是续航符,处理替换过长
int main()
{
reg int num = 0;
do_forever;
int m = M;
printf("%d\n", m);
return 0;
}
#define CASE break;case
int main()
{
int n = 0;
//switch (n)
//{
//case 1:
// break;
//case 2:
// break;
//case 3:
// break;
//}
switch (n)
{
case 1:
CASE 2:
CASE 3:
}
return 0;
}
尽量不要在符号后面加';'
#define M 1000;
int main()
{
int a = 10;
int b = 0;
if (a > 10)
b = M; //b = 1000;; 相当于两条语句,因此else不知道和谁匹配
else
b = -M;
return 0;
}
3.2.2 #define定义宏
#define机制包括了一个规定:允许把参数替换到文本中,这种实现通常称为宏
#define name( parament-list ) stuff
其中parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意
1.参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
2.括号很重要!
#define SQUARE(X) X*X //- SQUARE是一个宏
//#define SQUARE (X) X*X - SQUARE是一个符号,替换为(X) X*X
#define SQUARE2(X) ((X)*(X))
#define DOUBLE(X) (X)+(X)
#define DOUBLE2(X) ((X)+(X))
int main()
{
printf("%d\n", SQUARE(3)); //9
//printf("%d\n", 3 * 3);
printf("%d\n", SQUARE(3 + 1)); //7
//printf("%d\n", 3+1*3+1); - 预处理阶段已经替换宏为X*X,因此先替换后计算!
printf("%d\n", SQUARE2(3 + 1)); //16
//printf("%d\n", ((3+1)*(3+1)) );
printf("%d\n", 10 * DOUBLE(4)); //44
//printf("%d\n", 8 * (4)+(4));
printf("%d\n", 10*DOUBLE2(4)); //80
//printf("%d\n", 8 * ((4)+(4)));
return 0;
}
#define M 100
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int max = MAX(101, M);
//int max = MAX(101,100);
//int max = ((101)>(100)?(101):(100))
printf("%d\n", max);
return 0;
}
3.2.3
1.注意,宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容不被搜索
int main()
{
int max = MAX(101, M);
printf("M = %d\n", M); //字符串里的M不被搜索
}
3.2.4 #和##
如何把参数插入字符串?
void printf(int x)
{
printf("the value of ? is %d\n", x); //无论?是谁都不对
}
#define PRINT(X) printf("the value of "#X" is %d\n",X)
#X被替换为X名字所对应的字符串
#define PRINT(X,FORMAT) printf("the value of "#X" is "FORMAT"\n",X)
int main()
{
//printf("hello world\n");
//printf("hello" "world\n"); //两种形式相同
int a = 10;
PRINT(a, "%d");
//printf("the value of ""a"" is %d\n", a);
//the value of a is 10
int b = 20;
PRINT(b, "%d");
//printf("the value of ""b"" is %d\n", b);
//the value of b is 20
int c = 30;
//PRINT(c);
PRINT(c, "%d");
//printf("the value of ""c"" is %d\n", c);
//the value of a is 30
float f = 5.5f;
//PRINT(f);
//printf("the value of ""f"" is %d\n", f);
PRINT(f, "%f");
printf("the value of ""f"" is ""%f""\n", f);
return 0;
}
#define CAT(X,Y,Z) X##Y##Z
##将位于它两边的符号组合成一个符号
它允许宏定义从分离的文本片段创建标识符
int main()
{
int class101yes = 100;
printf("%d\n", CAT(class, 101,yes));
// printf("%d\n", class101yes)
return 0;
}
3.2.5 带副作用的宏参数
当宏参数的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏就有可能出现危险,导致不可预测的后果
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 5;
int b = 8;
int m = MAX(a++, b++);
//int m = (a++)>(b++)?(a++):(b++);
printf("m = %d\n", m); //9
printf("a = %d,b = %d\n", a, b); //6,10
}
3.2.6 宏和函数对比
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int Max(int x, int y)
{
return x > y ? x : y;
}
宏通常被应用于执行简单的运算。比如在两个数中找比较大的一个。
宏的优势?
1.宏比函数在程序的规模和速度方面更胜一筹 - 用于调用函数和函数返回的代码可能比实际的小型计算工作耗时更多
宏:运算
函数:调用-运算-返回
2.更为重要的是函数的参数必须声明为特定的类型,而宏与类型无关
宏的劣势?
1.每次使用宏的时候,一份宏定义的代码插入程序,可能大幅增加程序长度
2.宏是没法调试的 - 编译-链接-可执行程序(才可调试),宏在预处理阶段已经替换,和所看到的宏不一样,无法调试
3.宏由于类型无关,也就不够严谨
4.宏可能会带来运算符优先级的问题,导致程序出错
宏有时候可以完成函数完不成的工作,例如类型作为参数
#define MALLOC(num,type) (type*)malloc((num)*sizeof(type))
int main()
{
//malloc(10 * sizeof(int));
int* p = MALLOC(10, int);
//int* p = (int*)malloc((10)*sizeof(int))
return 0;
}
命名约定:把宏名全大写,函数名不要全部大写
3.3 #undef - 移除一个#define定义
#define M 100
int main()
{
int a = M;
#undef M
printf("%d", M);
return 0;
}
3.4 命令行定义
int main()
{
int arr[M] = 0;
int i = 0;
for (i = 0; i < M; i++)
{
arr[i] = i;
}
for (i = 0; i < M; i++)
{
printf("%d ", i);
}
return 0;
}
3.5 条件编译
在编译一个程序的时候我们要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
#define PRINT
int main()
{
#ifdef PRINT
printf("hehe"); //PRINT定义了预处理阶段不删除这条语句,因此会执行;没定义则在预处理阶段删除掉这条语句
#endif
return 0;
}
1.单分支的条件编译 #if 常量表达式
#endif
#define PRINT 1
int main()
{
#if 0
printf("hehe");
...
#endif
return 0;
}
2.多分支的条件编译 #if - #elif - ... - #else - #endif
int main()
{
#if 1==2
printf("hehe\n");
#elif 2==3
printf("haha\n");
#else
printf("heihei\n");
#endif
return 0;
}
3.判断是否被定义
#define TEST 0
#define HEHE 0
int main()
{
//如果test定义了,无论是真是假,下面参与编译
1.
#ifdef TEST
printf("test1\n");
#endif
2.
#if defined(TEST)
printf("test2\n");
#endif
//如果hehe不定义,下面参与编译
3.
#ifndef HEHE
printf("hehe1\n");
#endif
4.
#if !defined(HEHE)
printf("hehe2\n");
#endif
return 0;
}
#define HAPPY 1
int main()
{
#if defined(HAPPY) && !defined(SAD)
{
printf("feels good");
}
#endif
}
4.嵌套指令
int main()
{
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(US_MSDOS)
#ifdef OPTION2
msdos_vertion_option2();
#endif
#endif
}
3.6.1 头文件被包含的形式
库文件包含 - C语言库中提供的函数的头文件使用<>
#include <stdio.h>
本地文件包含 - 自定义的函数的头文件使用""
#include "test.h"
<>和""包含头文件的本质区别是:查找的策略区别
"" 1.代码所在的目录下查找 2.如果第一步找不到,则在库函数的头文件目录下查找
<> 直接去库函数头文件所在的目录下查找
自己去找库函数头文件所在库的方法:EOF -> 转到定义 -> 右键打开所在文件夹
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
3.6.2 嵌套文件包含
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
避免头文件重复引入的方法
法一
#pragma once
法二
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
int Add(int x, int y);
struct S
{
char c;
int i;
};
#endif
(2)编译阶段
gcc test.i -s
gcc test.i > test.s
把C语言代码转化成汇编代码
1.语法分析
2.词法分析
3.语义分析
4.符号汇总
将test.c(main,Add) , test.add(Add) 分别汇总符号
(3)汇编阶段
gcc test.s -c
——>test.o(test.obj)
把汇编代码转换成了机器指令(二进制指令)
1.生成符号表
将汇总的符号生成符号表
例:
对test.c
(Add | 0x0000)
(main | 0x1004)
对Add.c
(Add | 0x1008)
test.o是elf格式
readelf - 工具
2.把汇编代码转换成机器指令
声明外部符号
extern int Add(int, int);
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
return 0;
}
二、链接阶段
把多个目标文件和链接库进行链接的
1.合并段表(相同段的数据合并到一起)
2.符号表的合并和重定位(符号表合并,保留有效的符号地址 - test.c中的Add无效,与有效的Add.c中的Add符号合并)
反例:如果将Add.c中的Add定义屏蔽掉,则编译器显示无法解析的外部符号 - 是因为合并的符号表中Add是无效符号,链接阶段找不到这个函数
test.o - elf
add.o - elf
变成
a.out - elf
(三)运行环境
1.程序必须载入内存中。
在有操作系统的环境中:一般由操作系统完成。
2.程序的运行便开始。接着调用main函数
3.开始执行程序代码。
这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。
程序同时也可以使用静态(static)内存,存储于静态内存的变量在程序整个执行过程一直保留他们的值
Add(Add执行完回收)
x(Add执行完回收)
y(Add执行完回收)
main (main调用完回收)
4.终止程序。正常终止main函数;也有可能是意外终止
1669

被折叠的 条评论
为什么被折叠?



