c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译


文章目录

  • c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译
  • 前言
  • 一、程序的翻译环境和执行环境
  • 二、详解编译和链接
    • 1. 翻译环境
    • 2. 编译本身也分为几个阶段
      • 编译
        • 预编译:
        • 编译:
        • 汇编:
      • 链接
    • 3. 运行环境
  • 三、预处理详解
    • 1. 预定义符号
    • 2. #define
      • 2.1 #define 定义标识符
      • 2.2 #define 定义宏
      • 2.3 #define 替换规则
      • 2.4 #和##
      • 2.5 带副作用的宏参数
      • 2.6 宏和函数的对比
    • 3. #undef
    • 4. 命令行定义
    • 5. 条件编译
    • 6. 文件包含
  • 总结


前言

在本章会对程序的编译过程,进行详细的讲解,重点包括:

  • 程序的翻译环境
  • 程序的执行环境
  • c语言程序的编译+链接
  • 预定义符号介绍
  • 预处理指令#define
  • 宏和函数的对比
  • 预处理操作符#和##的介绍
  • 命令定义
  • 预处理指令#include
  • 预处理指令#undef
  • 条件编译

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

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

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令;
第2种是执行环境,它用于执行代码;

下图简单的表示了这两个环境的作用:
在这里插入图片描述

二、详解编译和链接

1. 翻译环境

在这里插入图片描述

  • 组成程序的每个源文件通过编译过程分别转换成目标代码(object code);
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序;
  • 链接器同时也会引入标准c语言库中任何被该程序使用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中;

以之前写过的通讯录程序为例子:
该程序有2个源文件:contact.c、test.c,这两个源文件在翻译环境中单独进行翻译生成对应的目标文件(后缀为.obj);
即生成contact.obj和test.obj
在这里插入图片描述
之后这些生成的目标文件将会一起经过链接器,链接在一起生成一个可执行程序,在这个过程中,还会将程序会使用到的库函数一起链接进来。

所以翻译环境中,代码经历的过程又可以分为两步,编译和链接:
在这里插入图片描述
在VS使用的编译器是cl.exe,使用的链接器是link.exe;

2. 编译本身也分为几个阶段

编译过程还可以详细的分为3个步骤:预编译(预处理),编译,汇编;
在这里插入图片描述
接下来我们会对这三个步骤逐个的详细讨论(在linux环境下进行):

编译

示例代码如下:
在这里插入图片描述

预编译:

gcc test.c -E 这个命令的作用是让源文件在预处理之后就停下;这条命令不生成文件,可以使用重定向将命令执行结果输出到一个文件中;这里使用gcc test.c -E > test.i将该命令等到的信息输出到test.i这个文件中;后缀为.i的文件是预处理后得到的文件;

打开得到的文件可以看到如下的结果:
在这里插入图片描述
在此之前还有很多内容,仅截取部分;可以发现我们并不能看懂这部分的内容;
在它之后的内容我们就能看懂了,这是我们的代码:
在这里插入图片描述
可以看到在我们的代码之前多出来了一大堆东西,但是少了一条语句 - #include <stdio.h>
在我们的心法篇中介绍了#include包含的头文件会以替换的形式将头文件中的内容包含进行源代码中,所以上面那一大堆代码其实就是头文件stdio.h中的内容。

那么我们就来验证一下,看看上面的内容到底是不是stdio.h中的内容,可以看到stdio.h的地址是/usr/include,我们打开这个文件比较一下,两者是否相同。
在这里插入图片描述
此目录下确实存在这个文件,接下来看看文件的内容:
在这里插入图片描述
可以看到之前的确实是stdio.h拷贝到test.c中的内容;

接下来我们就来看看,预处理过程究竟做了什么?
从上图中我们可以看出,预处理

  • 完成了头文件的包含#include;
  • 完成了#define定义的符号和宏的替换
  • 删除了所有注释;

    总结一下,预处理过程完成了源文件到目标文件过程中所有的文本操作
编译:

gcc test.c -S,此命令的作用是对test.c这个文件进行预处理和编译;也可以使用gcc test.i -S,这个命令可以得到同样的结果;命令执行之后会得到一个test.s
在这里插入图片描述
大概看一下test.s中有什么内容:
在这里插入图片描述
在预处理阶段,得到的文件仍是一个c语言代码,经过编译之后就得到了像上面一样的汇编代码;

这个过程中会进行语法分析、词法分析、语义分析、符号汇总;这4个步骤分别会做些什么,这里就不详细介绍,具体内容请自行学习编译原理;这里仅作简单介绍;

