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

Vivado IP核与约束文件管理指南:解决OOC警告、COE文件丢失与Block Design复用 本文详细介绍了Vivado IP核与约束文件管理的系统化方法,包括解决OOC警告、COE文件丢失与Block Design复用等常见问题。通过IP核锁定机制、分层约束架构和动态约束技术,提升FPGA工程的健壮性和团队协作效率,特别适合中大型项目开发。 阅读详情

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

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



前言

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

  • 程序的翻译环境
  • 程序的执行环境
  • 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 -E、gcc -test.c -S、gcc -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);
	// 代码1
	switch (input)
	{
	case 1:
	CASE 2:
	CASE 3:
	}
	// 代码2
	switch (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语句只能对应一条语句,但是这里有了两条语句,产生了语法错误
	else
		max = 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 * x

int 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##Y

int main()
{
	// 达文西现在除了要你命3000之外,还有了许多其他的要你命系列的武器
	// 现在需要根据提供的型号,产生对应的字符串

	printf("%s\n", CAT(STR, "4000"));

	// 譬如此时有一个变量名就是Annihilator3000
	int 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 100

int main()
{
	int a = M;
#undef M
	printf("%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");
#endif

	return 0;
}

常见的条件编译指令:

// 1. 
#if 常量表达式
	// ....
#endif
// 常量表达式为真时,被包括的内容参与编译;为假则反之
// 常量表达式由预处理器求值

// 如:
#define __DEBUG__ 1
#if __DEBUG
	printf("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 HEHE
		printf("HEHE\n");
	#endif
	#ifdef HAHA
		printf("HAHA\n");
	#elif HEIHEI
		printf("HEIHEI\n");
	#else
		printf("HELLO\n");
	#endif
#elif WORLD
	printf("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语言学习有帮助;

PCB板Gerber文件的理解和制作 Gerber文件是PCB生产的关键文件,主要包括:1)走线层(GTL/GBL);2)阻焊层(GTS/GBS);3)丝印层(GTO/GBO);4)板框层(GMx/GKO);5)钢网层(Gtp/GBp);6)钻孔文件(NCdrillFiles)。其中1-10项和15项钻孔文件必须提供,建议包含11-14项辅助钻孔层。Gerber文件不含过孔属性,需提前在AD中设置过孔盖油。导出步骤包括:导出非机械层、机械层和钻孔层,新版AD可通过"文件->制造输出->GerberFiles"操作 阅读详情

相关推荐

Understanding Lockup Cells

工具会分析扫描链和EDT逻辑之间的控制时序元素的时钟的时序关系,当必须要同步时钟并保持数据完整性时插入边沿触发寄存器(lockup cells)。可以使用report_edt_lockup_cells命令来展示工具已经插入的lockup cells的详细报告。

weixin_44954910的博客 1080

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

字符指针数组指针指针数组数组传参和指针传参几种数组、指针之间的比较函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组的笔试题指针是一个存储地址的变量,地址是一块内存空间的唯一标识;指针的大小是固定的(4字节或8字节);指针是有类型的,它的类型决定它 + - 整数时移动的步长、*解引用操作时的权限;指针是能够进行运算的,+ - 整数、指针相减、指针的关系运算;接下来,就让我们继续探讨指针的进阶主题。对于普通的指针,我们都已经很熟悉了。

PLKDTHFS的博客 1518

逆向实战:如何绕过iTunes 12.6.5.3的反调试机制(附X64dbg插件配置)

本文深入剖析了iTunes 12.6.5.3的反调试机制与登录协议。通过X64dbg动态调试与IDA静态分析,详细演示了如何绕过其反调试检测,并定位到核心的登录协议加密函数,揭示了其请求签名(X-Apple-ActionSignature)的生成逻辑与依赖关系,为客户端安全研究与协议分析提供了实战思路。

weixin_29202213的博客 742

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第一式】数据的存储

本章节的重点是数据类型详细介绍整型在内存中的的存储:原码、反码、补码大小端字节序介绍及判断浮点型在内存中的存储解析char // 字符数据类型short // 短整型int // 整型long // 长整型long long // 更长的整型float // 单精度浮点数double // 双精度浮点数// c语言有没有字符串类型?// 没有以及他们所占存储空间的大小。使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间的视角什么是大端小端?

