【C言語】void型とvoid*型(void型ポインタ)について解説

voidとvoid*型の解説ページのアイキャッチ

このページにはプロモーションが含まれています

このページではC言語における void 型と void* 型(void 型ポインタ)について解説していきます。

void

では早速 void 型とはどんな型なのかを解説していきたいと思います。

型の役割

まずその前に、C言語における型の役割について復習しておきましょう。

C言語ではプログラミングする際には頻繁に「型」を扱うと思います。

例えば下記のようなときは必ず型を指定しているはずです。

  • 変数宣言(変数の型)
  • 関数作成時(戻り値と引数の型)
  • キャスト(変換先の型)

C言語の一般的な型では、型ごとに「どんなデータであるか」が定義されています。

具体的には、型ごとに「データのサイズ」と「データの扱われ方(特に符号ありで扱うか符号なしで扱うか)」が定義されています。

例えば int 型は下記のように定義された型になります。

  • サイズ:4バイト(環境によって異なる可能性あり)
  • 扱われ方:符号ありとして扱う

で、プログラムは変数を扱うときにこの「どんなデータであるか」の定義に従って動作します。

例えば int 型の変数宣言が行われたのであれば、プログラム実行時にその変数用のメモリを4バイト分保確保します。

さらに、その変数を用いて演算が行われる場合は、その変数を符号ありデータとして扱って演算結果を算出します。

変数だけでなく、関数における戻り値や引数の型も同じです。

例えば下記のような関数 func であれば、4バイトの符号なしのデータ(unsigned int)と1バイトの符号あり(char)のデータを引数として受け取り、4バイトの符号ありのデータ(int)を戻り値として返却する関数として動作します。

関数の引数と戻り値の型
int func(unsigned int a, char b) {
    return (int)a + (int)b;
}

こんな感じで、C言語では型ごとに「どんなデータであるか」が定義されており、この定義に従ってプログラムが動作するようになっています。

スポンサーリンク

void 型とは

ここまで一般的な型について復習してきましたが、void 型はコレらとは全く異なる型になります。

void 型とは「無い」ことを示す型になります。

他の int 型や char 型と異なり、void 型には「どんなデータであるか」が定義されていません。単に「無い」ことを示す型です。

ですので、void 型の変数というものは存在しません。void 型の変数宣言は禁止されており、void 型の変数宣言を行うとコンパイルエラーが発生するようになっています。

コレは void 型には「どんなデータであるか(サイズや扱われ方)」が定義されていないためです。

「どんなデータであるか」が定義されていないと、その型の変数用に確保必要なメモリサイズや、その変数の扱い方をコンパイラが解釈することができず、コンパイルに失敗してしまうのです。

じゃあこんな変数宣言もできない型にどんな存在意義があるんだよ?

こう思う人も多いと思います。この void 型を利用する最大のメリットは、関数の引数や関数の戻り値が「無い」ことを明示的に示すことができる点です。というかコレ以外の用途は私は知らないです…。

関数では、当然引数や戻り値を持たないものが存在します。このような引数や戻り値が「無い」ことを明示的に示すために void が使用されます。

例えば下記のように宣言された関数では引数が無いことが、

引数が無い関数
int func(void);

下記のように宣言された関数では戻り値が無いことが一目で分かりますよね?

戻り値が無い関数
void func(int, char);

こんな感じで「無い」ことを明示的に示すのに void は有用です。

単に書き忘れただけでは無いことも、あえてこの void を記述していることで伝えることもできます。

まあはっきり言ってこの void は関数宣言や関数定義くらいでしか見かけません。とりあえず下記のことを覚えておけば void に関しては困らないと思います。

  • void は「無い」ことを示す型である
  • void 型の変数宣言はできない

void* 型(void 型ポインタ)

void 型は変数宣言できず、単に「無い」ことを示す型でした。

一方で void* 型は変数宣言することができ、様々な用途で利用されています。