从上面的图片中我们得知,编译的过程就是将c语言代码转换成汇编代码的过程;这个转换的过程是由计算机来完成的,这也意味着计算机需要能够“读懂”c语言代码;那么要读懂c语言代码需要做到什么呢?首先需要知道每个语句表示什么,比如,这个东西是一个变量,那个是一个for循环,这就是语法分析大概要完成的事情;除此之外,还需要对每个语句进行拆分,比如,将一个for循环语句中拆分出for关键字,变量名等等;在知道了语法和词法之后,还需要知道一个语句表示的是什么意思,这就是语法分析要做的事;至于符号汇总,我们放在汇编中再作介绍;

汇编:

使用gcc test.c -c可以将对源文件进行预编译、编译和汇编,生成test.o文件,相当于windows系统中的.obj文件,也就是目标文件;
在这里插入图片描述
那么我们打开这个test.o文件看看里面保存的是什么;
在这里插入图片描述
test.o文件打开之后就是一堆乱码,也就是test.o是一个二进制文件;
所以汇编过程的作用是将汇编代码转换成了机器指令(二进制指令);
在这个过程中除了上面我们能看出来的将汇编代码转换为机器指令之外,还有一件非常重要的事情:生成符号表

在这里需要生成的符号表和上一步中的符号汇总有什么联系呢?
在说明上面的联系之前,我们要先知道test.o文件的是一种elf类型的文件,这是一种分段的文件,代码的符号表就保存在这种文件的符号段中,使用readelf test.o --syms就可以获取到这个目标文件的符号表:
在这里插入图片描述
对其进行分析:
在这里插入图片描述
从上图中可以看到,汇编过程中得到的符号表中仅有一些全局的符号,而这些符号又是在编译过程的符号汇总中汇聚在一起的,这一步会将整个程序中所有源文件中存在的全局符号全部汇总在一起,之后再进行处理;

为了能更加清楚的展示编译过程会将符号汇总,在汇编过程中会生成对应的符号表,下面重新在对test.c和add.c进行编译:
在这里插入图片描述
分别对它们进行编译:
在这里插入图片描述
查看它们生成的符号表:
在这里插入图片描述
可以看到在test.o的符号表中也有Add这个符号,但是实际上它并知道这个符号的地址是什么,只是给它填充一个没有意义的地址;
在这里插入图片描述
可以看到单独生成的test.o和add.o中都包含有符号Add,只是一个的地址是有效的,一个是无效的;但是这两个符号在生成可执行程序时,到底使用哪一个呢?这一步就交到链接器来完成;

链接

在链接阶段,链接器会将多个目标文件和链接库进行链接,生成一个可执行文件;在上面的例子中,仅有两个目标文件,test.o和add.o,没有链接库,所以在此程序中,链接操作的作用就是将这两个目标链接成一个可执行文件a.out(可以通过参数设置,修改生成的可执行文件的名字),linux系统中;命令:gcc test.o add.o -o test.out生成一个名为test.out可执行文件;(注意,linux系统中并不以后缀名来区分文件是否可执行,而通过文件权限和文件格式(必须是elf格式)来区分,可以看到test.out文件是有执行的权限的X标志);

在这里插入图片描述

该阶段会进行的具体操作可分为:

  1. 合并段表;
  2. 符号表的合并和重定位;

因为,可执行文件的格式也是elf,所以在由多个目标文件生成可执行文件过程中,需要将这些文件中相同段中的内容进行合并,这就是合并段表;
而符号表的合并和重定位就更容易理解了,在介绍汇编时,我们提到了test.o和add.o这两个文件和符号表中都有Add这个符号,在两个目标文件合并成一个可执行文件的过程中,这两个Add肯定只能保留一个,也就是保留那个有着有效信息的Add,将另一个无效的Add删除,并将Add的地址保存为add.o中的地址;简单来说就是将多个目标文件的符号进行合并,并将有效的符号进行保留;
在这里插入图片描述
看到这里的同学肯定会疑惑,这个符号表有什么用呢?
这个符号表可以使得程序能够通过它们找到并使用对应的符号,比如,程序要调用Add函数时,就会到符号表中找Add这个符号的地址,也就是0x1008,之后程序通过这个地址调用了Add函数,完成了功能;

下面我们在VS环境中举个例子帮助大家更好的理解这点。
将add.c中所有内容注释掉,此时add.c变成了一个空文件,在汇编之后会生成一个空的符号表,test.c文件仍会生成包含两个符号的符号表,其中符号Add的地址是无效地址;
此时进行编译就会出现以下的错误:
在这里插入图片描述
在这里插入图片描述
在链接时,通过合并的符号表寻找Add函数时,链接器去往0x0000这个地址会发现并没有Add这个函数存在,也就发生了错误;

正是因为有了符号表的存在,跨文件的函数调用才得以实现;

