void ポインタ

プログラミングといえば C です。圧倒的存在感を誇る、プログラミング言語界の重鎮、それが C です。Linux の基本は C なので、非プログラマのシスアドも知っておいて損はないだろう、ということで勉強を始めました。入門書を読み終えて次に進むにはどうしたらいいだろうと考え、思い付いたのがこの C 言語脱初心者シリーズです。入門書には載っていなさそうなネタを取り上げていきます。

シリーズ第 1 回は void ポインタです。一見用途不明の void ポインタですが、例えばコンテナを実装する時に大活躍します。コンテナにはリスト、デック、キュー、ツリー等々色々ありますが、任意の型のデータを保存できないと不便です。

void* を使わない不便なリストのサンプルコードを見てみましょう (なお、例は全て Windows の MinGW/MSYS 環境を対象にしています (“Linux 修験道” なのに!?))。以下は int 型のリストの実装例です:

/* coding: cp932
 * ファイル名: list.c
 */
#include <stdio.h>
#include <stdlib.h>

struct List
{
  int data;
  struct List *next;
};
typedef struct List List;

List *List_prepend (List *list, int data);
void List_display (List *list);

int
main (int argc, char *argv[])
{
  List *list;
  list = List_prepend (NULL, 1);
  list = List_prepend (list, 2);
  list = List_prepend (list, 3);
  List_display (list);
  return 0;
}

/**
 * 既存のリストにデータを追加する関数です。
 * list に NULL を渡すことは、リスト自体の初期化を行うことに相当します。 GLib 参照。
 */
List *
List_prepend (List *list, int data)
{
  List *new_list = (List *) malloc (sizeof (List));
  new_list->data = data;
  new_list->next = list;
  return new_list;
}

void
List_display (List *list)
{
  List *current;
  int i = 1;
  for (current = list; current; current = current->next)
    {
      printf ("%d つめの値: %d\n", i, current->data);
      ++i;
    }
}

下は実行結果です:

$ gcc -Wall -pedantic list.c
$ ./a.exe
1 つめの値: 3
2 つめの値: 2
3 つめの値: 1

これで int 型のリストはバッチリですが、他の型、例えば構造体を保存するために、その都度対応するリストコンテナを作るのはいかにも無駄です。

この問題に対処するため、List 構造体と関連する関数の int data を void *data に置き換えました:

/* coding: cp932
 * ファイル名: list2.c
 */
#include <stdio.h>
#include <stdlib.h>

struct List
{
  void *data;
  struct List *next;
};
typedef struct List List;

List *List_prepend (List *list, void *data);
void List_display_int (List *list);
void List_display_string (List *list);

int
main (int argc, char *argv[])
{
  List *int_list, *string_list;

  int_list = List_prepend (NULL, (void *) 1);
  int_list = List_prepend (int_list, (void *) 2);
  int_list = List_prepend (int_list, (void *) 3);
  List_display_int (int_list);

  string_list = List_prepend (NULL, (void *) "!");
  string_list = List_prepend (string_list, (void *) "world");
  string_list = List_prepend (string_list, (void *) "Hello ");
  List_display_string (string_list);
  return 0;
}

List *
List_prepend (List *list, void *data)
{
  List *new_list = (List *) malloc (sizeof (List));
  new_list->data = data;
  new_list->next = list;
  return new_list;
}

void
List_display_int (List *list)
{
  List *current;
  int i = 1;
  for (current = list; current; current = current->next)
    {
      printf ("%d つめの値: %d\n", i, (int) current->data);
      ++i;
    }
}

void
List_display_string (List *list)
{
  List *current;
  for (current = list; current; current = current->next)
    {
      printf ((char *) current->data);
    }
  printf ("\n");
}

void* はポインタなので、その大きさは 32 ビットシステムなら 32 ビット、64 ビットシステムなら 64 ビットという具合で、上のように int 値を直接保存するには十分です。

データをリストに保存する時に void* にキャストしていますが、なくても動きます。ですが、コンパイラからの警告をなくすために、明示的にキャストしています。

下は実行結果です:

$ gcc -Wall -pedantic list2.c
$ ./a
1 つめの値: 3
2 つめの値: 2
3 つめの値: 1
Hello world!

これで任意の型のデータが、void ポインタを通して間接的に (int 値等ポインタより小さいデータは直接) 保存できます。void* は “型は分からないけど、とにかく何かのデータ” であることを明示するための便利なポインタというわけです。

List 構造体の data はただのポインタですが、C の組み込み配列のように予めメモリ領域を確保しておいて、データをそこに直接保存したい場合があるかも知れません。メモリの解放が簡単にできるというメリットあります。その場合、例えば以下のようにします:

/* coding: cp932
 * ファイル名: list3.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct List
{
  char data[100];
  struct List *next;
  size_t size;
};
typedef struct List List;

List *List_prepend (List *list, void *data, size_t size);
void List_display_int (List *list);
void List_display_string (List *list);

int
main (int argc, char *argv[])
{
  List *int_list, *string_list;
  int one = 1, two = 2, three = 3;

  int_list = List_prepend (NULL, (void *) &one, sizeof (int));
  int_list = List_prepend (int_list, (void *) &two, sizeof (int));
  int_list = List_prepend (int_list, (void *) &three, sizeof (int));
  List_display_int (int_list);

  string_list = List_prepend (NULL,
      (void *) "!", sizeof (char) * 2);
  string_list = List_prepend (string_list,
      (void *) "world", sizeof (char) * 6);
  string_list = List_prepend (string_list,
      (void *) "Hello ", sizeof (char) * 7);
  List_display_string (string_list);
  return 0;
}

List *
List_prepend (List *list, void *data, size_t size)
{
  List *new_list = (List *) malloc (sizeof (List));
  memcpy (new_list->data, data, size);
  new_list->next = list;
  new_list->size = size;
  return new_list;
}

void
List_display_int (List *list)
{
  List *current;
  int i = 1;
  int *data = (int *) malloc (sizeof (int));
  for (current = list; current; current = current->next)
    {
      memcpy ((void *) data, current->data, current->size);
      printf ("%d つめの値: %d\n", i, *data);
      ++i;
    }
  free (data);
}

void
List_display_string (List *list)
{
  List *current;
  for (current = list; current; current = current->next)
    {
      // 文字列の表示は、先頭のアドレスからヌル文字の直前までです。
      // なので、List_display_int のようなメモリ操作は不要です。
      printf ((char *) current->data);
    }
  printf ("\n");
}

任意のデータのコピーを List 構造体に保存するため、data を char の配列にしました。また、データの大きさを記録しておかないとデータが正しく取り出せなくなるので、size というメンバを追加しています。なお、バイト列の保存には、伝統的に char[] が使われるようです。char は 1 バイトで計算しやすいから、ということでしょうか。

さて、リストに保存したデータを表示するための関数、List_display_int と List_display_string ですが、型ごとに関数を書き足していくのは不便です。型に依存しない、もっと汎用性のある関数を書く上で重要なのが関数ポインタです。これについてはまた次回。乞う ご期待!

(コウヅ)



グルメ

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中