目次
0 はじめに
C プログラムのコンパイルには多くのステップが含まれます。その最初のステップは前処理ステージ ( preprocessing
) ステージです。C プリプロセッサは、コンパイル前にソース コードに対していくつかのテキスト操作を実行します。#including
その主なタスクには、コメントの削除、ディレクティブに含まれるファイルの内容の挿入、#define ディレクティブで定義されたシンボルの定義と置換、およびコードの一部を条件付きコンパイル ディレクティブに従ってコンパイルする必要があるかどうかの決定が含まれます。
この号の内容フレームワーク:
1 つの定義済みシンボル
プリプロセッサはいくつかのシンボルを定義しており、プログラムのデバッグやバージョン生成に非常に便利です。例えば:
#include <stdio.h>
int main()
{
printf("This obj file name is:%s\n",__FILE__);
printf("Current line is:%d\n", __LINE__);
printf("-------------------------\n");
printf("Today is:%s\n", __DATE__);
printf("The time now is:%s\n", __TIME__);
system("pause");
return 0;
}
印刷出力:
現在実行しているファイルの名前、print ステートメントの行番号、およびコンパイルの日時がすべて印刷されていることがわかります。
注: ここでの日付と時刻は、ファイルがコンパイルされた日付と時刻であり、ファイルが実行された日付と時刻ではありません。
2 #定義
第 2 章は、マクロを定義する方法、意味、注意点などをまとめた最優先事項です。
2.1 マクロ
#define メカニズムには、パラメータをテキストに置換できる機能が含まれており、この実装はマクロ ( macro
) または定義マクロ ( defined macro
) と呼ばれることがよくあります。
たとえば、マクロを使用して二乗演算を定義します。
#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
int a = 5;
printf("%d\n",SQUARE(a));
system("pause");
return 0;
}
印刷出力:
ただし、この方法では次のような問題が発生することがあります。
#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
int a = 5;
printf("%d\n",SQUARE(a + 1));
system("pause");
return 0;
}
印刷出力:
予想によれば、6 の 2 乗、つまり 36 が出力されるはずですが、これはなぜでしょうか?
関数の実装とは異なり、マクロは単純な置換を実行するだけで、パラメータの受け渡しなどの作業は行いません。
比較することができます。
#include <stdio.h>
#define SQUARE(x) x*x
int square(const int x)
{
return x * x;
}
int main()
{
int a = 5;
printf("%d\n", SQUARE(a + 1));
printf("%d\n", square(a + 1));
system("pause");
return 0;
}
印刷出力:
関数の形式であれば、実行結果が私たちの考えと一致していることがわかります。これは、マクロが単純な置換作業のみを行うためです。つまり、実行後に自然な実行が行われるためです。結果5+1*5+1
は11
。
ただし、マクロ定義を使用して、期待どおりの結果を達成することもできます。
#include <stdio.h>
#define SQUARE(x) (x)*(x)
int square(const int x)
{
return x * x;
}
int main()
{
int a = 5;
printf("%d\n", SQUARE(a + 1));
printf("%d\n", square(a + 1));
system("pause");
return 0;
}
プリントアウト:
問題は解決しました。
2.2 #define 置換
このパートでは主に、マクロ パラメータを文字列定数に挿入する方法について説明します。たとえば、マクロを使用して印刷関数を再定義する必要があります。
#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
printf("The value of "VALUE" is "FORMAT"\n",VALUE)
int main()
{
int a = 5;
PRINT("%d", a);
PRINT("%d", a + 3);
system("pause");
return 0;
}
このとき、値を正確に渡すことができないため、プログラムはエラーを報告しますが、このとき、受信した式を文字列に変換する単純な変換を実行する必要があります。
#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
printf("The value of "#VALUE" is "FORMAT"\n",VALUE)
int main()
{
int a = 5;
PRINT("%d", a);
PRINT("%d", a + 3);
system("pause");
return 0;
}
印刷出力
ここでマクロ定義の利点が明らかになりますが、このような機能を実現するには関数のカプセル化が少し弱いことが分かります。
2.3 マクロと関数
上記の例から、多くの場合、マクロと関数は同様の機能を持ち、相互に置き換えることができることがわかります。しかし、この 2 つには違いがあり、この本の中に非常に明確に見える表があります。
属性 | #マクロを定義する | 関数 |
---|---|---|
コード長 | 使用するたびにプログラムに挿入されるので、マクロの内容はそれほど多くないはずです | 各関数呼び出しでは同じコードが使用されるため、比較的メモリに優しいコードになります。 |
実行速度 | もっと早く | 関数呼び出し/戻りの追加のオーバーヘッドが発生します |
演算子の優先順位 | マクロ パラメーターは周囲のすべての式のコンテキストで評価されるため、括弧を含める必要があります。 | 式の結果がより予測可能になる |
パラメータの評価 | パラメータはマクロ定義で使用されるたびに再評価されます。副作用のあるパラメータは複数の評価により予測できない結果を引き起こす可能性があります | パラメータの副作用は特別な問題を引き起こしません |
パラメータの種類 | マクロはタイプに依存せず、操作が正当である限り、任意のパラメータ タイプを使用できます。 | 関数のパラメータはその型と強く関連しているため、パラメータの型が異なる場合は、別の関数を定義するか、実際のパラメータをキャストする必要があります。 |
マクロと関数にはそれぞれ長所と短所があるため、適切な場面で適切な実装方法を選択し、調整することが最善の選択であることがわかります。
2.4 副作用のあるマクロパラメータ
パラメーターに副作用がある場合、マクロは予期しない結果を生成することがよくあるため、実際の開発では特別な注意が必要です。次の例を見てみましょう。
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int x = 2, y = 5, z = 0;
z = MAX(x++, y++);
printf("x = %d, y = %d, z = %d",x, y, z);
system("pause");
return 0;
}
z の値はどうあるべきですか? これをmax
関数で実装すれば確かに答えは簡単に得られますが、マクロ定義では和を2つの式に何度も置き換える必要があるため、そう単純ではないようa
ですb
。
しかし、よく考えてみると、それほど難しいことではありません。2x++
回実行し、y++
3 回実行したので、答えは一目瞭然です。 プリントアウト:
では、++x と ++y の場合はどうなるでしょうか。私たち自身でそれを探ることができます。。。
2.5 命名規則
開発を改善するために、マクロと関数を正式に区別する必要がある場合が多く、マクロ名をすべて大文字にするのが一般的な方法です。開発中にこれを経験した人も多いと思います。
2.6 #undef
このプリプロセッサ ディレクティブはマクロ定義を削除します。
宣言方法:
#undef name
既存の名前を再定義する必要がある場合は、まず古い定義を#undef
削除する必要があります。
2.7 コマンドライン定義
多くの C コンパイラは、コンパイル プロセスを開始するために使用できるコマンド ラインでシンボルを定義する機能を提供します。
Windows 用の一般的なコンパイラでは当該内容は使用されないため省略しています。
3 条件付きコンパイル
3.1 定義されているかどうか
3.2 ネストされた命令
大規模なプロジェクト開発でも、ネストされた命令が使用されることはほとんどありません。そして、たとえそれに遭遇しても、それを理解するのは難しくありません。基本的な形は本に載っているので、この記事では詳しく説明しません。
#ifdef OS_UNIX
#ifdef OPTION1
unix_version_of_option1();
#endif
#ifdef OPTION2
unix_version_of_option2();
#endif
#elif defined OS_MSDOS
#ifdef OPTION2
msdos_version_of_option2();
#endif
#endif
条件文の入れ子に似ているので、比較的読みやすいです。
4つのファイルには以下が含まれます
実際の開発では、大規模なプロジェクトではモジュール開発が採用されるため、多くの問題を回避できます。これにより、コードの開発、反復、読み取りがはるかに便利になります。
そしてファイルの内容この考え方はよく説明されています。
4.1 関数ライブラリ ファイルには以下が含まれます
いわゆる関数ライブラリ ファイルは、プログラムを作成する前に作成されたいくつかのプログラム (通常は関数) です。それをインターフェースと呼ぶこともあります。通常、含めるには山括弧を使用します。前の手順と同様に、次のようになります。
#include <stdio.h>
このようにして、コンピュータは、printf
プログラムがコンパイルされて実行されるときに、そのようなステートメントを認識できます。
4.2 ローカルファイルの組み込み
いわゆるローカル ファイル インクルードとは、通常、自分で作成したローカル ファイルを指します。たとえば、ファイル
を作成しprint.c
、次の関数を記述します。
#include <stdio.h>
#include "print.h"
void fun_print(char *x)
{
if (x != NULL)
{
printf("%s",x);
}
}
次に、print.h
ファイルを作成して宣言します。
#pragma once
void fun_print(char *x);
最後に、main.c
ファイル内で呼び出しを行います。
#include <stdio.h>
#include "print.h"
int main()
{
fun_print("Hello world!");
system("pause");
return 0;
}
これはローカル ファイル インクルードです。システム ファイル インクルードと区別するために、通常は二重引用符が使用されます。
4.3 ネストされたファイルのインクルード
大規模なプロジェクトの開発では、ソースファイルやヘッダファイルの数が多く、当然複雑な包含関係が存在します。入れ子になった包含関係は一般的な現象となっています。それ自体は何でもありませんが、複数の包含関係があると、 、コンパイラはコンパイル中にエラーを報告し、コンパイルは成功しません。
つまり、複数のインクルードは、プログラムの実行効率やメモリ サイズに直接影響します。具体的な内容については関連情報をご確認ください。では、この問題を回避するにはどうすればよいでしょうか?
一般に、コンパイル時にヘッダー ファイルの複数のインクルードや複数の定義を防ぐには、次の方法を採用できます。
#ifndef __HEADERNAME__H
#define __HEADERNAME__H 1
または
#pragma once
#pragma Once は、ヘッダー ファイルの保護に使用される前処理ディレクティブです。その役割は、コンパイル プロセス中に再定義エラーが発生するのを避けるために、同じファイルが複数回インクルードされないようにすることです。#ifndef と #define の組み合わせと同様の効果と言えます。#ifndef メソッドと比較して、#pragma Once はより簡潔かつ効率的で、ファイル全体を保護できます。
ただし、#pragma Once は非標準メソッドであり、ほとんどのコンパイラで広くサポートされていますが、互換性の問題もあることに注意してください。一部の古いコンパイラは #pragma Once をサポートしていない可能性があるため、使用する場合はプロジェクトの互換性を考慮する必要があります。
要約すると、どの方法を使用するかの選択は特定の状況によって異なり、チームの開発仕様に従って合意することができます。欠点を合理的に回避できる限り、どちらのアプローチも受け入れられます。
その他の5つの指示
#error
C言語の前処理命令の一つで、コンパイル時にソースコードに誤りがないかチェックし、コンパイル時にエラー情報を出力します。コンパイラーが #error ディレクティブを検出すると、コンパイルを停止し、#error の後にエラー メッセージを表示します。
#include <stdio.h>
#include "print.h"
int main()
{
#ifdef PRINT
fun_print("Hello world!");
#else
#error Print is not defined!
#endif
system("pause");
return 0;
}
「実行」をクリックすると、コンパイルが実行できず、エラー内容が直接出力されることがわかります。
もちろん、IDE が異なれば、エラーが異なる形式で報告される可能性があります。
#progma
ディレクティブは、コンパイラ固有の機能をサポートするためのもう 1 つのメカニズムです。たとえば、先ほどもお話しましたが、
#pragma once
これにより、ヘッダー ファイルが再びインクルードされるのを防ぎます。
いわゆるコンパイラ固有の機能のサポートつまり、#pragma
特定の状況に応じて、同じ命令が異なるコンパイラでは異なる効果を生み出す可能性があります。
6 まとめ
C プログラムをコンパイルする最初のステップは、プログラムをプリプロセスすることです。プリプロセッサは合計 5 つのシンボルをサポートします。
#define ディレクティブを使用すると、C 言語を「書き換え」て別の言語のように見せることができます。
条件付きコンパイルは、異なるコード セグメントを異なる条件で実行でき、大規模なマスキング プログラムよりもはるかに便利で、大規模プロジェクトの開発で広く使用されています。
実際の開発では、ヘッダー ファイルの複数のインクルードを避けるようにしてください。ただし、開発環境がエラーや警告を直接報告しない場合もあります。