总结
翻译过程可以分为编译和链接两个步骤,其中编译又可以分为,预编译、编译、汇编三个步骤;
它们使用的命令分别是:gcc -test.c -Egcc -test.c -Sgcc -test.c -c
预编译过程进行文本操作(test.i),编译过程将c语言代码转换成汇编代码(test.s),汇编过程将汇编代码转换成机器指令(目标文件,test.o);最后由链接器将这些目标文件链接成一个可执行文件;

一图流:
在这里插入图片描述

3. 运行环境

程序运行的过程:

  1. 程序必须载入内存中才可以运行;在有操作系统的环境中,该步骤一般由操作系统来完成;在独立的环境中,程序的载入要么手动操作,要么通过可执行代码置入只读内存来实现;
  2. 程序的执行便开始。接着调用main函数;
  3. 开始执行程序代码。此时程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态内存(static),存储于静态内存中的变量在程序执行整个过程中一直保留它们的值;
  4. 终止程序。正常终止main函数;有时候也可能是意外终止;

关于运行时堆栈,这里仅简单介绍,想要详细了解,大家可以去看之前的文章内容,里面有详细介绍;在心法篇中的函数和秘术篇的动态内存分配里都有介绍;

在这里插入图片描述
注意:堆栈指的就是栈,并不是堆+栈

三、预处理详解

1. 预定义符号

__FILE__ // 进行编译的源文件
__LINE__ // 文件当前行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义
__FUNCTION__ // 当前函数

这些预定义符号都是语言内置的。
举个例子:

#include <stdio.h>int main()
{printf("file:%s\n", __FILE__);printf("line:%d\n", __LINE__);printf("date:%s\n", __DATE__);printf("time:%s\n", __TIME__);printf("function:%s\n", __FUNCTION__);// printf("STDC:%s\n", __STDC__); // VS 2022 未定义return 0;
}

运行结果:
在这里插入图片描述

这些内置符号具体有什么用呢?
可以用来写程序日志,在日志中记录时间和代码行号,在出错时,就可以非常方便的定位错误原因;就算是正常运行过程中,写日志也是一个非常重要的事,它可以让你可以了解程序当前是运行状态,检查可能出现的一些问题;

下面我们给出一个记录日志的例子供大家参考:

#include <stdio.h>int main()
{// 记录日志,就是在写文件// 将程序执行的信息输出到log.txt中FILE* pf = fopen("log.txt", "a");if (pf == NULL){perror(pf);return ;}int i = 0;for (i = 0; i < 10; i++){// 记录每行的信息fprintf(pf, "%s %d %s %s %d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);}fclose(pf);pf = NULL;return 0;
}

文件内容:
在这里插入图片描述

2. #define

2.1 #define 定义标识符

语法:
#define name stuff

示例:

// #define 定义标识符
#define MAX 100 // 定义一个数字
#define uint unsigned int // 有时一个关键字较长,可以创建一个更简短的方式
#define do_forever for(;;) // 用更形象的符号来替换一个种实现,这里实现的是一个死循环
#define CASE break;case // 在写case语句的时候自动把 break写上;// 因为有此语言中switch语句不用加break,习惯了这些语言的程序员可能在使用c语言时,// 害怕自己忘记添加break导致程序出错,就可以使用这种标识符
// 如果定义的stuff过长,可以分成几行来写,除了最后一行外,每一行的后面都加上一个反斜杠 \ (续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n", \__FILE__, __LINE__, \__DATE__, __TIME__)#include <stdio.h>int main()
{uint i = MAX;// do_forever; // 相较于,for(;;);,该语句以一个更清楚的方式,实现了一个死循环int input = 0;scanf("%d", &input);// 代码1switch (input){case 1:CASE 2:CASE 3:}// 代码2switch (input){case 1:break;case 2:break;case 3:}// 代码1和代码2完全等价DEBUG_PRINT;return 0;
}

大家可能在使用#define定义标识符时,会产生一个疑问,在标识符后面需不需要加上一个分号;呢?
比如:

#define MAX 1000;
#define MAX 1000

这两种定义方式有什么不同呢?

我们都知道,#define定义的标识符是在预处理阶段直接进行文本替换的;
放在代码中:

int main()
{int a = MAX; // 使用第一种定义方式时,这个代码会变成 int a = 1000;;// 赋值语句之后还有一个空语句,在这里语法是没有什么问题的,// 但是换一种情况就会产生问题if(condition)max = MAX; // 在这里一条if语句只能对应一条语句,但是这里有了两条语句,产生了语法错误elsemax = 0;
}

所以我们在使用#define定义标识符时,一般都不会在后面添加;

2.2 #define 定义宏

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

下面是宏的申明方式:

#define name(parament-list) stuff
// parament-list是一个逗号隔开的符号表,它们可能出现 stuff中

注意,参数列表的左括号必须与name紧邻;如果两者之间存在有任何空白存在,参数列表就会被解释为stuff的一部分,就变成了#define定义的标识符了

使用示例:

// 定义一个宏来计算平方
#define SQUARE(x) ((x) * (x))
// 这个宏接受一个参数,x// 在程序中使用
SQUARE(8);
// 该宏会像#define定义的标识符一样,直接在预处理阶段进行替换
// 即程序中的代码变成
((8) * (8));

易错点:
大家初次使用宏可能会疑惑,为什么上面定义的这个宏要有那么多的括号呢?
下面我们就来看看如果没有括号会发生什么:

#include <stdio.h>
#define SQUARE(x) x * xint main()
{int a = 2;int ret = SQUARE(a + 1);printf("%d\n", ret);return 0;
}

这段代码会如预期一样输出9吗?
在这里插入图片描述
可以看到输出结果是5
这是为什么呢?
宏的本质是在文本处理阶段直接替换,所以上面的代码在预处理之后变成了:

#include <stdio.h>int main()
{int a = 2;int ret = a + 1 * a + 1;printf("%d\n", ret);return 0;
}

可以看到,该程序是将a + 1 * a + 1赋值给了ret,也就是 2 + 1 * 2 + 1,结果就是5;

当我们的宏定义成#define SQUARE(x) ((x) * (x))时,上面代码在替换之后为,((2 + 1) * (2 + 1)),结果与预期相同;

这时有人可能还有疑问,在x外部加上括号不就行了,为什么在计算结果外面也要加上括号呢;
我们再看一个例子:

#define DOUBLE(x) (x) + (x)int main()
{int a = 5;int ret = 10 * DOUBLE(a);printf("%d\n", ret);return 0;
}

这段代码在替换之后变成了int ret = 10 * (5) + (5)这与预期的结果也是不符的,当宏的结果外面有了括号,此时int ret = 10 * ((5) * (5))才符合预期;

结论
在使用宏来对数值表达式求值时,宏的定义都应该在参数和总体外面加上括号,这样就可以避免在使用宏时出现预期之外的错误;

2.3 #define 替换规则

在程序中扩展#define定义的符号和宏时,需要涉及以下的几个步骤:

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,首先替换掉它们;
  2. 替换文本随后被插入到程序原来文本的位置。对于宏,宏的参数名被它的值替换;
  3. 最后,再对结果文件进行扫描,看看是否还有包含任何由#define定义的符号。如果是,就重复上述步骤;

示例:

#include <stdio.h>#define M 20
#define MAX(x, y) (((x) > (y))? (x) : (y))int main()
{int a = 10;printf("%d\n", MAX(a, M));return 0;
}

这段代码中,MAX这个宏中第二个参数是一个#define定义的符号,所以此时先替换M,变成printf("%d\n", MAX(a, 20));
之后替换文本,printf("%d\n", (((10) > (20)) ? (10) : (20)));

注意

  1. 宏参数和#define定义中可以出现其他#define定义的符号,但对于宏,不能出现递归;
  2. 当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索;

举个例子说明第二点:

printf("M = %d\n", M);

这段代码中的字符串中的M不会被替换;
输出结果为:M = 20

2.4 #和##

#的作用

如何把参数插入到字符串中?

在介绍这个内容之前,我们先看下面的代码:

#include <stdio.h>int main()
{printf("hello world\n");printf("hello ""world\n");return 0;
}

这两个输出语句,输出的结果相同吗?
在这里插入图片描述
由上图得知,这两句代码效果相同;

在了解了上面的printf函数的使用之后,再来看下面的代码:

#include <stdio.h>int main()
{int a = 10;// 需要得到 the value of a is 10 这样的输出int b = 20;// 需要得到 the value of b is 20 这样的输出int c = 30;// 需要得到 the value of c is 30 这样的输出return 0;
}

要完成上面的需求,应该怎么解决呢?函数?
试试看:

void print(int x)
{printf("the value of x is %d\n", x);
}

能这样写吗?显然不行,写成函数的话,输出结果就被写死了,只有值能随着参数的变化而变化,字符串内容无法改变;只能写成3个不同的函数来实现;

想到上面提到的printf函数的特性,能写成一个宏来实现这个功能吗?

#define PRINT(X) printf("the value of " X "is %d\n", X);

这样写正确吗?预想中,先输出字符串the value of ,再输出X,再输出字符串is %d\n%d对应X的值;

实际上:
在这里插入图片描述
这里需要第一个a变成一个字符串,c语言规定,使用#修饰宏的参数,在预处理阶段,该参数会作为字符串进行替换;即,#X会变成"X"
将代码改成:

