LoginSignup
127
89

More than 3 years have passed since last update.

C で関数に * や & を付けられる件の説明: ***printf の謎

Last updated at Posted at 2019-08-18

規格として C11 (n1570) を参照する。

注意: 途中に入れた図が崩れるようなら、半角・全角比が 1:2 になるようなフォントを使って閲覧してください。

現象

wandbox で実行

ソース:

(  &printf)("  &printf = %p\n", &printf);
    printf ("   printf = %p\n", printf);
(  *printf)("  *printf = %p\n", *printf);
( **printf)(" **printf = %p\n", **printf);
(***printf)("***printf = %p\n", ***printf);

出力例 (環境依存):

  &printf = 0x4003d0
   printf = 0x4003d0
  *printf = 0x4003d0
 **printf = 0x4003d0
***printf = 0x4003d0

この現象を解説する。

関数呼び出し

まず、関数呼び出しについての規定を見る。

The expression that denotes the called function shall have type pointer to function returning void or returning a complete object type other than an array type.

―― n1570 §6.5.2.2/4

訳すとこんな感じ:

呼び出される関数を表す式は、 {void を返す関数か、配列以外の完全型を返す関数} へのポインタ型を持たなければならない。

(英語クソザコ勢なので括弧使ったけど察して)

こういったわけで、実は関数呼び出しを行うとき、関数として指定する式は必ず関数ポインタ型となっている。
printf("hello") したとき、 printf は最終的に関数ポインタ型となっているのである。
冒頭で挙げた例のように、 &printf*printf**printf も、 printf と同じ関数ポインタ型の式へと評価される。

Function Designator (関数指示子)

さてお察しの通り、 printf などの「関数名」はポインタのように扱えるが、実のところその振る舞いは通常のポインタとは異なる。
(たとえば *pp&p が同じアドレスにあるというのは、直観的には p が自分自身のアドレスを持っているように解釈できるが、これは関数を示すものの性質としては明らかにナンセンスである。)

余談だが、配列なども関数名と似たような性質を持っており、ポインタ演算を適用可能ではあるが、 sizeof などに渡したときその動作は明確に異なるため、実態は「先頭要素へのポインタ」そのものではない。

さて、ここで「関数名」と読んだ概念は規格では "function designator" (関数指示子) という名前が付いている。

A function designator is an expression that has function type. Except when it is the operand of the sizeof operator, the _Alignof operator, or the unary & operator, a function designator with type "function returning type" is converted to an expression that has type "pointer to function returning type".

―― n1570 §6.3.2.1/4

訳すとこんな感じ:

関数指示子は関数型を持つ式である。
sizeof 演算子、 _Alignof 演算子、単項 & 演算子のオペランドである場合を除いて、何らかの関数型を持つ関数指示子は、その関数型へのポインタ型を持つ式へと変換される。

この function designator についての演算子の挙動は、以下のように規定されている。

単項 & 演算子

まずは & から:

The unary & operator yields the address of its operand. If the operand has type "type", the result has type "pointer to type". If the operand is (中略). Otherwise, the result is a pointer to the object or function designated by its operand.

―― n1570 §6.5.3.2/3

訳:

単項 & 演算子はオペランドのアドレスを生じる。
オペランドが何らかの型を持っていれば、結果はその型のポインタ型となる。
もしオペランドが (中略)
それ以外の場合、結果はオペランドで指定されたオブジェクトまたは関数へのポインタとなる。

単項 * 演算子

また、 * による derefence も規定される:

The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; (以下略)

―― n1570, §6.5.3.2/4

訳すとこう:

単項 * 演算子は間接参照を意味する。
オペランドが関数へのポインタなら、結果は関数指示子となる。
(以下略)

primary expression の ()

この括弧は関数呼び出しやキャストなどではなく、 2 * (3 + 1) などの素朴な式における括弧である。
この () が function designator にどう影響するかにも規定がある。

A parenthesized expression is a primary expression. Its type and value are identical to those of the unparenthesized expression. It is an lvalue, a function designator, or a void expression if the unparenthesized expression is, respectively, an lvalue, a function designator, or a void expression.

―― n1570, §6.5.1/5

訳すとこんな感じ:

括弧で括られた式は primary expression である。
その型と値は、括弧の中身の式と等価である。
もし括弧の中の式が lvalue、関数指示子、または void 式であったなら、括弧で括られた式もそれぞれ lvalue、関数指示子、void 式となる。

つまり、括弧の中身である function designator がやたら暗黙に型変換されてしまう曖昧な存在だからといって、 () で括っただけでは何の変換も起こらないということである。

実際の式の解釈

さて、上述の規格を前提に式を解釈してみよう。

printf("hello")

  • printf は function designator である。
  • function designator である printf は、 () による関数適用のオペランドである。
    • 呼び出される関数の式は関数へのポインタ型でなければならない (§6.5.2.2/4)。
    • 関数適用は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である printf は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
  • 以上より、式 printf("hello"){{関数ポインタである printf}}("hello") の形になり、 printf の関数呼び出しが行われる (§6.5.2.2/4)。
       printf        ("hello")
