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


188

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

いくつかのCコード

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

いくつかの興味深い説明

それで、この「螺旋状に読む」は有効なものですか?これはCコンパイラの解析方法ですか?
この奇妙なコードについて、より簡単な説明があればすばらしいでしょう。
すべてのほかに、これらの種類のコードは役に立ちますか?もしそうなら、いつ、どこで?

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


9
どうすれば説明がもっと簡単になりますか?これは、fw /の定義のすべての側面をカバーしており、各キーポイントについていくつかの単語があります。
スコットハンター

29
たぶんCは難しいですか?これは実際fにはvoid (*(*f[])(void))(void);、任意の引数を取ることができる関数へのポインタの配列として宣言されます。そうである場合、そうです、引数を取らない関数です...
txtechhelp

18
実際には、そのようなあいまいなコードをコーディングしないでください。署名にtypedefを使用する
Basile Starynkevitch 2015

4
関数ポインタを含む宣言は難しい場合があります。これは、通常のCまたはC ++が同じように難しいことを意味するものではありません。他の言語はこれをさまざまな方法で解決します。これには関数ポインターがないことも含まれます。これは場合によっては大幅な省略になる可能性があります
Kate Gregory

20
目を細めると、LISPのように見えます。
user2023861 2015

回答:


117

複雑な宣言の意味を見つけるのに役立つ「時計回り/らせん規則」と呼ばれる規則があります。

c-faqから:

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

  1. 未知の要素から始めて、らせん/時計回りに移動します。次の要素に遭遇すると、それらを対応する英語のステートメントで置き換えます。

    [X]または[]
    =>配列Xのサイズ...または配列未定義のサイズ...

    (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"として正しく読み取ります。

この問題はJames Kanzeによってこの回答でもカバーされていることに注意してください(コメントでハックによって指摘されました)。


5
cdecl.orgがもっと良かったと思います
Grady Player

8
"スパイラルルール"はありません... "int *** foo [] [] []"は、ポインターを指すポインターの配列の配列の配列を定義します。「スパイラル」は、この宣言が物事をかっこでグループ化して、それらを交互にさせたという事実からのみ生じます。それは、括弧の各セット内の右側、左側のすべてです。
Random832

1
@ Random832「スパイラルルール」があり、それは今述べたケースをカバーします。つまり、括弧/配列の処理方法などについて話します。もちろん、標準Cルールではありませんが、処理方法を理解するための優れたニーモニックです。複雑な宣言。私見、それは非常に便利であり、問​​題が発生した場合やcdecl.orgが宣言を解析できない場合に役立ちます。もちろん、そのような宣言を乱用するべきではありませんが、それらがどのように解析されるかを知ることは良いことです。
vsoftco 2016年

5
@vsoftcoしかし、かっこに達したときに向きを変えただけでは、「らせん/時計回りに動く」わけではありません。
Random832 2016年

2
ああ、あなたはスパイラルルールが普遍的ではないことを言及する必要があります。
2016年

105

「スパイラル」ルールは、次の優先ルールには含まれていません。

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)()

次に、それを認識しaf単なる識別子よりも複雑な式にすることができます。in T (*a)[N]a単純な識別子、または(*f())[N]a-> f())のような関数呼び出し、または(*p[M])[N]a-> p[M])のような配列、または(*(*p[M])())[N]a-> (*p[M])())のような関数へのポインターの配列等

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

そのような毛深い宣言に遭遇したとき、左端の識別子を見つけて、上記の優先規則を適用し、それらを任意の関数パラメーターに再帰的に適用することから始めます。

         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

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

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

innerfunc *f[N];

だが...

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


7
「シグナル」の例を挙げていただき、ありがとうございます。これらの種類のものが実際に出現することを示しています。
Justsalt 2015

それは良い例です。
ケーシー

私はあなたのf減速ツリーが好きで、優先順位を説明しました...何らかの理由で、特に物事を説明するときは特に、ASCII-artから
追い出され

1
どちらの関数も引数を取らないと仮定するとvoid、関数の括弧で使用する必要があります。それ以外の場合は、引数を取ることができます。
16年