#include <stdio.h>#define PRINT(X) printf("the value of " #X " is %d\n", X);int main()
{int a = 10;// 需要得到 the value of a is 10 这样的输出PRINT(a);int b = 20;// 需要得到 the value of b is 20 这样的输出PRINT(b);int c = 30;// 需要得到 the value of c is 30 这样的输出PRINT(c);return 0;
}

运行结果:
在这里插入图片描述
可能有人会想可以用3个引号吗?也就是写下面这样:

#define PRINT(X) printf("the value of " "X" " is %d\n", X);

很显然不行,这个与上面提示的预处理器处理时不会替换字符串常量中的符号冲突,此时的输出变成:
在这里插入图片描述

接下来我们再对这个宏进行优化,此时该宏只能处理整型数据,能否让它可以处理任何类型的变量呢?当然是可以的,要处理什么类型的数据,这个问题使用者是清楚的,所以我们的宏在增加一个参数,接收数据的类型;

#include <stdio.h>#define PRINT(X, FORMAT) printf("the value of " #X " is " FORMAT "\n", X);int main()
{int a = 10;// 需要得到 the value of a is 10 这样的输出PRINT(a, "%d");int b = 20;// 需要得到 the value of b is 20 这样的输出PRINT(b, "%d");int c = 30;// 需要得到 the value of c is 30 这样的输出PRINT(c, "%d");float f = 3.14f;PRINT(f, "%f");return 0;
}

在这里插入图片描述

##的作用

##可以将位于它两边的符号连成一个符号;
它允许宏定义以分离的文本片段创建标识符;

示例:

#include <stdio.h>#define STR "要你命"
#define CAT(X, Y) X##Yint main()
{// 达文西现在除了要你命3000之外,还有了许多其他的要你命系列的武器// 现在需要根据提供的型号,产生对应的字符串printf("%s\n", CAT(STR, "4000"));// 譬如此时有一个变量名就是Annihilator3000int Annihilator3000 = 100; // 这里为了演示方便使用int类型,也可以使用其他类型printf("%d\n", CAT(Annihilator, 3000));return 0;
}

运行结果:
在这里插入图片描述
注意
使用##时,除了#define定义的标识符会进行替换之外,符号两边是什么,就用什么进行拼接,使用变量时,不会使用变量指代的值,而是直接使用变量名本身;

2.5 带副作用的宏参数

什么叫做带副作用的宏参数呢?
副作用就是表达式求值时出现的永久性效果。比如,x+1这个表达式就没有副作用,x++这个表达式就有副作用,它会永久性的改变x的值;
当宏参数在宏的定义中出现超过一次时,如果参数带有副作用,那么你在使用这个宏时可能出现不可预测的结果;

示例:

#include <stdio.h>#define MAX(X, Y) ((X) > (Y)? (X) : (Y))int main()
{int a = 5; int b = 8;int c = MAX(a++, b++);// 这里的结果会是什么呢?// 会是6, 9, 9吗 printf("%d, %d, %d\n", a, b, c);return 0;
}

运行结果:
在这里插入图片描述
可以看到实际结果与预期不符,这是因为条件表达式在替换之后为int c = ((a++) > (b++)? (a++) : (b++)),两个参数比较之后,还执行了一次b++;所以b变成了10;

所以在使用宏时,尽量不要使用有副作用的参数;

2.6 宏和函数的对比

比较下面两种实现哪种更好

// 代码1
#define Add1(X, Y) ((X) + (Y))// 代码2
int Add2(int x, int y)
{return (x + y);
}int main()
{int a = 10;int b = 20;int c = Add1(a, b);int d = Add2(a, b);return 0;
}

结论是代码1更好;
原因如下:
这是宏的汇编代码,仅有三句;
在这里插入图片描述
下面的是函数实现的汇编执行过程:
在这里插入图片描述
很显然,函数调用比宏的实现要复杂得多;

所以简单的运算使用宏是更优的;

原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。而宏可以适用于整型、长整型、浮点数等可以使用>进行比较的类型,使用更灵活;宏与类型无关

当然宏与函数相比也存在有劣势的地方:

  1. 每次使用宏时,都是通过代码替换的方式实现的。除非一个宏较短,否则,这会大大增加程序的长度;
  2. 宏是无法调试的;因为宏的替换是在预处理阶段,而调试则是发生在可执行程序运行中;
  3. 宏与类型无关,既是优点也是缺点,因为没有类型,所以不够严谨;
  4. 宏可能存在运算符优先级的问题,导致程序更容易出错;

宏也能做到一些函数无法做到的事,比如#中的例子,还有宏的参数可以是类型,函数不行;

