C言語基礎|ポインタの型

ポインタは「住所」だけじゃない!型が付くことで“どれだけ・どう扱うか”が決まる。

ポインタって「番地(アドレス)を入れるもの」…ここまではOKですよね。
でもC言語のポインタは、ただの番地では終わりません。

Cではポインタに が付いていて、これがめちゃくちゃ重要です。
なぜなら、その型が

  • どんなオブジェクトを指すつもりなのか
  • どれくらいの大きさ(バイト数)を、ひとまとまりとして扱うのか

を決めてしまうからです。

型を間違えると、scanf で変数の外まで書き込んだり、swap で本来より大きい領域を交換してしまったりして、かなり危険なことが起きます。
ここでは「ポインタの型」が安全性に直結する理由を、図と表でしっかり整理していきますね。

スカラ型(scalar type)って何?

番地を表すポインタは「数っぽい量」とみなせます。
C言語では、算術型(int や double など)と、ポインタ型をまとめて スカラ型 と呼びます。

スカラ型のイメージ

分類特徴
算術型int, double数として計算できる
ポインタ型int *, char *番地という「量」を表す(数っぽい)

scalar は「大きさはあるけど方向はない量」という意味で、vector(向きがある量)と対比されます。
この話は「ポインタも値として変数に入る」ことを再確認するための導入だと思うとちょうどいいです。

ポインタの型とは(Type * が意味すること)

ここが本題です。

Type * 型ポインタは、単に「○○番地」を指すのではなく、

○○番地を先頭に格納された Type 型オブジェクト

を指すものとして扱われます。

ポインタ型が“何を指すか”を決める

ポインタ型指す対象のイメージ*p で得られる型
int *int 型オブジェクトint
double *double 型オブジェクトdouble
char *char 型オブジェクトchar

つまり「住所+型情報のセット」みたいなものです。

型を間違えると何が起きる?(安全性の話)

例1:scanf の指定と変数の型が違うと危険

scanf は、指定された変換指定子に合わせた型の値を、渡された住所に書き込みます。


サンプルプログラム

short に long を読み込ませようとするミスのあるプログラム例です。(わざと危ない例です)。
メッセージも別の日本語に変更しています。

注意:この例は「やってはいけない例」を理解するためのものです。実際に真似しないでね、という意図です。

#include <stdio.h>

int main(void)
{
    short s;

    puts("整数を入力してください(危険な例の説明用)。");
    printf("number = ");

    // 本来は %hd と &s を組み合わせるべきなのに、わざと間違える
    scanf("%ld", &s);

    printf("入力後の s = %hd\n", s);
    return 0;
}

何が危険なの?(図でバイト数のズレを見る)

環境によって大きさは違いますが、例えばこういうケースを想像してください。

  • short が 2バイト
  • long が 8バイト

scanf("%ld", …) は long を読み取って 8バイト書き込もうとします。
でも &s が指す先は 2バイト分しか用意していないので、はみ出して書き込む可能性があります。

図:領域を超えて書き込まれるイメージ

図の説明

  • 左の2バイトが本来の s の箱
  • scanf が要求する型(long)に合わせて書く範囲が決まる
  • だから「型の指定」と「変数の型」が一致しないと危険

scanf とポインタ型の関係(書式を整理)

scanf は「変換指定子」と「格納先ポインタの型」を対応させる必要があります。

代表的な scanf の対応(よく使うもの)

読み込みたい型変換指定子渡すべきもの
int%dint 変数のアドレス(例:&i)
long%ldlong 変数のアドレス(例:&l)
short%hdshort 変数のアドレス(例:&s)
double%lfdouble 変数のアドレス(例:&x)
char(1文字)%cchar 変数のアドレス(例:&c)

この表は、「scanf は型に応じて必要なポインタ型が変わる」ことを示しています。

例2:swap の引数型が合わないと交換サイズがズレる

swap が int * を受け取る関数だとします。

void swap_int(int *x, int *y);

ここに char の住所を渡すとどうなるか。
char は1バイトのつもりなのに、swap_int は int として扱い、sizeof(int) バイト単位で読み書きするつもりになります。

型違い swap が危険な理由

実際の交換したい対象渡してしまった関数関数が想定するサイズ起きうる問題
char(1バイト)swap_int(int *)int のサイズ(例:4バイト)周辺メモリまで巻き込む可能性

じゃあどうする?(型に合わせた関数を作る)

安全にやるなら、型に合わせて関数を分けるのが基本です。

例:char 用の swap

void swap_char(char *x, char *y)
{
    char temp = *x;
    *x = *y;
    *y = temp;
}

型ごとに作るメリット

方法メリットデメリット
型ごとに関数を作る安全で分かりやすい種類が増える
1つで何でもやる(無理やり)関数は少ない型不一致で危険が増える

入門段階では「安全で分かりやすい」を優先するのが正解です。

特殊なテクニックと注意(入門では避ける)

Cには、特殊な目的で「別の型として覗く」ようなテクニックも存在します。
ただしこれはルールや落とし穴が多く、入門の段階では混乱しやすいので、基本方針はこれでOKです。

Type * は Type を指すものとして使う(型を合わせる)

登場する命令(関数)と書式・何をする命令なのか

scanf

  • 書式:scanf(書式文字列, 格納先1, 格納先2, ...);
  • 何をする?:入力を読み取り、指定された格納先へ書き込む
  • 重要:書式文字列の変換指定子と、格納先の型(アドレスの型)を一致させる

puts

  • 書式:puts(文字列);
  • 何をする?:文字列を表示し、改行も出す

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする?:書式に従って表示する

使った表や図の説明(まとめ)

  • スカラ型の表は「ポインタも値として扱える量」という導入の整理です。
  • ポインタ型の表は「Type * は Type を指す」という核心を整理しています。
  • scanf 対応表は「変換指定子と格納先の型を一致させる」ためのチートシートです。
  • バイトはみ出し図は「型不一致がメモリ破壊につながる」ことを直感で理解するための図です。
  • swap の表は「関数側が想定するサイズが型で決まる」点を示しています。