预处理和程序的编程(跑路人笔记)

前言

最近好像被限流了很烦=-=

程序的翻译环境和执行环境

翻译环境类比于我们的VS2019集成开发环境

如图:

image-20220118151113009

image-20220118211250463

我们的可执行程序在生成的过程中,先将源文件逐个单独(一定是逐个单独)交给编译器使其变成我们连接器可以使用的目标文件(在Windows系统的拓展名是.obj )如test.c—>test.obj,然后经过连接器统一处理变成可执行程序.

翻译环境如何工作

image-20220118210127994

围绕着这个两个图展开

分两大步——编译和链接。

编译1

编译这一步通过编译器完成

编译又分

  • 预编译(也叫预处理)

    • 完成了头文件的包含

    • #define定义的符号和宏的替换

    • 删除注释

      一系列的文本操作

  • 编译

    • 语法分析

    • 词法分析

    • 语义分析

    • 符号汇总

      也就是将C语言代码转换成了汇编代码。

  • 汇编

    • 将汇编代码转换成二进制也就是机器语言

    • 生成符号表

      生成了.obj的文件(他的格式elf),我们可以通过readelf这个工具来观察这个文件.

预编译

当我们想在gcc编译器下得到预编译后的结果可以通过输入:gcc test.c -E这样我们就得到了预编译后的文件test.i

功能一:完成头文件的包含

​ 原代码:

image-20220119181626401

后得到的文件内容:

image-20220119181947816

又分为了两个部分:

1.看不懂的部分

image-20220119182108303

而这我们看不懂的部分其实是对#include<stdio.h>的操作.他讲stdio.h内的文件包含到了我们的函数内

注:我们的stdio.h文件并非仅仅只有这些只是但是并非我们需要重视的知识点.

2.几乎和我们代码相同的部分

image-20220119182200073

这些就是我们的代码

功能二: #define定义的符号和宏的替换

原代码:

image-20220119182958721

预编译后:

image-20220119183126194

image-20220119183309110

我们之前通过#define定义的均已被替换我们的#define也已经被处理不见.

功能三: 删除注释

直接将注释删除(偷个懒)

所以无论写多少注释都不会对程序运行产生影响

编译

通过指令gcc test.i -S

编译完成后会得到test.s文件

编译部分会在计算机专业一门叫做:《编译原理》的课上讲解如果大家感兴趣可以听一听在哪一节课上会讲

  • 语法分析
  • 词法分析
  • 语义分析
  • 符号汇编

符号汇总我会在汇编这一步提一嘴(毕竟汇编生成了符号表)

汇编

通过指令gcc test.s -c

将汇编指令转换成了二进制指令并形成符号表然后放在text.o(此文件为elf格式)文件内

文件内容:

image-20220119190111303

我们虽然肉眼看不懂二进制指令但是我们可以通过工具readelf来看此文件.

生成符号表

readelf-s就可以得到

image-20220119190905812

对比一下之前的代码

image-20220119192923078

不难发现我们的文件里有函数和全局变量

可是这个的作用是什么呢?

我们不妨将Add函数拆开到另一个源文件

image-20220120105327283

然后经过编译的全部过程就得到了这些

image-20220120110641826

这个时候我们就需要链接来继续后面的操作了

链接

我们知道链接的作用有

  • 符号表的合并和重定位
  • 合并段表

image-20220120111701708

将符号表重定位并合并之后就可以交给我们的运行环境来操作了.

题外话

不得不说的是之前的程序员才是真正的大佬

