<C言語>プリプロセスとマクロ

1. 定義済みのシンボル

__FILE__    //进行编译的源文件
__LINE__    //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

これらの定義済みシンボルはすべて C 言語に組み込まれています。

例えば:

#include <stdio.h>

int main() {
    
    
    printf("%s\n", __FILE__);//如:1.c
    printf("%d\n", __LINE__);// 5
    printf("%s\n", __DATE__);// Jul 30 2023
    printf("%s\n", __TIME__);// 10:13:20  记录的时间是编译的时间
    printf("%d\n", __STDC__);   //1   也可能是未定义  不遵循ANSI C
    return 0;
}

2 #定義

2.1 #define 定義識別子

#defineフォームの識別子を定義します。

#define 标识符 值

その中には、标识符定義する名前があり、値、文字列、または式を指定できます。

例:

#include <stdio.h>
#define MAX 100
#define STR "Hello Wrold"
#define do_forever for (;;)
int main() {
    
    
    printf("%d\n", MAX);//100
    printf(STR);        //Hello World
    do_forever;       //死循环

    return 0;
}

#define単純なテキスト置換のみを実行し、型チェックやエラー チェックは行いません。

#defineの後にセミコロンを追加しないことをお勧めします

#include <stdio.h>
#define MAX 1000;
int main() {
    
    
    int max = 0;
    if (3 > 5) {
    
    
        //max = MAX;   //报错  因为MAX ==1000; 出现了两个分号
        max = MAX//正确
    } else {
    
    
        max = 0;
    }

    return 0;
}

2.2 #define定義マクロ

#define メカニズムには、パラメーターをテキストに置換できるようにする機能が含まれており、この実装はマクロまたは定義マクロと呼ばれることがよくあります。

マクロの宣言方法は次のとおりです。

#define name( parament-list ) stuff

parament-list は、スタッフ内に現れる可能性のあるシンボルのカンマ区切りのリストです。

例:

#include <stdio.h>

//函数解决
int Max_hanshu(int x, int y) {
    
    
    return x > y ? x : y;
}
//宏解决
#define MAX(x, y) (x > y ? x : y)
int main() {
    
    
    int a = 10;
    int b = 20;
    int max = Max_hanshu(a, b);
    int m = MAX(a, b);
    printf("%d\n", max);  //20
    printf("%d\n", m);    //20
    return 0;
}

知らせ:

パラメータリストの開き括弧は名前のすぐ隣になければなりません。
2 つの間に空白が存在する場合、引数リストは引数の一部として解釈されます。

例えば:

#define SQUARE( x ) x * x

このマクロはパラメータ x を受け取ります。上記の宣言の後に、

SQUARE( 5 );

プログラムに配置すると、プリプロセッサは上記の式を次の式に置き換えます。

5 * 5

警告:
このマクロには問題があります。
次のコード スニペットを確認してください。

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

一見すると、このコードは値 36 を出力すると思うかもしれません。実際には 11 が出力されます。

なぜ?

テキストを置換する場合、パラメータ x は + 1 に置換されるため、このステートメントは実際には次のようになります。

printf ("%d\n",a + 1 * a + 1 );

これにより、置換によって生成された式が予期された順序で評価されていないことが明確になります。

マクロ定義に 2 つの括弧を追加すると、この問題は簡単に解決されます。

#define SQUARE(x) (x) * (x)

この方法で前処理を行うと、期待どおりの効果が得られます。

printf ("%d\n",(a + 1) * (a + 1) );

別のマクロ定義を次に示します。

#define DOUBLE(x) (x) + (x)

前の問題を回避するために定義内で括弧を使用しましたが、このマクロには新たなエラーが発生する可能性があります。

int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

これはどのような値を出力しますか?

警告:

100 が出力されているように見えますが、実際には 55 が出力されます。
置換後は次のことがわかります。

printf ("%d\n",10 * (5) + (5));

乗算演算はマクロで定義された加算の前に行われるため、55 が表示されます。

この問題の解決策は、マクロ定義式の前後に一対のかっこを追加することです。

#define DOUBLE( x)   ( ( x ) + ( x ) )
#include <stdio.h>

#define SQUARE(X) X *X
#define SQUARE1(X) (X) * (X)
#define DOUBLE(X) (X) + (X)
#define DOUBLE1(X) ((X) + (X))
int main() {
    
    
    printf("%d\n", SQUARE(5));     // 25
    printf("%d\n", SQUARE(5 + 1)); // 5+1*5+1 == 11
    printf("%d\n", SQUARE1(5 + 1));// 36

    printf("%d\n", DOUBLE(6));      // 12
    printf("%d\n", DOUBLE(6 + 1));  // 14
    printf("%d\n", 10 * DOUBLE(6)); // 66  10*(6)+(6) ==66
    printf("%d\n", 10 * DOUBLE1(6));//120
    return 0;
}

