ユーニックス総合研究所

  • home
  • archives
  • c-pointer

C言語のポインタをC言語歴17年が解説

  • 作成日: 2024-03-10
  • 更新日: 2024-03-11
  • カテゴリ: C言語

C言語のポインタは後発の色々な言語でも採用されているすばらしい発明です。
デニス・リッチーがC言語を生み出してからその影響は世界中に及んでいます。

C言語のポインタはC言語の開発では使えると大変便利なものです。
この記事ではC言語のポインタについて解説していきます。

記事を執筆しているのはC言語歴17年以上のベテランC言語使いです。
C言語の記事を多数執筆しています。

C言語や他の言語を扱うYoutubeも公開しています。
興味がある方は以下のリンクからご覧ください。

Youtubeの当チャンネル

関連記事
プログラミングのポインタをわかりやすく解説【C言語】
C言語の関数で戻り値にポインタを使う
C言語で関数から複数の戻り値を返す【ポインタ、構造体】

ポインタとは何なのか?

ポインタとは何なのでしょうか?
ポインタとは一言で言ってしまうと、以下になります。

  • 「特定のメモリへアクセスするためのショートカット機能」

ポインタではポインタ変数と言うものを使います。
ポインタ変数はメモリのアドレスを格納する変数です。

このメモリのアドレスにアクセス、たとえば読み書きしたい時はこのポインタ変数を通じて行うことができます。
つまり例えば変数Aがあっとします。そしてポインタ変数Pがあります。
この変数Aのアドレスを変数Pに入れておくと、変数Pから変数Aの値を読み込んだり変更できたりする、というのがポインタです。

たとえば関数を呼び出すことを考えましょう。
その関数の中で関数の外にある変数を参照しないといけないとします。
そういう場合は関数の引数をポインタにして、関数の外にある変数のアドレスをそのポインタに渡します。
そうするとその関数の中から関数の外にある変数を読み書きできる、ということです。

便利そうですが、実際便利です。
使い方を覚えたらこれ無くして開発はできません。

🦝 < いや、できるだろ

🐭 < できるけど大変だよ

アドレスとは何なのか?

メモリのアドレスとは番地のことです。
メモリは例えるならロッカールームです。
ロッカーが並んでいて、そのロッカー1つ1つに番地があります。

変数Aというのもメモリ上ではロッカーとして確保されています。
変数Aのためのロッカーで、番地は例えば123としましょう。
この変数Aの値を変更するには番地123を知っていればロッカーにアクセスできます。

たとえば太郎君が次郎君に「このペットボトルを俺のロッカーに入れてきてくれ」と頼んだとします。
しかし次郎君は「『俺のロッカー』ってどこだよ」と困惑します。
そこで太郎君は「ああ、ごめん。番地は123だよ」と次郎君に教えます。
そうすれば次郎君は「最初からそう言えよな。わかったよ」と答えられるわけです。

メモリは広大なロッカールームです。
ロッカーがずら~っと並んでいる巨大な部屋です。
その中から「俺のロッカー」と言われてもどのロッカーだかわかりません。
『番地123』がわかってやっと他の人はそのロッカーにアクセスできるわけですね。

この『番地123』がアドレスです。
そしてポインタはこのアドレスを格納する変数になります。

ポインタが使えると何が嬉しいのか?

ポインタが使えると何が嬉しいのでしょうか?
これは先ほども言いましたように変数のアクセスへのショートカットができるのが大変便利です。
また、ポインタのメリットとしては設計が綺麗になる、というのがあげられます。

ポインタは実は使い方が下手な人が使うと、非常にスパゲッティなコードになります。
ですがポインタの使い方を心得ている人が使うと、非常にコードが綺麗になります。

あとは他の言語ではたとえばPythonは変数の変数への代入は参照のコピーになる場合があります。
これはディープなコピーを発生させないようにして言語の速度を速めるためだと思われます。

