【C语言】程序环境和预处理(下)

简介: 【C语言】程序环境和预处理(下)

#define


一、#define 定义标识符


语法:

#define name stuff


#include <stdio.h>
#define M 100
int main()
{
  int m = M;
  printf("%d\n", m);
  return 0;
}

32afd2f003cd43e399b02b1364c127e8.png


#define 是用来定义符号的,但不仅仅只能定义常量,还可以做其他的东西。比如:为关键字创建一个简短的名字等。


#include <stdio.h>
#define reg register
int main()
{
  reg int num = 0;
  printf("%d\n", num);
  return 0;
}


用更形象的符号来替换一种实现,比如创建死循环。


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


也有可能有些程序员之前是学习其他语言的,现在来学习C语言。之前学习的语言里switch语句的每个case后面是不用加break,那么现在使用C语言的switch语句,就很容易忘掉加上break。那我们也可以通过 #define 来解决这个问题。


#include <stdio.h>
#define CASE break;case
int main()
{
  int n = 0;
  scanf("%d", &n);
  switch (n)
  {
  case 1:
    printf("hehe\n");
  CASE 2 :
    printf("haha\n");
  CASE 3 : 
    printf("heihei\n");
  }
  return 0;
}


上面的代码可等效为下面的代码。


#include <stdio.h>
#define CASE break;case
int main()
{
  int n = 0;
  scanf("%d", &n);
  switch (n)
  {
  case 1:
    printf("hehe\n");
    break;
  case 2 :
    printf("haha\n");
    break;
  case 3 :
    printf("heihei\n");
  }
  return 0;
}

知道了 #define 的几个应用后,那博主现在问大家一个问题:在define定义标识符的时候,要不要在最后加上 ; 呢?


其实大多数情况还是不要在最后加上;,避免出现无谓的错误。如果真的有需求的话,可以在最后加上;。接下来,我们来看一个例子。


错误的例子:


#include <stdio.h>
#define M 1000;
int main()
{
  int a = 10;
  int b = 0;
  if (a > 10)
    b = M;
  else
    b = -M;
  return 0;
}

206d58359c5a4eaaa85253d7034534aa.png


上面的代码等效于下面的代码。


#include <stdio.h>
#define M 1000;
int main()
{
  int a = 10;
  int b = 0;
  if (a > 10)
    b = 1000;
  ;
  else
    b = -1000;
  ;
  return 0;
}


if语句后面跟了句空语句,这就会导致else无法和if匹配上,导致语法错误。只要将 #define 定义标识符后面的;去掉,就能解决了这个语法错误了。所以在 #define 定义标识符的时候,很多时候不需要在最后加上 ;


二、#define 定义宏


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


下面是宏的声明方式:


#define name( parament-list ) stuff


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


注意:


参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。


代码示例:


#include <stdio.h>
#define SQUARE(X) X*X
//#define SQUARE (X) X*X  符号 内容
int main()
{
  printf("%d\n", SQUARE(3));
  //printf("%d\n", 3 * 3);
  return 0;
}

89f9a750eef84f4591ea5c51d1deb5c5.png


如果我们的宏向上面的代码那样写的话,就会存在一个问题。比如:给宏SQUARE传的参数是3 + 1,这时候就得不到我们想要的结果了。原因就是:宏的参数是不经过任何计算,就直接传过去的。


#include <stdio.h>
#define SQUARE(X) X*X
//#define SQUARE (X) X*X  符号 内容
int main()
{
  printf("%d\n", SQUARE(3+1));//7
  //printf("%d\n", 3 + 1 * 3 + 1); //宏的参数是不经过任何计算,就直接传过去的
  return 0;
}

37b64763a8da41ed8ac0a4afddfd681b.png


除了上面的例子外,还有很多的例子。比如:

#include <stdio.h>
#define DOUBLE(X) (X)+(X)
int main()
{
  printf("%d\n", 10 * DOUBLE(4));//44
  //printf("%d\n", 10 * (4) + (4));
  return 0;
}

2c7e14397dd143baba37fbadddd7aa37.png


所以,在定义宏的时候,要给宏多加几个括号以达到自己想要的效果。


#include <stdio.h>
#define SQUARE(X) ((X)*(X))
//#define SQUARE (X) X*X  符号 内容
int main()
{
  printf("%d\n", SQUARE(3 + 1));//16
  return 0;
}

8db4bedb4a28469cae7dbbab25f7fba0.png


#include <stdio.h>
#define DOUBLE(X) ((X)+(X))
int main()
{
  printf("%d\n", 10 * DOUBLE(4));//80
  return 0;
}

7f369d0b566a4747b2d2ddb45ccaa0d2.png


提示:

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中

的操作符或邻近操作符之间不可预料的相互作用。


