【C言語】ポインタの「型」について解説

ポインタの型の解説ページアイキャッチ

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

このページではポインタの型について解説していきたいと思います。

下のページでポインタは他の変数を指す矢印であることを説明しました(アドレスを格納する変数)。ただしこのページではポインタの型については触れていません。

ポインタの解説ページアイキャッチ 【C言語】ポインタを初心者向けに分かりやすく解説

通常の変数に intcharshort といった型が存在するように、ポインタ変数にも int*char*short* といった型が存在します。

ではこのポインタ変数の型によって何が異なるのでしょうか?

うーん、intchar はサイズが違うよね

さらに表現できる値の種類も違う

それと一緒で、型によってサイズや表現できる値の種類が異なるんじゃないの?

そう考えるのが自然だよね…

だけど、ポインタの型は全てサイズも同じ、さらに表現できる値の種類も一緒なんだ

は?

じゃあポインタの型の意味って…

実は、通常の変数の型の intchar などとは異なり、ポインタ変数の型では型によってサイズや表現できる値に差はありません。

 char* であろうが int* であろうが long* であろうがポインタ型は全てサイズは同じです。

では、この型の違いにどのような意味があり、型の違いによってどのような違いがあるのでしょうか?

この辺りをこのページで解説したいと思います。

ポインタの「型」とは

ではポインタの「型」がどのような意味を持つのかについて解説していきたいと思います。

通常変数の型

と、その前にまずは通常変数の型についておさらいしておきたいと思います。

通常変数では、型によって下記の違いがあります。

  • サイズ
  • 扱われ方

型によってサイズが違う

まず型によってサイズが異なります。

例えば int 型のサイズは 32 ビット(4 バイト)、char 型のサイズは 8 ビット(1 バイト)といったように、型によってサイズが異なります。

通常変数で型によってサイズが異なる様子

そして、これによって変数の型によって、その変数に格納できる数値(表現できる数値)の種類(範囲)に違いがあります。

各型の変数に格納できる数値の違いの一例は下記のようになります。

数値の範囲
char -128127
short -3276832767
int -21474836482147483647

型によって扱われ方が違う

また型によって変数に格納されたデータの扱いが異なります。

例えば int 型に格納されたデータはは符号 “あり” の数値として扱われますが、unsigned int 型に格納されたデータは符号 “なし” の数値として扱われます。

また int 型や short 型は格納されたデータは “整数” として扱われますが、float 型や double 型の変数に格納されたデータは “浮動小数点数” として扱われます。

こんな感じで、通常の変数においては型によって扱われ方が異なります。

スポンサーリンク

ポインタの型

では、本題である「ポインタの型」についてはどうでしょう?

全ての型でサイズは同じ

まずポインタ型の変数では、型による「サイズの違いは無い」です。全ての型でサイズは同じです。

ポインタ変数で型によってサイズが同じになる様子

ポインタ型の変数に格納される値は「アドレス」として扱われます。

このアドレスのサイズは CPU によって異なりますが、おそらく現在だと 64 ビット(8 バイト)になると思います。

型によって扱われ方が違う

ただし、通常の変数同様に型によって扱われ方が異なります。

具体的には、型によって下記の2点の違いがあります。要は、ポインタの型によって下記の2つが決まります。

  • アクセス先のデータの型
  • 加減算時のアドレス増減値

それぞれについて、詳細を解説していきたいと思います。

ポインタの型でアクセス先のデータの型が決まる

まず、ポインタの型によって「アクセス先のデータの型」が決まります。この詳細を説明していきます。

ポインタ変数でのデータのアクセス

ポインタ変数は、変数使用時に変数名の前に * を付加することで、そのポインタに格納されるアドレスのデータ(そのポインタが指すデータ)にアクセス(取得や格納)することができます。

下記はポインタ変数に格納されるアドレスのデータに値を格納する例になります。

データへのアクセス1
int *p;
int a = 0;
p = &a; /* pにaを指させる */
*p = 1000; /* pが指す先に100を格納 */
printf("%d\n", a); /* 1000が表示される */