#include <stdlib.h>#define MALLOC(x, type) \((type*)malloc(sizeof(type) * x))int main()
{// 开辟10个整型的空间int *p = MALLOC(10, int);return 0;
}
属性#define定义宏函数
代码长度每次使用时,宏代码都会插入程序,程序长度会增加函数代码只出现一次,每次调用都是使用同一份代码
执行速度更快存在函数调用和返回的额外开销,较慢
操作符优先级宏参数的求值是在所有周围上下方环境中,除非加上括号,否则邻近操作符的优先级可能会对求值产生不可预料的影响函数参数只在传参时求值一次,将求值结果传递给函数,结果更容易预测
带副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预期的结果函数参数只在传参时求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用任何参数函数的参数确定类型的,不同的参数需要不同的函数,即使它们执行的任务相同
调试宏无法调试函数可以逐语句调试
递归宏不可递归函数可以递归

命名约定
一般来说,函数和宏使用的语法非常相似;所以语言本身无法帮助我们区分它们。
所以平时,我们习惯于将:

宏名全部大写
函数名不要全部大写

3. #undef

这条语句用于移除一个宏的定义

#include <stdio.h>
#undef NAME
// 如果存在一个名为 NAME的宏,当你不再想要它时,可以使用#undef来将它移除#define M 100int main()
{int a = M;
#undef Mprintf("%d %d\n", a, M);return 0;
}

在这里插入图片描述

4. 命令行定义

对于许多c语言的编译器提供了在命令行中定义符号的功能;
假如对于一个可移植的代码,代码中有一个数组,有些机器的内存空间有限,所以这个数组的长度就小,对于另一些内存较大的机器,数组的长度就可以更长一点;
此时就可以使用命令行定义:

示例:

在这里插入图片描述
在这里插入图片描述
可以看到直接编译时会出现错误,提示M没有定义;此时可以使用命令行命令定义变量M的值为10,之后再进行编译就可以通过,并且成功运行;

5. 条件编译

c语言中语句的编译可以像条件语句一样,选择性的进行编译;

比如:

#include <stdio.h>
#define __PRINT__int main()
{
// 如果已经定义了__PRINT__,则#ifdef和#endif之间的代码参与编译
#ifdef __PRINT__printf("hehe\n");
#endifreturn 0;
}

常见的条件编译指令:

// 1. 
#if 常量表达式// ....
#endif
// 常量表达式为真时,被包括的内容参与编译;为假则反之
// 常量表达式由预处理器求值// 如:
#define __DEBUG__ 1
#if __DEBUGprintf("hehe");
#endif// 2. 多个分支的条件编译
#if 常量表达式// ...
#elif 常量表达式// ...
#else// ...
#endif
// 可以看到条件编译语句和条件分支语句很像// 3. 判断是否被定义
#if defined(symbol) // ...
#endif
// 写法1
#ifdef symbol // ...
#endif
// 写法2
// 这两种写法等价,意思是如果symbol已经被定义,就编译它们包含的内容#if !defined(symbol)// ...
#endif
// 写法1
#ifndef symbol// ...
#endif
// 写法2
// 与上面相反,如果symbol已经被定义,它们包含的内容就不编译// 4. 嵌套指令
#if defined(HELLO)#ifdef HEHEprintf("HEHE\n");#endif#ifdef HAHAprintf("HAHA\n");#elif HEIHEIprintf("HEIHEI\n");#elseprintf("HELLO\n");#endif
#elif WORLDprintf("WORLD\n");
#endif
// 嵌套指令和嵌套使用条件语句一样	

6. 文件包含

我们已经知道,#include指令可以使另一个文件也被编译进程序。就像直接将它的内容替换到这个地方一样;

这种替换非常简单:
预处理器会先删除这条指令,将用被包含文件的内容来替换;
如果这个文件被包含了10次,那么它的内容就被包含了10次;

因为这个性质在包含头文件时就会产生问题;
如果一个头文件在多个文件中都被包含,那么这个头文件的内容就被重复包含了多次,这使得程序代码变得冗余臃肿;

比如:

此时有4个源文件,common.c, test1.c, test2.c, test.c
它们都有对应的头文件,common.h, test1.h, test2.h, test.h
test1.h 包含了common.h, test2.h 也包含了common.h, test.h 包含了test1.h和test2.h

在这里插入图片描述
可以看到test.h中包含了两次的common.h的内容;

为了解决这种问题就可以使用条件编译语句:

// test1.h
#ifndef COMMON
#define COMMON
#include "common.h"
#endif// test2.h
#ifndef COMMON
#define COMMON
#include "common.h"
#endif// test.h
#ifndef TEST1
#define TEST1
#include "test1.h"
#endif
#ifndef TEST2
#define TEST2
#include "test1.h"
#endif