三、#define 替换规则


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


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

注意:


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


举几个例子来说明一下上面的内容。


#include <stdio.h>
#define M 100
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
int main()
{
  int max = MAX(101, M);
  //int max = MAX(101, 100);
  //int max = ((101)>(100)?(101):(100));
  printf("%d\n", max);
  return 0;
}

因为M是 #define 定义的符号,所以它首先被替换成100。然后101100再将相应的宏和参数名替换掉。最后,往下搜索,看还有没有 #define 定义的符号和宏。

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


#include <stdio.h>
#define M 100
int main()
{
  printf("M = %d\n", M);
  return 0;
}

4ff15a55248c4af097117147415c6c28.png


因为M是在字符串内的,所以它不会被替换成相应的常量。


四、#和##


1.#


#可以将参数插入到字符串


学习这个之前,我们先来看一段代码。


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

7b8ce37739734aa89658d69a7a45afc2.png

我们可以发现字符串是有自动连接的特点的。知道了这个,我们就来学习#的作用。


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

a343d8b2126344f5b76162d5e39e8639.png


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

0da8e838dc024296b6327ad157095b4e.png


我们可以发现,当我们传的参数不一样,打印的字符串也不一样。这就#的作用了,将参数替换成相应的字符串。这用一个单一的函数接口是很难实现的,所以这也是一个非常好用的技巧。当时不要就#漏掉了,否则就会出错。


2.##


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

究竟是什么意思呢?看完下面的代码,你就懂了。


#include <stdio.h>
#define CAT(X, Y) X##Y  //将两个符号合成一个符号
int main()
{
  int class107 = 520;
  printf("%d\n", CAT(class, 107));
  //printf("%d\n", class107);
  return 0;
}


需要注意的是,##可以将多个符号合成一个符号,不只局限于将两个符号合成一个符号。

d20d5f53ee0845a7a2b91a1b23710a8e.png


注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。


五、带副作用的宏参数


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


例如:


x+1; //不带副作用 x 的值不会改变

x++; //带有副作用 x 的值会改变


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


#include <stdio.h>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
int main()
{
  int a = 5;
  int b = 8;
  int m = MAX(a++, b++);
  //int m = ((a++) > (b++) ? (a++) : (b++)); //宏替换
  printf("%d\n", m);
  printf("a = %d b = %d\n", a, b);
  return 0;
}

48209bab79e9476b823b087dbd48ae13.png如果将MAX宏替换成Max函数,就可以看到宏和函数的一些区别了。


#include <stdio.h>
int Max(int x, int y)
{
  return x > y ? x : y;
}
int main()
{
  int a = 5;
  int b = 8;
  int m = Max(a++, b++);
  printf("%d\n", m);
  printf("a = %d b = %d\n", a, b);
  return 0;
}

f1028451a98b4edb84b1209868bdb548.png


六、宏和函数对比


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


#define MAX(a, b) ((a)>(b)?(a):(b))


1.宏的优点


那为什么不用函数来完成这个任务?原因有二:


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


  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。
  2. 宏是没法调试的。(在预编译期间,就发生了 #define 定义的符号和宏的替换 )

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

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


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


#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
  int* p = MALLOC(10, int);
  //int* p = (int*)mallo(10 * sizeof(int));
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
    printf("%d ", p[i]);
  }
  free(p);
  p = NULL;
  return 0;
}

074e675c6019480db37be127afe9dc86.png


3.宏和函数的对比

5dca6ffc718b44f5ada7b2bb4eb88e53.png


七、命名约定


一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:


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



#undef


这条指令用于移除一个宏定义。


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

563ea7275f85477ba0a6da3409443578.png

移除宏定义后,该宏定义的标识符就不能再使用了。


命令行定义


许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)


比如,我在 Linux 系统上写了一份test.c代码。

1f7091520c9f484e8d7f4488d68177f2.png

其中M的大小是没有定义的,所以直接编译这个代买会出错。

80f1c0089a94462cb21577f750508de9.png

但是我们可以输入指令gcc test.c -D M=10,进行命令行定义。那么这个代码就能通过编译并生成可执行程序。

c5eb9a3b1c814f6495c81717d7274924.png

条件编译


在编译一个程序的时候,如果我们想要将一条语句(一组语句)编译或者放弃,这是是很方便的。因为我们有条件编译指令,满足条件就编译,不满足条件就不编译。


比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。


#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 
  }
  return 0;
}

如果我们不需要打印数组元素的值了,我们就可以将__DEBUG__的定义给去除掉。


常见的编译指令


  • #if 常量表达式
    //…语句
  • #endif //常量表达式由预处理器求值。

如:

#define __DEBUG__ 1

#if __DEBUG__ //..语句

#endif

多个分支的条件编译

#if 常量表达式 //…