1
@haccks:宣言については、はい。関数呼び出しについて話していました。
John Bode、

57

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

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

(*(*f[i])())()がtypeの結果を生成するという表明ですvoid。つまり:

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

    f[i]
  • の要素はf逆参照できるため、ポインタでなければなりません。

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

    (*f[i])()
  • それらを逆参照できるので、これらの関数の結果もポインタでなければなりません。

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

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

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


3
今まで見たことのない素晴らしい見方。+1
tbodt 2016年

4
いいね。このように見ると、それは本当に簡単です。vector< function<function<void()>()>* > f特にを追加する場合は特に、のようなものよりもかなり簡単ですstd::。(しかし、この例f :: [IORef (IO (IO ()))]
不自然

1
@TimoDenk:宣言a[x]は、式a[i]がの場合に有効であることを示しますi >= 0 && i < x。一方、a[]サイズは指定されていないため、と同じです*a。これは、式a[i](または同等の*(a + i))がのある範囲で有効であることを示しiます。
Jon Purdy、2016年

4
これは、はるかにCのタイプについて考えるための最も簡単な方法は、このためのおかげで
アレックスOZER

4
これ大好き!愚かな螺旋よりもずっと推論しやすい。(*f[])()インデックスを付け、逆参照してから呼び出すことができる型なので、関数へのポインタの配列になります。
Lynn

32

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

スパイラルルールの適用またはcdeclの使用は常に有効であるとは限りません。どちらも失敗する場合があります。スパイラルルールは多くの場合に機能しますが、一般的ではありません

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

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

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

このルールには、実際には、識別子の一方の側からもう一方の側へジグザグに移動することが含まれます。

簡単な宣言を解読する

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があります(拡大するには画像をクリックしてください):

ここに画像の説明を入力してください


ここで言及されているルールは、KN KING著のCプログラミングAモダンアプローチから引用しています。


これは、標準のアプローチ、つまり「宣言は使用法を反映する」のようなものです。この時点で別の質問をしたいと思います。KNキングの本を提案しますか?その本に関する素晴らしいレビューをたくさん見ています。
Motun

1
うん。私はその本を勧めます。その本からプログラミングを始めました。良い文章とそこの問題。
ハッキング

宣言を理解できないcdeclの例を提供できますか?私はcdeclがコンパイラーと同じ解析規則を使用していると思っていました。
Fabioは、モニカを2016

@FabioTurati; 関数は配列や関数を返すことができません。char (x())[5]構文エラーが発生するはずですが、cdeclは次のように解析します:配列5を返す関数として宣言xしますchar
2016年

12

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


まあ、「スパイラルアプローチ」では、あなたはできる限り右に行き、それからできる限り左に行くなどします。しかし、それはしばしば誤って説明されます...
Lynn

7

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


3
それでも、typedefの解析方法を知っているだけでも、それを解析する方法を知ることは重要です!
inetknght 2015

1
@ inetknght、typedefでそれを行う方法は、構文解析が不要になるようにそれらを十分に単純にすることです。
SergeyA

2
インタビュー中にこれらの種類の質問をする人々は、自分の自我を撫でるためだけにそれを行います。
Casey

1
@JohnBode、そしてあなたは関数の戻り値をtypedefingすることによって自分自身を支持するでしょう。
SergeyA、2015

1
@JohnBode、私はそれは議論する価値のない個人的な選択の問題だと思います。私はあなたの好みを見ます、私はまだ私のものを持っています。
SergeyA

7

ランダムな雑学の事実として、Cの宣言がどのように読み取られるかを説明する実際の英語の単語があることを知っているとおもしろいかもしれません。

参考:リンデン、1994デア・ヴァン - ページ76


1
その言葉が示すものではありません括弧によって、または単一の行にネストされたのように。これは、「スネーク」パターンを説明し、LTRラインの後にRTLラインが続きます。
Potatoswatter 2016年

5

これの有用性については、シェルコードを操作するときに、この構成がたくさん見られます:

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

構文的にはそれほど複雑ではありませんが、この特定のパターンはよく出てきます。

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

したがって、元の図の範囲での有用性は疑わしいものですが(プロダクションコードは大幅に簡略化する必要があることをお勧めします)、かなりの構文構文があります。


5

宣言

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

あいまいな言い方です

Function f[]

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

実際には、ResultFunctionおよびFunctionではなく、より説明的な名前が必要になります。可能であれば、パラメータリストをとして指定することもできますvoid


4

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

関数ポインターの定義

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

void (*funcPtr)();

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

復習するには、「真ん中から始めて」(「funcPtrは...」)、右に移動し(そこには何もない-右括弧で止められています)、左に移動して「*」を見つけます(「 ...へのポインタ...))、右側に移動して空の引数リストを検索し(「...引数を取らない関数...」)、左側に移動して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() {} ///:~ 