推荐书:《程序员的自我修养》当然这是后期看的书现在看多少有点劝退.(买之前看一下豆瓣评分不要买成同名的书了!!!

VIM作为极强一个编译器也可以搞一下

VIM学习资料

简明VIM练级攻略:

简明 Vim 练级攻略 | 酷 壳 - CoolShell

VIM速查卡

给程序员的VIM速查卡 | 酷 壳 - CoolShell

运行环境

  1. 程序必须载入内存中。在有操作系统的环境中: 一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

  2. 程序的执行便开始。接着便调用main函数。

  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。

  4. 终止程序。正常终止main函数;也有可能是意外终止。

对第3的小讲

image-20220120120804801

执行程序时就在栈区分配的main函数和Add函数的空间将临时变量放入栈区来后运行程序.

关于函数栈帧的建立和摧毁我会再出一篇博客继续聊.

预处理

预处理定义符号

__FILE__ //进行编译的源文件

__LINE__ //文件当前的行号

__DATE__ //文件被编译的日期

__TIME__ //文件被编译的时间

__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

这些符号都是语言内置的无需定义

image-20220121092200525

最后__STDC__并不是所有编译器都定义了的至少在VS2019上未被定义.

#define

#define有本质上是将文本内容在编译时进行替换,又因为他可以替换参数所以就出现了宏

注意点:

  • #define有替换规则:

  • 先替换参数的#define

  • 随后插入文本将宏参数名所替换

  • 最后对结果文件进行扫描看是否还有

  • 宏定义格式:#define name( parament-list ) stuff

  • 宏定义时不能吝啬括号

  • name和左括号直接不能有空隙如#define MAX(a,b) MAX不能和左括号有空隙

  • 宏不可以递归 不可以一个宏套自己.

  • 使用是宏最好不要使用带有副作用的宏参数如x++

  • 宏一般比函数速度快一般简单的功能使用宏来实现较好

  • 有时我们可以通过宏来实现函数无法实现的功能

  • 宏无法调试因为在编译期间就被替换掉了

如使用宏PRINT打印无论浮点型或者整形还可以实时将我们要打印的变量名加入其中.通过#和##来实现(目录中有)

#define替换文本

语法:#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允许把参数替换到文本中,这种实现通常被称为宏,或定义宏

#define name(由逗号隔开的符号) stuff

举例:

#include<stdio.h>
#define SQUARE(x) x*x
int main()
{
	printf("%d", SQUARE(3));
	return 0;
}

image-20220121094331474

值得注意的是:

还有上面的宏其实有一个很大的弊病

我们用下面的代码来说明:

#include<stdio.h>
#define SQUARE(x) x * x
int main()
{
	printf("%d", SQUARE(3+2));
	return 0;
}

image-20220121094501090

这并不是我们想要的答案我们想得到5*5可是这个宏定义给的式子却给了我们11这是因为我们在定义宏时没有考虑到运算的优先级

首先我们的宏在编译阶段会直接和代码替换本次的宏就将printf函数内容进行了替换使SQUARE(3+2)替换成了3+2*3+2这样我们就得到了11的值

所以这也提醒我们在定义宏的时候一定不要吝啬我们的括号只需稍加修改我们的宏变为如下:

#include<stdio.h>
#define SQUARE(x) ((x) * (x))
int main()
{
	printf("%d", SQUARE(3+2));
	return 0;
}

就不会出现以上情况了.

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

  2. 当预处理器搜索#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;
}

image-20220121125229338

这样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; 
}

image-20220121131745511

这样我们就在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;
}

image-20220121132440645

不然总会带来我们不想要的结果因为y进行了两次++而x进行了一次++ z得到的是y++的值也就是9当然我们知道宏MAX是什么可以轻松反推但是未来大型项目中这样搞不知会出现什么bug

undef

移除宏定义

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

命令行定义^ 2

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假 定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一 个机器内存大写,我们需要一个数组能够大写。)

image-20220121134846066

这样我们就直接将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<stdio.h>

直接就在库函数的头文件找,找不到就报错

嵌套文件的包含

有时我们会不小心多次嵌套自己的头文件或者一个头文件被多次嵌套使用就浪费了内存,这时我们只需在文件开头写到

#pragma once

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H_

结尾

C进阶就基本算是完成了,等我吧文件和通讯录更出来就算完成了C语言的进阶,后续会先C深剖再数据结构.数据结构穿插着题目进行.


  1. 本次测试均在Linux环境下进行。 ↩︎

  • 31
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

就一个挺垃圾的跑路人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值