PLKDTHFS的博客 1487

告别手动点鼠标:用Python脚本批量跑ADS版图仿真,效率提升10倍(附完整代码)

本文详细介绍了如何利用Python脚本自动化ADS版图仿真流程,显著提升射频工程师的工作效率。通过调用ADS底层接口,实现参数化修改、批量任务调度和结果自动收集,解决了手动操作中的常见问题,如仿真不更新等。附完整代码,帮助开发者快速实现10倍效率提升。

weixin_42523543的博客 331

从零训练Qwen3-VL

如果你未来想做的是「在公开权重上继续做大规模多模态 pretrain(比如换数据、换目标函数)」而不是在完全随机初始化上起步,这两类代码会比 Open-Qwen2VL 更直接。参考 Qwen2-VL / Qwen2.5-VL 披露的流程,可以大致推测 Qwen3-VL 也是类似的三阶段:(直接在学术算力(8×A100-40G)上从零预训练了一个 2B 参数的多模态 Qwen2-VL 风格模型:(的配置和训练脚本,理解它是如何把「任意 ViT + 任意 LLM + 任意图文数据」拼在一起的。

u013250861的博客 563

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作

为什么使用文件什么是文件文件的打开和关闭文件的顺序读写文件的随机读写文本文件和二进制文件文件读取结束的判定文件缓冲区磁盘上的文件就是文件。但是在程序设计上,我们谈到的文件一般有两种:程序文件、数据文件;本节详细的对c语言中数据文件的使用进行了详细的介绍,包括对文件读写操作使用的函数,以及通过文件位置指针的操作函数来对文件进行随机读写,并指出了feof函数可能出现的使用错误,在最后,介绍了文件缓冲区的概念;

PLKDTHFS的博客 1226

【vsomeip3 教程】基于 vSomeIP 3.5 的路由域实践:从配置到运行状态的完整指南

___ > 前言(读者定位与准备) 目标读者:在 Linux 上做分布式/车载中间件通信的 C++ 工程师。 > 先决条件:CMake、Boost 基础;理解 TCP/UDP 与多进程通信。 > 文中术语:Routing Host(路由进程)、Application、Service/Instance、Method、Event/EventGroup、SOME/IP-SD。 > 演示环境:单机多进程 & 跨主机两套对照。

探索C++编程的奥秘,分享深入的技术见解和实践,旨在激发读者创造力与解决问题的思维。 1159

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

大家在使用数组时肯定都会有过这样的疑问,如果一个数组的长度不足了,可以延长这个数组吗?使其能够满足存储的需求。又或者是现在不确定数组应该设为多长,先把它放着,等之后再设置它的长度。上面的这些需求,数组显然是无法实现的,那么c语言中有什么东西能够做到这一点呢?动态内存分配!你现在不知道需要多大的空间,没关系,之后你明确了的时候,使用动态内存分配,将你要的空间分配给即可。为什么存在动态内存分配动态内存函数的介绍mallocfreecallocrealloc常见的动态内存错误。

PLKDTHFS的博客 1293

31、Labview如何搭建程序框架?--状态机到数据采集等常用框架

本文废话不多,直接列举几种框架搭建方式,为了尽量向大家提供干货,本文直接从简介和手把手教搭建框架两方面来讲解,本文提到五种经典框架模式,学会这五种就很够了,分别是:状态机框架(State Machine)、生产者-消费者框架(Producer-Consumer)、主-从框架(Master-Slave)、事件驱动框架(Event-Driven)、分层架构(Layered Architecturep)。这个框架是基于状态机结构的。顾名思义“状态机”就是个“切换程序状态的机器”。本质是一个。

关注我,更多技术问题欢迎私聊~ 3101

基于Simulink实现电动汽车制动能量回收系统的仿真

通过上述步骤,我们成功实现了基于Simulink的电动汽车制动能量回收系统的仿真研究。该系统能够全面评估制动能量回收系统的性能,验证控制策略的有效性,并通过优化设计提高能量回收效率和制动安全性。引入智能预测:结合人工智能技术,实现更智能的能量回收控制策略。扩展功能:增加对多种车型和制动场景的支持,提升系统通用性。实验验证:将仿真模型应用于实际硬件,进行实验验证,评估其在实际工况下的表现。

xiaoheshang_123的博客 336

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第三式】字符函数和字符串函数

c语言中对字符和字符串的处理非常频繁,但c语言本身是没有字符串类型的,字符串通常存储在常量字符串或字符数组中。常量字符串适用于不需要对其进行修改的字符串函数。strlen函数的作用是获得一个字符串的长度;接收一个const修饰的字符指针变量,这个指针指向的字符串必须有结束符\0;返回一个无符号(易错点)的int类型的数值,表示这个字符串的长度(不包括结束符\0如果没有返回值,则表示出错;// 使用示例int main()printf("a的长度 = %d\n", strlen(a));

PLKDTHFS的博客 926

财务数字化——解读美的战略规划之财务组织体系建设【附全文阅读】

本文提出适用于集团企业的财务管控方案,面向财务负责人、HR及战略规划人员。方案整合四种管控模式,结合500强案例,构建"职能定位-架构设计-权责划分-人才发展"体系,涵盖财务五大模块。通过明确总部与事业部权责边界,提供组织架构等实操框架,旨在平衡财务管控与业务支持,提升效率与风控能力,满足集团财务体系规范化建设需求。

cdfunlove的博客 663

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)

c语言中虽然定义得有许多的类型,如char、int、double等等,但是现实生活中需要描述的问题,仅仅使用这些已有类型是不够的,所以在c语言中,程序员可以根据自己的需要,定义出自己的类型结构,以满足需求。结构体:结构体类型的声明结构的自引用结构体变量的定义和初始化结构体内存对齐结构体传参结构体实现位段(位段的填充&可移植性)枚举枚举类型的定义枚举的优点枚举的使用联合联合类型的定义联合的特点联合的使用有了结构体类型,之后应该如何定义变量呢?

PLKDTHFS的博客 989

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

C语言修炼秘籍【第零章】c语言概述

c语言是一门编程语言。语言的作用就是交流,人们可以利用它与计算机进行交流,让计算机知道你需要它完成什么工作。本系列文章是对笔者在c语言学习的总结记录。本文会先粗略的介绍c语言中需要学习的内容。

PLKDTHFS的博客 1664

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

c语言修炼秘籍【第二章】函数

本文会从函数是什么开始,逐一介绍函数的语法结构,如何声明和定义一个函数,函数的参数类型,调用函数时的传参类型,函数的调用方式,函数递归,并在最后对汉诺塔、青蛙跳台阶和八皇后问题进行详细分析(包含全部代码)

PLKDTHFS的博客 1442

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

c语言修炼秘籍【第七章】const与c语言中一些错误代码

而数组内部的内存使用分配是从低到高的,arr[0]的地址在最低位,当循环进行到第11次 - - arr[11]设置的其实是i的值,此时将 i 的值设为了0,循环继续。这是因为,程序要进行报错也是需要执行时间的,访问arr[11]时程序仍处于循环中,没有时间报错就又进入了下一次循环。test1()可以正常使用指针对n为进行修改,也可以对p指向的位置进行修改。test2()不能通过指针对n进行修改,可以对p指向的位置进行修改。,arr后分配,地址会比i的地址低,分配到的地址为。,i先分配空间,地址为。

PLKDTHFS的博客 674

编程领域这些禁术相当精彩,掌握其一,方可修炼编程大法!

之前在一个网站看到这样的一个话题,原来编程领域还有这么多禁术。有人说这些禁术如果用得好,那么牛逼哄哄。如果用不好,那么遗憾无穷! 造轮子术 在面对一个需求时觉得有的步骤可以写一个工具方便自己更好地完成该需求,从而暂时丢弃当前需求去实现自动化工具,在实现工具的时候就会发现有更多可以自动化的东西,然后开始造底层库的轮子。 二分调试法 在无法找到问题时,尝试把相关代码删除一半并编译运行,如问题依然存在,则再删除一半代码编译运行。 洁癖重构术 把同事所有代码中的大括号改为写在下一行,并...

HUYA69的博客 319

c语言修炼秘籍【第五章】指针

本文会从介绍指针是什么开始,循序渐进地介绍指针的使用、指针使用时会遇到的问题、指针能进行哪些运算、指针与数组之间的关系,以及指针数组这些内容。本文对c语言中使用的指针进行了较简略的介绍,包括什么是指针,指针类型有什么用,指针的一些运算,什么是野指针,如何利用指针访问数组,二级指针使用中会出现的问题,以及指针数组。

PLKDTHFS的博客 1763

c语言修炼秘籍【第三章】数组

本文将会介绍常用到的一维数组和二维数组是如何使用及它们在内存中是如何存储,说明何为数组越界,并讨论数组在作为函数参数时应该如何使用。在最后附带三子棋和扫雷的代码实现详解。本文对一维和二维数组的初始化、创建、使用,作出的简要的介绍,并通过冒泡排序算法的实现,详细说明了当数组作为函数参数时的性质 - - 传递的是首元素地址。在最后,给出了两个数组的使用实例,附带了完整的实现代码,并配有详细注释。

PLKDTHFS的博客 1257

c语言修炼秘籍【第四章】操作符

本文会对c语言中使用到的操作符进行介绍,包括算术操作符移位操作符位操作符赋值操作符单目操作符关系操作符逻辑操作符条件操作符逗号表达式下标引用、函数调用和结构成员表达式求值单目操作符的种类!// 逻辑反操作- // 负值+ // 正值& // 取地址sizeof // 操作数的类型长度(以字节为单位)~ // 按二进制位取反++ // 前置、后置++-- // 前置、后置--* // 间接访问操作符(解引用操作符) -- 指针的使用。

PLKDTHFS的博客 1136

c语言修炼秘籍【第一章】分支与循环语句

在处理现实中的问题时,我们总会遇到需要作出选择或是需要重复多次的完成一种事件。你去图书馆学习,需要日复一日的坚持,每多学一天,你的代码功力就能+1,当你的代码功力,达到了某一程度,你就能够迎娶白富美,走上人生巅峰。为了实现这些目的,c语言提供了分支语句,用以实现选择;提供循环语句,用以实现重复多次的机械活动;表达式语句函数调用语句控制语句复合语句空语句分支、循环语句属于控制语句。控制语句用于控制程序的执行流程,以实现程序的各种结构方式,它们由各种特定的语句定义符构成;

PLKDTHFS的博客 1244

C语言修炼秘籍之——函数递归

函数递归是什么玩意儿?有啥用啊?来个例题看看啊?和迭代之间有啥区别啊? 这位客官,咱里边儿请~坐下来听我娓娓道来~

2301_80029060的博客 1534

什么是 C 语言中的条件编译?

宏定义可以在条件编译中用于定义常量、控制表达式的值或者作为标志。10# endif# endif5#endif条件编译是 C 语言中强大的预处理特性,它使程序员能够根据各种条件灵活地控制代码的编译和生成。通过合理地运用条件编译,可以提高代码的可移植性、可维护性和效率。但同时也要注意避免过度使用导致代码的复杂性增加。在实际编程中,根据具体的需求和项目特点,恰当地使用条件编译能够更好地优化代码结构和适应不同的运行环境。🎉相关推荐📙C 语言百万年薪修炼课程。

发现生活,分享智慧,一起成长! 2169

c语言修炼秘籍【第六章】结构体

将结构体与数组进行对比:数组是一组相同类型的元素集合;结构体是一些值的集合,它们类型可以不同,这些值被称为成员变量。使用结构体类型定义变量int x;int y;} p1;// 声明类型的同时定义变量p1// 定义结构体变量 p2// 初始化:定义变量的同时赋初值struct Stu// 名字int age;// 年龄int data;// 结构体嵌套的初始化// 结构体嵌套的初始化本文对c语言中使用到的结构体进行了简单介绍,更详细的会在后续攻略中详解。

PLKDTHFS的博客 761

C语言修炼秘籍之——动态内存分配

客官可是在寻找动态内存分配秘方?快快请进,且听我娓娓道来~

2301_80029060的博客 1240
上一篇: c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作
下一篇: C++学习-入门到精通-【0】计算机和C++简介
YKPG
博客等级 码龄6年 344粉丝 37原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值