上記では、*p によりアクセスされるデータは何型として扱われるでしょうか?

うーん、int 型の変数 a を指してるんだから int 型じゃない?
なるほど!

では次のポインタでのデータのアクセスについてみてみよう

スポンサーリンク

ポインタ変数でアクセスしたデータの扱い

ポインタ変数では、そのポインタの型の基になる型の変数を指す(アドレスを格納する)のが一般的です。

MEMO

「ポインタの型の元になる型」とは、例えば int* 型であれば int 型、char* 型であれば char 型といったように、ポインタの型から * を除去した型のことを言っています

ただし、異なる型の変数を指すことも可能です。

下記は char* 型のポインタ変数に int 型の変数を指させる例になります。

データへのアクセス2
char *p;
int a = 0;
p = &a;
*p = 1000;
printf("%d\n", a);

上記では、*p によりアクセスされるデータは何型として扱われるでしょうか?

うーん、さっきと一緒じゃ無い?

int 型の変数 a を指してるんだから int 型?

これは実は不正解なんだ

実際は char 型として扱われているんだ

上記のソースコードでは、ポインタ変数 p に int 型の変数 a を指させているので、*p でアクセスしたデータは int 型として扱われると思われる方もいるかもしれません。

しかし、実際にはポインタ変数 p の型の「基になる型」である char 型のデータとして扱われます。

なので、上記プログラムを実行すると printf での表示は 1000 にはなりません。

これは *pchar 型のサイズである 8 ビットのデータとして扱われるためです。10008 ビットでは表現できないため桁あふれが発生し、実際にはその桁あふれが発生した結果である 232p の指す変数 a に格納されることになります。

この辺りの動きを図示しながら説明すると、下記のようになります。

まず変数宣言により変数 a と変数 p がメモリ上に配置されます。

変数がメモリ上に配置される様子

一つの四角が 1 バイトを表しており、変数 aint 型なので 4 バイト、変数 p はポインタ型なので 8 バイトのデータとして図示しています。

次に p = &a により、pa のアドレスが格納され、pa を指すことになります。

ポインタが変数を指す様子

次に行うのが *p = 1000 です。

pa を指しているので、当然 a に対して 1000 を格納しようとします。

ポインタの指す先に数値を格納しようとする様子

ただし、*p でアクセスするデータは、p の型の「基になる型」として扱われるため、char 型のサイズである 1 バイトのデータに 1000 を格納することになります。

ポインタの基になる型のサイズに数値を格納する様子

つまり、a の先頭の 1 バイトに 1000 を格納しようとします。ただ、桁あふれが発生するので、桁あふれ発生後の値が格納されることになります。

int型のデータをchar型のデータとして扱うことで桁あふれが発生する様子

で、最後に a を表示しているので、先頭の 1 バイトにのみ値が格納された int 型変数の中身、つまり 232 が表示されることになります。

こんな感じで、ポインタ変数がどんな型の変数を指していたとしても(どんな型の変数のアドレスを格納していたとしても)、ポインタ変数からアクセスするデータは、そのポインタ変数の型の「基になる型」として扱われます。

つまり、ポインタの型によって「アクセス先のデータの型」が決まるということになります。

ポインタの型によってアクセスする先のデータの扱いが変わる様子

異なる型のデータを指す応用例

これを利用することで、結構いろんなことが行えます。

例えば、下記では int 型の変数を char 型のデータとして扱うことで、4 バイトのデータを 1 バイトずつ分割して表示しています。

int型をchar型でアクセス
#include <stdio.h>

int main(void) {
    char *p;
    int a = 0x01234567;

    p = &a;

    printf("%02x\n", *p);
    printf("%02x\n", *(p+1));
    printf("%02x\n", *(p+2));
    printf("%02x\n", *(p+3));

    return 0;
}

実行すると下記のように int 型の変数のデータが 1 バイトずつ表示されます。

