【程序环境和程序预处理】万字详文,忘记了,看这篇就对了(2)

简介: 1.程序翻译环境和运行环境假设一个test.c文件经过编译器编译运行后生成可执行文件test.exe,这中间存在两个过程:一个是翻译,在这个环境中源代码被转换为可执行的机器指令。一个是运行,它用于实际执行代码。在翻译环境阶段,会进行编译和链接操作。在汇编阶段,是将汇编指令转换成二进制指令。

2.2.5 带副作用的宏参数

副作用就是后遗症的意思,

x+1;   不带副作用
x++;   带有副作用

举个例子:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
  x = 2;
  y = 5;
  printf("%d\n", MAX(a++, b++));
  printf("%d\n", a);
  printf("%d\n", b);
}

请问上面的这段代码输出结果是什么?

输出结果是:6,3,7

原因是,宏是被替换的,首先x是2,y是5,在使用宏时,参数是a++和b++,然后进行替换,替换结果是:

  printf("%d\n", ((a++) > (b++) ? (a++) : (b++)));

所以在比较时,先使用,后++,2和5比较完,再各自++,此时2小于5,执行b++,此时b已经是6了,先使用后++,所以打印第一个结果是6,对于a,a只++一次,结果是3,

最后再打印b出来时,b已经++完成了,所以b打印出来是7.

总结:当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

2.2.6 宏和函数对比

宏是经常被用来执行计算量较小的计算,如比较大小,那为什么不使用函数来比较呢?

1.我们知道,函数在调用的时候会有返回的开销,反观宏,则没有类似的问题。

以比较大小为例:

#define MAX(a,b) ((a)>(b)?(a):(b))
float MAX2(float c, float d)
{
  return c > d ? c : d;
}
int main()
{
  int a = 2;
  int b = 5;
  float c = 3.0f;
  float d = 4.0f;
  float max2 = MAX2(c, d);//函数调用
  printf("%f\n", max2);
  printf("%f\n", MAX(c, d));//宏调用
  return 0;
}

我们分别使用函数和使用宏来比较大小,

03eff10de87145939924c77dd17c45e8.png

我们调试起来,转到反汇编后,注意看,现在准备进入函数调用,在此之前是准备工作。

当我们调用该函数时,会发现这么一大堆东西,这些都是函数在调用时需要做的工作,以及返回值需要做的工作。

56f566fa1e5649b5b28fc576a59709d0.png

再来看宏的开销:

0cd9f49fed1240659135f0cb72313d7b.png

对比函数和宏调用的开销,会发现,仅仅是比较大小,函数的开销比宏的开销多出了很多。

所以宏比函数在程序的规模和速度方面更胜一筹。

2. 函数的参数必须声明为特定的类型,而宏是类型无关的。

以上面的例子为例:

#define MAX(a,b) ((a)>(b)?(a):(b))
float MAX2(float c, float d)
{
  return c > d ? c : d;
}
int MAX1(int a, int b)
{
  return a > b ? a : b;
}
int main()
{
  int a = 2;
  int b = 5;
  float c = 3.0f;
  float d = 4.0f;
  float max2 = MAX2(c, d);//函数调用
  printf("%f\n", max2);
  int max1 = MAX1(c, d);//函数调用
  printf("%d\n", max1);
  printf("%f\n", MAX(c, d));//宏调用
  printf("%d\n", MAX(a, b));//宏调用
  return 0;
}

分别使用函数和宏对整型和浮点型数据进行大小比较,此时两个没有任何问题,但是接下来,

当我们更改图中数据,用浮点型函数比较整型大小时,回出现警告,可能会丢失数据,

162b441389bd4be6ab5ebb16960fce3e.png

结果也不符合,


ed0b324340084425b1a78ab73a23899c.png

所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。

记住,宏是类型无关的。

宏的缺点:

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

#define TEST() printf("Test Successfully!\n")
int main()
{
  TEST();
  TEST();
  TEST();
    //等价于
  printf("Test Successfully!\n");
  printf("Test Successfully!\n");
  printf("Test Successfully!\n");
}

我们这样复制三份宏,替换后就已经出现代码较冗余的情况, 假如宏定义的代码有五十行,复制三份后就有一百五十行,情况更加严重。

2. 宏是没法调试的

f291d6d44e7343d68e09308c18a8ed7f.png

