C语言从入门到精通 第十二章(程序的编译及链接)

  写在前面:

  1. 本系列专栏主要介绍C语言的相关知识,思路以下面的参考链接教程为主,大部分笔记也出自该教程。
  2. 除了参考下面的链接教程以外,笔者还参考了其它的一些C语言教材,笔者认为重要的部分大多都会用粗体标注(未被标注出的部分可能全是重点,可根据相关部分的示例代码量和注释量判断,或者根据实际经验判断)。
  3. 如有错漏欢迎指出。

参考教程:C语言程序设计从入门到进阶【比特鹏哥c语言2024完整版视频教程】(c语言基础入门c语言软件安装C语言指针c语言考研C语言专升本C语言期末计算机二级C语言c语言_哔哩哔哩_bilibili

一、程序环境及编译+链接

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

        在ANSI C的任何一种实现中,存在两个不同的环境:

(1)第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

(2)第2种是执行环境,它用于实际执行代码。

2、翻译环境下的工作

(1)组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code),每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

(2)链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

3、编译本身分为几个阶段

(1)预处理(预编译):预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。(把#include包含的文件全部拷贝进文件,注释全部删除,#define全部代入替换)

(2)编译:经过语法分析、词法分析、语义分析和符号汇总(把在全局中的符号汇总,比如全局变量名及函数名)把C代码转换成汇编代码,编译完成之后就停下来,结果保存在test.s中。

(3)汇编:汇编完成之后就停下来,结果保存在test.o中。(把汇编代码转换为二进制/机器指令,利用编译阶段符号汇总的结果形成符号表)

(4)最后还要进行链接,包括合并段表以及符号表的合并和符号表的重定位(如上图,sum有两个地址,重定位会在其中选取一个有效的地址,然后进行合并),然后生成可执行程序.elf。

4、程序执行的过程

(1)程序必须载入内存中。

①在有操作系统的环境中,一般这个由操作系统完成。

②在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

(2)程序的执行开始——调用main函数。

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

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

二、预处理(一)

1、C语言内置的预定义符号

__FILE__      //进行编译的源文件

__LINE__     //文件当前的行号

__DATE__    //文件被编译的日期

__TIME__    //文件被编译的时间

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

2、#define 定义标识符

(1)用#define指令指定一个符号名称(例如PI)代表一个常量(例如3.1415),这个常量就是符号常量,它不占内存,在预编译后这个符号就不存在了。

(2)经过#define的指定后,文件从该行开始所有的(PI)都代表3.1415,对程序进行编译前,预处理器先对PI进行处理,把代码中所有PI全部置换为3.1415,在预编译后,符号常量就全部变成了字面常量3.1415。

(3)“#define 定义标识符”的好处:

①含义清楚。

②在需要改变程序中多出用到的同一个常量时,能做到“一改全改”,非常方便。

(4)#define指令不止可以定义符号常量,它可以把一大段内容(比如一个函数的调用)用一个简短的标识符代替,对程序进行编译前,预处理器会将代码中出现过的这个简短的标识符全部替换成那一大段内容

(5)举例:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#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\tdate:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,\
                          __DATE__,__TIME__ )

int main()
{
	DEBUG_PRINT;
	return 0;
}

3、#define 定义宏

(1)#define 机制包括了一个规定,它允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

        #define name( parament-list ) stuff

①其中parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。

②参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

③stuff中的参数以及整个stuff最好都加括号,这是为了防止运算符优先级引发若干问题。

④在代码中使用定义的宏(函数)时,格式为“name(parament-list)”。

(2)举例:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define SQUARE(x) ((x) * (x))   //整个表达式以及每个参数最好都加括号

int main()
{
	int a = 5;
	printf("%d\n", 10 * SQUARE(a + 1));
	//printf("%d\n", 10 * ((a + 1) * (a + 1)));
	return 0;
}

4、#define 的替换规则

(1)在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换。

(2)替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们对应的值替换。

(3)最后再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程。

(4)需要注意的是:

①宏参数和#define 定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归。

②当预处理器搜索#define定义的符号的时候,字符串常量的内容并不会被搜索。

5、#和##

(1)使用“#”可以把一个宏参数变成对应的字符串,具体方法是在stuff中的字符串参数的首部(双撇号内)添加一个“#”。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

#define PRINT(FORMAT, VALUE)(printf("the value of " #VALUE " is "FORMAT "\n", VALUE))

int main()
{
	int i = 1;
	PRINT("%d", i + 3);
	return 0;
}

(2)使用“##”可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

#define ADD_TO_SUM(num, value) (value##num)

int main()
{
	int sum = ADD_TO_SUM(5, 10);
	printf("%d\n", sum);//105
	return 0;
}

6、带副作用的宏参数

(1)当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。

(2)举例:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	//z = ( (x++) > (y++) ? (x++) : (y++));
	printf("x=%d y=%d z=%d\n", x, y, z); //x=6 y=10 z=9
	return 0;
}

7、宏(函数)和普通函数的对比

(1)用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹

(2)更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用;反之宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型,宏是类型无关的