67
45
23
01

また、下記では浮動小数点数を int 型のデータとして扱うことで、内部データを整数として扱っています。

float型をint型でアクセス
#include <stdio.h>

int main(void) {
    int *a;
    float b = 0.625;

    a = &b;

    /* 内部データを2進数表示 */
    for (int i = 0; i < 32; i++) {
        unsigned int shift;
        shift = (unsigned int)1 << (31 - i);
        printf("%u", (*a & shift) >> (31 - i));
    }
    printf("\n");

    return 0;
}

実行すると、浮動小数点数の内部データを2進数化したものが表示されます。

00111111001000000000000000000000

浮動小数点数や内部データって何?という方は、下記ページで解説していますのでこちらを参照していただければと思います。

浮動小数点数における数値と内部データの変換方法解説ページアイキャッチ 【C言語】浮動小数点数における「数値⇔内部データ(符号部・指数部・仮数部)」の変換

へー結構便利じゃん!

って試しにコンパイルしたら思いっきり警告出るんですけど…

ポインタ変数に「基になる型」意外の変数のアドレスを格納しようとすると警告が出るんだ

意図しない結果になる可能性がありますよ?!っていう警告だね

実際上記の例でも結構イレギュラーな使い方してるからね…

このページでは、あえてポインタに「基になる型」とは異なる型の変数を指させています。

実際便利な場面もあるのですが、バグの原因になる可能性も高いです。

例えば、下記のように int* 型のポインタで、int 型よりもサイズの小さい char 型の変数を指すと、指している変数のサイズを超えて他の変数の値を意図せず変更してしまうような場合があります。

他の変数まで影響を及ぼす例
#include <stdio.h>

char a = 0;
char b = 0;
char c = 0;
char d = 0;

int main(void) {
    int *p;
 
    p = &a;

    *p = 0x01234567;

    printf("%02x\n", a);
    printf("%02x\n", b);
    printf("%02x\n", c);
    printf("%02x\n", d);

    return 0;
}

使い方には十分に気をつけましょう!

加減算時のアドレス増減値

次はポインタの型による「加減算時のアドレス増減値」の違いについて解説していきたいと思います。

スポンサーリンク

ポインタ変数に対する加減算

C言語では、ポインタ変数に対して加減算を行うことで、ポインタ変数に格納されているアドレスを増やしたり減らしたりすることができます。

例えば下記のように、配列の先頭アドレスを指しているポインタ変数に加算を行うことで、次の要素のデータにアクセスすることができますね!

ポインタ変数への加算
#include <stdio.h>

int main(void) {
    
    int *p;
    int a[100];
    int i;
    p = a;

    for (i = 0 ; i < 100; i++) {
        *p = i;
        p++;
    }

    for (i = 0; i < 100; i++) {
        printf("%d\n", a[i]);
    }

    return 0;
 }

加減算時のアドレスの増減値

では、ポインタ変数に +1 すると、アドレスはいくつ増減するでしょう?

例えば下記のように float* 型のポインタ変数に +1 する前後のアドレスを表示するといくつの差が生じるでしょうか?

float*型ポインタへの加算
float *p;
float a[2];
p = a;

printf("before = %p\n", p);
printf("after  = %p\n", p+1);

いや 1 足してるんだから 1 でしょ!
実は 4 なんだ…

なんで…

ポインタ難しい…

ポインタ変数に +1 した時に、格納されているアドレスがいくつ増加するかは「ポインタの型」によって異なります。

より具体的に言うとポインタの型の「基になる型のサイズ」分増加することになります。

ポインタの型によるアドレス増減値の違い

+n した時は n × 基になる型のサイズ アドレスが “増加”し、-n した時は n × 基になる型のサイズ アドレスが “減少” することになります。

前述のプログラムでは、float* 型のポインタに対して +1 しているので、アドレスとしてはその基になる float のサイズ分、すなわち 4 バイト増加することになります。

