web-dev-qa-db-ja.com

Cはそれほど難しくありません:void(*(* f [])())()

今日写真を見たばかりで、説明をいただければ幸いです。だからここに写真があります:

some c code

私はこれを混乱させ、そのようなコードが実用的かどうか疑問に思いました。私は写真をグーグルで検索し、 this redditエントリで別の写真を見つけました。ここにその写真があります:

some interesting explanation

それで、この「螺旋状に読む」ことは有効なものですか?これはCコンパイラがどのように解析するのですか?
この奇妙なコードについてもっと簡単な説明があれば素晴らしいと思います。
すべてとは別に、この種のコードは有用ですか?もしそうなら、いつどこで?

「スパイラルルール」について 質問 がありますが、私はそれがどのように適用されるのか、そのルールでどのように式が読み取られるのかを尋ねているだけではありません。そのような表現の使用法とスパイラルルールの有効性についても疑問に思っています。これらに関して、いくつかの素晴らしい回答がすでに投稿されています。

183
Motun

"Clockwise/Spiral Rule" というルールがあり、複雑な宣言の意味を見つけるのに役立ちます。

c-faq から:

次の3つの簡単な手順があります。

  1. 未知の要素から始めて、スパイラル/時計回りの方向に移動します。次の要素に遭遇した場合、それらを対応する英語のステートメントに置き換えます。

    [X]または[]
    => Array X size of ...またはArray undefined size of ...

    (type1, type2)
    => type1およびtype2を渡す関数が戻ります...

    *
    =>へのポインター...

  2. すべてのトークンがカバーされるまで、これをスパイラル/時計回りに続けてください。

  3. 必ず最初に括弧内の何かを解決してください!

例については、上記のリンクを確認してください。

また、あなたを助けるために次のウェブサイトもあります:

http://www.cdecl.org

C宣言を入力すると、英語の意味が与えられます。ために

void (*(*f[])())()

以下を出力します:

voidを返す関数へのポインターを返す関数へのポインターの配列としてfを宣言する

編集:

Random832 によるコメントで指摘されているように、スパイラルルールは配列の配列をアドレス指定せず、これらの宣言(のほとんど)で誤った結果をもたらします。たとえば、int **x[1][2];の場合、スパイラルルールは、[]*よりも優先順位が高いという事実を無視します。

配列の配列の前にいる場合、スパイラルルールを適用する前に、最初に明示的なかっこを追加できます。例:int **x[1][2];は優先順位のためint **(x[1][2]);(有効なC)と同じで、スパイラルルールは「xはintへのポインターへのポインターの配列2の配列1」として正しく読み取ります。これは正しい英語の宣言です。

この問題は answer by James Kanze (コメントの haccks で指摘されている)でもカバーされていることに注意してください。

117
ouah

「スパイラル」ルールは、次の優先ルールから外れます。

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

添字[]および関数呼び出し()演算子は、単項*よりも優先順位が高いため、*f()*(f())として解析され、*a[]*(a[])として解析されます。

したがって、配列へのポインターまたは関数へのポインターが必要な場合は、*または(*a)[]のように、(*f)()を識別子で明示的にグループ化する必要があります。

次に、aおよびfは単なる識別子よりも複雑な式になり得ることに気付きます。 T (*a)[N]では、aは単純な識別子であるか、(*f())[N]a-> f())のような関数呼び出し、または(*p[M])[N]のような配列である可能性があります。 、(a-> p[M])、または(*(*p[M])())[N]a-> (*p[M])())などの関数へのポインターの配列などです。

間接演算子*が単項ではなく接尾辞であるといいでしょう。左から右に宣言を多少読みやすくします(void f[]*()*();void (*(*f[])())()よりも確実にフローします)が、そうではありません。

そのような毛むくじゃらの宣言に出くわしたら、leftmost識別子を見つけて、上記の優先順位ルールを適用し、それらを再帰的に関数パラメーターに適用します:

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

標準ライブラリのsignal関数は、おそらくこの種の狂気の型標本です。

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

この時点で、ほとんどの人は「typedefを使用する」と言っていますが、これは確かにオプションです。

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

しかし...

どのように式でsefを使いますか?ポインターの配列であることは知っていますが、正しい関数を実行するためにどのように使用しますか? typedefを調べて、正しい構文を解決する必要があります。対照的に、「裸の」バージョンはかなり見栄えがしますが、式でsef(つまり、どちらの関数も引数を取らないと仮定すると(*(*f[i])())();)を正確に指示します。 。

101
John Bode

Cでは、宣言は使用法を反映しています。それが標準での定義方法です。宣言:

void (*(*f[])())()