要約:

数値式の評価に使用されるすべてのマクロ定義は、マクロの使用時に引数内の演算子または隣接する演算子間の予期しない相互作用を避けるために、この方法で括弧で囲む必要があります。

2.3 #define の置換規則

#define を展開してプログラム内のシンボルとマクロを定義する場合は、いくつかの手順が必要です。

  • マクロを呼び出すときは、最初に引数がチェックされ、#define で定義されたシンボルが含まれているかどうかが確認されます。「はい」の場合、最初に置き換えられます。
  • 置換テキストは、元のテキストの代わりにプログラムに挿入されます。マクロの場合、パラメータ名はその値に置き換えられます。
  • 最後に、結果のファイルが再度スキャンされ、#define で定義されたシンボルが含まれているかどうかが確認されます。「はい」の場合は、上記の処理を繰り返します。

知らせ:

  • 他の #define 定義シンボルは、マクロ パラメーターおよび #define 定義に使用できます。しかし、マクロでは再帰は発生しません。
  • プリプロセッサが #define で定義されたシンボルを検索する場合、文字列定数の内容は検索されません。

例 1 - 正当なマクロ定義:

#define PI 3.14159
#define CIRCLE_AREA(radius) (PI * (radius) * (radius))

double area = CIRCLE_AREA(2.5); // 宏 CIRCLE_AREA 使用了已定义的宏 PI

例 2 - 不正なマクロ定義 (再帰的):

// 这是一个非法的宏定义,宏 AREA 使用了它自身
#define AREA(x) (x > 0 ? x * x : AREA(x))

int result = AREA(5); // 这将导致宏展开的无限循环,造成编译错误

2.4 # と ##

#演算子はマクロ引数を文字列定数に変換できます。これにより、マクロ定義内の引数を文字列リテラルに変換できます。

例:

#define STRINGIFY(x) #x

int main() {
    
    
    int num = 42;
    const char* str = STRINGIFY(num);
    // 在宏展开时,num 被转换为字符串 "42"
    printf("num as a string: %s\n", str); // Output: "num as a string: 42"
    return 0;
}

##演算子は、マクロ定義に 2 つのトークンを貼り付けるために使用されます。これにより、複数の識別子を新しい識別子に結合できます。

例:

#define CONCAT(x, y) x ## y

int main() {
    
    
    int num1 = 10;
    int num2 = 20;
    int result = CONCAT(num, 1) + CONCAT(num, 2);
    // 在宏展开时,CONCAT(num, 1) 变成 num1,CONCAT(num, 2) 变成 num2
    // 所以,result 的值就是 num1 + num2,即 10 + 20
    printf("result: %d\n", result); // Output: "result: 30"
    return 0;
}

2.5 副作用のあるマクロ引数

マクロ パラメータがマクロの定義内で複数回出現する場合、そのパラメータに副作用がある場合、このマクロの使用は危険であり、予測できない結果が生じる可能性があります。副作用は、式が評価されるときに発生する永続的な影響です。

副作用のあるコード:

int main() {
    
    
    int a = 1;
    int b = a + 1;// b=2,a=1
    a = 1;
    b = ++a;// b=2,a=2  带有副作用的代码,a的值发生了改变
    int ch = getchar();//读一个字符,缓冲区少一个字符

    return 0;
}
x+1;//不带副作用
x++;//带有副作用

MAX マクロは、副作用のあるパラメーターによって引き起こされる問題を実証できます。

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

ここで、プリプロセッサの結果が何であるかを知る必要があります。

z = ( (x++) > (y++) ? (x++) : (y++));

したがって、出力は次のようになります。

x=6 y=10 z=9

2.6 マクロと関数の比較

マクロは通常、単純な操作を実行するために使用されます。たとえば、2 つの数値のうち大きい方を見つけます。

では、このタスクに関数を使用してみてはいかがでしょうか?

理由は 2 つあります。

1. 関数の呼び出しと関数からの戻りに使用されるコードは、この小さな計算作業を実際に実行するのにかかる時間よりも長い時間がかかる場合があります。
したがって、プログラムのサイズと速度の点では、マクロの方が関数よりも優れています。