なので、前述のプログラムを実行して表示されるアドレスは下記のようになり、+1 することでアドレスが 4 増加していることが分かると思います。

before = 0x7ffeefbff460
after = 0x7ffeefbff464

要は、前述の通りポインタからアクセスするデータは、「そのポインタの型の基になる型として扱われる」ので、次にアクセスするデータ(ポインタに +1 してアクセスするデータ)は、その基になる型の「サイズ分ずらしたアドレスのデータ」になるということです。

下記は各ポインタ型に +1 した時のアドレスの変化を表示するプログラムの例になります。

アドレスの変化
#include <stdio.h>

int main(void) {
    
    char *charp = 0x1000;
    short *shortp = 0x1000;
    int *intp = 0x1000;
    double *doublep = 0x1000;

    printf("%p(%d)\n", charp+1, sizeof(char));
    printf("%p(%d)\n", shortp+1, sizeof(short));
    printf("%p(%d)\n", intp+1, sizeof(int));
    printf("%p(%d)\n", doublep+1, sizeof(double));

    return 0;
 }

ちなみに配列の要素指定においても、同様のことが言えます。 例えば int 型の配列 array について考えると、array[n] のアドレスと array[n+1] のアドレスの差は int 型のサイズである 4 になります。

配列の各要素のアドレス
int array[100];

printf("array[10] = %p\n", &array[10]);
printf("array[11] = %p\n", &array[11]);

ポインタの型の選び方

最後にポインタの型の選び方について解説していきたいと思います。

ここまでポインタの型によって下記の2つが異なることを説明してきました。

  • アクセス先のデータの型
  • 加減算時のアドレス増減値

この2つのうち、ポインタの型の本質的な違いは前者の「アクセス先のデータの型」だと思います。

後者に関しては、前者の「アクセス先のデータの型」の違いを実現するための帳尻合わせのようなものです。前者を行う上で後者を行う方が便利なのでそうなっているだけだと思います。

したがって、ポインタの型は「アクセス先のデータをどの型として扱うか」に応じて設定すれば良いと思います。

例えば、アクセス先のデータを「符号なしの 4 バイトの整数」として扱いたいのであれば unsigned int* 型を選べばいいですし、「8 バイトの浮動小数点数」として扱いたいのであれば float* 型を選べばいいです。

うーん…

4 バイトとか浮動小数点数とか難しい….

ちょっと難しく感じる方もおられるかもしれませんが、変数をポインタに指させるのであれば、結局「ポインタが指す変数の型」に対するポインタ型を選べば良いと思います。

ポインタに変数を指させる場合、結局その変数に対してポインタを介してアクセスしたいだけだと思います。したがって、その変数の型としてデータを扱えるように、その変数の型に合わせたポインタの型を選べば良いです。

一方で、ポインタに変数以外を指させる場合(例えば malloc で確保したメモリのデータを指させる場合)は、前述の通り、「アクセス先のデータをどの型として扱うか」をしっかり考えてポインタの型を選ぶようにしましょう!

スポンサーリンク

まとめ

このページでは「ポインタの型」について解説しました。

ポインタ変数においては、通常の変数と異なり型によってサイズは変わりません。

しかし、変数名に * を付加してポインタからデータにアクセスする際、そのデータはそのポインタの型の「基になる型」として扱われることになります。

また、ポインタに加減算を行った時のアドレスの変化もポインタの型に応じて異なります。

具体的にはポインタに +1 した際には、そのポインタの型の「基になる型」のサイズ分アドレスが増加することになります。

これらを利用して色々応用することも可能ですが、特にポインタに慣れていない方は、まずはポインタに指させたい変数の方に応じてポインタの型を選ぶようにしましょう!

ちなみにポインタの型には void* もあります。

この void* については下記ページで解説していますので、是非こちらも合わせてお読みください!

voidとvoid*型の解説ページのアイキャッチ 【C言語】void型とvoid*型(void型ポインタ)について解説

オススメの参考書(PR)

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

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

https://daeudaeu.com/c_reference_book/

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