(3)当然,和函数相比,宏也有劣势的地方:

①每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。

②宏是没法调试的。

③宏由于类型无关,也就不够严谨。

④宏可能会带来运算符优先级的问题,导致程容易出现错。

(4)宏有时候可以做函数做不到的事情,比如宏的参数可以出现类型,但是函数做不到。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))

int main()
{
	//使用
	MALLOC(10, int);//类型作为参数
	//预处理器替换之后:
	//(int *)malloc(10 * sizeof(int));
	return 0;
}

8、#undef

        这条指令用于移除一个宏定义,使用方式如下。

#undef NAME

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

三、预处理(二)

1、条件编译

(1)在编译一个程序的时候,如果要将一条语句(一组语句)编译或者放弃是很方便的,因为C语言有条件编译指令。

(2)调试性的代码(比如用printf输出一些中间变量)删除可惜,保留又碍事,这时就可以选择性地编译。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define __DEBUG__

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
#endif //__DEBUG__
	}
	return 0;
}

(3)常见的条件编译指令:

①单个分支的条件编译:

#if 常量表达式

//...

#endif

//常量表达式由预处理器求值,其逻辑和一般的if语句相同,表达式结果不为零即编译其下的内容

如:

#define __DEBUG__ 1

#if __DEBUG__

//..

#endif

②多个分支的条件编译:

#if 常量表达式

//...

#elif 常量表达式

//...

#else

//...

#endif

③判断是否被定义:

#if defined(symbol)

#ifdef symbol        //如果有用#define定义symbol

#if !defined(symbol)

#ifndef symbol       //如果没有用#define定义symbol(这条指令常用于防止头文件被重复包含)

//这四条指令都需接#endif

④嵌套指令:

#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       //每条条件编译指令都需要配对一个“#endif”

2、offsetof宏的实现

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>  //使用offsetof需要包含此头文件

//写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
#define OFFSETOF(type,member) (size_t)&(((type*)0)->member)

