不幸にも?C言語の複雑な宣言に出くわしてしまった場合、その宣言をどのようにして読み進めれば良いのでしょうか?
その手助けになるかも(あくまでも「かも」です(^_^; )しれないヒントを書きます。

【左側から読み進め、左側から読めない場合のみ右側から読む。】

これだけなの?と思われたかも知れませんが、たったこれだけです。
ヒントにそって読み進める例を、下記に記します。
一つずつ順番に読み進める感覚を、掴んでもらえたら幸いです。

「例1」 int **p[4]
int **p[4]
intへのポインタ *p[4]
intへのポインタへのポインタ p[4]
intへのポインタへのポインタの4要素配列 p

pは「intへのポインタへのポインタの4要素配列」です。

「例2」 int *(*p)[4]
int *(*p)[4]
intへのポインタ (*p)[4]
intへのポインタの4要素配列 (*p)
intへのポインタの4要素配列へのポインタ p

pは「intへのポインタの4要素配列へのポインタ」です。

「例3」 (int **[4])c (キャスト例)
c (int **[4])
c int **[4]
c intへのポインタ *[4] 
c intへのポインタへのポインタ [4]
c intへのポインタへのポインタの4要素配列

cをキャストした型は「intへのポインタへのポインタの4要素配列」です。

「例4」 (int *(*)[4])c (キャスト例)
c (int *(*)[4])
c int *(*)[4]
c intへのポインタ (*)[4] 
c intへのポインタの4要素配列 (*)
c intへのポインタの4要素配列へのポインタ

cをキャストした型は「intへのポインタの4要素配列へのポインタ」です。

*[4] と (*)[4] は異なる。(*)[4] の場合は右側から読む。

「例5」 int *(*(*const p)(void))(int)
int *(*(*const p)(void))(int)
intへのポインタ (*(*const p)(void))(int)
intへのポインタを返す関数(引数int) (*(*const p)(void))
intへのポインタを返す関数(引数int)へのポインタ (*const p)(void)
intへのポインタを返す関数(引数int)へのポインタを返す関数(引数無し) (*const p)
intへのポインタを返す関数(引数int)へのポインタを返す関数(引数無し)への不変ポインタ p

pは「intへのポインタを返す関数(引数int)へのポインタを返す関数(引数無し)への不変ポインタ」です。
 
C 言語の関数呼出しは、式として実行されます。式として関数呼出しが行われ、関数の返却値が式の値になります。

JIS C では、関数へのポインタ(以後「関数ポインタ」と記述)を使用して関数を呼出す場合、"関数ポインタ(実引数)"で呼出すことも、"(*関数ポインタ)(実引数)"で呼出すことも出来ます。

"関数ポインタ(実引数)"は、「関数ポインタ型(実引数)」ので関数を呼出し、"(*関数ポインタ)(実引数)"は、「関数型(実引数)」(*1)で関数を呼出します。

では、「関数ポインタ型(実引数)」「関数型(実引数)」の両方が、関数呼出し式として規定されているのでしょうか?

実は、関数ポインタ型の後に実引数の括弧()が続く後置式が、関数呼出し式として規定されているだけで、関数型の後に実引数の括弧()が続く式は、関数呼出し式として規定されていません。(*2)

ではどうして、「関数型(実引数)」ので関数を呼出すことが出来るのでしょうか?

それは、sizeof 及び単項&演算子オペランド以外の関数型の式は、その関数へのポインタ型の式に変換されるルールがあるからです。(*3)

つまり、(*関数ポインタ)の結果は関数型ですが、上記のルールにより関数ポインタ型に変換され、その結果として関数呼出し式になり、関数が呼出されるのです。
 
 
*1オペランドが関数を指す単項*演算子の結果は、関数型(関数指示子)になります。

*2JIS C (JIS X 3010-1993 6.3.2.2 関数呼出し) より一部引用。

制約 呼び出される関数を表す式は、voidを返す関数へのポインタ型、又は配列型以外のオブジェクト型を返す関数へのポインタ型をもたなければならない。


意味規則 コンマで区切られた式の並び(空であってもよい。)を囲む括弧()が続く後置式は、関数呼出しとする。
後置式は呼び出される関数を表す。式の並びは、関数への実引数を指定する。


*3JIS C (JIS X 3010-1993 6.2.2.1 左辺値及び関数指示子) より一部引用。

 関数指示子(function designator)は、関数型をもつ式とする。関数指示子がsizeof演算子又は単項&演算子のオペランドである場合を除いて、型"〜型を返す関数"をもつ関数指示子は、型"〜型を返す関数へのポインタ"をもつ式に型変換する。

 
 
●関数へのポインタを使用した関数呼び出し例●

プログラムをコピー&ペーストしてコンパイルする際は、下記の作業を行う必要があります。
全ての全角’<’文字を半角文字に変更する。
全ての全角’>’文字を半角文字に変更する。
全ての全角’,’文字を半角文字に変更する。
全角スペース1文字を、全て半角スペース2文字に変更する。

--
#include <stdio.h>

int main(void);

int main(void)
{
  int (*PointerPrintf)(const char *, ...) = printf;

  /* 全て printf関数へのポインタ(実引数);として printf関数が呼出される */
  PointerPrintf("Pointer()\n");
  (*PointerPrintf)("(*Pointer)()\n");
  (**PointerPrintf)("(**Pointer)()\n");
  (***PointerPrintf)("(***Pointer)()\n");
  (&*PointerPrintf)("(&*Pointer)()\n");

  return 0;
}
--
 
Visual C++ の stdlib.h に宣言されている _winmajor 等のグローバル変数を使って、Windows のバージョンをチェックするサンプルプログラムを( ..)φメモメモ

 
(注意)
ソースをコピー&ペーストしてビルド(コンパイルリンク)する場合は、下記の作業を行う必要があります。
全角’<’文字を、全て半角文字に変更する。
全角’>’文字を、全て半角文字に変更する。
全角’,’文字を、全て半角文字に変更する。
全角’%’文字を、全て半角文字に変更する。
全角’|’文字を、全て半角文字に変更する。
全角スペース1文字を、全て半角スペース2文字に変更する。

--
//【Windows のバージョンをチェックする Visual C++ Win32 コンソールプログラム】
// ※このプログラムは Visual C++ 以外ではビルド出来ません。(たぶん(^^;)

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
  DWORD GV = GetVersion();

  //【GetVersion API 値と _winver, _winmajor, _winminor, _osver の関係を表示】
  puts("GetVersion API 値と _winver, _winmajor, _winminor, _osver の関係\n");
  printf("GetVersion API 値 (以下 GV)        = %08X\n\n", GV);
  printf("_winver 値                = %08X\n", _winver);
  printf("((GV << 8) | ((GV >> 8) & 0XFF)) & 0XFFFF = %08X\n\n",
                  ((GV << 8) | ((GV >> 8) & 0XFF)) & 0XFFFF );
  printf("_winmajor 値               = %08X\n", _winmajor);
  printf("GV & 0XFF                 = %08X\n\n", GV & 0XFF );
  printf("_winminor 値               = %08X\n", _winminor);
  printf("(GV >> 8) & 0XFF             = %08X\n\n",
                               (GV >> 8) & 0XFF);
  printf("_osver 値                 = %08X\n", _osver);
  printf("GV >> 16                 = %08X\n\n\n", GV >> 16 );

  //【_winmajor, _winminor, _osver を使用して Windows を判定】
  fputs("このプログラムを実行している Windows は [", stdout);
  if ( _osver < 0X8000 ) {
    //【NT系】 (_osver 下位ワードの最高位ビットが 0 なら NT系)
    switch (_winmajor) {
      case 3:
      case 4:
        printf ("Windows NT %u.%u Build %u",
                         _winmajor, _winminor, _osver);
        break;
      case 5:
        switch (_winminor) {
          case 0:
            printf ("Windows 2000 Build %u", _osver);
            break;
          case 1:
            printf ("Windows XP Build %u", _osver);
            break;
          case 2:
            printf ("Windows Server 2003 family Build %u",
                                    _osver);
            break;
          default:
            printf ("Server 2003 family より新しい NT系Windows"
                         " Build Build %u", _osver);
        }
        break;
      default:
        printf ("Server 2003 family より新しい NT系Windows"
                         " Build Build %u", _osver);
    }
  }
  else {
    switch (_winmajor ) {
      case 0: //【Win32s】
      case 1: // 参照した資料には、 Win32s の時 _winmajor は 3 以下になる
      case 2: // ことしか書かれていない。
      case 3: // よって、0 から 3 までを列挙。
        printf ("Win32s Build %u", _osver & 0X7FFF);
        break;
      case 4: //【9X系】(9X系の場合、_osver にビルド番号が格納されない。)
        switch (_winminor) {
          case 0:
            printf ("Windows 95 Build ----");
            break;
          case 10:
            printf ("Windows 98 Build ----");
            break;
          case 90:
            printf ("Windows Me Build ----");
            break;
          default: // 念の為
            printf ("Me より新しい 9X系Windows Build ----");
        }
        break;
      default: // 念の為
        printf ("Me より新しい 9X系Windows Build ----");
    }
  }
  puts("] です。");

  return 0;
}
--
 