しかしC言語では変数の変数への代入は実体のコピーになります。
つまり超でかい構造体の変数を代入するとその超でかい構造体のメンバもごりごりっとコピーされてしまうわけです。
そうすると「きぃ~遅いざます!」となってしまいます。

これを解決するのがポインタです。
ポインタを使えば超でかい構造体のメンバを丸々っとコピーせずに、アドレスの代入だけで済みます。
アドレスの代入だけなので実質的なコストはアドレスの代入コストだけです。
超でかいメンバのコピーは発生しません。

実はC言語が速いと言われるゆえんの1つにこのポインタの存在があります。
ポインタはメモリ効率的に大変優れた機能なわけです。

C言語のポインタのメリットとは?コピーしますかメモリを共有しますか

NULLポインタとは?

「NULL(ナル、ヌル)」とは何も存在しないことを表すキーワードです。
NULLポインタは何も存在しないことを表すポインタになります。

なぜNULLポインタが必要なのでしょうか?
変数は初期化される場合があります。
もちろんポインタ変数も初期化できます。

その初期化の時にポインタ変数は何の値で初期化されるべきでしょうか?
はい、そうです。その値がNULLポインタになります。

いわばNULLポインタは数字で言うところの「0」と一緒です。
何もないことを表すキーワードです。

「NULL」はC言語ではあらかじめ定義されているので、stdio.hをインクルードすれば使えるようになります。
「NULL」の実装は処理系(コンパイラ)によりますが、たいていは

#define NULL ((void *) 0)  

のように定義されています。

NULLポインタを参照するとどうなる?

NULLポインタを参照するとどうなるのでしょうか?
つまりNULLポインタからメモリの実体にアクセスしようとした場合です。

その場合は運が良ければセグフォになることが多いです。
運が悪ければセグフォになりません。
セグフォとは「Segmentation fault」のことです。
不正なメモリ領域にアクセスした場合などにこのエラーになります。

もっともこのエラーになるのは運が良いケースで、運が悪い場合はエラーに気づかずにプログラムが走りっぱなしになります。
この辺はC言語の怖いところです。

ダングリング・ポインタとは?

ポインタと関連するキーワードにダングリング・ポインタがあります。
これは不正なアドレスが入ったポインタのことです。

不正なアドレスとは例えばメモリ上に存在しないアドレス。
それから動的メモリ確保ですでに解放されているアドレス。
などになります。

パソコンの気持ちになってください。
プログラムが存在しないメモリ上のアドレスに書き込みをしようとしました。
パソコンからすると「なにやってくれてんねん」と言う感じです。
ですのでパソコンはどうしていいかわからずエラーを吐きます。

ポインタを扱う場合はこのダングリング・ポインタの問題が常に付いてきます。
覚えておきましょう。

色々なポインタ

では実際にコードでC言語のポインタを見ていきましょう。
ポインタはC言語で扱える型であればどの型でもポインタにできます。

C言語の変数はintとかdoubleとか型があります。
その型にアスタリスク(*)を付けるとポインタになります。

ポインタ変数の定義方法:  

型 アスタリスク 変数名 = 初期値;  

関連記事
C言語のアスタリスクの意味を解説します【ポインタ、掛け算】
C言語のポインタの宣言と初期化方法

int型のポインタ

ではint型のポインタを見てみましょう。

int *p = NULL;  

上記のpがポインタ変数です。
上記ではpの定義で初期化してpにNULLポインタを入れています。
型はint型です。

つまりこのpint型の変数のアドレスを格納できるポインタと言うことになります。

float型のポインタ

続いてはfloat型のポインタを見てみましょう。

float *p = NULL;  

これも型以外はint型のポインタと変わりません。
しかし型はfloatになっています。
ですのでこれはfloat型の変数のアドレスを格納できるポインタになります。

この他にはもちろんshortdouble, longなどもポインタにできます。

char型のポインタ

ではchar型のポインタを見てみましょう。

