程序环境和预处理(1)

简介: 程序环境和预处理(1)


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

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

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

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

计算机是能够执行二进制指令的,但是我们写出的C语言代码是文本信息,计算机不能直接理解。

翻译环境:C语言代码 —> 二进制的指令(放在可执行程序中)

执行环境:执行二进制的代码

2. 详解编译+链接

2.1 翻译环境

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

我们也可以通过代码来观察:

//add.c
int Add(int x, int y)
{
  return x + y;
}
//test.c
#include <stdio.h>
extern int Add(int, int);
int main()
{
  int a = 10;
  int b = 20;
  int c = Add(a, b);
  printf("%d\n", c);
  return 0;
}

2.2 编译本身也分为几个阶段

  1. 预处理阶段
  2. 编译阶段
  3. 汇编阶段

解释一下符号汇总、符号表:

在这个目标文件里,确实能看到一些符号(都是全局的


接下来,我们用两个文件来举例子:

这有什么用呢?

如果没有定义Add函数,那么在链接的时候就定位不到这个函数,就会发生链接错误,生成不了可执行程序。

2.3 运行环境

程序执行的过程:

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

3. 预处理详解

3.1 预定义符号

__FILE__ --> 进行编译的源文件

__LINE__ --> 文件当前的行号

__DATE__ --> 文件被编译的日期

__TIME__ --> 文件被编译的时间

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

这些预定义符号都是语言内置的。

举个例子:

#include <stdio.h>
int main()
{
  printf("%s\n", __FILE__);
  printf("%d\n", __LINE__);
  printf("%s\n", __DATE__);
  printf("%s\n", __TIME__);
  //printf("%d\n", __STDC__);//当前VS是不支持ANSI C
  return 0;
}

对代码进行预处理之后:

3.2 #define

3.2.1 #define 定义标识符

语法:

#define name stuff

#include <stdio.h>
#define M 100
#define STR "abc"
#define FOR for(;;)
#define reg register//为 register这个关键字,创建一个简短的名字
int main()
{
  printf("%d\n", M);
  printf("%s\n", STR);
  FOR;//死循环
  return 0;
}

预处理之后:


int main()
{
  int d = 0;
  switch (d)
  {
  case 1:
    break;
  case 2:
    break;
  case 3:
    break;
  }
  return 0;
}

以上代码还可以这样写:

#define CASE break;case
int main()
{
  int d = 0;
  switch (d)
  {
  case 1:
  CASE 2:
  CASE 3:
  }
  return 0;
}

#include <stdio.h>
//如果定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\t\
          time:%s\n" , __FILE__, __LINE__,\
          __DATE__, __TIME__)
int main()
{
  DEBUG_PRINT;
  return 0;
}

提问:

在define定义标识符的时候,要不要在最后加上 ;

比如:

#define M 100;
int main()
{
  int a = M;
  return 0;
}

这样的代码看上去是没有问题的,但是这样写是非常容易出错的:

#define M 100;
int main()
{
  int a = 0;
  int b = 0;
  if (a > 5)
    b = M;
  else
    b = -1;
  return 0;
}

if 语句后面默认只能跟一条语句,这里再加上一个 ; 就变成了两条语句,这就意味着下面的 else 不知道和谁匹配了。

因此,建议不要加上 ; ,这样容易导致问题。

3.2.2 #define 定义宏

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

下面是宏的申明方式:

#define name( parament-list ) stuff

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

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

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

预处理之后:

提示:

所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间操作符的优先级而导致不可预料的相互作用。

应该这样写:

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

应该这样写:

#include<stdio.h>
#define DOUBLE(x) ((x) + (x))
int main()
{
  int a = 3;
  int r = 10 * DOUBLE(a);
  printf("r=%d\n", r);
  return 0;
}
3.2.3 #define 替换规则

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

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

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#include<stdio.h>
#define M 3
int main()
{
  printf("M=%d\n", M);//M=3
  return 0;
}
3.2.4 #和##

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

首先我们看看这样的代码:

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

我们发现字符串是有自动连接的特点的。


我们想实现这样一个功能:

#include <stdio.h>
int main()
{
  int a = 20;
  printf("the value of a is %d\n", a);
  
  int b = 15;
  printf("the value of b is %d\n", b);
  float f = 4.5f;
  printf("the value of f is %f\n", f);
  return 0;
}

我们可以用宏来实现(函数做不到):

#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "format"\n", n)
int main()
{
  int a = 20;
  //printf("the value of a is %d\n", a);
  PRINT(a, "%d");
  int b = 15;
  //printf("the value of b is %d\n", b);
  PRINT(b,"%d");
  float f = 4.5f;
  //printf("the value of f is %f\n", f);
  PRINT(f, "%f");
  return 0;
}

代码中的 #n 会预处理为 “n”


##的作用:

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

#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
  int Class110 = 2024;
  printf("%d\n", CAT(Class, 110));//2024
  printf("%d\n", Class110);//2024
  return 0;
}

3.2.5 带副作用的宏参数

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

int main()
{
  int a = 10;
  //int b = a + 1;//b=11, a=10
  int b = ++a;//b=11, a=11
  return 0;
}

x+1;//不带副作用

x++;//带有副作用

MAX宏可以证明具有副作用的参数所引起的问题:

#include <stdio.h>
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
  int a = 5;
  int b = 6;
  int c = MAX(a++, b++);
  //int c = ((a++) > (b++) ? (a++) : (b++));
  //        5       6               7
  //c=7
  //b=8
  //a=6
  printf("c = %d\n", c);
  printf("a = %d\n", a);
  printf("b = %d\n", b);
  return 0;
}
3.2.6 宏和函数对比
#include <stdio.h>
//1
#define MAX(x, y) ((x)>(y)?(x):(y))
//2
int Max(int x, int y)
{
  return (x > y ? x : y);
}
int main()
{
  int a = 5;
  int b = 6;
  int c = MAX(a, b);
  //int c = Max(a, b);
  printf("c = %d\n", c);
  printf("a = %d\n", a);
  printf("b = %d\n", b);
  return 0;
}