Windows Win32 C/C++ プログラム実行環境指定マクロ値について( ..)φメモメモ

----- WINVER, _WIN32_WINNT, _WIN32_WINDOWS -----

Windows 95
WINVER=0x0400 _WIN32_WINDOWS=0x0400

Windows 98
WINVER=0x0410 _WIN32_WINDOWS=0x0410

Windows Me
WINVER=0x0500 _WIN32_WINDOWS=0x0500

Windows NT 4.0
WINVER=0x0400 _WIN32_WINNT=0x0400

Windows 2000
WINVER=0x0500 _WIN32_WINNT=0x0500

Windows XP
WINVER=0x0501 _WIN32_WINNT=0x0501

Windows Server 2003 family
WINVER=0x0502 _WIN32_WINNT=0x0502

----- WIN32_IE -----

Windows 95, Windows NT 4.0 (Comctl32.dll 4.00, Shell32.dll 4.00)
WIN32_IE=0x0200

Internet Explorer 3.0, 3.01, 3.02
WIN32_IE=0x0300

Internet Explorer 4.0
WIN32_IE=0x0400

Internet Explorer 4.01
_WIN32_IE=0x0401

Internet Explorer 5.0, 5.0a, 5.0b
_WIN32_IE=0x0500

Internet Explorer 5.01, 5.5
_WIN32_IE=0x0501

