trifle

技術メモ

Cには参照渡しがない

booth.pm

プログラミング言語神経衰弱、というものをバイト先の先輩の家でやる機会があり、とても盛り上がった。言語は16種類くらいあって、一目で分かるもの(BrainfxxkやWhiteSpaceなど)もあれば、なかなか区別しづらいものもあった。その中で、

int add(int a, int& b) {
  return a + b; 
}

これが C ではなく C++ だよね、というのが一目では分からなかった。C には「ポインタ渡し」はあるが、「参照渡し」はない。初歩的な内容なのかもしれないけれど、私はいまいち分かっていなかったので、ちょっとまとめる。


まず、Cについて。ある型 T に対して常に型 T* を作ることができる。Cはきちんとした型推論はないはずなので、この型はただ単に何bitかをひとつの単位として仕切りを作っているという以上の情報はないけれども、とにかく、この型 T*T のアドレスが入る。これがポインタと呼ばれるものだ。

int x = 10;
int* p = &x;
*p = 42;

このように使う。& を変数の前に付けることで、その変数のアドレスを取ることができる。だから p にはアドレスが格納されている。逆に、そのアドレスに格納されている変数を * を付けて取り出すことができる。*p = 42; と書けば、格納されている x42 に書き換わる。
ふつうの高級言語はポインタが隠蔽されているので、例を挙げるのが難しいが、OCaml の参照はこれに非常に似ている。OCaml は基本的には変数がimmutableなのだけれども、その例外として参照がある。

# let x = ref 5;;
val x : int ref = {contents = 5}
# !x;;
- : int = 5
# x := 10;;
- : unit = ()
# !x;;
- : int = 10

ref は変数を格納しているアドレスを取るので C の & と対応しているし、dereference の !x*x と対応している。

さて、ポインタ渡しの話に進む。ポインタ渡しは、名前渡しとは異なり、関数を呼び出した側の変数も書き換わるので、それが用途に適っている場合はポインタ渡しを使う。そもそも文字列 char* のように実態がポインタのものは当然ポインタ渡しをしなければならない。
最近、大学の某実験でアセンブラを書く必要に迫られたが、以下のような int reg(char* reg) というポインタ渡しの関数を書いたりしていた。これは x29 のようにアセンブラに書かれているレジスタを5bitのバイナリの10進数表記 11101 にしたいのでこのようなものを書いている。

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

int reg(char* reg) {
  int n = atoi(++reg);
  int binary = 0;
  int base = 1;
  
  while (n > 0) {
    binary += (n % 2) * base;
    n /= 2;
    base *= 10;
  }

  return binary;
}

int main() {
  printf("%d\n", reg("x29")); // 11101 

  return 0;
}

なぜこれで上手くいくかというと、char* reg にはx29の先頭文字 x のアドレスが格納されていて、これをchar型においてインクリメントすると一個先の 2 が格納されているアドレスに増えるので ++reg29 を意味するようになり、atoi で文字列を数値に変えるので後はお察しの通りである。


次に、C++の参照渡しの話をする。まず、参照型 T& の話をしなければならない。参照型は、先ほどの変数のアドレスを取り出す演算子 & とは全く別物であることに留意する必要がある。

int n = 2;
int& v = n;

この v が参照である。ポインタと似ているようだが、

  • 初期化時にどの変数を指しているかを決める必要がある
  • 別のアドレスを後から指すことはできない

という部分が決定的に異なる。ポインタと比べて指し示す先がガチガチに固まっているので、nv という別名を付けていると考えたほうが良さそうだ。

参照においては、ポインタにおける &* のような記号を使わなくてもいいというメリットがある。つまり、Cでは

void hoge(int* a) { // ポインタ渡し
  *a = 2; // アドレスに格納されている中身を変える
}

int main() {
  int n;
  hoge(&n); // アドレスを引数とする
}

みたいだったのが、C++では

void hoge(int& a) { // 参照渡し
  a = 2;
}

int main() {
  int n;
  hoge(n);
}

というようにすっきり書ける。

また、この参照型を引数として渡す参照渡しは、ポインタ渡しと比べて、NULLポインタを入れられてしまう危険が無いというメリットがある。確かに、上で書いたレジスタを機械語に変換する関数も、本当はNULLチェックを行わねばならない。そういえばCで二分探索木を書いたときNULLチェックが大変だった記憶がある。

なお、上で書いたレジスタを機械語に変換するプログラムは C++ でも実行できるが、当然、充実した C++ の std::string を使うべきだろう。

qiita.com