C语言程序环境与预处理
1. 程序环境
在C语言执行的过程中,存在两个环境(翻译环境,执行环境)
每个.c源文件都会经过编译器形成各自的.obj目标文件,最后再由目标文件和链接库经过连接器形成一个.exe可执行程序
2 翻译环境 + 执行环境
2.1 编译
编译期间,又被划分为三个小部分:
预编译,编译,汇编
下面的过程在VS中很难查看到,需要查看的可以去Linux下试试(基于test.c文件进行演示)
2.1.1 预编译(文本操作)
-
指令:
gcc -E test.c -o test.i
-
作用
-
加载
头文件
里面的所有内容 - 注释的删除(用空格替换)
- #define定义的值进行替换
- 条件编译(保留符合条件的代码,往下看)
注意:
// 结论:预编译的时候先进行去注释,再进行宏替换 // 这里的 // 直接被当做注释去掉了,BSC就替换 空 了 #define BSC // int main() { BSC printf("Hello World\n"); // 屏幕上的结果:Hello World // 说明BSC并没有被 // 替换 return 0; }
-
加载
2.1.2编译
把C代码翻译成汇编代码,推荐书籍《编译原理》,《程序员的自我修养》
-
指令:
gcc -S test.c
-
作用:
- 语法分析
- 词法分析
- 语义分析
- 符号汇总(全局变量,函数名)
2.1.3 汇编
把汇编代码转换成二机制编码,并且形成符号表(
符号名
对应
地址
)
-
指令:
gcc -c test.c
,执行完成后就生成了目标文件,.obj
前面的就是编译期间的符号汇总,后面就是汇编的符号表
2.2 链接
所有的目标文件(.obj文件)+ 链接库 通过
链接器
,然后生成一个可执行文件.exe
- 合并段表(elf文件格式):.obj文件会有一定的格式存放数据的(很多块/段),链接时会讲多个.obj文件对应的每个段进行合并处理
- 符号表的合并和重定位
- readelf -s test.o(可查看目标文件源码,但里面是二进制的)
在链接期间,就会通过符号表来查看来自外部引用的符号是否真实存在(不存在,就会报链接错误(无法找到外部符号))
2.3 执行环境
- 加载到内存
- 从main函数开始执行
- 使用一个运行时堆栈,存储函数的局部变量和返回地址
- 终止程序:正常运行完终止;意外终止
VIM学习资料
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡
https://coolshell.cn/articles/5479.html
3. 预处理详解
3.1 预定义符号
//1.__FILE__:文件的绝对地址
//2.__LINE__:当前的行号
//3.__DATE__:当天的日期
//4.__TIME__:当前的时间
3.2 日志案例
// 日志文件
int i = 0;
int arr[10] = {0};
FILE* pf = fopen('log.txt','w');
for(i=0;i<10;i++)
{
arr[i] = i;
fprintf(pf,"file:%s line:%d date:%s",__FILE__,__LINE__,__DATE__);
}
for(i=0;i<10;i++)
{
printf("%d ",arr[i]);
}
3.3. #define
3.3.1 #define定义标识符
- #define 标识符 值(注意:千万不能加分号)
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
3.3.2 #define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或 定义宏(define macro)。
- 源文件的任何地方都可以定义宏,与是否在函数内外无关
- 宏的作用范围:从定义处开始,往后都是有效的(若函数在宏上面定义的,函数里面将不会进行宏替换)
#define name(参数列表) 值
- #define sqt(X) X*X – 宏的值是替换的(字符串里面的除外),不是传参的,int ret = sqt(5) – 输出ret结果为:25
- int ret = sqt(5+1); – ret结果即为11 – 5+1*5+1 = 11
- 注意:如果参数是表达式,最好加一个括号(#define sqt(X) (X)*(X))
- 宏里面的参数可以是#define 定义标识符的值
// 多条语句的宏建议用 do-while-zero模式(经常简单的函数就可以写成这样的)
#define INI_VAL(a,b) \
do{ \
// 语句块 - 每个语句后需要续行
}while(0)
3.3.3 命令行定义宏
- Linux中:gcc test.c -D 标识符 = 值
- Windows中:在控制台设置的地方 – 预处理器 – 预处理器定义直接加上就可以了
3.3.4 #undef 取消宏
#undef 标识符(可以取消定义的宏)
int main()
{
#define X 3
#define Y X*2
#undef X
#define X 2
int z = Y; // z = Y = 2*2; // 使用的是最后一个宏
return 0;
}
3.3.5 带有副作用的宏的参数
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 11;
int max = MAX(a++,b++);
// int max = MAX((a++)>(b++)?(a++):(b++))
// 执行时: a=10 b=11(后++),10>11,执行b++,此时b = 12
printf("%d\n",max);//12
printf("%d\n",a);//11
printf("%d\n",b);//13
return 0;
}
3.3.6 #和##
#输入预处理指令,放在宏里面,就是将传入的参数变成字符串类型(若传的是变量,改的则是变量名 – —-> define替换是在预编译阶段进行的,而变量定义则是到了汇编了—–>优先顺序问题)
##可以把位于它两边的符号合并成一个符号,它允许宏定义从分离的文本片段创建标识符
#define PRINT(X) printf("the value of "#X" is %d\n",X)
int main()
{
int a = 10;
int b = 20;
PRINT(a); // #X 将替换成值所对应的变量,然后将变量转换为字符串,相邻字符串具有连接属性(相邻的字符串就是一个连在一起的字符串)
PRINT(b);
return 0;
}
#define Combine(X,Y) X##Y
int main()
{
int Class84 = 2020;
//两个的结果一样
printf("%d\n",Class84);
printf("%d\n",Combine(Class,84));
//printf("%d\n",Class##84);
//printf("%d\n",Class84);
}
3.3.7 宏和函数的差别
属性 | 宏 | 函数 |
---|---|---|
代码长度 | 没使用一次就会加载一下代码,所以代码冗余 | 实现一次即可,后面每次使用,都是直接调用该函数 |
执行速度 | 更快 | 存在函数调用+返回值的额外开销 |
操作符优先级 | 因为是直接替换,所以很容易有优先级的问题,所以建议多加括号 | 函数参数只能在调用时计算一次,表达式的结果更容易预料 |
副作用参数 | 参数可能被替换到宏体中的多个位置,结果难以预料 | 参数在调用的时候会进行计算,函数里面的参数就不会有副作用了 |
参数类型 | 宏的参数与类型无关,只要操作合法就行,可以是任何参数 | 形参有类型检查的 |
调试 | 不支持 | 支持 |
递归 | 不支持 | 支持 |
3.3.8 宏的参数可以为 类型
// 函数就不能传递参数为 类型
#define SIZEOF(type) sizeof(type)
int main()
{
int ret = SIZEOF(int);
// int ret = sizeof(int)
}
#define MALLOC(count,type) ((type*)malloc(count*sizeof(type)))
int main()
{
int* p = (int*)malloc(10*sizeof*(int)); //开辟10个整形空间
int* p = MALLOC(10,int);
}
3.4 条件编译
当满足某条件时进行编译里面的代码(其他的代码不加载)(例如:付费版和普通版)
可以嵌套
// 1.
#define __DEBUG__ 1
#if __DEBUG__ // #if 需要后面赋值,来判断真假
//……
#endif
// 2.
#if 常量表达式
//……
#elif 常量表达式
//……
#else 常量表达式
//……
// 3. - 双分支(#ifdef == #if defined;#ifndef == #if ndefined) 定义就行,不需要值来判断真假
#ifdef
// ……
#else
//……
#endif
// 4.
#if (defined(C) && defined(CPP)) //- 两个宏都要被定义
#define DEBUG // 这是定义了,但没有真假
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int i = 0;
for(i=0;i<10;i++)
{
arr[i] = 0;
//如果DEBUG定义了,就打印,若没定义,就不打印
#ifdef DEBUG //或者#if defined(DEBUG)
printf("%d\n",arr[i]);
#endif
}
return 0;
}
#define DEBUG
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int i = 0;
for(i=0;i<10;i++)
{
arr[i] = 0;
//如果DEBUG没定义,就打印,若没定义,就不打印
#ifndef DEBUG //或者#if !defined(DEBUG)
printf("%d\n",arr[i]);
#endif
}
return 0;
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int i = 0;
for(i=0;i<10;i++)
{
arr[i] = 0;
//如果条件为真就编译,否则就不编译
#if 1
printf("%d\n",arr[i]);
#elif 条件
……
#endif
}
return 0;
}
3.4.1 #ifdef与#if的区别
- #ifdef 只有双分支 – #ifdef …… #else (ifdef判断的是宏定义即可,可以不用赋值,不用判断真假)
- #if 是多分支,#if……#elif……#else (必须给宏赋值,需要判断真假)
3.5 头文件包含
-
本地头文件包含(自己写的.h文件) – #include “filename”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
-
库头文件包含 —- #include “filename”
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
3.5.1 头文件重复包含问题
预编译时,会将头文件多次进行加载,造成代码的大量冗余
3.5.2 解决办法
// 1.
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
// 2.
#pragma once