最近やったことの一つ、C言語の整理(2) — マクロの使い方

私は常に一連の共有をしたいと思っていました。ユニークでありたいと思っていました。また、それがすべての人に真の価値をもたらすことができるとも考えていました。これを見て、あなたは私が少し夢想家であると推測したかもしれません。なぜなら、実際、よく考えてみると、それは簡単な仕事ではないからです。多くの大きな牛がそれを果たせなかったのに、あなたにはできるでしょうか? 総じて、夢を持つこと、夢を持つこと、目標を持つことは良いことです。一生懸命働きます モチベーション、混乱するのは簡単ではありません、ちょうどこの 2 日間で新しいクラウンが終わり、情報を確認する時間があり、知識の点から皆のために何かを書こうとします。

マクロの基本概念

  • マクロとは何ですか
  • マクロの定義と使用方法
  • なぜマクロが必要なのか

マクロとは何ですか

事前定義されたルールのセットに従って、特定のテキスト パターンを置き換えます。このパターン置換は、マクロが検出されたときに、インタープリターまたはコンパイラーによって自動的に行われます。コンパイル言語の場合、マクロ展開はコンパイル時に行われ、マクロ展開を実行するツールはマクロ エキスパンダと呼ばれることがよくあります。

簡単に言うと、いわゆるマクロとは、ファイル内のマクロマークの位置を置き換える文字列を定義するもので、単純な元のテキストの置き換えですただし、ここではいくつかの注目すべき問題といくつかのヒントを紹介します。これは、今日説明する重要なポイントの 1 つでもあります。言及すべきもう 1 つの重要な点は、マクロ アプリケーションのシナリオです。

#include <stdio.h>

#define W_WIDTH 800
#define W_HEIGHT 600


void createWindow(int w,int h)
{
    printf("create (%d,%d)",w,h);
}

int main(int argc, char const *argv[]){

    createWindow(W_WIDTH,W_HEIGHT);
    
    return 0;
}

マクロを簡単に理解していれば、上記のコードに精通している必要があります。たとえば、ソフトウェアを開発している場合でも、ゲームを開発している場合でも、常にウィンドウが作成され、そのウィンドウにはサイズがあります。ウィンドウの値は通常、アプリケーション全体で固定されます。これらの量は通常、アプリケーションでは変更されませんが、通常はマクロを使用して定義できます。

元のテキストの置換、つまりコードcreateWindow(W_WIDTH,W_HEIGHT)内で800 と 600を置換してW_WIDTH表示するものを理解するのに役立つマクロ定義の例を次に示します。W_HEIGHT

上記は値の絶対値を計算する実装です. 2 つの問題があることは難しくありません. 1 つ目の問題は, マクロ内に型チェックがないことです. マクロを使用するとき, 呼び出し元は意識的に次のことを守る必要があります.のルール

前処理とは何ですか

gcc を使用して C 言語ソース ファイルを実行可能ファイルにコンパイルすることは、1 ステップのプロセスではありません。主に次の 4 つの段階を経ます。前処理はコンパイル プロセスの 1 段階です。ここではコンテンツのこの部分には焦点を当てません。興味があるなら自分でやってもいいよ 調べてみて

  • 前処理
  • コンパイル
  • 編集
  • リンク

前処理では何をしましたか?

  • 宏展开
  • 头文件包含
  • 条件编译

如何定义宏

在宏的定义会使用 #define 这个关键字

  • 一般形式,也就是不带参数宏的定义

#define 宏名称 字符串

  • 带参数形式 #define 宏名称 字符串 接下来我们再来举一个例子,为了是说明如何使用带参数的宏,
#include <stdio.h>

#define ABS(a) (((a) > 0)?(a):(-(a)))

int main(int argc, char const *argv[]){
    
    int a = 12;
    
    printf("%d",ABS(a));//12
    
    return 0;
}

这里举了一个带参数的宏例子,也就是求数值的绝对值,比较简单。可能大家唯一会有疑惑的就是为什么有这么多的括号。这也是大家在定义宏时候需要注意一点,因为宏是原文替换,所以在替换过程中,没有考虑运算一些先后顺序,所以需要我们通过添加一些括号来保证能够得到我们想要的效果。

#include <stdio.h>

#define ABS(a) a > 0?a:-a

int main(int argc, char const *argv[]){
    
    int a = 12;
    
    printf("%d",ABS(-3+2));//5
    
    return 0;
}

这里因为没有添加括号,我们不难看出