struct S
{
	char c1;
	int i;
	char c2;
};
int main()
{
	struct S s = { 0 };
	printf("%d\n", OFFSETOF(struct S, c1));
	printf("%d\n", OFFSETOF(struct S, i));
	printf("%d\n", OFFSETOF(struct S, c2));

	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/438326.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【学习笔记】数据结构与算法06 - 堆:上堆、下堆、Top-K问题以及代码实现

知识来源&#xff1a;https://www.hello-algo.com/chapter_heap/heap/#4 文章目录 2.5 堆2.5.1 堆&#xff08;优先队列2.5.1.1 堆的常用操作 2.5.2 堆的存储与表示2.5.2.1 访问堆顶元素2.5.2.2 入堆时间复杂度 2.5.2.3 堆顶元素出堆时间复杂度 2.5.3 堆的常见应用2.5.4 建堆问…

每日OJ题_牛客_井字棋

目录 牛客_井字棋 解析代码 牛客_井字棋 井字棋__牛客网 解析代码 class Board {public:bool checkWon(vector<vector<int> > board) {// 当前玩家是否胜出&#xff01;&#xff01;&#xff01;不是有玩家胜出int row board.size(), col board[0].size();fo…

vue 常用的 UI 组件库之一:Vuetify组件库

Vuetify是一个基于Vue.js 的Material Design组件库&#xff0c;它提供了一套完整的、预构建的、可自定义的、响应式的组件&#xff0c;以便开发者可以快速构建美观且功能强大的Web应用程序。Vuetify遵循Material Design设计指南&#xff0c;提供了一系列易于使用的组件&#xf…

【STM32】HAL库 CubeMX教程---基本定时器 定时

目录 一、基本定时器的作用 二、常用型号的TIM时钟频率 三、CubeMX配置 四、编写执行代码 实验目标&#xff1a; 通过CUbeMXHAL&#xff0c;配置TIM6&#xff0c;1s中断一次&#xff0c;闪烁LED。 一、基本定时器的作用 基本定时器&#xff0c;主要用于实现定时和计数功能…

Leetcode : 147. 对链表进行插入排序

给定单个链表的头 head &#xff0c;使用 插入排序 对链表进行排序&#xff0c;并返回 排序后链表的头 。 插入排序 算法的步骤: 插入排序是迭代的&#xff0c;每次只移动一个元素&#xff0c;直到所有元素可以形成一个有序的输出列表。 每次迭代中&#xff0c;插入排序只从输…

【Python学习篇】Python基础入门学习——你好Python(一)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;Vir2021GKBS &#x1f43c;本文由…

全网最细,web自动化测试实战场景(滚动元素的滚动操作)直接上干g货......

前言 使用 selenium 进行 web 自动化测试对我们来说是个常规操作。用了很多次后&#xff0c;我们经常会抱怨 selenium 封装的操作实在是太少了。 比如说 selenium 没有对页面的滚动提供丰富 API , 有的只有一个孤零零的 location_once_scrolled_into_view 方法&#xff0c;把…

融资项目——OpenFeign的降级与熔断

当一个微服务调用其他微服务时&#xff0c;如果被调用的微服务因各种原因无法在规定时间内提供服务&#xff0c;则可以直接使用本地的服务作为备选&#xff0c;即进行降级熔断。 如之前所提到的微服务为例&#xff1a; 如果希望实现降级熔断&#xff0c;可以在本地创建一个实现…

Mac版2024 CleanMyMac X 4.14.6 核心功能详解以及永久下载和激活入口

CleanMyMac 是 macOS 上久负盛名的系统清理工具&#xff0c;2018 年&#xff0c;里程碑式版本 CleanMyMac X 正式发布。不仅仅是命名上的变化&#xff0c;焕然一新的 UI、流畅的动画也让它显得更加精致。新增的系统优化、软件更新等功能&#xff0c;使得在日常使用 macOS 时有了…

【Linux】Linux原生异步IO(一):libaio-介绍

1、IO模型 1.1 简述 相信大家在搜索的时候,都会看到下面这张图,IO的使用场景:同步、异步、阻塞、非阻塞,可以组合成四种情况: 同步阻塞I/O: 用户进程进行I/O操作,一直阻塞到I/O操作完成为止。同步非阻塞I/O: 用户程序可以通过设置文件描述符的属性O_NONBLOCK,I/O操作可…

Cesium-记录差值线

/*** param {Object} startTime Date格式的开始时间* param {Object} endTime Date格式的结束时间* param {Object} coordinates [x1,y1,x2,y2,x3,y3.......]* param {Object} entityCollection 实体收集器*/ async function interpolationLine(startTime,endTime,coordinat…

工厂 模式

一、工厂模式是什么&#xff1f; 是C多态的一种很好的具体表现。通过继承&#xff0c;重写抽象父类的虚函数&#xff0c;并在main函数中通过基类指针指向子类对象的一种编码风格 工厂模式分为三种&#xff08;简单工厂模式&#xff0c;工厂方法模式&#xff0c;抽象工厂模式&…

晶圆测量新利器:光谱共焦传感器优势解析

光谱共焦位移传感器和激光三角位移传感器在表面测量领域均占据重要位置&#xff0c;它们各自在测量物体厚度方面表现出独特的优势。尽管两者具备测量功能&#xff0c;但根据应用环境和所需精度&#xff0c;它们的适应性呈现出显著差异。 具体而言&#xff0c;光谱共焦位移传感器…

PSCA电源控制集成之分布式PPU

PPU的放置是一个重要考虑因素。最简单的方法是将所有的PPU都放置在SCP所在的始终开启的域中。将所有的PPU放置在一个层次结构中&#xff0c;集成问题&#xff0c;如地址映射、互连、时钟和复位等问题都比较简单。然而&#xff0c;有几个原因可能导致这不是最佳选择。 首先&…

Qt---项目代码解析

文章目录 一、main.cpp代码解析二、widget.h代码解析三、widget.cpp代码解析(一) form file 四、.pro Qt项目的工程文件 一、main.cpp代码解析 main函数的形参就是命令行参数。qt是CDefinitely图形界面化编程&#xff0c;要想编写一个qt的图形界面程序&#xff0c;一定要有QAp…

【Spring底层原理高级进阶】Spring Batch清洗和转换数据,一键处理繁杂数据!Spring Batch是如何实现IO流优化的?本文详解!

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

蚂蚁感冒 刷题笔记

/* 解题思路 首先根据题意可知 1.蚂蚁速度均为1 即同向蚂蚁永远不可能追上 我们需要求最后感冒蚂蚁的数量 因为蚂蚁碰头将会掉头 效果和俩蚂蚁互相穿过继续走是一样的 所以我们将俩蚂蚁碰头视作穿过 2. 如果俩蚂蚁相向而行 则俩蚂蚁必定碰头 首先 我们获得第一个感冒蚂蚁的…

Vue+OpenLayers7入门到实战:OpenLayers7如何使用gifler库来实现gif动态图图片叠加到地图上

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 OpenLayers7本身不支持gif图片作为图标要素显示到地图上,所以需要通过其他办法来实现支持gif图片。 本章介绍如何使用OpenLayers7在地图上使用gifler库先生成canvas画板,然后通过canvas画板的重绘事件来重新渲染地图…

URL输入到页面渲染过程详解

当我们在浏览器中输入一个URL并按下回车键时&#xff0c;浏览器会执行一系列步骤来解析URL、发送请求、接收响应&#xff0c;并最终渲染页面。这个过程涉及到多个阶段&#xff0c;包括DNS解析、TCP握手、发送HTTP请求、服务器处理请求、返回HTTP响应、浏览器解析和渲染等。下面…

19-Java中介者模式 ( Mediator Pattern )

Java中介者模式 摘要实现范例 中介者模式&#xff08;Mediator Pattern&#xff09;提供了一个中介类&#xff0c;该类通常处理不同类之间的通信&#xff0c;并支持松耦合&#xff0c;使代码易于维护中介者模式是用来降低多个对象和类之间的通信复杂性中介者模式属于行为型模式…