2. さらに重要なのは、関数のパラメータを特定の型として宣言する必要があることです。したがって、関数は適切な型の式でのみ使用できます。逆に、このマクロは、> で比較できる整数、長整数、浮動小数点型などの型にどのように適用できますか。マクロはタイプに依存しません。

例:

#include <stdio.h>

int Max(int x, int y) {
    
    
    return x > y ? x : y;
}

#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main() {
    
    
    int a = 10;
    int b = 20;
    //函数的方式
    int m1 = Max(a, b);
    printf("%d\n", m1);

    //宏的方式
    int m2 = MAX(a, b);
    printf("%d\n", m2);
    return 0;
}

マクロの欠点: もちろん、関数と比較すると、マクロにも欠点があります。

  1. マクロが使用されるたびに、マクロによって定義されたコードのコピーがプログラムに挿入されます。マクロが比較的短い場合を除き、プログラムの長さが大幅に長くなる可能性があります。

  2. マクロはデバッグできません。

  3. マクロは型に依存しないため、厳密さが不十分です。

  4. マクロにより演算子の優先順位の問題が発生し、プログラムがエラーを起こしやすくなる場合があります。

マクロは、関数ではできないことを実行できることがあります。例: マクロパラメータはtypeを持つことができますが、関数は持つことができません。

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
int main(){
    
    
    int *p = malloc(10, sizeof(int));
    MALLOC(10, int);  //类型作为参数
    return 0;
} 

マクロと関数の比較

属性 #definemacros 関数
コード長 マクロ コードは、プログラムが使用されるたびにプログラムに挿入されます。非常に小さなマクロを除いて、プログラムの長さは大幅に増加する可能性があります 関数コードは 1 か所のみに出現し、関数が使用されるたびに、その場所にある同じコードが呼び出されます。
実行速度 もっと早く 関数の呼び出しと戻りには追加のオーバーヘッドがあるため、比較的遅くなります。
演算子の優先順位 マクロ パラメーターの評価は、周囲のすべての式のコンテキスト内で行われます。括弧を追加しないと、隣接する演算子の優先順位により予期しない結果が生じる可能性があるため、マクロを作成するときは括弧を多く使用することをお勧めします。 関数パラメーターは、関数が呼び出されたときに 1 回だけ評価され、その結果の値が関数に渡されます。式の評価結果がより予測可能になります。
副作用のあるパラメータ パラメータはマクロ本体内の複数の場所で置換される可能性があるため、副作用のあるパラメータ評価では予測できない結果が生じる可能性があります。 関数パラメータは渡されたときに 1 回だけ評価されるため、結果の制御が容易になります。
パラメータの種類 マクロのパラメータは型とは関係がありません。パラメータに対する操作が正当である限り、どのパラメータ型でも使用できます。 関数のパラメータは型に関係しており、パラメータの型が異なれば、同じタスクを実行する関数であっても、異なる関数が必要になります。
デバッグ マクロはデバッグに不便です 関数はステートメントごとにデバッグ可能
再帰 マクロは再帰的にはできません 関数は再帰的に使用できます

2.7 命名規則

一般に、関数マクロの使用構文は非常に似ています。したがって、言語自体はこの 2 つを区別するのに役立ちません。そして、私たちのいつもの習慣の1つは次のとおりです。

マクロ名を大文字にする

すべて大文字の関数名を使用しないでください

3.#undef

このコマンドはマクロ定義を削除するために使用されます。

#include <stdio.h>

#define M 100
int main() {
    
    
    printf("%d\n");
#undef M
    printf("%d\n", M);//报错,M的宏定义已经被移除
    return 0;
}

4. コマンドライン定義

C 言語は、開発者がコマンドライン プログラムを作成してコンピュータと対話できるようにする汎用プログラミング言語です。コマンド ライン プログラムは、コマンド ライン インターフェイス (ターミナルまたはコマンド プロンプトとも呼ばれます) で実行されるプログラムです。ユーザーはコマンドとパラメータを入力してこれらのプログラムを呼び出し、プログラムの出力から結果を取得できます。

C 言語では、コマンド ライン パラメータは main 関数のパラメータを通じてプログラムに渡されます。main 関数は C プログラムのエントリ ポイントであり、argcと の2 つのパラメータがありますargv

  1. argc: プログラム自体を含むコマンドライン引数の数を示します。整数型の変数です。
  2. argv: は、コマンドライン引数の文字列を格納するために使用される文字ポインターの配列へのポインターです。各文字列はコマンドライン引数を表します。このうち、argv[0] にはプログラム名 (実行可能ファイルの名前) が格納され、argv[1] には最初のコマンド ライン パラメータが格納されます。
