C言語 void型の意味と使い方【void型ポインタの扱い方も解説】

C言語
この記事は約11分で読めます。

こんにちは、ナナです。

本記事では「void型」という、少し特殊なデータ型を紹介しましょう。

C言語で「void型」が登場するシーンは次の2つです。

  • 関数定義の引数と戻り値
  • void型ポインタ

「void型」を知るために、これらを深堀りしていきます。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • void型の意味とは?
  • 関数定義で使われるvoid型の意味とは?
  • void型ポインタの定義と解釈方法とは?
  • void型ポインタの汎用ポインタとしての扱い方とは?
  • void型ポインタで型を隠蔽するとは?
  • 「void型変数」と「void型ポインタ変数」の違いに注意

では、「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型ポインタ」です。

ここから先はポインタに関する知識が不明瞭な方は太刀打ちできません。ポインタ技術に自信がない方は、ポインタ機能をおさらいしてから挑んでください。

void型ポインタを理解するためには最低限『C言語 ポインタ変数定義の正しい解釈とは【「*」の意味を解説】』の知識を手に入れておく必要があります。

void型ポインタの定義方法

ポインタの種類の中で「void型ポインタ」と呼ばれる、少し変わったポインタを定義することができます。

まずは「void型ポインタ」の定義方法を示しましょう。

// void型ポインタの変数定義
void * pdata;

「char型ポインタ」や「int型ポインタ」は次のような定義方法ですね。

char * pCharData;  // char型ポインタ
int  * pIntData;   // int型ポインタ

違いは、定義先頭のデータ型名が「void」になっているかどうかの違いです。

ナナ
ナナ

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型ポインタ」を使う、この考え方はかなり高度な知識が求められる使い方です。

「ハンドル」という概念を効果的に利用する場面において活用されます。

詳細は『C言語入門 ファイルハンドルから学ぶハンドルの概念と作り方』の記事にて利用シーンを紹介していますので、参考にしてください。

スポンサーリンク

注意!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型変数」はポインタが指し示す先のデータ型がわからないだけであり、ポインタ変数自身はちゃんとしたデータ型なんですよ。

スポンサーリンク
C言語 機能解説
シェアする
モノづくりC言語塾