C言語のコンパイル処理と前処理

ヒント: 記事を作成した後、目次を自動的に生成できます。生成方法は、右側のヘルプドキュメントを参照してください。


序文

1. C言語のコンパイルとリンク

1. コンパイルとリンクの概要

C言語のプロジェクトには、多数の.cファイルが存在しますが、この.cファイルがソースファイルであり、プログラムを構成する各ソースファイルは、コンパイル処理を通じてオブジェクトコード(オブジェクトコード)に変換されます。ファイルは、対応するオブジェクト ファイル、つまり .obj ファイルに変換されます。
これらのオブジェクト ファイルはリンカーによってまとめられ、単一の完全な実行可能プログラムを形成します。
また、リンカは、プログラムで使用される標準 C ライブラリ内の関数をインポートし、プログラマの個人ライブラリを検索して、必要な関数をプログラムにリンクできます。
コンパイラは、コードを格納するこれらのソース ファイルを、マシンが認識できるバイナリ命令を格納するオブジェクト ファイルにコンパイルします。その後、リンカがこれらのオブジェクト ファイルをリンクします。これは、 add.c および sub.c で定義された関数が test.c で使用されるためです。リンク手順では、test.c で使用されるこれらの関数をリンクします。次に、実行可能プログラムが形成されます。
ここに画像の説明を挿入

2. 詳細な編集とリンク

ソース ファイル test.c および add.c は、コンパイル プロセス中に最初にプリコンパイルされます。プリコンパイル段階では、コード内のすべてのコメントが削除され、その後、#define で定義された識別子とマクロが置き換えられます。ヘッダー ファイルのインクルードも導入されます。つまり、test.i と add.i の一部は前処理されたコードです。test.c の一部のコードとコメントが test.i ではなくなっていることがわかります。

ここに画像の説明を挿入
前処理が完了したら、次のステップは、生成された test.i ファイルをコンパイルすることです。これにより、C 言語コードがアセンブリ コードに変換されます。そして、構文分析、字句分析、意味分析、記号の要約を実行します。
ここに画像の説明を挿入
最後に、コンパイラは、コンパイルされた test.cod アセンブリ ファイルを、アセンブリ処理を通じてマシンが認識できるバイナリ命令に変換します。次に、シンボルテーブルも作成されます。
ここに画像の説明を挿入
次に、リンカは、生成されたオブジェクト ファイル test.obj と add.obj のシンボル テーブルをマージおよび再配置し、セグメント テーブルをマージします。
test.cod はアセンブル後に一部のグローバル関数のシンボルテーブルを形成し、add.cod もアセンブル後に一部のグローバル関数のシンボルテーブルを形成しますリンク時には、test.obj と add.obj のシンボルのアドレスが適用されるため、テーブル内の Add シンボルを検索しますが、検査の結果、test.obj 内の Add アドレスが役に立たないことが判明したため、シンボル テーブルはマージされます。つまり、プログラムが Add 関数を呼び出す必要がある場合、プログラムはマージされます。アドレス 0x 500 に移動して関数を見つけます。
次に、リンカはこれらのオブジェクト ファイルと、プログラムによって参照されるライブラリ内のいくつかのファイルをリンクして、実行可能プログラム test.exe を形成します。
ここに画像の説明を挿入
ここに画像の説明を挿入

2. C言語の前処理

1. C言語での組み込みの定義済みシンボル

これらの事前定義されたシンボルは C 言語に組み込まれており、これらのシンボルを直接使用して必要な情報を出力できます。
ここに画像の説明を挿入
ここに画像の説明を挿入
現在のステートメントの実行に関する情報は、これらの事前定義されたシンボルを使用して取得できます。
ここに画像の説明を挿入

2. #define は識別子を定義します

上記の定義済みシンボルに加えて、C 言語では #define を使用した識別子の定義もサポートされています。
例えば、100を置換するようにMAXを定義した場合、以下のプログラムでMAXを使用すると、前処理の段階で100に置換されます また、#defineでは数値以外にも文字、型、コードなどを定義することができます。

#define MAX 100
#define STR "abcdef"
#define FAC fac()  //调用函数
//如果定义的语句过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s line:%d" \
							"date:%s  time:%s\n",\
							__FILE__, __LINE__,\
							__DATE__,__TIME__)

void fac()
{
    
    
	printf("hh\n");
}
int main()
{
    
    
	int a = MAX;
	printf("%d\n", a);
	printf("%d\n", MAX);
	FAC;
	DEBUG_PRINT;
	return 0;
}

test.c 内のコメントと #define で定義された識別子が前処理段階で削除され、#define で定義された識別子が次のステートメントに直接置き換えられていることがわかります。したがって、 #define を使用して識別子を定義した後は、置換時に ; 文字も追加され、ステートメントに 2 つの ; ; ステートメント終了文字が含まれるため、ステートメントの後に ; 文字を追加する必要はありません。
ここに画像の説明を挿入