调试起来的时候,按下F11,并没有跳转到宏所在的地方,因为宏在预编译的时候就已经完成了替换。

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

宏是类型无关的,既是优点,也是缺点。

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

5.宏还可以做到传一个类型,然而函数做不到。

比如说,宏的参数可以是一个int,但是没有函数传参传一个int的说法,函数只能传一个int类型的值,但是绝对不会传一个int。

举个例子,好好体会一下。

#define MALLOC(num,type)  (type*)malloc(num*sizeof(type))
int main()
{
  int ret1 = (int*)malloc(10 * sizeof(int));
  int ret = MALLOC(10, int);
  //等价于
  printf("%d\n", ret1);
  printf("%d\n", ret);
  return 0;
}

2.2.7命名约定

一般函数的宏的使用语法很相似。

所以语言本身没法帮我们区分二者。

我们平时的一个习惯是:

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

2.3#undef的用法

#undef 是用来移除一个宏定义的

举个例子:

#define MAX 100
int main()
{
  printf("%d\n", MAX);
#undef MAX
  printf("%d\n", MAX);
  return 0;
}

20d9115f9d474e378ec8230a455b4fb6.png

可以发现,编译都无法编译成功,说明MAX已经被移除了。

2.4 命令行定义

通俗地讲,命令行定义就是再程序预编译的时候改变一些参数,使他们能够随时地发生变化。

比如说下面:

给一个数组赋值,ARRARY_SIZE代表数组的大小,数组的大小通过命令行定义,在预编译阶段是可以发生改变的。

#include <stdio.h>
int main()
{
  int array [ARRAY_SIZE];
  int i = 0;
  for(i = 0; i< ARRAY_SIZE; i ++)
 {
    array[i] = i;
 }
  for(i = 0; i< ARRAY_SIZE; i ++)
 {
    printf("%d " ,array[i]);
 }
  printf("\n" );
  return 0;
}

总结:命令行定义就是再程序预编译的时候改变一些参数,使他们能够随时地发生变化。

2.5 条件编译

条件编译,也就是有选择性地编译,把想要的留下。

比方说:

int main()
{
  int arr[10] = { 0 };
  for (int i = 0; i < 10; i++)
  {
#ifdef DEBUG
    arr[i] = i;
#endif
    printf("%d ", arr[i]);
  }
  return 0;
}

请问这段代码输出结果是什么?


结果输出10个0。


因为这里我们使用了条件编译,#ifdef DEBUG,意思就是如果定义有DEBUG,就使用下面的语句,结束编译语句是#endif,在这区间内,如果条件成立,则执行,不成立就不执行。


由于未定义有DEBUG,所以条件不成立,不执行赋值语句,当我们在前面定义DEBUG,就可以了

82324a9a4f314504bae05ebebab61655.png

在这里可以给DEBUG一个替换对象,也可以仅仅定义DEBUG。

常见的条件编译指令:

1.常量表达式

int main()
{
#if 1
  printf("hehe\n");
#endif
  return 0;
}

2.多分支的条件编译

int main()
{
#if 1==1
  printf("hehe\n");
#elif 2==1
  printf("haha\n");
#else
  printf("heihei\n");
#endif
  return 0;
}


3.判断是否被定义

#define DEBUG 0 //即使DEBUG被定义为0,为假,但是它已经被定义过了,就打印hehe
int main()
{
#if !defined(DEBUG)  // 只要定义过,不管定义什么,满足条件就参与编译
  printf("hehe\n");
#endif 
  return 0;
}

注意,只要被定义过,不管被定义成什么,都成立。

并且,define后面加了一个字母d,表示defined,定义过的意思。

还有一个是

#ifndef DEBUG //注意这里多了个n,表示no
  printf("hehe\n",);
#endif

4,嵌套定义

嵌套定义可以跟嵌套的条件判断类比,也就是 if 中还有 if 。

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

2.6文件包含

2.6.1 #include <> 和#include " "

我们知道,对于文件来说,假如我们需要打印东西,就需要引一个头文件,引#include

那假如我用 #include "stdio.h "

这样的写法呢?能否通过?

0f581b4269d74b73ba2c683f6ccf313f.png

仍然可以打印出来。

#include "filename"

的查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误。

用#include " "的方式包含文件,会先在源文件所在目录下查找,

