こんにちは、ナナです。
本記事では「void型」という、少し特殊なデータ型を紹介しましょう。
C言語で「void型」が登場するシーンは次の2つです。
「void型」を知るために、これらを深堀りしていきます。
「データ型」と「void型」の関係性
C言語は「型」に支配された言語です。
変数にも関数にも「型」が存在し、「型」という枠組みの中で厳密に情報が管理されています。
この「型」の中で異質な存在であるのが「void型」です。異質であるがゆえに扱い方が他の型とは違うのです。
「void型」を理解するためには、データ型をしっかりと理解する必要があります。
C言語の情報はデータ型という形で管理されている
C言語では「データ型」という変数や関数の形を決める情報があります。
これはクッキーの金型のような形を決めるためのものであり、「データ型」とは、C言語における変数や関数の形を決めるためのものなのです。
C言語における代表的なデータ型は「char型」「int型」「float型」といったものになります。
このデータ型によって「情報のサイズ」や「情報の種類」を決めているのです。
「データ型」を知ることはC言語において基本中の基本です。皆さんが作るプログラムでは、皆さん自身がデータ型を選択する必要があるのです。
「void型」は数あるデータ型のひとつである
それでは「void型」とは何なのか?「型」と付いているわけですから、データ型のひとつなのですが、他の型とは役割が違います。
void型とは「型がないことを示す型」
です。
英語である「void」は「無効・ない・虚」といった意味になります。「void型」とはまさしく、型がないことを示している型なのです。
「ない」ことを示すってどういうこと?
「ない」ことを示すって不可思議なことではないんです。皆さんにとって身近な数の「0(ゼロ)」という概念と似ています。
古くは「1」「2」「3」といった”存在すること”を前提とした自然数という概念が基本でした。
ここに、”存在しない”ことを示す「0」という概念を作ることで、数の扱いはめちゃくちゃ便利になったのです。
これと同じで「型がない」ことを示すためのデータ型として、「void型」と呼ばれる型を作ったのです。
「void型」を作ったことにより、「型がない」という特殊な状況を表現できるようにしたのです。これが「void型」の存在意義です。
関数の引数と戻り値の型として使われるvoid型
まず、「void型」が使用される代表的なシーンは「関数の引数と戻り値のデータ型」です。
関数の「引数」と「戻り値」とは情報の入出力
関数とはサービスであり、サービスに情報を入力するための「引数」と、サービスから出力される情報の「戻り値」が存在します。
入力と出力はそれぞれ「情報あり」と「情報なし」を選択することができます。つまり、次の4種類から選択することができるということになります。
関数において「void型」は、この「入力」と「出力」が存在しないことを示すためのデータ型として利用されます。
関数の引数と戻り値で「void型」が使われるプログラム例
ケース①:「引数」と「戻り値」が共に存在する場合は「void型」は登場しません。
int sum(int num1, int num2)
{
return num1 + num2;
}
ケース②:「引数」はあるが「戻り値」がない場合は、戻り値のデータ型を「void型」にします。
void printNum(int num)
{
printf("%d\n", num);
}
ケース③:「引数」はないが「戻り値」があるケースは、引数を「void型」にします。
double getPI(void)
{
return 3.141592;
}
ケース④:「引数」と「戻り値」が共にないケースは、引数と戻り値の両方を「void型」にします。
void Hello(void)
{
printf("Hello\n");
}
このように関数の定義において、引数と戻り値のデータ型として情報がないことを示すために「void型」は利用されるのです。
関数における「void型」は、入力と出力の情報の「有無を区別するため」のデータ型として存在するのです。
void型ポインタ:ポインタ定義で使用される「void型」とは
関数の定義以外で「void型」が登場するケースがもうひとつ存在します。それが「void型ポインタ」です。
ここから先はポインタに関する知識が不明瞭な方は太刀打ちできません。ポインタ技術に自信がない方は、ポインタ機能をおさらいしてから挑んでください。
》参考:ポインタを使いこなせ【身に付けるための9の極意】
void型ポインタを理解するためには最低限『ポインタ変数定義の正しい解釈とは【「*」の意味を解説】』の知識を手に入れておく必要があります。
void型ポインタの定義方法
ポインタの種類の中で「void型ポインタ」と呼ばれる、少し変わったポインタを定義することができます。
まずは「void型ポインタ」の定義方法を示しましょう。
void * pdata;
「char型ポインタ」や「int型ポインタ」は次のような定義方法ですね。
char * pCharData; // char型ポインタ
int * pIntData; // int型ポインタ
違いは、定義先頭のデータ型名が「void」になっているかどうかの違いです。
void型ポインタは「ポインタ」の一種ですから、やはりメモリ番地を管理するための変数ですよ。それはポインタと一緒です。
void型ポインタの意味とは? 解釈の仕方を解説します!
void型の意味は「ない」というものでした。「void型ポインタ」とは何がないのかを明らかにしていきます。
ポインタ定義の解釈を改めて紹介しましょう。
部品①
変数につけるラベル名を示す。皆さんが自由に名前を与えることができる。
部品②
部品①に対してのデータ型を示す。データ型をポインタにしたい場合は「*」を指定することにより、「ポインタ型」であることを示すことができる。
部品③
ポインタが参照する先のデータの「データ型」を示す。
部品③に着目しましょう。
このポインタ定義の解釈からいくと「void型ポインタ」とは、参照先のデータ型がないポインタという意味になります。
100番地というメモリの場所を示しているがアクセスする型がわからない、それが「void型ポインタ」です。
そのため、次のようにプログラムを書くとビルドエラーとなります。
#include <stdio.h>
int main(void)
{
char data = 'A'; // 的となるメモリ
void * pdata = &data; // 照準の設定 pdata --> data
// void型ポインタによる読み書き
*pdata = 'B';
return 0;
}
main.c(9): error C2100: 間接指定演算子 (*) の使い方が正しくありません。
「void型ポインタ」を使った参照先のメモリへの読み書きはできないのです。
こんなポインタが何の役に立つんでしょうね?
「void型ポインタ」が有効に使われるシーンは次の時です。
メモリアクセスができないポインタとは、「矢のない弓」のようなものです。照準を合わせても矢を射ることはできません。
void型ポインタの役割①:全てのポインタを受け入れる汎用ポインタ
「void型ポインタ」は別名「汎用ポインタ」とも呼ばれ、あらゆるポインタ型の代入が認められています。
次のプログラムを見てください。
// 変数定義
char data1;
short data2;
long data3;
double data4;
// void型ポインタ変数の定義
void * pdata;
// void型ポインタ変数への代入
// あらゆるポインタ型が代入可能
pdata = &data1; // pdata --> data1
pdata = &data2; // pdata --> data2
pdata = &data3; // pdata --> data3
pdata = &data4; // pdata --> data4
このように「void型ポインタ」には、あらゆるデータ型のメモリ番地を設定することができます。
「int型ポインタ」には「int型変数」のメモリ番地しか設定することはできません。他のポインタにはできないことが「void型ポインタ」にはできるのです。
void型ポインタを引数に持つmemset関数
汎用ポインタとして「void型ポインタ」を使っている代表的なライブラリ関数が、memset関数です。
memset関数は、第1引数で指定したメモリ番地から第3引数のサイズ分だけ、第2引数のデータを1バイト単位で書き込む機能を持っています。
#include <string.h>
void* memset(void* dst, int val, size_t size);
注目すべきは第1引数dstの型であり、「void型ポインタ」になっていますね。
引数がvoid型ポインタになっているおかげで、次のようなプログラムが可能となります。
#include <stdio.h>
#include <string.h>
int main(void)
{
char c_data;
short s_data;
long l_data;
// memsetを使って0x12をメモリに書き込み
memset(&c_data, 0x12, sizeof(c_data)); // char 型変数の番地を渡す
memset(&s_data, 0x12, sizeof(s_data)); // short型変数の番地を渡す
memset(&l_data, 0x12, sizeof(l_data)); // long 型変数の番地を渡す
// 設定結果の表示
printf("char : 0x%x\n", c_data);
printf("short: 0x%x\n", s_data);
printf("long : 0x%x\n", l_data);
return 0;
}
char : 0x12
short: 0x1212
long : 0x12121212
どのような変数のメモリ番地でも受け入れることができる、これはポインタが汎用ポインタになっているからこそできる芸当なのです。
でもおかしいですよね。void型ポインタはメモリへのアクセスができないからどうやって値を設定しているのでしょうか。そこには秘密があるんです。
memset関数内では、void型ポインタをchar型ポインタへキャストすることでメモリアクセスをしているんです。
void型ポインタの役割②:データ型を意図的に隠ぺいする
データ型を隠蔽するために「void型ポインタ」を使う、この考え方はかなり高度な知識が求められる使い方です。
「ハンドル」という概念を効果的に利用する場面において活用されます。
詳細は『オブジェクト指向【ハンドルから学ぶオブジェクトの概念】』の記事にて利用シーンを紹介していますので、参考にしてください。
注意!void型の変数は定義できません
void型ポインタの話をすると、void型の変数を定義しようとする方がいますが、それは作ることができません。
#include <stdio.h>
int main(void)
{
void num; // void型の変数定義はNG
return 0;
}
main.c(5): error C2182: 'num': 'void' 型が不適切に使用されています。
「void型変数」と「void型ポインタ変数」は全然違います。この2つを一緒にしてはいけません。
void num; // void型変数の定義はNG
void * pnum; // void型ポインタ変数の定義はOK
変数定義とはメモリ上に型の枠組みの実体を作り出すことです。つまり、型のない「void型変数」は作り出すことができないのです。
void型ポインタ変数は、あくまでもポインタ変数の一種であり、ポインタ変数としての枠組みの実体を作り出すことができるのです。
「void型ポインタ」はポインタが指し示す先のデータ型がわからないだけであり、ポインタ変数自身はちゃんとしたデータ型なんですよ。