最近好像被限流了很烦=-=
翻译环境类比于我们的VS2019集成开发环境
如图:
我们的可执行程序在生成的过程中,先将源文件逐个单独
(一定是逐个单独)交给编译器使其变成我们连接器可以使用的目标文件(在Windows系统的拓展名是.obj )如test.c—>test.obj,然后经过连接器统一处理变成可执行程序.
围绕着这个两个图展开
分两大步——编译和链接。
编译这一步通过编译器完成
编译又分
预编译(也叫预处理)
完成了头文件的包含
#define
定义的符号和宏的替换
删除注释
一系列的文本操作
编译
语法分析
词法分析
语义分析
符号汇总
也就是将C语言代码转换成了汇编代码。
汇编
将汇编代码转换成二进制也就是机器语言
生成符号表
生成了.obj的文件(他的格式elf),我们可以通过readelf这个工具来观察这个文件.
当我们想在gcc编译器下得到预编译后的结果可以通过输入:gcc test.c -E
这样我们就得到了预编译后的文件test.i
原代码:
后得到的文件内容:
又分为了两个部分:
1.看不懂的部分
而这我们看不懂的部分其实是对#include
注:我们的stdio.h文件并非仅仅只有这些只是但是并非我们需要重视的知识点.
2.几乎和我们代码相同的部分
这些就是我们的代码
#define
定义的符号和宏的替换原代码:
预编译后:
我们之前通过#define
定义的均已被替换我们的#define
也已经被处理不见.
直接将注释删除(偷个懒)
所以无论写多少注释都不会对程序运行产生影响
通过指令gcc test.i -S
编译完成后会得到test.s文件
编译部分会在计算机专业一门叫做:《编译原理》的课上讲解如果大家感兴趣可以听一听在哪一节课上会讲
符号汇总我会在汇编这一步提一嘴(毕竟汇编生成了符号表)
通过指令gcc test.s -c
将汇编指令转换成了二进制指令并形成符号表然后放在text.o(此文件为elf格式)文件内
文件内容:
我们虽然肉眼看不懂二进制指令但是我们可以通过工具readelf
来看此文件.
readelf-s就可以得到
对比一下之前的代码
不难发现我们的文件里有函数和全局变量
可是这个的作用是什么呢?
我们不妨将Add函数拆开到另一个源文件
然后经过编译的全部过程就得到了这些
这个时候我们就需要链接来继续后面的操作了
我们知道链接的作用有
将符号表重定位并合并之后就可以交给我们的运行环境来操作了.
不得不说的是之前的程序员才是真正的大佬
推荐书:《程序员的自我修养》当然这是后期看的书现在看多少有点劝退.(买之前看一下豆瓣评分不要买成同名的书了!!!
VIM作为极强一个编译器也可以搞一下
简明VIM练级攻略:
简明 Vim 练级攻略 | 酷 壳 - CoolShell
VIM速查卡
给程序员的VIM速查卡 | 酷 壳 - CoolShell
程序必须载入内存中。在有操作系统的环境中: 一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
程序的执行便开始。接着便调用main函数。
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
终止程序。正常终止main函数;也有可能是意外终止。
执行程序时就在栈区分配的main函数和Add函数的空间将临时变量放入栈区来后运行程序.
关于函数栈帧的建立和摧毁我会再出一篇博客继续聊.
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些符号都是语言内置的无需定义
最后__STDC__
并不是所有编译器都定义了的至少在VS2019上未被定义.
#define有本质上是将文本内容在编译时进行替换,又因为他可以替换参数所以就出现了宏
注意点:
#define有替换规则:
先替换参数的#define
随后插入文本将宏参数名所替换
最后对结果文件进行扫描看是否还有
宏定义格式:#define name( parament-list ) stuff
宏定义时不能吝啬括号
name和左括号直接不能有空隙如#define MAX(a,b) MAX不能和左括号有空隙
宏不可以递归 不可以一个宏套自己.
使用是宏最好不要使用带有副作用的宏参数如x++
宏一般比函数速度快一般简单的功能使用宏来实现较好
有时我们可以通过宏来实现函数无法实现的功能
宏无法调试因为在编译期间就被替换掉了
如使用宏PRINT打印无论浮点型或者整形还可以实时将我们要打印的变量名加入其中.通过#和##
来实现(目录中有)
语法:#define name stuff
举一个例子
#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__ )
我们C语言的程序员内部有默契,我们把#define
定义的一般做全大写函数命名一般不是全大写一般是首字母大写或其他部分大写,当然我们宏定义假做成函数也会没有全大写.
#define允许把参数替换到文本中,这种实现通常被称为宏,或定义宏
#define name(由逗号隔开的符号) stuff
举例:
#include
#define SQUARE(x) x*x
int main()
{
printf("%d", SQUARE(3));
return 0;
}
值得注意的是:
还有上面的宏其实有一个很大的弊病
我们用下面的代码来说明:
#include
#define SQUARE(x) x * x
int main()
{
printf("%d", SQUARE(3+2));
return 0;
}
这并不是我们想要的答案我们想得到5*5可是这个宏定义给的式子却给了我们11这是因为我们在定义宏时没有考虑到运算的优先级
首先我们的宏在编译阶段会直接和代码替换本次的宏就将printf函数内容进行了替换使SQUARE(3+2)替换成了3+2*3+2这样我们就得到了11的值
所以这也提醒我们在定义宏的时候一定不要吝啬我们的括号只需稍加修改我们的宏变为如下:
#include
#define SQUARE(x) ((x) * (x))
int main()
{
printf("%d", SQUARE(3+2));
return 0;
}
就不会出现以上情况了.
注意:
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
介绍这俩之前我们需要先知道
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s\n", p);
他们的打印结果都是"hello bit"这是因为字符串有自动连接的特点
首先介绍这步用到的知识点
介绍一下#
操作直接上例子:
#define PRINT(x) printf("The "#x" value is %d",x)
int main()
{
int a = 10;
PRINT(a);
return 0;
}
这样define里的#x
和"x"一样.
很适合偷懒
包括打印其他类型也就只是多个参数
#define PRINT(x,y) printf("The "#x" value is" #y,x)
##的作用把两边的符号合成一个符号直接上例子:
#define ADD_TO_SUM(num, value) sum##num += value
int main()
{
int sum1 = 20;
int sum2 = 10;
ADD_TO_SUM(1, 20);
printf("%d", sum1);
return 0;
}
这样我们就在sum1直接加了20或把1改完2就是将sum2加20
x+1;//不带副作用x++;//带有副作用
不要对宏使用带有副作用的参数
如下例
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
不然总会带来我们不想要的结果因为y进行了两次++而x进行了一次++ z得到的是y++的值也就是9当然我们知道宏MAX是什么可以轻松反推但是未来大型项目中这样搞不知会出现什么bug
移除宏定义
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假 定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一 个机器内存大写,我们需要一个数组能够大写。)
这样我们就直接将M定义成了100直接就加入了代码中
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件 编译指令。
比如:
那些我们为了调试而编写的代码,删除可惜但是保留住又十分碍事.
常见的条件编译指令(使用时均包含在main函数中):
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(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
一个一个讲
#if 常量表达式 //... #endif
后面跟常量表达式为真就进入为假就跳过(除了0都是真)所以有时也被当做注释使用(为了装逼=-=)
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
只要这个__DEBUG__
为非零就进入
#if defined(symbol)//symbol被定义了就进入
#ifdef symbol//同上
#if !defined(symbol)//没有被定义就进入
#ifndef symbol//同上
可以嵌套使用
我们可以使用#include
来使另一个文件被编译就行他实际就在#include
的位置一样
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次
分两种
#include"Add.h"
这种包含方式我们的编译器会现在源文件的目录下查找如果没找到编译器就会想查找库函数头文件一样寻找.还没找到就报错
#include
直接就在库函数的头文件找,找不到就报错
有时我们会不小心多次嵌套自己的头文件或者一个头文件被多次嵌套使用就浪费了内存,这时我们只需在文件开头写到
#pragma once
或
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H_
C进阶就基本算是完成了,等我吧文件和通讯录更出来就算完成了C语言的进阶,后续会先C深剖再数据结构.数据结构穿插着题目进行.
本次测试均在Linux环境下进行。 ↩︎