通常被应用于执行简单的运算:比如在两个数中找出较大的一个。

那为什么不用函数来完成这个任务?

原因有二:

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

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

宏的缺点:

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

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。(代码如果特别长,那么编译的压力就会很大,因为在编译的时候会对代码做各种各样的处理,如:语法分析、词法分析等等)
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

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

#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
  int* p = (int*)malloc(126 * sizeof(int));
  //malloc(126, int);//err
  int* p = MALLOC(126, int);
  //int* p = (int*)malloc(126 * sizeof(int));
  return 0;
}

宏和函数的一个对比

对于第三点的一个例子:

//1
#define MAX(x, y) ((x)>(y)?(x):(y))
//2
int Max(int x, int y)
{         //5      6
  return (x > y ? x : y);
}
int main()
{
  int c = MAX(2 + 3, 6);
  //int c = ((2+3)>(6)?(2+3):(6))
  c = Max(2 + 3, 6);
  return 0;
}

补充:

宏有自己的优势,当然也有劣势

函数也有自己的优势,也有劣势

能不能有一个函数既具有函数的好,也具有宏的好呢?

inline — 内联

内联函数


目录
相关文章
|
机器学习/深度学习 算法 数据可视化
浅析特征数据离散化的几种方法(上)
什么是离散化? 离散化就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:
|
关系型数据库 MySQL 数据库
【Navicat 连接MySQL时出现错误1251:客户端不支持服务器请求的身份验证协议;请考虑升级MySQL客户端】
【Navicat 连接MySQL时出现错误1251:客户端不支持服务器请求的身份验证协议;请考虑升级MySQL客户端】
1491 0
|
5月前
|
存储 供应链 监控
供应链复杂、工厂分散,半导体行业如何安全访问总部ERP系统?
电子元器件与半导体行业面临供应链复杂、生产计划多变等挑战,智能化ERP系统成为提升效率的关键。然而,数据安全至关重要,许多企业选择本地部署并结合内网穿透技术实现远程访问。以神州讯盟ERP为例,搭配贝锐花生壳,无需公网IP即可安全接入总部系统。花生壳采用多重加密与权限控制,保障数据传输安全,同时支持高速跨地区访问,仅需三步即可完成配置,满足多地协同办公需求,助力企业高效管理。
137 0
|
XML Java Maven
【Maven技术专题】「实战开发系列」盘点Maven项目中打包需要注意到的那点事儿
【Maven技术专题】「实战开发系列」盘点Maven项目中打包需要注意到的那点事儿
302 1
|
10月前
|
存储 监控 安全
前端框架的数据驱动方式如何保证数据的安全性?
总之,前端框架的数据驱动方式需要综合运用多种手段来保证数据的安全性。从传输、存储、访问控制到防范攻击等各个方面进行全面考虑和实施,以确保用户数据的安全可靠。同时,不断加强安全管理和技术创新,以应对不断变化的安全挑战。
350 60
|
存储 网络协议 容灾
降低存储网络55% 延迟!阿里云存储论文入选计算机顶会
凭借在规模化部署和应用模型上的创新,阿里云存储团队发表的技术论文《Deploying User-space TCP at Cloud Scale with LUNA》被 USENIX ATC'23 收录。
1533 4
降低存储网络55% 延迟!阿里云存储论文入选计算机顶会
|
弹性计算 负载均衡 网络协议
云计算中的弹性伸缩与负载均衡技术解析
【7月更文挑战第4天】弹性伸缩与负载均衡作为云计算平台中的两大关键技术,对于构建高可用、可扩展的应用系统具有重要意义。通过合理利用这两种技术,企业可以灵活应对不断变化的业务需求,降低运营成本,提高资源利用效率。未来,随着技术的不断进步和应用的深入,弹性伸缩与负载均衡技术将在更多领域发挥重要作用,推动云计算技术的持续发展。
|
API Go 数据安全/隐私保护
go-zero微服务框架的静态文件服务
【8月更文挑战第7天】`go-zero` 微服务框架支持多种静态文件服务实现方式。常用方法是利用 `Go` 标准库 `http.FileServer`。通过设置静态文件根目录并使用 `http.StripPrefix` 去除路径前缀,能确保 `/static/` 开头的请求正确返回文件。此外,结合 `go-zero` 的路由机制可更灵活地控制静态文件服务,例如仅在特定 API 路径 `/api/static` 下提供服务,从而实现精细化访问控制。
324 0
|
Prometheus 监控 Cloud Native
【揭秘可观测性】构建完美参考框架,打造系统监控的瑞士军刀!
【8月更文挑战第25天】在现代软件设计中,可观测性是确保系统稳定性和效率的关键因素。它主要由日志、指标及链路追踪(统称LMx)三大核心组件构成。本文详细介绍了构建高效可观测性框架的六个步骤:需求分析、工具选择、数据收集策略设计、实施集成、数据可视化及持续优化。并通过一个Spring Boot应用集成Prometheus和Micrometer收集指标的示例,展示了具体实践方法。合理构建可观测性框架能显著提升团队对软件系统的管理和监控能力,进而增强系统整体性能和可靠性。
183 2
|
机器学习/深度学习 算法 数据可视化
R语言惩罚logistic逻辑回归(LASSO,岭回归)高维变量选择的分类模型案例
R语言惩罚logistic逻辑回归(LASSO,岭回归)高维变量选择的分类模型案例