まずは void* 型を理解するためにポインタの型の役割を復習し、続いて void* 型がどのような型で、どのような用途で利用されているかを解説していきたいと思います。

ポインタの型の役割

ではポインタの型の役割について解説していきたいと思います。

まずはポインタについて簡単に復習しましょう。

ポインタはアドレスを格納する変数

ポインタも通常の変数同様に変数宣言を行ってから使用します。

この変数宣言時には当然 “型” を指定します。C言語では変数を宣言するときに必ず型を指定する必要があります。

さらに、ポインタでは他の変数を指す(他の変数のアドレスを格納する)ことができます。

例えば int* 型のポインタであれば int 型の変数を指すことができます。

ポインタの使い方の復習
int *p;
int a;

p = &a;

ポインタの型も「どんなデータであるか」が定義されている

さらに一般的なポインタはアドレスを格納するだけでなく、ポインタの型自体にも「どんなデータであるか」が定義されています。

具体的には例えば下記が定義されています。

  • データのサイズ(アドレスのバイト長)
  • 加算・減算時の増減量
  • 参照先のデータのサイズ
  • 参照先のデータの扱われ方

「加算・減算時の増減量」について説明すると、ポインタの基になる型(int* 型のポインタであれば intchar* 型のポインタであれば char)のサイズに応じて +1-1 されたときのアドレスの増減量が異なります。例えば int* 型の変数に +1 すれば、変数に格納されたアドレスが +4 されます。

ポインタへの演算時の増加量を図示したもの

またポインタ変数の前に * をつければ、そのポインタの指すデータを参照(アクセス)することができます。

このときに参照されるデータは、ポインタの基になる型(int* 型のポインタであれば intchar* 型のポインタであれば char)のデータとして扱われます。

ポインタ参照時のデータの扱われ方

このように、ポインタの型自体にも「どんなデータであるか」が定義されています。

「どんなデータであるか」が定義されているため、ポインタに異なる型の変数を指させてしまうと、その変数の型とは異なったサイズ・扱われ方をしてしまう可能性があるのでコンパイル時に警告が出ます。

例えば下記のように int* 型のポインタ変数に double の変数を指させると、

ポインタの使い方の復習
int *p;
double a;

p = &a;

コンパイル時に下記のような警告文が出ます(コンパイラによって異なると思います)。

warning: incompatible pointer types
      assigning to 'int *' from 'double *' [-Wincompatible-pointer-types]

この「ポインタの型の役割」についての詳細は下記で解説していますので詳しくはこのページを参照していただきたいです。

ポインタの型の解説ページアイキャッチ 【C言語】ポインタの「型」について解説

スポンサーリンク

void* 型(void 型ポインタ)とは

では void* 型(void 型ポインタ)とはどのような型であるかについて解説していきたいと思います。

void* 型は単にアドレスを格納する型

一般的なポインタ型(int* 型や char* 型など)は単にアドレスを格納するだけでなく、「どんなデータであるか」が定義されていました。

それに対して void* 型は単にアドレスを格納する型です。「どんなデータであるか」が定義されていません。

「どんなデータであるか」が定義されていないので、void* 型変数にどんな型の変数を指させてもデータの不整合が起きず、どんな変数でも指させることができるようになっています。

こういった特性から、void* 型は汎用ポインタと呼ばれることもあります。

例えば下記のように void* 型変数 vp に様々な型の変数のアドレスを格納しても(様々な型の変数を指させても)、警告は出ません。

void*型変数へのアドレス格納
int main(void) {
    void *vp;

    int a;
    char b;
    double c;
    long long d;
    int *ip;

    vp = &a;
    vp = &b;
    vp = &c;
    vp = &d;
    vp = &ip;

    return 0;
}

こんな感じで void* 型は単なるアドレスを格納する型であり、どんな型の変数を指させても警告が表示されません。

void* 型変数への演算時の動きは未定義

ただし、void* 型は他の一般的なポインタとは異なり「どんなデータであるか」が定義されていませんので、void* 型変数に加算・減算をした時の増減量は未定義となっています。

