技術共有

【C言語】前処理の詳細解説(その2)

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

序文

最終号で【C言語】前処理の詳細解説(その2)学習中に、前処理におけるマクロの関連知識を詳しく紹介しました。誰もが多くのことを得ることができると思います。心配しないで、この号で前処理に関するその他の知識を学びましょう。

7. # と ##

7.1 # 演算子

  • # 运算符マクロの引数を文字列リテラルに変換します。パラメータを含むマクロの置換リストに含めることができます。
  • # 运算符実行される操作は「文字列化」として理解できます。

それはどういう意味ですか?
まず前置きをしておきます。

int mian()
{
	printf("hello"   "worldn");
	printf("helloworldn");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上記の 2 行のコードの違いは何ですか?一緒に見てみましょう:

ここに画像の説明を挿入します

ご覧のとおり、2 つの文字列と 1 つの文字列の効果は同じです。C语言会把两个字符串天然连成一个字符串、途中にスペースを追加しても意味がありません。

さて、次のようなシーンがあります。

int main()
{
	int a = 1;
	printf("The value of a is %dn", a);

	int b = 20;
	printf("The value of b is %dn", b);

	float f = 8.5f;
	printf("The value of f is %fn", f);

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3 行のコードのロジックは非常に複雑であることがわかりました。相像はい、でもあります些许不同
  
そこで、とても似ているので、これらを入れてもいいだろうかと考えました。封装成一个函数、使いやすさのためですか?
しかし、関数ではこの機能を実行できません。
それではどうすればいいでしょうか?
マクロを使用してそれを解決してみることができます

#define Print(n, format) printf("The value of n is " format "n", n)

int main()
{
	int a = 1;
	Print(a, "%d");
	//printf("The value of a is %dn", a);

	int b = 20;
	Print(b, "%d");
	//printf("The value of b is %dn", b);

	float f = 8.5f;
	Print(f, "%f");
	//printf("The value of f is %fn", f);

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

  
操作結果:

ここに画像の説明を挿入します

私たちは発見します んん 変更されていないのですが、どのように変更すればよいでしょうか?
ここで # 演算子を使用する必要があります。# マクロの引数を次のように変換します。文字列リテラル、今すぐ んん なる 「ん」 「ん」

このとき、スプライシング方法を使用すると、次のようになります。

#define Print(n, format) printf("The value of " #n " is " format "n", n)
  • 1

  
分かりませんか?以下の説明を読めば理解できると思います。
ここに画像の説明を挿入します

  

7.2 ## 演算子

  ## 彼の中に見つけることができる両側の記号が 1 つの記号に結合されます、マクロ定義が可能になります从分离的文本片段创建标识符## と呼ばれますマークのり付け 。このような接続は、合法それ以外の場合、結果は未定義です。
  
2 つの数値の大きい方の値を見つける関数を作成するには、異なるデータ型に対して異なる関数を作成する必要があると前に述べました。

int int_max(int x, int y)
{
	return x > y ? x : y;
}
float float_max(float x, float y)
{
	return x > y ? x : y;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  

ここに画像の説明を挿入します

これは必然的に非常に煩雑になり、2 つの関数には多くの類似点があります。
このような関数をすばやく作成する方法はありますか?関数のように同じです、関数を適用するだけで出てきます
  
このように書くことができます

#define GENERIC_MAX(type) 
		type type##_max(type x, type y)
		{
			return x > y ? x : y;
		}
  • 1
  • 2
  • 3
  • 4
  • 5

  

#define GENERIC_MAX(type) 
		type type##_max(type x, type y)
		{
			return x > y ? x : y;
		}

GENERIC_MAX(int);    //相当于定义了一个函数int_max
GENERIC_MAX(float);  //相当于定义了一个函数float_max

int main()
{
	int r1 = int_max(3, 5);
	printf("%dn", r1);
	float r2 = float_max(2.3f, 7.6f);
	printf("%fn", r2);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

  
操作結果:

ここに画像の説明を挿入します

  
私たちもできます gccgcccc 環境内で前処理された結果を観察する.i ファイルについてより直感的に理解できるようになります

ここに画像の説明を挿入します

もちろん、この方法で生成された関数はデバッグにも不便です。

それではここで ## どのような役割を果たしますか?

追加 ##、コンパイラはそれらがシンボルであるとみなします。
カンガを見てみましょう## 効果:

ここに画像の説明を挿入します

  

8. 命名規則

一般に、関数とマクロを使用するための構文は非常に似ているため、语言本身没法帮我们区分二者
  
私たちの普段の習慣の 1 つは次のとおりです。

  • マクロに名前を付けるすべて大文字
  • 関数名すべて大文字を使用しないでください

もちろん、これらの命名規則は絶対的なものではありません
例えば オフセット オフセットofft このマクロはすべて小文字で書かれています
  
注記 オフセット オフセットofft これは、構造体の開始位置に対する構造体メンバーのオフセットを計算するために使用されます。
  

九、# 未定義 未定義あなたde

  # 未定義 未定義あなたde 指示は次の目的で使用されます。マクロ定義を削除する

ここに画像の説明を挿入します
上記のコードは 169 行目で使用されています# 未定義 未定義あなたde マクロMAXを削除しました。削除前の 168 行を呼び出す場合は問題ありませんが、削除後の 170 行を呼び出すとエラーが報告されます。

  

10. コマンドライン定義

多くの C コンパイラ (VS を除く) は、コマンド ラインでシンボルを定義する機能を提供します。コンパイルプロセスを開始するために使用されます
  
  例えば : この機能は、同じソース ファイルに基づいてプログラムの異なるバージョンをコンパイルする場合に便利です。 (プログラム内で特定の長さの配列が宣言されているとします。マシンのメモリが限られている場合は非常に小さな配列が必要ですが、別のマシンのメモリが大きければ、必要な配列はさらに大きくなる可能性があります)

ここに画像の説明を挿入します

ここに画像の説明を挿入します

コマンドライン定義は次のとおりです。前処理段階上記のコードの前処理段階で処理されます すず すずサイズ の値が決定されました

  

11. 条件付きコンパイル

プログラムをコンパイルするとき、1 つ (ステートメントのグループ) をコンパイルしたり破棄したりするのは非常に困難です。方便の。使えるから条件付きコンパイルディレクティブ
  
条件付きコンパイル命令はこのコードです我想让你编译就编译,不想让你编译你就不要编译了 。条件を設定できます。条件が true の場合、このコードはコンパイルに参加します。条件が false の場合、このコードはコンパイルされません。
  
例えば:
デバッグ コードを削除するのは残念ですが、残しておくのは邪魔なので、選択的にコンパイルすることができます。
  

ここに画像の説明を挿入します

  
一般的に使用される条件付きコンパイル命令:

11.1. 単一ブランチの条件付きコンパイル

#if 常量表达式
    //···
#endif
  • 1
  • 2
  • 3

ここに画像の説明を挿入します

  

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

#if  常量表达式
	//···
#elif  常量表达式
	//···
#else
	//···
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

どのステートメントが true であれば、そのステートメントが実行されます

#define M 1
int main()
{
#if M == 0
	printf("hellon");
#elif M == 1
	printf("worldn");
#elif M == 2
	printf("csdnn");
#endif
	printf("886n");

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

  

11.3. 定義されているかどうかを確認する

#if defined(symbol)
#ifdef symbol

//上面两个的反面
if !defined(symbol)
#ifndef symbol
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ここに画像の説明を挿入します

  

11.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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

  

12. ヘッダーファイルのインクルード

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

(1) ローカルファイルの組み込み方法

# include "filename"
  • 1

検索戦略: 最初にソースファイルが配置されているプロジェクトディレクトリヘッダー ファイルが見つからない場合、コンパイラは同様の方法でライブラリ関数ヘッダー ファイルを検索します。標準的な位置検索ヘッドファイル
また見つからなかったらコンパイルエラー
  
Linux リナックスux 環境の標準ヘッダー ファイル パス (ヘッダー ファイルが配置される場所):

 /usr/include
  • 1

VS 環境の標準ヘッダー ファイル パス:

C:Program Files (x86)Microsoft Visual Studio 12.0VCinclude
//这是VS2013的默认路径
  • 1
  • 2

  

(2) ライブラリファイルには次の内容が含まれます。

#include <filename.h>
  • 1

ヘッダー ファイルを見つけて、次の場所に直接移動します。標準パス検索して見つからない場合は、プロンプトが表示されます。コンパイルエラー
  
ということはライブラリファイルにも使えるということでしょうか?“ ” フォームには次の内容が含まれます
答えはアフィムはい、ただし検索はこの方法で行われます効率が低い、もちろんこれも当てはまります区別するのは簡単ではありませんライブラリファイルですか、それともローカルファイルですか?
  

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

これまでの内容 (コンパイルとリンク) を検討した結果、前処理段階でヘッダー ファイルがインクルードされることがわかりました。直接将该文件的代码拷贝到包含头文件的地方
  
ヘッダー ファイルが 10 回インクルードされると、実際には 10 回コンパイルされることになります。繰り返しインクルードされると、コンパイルの負荷が大きくなります。

テスト.c test.ctest.c

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

テスト .h テスト.htest.h

void test();
struct Stu
{
int id;
char name[20];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

しかし、プロジェクトでは必然的にファイルが複数回インクルードされるため、この問題をどのように解決するのでしょうか?
答え:条件付きコンパイル

#ifndef __TEST_H__
#define __TEST_H__
		//头文件的内容
#endif
  • 1
  • 2
  • 3
  • 4

  
どのように理解すればよいでしょうか?

  • 初めてヘッダファイルをインクルードする場合、コンパイルする必要がありますか?最初に判断する
  • シンボル __TEST_H__ は、定義されていません欲しいコンパイル
  • その直後__TEST_H__ シンボルを定義する
  • 次に、ヘッダー ファイルを再度インクルードし、__TEST_H__ を見つけます。定義されていますもはや後でインクルードされるヘッダー ファイルについては、次の操作を実行します。コンパイル

ただし、上記の書き方はさらに面倒です。次のような書き方もあります。

#pragma once
  • 1

効果は上記の方法と同じです
これにより、ヘッダー ファイルの繰り返しの導入が回避されます。
  

13. その他の前処理命令

#error
#pragma
#line
···
#pragma pack()//在结构体部分介绍
  • 1
  • 2
  • 3
  • 4
  • 5

興味のある友達は読んでみてください 「C言語の徹底解剖」
  
  
  
  
  


  さて、今回の前処理に関する知識は以上です。このブログがお役に立てば幸いです。同時に、間違いがあれば修正して、一緒に C 言語学習の道を進んでいきましょう。