char *p = "Hello, World!";  

上記のpchar型のポインタです。
値は文字列リテラルの「Hello, World!」を代入しています。

char型のポインタは文字列の表現でよく使われます。
上記のように文字列リテラルのアドレスを代入する場合はconstを付けておくのがマナーです。

const char *p = "Hello, World!";  

これは文字列リテラルは変更できるかどうかわからないものだからです。
大抵の処理系は文字列リテラルを変更できないメモリ領域に置きます。
ですのでconstを付けて変数の値が変更できないようにしておきます。
これはC言語ではおなじみのマナーですので覚えておいてください。

C言語の文字列のポインタの使い方
C言語の文字列のポインタを比較する

void型のポインタ

void型のポインタは汎用的に使えるポインタです。

void *p = NULL;  

voidは特定の型に依存しない汎用的な型です。
また、関数の返り値がない時にも関数の定義で使われます。

このvoid型のポインタにはあらゆる型のポインタを入れておくことができます。
それじゃ、実体を取り出すときはどうすればいいの?
と思うかもしれませんが、そういう時はvoid型のポインタをちゃんと目的の型にキャストしてから使います。

int i = 1;  // int型の変数  
void *p = &i;  // 変数iのアドレスをvoid型ポインタに入れる  
int *ip = (int *) p;  // void型ポインタをint型ポインタにキャスト  
printf("%d\n", *ip);  // ipにアスタリスクを付けてメモリの実体を参照  
// 1  

このvoid型のポインタを使うテクニックは上級者向けのものです。
初心者の方は今は別に覚えなくても構いません。
しかし頭の片隅には入れておくといいでしょう。

構造体型のポインタ

構造体もポインタにできます。

struct Animal {  
    int age;  
};  
struct Animal *p = NULL;  

型が構造体になっただけで、他のところは変わりません。
構造体のポインタの場合はメンバ変数にアクセスするんですが、この時にアロー演算子という特殊な演算子を使います。
これについては後述します。

関連記事
C言語の構造体のポインタの使い方

配列型のポインタ

C言語の配列もポインタ変数に入れておくことができます。

int ary[] = {1, 2, 3};  
int *p = ary;  

printf("%d %d %d\n", p[0], p[1], p[2]);  

配列のアドレスを入れたポインタは配列と同じように使うことができます。
ですが1つだけ注意点があります。それはsizeof()です。
sizeof()は変数のバイト数を求める演算子です。

これは配列に使うと配列のバイト数が求まります。

int ary[] = {1, 2, 3};  // 4バイト(int)が3つで12バイトの配列  
printf("%ld\n", sizeof ary);  // 12  

しかし配列のアドレスを入れたポインタにsizeof()を使うと、配列ではなくポインタ変数のサイズが求まります。

int ary[] = {1, 2, 3};  
int *p = ary;  
printf("%ld\n", sizeof p);  // 8  

この辺は配列のポインタを使う場合は注意が必要です。

関連記事
C言語でポインタと配列を入れ替える(スワップする)方法
C言語の配列とポインタの使い方~この2つの関係性について~

関数ポインタ

関数のアドレスもポインタに入れられます。
この場合は少し特殊なポインタを使います。
それは関数ポインタと呼ばれるものです。

関数ポインタの定義方法  

関数の返り値の型 (*ポインタ変数名)(関数の引数の型, ...);  

コードで書くと以下のようになります。

void func2(int a, short b) {  
    printf("%d %d\n", a, b);  
}  

void func_ptr(void) {  
    void (*p)(int, short) = func2;  
    p(1, 2);  // 1 2  
}  

上記のpというのが関数ポインタです。
関数ポインタはC言語の中でもかなり変わった書き方をします。
面食らうかもしれませんね。

🦝 < な、なんじゃこりゃぁ!

上記のように関数ポインタに入れた関数のアドレスから関数を呼び出すことができます。

