宏の妙用
9232 ワード
[TOC]
へんちょうはいれつ
厳密に言えば,長くなる配列の実現はc++において面倒なことではない.Stlのvector自体が長くなる配列であり、メモリを自動的に管理する能力があります.しかしcでは,長くなる配列を実現するのはやや面倒である.Cで実現するには、必然的に1つの構造が必要であり、構造の中に1つのポインタが必要であり、ポインタはメモリ空間を割り当て、空間の大きさは必要に応じて異なり、また別のフィールド記録がどれだけの空間を開いたかを記録しなければならない.
以下に大まかに説明する.
問題ありませんが、C言語の使用者が最大の誇りを持っているのは、効率、空間の使用をコントロールすることです.count=0ならpは必要なく、4(64ビットシステムは8)バイトの空間を無駄に占めているという疑問がある.それはもっと良い方法で上の需要を実現することができて、また空間が合理的であることを保証することができますか?答えはあります.0の長さで.次のようになります.
上記の構造の使用方法と一致するがsizeofでそのサイズを読み取ることを試みることができ,countフィールドの長さ4バイトしかなく,pには割り当てられた空間がないことが分かった.完璧!
宏の妙用
1. do{...}while(0)
多くのCプログラムでは、それほど直接的ではないように見える特殊なマクロ定義がたくさん見られるかもしれません.次に例を示します.
Linuxカーネルや他の有名なCライブラリではdo{...}が多く使用されています.while(0)のマクロ定義.このマクロの用途は何ですか.何のメリットがありますか?GoogleのRobert Love(以前Linuxカーネル開発に従事していた)は、次のように答えています.
do{...}while(0)はCの唯一のコンストラクタで、定義したマクロを常に同じ方法で動作させ、マクロ(特に呼び出しマクロをカッコで囲んでいない文)をどのように使用しても、マクロの後ろのセミコロンは同じ効果です.
この言葉は少し拗ねて聞こえるかもしれませんが、実用的な言葉はdo{...}を使うことです.while(0)構築後のマクロ定義は、カッコやセミコロンなどの影響を受けず、常に所望の方法で実行を呼び出します.同時にほとんどのコンパイラがdo{...}を認識できるためwhile(0)という無駄なループを最適化するので,この方法を用いるとプログラムの性能が低下することはない.
例:
次のように呼び出すことができます.
マクロは次のように拡張されます.
これは確かに私たちが望んでいる正しい出力です.このように呼び出すと、次のようになります.
拡張すると、あなたが望んでいる結果ではないかもしれません.次の文が拡張されます.
明らかに、これは間違いであり、みんながよく犯す間違いの一つでもある.
do{...}を使うwhile(0)goto文の消去
通常、1つの関数でリソースの割り当てを開始し、途中で実行中にエラーが発生した場合に関数を終了します.もちろん、終了する前にリソースを解放するコードは次のようになります.
ここで最大の問題はコードの冗長性であり、操作を増やすたびに、対応するエラー処理が必要で、非常に柔軟ではありません.そこで私たちはgotoを思い浮かべました
コード冗長性は解消されましたが、C++の中で身分の微妙なgoto文を導入しました.gotoを正しく使うことでプログラムの柔軟性と簡潔性を大幅に向上させることができますが、あまり柔軟なものは危険で、私たちのプログラムを混乱させます.では、どのようにgoto文を避け、コード冗長性を解消することができますか.doを見てください.while(0)サイクル:
2.「#」記号
「#」記号は、1つの記号を文字列に直接変換するために使用されます.たとえば、
strが指す内容は「test」であり、つまりマクロ定義では#がその後の記号に二重引用符を直接付ける.この特性はC++の反射の実現に極めて便利を提供した.
3.“##”記号
2つの記号を接続して、新しい語法(語法階層)を生成します.たとえば、次のようにします.
マクロが展開されると、int INT_となります.1; をハイフネーションと見なすことができ、ハイフネーションは新しいシンボルの生成に便利です.GoogleのGtestフレームワークは、ハイフンを巧みに駆使して新しいテストケースを生成しています.
4.パラメトリックマクロ
_VA_ARGS_システム定義のマクロで、パラメータリストに自動的に置き換えられ、フォーマット出力が必要になることがよくあります.再定義する場合は、以上のテクニックを使用します.
Cのいくつかの特殊なマクロは
5.マクロパラメータのprescan
prescanの定義は、1つのマクロパラメータがマクロボディに格納されると、このマクロパラメータはまずすべて展開され、展開後のマクロパラメータがマクロボディに格納されると、プリプロセッサは新しい展開されたマクロボディを2回スキャンし、展開を継続する.
ADD PARAM(1)はPARAMとしてのマクロパラメータであるため、ADD PARADM(1)をINT_に展開する1、それからINT_1 PARAMに入れますが、例外としてPARAMマクロにマクロパラメータに「#」または「###」を使用した場合、マクロパラメータは展開されません.
PARAM( ADDPARAM( 1 ) ); ADDPARAM(1)に展開される.この場合、INT_1の結果を得るには、中間マクロを追加する必要があります.
PARAM( ADDPARAM( 1 ) );このときの結果は「INT_1」となります.prescanの原則に従って、ADDPARAM(1)が入ってくると、展開してINT_を得る1、INT_1 PARAM 1マクロを持ち込み、最終的に「INT_1」の結果を得る.
6.C++のインタフェースマクロ
C++の目標の一つはクラスの宣言と定義を分離することであり、これはプロジェクトの開発に極めて有利である--これは開発者がクラスの実現を見なくてもクラスの機能を知ることができる.しかし、C++はクラスの宣言とクラス定義の分離を実現する方法で、非インライン関数の各表現を2回書く必要があります.1回はクラス宣言で、1回はクラス定義で書きます.コードは次のとおりです.
Tickの識別は両方に現れたので,この方法のパラメータを変更する必要がある場合(関数名,戻りタイプ,constを加える)は,両方を変更する必要がある.もちろん通常は仕事量はありませんが、この特性は多くの面倒をもたらす場合があります.例えば、BaseClassというベースクラスがある場合、BaseClassから継承された3つのサブクラス、D 1、D 2、D 3がある.ここで、BaseClassは虚関数Foo()を宣言し、デフォルトの実装があり、D 1、D 2、D 3にはFoo()関数が再ロードされている.今、BaseClass::Foo()にパラメータを追加したとしたら、D 3に対応する修正をするのを忘れました.ご迷惑をおかけしました.コンパイルはパスできます.コンパイラはBaseClass::Foo(...)とD 3::Foo()をまったく異なる関数と見なします.D 3のFooを虚関数機構で呼び出そうとすると,いくつかの問題が起こりやすい.UE 4では、AActorクラスから継承されたクラスだけで千以上あり、AActorクラスを修正する必要がある場合は、従来の方法を使用すると、千以上の派生クラスに対して修正を行い、もし1つの派生クラスが修正されていなければ、コンパイラはエラーを報告しません.このように、理想的には、1つの関数の表現が1つの場所にしか存在しないことを望んでいます.BaseClass::Foo()を1回だけ宣言し、派生クラスでFooを追加宣言しなくてもいいと言えばいいのです.また効率の面では,C++で継承を使用する場合,浅い階層のクラス継承関係を多く使用することが多く,親クラスには子クラスが山積みになることが多い.多くの場合、相互に関連しない多くの機能を個別のクラス継承ファミリーに統合する必要があります.浅い継承では、最初の親をインタフェースとして宣言するだけです.つまり、いくつかの虚関数(ほとんどが純虚関数)を宣言します.多くの場合、私たちはこのクラスの家族の中にベースクラスと残りの派生クラスがあります.ベースクラスに10個の関数があり、このベースクラスから20個のクラスが派生した場合、200個の関数宣言を追加する必要があります.しかし、これらの宣言の目的は、Implementベースクラスのメソッドのためにあることが多く、ヘッダファイルのメンテナンスが容易ではありません.従来の方法の実装は,Animalのクラスがあり,このクラスはベースクラスと見なされ,このベースクラスから異なるサブクラスを派生させることを望んでいる.以下に示すように、Animalには3つの純粋な必要関数があります.
また、このベースクラスには、Monkey、Tiger、Lionの3つの派生クラスがあります.では、私たちの3つの方法のそれぞれは7つの場所に存在します.Animalでは、Monkey、Lion、Tigetの宣言と定義が1回ずつあります.次に、GetPositionとGetVelocityの戻りタイプをTransform変換に適応するためにVector 4 fに変更したいと仮定します.7つの場所で修正します.Animalです.hファイル、Lion、Tiger、Monkeyの.hファイルと.cppファイル.マクロを用いた実装には,これらの方法をパッケージ化し,いわゆるインタフェースマクロの形式に変更する優れた処理方法がある.試してみましょう
特筆すべきは、##記号は接続を表し、記号は次の行をつなぐことを表す.これらのマクロにより、Animalの宣言を大幅に簡略化し、それから派生したクラスの宣言をすべて簡略化することができます.
へんちょうはいれつ
厳密に言えば,長くなる配列の実現はc++において面倒なことではない.Stlのvector自体が長くなる配列であり、メモリを自動的に管理する能力があります.しかしcでは,長くなる配列を実現するのはやや面倒である.Cで実現するには、必然的に1つの構造が必要であり、構造の中に1つのポインタが必要であり、ポインタはメモリ空間を割り当て、空間の大きさは必要に応じて異なり、また別のフィールド記録がどれだけの空間を開いたかを記録しなければならない.
以下に大まかに説明する.
struct MutableLenArray
{
Int count;
Char *p;
}
P = new Char[Count];
問題ありませんが、C言語の使用者が最大の誇りを持っているのは、効率、空間の使用をコントロールすることです.count=0ならpは必要なく、4(64ビットシステムは8)バイトの空間を無駄に占めているという疑問がある.それはもっと良い方法で上の需要を実現することができて、また空間が合理的であることを保証することができますか?答えはあります.0の長さで.次のようになります.
Struct MutableLenArray
{
Int count;
Char p[0];
};
上記の構造の使用方法と一致するがsizeofでそのサイズを読み取ることを試みることができ,countフィールドの長さ4バイトしかなく,pには割り当てられた空間がないことが分かった.完璧!
宏の妙用
1. do{...}while(0)
多くのCプログラムでは、それほど直接的ではないように見える特殊なマクロ定義がたくさん見られるかもしれません.次に例を示します.
#define __set_task_state(tsk,state_value) \
do{(tsk)->state = (state_value); }while(0)
Linuxカーネルや他の有名なCライブラリではdo{...}が多く使用されています.while(0)のマクロ定義.このマクロの用途は何ですか.何のメリットがありますか?GoogleのRobert Love(以前Linuxカーネル開発に従事していた)は、次のように答えています.
do{...}while(0)はCの唯一のコンストラクタで、定義したマクロを常に同じ方法で動作させ、マクロ(特に呼び出しマクロをカッコで囲んでいない文)をどのように使用しても、マクロの後ろのセミコロンは同じ効果です.
この言葉は少し拗ねて聞こえるかもしれませんが、実用的な言葉はdo{...}を使うことです.while(0)構築後のマクロ定義は、カッコやセミコロンなどの影響を受けず、常に所望の方法で実行を呼び出します.同時にほとんどのコンパイラがdo{...}を認識できるためwhile(0)という無駄なループを最適化するので,この方法を用いるとプログラムの性能が低下することはない.
例:
#define foo(x) bar(x); baz(x)
次のように呼び出すことができます.
foo(wolf);
マクロは次のように拡張されます.
bar(wolf); baz(wolf);
これは確かに私たちが望んでいる正しい出力です.このように呼び出すと、次のようになります.
if (!feral)
foo(wolf);
拡張すると、あなたが望んでいる結果ではないかもしれません.次の文が拡張されます.
if (!feral)
bar(wolf);
baz(wolf);
明らかに、これは間違いであり、みんながよく犯す間違いの一つでもある.
do{...}を使うwhile(0)goto文の消去
通常、1つの関数でリソースの割り当てを開始し、途中で実行中にエラーが発生した場合に関数を終了します.もちろん、終了する前にリソースを解放するコードは次のようになります.
bool Execute()
{
//
int *p = new int;
bool bOk(true);
//
bOk = func1();
if(!bOk)
{
delete p;
p = NULL;
return false;
}
bOk = func2();
if(!bOk)
{
delete p;
p = NULL;
return false;
}
bOk = func3();
if(!bOk)
{
delete p;
p = NULL;
return false;
}
// ..........
// ,
delete p;
p = NULL;
return true;
}
ここで最大の問題はコードの冗長性であり、操作を増やすたびに、対応するエラー処理が必要で、非常に柔軟ではありません.そこで私たちはgotoを思い浮かべました
bool Execute()
{
//
int *p = new int;
bool bOk(true);
//
bOk = func1();
if(!bOk) goto errorhandle;
bOk = func2();
if(!bOk) goto errorhandle;
bOk = func3();
if(!bOk) goto errorhandle;
// ..........
// ,
delete p;
p = NULL;
return true;
errorhandle:
delete p;
p = NULL;
return false;
}
コード冗長性は解消されましたが、C++の中で身分の微妙なgoto文を導入しました.gotoを正しく使うことでプログラムの柔軟性と簡潔性を大幅に向上させることができますが、あまり柔軟なものは危険で、私たちのプログラムを混乱させます.では、どのようにgoto文を避け、コード冗長性を解消することができますか.doを見てください.while(0)サイクル:
bool Execute()
{
//
int *p = new int;
bool bOk(true);
do
{
//
bOk = func1();
if(!bOk) break;
bOk = func2();
if(!bOk) break;
bOk = func3();
if(!bOk) break;
// ..........
}while(0);
//
delete p;
p = NULL;
return bOk;
}
2.「#」記号
「#」記号は、1つの記号を文字列に直接変換するために使用されます.たとえば、
#define TO_STRING(x) #x
const char * str=TO_STRING(test);
strが指す内容は「test」であり、つまりマクロ定義では#がその後の記号に二重引用符を直接付ける.この特性はC++の反射の実現に極めて便利を提供した.
3.“##”記号
2つの記号を接続して、新しい語法(語法階層)を生成します.たとえば、次のようにします.
#define SIGN(x) INT_##x
int SIGN(1);
マクロが展開されると、int INT_となります.1; をハイフネーションと見なすことができ、ハイフネーションは新しいシンボルの生成に便利です.GoogleのGtestフレームワークは、ハイフンを巧みに駆使して新しいテストケースを生成しています.
4.パラメトリックマクロ
#define LOG(format,...) printf(format,__VA_ARGS__)
LOG("%s %d",str,count);
#define LogE(format, ...) usb_logWrite('e', __FILE__, __LINE__, format, ##__VA_ARGS__)
// __VA_ARGS__ ##__VA_ARGS__
#define LogD(format, ...) usb_logWrite('d', __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LogI(format, ...) usb_logWrite('i', __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LogW(format, ...) usb_logWrite('w', __FILE__, __LINE__, format, ##__VA_ARGS__)
#define LogO(format, ...) usb_logWrite('o', __FILE__, __LINE__, format, ##__VA_ARGS__)
extern int usb_logWrite(char level,char *fileName, int lineNum, const char * format, ...);
int usb_logWrite(char level,char *fileName, int lineNum, const char * format, ...)
{
int n, m;
char logWriteBuf[BUF_SIZE];
_init_fd();
if(fileName != NULL) {
char *rf = strrchr(fileName, '/');
if(rf != NULL) fileName = rf+1;
}
n = snprintf(logWriteBuf,BUF_SIZE,"%%%%",\
level,Id,name,fileName,lineNum);
va_list argList;
va_start(argList,format);
m = vsnprintf(logWriteBuf+n,BUF_SIZE-n,format,argList);
va_end(argList);
if(m <= 0) {
m = snprintf(logWriteBuf+n,BUF_SIZE-n, "format error
");
}
n += m;
if(n > BUF_SIZE-2) n = BUF_SIZE-2;
logWriteBuf[n++] = '#';
logWriteBuf[n++] = '#';
write(log_fd, logWriteBuf, n);
return n;
}
_VA_ARGS_システム定義のマクロで、パラメータリストに自動的に置き換えられ、フォーマット出力が必要になることがよくあります.再定義する場合は、以上のテクニックを使用します.
Cのいくつかの特殊なマクロは
__FUNCTION__、__DATE__、__FILE__、__LINE__、__STDC__、__TIME__、__TIMESTAMP__
もあります5.マクロパラメータのprescan
prescanの定義は、1つのマクロパラメータがマクロボディに格納されると、このマクロパラメータはまずすべて展開され、展開後のマクロパラメータがマクロボディに格納されると、プリプロセッサは新しい展開されたマクロボディを2回スキャンし、展開を継続する.
#define PARAM(x) x
#define ADDPARAM(x) INT_##x
PARAM(ADDPARAM(1))
ADD PARAM(1)はPARAMとしてのマクロパラメータであるため、ADD PARADM(1)をINT_に展開する1、それからINT_1 PARAMに入れますが、例外としてPARAMマクロにマクロパラメータに「#」または「###」を使用した場合、マクロパラメータは展開されません.
#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) ); ADDPARAM(1)に展開される.この場合、INT_1の結果を得るには、中間マクロを追加する必要があります.
#define PARAM(x) PARAM1(x)
#define PARAM1( x ) #x
PARAM( ADDPARAM( 1 ) );このときの結果は「INT_1」となります.prescanの原則に従って、ADDPARAM(1)が入ってくると、展開してINT_を得る1、INT_1 PARAM 1マクロを持ち込み、最終的に「INT_1」の結果を得る.
6.C++のインタフェースマクロ
C++の目標の一つはクラスの宣言と定義を分離することであり、これはプロジェクトの開発に極めて有利である--これは開発者がクラスの実現を見なくてもクラスの機能を知ることができる.しかし、C++はクラスの宣言とクラス定義の分離を実現する方法で、非インライン関数の各表現を2回書く必要があります.1回はクラス宣言で、1回はクラス定義で書きます.コードは次のとおりです.
// .h File
class Element
{
void Tick ();
};
// .cpp File
void Element ::Tick ()
{
// todo
}
Tickの識別は両方に現れたので,この方法のパラメータを変更する必要がある場合(関数名,戻りタイプ,constを加える)は,両方を変更する必要がある.もちろん通常は仕事量はありませんが、この特性は多くの面倒をもたらす場合があります.例えば、BaseClassというベースクラスがある場合、BaseClassから継承された3つのサブクラス、D 1、D 2、D 3がある.ここで、BaseClassは虚関数Foo()を宣言し、デフォルトの実装があり、D 1、D 2、D 3にはFoo()関数が再ロードされている.今、BaseClass::Foo()にパラメータを追加したとしたら、D 3に対応する修正をするのを忘れました.ご迷惑をおかけしました.コンパイルはパスできます.コンパイラはBaseClass::Foo(...)とD 3::Foo()をまったく異なる関数と見なします.D 3のFooを虚関数機構で呼び出そうとすると,いくつかの問題が起こりやすい.UE 4では、AActorクラスから継承されたクラスだけで千以上あり、AActorクラスを修正する必要がある場合は、従来の方法を使用すると、千以上の派生クラスに対して修正を行い、もし1つの派生クラスが修正されていなければ、コンパイラはエラーを報告しません.このように、理想的には、1つの関数の表現が1つの場所にしか存在しないことを望んでいます.BaseClass::Foo()を1回だけ宣言し、派生クラスでFooを追加宣言しなくてもいいと言えばいいのです.また効率の面では,C++で継承を使用する場合,浅い階層のクラス継承関係を多く使用することが多く,親クラスには子クラスが山積みになることが多い.多くの場合、相互に関連しない多くの機能を個別のクラス継承ファミリーに統合する必要があります.浅い継承では、最初の親をインタフェースとして宣言するだけです.つまり、いくつかの虚関数(ほとんどが純虚関数)を宣言します.多くの場合、私たちはこのクラスの家族の中にベースクラスと残りの派生クラスがあります.ベースクラスに10個の関数があり、このベースクラスから20個のクラスが派生した場合、200個の関数宣言を追加する必要があります.しかし、これらの宣言の目的は、Implementベースクラスのメソッドのためにあることが多く、ヘッダファイルのメンテナンスが容易ではありません.従来の方法の実装は,Animalのクラスがあり,このクラスはベースクラスと見なされ,このベースクラスから異なるサブクラスを派生させることを望んでいる.以下に示すように、Animalには3つの純粋な必要関数があります.
class Animal
{
public:
virtual std :: string GetName () const = 0 ;
virtual Vector3f GetPosition () const = 0;
virtual Vector3f GetVelocity () const = 0;
};
また、このベースクラスには、Monkey、Tiger、Lionの3つの派生クラスがあります.では、私たちの3つの方法のそれぞれは7つの場所に存在します.Animalでは、Monkey、Lion、Tigetの宣言と定義が1回ずつあります.次に、GetPositionとGetVelocityの戻りタイプをTransform変換に適応するためにVector 4 fに変更したいと仮定します.7つの場所で修正します.Animalです.hファイル、Lion、Tiger、Monkeyの.hファイルと.cppファイル.マクロを用いた実装には,これらの方法をパッケージ化し,いわゆるインタフェースマクロの形式に変更する優れた処理方法がある.試してみましょう
#define INTERFACE_ANIMAL(terminal) \
public: \
virtual std::string GetName() const ##terminal \
virtual IntVector GetPosition() const ##terminal \
virtual IntVector GetVelocity() const ##terminal
#define BASE_ANIMAL INTERFACE_ANIMAL(=0;)
#define DERIVED_ANIMAL INTERFACE_ANIMAL(;)
特筆すべきは、##記号は接続を表し、記号は次の行をつなぐことを表す.これらのマクロにより、Animalの宣言を大幅に簡略化し、それから派生したクラスの宣言をすべて簡略化することができます.
// Animal.h
class Animal
{
BASE_ANIMAL ;
};
// Monkey.h
class Monkey : public Animal
{
DERIVED_ANIMAL ;
};
// Lion.h
class Lion : public Animal
{
DERIVED_ANIMAL ;
};
// Tiger.h
class Tiger : public Animal
{
DERIVED_ANIMAL ;
};