int main(int argc, char *argv[]) {
    
    
    // Your code here
    return 0;
}

説明例:

「my_program」というプログラム、コンパイルされた実行可能ファイル「my_program.exe」(Windows の場合) があり、そのプログラムをコマンド ラインで実行すると、入力は次のようになります。

my_program hello world

この例では、プログラム名「my_program」、「hello」、「world」、および文字列の終わりを示す暗黙的な null ポインターの 4 つの引数があるため、argc の値は 4 になります。

argv 配列には次のものが含まれます。

argv[0] -> "my_program"
argv[1] -> "hello"
argv[2] -> "world"
argv[3] -> NULL
#include <stdio.h>

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

出力結果:

Argument 0: my_program
Argument 1: hello
Argument 2: world

5. 条件付きコンパイル

条件付きコンパイルは、コンパイル フェーズ中にさまざまな条件に基づいてコード フラグメントを選択的に含めたり除外したりできるようにする前処理ディレクティブです。条件付きコンパイルを使用すると、異なるプラットフォームで異なるコードを使用したり、特定の機能を有効/無効にしたりするなど、さまざまなコンパイル条件に基づいてプログラムの動作を制御できます。

条件付きコンパイルは、前処理ディレクティブ#ifdef#ifndef#else#endif#if#elifなどを使用して実装されます#defineこれらのディレクティブはすべてポンド記号 (#) で始まり、コンパイル前にプリプロセッサによって処理されます。

C 言語での条件付きコンパイルの基本的な手順は次のとおりです。

1.#ifdef#ifndef:

#ifdef 宏名
    // 如果宏已定义,则编译这里的代码
#else
    // 如果宏未定义,则编译这里的代码
#endif

#ifdefこれは、マクロが定義されているかどうかを確認するために使用されます。定義されている場合は、と#ifdefの間のコードをコンパイルします#endif。そうでない場合は、コードのこの部分をスキップします。

#ifndef次に、 の代わりに#ifdef、マクロが未定義かどうかをチェックし、未定義の場合は と#ifndefの間のコードをコンパイルします#endif

2. #else

#ifdef 宏名
    // 如果宏已定义,则编译这里的代码
#else
    // 如果宏未定义,则编译这里的代码
#endif

#else#ifdefor条件が満たされないときとの間でコードを#ifndefコンパイルするために使用されます#else#endif

3. #if#elifおよび#endif:

#if 表达式
    // 如果表达式为真,则编译这里的代码
#elif 其他表达式
    // 如果其他表达式为真,则编译这里的代码
#else
    // 如果前面的条件都不满足,则编译这里的代码
#endif

#if式の結果に基づいて次のコードをコンパイルするかどうかを決定できます。#elif前の条件が満たされない場合に、他の条件を確認するために使用されます。#elseこれは、前の条件がいずれも満たされない状況に対処するために使用されます。

ネストされた条件コンパイル

#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

6. ファイルには次の内容が含まれます

6.1 ヘッダーファイルのインクルード方法

1. ローカルファイルに「」が含まれています

検索方法: まず、ソース ファイルが存在するディレクトリを検索します。ヘッダー ファイルが見つからない場合、コンパイラは、ライブラリ関数のヘッダー ファイルを検索するのと同じように、標準の場所でヘッダー ファイルを検索します。

見つからない場合は、コンパイル エラーが表示されます。

#include"add.h"
int main(){
    
    
    printf("hehe\n");
    return 0;
}

2. ライブラリファイルには < > が含まれています

ヘッダー ファイルを見つけるには、標準パスに直接移動して見つけます。見つからない場合は、コンパイル エラーが表示されます。

このように、ライブラリファイルも「 」の形でインクルードできると言えるのではないでしょうか?
答えは「はい、できます」です。

ただし、この方法では検索効率が低くなりますし、ライブラリ ファイルかローカル ファイルかを区別するのは当然容易ではありません。

6.2 ネストされたファイルのインクルード

ここに画像の説明を挿入

comm.h と comm.c は共通モジュールです。
test1.h と test1.c は共通のモジュールを使用します。
test2.h と test2.c は共通のモジュールを使用します。
test.h と test.c は test1 モジュールと test2 モジュールを使用します。
このようにして、comm.h の 2 つのコピーが最終的なプログラムに表示されます。これにより、ファイルの内容が重複して作成されます。

この問題をどうやって解決すればいいでしょうか?
答え: 条件付きコンパイル。

各ヘッダー ファイルの先頭に次のように記述します。

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

または:

#pragma once

おすすめ

転載: blog.csdn.net/ikun66666/article/details/132005546