関連記事
C言語の関数ポインタをtypedefする方法【型定義】
C言語の関数ポインタの使い方

ポインタ変数にアドレスを入れる【&演算子】

ではここからポインタ変数を実際に使っていきたいと思います。
まずポインタ変数に変数のアドレスを入れる方法です。

変数からアドレスを取り出すには「&」を使います。
変数の頭にこれを付けるとアドレスを取り出すことができます。
あとはポインタ変数にそのアドレスを代入するだけです。

int i = 1;  // int型の変数i  
int *p = &i;  // iのアドレスをpに代入  

上記ではポインタpを初期化しています。
初期化に使う値は変数iのアドレスです。

ポインタ変数は宣言時にはアスタリスクが必要ですが、宣言後の変数にはアスタリスクは必要ありません。
ですので以下のように代入することもできます。

int i = 1;  // int型の変数i  
int *p;  // ポインタpの宣言  
p = &i;  // iのアドレスをpに代入  

ポインタ変数に格納されたアドレスを参照する

ポインタ変数に格納されたアドレスは出力してみたいというのが人の心というものです。
この場合はprintf()を使います。
出力変換指定子は「%p」です。

int i = 1;  
int *p = &i;  

printf("%p\n", p);  // 0x7ffdb532780c  

上記ではint型の変数iのアドレスをポインタ変数pに入れています。
ポインタ変数pprintf()%pで出力すると上記のような結果になります。
上記の例では「0x7ffdb532780c」という値が出力されていますが、これがアドレスです。
また、上記の値はみなさんの環境では違う値になると思います。
これはどのメモリを確保するかは実行時によるからです。

ポインタ変数に格納されたアドレスの実体を参照する

ポインタ変数pには変数iのアドレスが入ってます。
では変数iの値をポインタ変数pを使って出力したい場合はどうすればいいのでしょうか。
これは単純です。ポインタ変数にアスタリスクを付けるだけです。

int i = 1;  
int *p = &i;  

printf("%d\n", *p);  // 1  

上記の例では*pというようにポインタ変数にアスタリスクをつけて変数iの値である「1」を参照しています。
printf()で出力しているだけですね。

このようにポインタ変数にアスタリスクを付けるとアドレスの実体を参照することが可能です。
もちろんこの値は他の変数に代入することもできます。

int i = 1;  
int *p = &i;  
int j = *p + *p;  

printf("%d\n", j);  // 2  

ポインタを使って間接的に実体を変更する

ポインタ変数にアスタリスクを付けた状態では実体の値を変更することもできます。

int i = 1;  
int *p = &i;  

*p += 10;  // ポインタを使ってiの値を変更  

printf("%d\n", i);  // 11  
printf("%d\n", *p);  // 11  

上記の例では*p += 10とやって*pに値10を加算しています。
これはi += 10と同じ意味になります。

結果は上記のようにi*pの値も11になっています。
pはポインタなのでiへのショートカットです。
ですのでこのような芸当が可能になります。

関数の引数のポインタ

関数の引数にもポインタが使えます。
宣言方法は変数の宣言と同じです。

// ポインタの引数を取る関数  
void func(int *p) {  
    *p += 10;  // ポインタを使って間接的に値を10加算  
}  

void func_arg(void) {  
    int i = 1;  
    func(&i);  // 関数の引数にアドレスを渡す  
    printf("%d\n", i);  // 11  
}  

先述しましたがこのように関数の引数にポインタを使うと、関数の呼び出し側にある変数を関数内で変更することができます。
関数に計算処理を任せて処理を分割できますのでコードが整理されて綺麗になります。

関連記事
C言語で関数の引数にポインタを渡す【ポインタの値渡し】
C言語の配列とポインタについて
C言語の関数ポインタを引数に渡す方法

構造体のポインタとアロー演算子

構造体のポインタから実体を参照するにはアロー演算子を使います。
アロー演算子は「->」です。ハイフンと大なり記号を組み合わせたものです。