3、#define定義マクロ

#define メカニズムには、テキストにパラメータを置換できるようにする機能が含まれており、この実装はマクロ (macro) またはマクロ定義 (definemacro) と呼ばれることがよくあります。
#define name( parament-list ) math ここで、 parament-list は、stuff 内に現れる可能性のあるシンボルのカンマ区切りのリストです。
知らせ: パラメータ リストの左括弧は名前に隣接する必要があります。2 つの間に空白が存在する場合、引数リストは引数の一部として解釈されます。
次の状況を避けるために、マクロを定義するときはできるだけ括弧を使用する必要があります。
ここに画像の説明を挿入
数値式の評価に使用されるすべてのマクロ定義は、マクロの使用時に引数内の演算子または隣接する演算子間の予期しない相互作用を避けるために、この方法で括弧で囲む必要があります。

#define SQUARE(x) ((x)*(x)) 
#define DOUBLE(x) ((x)+(x))

int main()
{
    
    
	int a = 9;
	int r = SQUARE(a);  //81
	//经过预处理后,等价于 int r = ((a) * (a));
	printf("%d\n", r);

	int z = SQUARE(a + 1);  //100
	//经过预处理后,等价与 int z = ((a + 1) * (a + 1));
	printf("%d\n", z);

	int ret = 3 * DOUBLE(100); //600
	//经过预处理后,等价于 int ret = 3 * ((100) + (100))
	printf("%d\n", ret);


	return 0;
}

4. #置換ルールを定義する

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

  1. マクロを呼び出すときは、最初に引数がチェックされ、#define で定義されたシンボルが含まれているかどうかが確認されます。「はい」の場合、最初に置き換えられます。
  2. 置換テキストは、元のテキストの代わりにプログラムに挿入されます。マクロの場合、パラメータ名はその値に置き換えられます。
  3. 最後に、結果のファイルが再度スキャンされ、#define で定義されたシンボルが含まれているかどうかが確認されます。「はい」の場合は、上記の処理を繰り返します。
    知らせ:
  4. 他の #define 変数は、マクロ パラメーターおよび #define 定義に使用できます。しかし、マクロでは再帰は発生しません。
  5. プリプロセッサが #define で定義されたシンボルを検索する場合、文字列定数の内容は検索されません。
#define ADD(x,y) ((x)+(y))
#define MAX 100

int main()
{
    
    
	//会先将MAX替换为100,然后再将ADD(2,100)替换为 ((2)+(100))
	int ret = ADD(2, MAX);
	printf("%d\n", ret);

	//字符串里面的MAX不会被替换为100
	printf("MAX = %d\n", MAX);
	return 0;
}

5. #define を使用してマクロを定義する場合、# と ## の役割

# を使用して、マクロ パラメータを対応する文字列に変換します。

//宏可以做到这样的操作
//参数前面加上#就表示直接替换为N这个字符,而不是N的值
#define PRINT(N,format) printf(#N" = "format"\n",N)

//函数不能将n改变为其他字符
void print(int n)
{
    
    
	printf("n = \n", n);
}
int main()
{
    
    
	int a = 10;
	double b = 3.14;
	//当我们想要输出这样的语句时,函数做不到,但是宏可以做到
	/*printf("a = %d\n", a);
	printf("b = %lf\n", b);*/
	PRINT(a,"%d");
	PRINT(b,"%lf");

	return 0;
}

## 両側のシンボルを 1 つのシンボルに結合できます。これにより、マクロ定義で分離されたテキストの断片から識別子を作成できます。

#define CAT(name,num) name##num

int main()
{
    
    
	int class101 = 101;
	printf("%d\n", CAT(class, 101));
	//在预处理时,会变为 printf("%d\n", class101)
	//##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

	return 0;
}

6. 副作用のあるマクロパラメータ

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

x+1;//不带副作用
x++;//带有副作用

つまり、マクロの実行中にパラメータの値が変更されます。

#define MAX(x,y) ((x)>(y)?(x):(y))

int main()
{
    
    
	int a = 5;
	int b = 8;
	int c = MAX(a++, b++);
	//在预处理后变为
	//int c = ((a++) > (b++) ? (a++) : (b++));

	printf("%d\n", a);
	printf("%d\n", b);
	printf("%d\n", c);

	return 0;
}

7. マクロと関数の比較