それぞれについて説明し、左右のガイドラインを使用して理解します。番号1は、「fp1は整数の引数を取り、10個のvoidポインターの配列へのポインターを返す関数へのポインターです。」と述べています。

番号2は、「fp2は3つの引数(int、int、float)を取り、整数の引数を取り、floatを返す関数へのポインターを返す関数へのポインターです。」

複雑な定義をたくさん作成している場合は、typedefを使用することをお勧めします。番号3は、typedefが毎回複雑な説明を入力する手間を省く方法を示しています。「fp3は、引数を取らず、引数を取らずdoubleを返す関数への10個のポインターの配列へのポインターを返す関数へのポインターです。」次に、「aはこれらのfp3タイプの1つです」と表示されます。typedefは通常、単純な記述から複雑な記述を構築するのに役立ちます。

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

このような複雑な宣言と定義が必要になることはめったにありません。しかし、それらを理解する練習をすれば、実際の生活で遭遇するかもしれない少し複雑なものに少しでも邪魔されることはありません。

引用元:C ++ボリューム1、第2版、第3章、ブルースエッケルによる「関数アドレス」のセクションでの考え方。


4

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

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

真剣に、これは一目で行うことを学ぶのは難しくありません。あなたはスキルを練習するためにいくらかの時間を費やすことをいとわない必要があります。あなたが他の人によって書かれたCコードを維持または適応するつもりなら、それはだ間違いなく投資その時間の価値。また、それを学んでいない他のプログラマーを驚かせるための楽しいパーティートリックでもあります。

あなた自身のコードの場合:いつものように、何かワンライナーとして記述できるという事実は、それが標準のイディオムになった非常に一般的なパターン(文字列コピーループなど)でない限り、そうである必要があることを意味しません。これらの「1つのスウェルグループ」を生成して解析する能力に依存するのではなく、レイヤー化されたtypedefと段階的な逆参照から複雑なタイプを構築する場合、あなたとあなたに従う人ははるかに幸せになります。パフォーマンスも同様に良好になり、コードの可読性と保守性が大幅に向上します。

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

if if if = then then then = else else else = if then ...

2
PL / IステートメントはIF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIFとして解析されましたif (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF)
Cole Johnson

私が考えて Cのと同等の条件付きIF / THEN / ELSE式(?:)、ミックスに第三のセットを持って使用することにより、一歩遠くそれを取ったバージョンがあった...しかし、それは数十年をされているとしていて言語の特定の方言に依存していました。ポイントは、どの言語にも少なくとも1つの病理学的形態があることです。
ケシュラム2016年

4

私はたまたま何年も前に(髪がたくさんあったとき)書いたスパイラルルールの元の作者であり、cfaqに追加されたときに光栄でした。

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

記録のために、

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

char *ar[10][10];

このスレッドで他の人が指摘しているように、配列に遭遇したときは、次のようにすべてのインデックス消費するようにルールを更新できます。

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

今、スパイラルルールに従って、私は得ます:

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

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

PS:

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


3
  • ボイド (*(*f[]) ()) ()

解決中void>>

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

安心()>>

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

解決中*>>

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

解決中()>>

  • (* f[])=関数の戻り値((関数の戻り値(void)へのポインタ))

解決中*>>

  • f[] =(関数の戻り値へのポインター((関数の戻り値(void)へのポインター)))

解決中[ ]>>

  • f =(ポインタから(関数の戻り値(ポインタから(関数の戻り値(void)))))の配列)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.