へっぽこ社会人4年生がプログラミングを頑張る

へっぽこ社会人4年目がプログラミング系統を中心に書きたいことをつらつらと書きます

C のポインタへのキャストをちょっと理解する話

マルチスレッドのプログラムの書き方を勉強するために、 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*)argint へのポインタ型に変換し、 arg の参照先の値を参照するために * 演算子を使う必要があると考えていました。 コンパイルも問題なく通ったので、実行してみると Segumentation fault でコケました。

筆者のイメージでは、上記のコードは、図1 のように動作していると思いました。

f:id:sierra-kilo:20220412172721p:plain
図1. ポインタのキャストによる動作イメージの誤解

疑問点1 のコード部分で整数リテラル 1 がどこかしらのメモリ (図1 中の 0xDEADBEEF) に割り当てられ、それを void* にキャストし、疑問点2 の関数 threadFunc() の引数 arg が別のアドレス (図1 中の 0xCAFEBABE) が割り当てられ、その値に参照元のアドレス 0xDEADBEEF が代入されると思っていました。

解決方法の模索とポインタの理解

明示的に領域を確保

threadFunc() 側のキャストを書き換えただけなので、 pthread_create() の第 4 引数も修正しないといけないのかなと考えました。 整数リテラル 1void* でキャストする代わりに、 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 を代入します。 valueint* なので、 pthread_create() の引数で void* にキャストしています。

こちらは問題なく動作しました。

まず、 malloc() で明示的にメモリ領域 (図2 中 0xDEADBEEF) を確保し、 変数 value の値に、先程確保したアドレス 0xDEADBEEF を指定します。 関数 threadFunc() の引数 arg の値には、main() の変数 value の参照先のアドレス 0xDEADBEEF が格納されます。 argvoid* 型なので、 int* にキャストし、参照先 0xDEADBEEF に格納されている値を取り出すために * 演算子を用いることで、欲しかった値 1 を無事取り出せました。

f:id:sierra-kilo:20220412201351p:plain
図2. ポインタ変数を経由した際の動作イメージ

変数のアドレスの確認

変数を用意し、領域を明示的に確保すると問題なく動作するのに、整数リテラル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 という、プログラム上で確保されていない領域にアクセスしようとしたからです。

f:id:sierra-kilo:20220413164356p:plain
図3. 実際のメモリの動作イメージ

一方で、元のサンプルコードのように、 void* から int にキャストする場合は、 0x01 というアドレスの値を、 1 という整数にキャストしているので、意図した通りに値 1 を取得できます。

コンパイラの警告の内容

ポインタがどのように動作しているかを理解して、サンプルコードが意図した通りに動作するのはなぜかという疑問は解決できました。 しかし、コンパイラは以下のような warning を吐いていました。

warning: cast to smaller integer type 'int' from 'void *' [-Wvoid-pointer-to-int-cast]

実行ファイルは生成され、ちゃんと動作もするのですが、どうにも warning が出ているのが気持ち悪いと感じます。 警告メッセージは、小さいサイズの型にキャストしていると言っているようです。

intvoid* のサイズの比較

それぞれ intvoid* のサイズを比較するために、以下のコードを書いて、実行してみます。

#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 だったので、 intlong に書き換えてコンパイルしてみると、この warning は消えました。

なお、以下の環境で動作確認しました。

intvoid* などのサイズは 環境に依存 するので、上記のコードの 実行結果が異なる場合があります

まとめ

C で int の値をポインタ型へキャストすると、 int の値が参照先のアドレスとして扱われます。 逆にポインタ型を int などの整数型にキャストすると、アドレスの値が整数値として扱われます。 int 以外でも、 longchar でも同様のことができるはずです。

void* から int にキャストするような場合、 int とポインタ型のサイズが異なる場合があるので、コンパイル時に警告が出る可能性があります。 それに、コードの意図が若干わかりづらくなる可能性もあるので、筆者は「明示的に領域を確保」のセクションで書いたコード例のように、ポインタ型の変数を用意し、領域を確保した上で、値を代入して渡す方がより分かりやすいのではないかと考えます。

ポインタの理解が浅いので、「C言語ポインタ完全制覇1をもう一度読み直してみようと思います。 (特に関数ポインタの部分が理解できていないので...)

参考文献