ABC(-3+2) 会被替换为 -3+2 > 0?-3+2:--3 + 2 所以会得到 5。

define 定义以及撤销

#include <stdio.h>

#define PI 3.1415926

int main(int argc, char const *argv[]){
    
    float r = 12.0;
    
    float area = PI * r * r;
    
    printf("%f",area);
    
    return 0;
}
  • 这里简单定义了一个宏 PI ,然后利用 PI 和半径来计算圆的面积

那么我们如何来撤销已经定义好的宏

#include <stdio.h>

#define PI 3.1415926

int main(int argc, char const *argv[]){
    
    float r = 12.0;
 #undef PI   
    float area = PI * r * r;
    
    printf("%f",area);
    
    return 0;
}

在宏中 # 和 ## 的使用

  • 单个 # 作用是将参数字符串化也就是这里 #a 应该替换为 "a" 而不再是 a
  • 两个 # 作用是将两个两个对象拼接在一起,a##b 拼接后等价于 ab 合并一起写的

对于一个 # 来表示字符串字面量

#include <stdio.h>

#define STRING(str) str

int main(int argc, char const *argv[]){
    
    int a = 2;
    
    printf("%d",STRING(a));
    
    return 0;
}

也就是将 str 转换为字符串

#include <stdio.h>

#define STRING(str) #str

int main(int argc, char const *argv[]){
    
    int a = 12;
    printf("%s\n",STRING(a));
    printf("%s\n",STRING(abc         def));
    printf("%s\n",STRING(       abcdef));
    printf("%s","hello world");
    
    return 0;
}
  • 这里 abc def 只会保留一个空格
  • 这里 abcdef 会去掉之前的空格
#include <stdio.h>

#define NAME(a,b) a##b
#define HW "helloworld"
int main(int argc, char const *argv[]){
    
    char* hello = "hey";
    
    printf("%s\n",hello);//hey
    printf("%s\n",NAME(h,ello));//hey
    printf("%s\n",NAME(he,llo));//hey
    printf("%s\n",NAME(H,W));//helloworld
    printf("%s\n",NAME(HW,));//helloworld
    
    return 0;
}
  • a##b 表示连接将 ab 连接在一起,那么上面例子就不难理解了,而且这里也支持嵌套宏

我们在哪里见过宏(宏应用场景)

  • 避免头文件重复包含
  • 宏可以简单定义常量
  • 宏提供了代码复用性
  • 我们可以通过 for 或者 while 循环来让一行或者多行代码反复执行
  • 函数提供了,让代码块来可以反复使用

避免头文件重复包含

估计对于一些初学者,避免头文件重复包含使用场景大家并不陌生。下面通过代码解释一下是如何通过宏来防止头文件重复加载。

#ifndef _ADD_H_
#define _ADD_H_
int add(int a,int b);
#endif
  • 首先 ifndef 表示 if not define 含义也就是检查是否定义了宏 _ADD_H_ 如果没定义宏就会执行条件语句内部逻辑来定义宏,然后包含我们头文件的内容 endif 解释条件语句,这种条件语句结束方式与我们熟悉方式有所不同。
  • 如果已经定义了,不满足条件语句,也就是不会再次包含头文件的内容,避免了代码重复包含。

错误信息输出

  • 使用宏要比调用一个函数的成本低得多,下面的例子我们就定义了一个宏来实现错误信息打印功能来代替原有错误信息输出,关于这部分代码中有两点想要说的,也可能是你困惑的地方,第一个就是 \ 在宏中多行结尾处都出现了。表示扩展符号,因为宏定义时需要将定义字符串写在同一行,不过对于一些复杂情景这样做不便于阅读,所以通过 \ 可以一行内容拆分多行来写
#include <stdio.h>

#define error_t(A) \
    do{\
        printf(A); \
        putchar(10); \
        return 0;\
    }while(0)

int main(int argc, char const *argv[]){
    
    int a;
    int ret = scanf("%d",&a);
    if(ret < 0)
    {
        error_t("hello world");
    }
    
    printf("%d\n",a);
    
    return 0;
}

还有就是为什么要用 do while 这样结构来包括要执行的代码呢?这个可能是大家另一个疑惑的点,其实我们注意在定义宏 while(0) 后面没有加 ;,这样做的好处就是让我们调用宏和调用函数一样都需要在结尾处添加; 只是为代码看起来更加优雅。

举几个小例子

计算某一个字段在结构体中偏移量

#include <stdio.h>