#elif 常量表达式 //…

#else //…

#endif

注意:只要前面的表达式有一个满足,后面的表达式就算满足了也不会编译。

判断是否被定义

#if defined(symbol)

#ifdef symbol

#if !defined(symbol)

#ifndef symbol

嵌套指令

80025781ad6149469670dbc514e5dce9.png


代码示例 1


//满足条件就编译
#include <stdio.h>
int main()
{
#if 1==1
  printf("hehe\n");
  //因为前面的表达式满足,所以后面的语句就算满足了也不会编译
#elif 2==2
  printf("haha\n");
#else
  printf("heihei\n");
#endif
#if 0
  printf("hello world\n");
#endif
  return 0;
}

16bf79c797894cf48a72207e292c8247.png


代码示例 2

#include <stdio.h>
#define PRINT 1
int main()
{
  //如果PRINT定义了,就编译后面的语句
#ifdef PRINT
  printf("hello world1\n");
#endif
#if defined(PRINT)
  printf("hello world2\n");
#endif
  return 0;
}


2fbb66642c6d4e5b998ce7f27ffbf37d.png

代码示例 3


//如果PRINT没有定义,就编译后面的语句
#include <stdio.h>
int main()
{
#ifndef PRINT
  printf("hello world1\n");
#endif
#if !defined(PRINT)
  printf("hello world2\n");
#endif
  return 0;
}

140e42935c9f428390cd0ce35d9b9b7c.png


关于嵌套条件编译的例子就不举了,大家可以发挥自己的聪明才智想一想好的例子。

除了以上所列举的例子,条件编译还有用来注释代码。比如:



//相当于将下面的代码注释掉了
#if 0
#define PRINT 1
int main()
{
#if PRINT
  printf("hello world\n");
#endif
  return 0;
}
#endif

489a39c8ab6c47609882a44db80addf5.png

6b3b6af1a7dd45a1a7dc672fa2d33235.png


博主在 Linux 环境下对上面的代码进行了预处理操作,并查看test.i文件里的内容,可以发现没有任何的信息,说明这段不会参与后面的编译。


文件包含


我们已经知道, #include 预处理指令可以使另外一个文件被编译。这种替换的方式很简单:


预处理器先删除这条指令,并用包含文件的内容替换。如果一个源文件被包含10次,那就实际被编译10次。


一、头文件被包含的方式


1.本地文件包含
#include "filename"
• 1
2.库文件包含
#include <filename.h>



  • <>""包含头文件的本质区别是:查找的策略的区别
  • "" 1.代码所在的目录下查找 2.如果第一步找不到,则在库函数的头文件目录中查找
  • <> 直接去库函数头文件所在的目录下查找


Linux环境的标准头文件的路径


/usr/include


VS环境的标准头文件的路径


C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

//这是VS2013的默认路径,注意按照自己的安装路径去找。


如果去库函数头文件所在的目录下查找也查找不到所包含的文件的话,就会提示编译错误。


注意:对于库文件也可以使用 "" 的形式来包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。


二、嵌套文件包含


如果出现下图这样的场景:

dff5b630545f4e688f827d7d5964ec9b.png


comm.h和comm.c是公共模块。

test1.h和test1.c使用了公共模块。

test2.h和test2.c使用了公共模块。

test.h和test.c使用了test1模块和test2模块。

这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。


再举个例子,在 Linux 环境下编写了两份代码,预处理后可以看到头文件test.h的内容被包含了两次。

72ba723107d641b390e9f51cf5a54c4a.png


那如何解决这个问题呢?我们可以借助条件编译。

每个头文件的开头可以这样写:


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


或者


#pragma once


这样就可以避免头文件的重复包含了。


在头文件test.h开头写上#pragma once,就算源文件test.c内包含了两次头文件test.h,也只会包含一次头文件test.h。


b8f05eced3704d00970870d94570dc7a.png

在本篇文章讲解的预处理指令就是这些,其实还有很多其他的预处理指令。比如:#error、#pragma、#line和#pragma pack()等等,大家可以自行搜索学习或者参考《C语言深度剖析》来学习,在这里就不一一讲解了。


👉总结👈


在本篇文章里,主要讲解了翻译环境中的预处理(预编译)、编译和链接、运行环境、预处理详解,也介绍了相关的预处理指令。如果大家觉得文章写得不错,大家给个三连支持一下哦!谢谢大家啦!💖💝❣️


















相关文章
|
2月前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
|
2月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
2月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
1月前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
24 2
|
2月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
2月前
|
存储 文件存储 C语言
深入C语言:文件操作实现局外影响程序
深入C语言:文件操作实现局外影响程序
|
2月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
2月前
|
Linux C语言 iOS开发
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
25 0
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
36 3
|
2天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
23 6