Internet Explorer 5.6
_WIN32_IE=0x0560

Internet Explorer 6.0
_WIN32_IE=0x0600
 
 
( 参照 URL http://msdn.microsoft.com/library/shared/deeptree/asp/rightframe.asp?dtcfg=/library/deeptreeconfig.xml&;;;;;url=/library/en-us/winprog/winprog/using_the_windows_headers.asp?frame=true&hidetoc=false )
 
 
●処理系デフォルトマクロ値チェック コンソールプログラム●

実際にコンパイルする場合は下記の作業を行う必要があります。
全角"<"文字を、全て半角文字に変更する。
全角">"文字を、全て半角文字に変更する。
全角","文字を、全て半角文字に変更する。
全角"%"文字を、全て半角文字に変更する。
全角スペース文字を、全て半角スペース文字に変更するか又は削除する。

--
#include <windows.h>
#include <stdio.h>

int main(void)
{
#if defined WINVER
  printf("WINVER     = 0x%04X\n", WINVER);
#else
  printf("WINVER 未定義\n");
#endif
#if defined _WIN32_WINNT
  printf("_WIN32_WINNT  = 0x%04X\n", _WIN32_WINNT);
#else
  printf("_WIN32_WINNT 未定義\n");
#endif
#if defined _WIN32_WINDOWS
  printf("_WIN32_WINDOWS = 0x%04X\n", _WIN32_WINDOWS);
#else
  printf("_WIN32_WINDOWS 未定義\n");
#endif
#if defined _WIN32_IE
  printf("_WIN32_IE   = 0x%04X\n", _WIN32_IE);
#else
  printf("_WIN32_IE 未定義\n" );
#endif

  return 0;
}
--

<CudeWarrior 8.0 実行結果>

WINVER     = 0x0400
_WIN32_WINNT 未定義
_WIN32_WINDOWS 未定義
_WIN32_IE   = 0x0501

<Borland C++ 5.3 実行結果>

WINVER     = 0x0400
_WIN32_WINNT  = 0x0400
_WIN32_WINDOWS 未定義
_WIN32_IE   = 0x0400

<Borland C++ 5.5.1 実行結果>

WINVER     = 0x0500
_WIN32_WINNT  = 0x0500
_WIN32_WINDOWS 未定義
_WIN32_IE   = 0x0501

<Visual C++ 5.0 Learning Edition 実行結果>

WINVER     = 0x0400
_WIN32_WINNT 未定義
_WIN32_WINDOWS 未定義
_WIN32_IE 未定義

<Visual C++ 6.0 Standard Edition 実行結果>
WINVER     = 0x0400
_WIN32_WINNT 未定義
_WIN32_WINDOWS 未定義
_WIN32_IE   = 0x0400

<Visual C++.net Standard 2003 実行結果>
WINVER     = 0x0501
_WIN32_WINNT 未定義
_WIN32_WINDOWS 未定義
_WIN32_IE   = 0x0501
JIS C 新JIS C の 1バイトのビット数は、必ずしも 8ビットではありませんが、説明の都合上「1バイト = 8ビット」と仮定して文章を書いています。
 
 
今まで、C言語の unsigned int ビット数と(符号ビットを含む) signed int ビット数は、必ず同じだ思っていました。たとえば、unsigned int が 4バイト 32ビットであれば、signed int も必ず 4バイト 32ビットだと思っていました。

が…新JIS C を見てみると、そうでは無かったのです。(^_^;

新JIS C では、unsigned char 以外の符号無し整数型のビットは、「(省略可能な)詰め物ビット」「値ビット」から構成されます。
例えば、図のような unsigned int 型が存在しえます。

[図1]
┏━━━━━━━━━━━━━━━━━━━━━┓
┃  unsigned int 型 (4バイト 32ビット)     ┃
┣━━━━━━━┯━━━━━━━━━━━━━┫
┃詰め物ビット 2 │     値ビット 30        ┃
┗━━━━━━━┷━━━━━━━━━━━━━┛


また、符号付き整数型のビットは、「(省略可能な)詰め物ビット」「符号 1ビット」「値ビット」から構成されます。
例えば、図のような signed int 型が存在しえます。

[図2]
┏━━━━━━━━━━━━━━━━━━━━━┓
┃   signed int 型 (4バイト 32ビット)      ┃
┣━━━━━━━┯━━━━━━┯━━━━━━┫
┃詰め物ビット 1 │符号ビット 1  │値ビット 30  ┃
┗━━━━━━━┷━━━━━━┷━━━━━━┛


そして、符号付き整数型の値ビットのビット数は、対応する符号無し整数型の値ビット数以下でなければいけません。
つまり、unsigned int 型の値ビット数が 30 であれば、signed int 型の値ビット数は 30、もしくはそれ未満になります。

もう一度、これらを踏まえて[図1][図2]を見てください。
[図1]の unsigned int 型と[図2]の signed int 型は、対応する整数型の条件を満たしています。しかし、unsigned int 型のビット数は 30、(符号ビットを含む) signed int 型のビット数は 31 となり、ビット数が一致していません。
このような unsigned int 型と signed int 型を持つ処理系は、通常存在しないと思いますが、新JIS C 規格から外れたものではありません。

新JIS C において、詰め物ビットを除くビット数が一致するのは、符号付き型の値ビット数が、 符号無し型の値ビット数より 1 ビット少ない場合だけです。ビット数が一致するとは限らないのです。
 
 
【おまけ】
unsigned int signed int の詰め物ビット数や値ビット数等を表示する Cプログラムです。
なお、実際にコンパイルする場合は下記の作業を行う必要があります。
全角"<"文字を、全て半角文字に変更する。
全角">"文字を、全て半角文字に変更する。
全角","文字を、全て半角文字に変更する。
全角"%"文字を、全て半角文字に変更する。
全角スペース文字を、全て半角スペース文字に変更するか又は削除する。

--
#include <limits.h>
#include <stdio.h>

int main(void);

int main(void)
{
  unsigned byte_size, bit_size, count_val;

  byte_size = sizeof(unsigned);
  for ( bit_size = 0, count_val = UINT_MAX; bit_size++, count_val >>= 1; );
  printf("unsigned int 型の バイト数=%u 詰め物ビット数=%u 値ビット数=%u\n", byte_size, byte_size * CHAR_BIT - bit_size, bit_size);

  byte_size = sizeof(int);
  for ( bit_size = 0, count_val = INT_MAX; bit_size++, count_val >>= 1; );
  printf("signed int 型の バイト数=%u 詰め物ビット数=%u 符号ビット数=1(固定) 値ビット数=%u\n", byte_size, byte_size * CHAR_BIT - (bit_size + 1), bit_size);

  return 0;
}
--
JIS C の fopen 関数を使用してファイルの存在を確かめる方法が、あるホームページに書かれていました。二番目の引数 mode に "r" を指定してファイルをオープンし、その結果で、最初の引数に指定したファイルの存在を判定するというものです。オープンに成功した場合は存在すると判定し、失敗した場合は存在しないと判定します。

確かに存在しないファイルを ’r’ で始まる mode 引数でオープンすると失敗します。*1しかし、その他の理由で失敗することは無いのでしょうか?
疑問に思ったので JIS C を調べてみました。

(*1) 存在しないファイルを、’r’ で始まらない mode 引数でオープンすると、(何らかの原因によりファイルが生成出来ない場合を除き)ファイルが新しく生成されます。

調べた結果、下記の記述を見つけました。

●JIS C (JIS X 3010-1993) 7.9.5.3 より一部引用

 存在しないファイル又は読取り操作が許されていないファイルに対する読取りモードでのオープン(mode実引数の最初の文字が’r’の場合)は失敗する。


この引用部から、’r’ で始まる mode 引数でオープンする場合、ファイルが存在していても読取り操作が許されていない場合は、オープンに失敗するということが分かります。
これでは、「オープン失敗 = ファイルが存在しない」という関係が成り立ちません。
つまり、fopen 関数の結果でファイルの存在判定を行うことは出来ないのです。

では、読取り操作が禁止されることのないファイルシステムであれば、fopen 関数でファイルの存在判定を行うことが出来るのでしょうか?

調べた結果、下記の記述を見つけました。

●JIS C (JIS X 3010-1993) 7.9.1 より一部引用

    FOPEN_MAX
は、同時にオープンできることを処理系が保証するファイルの最小数を表す汎整数定数式に展開する。



この引用部から、( stdio.h ヘッダ で宣言されている) FOPEN_MAX マクロ値を越えるの数のファイルを一度にオープンしようとすると、オープンに失敗する可能性があることが分かります。
つまり、読取り操作が禁止されることのないファイルシステムにおいても、fopen 関数の結果でファイルの存在判定を行うことは出来ないのです。

 
【蛇足】下記の記述も見つけました。

●JIS C (JIS X 3010-1993) 7.9.3 より一部引用

同一ファイルを同時に複数回オープンすることが可能かどうかも、処理系定義とする。


この引用部から、ファイルが存在していても既にオープンされてる場合、同一ファイルを同時に複数回オープン出来ない処理系では、必ずオープンに失敗するということが分かります。

main 関数の違い

2004年10月7日 C言語
JIS C 新JIS C での、ホスト環境 main 関数の違いを( ..)φメモメモ

● main 関数の定義が変わった。

JIS C での main 関数は、下記のどちらかでなければならない。

int main(void) {/*...*/}

int main(int argc, char *argv[]) {/*...*/}
※ argc argv は、どのような名前でもよい。

新JIS C は、JIS C の方法に加えて、下記の main 関数が許されるようになった。

JIS C の方法いずれかと等価な方法。たとえば
typedef int INT; INT main(INT argc, char **argv) {/*...*/}

これら三つの方法のいずれでも無い処理系定義の方法。たとえば
void main(void) {/*...*/}
int main(int argc, char *argv[], char *env[]) {/*...*/}

● main 関数の最初の呼び出しからの復帰が変わった。
※ main 関数も、再帰関数呼出し(リカーシブルコール)が許される(はず)なので、「最初の呼び出し」と限定されている(ような気がする(^_^; )。

JIS C では、返す値を引数として持つ exit 関数と等価。

新JIS C でも、返す値の型が int と適合する場合は JIS C と同じだが、下記が加わった。

返す値の型が int と適合する場合、終了する } に達すると値 0 を返す。
※つまり、return 0; と書かなくても、終端 } に達して終了したら値 0 を返す。

返却値の型が int と適合しない場合、戻される終了状態は未規定。
とりあえず何かを書きこまねばと思いネタを探したら、以前某 ML に投稿した、初期の ANSI C JIS C ISO C 規格についての文章が出てきました。

と言うわけで、その時の文章をそのまま引用して( ..)φメモメモ


--
「ANSI C (ANSI X3.159-1989) (ANSI/ISO-IEC 9899-1990[1992]) と ISO C (ISO
/IEC 9899:1990) の関係」

 1989年に ANSI X3.159-1989 が認可され、1990年に ANSI X3.159-1989 を元に
した ISO/IEC 9899:1990 が制定される。その後、米国は ANSI X3.159-1989 廃
止して、ISO/IEC 9899:1990 を正式な規格 (ANSI/ISO-IEC 9899-1990[1992])
として採用。(注:ANSI/ISO-IEC 9899-1990[1992] は正式名称ではなくて、通称
かもしれません。)

「ANSI C (ANSI X3.159-1989) と ISO C (ISO/IEC 9899:1990) の一致性」

 章立てや体裁が異なっているだけで、言語仕様を規定している部分は等しい。
実際、ANSI X3.159-1989 から ISO/IEC 9899:1990 への以降は、元ファイルにな
る troff ファイルのマクロを書き直しただけ。

「JIS C (JIS X 3010-1993) と ISO C (ISO/IEC 9899:1990) の一致性」

 JIS X 3010-1993 は、原則的には ISO/IEC 9899:1990 を忠実に翻訳したもの。
相違は下記の 4 点だが、いずれも規格外の項目なので、規格として
JIS X 3010-1993 と ISO/IEC 9899:1990 に相違はない。
・JIS X 3010-1993 では、日本工業規格として不適当と思われる点について、勝
手に直したりせず、参考(注釈のようなもの)を挿入。
・米国国内規格での事情が書かれている footnote 1 の最後の文を削除。この文
は ANSI X3.159-1989 を取り込んだ際の削除もれで、ISO/IEC 9899:1990 自体
の間違い。
・footnote 29 の最後に、注釈を 1 文追加。
・7.4.2.1 に、日本向けの例を追加。
--