マルチスレッドのプログラムの書き方を勉強するために、 C の pthreadsライブラリを使ったマルチスレッドプログラミングの参考書のサンプルコードを写経して動作を確認して勉強していました。
スレッドを作成する際に、 pthread_create()
という関数を利用するのですが、サンプルコードで少し奇妙に感じる書き方をしていました。
pthread_create(&thread, NULL, threadFunc, (void*)1);
(void*)1
が何を意味しているか全然わからん!!!
何をやっているのか分からなかったので、 (void*)1
と書く代わりに、 int*
の変数を使って書いていました。
少し日をおいてから、ふとしたきっかけから解決したので、自分の備忘録を兼ねてまとめておこうと思います。
サンプルコードの疑問点
冒頭でも挙げたように、 (void*)1
がどのように動くのか、何を意味しているのかがわかりませんでした。
スレッド作成時に実行する関数の引数に、 int
型の値を渡したいが、 pthread_create()
のインタフェースが void*
だからキャストしているのかなと思いました。
しかし、 1
は 整数型 であるのに対し、 void*
へのキャストは ポインタ型 に変換しています。
なぜこのキャストで動くのか疑問でした。 (疑問点1)
また、関数側では、受け取った引数を void*
から int
にキャストしていました。
疑問点 1 と真逆のキャストですが、ポインタから整数型へのキャストでどのように動作するのか疑問でした。 (疑問点2)
#include <stdio.h> #include <pthread.h> void* threadFunc(void*); int main(int argc, char **argv) { pthread_t thread; // 疑問点1: (void*)1 で通るのなんで? pthread_create(&thread, NULL, threadFunc, (void*)1); pthread_join(thread, NULL); return 0; } void* threadFunc(void* arg) { // 疑問点2: void* を int にキャスト? int n = (int)arg; printf("n: %d\n", n); return NULL; }
ポインタのキャストの誤解
疑問点2 について、 void*
はポインタだから、 int*
に変換した上で参照先の値を取り出さないといけないのではないかと考えました。
自分なりに考えて、疑問点2 のコード箇所を以下のように書いていました。
int n = *(int*)arg;
まず (int*)arg
で int
へのポインタ型に変換し、 arg
の参照先の値を参照するために *
演算子を使う必要があると考えていました。
コンパイルも問題なく通ったので、実行してみると Segumentation fault でコケました。
筆者のイメージでは、上記のコードは、図1 のように動作していると思いました。
疑問点1 のコード部分で整数リテラル 1
がどこかしらのメモリ (図1 中の 0xDEADBEEF
) に割り当てられ、それを void*
にキャストし、疑問点2 の関数 threadFunc()
の引数 arg
が別のアドレス (図1 中の 0xCAFEBABE
) が割り当てられ、その値に参照元のアドレス 0xDEADBEEF
が代入されると思っていました。
解決方法の模索とポインタの理解
明示的に領域を確保
threadFunc()
側のキャストを書き換えただけなので、 pthread_create()
の第 4 引数も修正しないといけないのかなと考えました。
整数リテラル 1
を void*
でキャストする代わりに、 int*
型の変数を用意してから、その変数を void*
にキャストして引数に渡してみることにしました。
疑問点1 の部分を以下のように書き換えました。
// int* 型の変数 arg を宣言して malloc() でメモリ領域を確保 int* value = malloc(sizeof(int)); // 値 1 を arg の参照先に代入 *value = 1; // arg を void* にキャストして pthread_create() の第 4 引数に指定 pthread_create(&thread, NULL, threadFunc, (void*)value);
一旦 value
という int*
型の変数に、 malloc()
で領域を確保し、 *value = 1
で実際に threadFunc
に渡すための値 1
を代入します。
value
は int*
なので、 pthread_create()
の引数で void*
にキャストしています。
こちらは問題なく動作しました。
まず、 malloc()
で明示的にメモリ領域 (図2 中 0xDEADBEEF
) を確保し、 変数 value
の値に、先程確保したアドレス 0xDEADBEEF
を指定します。
関数 threadFunc()
の引数 arg
の値には、main()
の変数 value
の参照先のアドレス 0xDEADBEEF
が格納されます。
arg
は void*
型なので、 int*
にキャストし、参照先 0xDEADBEEF
に格納されている値を取り出すために *
演算子を用いることで、欲しかった値 1
を無事取り出せました。
変数のアドレスの確認
変数を用意し、領域を明示的に確保すると問題なく動作するのに、整数リテラルを void*
でキャストするとうまく動作しないのはなぜだろうと疑問に思いました。
そこで、 (void*)1
とキャストした際に、 threadFunc()
の引数 arg
の参照先がどうなるのか確認してみることにしました。
以下のようなコードを書いて、動作を確認してみることにしました。
#include <stdio.h> // void* 型の引数を 1 つ受け取る手続き func() を宣言 void func(void*); int main(int argc, char **argv) { // func() の引数に 整数リテラル 1 を void* にキャストして渡す func((void*)1); return 0; } void func(void* arg) { // arg のアドレスを画面に出力して確認する printf("arg address: %p\n", arg); }
コードをコンパイルして実行すると、以下の実行結果が得られました。 (行頭の $
はプロンプト)
$ ./a.out arg address: 0x1 $
この実行結果から、 arg
の参照先は 0x01
であることがわかります。
つまり、図1 のように、 1
という整数リテラルの値がどこかしらのメモリに割り当てられて格納されるのではなく、 void*
でキャストすることで、図3 のように参照先が 1
, つまり 0x01
のアドレスを参照するという意味だったのです。
「ポインタのキャストの誤解」のセクションで修正したプログラムが Segumentation fault を起こしたのは、 0x01
という、プログラム上で確保されていない領域にアクセスしようとしたからです。
一方で、元のサンプルコードのように、 void*
から int
にキャストする場合は、 0x01
というアドレスの値を、 1
という整数にキャストしているので、意図した通りに値 1
を取得できます。
コンパイラの警告の内容
ポインタがどのように動作しているかを理解して、サンプルコードが意図した通りに動作するのはなぜかという疑問は解決できました。 しかし、コンパイラは以下のような warning を吐いていました。
warning: cast to smaller integer type 'int' from 'void *' [-Wvoid-pointer-to-int-cast]
実行ファイルは生成され、ちゃんと動作もするのですが、どうにも warning が出ているのが気持ち悪いと感じます。 警告メッセージは、小さいサイズの型にキャストしていると言っているようです。
int
と void*
のサイズの比較
それぞれ int
と void*
のサイズを比較するために、以下のコードを書いて、実行してみます。
#include <stdio.h> int main(int argc, char **argv) { // int のサイズ printf("sizeof(int): %lu\n", sizeof(int)); // void* のサイズ printf("sizeof(void*): %lu\n", sizeof(void*)); return 0; }
実行結果は以下の通りでした。 (行頭の $
はプロンプト)
$ ./a.out sizeof(int): 4 sizeof(void*): 8 $
筆者の環境では、 void*
の型サイズが 8 なのに対して、 int
の型サイズが 4 であることがわかりました。
このサイズの違いが warning になっている原因のようです。
ちなみに、動作確認した環境では long
の型サイズが 8 だったので、 int
を long
に書き換えてコンパイルしてみると、この warning は消えました。
なお、以下の環境で動作確認しました。
- MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)
int
や void*
などのサイズは 環境に依存 するので、上記のコードの 実行結果が異なる場合があります。
まとめ
C で int
の値をポインタ型へキャストすると、 int
の値が参照先のアドレスとして扱われます。
逆にポインタ型を int
などの整数型にキャストすると、アドレスの値が整数値として扱われます。
int
以外でも、 long
や char
でも同様のことができるはずです。
void*
から int
にキャストするような場合、 int
とポインタ型のサイズが異なる場合があるので、コンパイル時に警告が出る可能性があります。
それに、コードの意図が若干わかりづらくなる可能性もあるので、筆者は「明示的に領域を確保」のセクションで書いたコード例のように、ポインタ型の変数を用意し、領域を確保した上で、値を代入して渡す方がより分かりやすいのではないかと考えます。
ポインタの理解が浅いので、「C言語ポインタ完全制覇」1をもう一度読み直してみようと思います。 (特に関数ポインタの部分が理解できていないので...)
参考文献
-
新・標準プログラマーズライブラリ C言語 ポインタ完全制覇 という新装版が出ています。↩