19

私は最近、複雑な宣言を難読化解除するためのスパイラル ルールを学びました。これは、一連の typedef で記述されている必要があります。ただし、次のコメントは私を驚かせます。

よく引用される単純化で、いくつかの単純なケースでのみ機能します。

void (*signal(int, void (*fp)(int)))(int);「単純なケース」が見つかりません。ちなみに、これはさらに憂慮すべきことです。

それで、私の質問は、どの状況でルールを適用するのが正しく、どの状況でそれが間違っているのでしょうか?

4

4 に答える 4

21

基本的に、ルールは単純に機能しないか、スパイラルの意味を再定義することによって機能します (この場合、意味がありません。たとえば、次のように考えてください。

int* a[10][15];

スパイラル ルールでは、int の配列 [15] へのポインタの配列 [10] が与えられますが、これは誤りです。あなたが引用した場合、それも機能しません。実際、 の場合、signalどこからスパイラルを開始すべきかさえ明確ではありません。

一般に、ルールが機能する例よりも、ルールが失敗する例を見つける方が簡単です。

C++ 宣言の解析は簡単だと言いたくなることがよくありますが、複雑な宣言を試したことのある人は誰も信じないでしょう。一方で、時々言われるほど難しくはありません。秘訣は、式とまったく同じように宣言を考えることです。ただし、演​​算子ははるかに少なく、非常に単純な優先規則があります。右側のすべての演算子は、左側のすべての演算子よりも優先されます。括弧がない場合、これは、最初にすべてを右に処理し、次にすべてを左に処理し、他の式の場合とまったく同じように括弧を処理することを意味します。実際の難易度はそうではない特に関数の戻り値と関数へのポインターが関係する場合: 最初に右、次に左の規則は、特定のレベルの演算子がしばしば大きく分離されることを意味します。 :

int (*f( /* lots of parameters */ ))[10];

ここでの拡張の最後の項は ですが、完全な関数指定の後にint[10]を付けるの[10]は (少なくとも私にとっては) 非常に不自然であり、毎回停止して解決する必要があります。(これはおそらく、論理的に隣接する部分が拡大するこの傾向で、らせんの規則につながります。問題は、もちろん、括弧がない場合、それらが常に拡大するとは限らないことです[i][j]。 、その後、スパイラルではなく、再び右に進みます。)

式の観点から宣言を考えているので、式が複雑すぎて読めなくなったらどうしますか? 読みやすくするために、中間変数を導入します。宣言の場合、「中間変数」はtypedef. 特に、戻り値の型の一部が関数の引数の後に終了する場合 (および他の多くの場合も同様) はいつでもtypedef、宣言をより単純にするために a を使用する必要があると私は主張します。(ただし、これは「私が言うようにするのではなく、私が言うようにする」というルールです。残念ながら、非常に複雑な宣言を使用することがあります。)

于 2013-04-28T17:04:10.800 に答える
5

ルールは正しいです。ただし、適用には細心の注意を払う必要があります。

C99+ の宣言には、より正式な方法で適用することをお勧めします。

ここで最も重要なことは、すべての宣言の次の再帰構造を認識することです ( constvolatilestaticexterninline、は、struct簡単にするために図から削除されていますが、簡単に追加し直すことができます)。uniontypedef

base-type [derived-part1: *'s] [object] [derived-part2: []'s or ()]

はい、以上です、4 つの部分です。

where

  base-type is one of the following (I'm using a bit compressed notation):
    void
    [signed/unsigned] char
    [signed/unsigned] short [int]
    signed/unsigned [int]
    [signed/unsigned] long [long] [int]
    float
    [long] double
    etc

  object is
      an identifier
    OR
      ([derived-part1: *'s] [object] [derived-part2: []'s or ()])

  * is *, denotes a reference/pointer and can be repeated
  [] in derived-part2 denotes bracketed array dimensions and can be repeated
  () in derived-part2 denotes parenthesized function parameters delimited with ,'s
  [] elsewhere denotes an optional part
  () elsewhere denotes parentheses

4 つの部分をすべて解析したら、

  [ object] は [ derived-part2(含む/返す)] [ derived-part2(へのポインター)] base-type 1です。

再帰がある場合、(ある場合) 再帰スタックの一番下にあるものを見つけますobject。それは最も内側のものであり、戻って各レベルで派生部分を収集して結合することにより、完全な宣言を取得します。再帰の。

解析中は、 (もしあれば) [object]after に移動できます。[derived-part2]これにより、線形化されたわかりやすい宣言が得られます (上記の1を参照)。

したがって、

char* (**(*foo[3][5])(void))[7][9];

あなたが得る:

  1. base-type=char
  2. レベル 1: derived-part1= *object= (**(*foo[3][5])(void))derived-part2=[7][9]
  3. レベル 2: derived-part1= **object= (*foo[3][5])derived-part2=(void)
  4. レベル 3: derived-part1= *object= fooderived-part2=[3][5]

そこから:

  1. レベル3:* [3][5] foo
  2. レベル2:** (void) * [3][5] foo
  3. レベル1:* [7][9] ** (void) * [3][5] foo
  4. 最後に、char * [7][9] ** (void) * [3][5] foo

さて、右から左に読んでください:

foo関数への 5 つのポインターの 3 つの配列の配列 (パラメーターをとらない) は、char への 9 つのポインターの 7 つの配列の配列へのポインターを返します。

derived-part2プロセスのすべてで配列の次元を逆にすることもできます。

それがあなたのスパイラルルールです。

そしてスパイラルが見やすい。左からさらに深くネストされたものに飛び込み[object]、次に右に再浮上して、上のレベルに別の左と右のペアがあることに注意してください。

于 2013-04-28T09:55:41.093 に答える
2

例えば:

int * a[][5];

これは、 の配列へのポインターの配列ではありませんint

于 2013-04-28T08:23:20.950 に答える