(*(*f[i])())()void型の結果を生成するというアサーションです。つまり:

  • fは、インデックスを作成できるため、配列である必要があります。

    f[i]
    
  • fの要素はポインターである必要があります。これらは逆参照できるためです。

    *f[i]
    
  • これらのポインターは、引数をとらない関数へのポインターである必要があります。呼び出すことができるためです。

    (*f[i])()
    
  • これらの関数の結果もポインターである必要があります。これらは逆参照できるためです。

    *(*f[i])()
    
  • これらのポインターは、alsoを呼び出すことができるため、引数を取らない関数へのポインターでなければなりません。

    (*(*f[i])())()
    
  • これらの関数ポインターはvoidを返す必要があります

「スパイラルルール」は、同じことを理解するための異なる方法を提供するニーモニックです。

55
Jon Purdy

それで、この「螺旋状に読む」ことは有効なものですか?

スパイラルルールの適用または cdecl の使用は常に有効ではありません。場合によっては両方とも失敗します。スパイラルルールは多くの場合に機能しますが、 ユニバーサルではありません

複雑な宣言を解読するには、次の2つの単純なルールを覚えておいてください。

  • 常に内側から宣言を読み込む:括弧がある場合は、最も内側から開始します。宣言されている識別子を見つけ、そこから宣言の解読を開始します。

  • 選択肢がある場合は、常に[]および()よりも*を優先:*の場合識別子の前に[]が続き、識別子はポインタではなく配列を表します。同様に、*が識別子の前にあり、()がその後にある場合、識別子はポインタではなく関数を表します。 (括弧は、[]および()よりも*の通常の優先順位をオーバーライドするために常に使用できます。)

このルールには、実際には識別子の一方から他方へのzigzaggingが含まれます。

単純な宣言を解読する

int *a[10];

ルールの適用:

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

複雑な宣言を次のように解読しましょう

void ( *(*f[]) () ) ();  

上記のルールを適用することにより:

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

これがどのように進むかを示すGIFです(画像をクリックすると拡大表示されます)。

enter image description here


ここで言及されているルールは、本から引用されています C Programming A Modern Approach by K.N KING

30
haccks

この宣言では、各レベルの括弧内の各側に演算子が1つしか存在しないため、これは「スパイラル」にすぎません。 「らせん状に」進むと主張することは、実際には、すべての配列レベルがポインターレベルのいずれかよりも前にあるときに、宣言とint ***foo[][][]で配列とポインターを交互に使用することをお勧めします。

12
Random832

このような構造は、実際の生活でどのような用途にも使用できるとは思いません。私はそれらを、通常の開発者へのインタビューの質問としても嫌っています(おそらく、コンパイラ作成者にとっては問題ありません)。代わりにtypedefを使用する必要があります。

7
SergeyA

ランダムな雑学ファクトイドとして、C宣言の読み方を説明する英語の実際の単語があることを知っていると面白いかもしれません: Boustrophedonically 、つまり、右から左に左から-右。

参照: Van der Linden、1994 - ページ76

7
asamarin

宣言

void (*(*f[])())()

あいまいな言い方です

Function f[]

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

実際には、ResultFunctionおよびFunctionではなく、よりわかりやすい名前が必要になります。可能であれば、パラメーターリストをvoidとして指定します。

5

これの有用性に関して、シェルコードで作業するとき、あなたはこの構成をたくさん見る:

int (*ret)() = (int(*)())code;
ret();

構文的にはそれほど複雑ではありませんが、この特定のパターンは多く登場します。

this SOの質問のより完全な例。

したがって、元の図の範囲での有用性は疑わしいですが(実動コードは大幅に簡素化することをお勧めします)、かなりの構文構文がいくつかあります。

5
Casey

Bruce Eckelによって説明された方法は、役に立つし、従うのが簡単であることがわかりました。

関数ポインタの定義

引数も戻り値もない関数へのポインタを定義するには、次のように言います。

void (*funcPtr)();

このような複雑な定義を見ている場合、それを攻撃する最善の方法は、途中から始めて解決することです。 「中央」とは、変数名で始まることを意味します。これはfuncPtrです。 「外に出る」とは、最も近い項目を右に見て(この場合は何もありません。右括弧で短くなります)、次に左に(アスタリスクで示されたポインター)、次に右に見て(引数をとらない関数を示す空の引数リスト)、左を見る(void、関数に戻り値がないことを示します)。この左右左右の動きは、ほとんどの宣言で機能します。

確認するには、「真ん中から開始」(「funcPtr is a ...」)、右に移動(何もない-右括弧で停止)、左に移動して「*」(「 ... aへのポインター」)、右に移動して空の引数リストを検索し(「...引数を受け取らない関数...」)、左に移動してvoidを検索します(「funcPtr is引数を取らずvoidを返す関数へのポインター」)。

なぜ* funcPtrに括弧が必要なのか疑問に思うかもしれません。それらを使用しなかった場合、コンパイラは次のように表示されます。

void *funcPtr();