未定義とは「どう動くか保証しない」ということです。

環境によって ±1 される場合もあれば ±4 されるような場合もありうるということです。環境によって意図した動きにならないので、結論的には void* 型変数への演算はするな、ということになります。

他の一般的なポインタ型の場合は、前述の通り加算した時の増加量は「その基となる型のサイズ」と定義されています。例えば int 型のサイズが 4 の環境であれば、int* 型の変数に +1 すれば、必ずその変数の値は +4 されます。

ですので、一般的なポインタ型変数への加算や減算は行っても問題ないのですが、void* 型変数の場合は問題あるので注意しましょう。

void* 型変数の指すデータは参照できない

また、void* 型は他の一般的なポインタとは異なり「どんなデータであるか」が定義されていないため、void* 型変数の指すデータへの参照はできません。

例えば下記のように void* 型変数の vp の指すデータへの参照を行った場合(*vp)、コンパイル時点でエラーになります(エラーになるのは printf 関数を実行している行です)。

void*型変数への参照
int main(void) {
    void *vp;
    int a;
    
    a = 100;
    vp = &a;
    
    printf("%d\n", *vp);

    return 0;
}

コレは、他のポインタ方同様に、void* 型変数に * 演算子を付加して参照したデータが void 型のデータとして扱われることを考えるとエラーになる理由がイメージしやすいと思います。

前述の通り void 型は「無い」ことを表す型であり、void 型のデータ(変数)はC言語では扱えないのです。なので void 型のデータを扱おうとしてエラーになっているのです。

void* 型変数はキャストしてから参照する

void* 型変数の指すデータを参照したい場合は、他のポインタ型に「キャストを行ってから」参照を行います。

例えば下記のソースコードは先ほど示したソースコードに対し、printf 関数実行時に void* 型変数 vpint* 型にキャストしてから参照するように変更したものになります。

void*型変数への参照
int main(void) {
    void *vp;
    int a;
    
    a = 100;
    vp = &a;
    
    printf("%d\n", *(int*)vp);

    return 0;
}

この場合、int* 型にキャストを行ってから参照することになるため、void* 型変数への参照ではなく int* 型変数への参照として扱われ、プログラムは正常に動作します。

こんな感じで他のポインタ型へのキャストを利用することで、void* 型変数が指すデータへの参照を行うことができます。

ただし、キャストする先の型は、基本的に void* 型変数に指させているデータの型と対応させる必要があります。

例えば下記のように一旦 char 型の変数を指した後に int* 型にキャストして参照すると、意図した結果にならない場合があります。場合によってはセグメンテーションフォールトになる場合もあります。

キャスト時の注意
int main(void) {
    void *vp;
    char a;
    
    
    a = 100;
    vp = &a;
    
    printf("%d\n", *(int*)vp);

    return 0;
}

私が実行すると、出力結果は下記のようになりました。

-1074479260

コレは void* 型変数 vp に1バイトのデータである char 型変数を指させているにも関わらず、参照時には4バイトのデータである int 型として扱っているためです。

下の図のように3バイト分無駄に参照しているので、ゴミ値を含んで結果を表示してしまっています。

他の型にキャストした時のデータ参照

しかも、不適切なキャストを行っているにも関わらずコンパイル時に警告が出ないのでより一層注意が必要です。

void* 型変数は前述の通りどんな型の変数も指すことができますのでアドレス格納時やキャスト時に警告は出ません。

void* 型はいろんな型の変数を指させることができるので便利ではありますが、プログラマー自身で型の不整合等をチェックする必要がありますので気をつけてください。割と上級者向けの型だと思います。

void* 型の使いどころ

最後にこの void* 型の使いどころについて解説したいと思います。

アドレス管理のみを行いたい時

単にアドレスのみを扱いたいときに void* 型を使ったりします。

単純にアドレスを表示したい場合や、malloc したメモリの先頭アドレスを管理したい場合などに使えます。