struct Animal {  
    int age;  
    int weight;  
};  

void animal_test(void) {  
    struct Animal animal;  // 構造体変数animal  
    struct Animal *p = &animal;  // アドレスをpに代入  

    p->age = 20;  // メンバageの値を変更  
    p->weight = 60;  // メンバweightの値を変更  

    printf("%d %d\n", p->age, p->weight);  // 20 60  
}  

上記の場合、構造体Animalにはメンバ変数ageweightがあります。
そしてanimalという構造体変数を作りpanimalのアドレスを入れます。
pからanimalのメンバにアクセスするには

p->age  
p->weight  

というようにpからアロー演算子を伸ばしてメンバ変数を書きます。
こうすることでメンバ変数の変更や参照ができるようになります。

動的メモリ確保とポインタ

上級者向けですがC言語には動的メモリ確保というものがあります。
これはヒープ領域からメモリを動的に割り当てるものです。
普通の自動変数などはスタック領域からメモリが確保されますが、ヒープ領域を使うとメインメモリ(たとえば8GBとか16GBなどの容量があるRAM)からメモリを確保できます。
ですので動的メモリ確保を使うと1GBぐらいのメモリも難なく確保できます(環境によります)。

この動的メモリ確保でもC言語ではポインタを使います。

mallocでメモリを確保

動的メモリ確保を行う関数にはmalloc()calloc()などがあります。
これらの関数はstdlib.hをインクルードすると使えます。
malloc()は指定バイト数のメモリを動的に確保し、calloc()は確保した後にメモリを初期化します。

int *p = malloc(sizeof(int));  // ヒープ領域からint型のバイト数のメモリを確保  

*p = 123;  // 値を代入  
printf("%d\n", *p);  // 123  

free(p);  // 動的に確保したメモリを解放  

malloc()系の関数は動的にメモリを確保したらvoid *のポインタを返します。
これを好きな型のポインタにキャストすることでメモリ確保が成立します。
上記ではint型のサイズ(バイト数)をsizeof()で求めてそれをmalloc()に渡しています。
そしてその返り値をint型のポインタpに入れています。

malloc()の関数が返す動的なメモリのアドレスはfree()で解放する必要があります。
C言語では動的メモリを使い終わったら自分で手動で動的メモリを解放する必要があるのです。
これを忘れるとメモリリークというバグになります。

ダブルフリーの恐怖

動的に確保したメモリをfree()し再度free()するとダブルフリーというエラーになります。

int *p = malloc(100);  
free(p);  
free(p);  // ダブルフリー!  

動的に確保されたメモリのアドレスはfree()されると無効になります。
ポインタpには一回目のfree()のあとにもアドレスが入ったままです。
この状態で再度pfree()すると無効なアドレスを解放することになり、エラーになります。
(環境によってはエラーにならないかもしれません。しかしバグであることは確かです)

ダブルポインタ

ポインタのアスタリスクは複数付けることができます。
ポインタ変数の宣言時にアスタリスクを2つ付けると俗にダブルポインタと呼ばれるポインタになります。

int **pp = NULL;  

ダブルポインタはよくポインタ配列のアドレスを格納したい時に使われます。
ポインタ配列は例えば文字列リテラルの配列などです。

// 文字列リテラルのポインタ配列  
const char *ary[] = {  
    "Hello",  
    "World",  
    NULL,  
};  
const char **pp = ary;  // アドレスをppに代入  

printf("%s\n", pp[0]);  // Hello  
printf("%s\n", pp[1]);  // World  

ダブルポインタの他にはもちろんトリプルポインタ(アスタリスク3つ)も存在します。
しかしよく使うのはダブルポインタの方でしょう。

関連記事
C言語のポインタのポインタを解説

おわりに

今回はC言語のポインタを解説しました。
何か参考になれば幸いです。

🦝 < ポインタでショートカット!

🐭 < はかどる~