変数を定義するのではなく、関数(void *を返す)を宣言することになります。コンパイラーは、宣言または定義が何であるかを理解するときに行うのと同じプロセスを実行するものと考えることができます。これらの括弧は「ぶつかる」必要があるため、右に進み続けて空の引数リストを見つける代わりに、左に戻り「*」を見つけます。

複雑な宣言と定義

余談ですが、CおよびC++の宣言構文がどのように機能するかを理解すると、はるかに複雑なアイテムを作成できます。例えば:

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

それぞれをウォークスルーし、右から左のガイドラインを使用してそれを把握します。 Number 1は、「fp1は整数の引数を取り、10個のvoidポインタの配列へのポインタを返す関数へのポインタです」と言います。

Number 2は、「fp2は3つの引数(int、int、およびfloat)を受け取る関数へのポインターであり、整数の引数であり、floatを返します。」

多数の複雑な定義を作成している場合は、typedefを使用できます。 数値3は、typedefが毎回複雑な説明の入力を節約する方法を示しています。 「fp3は、引数をとらない関数へのポインターであり、引数をとらず、doubleを返す関数への10個のポインターの配列へのポインターを返します。」と表示されます。一般に、単純な説明から複雑な説明を作成するのに役立ちます。

数値4は、変数定義ではなく関数宣言です。 「f4は、整数を返す関数への10個のポインターの配列へのポインターを返す関数です。」

このような複雑な宣言と定義が必要になることはめったにありません。ただし、それらを理解する演習を行っても、実際の生活で遭遇する可能性のあるわずかに複雑なもので穏やかに邪魔されることはありません。

取得:C++ボリューム1、第2版、第3章、ブルースエクケルの「関数のアドレス」のセクションで考える

4
user3496846

C宣言のこれらのルールを覚えておいてください
そして、優先順位が疑わしいことはありません。
接尾辞から始め、接頭辞に進み、
そして両方のセットを内側から、外側から読みます。
-1980年代半ばの私

もちろん、括弧で変更されている場合を除きます。そして、これらを宣言するための構文は、その変数を使用して基本クラスのインスタンスを取得するための構文を正確に反映していることに注意してください。

真剣に、これは一目でやることを学ぶのは難しいことではありません。スキルの練習に少し時間をかけるだけです。他の人が書いたCコードを維持または調整する場合、間違いなくその時間を投資する価値があります。また、これを学んでいない他のプログラマーを驚かせるための楽しいパーティートリックでもあります。

あなた自身のコードの場合:いつものように、何かcanがワンライナーとして書かれているという事実は、標準的なイディオムになっている非常に一般的なパターンでない限り文字列コピーループとして)。あなたとあなたをフォローしている人は、これらを「一度に膨らませて生成して解析する能力に依存するのではなく、階層化されたtypedefとステップバイステップの参照から複雑な型を構築する方がmuch幸せですフープ。」パフォーマンスも同様に良好であり、コードの可読性と保守性は非常に優れています。

もっと悪いかもしれません。次のようなものから始まる法的PL/Iステートメントがありました。

if if if = then then then = else else else = if then ...
4
keshlam

私はたまたま何年も前に(多くの髪の毛を持っていたときに)書いたスパイラルルールの元の著者であり、cfaqに追加されたときに名誉を与えられました。

私は、学生や同僚がC宣言を「頭の中で」読みやすくする方法として、スパイラルルールを書きました。つまり、cdecl.orgなどのソフトウェアツールを使用する必要はありません。スパイラルルールがC式を解析するための標準的な方法であることを宣言することは、私の意図ではありませんでした。しかし、この規則が何年にもわたって文字通り何千ものCプログラミングの学生と実務家を助けてきたことを嬉しく思います!

記録のために、

Linus Torvalds(私が非常に尊敬している人)を含む多くのサイトで何度も「正しく」特定されており、私のスパイラルルールが「破綻する」状況があることがあります。最も一般的なもの:

char *ar[10][10];

このスレッドの他の人が指摘したように、ルールが更新され、配列に遭遇したときに、すべてのインデックスを単純に消費するようになりますas if

char *(ar[10][10]);

さて、スパイラルルールに従って、私は得るでしょう:

「arは、charへのポインターの10x10の2次元配列です」

スパイラルルールがCの学習に役立つことを願っています!

追伸:

「Cは難しくない」という画像が大好きです:)

3
David Anderson
  • void (*(*f[]) ()) ()

void >>を解決しています

  • (*(*f[]) ())()= void

再割り当て() >>

  • (* (*f[]) ())=関数が返す(void)

* >>の解決

  • (*f[])()=((void)を返す関数)へのポインター

() >>の解決

  • (* f[])=関数を返す(ポインタ((void)を返す関数)))

* >>の解決

  • f [] =(関数を返す関数へのポインター((関数を返す(void)))))へのポインター

[ ] >>の解決

  • f =(ポインターを返す関数へのポインター(ポインターを返す関数(void)を返す))))
3
Shubham