後者に関しては下記の「メモリリーク検出する方法」の解説ページで実例を示していますので、興味のある型は是非読んでみてください。

メモリ解放忘れの検出方法解説ページのアイキャッチ 【C言語】メモリの解放忘れ(メモリリーク)を自力で検出する方法

汎用的な関数を作成したい時

あとは汎用的な関数を作成したい時にも void* 型を使ったりします。

標準関数でも例えば下記のような関数は全て引数に void* 型が使用されています。

  • free
  • memset
  • memcpy

コレらの関数は、引数が void* 型なので、どのポインタ型であるかを意識することなく使用できます。便利です。ポインタの型に応じて異なる関数を使用しなければならないと面倒ですよね…。

関数を自作する場合も void* 型を利用することで汎用的な関数に仕上げることができます。

例えば下記ソースコードの printData 関数は引数の1つが void* 型であり、どんなポインタ型でも引数に指定可能な関数になっています。

実際に printData 関数で扱えるのは STRUCT_INT_T*STRUCT_CHAR_T*STRUCT_PTR_T* 型のポインタのみですが、1つの関数で様々な型のデータを扱うことができ、汎用的な関数として作成できていることが確認できると思います。 

void*型を引数とした自作関数
#include <stdio.h>

typedef enum {
    TYPE_INT,
    TYPE_CHAR,
    TYPE_PTR
} TYPE_T;

typedef struct {
    int a;
    int b;
    int c;
} STRUCT_INT_T;

typedef struct {
    char a;
    char b;
    char c;
} STRUCT_CHAR_T;

typedef struct {
    char *a;
    char *b;
    char *c;
} STRUCT_PTR_T;

void printData(void* data, TYPE_T type) {

    STRUCT_INT_T *int_data;
    STRUCT_CHAR_T *char_data;
    STRUCT_PTR_T *ptr_data;

    switch (type) {
        case TYPE_INT:
            int_data = (STRUCT_INT_T *)data;
            printf("a = %d\n", int_data->a);
            printf("b = %d\n", int_data->b);
            printf("c = %d\n", int_data->c);
            break;
        case TYPE_CHAR: 
            char_data = (STRUCT_CHAR_T *)data;
            printf("a = %c\n", char_data->a);
            printf("b = %c\n", char_data->b);
            printf("c = %c\n", char_data->c);
            break;
        case TYPE_PTR:
            ptr_data = (STRUCT_PTR_T *)data;
            printf("a = %p\n", ptr_data->a);
            printf("b = %p\n", ptr_data->b);
            printf("c = %p\n", ptr_data->c);
            break;
        default:
            break;
    }
}

int main(void) {

    STRUCT_INT_T int_data;
    STRUCT_CHAR_T char_data;
    STRUCT_PTR_T ptr_data;

    int_data.a = 10;
    int_data.b = 20;
    int_data.c = 30;

    char_data.a = 'a';
    char_data.b = 'b';
    char_data.c = 'c';

    ptr_data.a = &(char_data.a);
    ptr_data.b = &(char_data.b);
    ptr_data.c = &(char_data.c);

    printData(&int_data, TYPE_INT);
    printData(&char_data, TYPE_CHAR);
    printData(&ptr_data, TYPE_PTR);

    return 0;
    
}

まとめ

このページでは void 型と void* 型(void 型ポインタ)について解説しました。

void 型は「無い」ことを表す型であり、主に関数作成時に使用します。void 型の変数宣言はできません。

void* 型は「単にアドレスを格納する」型です。変数宣言することも可能ですが、void* 型のポインタが指すデータへの参照を行うためには必ずキャストを利用する必要があります。

特に void* 型は、型の不整合を招きやすいので上級者向けの型だと思います。

ですが、この void* 型を用いてプログラミングすることで、C言語における「型の重要性」に気付けやすくなるのでは無いかと思います。

是非初心者の方にも void 型を利用したプログラミングに挑戦し、型の重要性を実感してみていただきたいです!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示