| | `-関数指示子-' |          |   printf は関数指示子
| `--関数ポインタ--'          |   printf は関数ポインタ
|                             |
`----------関数適用-----------'   printf("hello") は関数呼び出し

(&printf)("hello")

  • printf は function designator である。
  • function designator である printf は、単項 & 演算子のオペランドである。
    • &printf は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
  • (&printf)&printf と同じ型を持つため、 printf への関数ポインタである (§6.5.1/5)。
  • (&printf)("hello"){{printf への関数ポインタ}}("hello") の形になり、 printf の関数呼び出しが行われる (§6.5.2.2/4)。
   (  &    printf       )  ("hello")
| | |   `-関数指示子-' | |          |   printf は関数指示子
| | `---関数ポインタ---' |          |   &printf は関数ポインタ
| |                      |          |
| `-----関数ポインタ-----'          |   (&printf) は関数ポインタ
|                                   |
`--------------関数適用-------------'   (&printf)("hello") は関数呼び出し

(*printf)("hello")

  • printf は function designator である。
  • function designator である printf は、単項 * 演算子のオペランドである。
    • 単項 * 演算子は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である printf は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
    • *printf* {{printf への関数ポインタ}} の形になり、単項 * 演算子によりこれは printf 関数の function designator となる (§6.5.3.2/4)。
  • (*printf)*printf と同じ型を持つため、 printf 関数の function designator である (§6.5.1/5)。
  • function designator である (*printf) は、 () による関数適用のオペランドである。
    • 呼び出される関数の式は関数へのポインタ型でなければならない (§6.5.2.2/4)。
    • 関数適用は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である (*printf) は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
  • (*printf)("hello"){{printf への関数ポインタ}}("hello") の形になり、 printf の関数呼び出しが行われる (§6.5.2.2/4)。
     (  *       printf         )    ("hello")
| | | |   | `-関数指示子-' |  | | |          |    printf は関数指示子
| | | |   `--関数ポインタ--'  | | |          |    printf は関数ポインタ
| | | |                       | | |          |
| | | `-------関数指示子------' | |          |    *printf は関数指示子
| | |                           | |          |
| | `---------関数指示子--------' |          |    (*printf) は関数指示子
| `----------関数ポインタ---------'          |    (*printf) は関数ポインタ
|                                            |
`-------------------関数適用-----------------'    (*printf)("hello") は関数呼び出し

(***printf)("hello")

  • printf は function designator である。
  • function designator である printf は、単項 * 演算子のオペランドである。
    • 単項 * 演算子は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である printf は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
    • *printf* {{printf への関数ポインタ}} の形になり、単項 * 演算子によりこれは printf 関数の function designator となる (§6.5.3.2/4)。
  • function designator である *printf は、単項 * 演算子のオペランドである。
    • 単項 * 演算子は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である *printf は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
    • **printf* {{printf への関数ポインタ}} の形になり、単項 * 演算子によりこれは printf 関数の function designator となる (§6.5.3.2/4)。
  • function designator である **printf は、単項 * 演算子のオペランドである。
    • 単項 * 演算子は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である **printf は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
    • ***printf* {{printf への関数ポインタ}} の形になり、単項 * 演算子によりこれは printf 関数の function designator となる (§6.5.3.2/4)。
  • (***printf)***printf と同じ型を持つため、 printf 関数の function designator である (§6.5.1/5)。
  • function designator である (***printf) は、 () による関数適用のオペランドである。
    • 呼び出される関数の式は関数へのポインタ型でなければならない (§6.5.2.2/4)。
    • 関数適用は、 sizeof, _Alignof, 単項 & 演算子のいずれでもないため、 function designator である (***printf) は printf 関数を指す関数ポインタへと変換される (§6.5.3.2/3)。
  • (***printf)("hello"){{printf への関数ポインタ}}("hello") の形になり、 printf の関数呼び出しが行われる (§6.5.2.2/4)。
   (   *   *   *     printf                   )  ("hello")
| | | | | | | | | `-関数指示子-' | | | | | | | |          |   printf は関数指示子
| | | | | | | | `--関数ポインタ--' | | | | | | |          |   printf は関数ポインタ
| | | | | | | |                    | | | | | | |          |
| | | | | | | `-----関数指示子-----' | | | | | |          |   *printf は関数指示子
| | | | | | `------関数ポインタ------' | | | | |          |   *printf は関数ポインタ
| | | | | |                            | | | | |          |
| | | | | `---------関数指示子---------' | | | |          |   **printf は関数指示子
| | | | `----------関数ポインタ----------' | | |          |   **printf は関数ポインタ
| | | |                                    | | |          |
| | | `-------------関数指示子-------------' | |          |   ***printf は関数指示子
| | |                                        | |          |
| | `---------------関数指示子---------------' |          |   (***printf) は関数指示子
| `----------------関数ポインタ----------------'          |   (***printf) は関数ポインタ
|                                                         |
`---------------------------関数適用----------------------'   (***printf)("hello") は関数呼び出し

ヤバいですね

ヤバいですね。

まあ実用的な知識とはいえない気がするので C 言語クイズにでも使ってください。

References

127
89
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
127
89