上面的条件编译语句只有在目标头文件没有被编译到文件中时才会参与编译,在编译之后就设置一个标志,使得之后不会再重复包含这个文件;

或者是使用

#pragma once // 文件内容只会被包含一次

在上面的代码中,包含头文件时,使用的是""并不是之前使用过的<>;这两者有什么区别呢?

  • 本地文件包含:#include "name"
    查找策略:先在源文件所在目录下查找,如果找不到,编译器就会在标准位置(库函数头文件所在位置)查找;此时再找不到就编译错误;

linux系统中的标准位置:/usr/include
VS环境下的标准位置:C:\Program Files (x86)\Windows Kits\10\Include 这个地址以自己的机器为准

  • 库文件包含:#include <name>
    查找头文件时,直接到标准位置查找,找不到就编译错误;

可能有的同学会有疑问,那么包含库函数头文件时,是否可以使用#include "name"来包含呢?
可以,但是不推荐,因为使用这种方法,会先去当前目录查找,这肯定是找不到的,浪费了系统资源,降低了查找效率;


总结

这是c语言学习的最后一个部分了,此章节介绍了一个c语言程序从源文件变成可执行文件的过程,还详解了c语言中使用的预处理指令;以及一些宏的使用方法;
希望这系列文章对大家的c语言学习有帮助;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Feign 深度解析:Java 声明式 HTTP 客户端的终极指南

Feign 深度解析&#xff1a;Java 声明式 HTTP 客户端的终极指南 Feign 是由 Netflix 开源的 ​声明式 HTTP 客户端&#xff0c;后成为 Spring Cloud 生态的核心组件&#xff08;现由 OpenFeign 维护&#xff09;。它通过注解和接口定义简化了服务间 RESTful 通信&#xff0c;并…

如何Ubuntu 22.04.5 LTS 64 位 操作系统部署运行SLAM3! 详细流程

以下是在本地部署运行 ORB-SLAM3 的详细步骤&#xff0c;基于官方 README.md 和最佳实践整理&#xff0c;适用于 Ubuntu 16.04/18.04/20.04/22.04 系统&#xff1a; 一、系统要求与依赖项安装 1. 基础系统要求 操作系统&#xff1a;Ubuntu 16.04/18.04/20.04/22.04&#xff…

USB 共享神器 VirtualHere 局域网内远程使用打印机与扫描仪

本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 很久之前,有分享过使用 CUPS 和路由器来实现局域网内共享打印机,但由于 SANE 支持的打印机较少以及扫描驱动的缺失,试了很多种方法都没有办法成功远程使用打印机的扫描功能。 后面偶然发现 VirtualHere 可以曲线…

一洽智能硬件行业解决方案探索与实践

一、智能硬件行业发展现状剖析 在数字化浪潮推动下&#xff0c;智能硬件行业呈现蓬勃发展态势。软硬件一体化的深度融合&#xff0c;构建起智能化服务的核心架构&#xff0c;而移动应用作为连接用户与设备的重要桥梁&#xff0c;其作用愈发关键。深入研究该行业&#xff0c;可…

【C++ 类和数据抽象】构造函数

目录 一、构造函数的基本概念 1.1 构造函数核心特性 1.2 构造函数的作用 1.3 构造函数类型体系 二、构造函数的类型 2.1 默认构造函数 2.2 带参数的构造函数 2.3 拷贝构造函数 2.4 移动构造函数&#xff08;C11 及以后&#xff09; 三、初始化关键技术 3.1 成员初始…

图数据库nebula测试指南

概述 Nebula是一个开源的分布式图数据库系统&#xff0c;专为处理超大规模关联数据而设计。可以将复杂的关联关系存在nebula图数据库中&#xff0c;提供可视化平台用于案件关联查询及调查。测试的前提是了解nebula图数据库&#xff0c;会使用基本的插入语句和查询语句&#xf…

dispaly: inline-flex 和 display: flex 的区别

display: inline-flex 和 display: flex 都是 CSS 中用于创建弹性盒子布局&#xff08;Flexbox&#xff09;的属性值&#xff0c;但它们之间有一些关键的区别&#xff0c;主要体现在元素如何在页面上被渲染和它们对周围元素的影响。 主要区别 1&#xff0c;块级 vs 行内块级 d…

Sqlserver安全篇之_Sqlcmd命令使用windows域账号认证sqlserver遇到问题如何处理的案例

sqlcmd https://learn.microsoft.com/zh-cn/sql/tools/sqlcmd/sqlcmd-connect-database-engine?viewsql-server-ver16 sqlcmd -S指定的数据库连接字符串必须有对应的有效的SPN信息&#xff0c;否则会报错SSPI Provider: Server not found in Kerberos database. 正常连接 1、…