#define OFFSETOF(type,field) ((size_t)&((type*) 0)->field)

typedef struct Employee
{
    int Id;
    char name[32];
    float salaray; 
} Employee;

int main(int argc, char const *argv[]){
    
    int offset = OFFSETOF(Employee,salaray);
    printf("offset=%d\n",offset);
    
    return 0;
}
  • 这里可能有疑惑的就是 ((type*) 0 ) 这里的 0 表示一个起始地址,然后将其转换为 type 类型的指针,-> 访问其属性的地址,
  • 在 ANSI C 语言标准中,值为 0 的常量可以强制转换为任何一种类型的指针,并且转换的结果为 NULL 因此 ((type*) 0) 的结果就是一个类型为 type 指向 NULL的指针
  • 内存地址起始于 0 ,这里指向 NULL ,拿 NULL 是一个空指针,既然是一个指针也一定存放在内存中某一个位置的地址,通过下面代码不难看出 NULL 指向内存起始位置 0 的内存地址。
char *str = NULL;
printf("%d\n",str);//0
  • 接下来问题又来了,我们尝试访问一个空指针的 field 为什么没有报错呢? 在 c 语言中,我们要访问一个地址空间,通常先找到地址,然后再去访问地址所指的空间。因为这里
#define OFFSETOF(type,field) ((size_t) &(((type*) 0)->field) )

因为这里 & 表示计算只关心地址,而不关心空间值,所有编译器在优化时,直接计算地址,而不会方法地址对应的空间,所以不会报错。

计算结构体某一个字段的大小

#include <stdio.h>

#define FIELDSIZE(type,field) sizeof( ((type*) 0)->field )

typedef struct Employee
{
    int Id;
    char name[32];
    float salaray; 
} Employee;

int main(int argc, char const *argv[]){
    
    int offset = FIELDSIZE(Employee,salaray);
    printf("offset=%d\n",offset);
    
    return 0;
}

字符字母大小写转换

#define UPCASE(ch) ((ch<='Z' && ch>='A')?(ch - 0x20):ch)
#define LOWCASE(ch) ((ch<='Z' && ch>='A')?(ch + 0x20):ch)

调试输出

在开发过程中,少不了调试输出一些信息,当然如果不是做嵌入式,在有条件情况下,还是推荐使用 IDE 提供功能通过设置断点来进行调试。不过在 c 语言开发中,很多情况下,并没有条件来进行断点调试,这时就少不了 printf 不过这样做比较繁琐,而且每次在发布前还需要做一些注释 printf 语句的工作。为此我们看一看如何通过宏来解决这个痛点。

int main(int argc, char const *argv[]){
    
    int a = 0;
    for(int i = 0; i < 10;i++)
    {
        printf("a=%d ",a);
    }
    
    printf("\n");
    
    return 0;
}

最近在开发 js ,个人喜欢代码中到处都是 console.log ,在这里多说两句,一个好的项目,一定有关于这些问题解决方案或者说明,也就是错误跟踪,例如日志输出,版本管理还有就是单元测试,参与到项目几乎很少有人愿意投入精力来做这些工作。

#define PRINT(fmt,...) \
    printf("[FILE:%s][FUNC:%s][LINE:%d] " fmt, \
    __FILE__,__FUNCTION__,__LINE__,__VA_ARGS__)

int main(int argc, char const *argv[]){
    
    int a = 0;
    for(int i = 0; i < 10;i++)
    {
        // printf("a=%d ",a);
        PRINT("a=%d",a);
    }
    
    printf("\n");
    
    
    return 0;
}

关于这部分代码感兴趣,没看不懂可以给我留言。这样会打印出信息出自哪一个文件的哪一个方法。我们还是简单分析吧,这里有一个 c 语言的可变参数,用过 js 应该了解这个语法现象。

接下来加一个控制是否输出信息调试开关,通过控制 DEBUG

#define DEBUG 0

#if DEBUG
#define PRINT(fmt,...) \
    printf("[FILE:%s][FUNC:%s][LINE:%d] " fmt, \
    __FILE__,__FUNCTION__,__LINE__,__VA_ARGS__)
#else 
#define PRINT(fmt,...) 
#endif
int main(int argc, char const *argv[]){
    
    int a = 0;
    for(int i = 0; i < 10;i++)
    {
        // printf("a=%d ",a);
        PRINT("a=%d",a);
    }
    
    printf("\n");
    
    
    return 0;
}

おすすめ

転載: juejin.im/post/7233981178099793979