マクロは通常、単純な操作を実行するために使用されます。たとえば、2 つの数値のうち大きい方を見つけます。
#define MAX(a, b) ((a)>(b)?(a):(b)) では、
このタスクに関数を使用しないのはなぜでしょうか。理由は 2 つあります。
1. 関数の呼び出しと関数からの戻りに使用されるコードは、この小さな計算作業を実際に実行するよりも時間がかかる可能性があります。したがって、プログラムのサイズと速度の点では、マクロの方が関数よりも優れています。
2. さらに重要なのは、関数のパラメータを特定の型として宣言する必要があることです。したがって、関数は適切な型の式でのみ使用できます。逆に、このマクロは、> で比較できる整数、長整数、浮動小数点型などの型にどのように適用できますか。マクロはタイプに依存しません。
もちろん、マクロと比較すると、関数には欠点もあります。
1. マクロが使用されるたびに、マクロによって定義されたコードがプログラムに挿入されます。マクロが比較的短い場合を除き、プログラムの長さが大幅に長くなる可能性があります。
2. マクロはデバッグできません。
3. マクロはタイプに依存しないため、厳密さが十分ではありません。
4. マクロはオペレータの優先順位の問題を引き起こす可能性があり、それによりプログラムでエラーが発生する可能性があります。

マクロは、関数ではできないことを実行できることがあります。たとえば、マクロパラメータには型を指定できますが、関数には型を指定できません。

#define MALLOC(num,type) (type*)malloc(num*sizeof(type))

int main()
{
    
    
	int* p = (int*)malloc(10 * sizeof(int));
	int* p2 = MALLOC(10, int);
	//int* p2 = (int*)malloc(10 * sizeof(int));
	return 0;
}

命名規則:すべてのマクロ名と関数名を大文字にしないでください。

8、#undef

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

#define MAX 100
int main()
{
    
    
	int a = MAX;
	printf("%d\n", a);
	printf("%d\n", MAX);
#undef MAX 
	//此时MAX已经不再为定义的标识符
	printf("%d\n", MAX);

	return 0;

}

3. 条件付きコンパイル

1. 一般的な条件付きコンパイル命令

プログラムをコンパイルするときに、ステートメント (ステートメントのグループ) をコンパイルまたは破棄したい場合に非常に便利です。条件付きコンパイルディレクティブがあるからです。
1.#if

#if 常量表达式
 //... 
#endif 
//常量表达式由预处理器求值。
如:
#define __ DEBUG __ 1 
#if __ DEBUG __ 
 //.. 
#endif 
int main()
{
    
    
	int arr[10] = {
    
     0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		arr[i] = i + 1;
//如果#if 后面的常量表达式为真,则里面的语句会参与编译
#if 1
		printf("%d ", arr[i]);
#endif
	}

	return 0;
}

2. 複数分岐の条件付きコンパイル

 //... 
#elif 常量表达式
 //... 
#else 
 //... 
#endif 
#define NUM 1
int main()
{
    
    
	//只有满足条件的语句才会编译,而不满足条件的语句在预处理阶段就被去除了
#if NUM==1
	printf("hehe\n");
#elif NUM==2
	printf("haha\n");
#else
	printf("heihei\n");
#endif

	return 0;
}

3. 定義されているかどうかを確認します

#if defined(symbol) 
#ifdef symbol 
#if !defined(symbol) 
#ifndef symbol 
#define MAX 0
int main()
{
    
    
	//如果使用#define定义了MAX标识符就为真
#if defined(MAX)
	printf("defined\n");
#endif

#if !defined(MAX)
	printf("!defined\n");
#endif

//下面两组和上面两组等价
#ifdef MAX
	printf("ifdef\n");
#endif

#ifndef MAX
	printf("ifndef\n");
#endif

	return 0;

}

4. ネストされた命令

#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 
#define OPTION1 1

#define FAC1 1
#define FAC2 2
#define FAC3 3

int main()
{
    
    
#if defined(OPTION1)
	#ifdef FAC1
	printf("FAC1");
    #endif
#else 
	#ifdef FAC2
	printf("FAC2");
	#endif
#endif



	return 0;
}

第 4 に、ファイルには以下が含まれます

#include ディレクティブによって別のファイルがコンパイルされる可能性があることはすでにわかっています。#include ディレクティブに実際に表示される場所と同じです。この置換の仕組みは簡単です。プリプロセッサはまずディレクティブを削除し、それをインクルード ファイルの内容に置き換えます。このようなソース ファイルは 10 回インクルードされ、実際には 10 回コンパイルされます。

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

ローカルファイルには以下が含まれます

#include "filename" 

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

#include <filename.h>

ヘッダー ファイルを見つけるには、標準パスに直接移動して見つけます。見つからない場合は、コンパイル エラーが表示されます。
上記のことから、ライブラリファイルの場合は「 」を使用してインクルードすることもできますが、検索効率は低くなりますし、当然ながらライブラリファイルなのかローカルファイルなのかを区別するのは容易ではありません。
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/dong132697/article/details/132498637
おすすめ