电脑硬盘常见的几种接口类型

一、传统接口&#xff08;机械硬盘为主&#xff09; 1. SATA 接口&#xff08;Serial ATA&#xff09; 特点&#xff1a; 最主流的机械硬盘&#xff08;HDD&#xff09;接口&#xff0c;广泛用于台式机和笔记本电脑。传输速度较慢&#xff0c;理论最大带宽为 6 Gbps&#xff…

【前端HTML生成二维码——MQ】

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 前端HTML生成二维码——MQ 前言本文将介绍前端HTML如何更具用户输入的内容生成对应的二维码,附页面代码、实现函数、js脚本。一、自定义显示页面1、效果图二、使用步骤1、引入库2、实现函数3、页面及函数代…

研发效率破局之道阅读总结(3)工程优化

研发效率破局之道阅读总结(3)工程优化 Author: Once Day Date: 2025年4月22日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: 程序的艺术_Once-Day…

C# MP3 伴奏

使用建议&#xff1a; 参数调节指南&#xff1a; 低频人声残留&#xff1a;降低CenterFrequency(800-1500Hz) 高频人声残留&#xff1a;提高CenterFrequency(2500-3500Hz) 消除力度不足&#xff1a;提高EliminationStrength(0.9-1.0) 伴奏失真&#xff1a;降低EliminationSt…

大模型面经 | 春招、秋招算法面试常考八股文附答案(四)

大家好,我是皮先生!! 今天给大家分享一些关于大模型面试常见的面试题,希望对大家的面试有所帮助。 往期回顾: 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题一) 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题二) 大模型面经 | 春招、秋招算法…

Springboot 集成 RBAC 模型实战指南

RBAC 模型核心原理 详情可参考之前的笔记&#xff1a;https://blog.csdn.net/qq_35201802/article/details/146036789?spm1011.2415.3001.5331 RBAC 定义与优势 RBAC&#xff08;Role-Based Access Control&#xff0c;基于角色的访问控制&#xff09;** 是一种通过角色关联…

如何收集用户白屏/长时间无响应/接口超时问题

想象一下这样的场景:一位用户在午休时间打开某电商应用,准备购买一件心仪已久的商品。然而,页面加载了数秒后依然是一片空白,或者点击“加入购物车”按钮后没有任何反馈,甚至在结算时接口超时导致订单失败。用户的耐心被迅速消耗殆尽,关闭应用,转而选择了竞争对手的产品…

用户需求报告、系统需求规格说明书、软件需求规格说明的对比分析

用户需求报告、系统需求规格说明书&#xff08;SyRS&#xff09;和软件需求规格说明书&#xff08;SRS&#xff09;是需求工程中的关键文档&#xff0c;分别对应不同层次和视角的需求描述。以下是它们的核心区别对比&#xff1a; ​​1. 用户需求报告&#xff08;User Requirem…

iostat指令介绍

文章目录 1. 功能介绍2. 语法介绍3. 应用场景4. 示例分析 1. 功能介绍 iostat (input/output statistics)&#xff0c;是 Linux/Unix 系统中用于监控 CPU 使用率和 磁盘 I/O 性能的核心工具&#xff0c;可实时展示设备负载、吞吐量、队列状态等关键指标。 可以使用 man iostat查…

神经网络 “疑难杂症” 破解指南:梯度消失与爆炸全攻略(六)

引言 在神经网络的发展历程中&#xff0c;梯度消失和梯度爆炸如同两座难以翻越的大山&#xff0c;阻碍着深层神经网络发挥其强大的潜力。尤其是在处理复杂任务时&#xff0c;这两个问题可能导致模型训练陷入困境&#xff0c;无法达到预期的效果。本文将深入探讨梯度消失和梯度…

“多模态SCA+DevSecOps+SBOM风险情报预警 “数字供应链安全最佳管理体系!悬镜安全如何用AI守护万亿数字中国?

夜深人静&#xff0c;程序员青丝盯着屏幕上自动生成的代码模块陷入沉思。在AI大模型的加持下&#xff0c;仅用一周团队就完成了原本需要半年的开发进度&#xff0c;但代码审查时却发现了不少高危漏洞。“生成效率提升了&#xff0c;但安全漏洞像定时炸弹一样藏在代码里”&#…

Node.js简介(nvm使用)

Node.js是一个基于Chrome V8 JavaScript引擎构建的开源、跨平台JavaScript运行环境。它允许开发者在服务器端运行JavaScript代码&#xff0c;从而实现前后端统一的开发语言。Node.js具有事件驱动、非阻塞I/O模型&#xff0c;使其非常适合于构建高性能的网络应用。 Node.js不是…