98e24df3cdb7414e9dc52fd49e04a5b0.png

也就是这些文件里查找,如果找不到,才会去标准库函数里面查找。


7ab2e87d797e40bca87bc1de78e8f8c6.png

如上图.

而#include< >,则是直接在目录里面查找了。

所以,当我们有成千上万个源文件时,应该使用#include<>去查找。

2.6.2 嵌套文件包含

d2e708051c774568a88d345d9c00ae4a.png

如果出现了这样的情况,也就是一个多个文件中都包含了同一个头文件,这会重复调用头文件,造成代码冗余,也会造成文件的速度的减慢。

解决办法:

1.条件编译

每个头文件的开头写

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

意思就是,如果没有定义TEST.H这个头文件,那么就定义它,如果定义了,就不再次定义。

e4b2f41d661640b58d219d2fcb5fdc9a.png

比如说这个,上面的红色框框,是test.h文件的内容,下面红色框框是test.c文件内容,在test.c文件中包含test.h文件,然后进入test.h文件,执行#ifndef,如果自己没有被定义,那就定义,如果定义过了,那就不重复定义了。


直接写下面这句话就可以了。

#pragma once

更加推荐第二种写法。

这篇文章到这里就结束了!
如果对于有帮助,不妨点赞关注吧!

相关文章
|
JavaScript 前端开发
Vue系列教程(11)- 组件详解(Vue.component、props)
Vue系列教程(11)- 组件详解(Vue.component、props)
183 0
|
域名解析 缓存 网络协议
如何解决域名解析不生效问题?
文中对域名解析不生效的原因进行了分析,并针对最常见的本地递归域名服务器缓存不生效的问题提出了解决方案,尤其移动域名解析HTTPDNS对无线场景下的应用特别有效。
32790 0
|
分布式计算 Serverless 调度
EMR Serverless Spark:结合实时计算 Flink 基于 Paimon 实现流批一体
本文演示了使用实时计算 Flink 版和 Serverless Spark 产品快速构建 Paimon 数据湖分析的流程,包括数据入湖 OSS、交互式查询,以及离线Compact。Serverless Spark完全兼容Paimon,通过内置的DLF的元数据实现了和其余云产品如实时计算Flink版的元数据互通,形成了完整的流批一体的解决方案。同时支持灵活的作业运行方式和参数配置,能够满足实时分析、生产调度等多项需求。
61231 107
|
网络协议 前端开发 API
HTTP 和 TCP 协议的应用场景有哪些不同
【10月更文挑战第25天】HTTP(超文本传输协议)和 TCP(传输控制协议)处于网络协议栈的不同层次,各自具有独特的功能和特点,因此它们的应用场景也存在明显的差异。
|
安全 网络协议 关系型数据库
最好用的17个渗透测试工具
渗透测试是安全人员为防止恶意黑客利用系统漏洞而进行的操作。本文介绍了17款业内常用的渗透测试工具,涵盖网络发现、无线评估、Web应用测试、SQL注入等多个领域,包括Nmap、Aircrack-ng、Burp Suite、OWASP ZAP等,既有免费开源工具,也有付费专业软件,适用于不同需求的安全专家。
2346 2
|
C++ Windows
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
在Windows上使用Visual Studio 2022进行FFmpeg和SDL2集成开发,首先安装FFmpeg至E:\msys64\usr\local\ffmpeg,然后新建C++控制台项目。在项目属性中,添加FFmpeg和SDL2的头文件及库文件目录。接着配置链接器的附加依赖项,包括多个FFmpeg及SDL2的lib文件。在代码中引入FFmpeg的`av_log`函数输出"Hello World",编译并运行,若看到"Hello World",即表示集成成功。详细步骤可参考《FFmpeg开发实战:从零基础到短视频上线》。
844 0
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
|
编译器 Linux C语言
Windows下编译并使用64位GMP
Windows下编译并使用64位GMP
802 0
|
存储 Oracle 关系型数据库
Flink CDC在处理数据时,会将字段名转换为小写
【2月更文挑战第15天】Flink CDC在处理数据时,会将字段名转换为小写
372 3
|
缓存 JavaScript 前端开发
10个常见的使用场景,助你从 Vue2 丝滑过渡到 Vue3 !
10个常见的使用场景,助你从 Vue2 丝滑过渡到 Vue3 !
484 1