【C言語入門】ポインタのわかりやすい使い方(配列、関数、構造体)

こんにちは!フリーランスの長野です。

ポインタって使ってますか?

ポインタの使い方を理解するのは最初はなかなか難しいかもしれません。C言語を学習する上では、どうしても手が止まってしまう部分です。

しかし、ポインタを使ってやっていることはアドレスかアドレス先の値を操作するかどちらかに限られます。これを変数や配列、関数、構造体に対して適用して使っています。

配列、関数、構造体のようにひとかたまりにしたものを、ポインタを使うことでひとかたまりのまま扱うことができるので便利です。配列の要素を一つずつ扱ったり、構造体のメンバを一つずつ扱う手間を省くことができます。

ですのでポインタを使いこなせるようになると、記述の手間を省きつつもできることが増えます。

この記事では、ポインタについて

  • ポインタとは
  • ポインタを使うメリットとは
  • ポインタの使い方について
  • アドレスの使い方について
  • ポインタの演算について
  • ポインタのポインタについて
  • 配列でのポインタについて
  • 関数でのポインタについて
  • 構造体でのポインタについて


など基本的な内容から、具体的な使い方の内容についても解説していきます。

今回はポインタについて、使い方をわかりやすく解説します!

目次

ポインタ、アドレスとは

ポインタとは、変数のアドレスを記憶する変数のことです。

アドレスとはメモリ上に与えられた番号のことです。変数を宣言すると、その変数にアドレスすなわちメモリ上の番号が与えられます。

アドレスにアクセスすることで変数の値を取得することができます。アドレスはデフォルトでは16進数で表されます。また、ポインタ変数は整数の加減算ができます。

メモリ上の番号の演算やアドレス先の値を取得する際に使用します。

ポインタを使うメリットとは

ポインタを使うメリットについてはいくつか挙げられます。

  • ポインタを使うことで、アドレス先の値を取得したり、変更することができる。
    特に関数の引数でポインタを使って参照渡しを行うと、複数の変数を1つの関数で処理変更できる。
  • ひとつのポインタ変数で、配列のすべての要素の値を取得したり、変更することができる。
    したがって、配列の要素を一つずつ扱う手間が省ける。
  • ひとつのポインタ変数で、構造体のすべてのメンバの値を取得したり、変更することができる。
    したがって、構造体のメンバを一つずつ扱う手間が省ける。
  • ひとつのポインタ変数で、複数の関数から使う関数を選択できる。
    したがって、後からでも簡単に処理内容を変更することができる。


などが挙げられます。

それぞれのメリットについては、これから詳しく解説していきます。

ポインタの使い方について

ポインタの使い方について説明していきます。

ポインタの使い方について

まずはポインタの使い方について説明します。

ポインタ変数は変数名の前に「*」(アスタリスク)を付けて宣言します。ポインタ変数には変数のアドレスを代入します。

変数のアドレスは変数名の前に「&」(アンパサンド)を付けて表します。変数とポインタ変数は同じ型である必要があります。

それではサンプルコードで確認していきましょう。

#include <stdio.h>
 
int main(void) {
    int num = 1; // int型変数
    int *p_num; // int型ポインタ変数
    p_num = &num; // ポインタ変数p_numに変数numアドレスを代入
    
    printf("int型変数numの値:%d\n", num);
    printf("int型ポインタ変数p_num:%p\n", p_num);
 
    return 0;
}

実行結果:

int型変数numの値:1
int型ポインタ変数p_num:0x7ffd0a0ecc6

このサンプルコードでは、int型のポインタ変数「p_num」を宣言しています。

ポインタ変数「p_num」に同じint型の変数「num」のアドレス「&num」を代入しています。ポインタ変数をprintf関数で出力表示する際にアドレスを表示するために「%p」変換子を使用しています。

アドレスの使い方について

次にアドレスの使い方について説明します。

変数のアドレスは変数名の前に「&」(アンパサンド)を付けて表します。変数のアドレスがポインタ変数の値となります。変数のアドレスの前に「*」(アスタリスク)を付けるとアドレス先の値を取得することができます。

それではサンプルコードで確認していきましょう。

#include <stdio.h>
 
int main(void) {
    int num = 1; // int型変数
    int *p_num; // int型ポインタ変数
    p_num = &num; // ポインタ変数p_numに変数numアドレスを代入
        
    printf("int型変数numのアドレス:%p\n", &num);
    printf("int型変数numのアドレス先の値:%d\n", *(&num));
    printf("int型ポインタ変数p_numの参照先の値:%d\n", *p_num);
 
    return 0;
}

実行結果:

int型変数numのアドレス:0x7ffd0a0ecc64
int型変数numのアドレス先の値:1
int型ポインタ変数p_numの参照先の値:1

このサンプルコードではint型変数「num」のアドレス「&num」をprintf関数を使って出力表示しています。アドレス「&num」の前に「*」(アスタリスク)を付けアドレス先の値を取得表示しています。

またアドレス「&num」を代入したポインタ変数「p_num」の前に「*」(アスタリスク)を付けても、同様にアドレス先の値を取得表示していることがわかります。

ポインタの演算について

ポインタの演算について説明します。

配列の要素はメモリ上の番号つまりアドレスが連続していますので配列の要素を操作する場合にポインタの演算を使用すると便利です。

#include <stdio.h>
 
int main(void) {
    char str[] = "Hello";
    int i_arr[] = {0, 1, 2, 3, 4};
    float f_arr[] = {0.0f, 0.1f, 0.2f, 0.3f, 0.4f};
    double d_arr[] = {0.0, 0.1, 0.2, 0.3, 0.4};
    
    char *p_chr; // char型ポインタ変数
    int *p_int; // int型ポインタ変数
    float *p_flt; // float型ポインタ変数
    double *p_dbl; // double型ポインタ変数
    
    // ポインタにアドレスを代入
    p_chr = str;
    p_int = i_arr;
    p_flt = f_arr;
    p_dbl = d_arr;
    
    printf("char型ポインタ変数p_chr:%p, アドレス先の値:%c\n", p_chr, *p_chr);
    printf("char型配列要素str[0]のアドレス:%p, アドレス先の値:%c\n", &str[0], *(&str[0]));
    printf("char型ポインタ変数(p_chr + 1):%p, アドレス先の値:%c\n", p_chr + 1, *(p_chr + 1));
    printf("char型配列要素str[1]のアドレス:%p, アドレス先の値:%c\n", &str[1], *(&str[1]));
 
    printf("int型ポインタ変数p_int:%p, アドレス先の値:%d\n", p_int, *p_int);
    printf("int型配列要素i_arr[0]のアドレス:%p, アドレス先の値:%d\n", &i_arr[0], *(&i_arr[0]));
    printf("int型ポインタ変数(p_int + 1):%p, アドレス先の値:%d\n", p_int + 1, *(p_int + 1));
    printf("int型配列要素i_arr[1]のアドレス:%p, アドレス先の値:%d\n", &i_arr[1], *(&i_arr[1]));
    
    printf("float型ポインタ変数p_flt:%p, アドレス先の値:%.1f\n", p_flt, *p_flt);
    printf("float型配列要素f_arr[0]のアドレス:%p, アドレス先の値:%.1f\n", &f_arr[0], *(&f_arr[0]));
    printf("float型ポインタ変数(p_flt + 1):%p, アドレス先の値:%.1f\n", p_flt + 1, *(p_flt + 1));
    printf("float型配列要素f_arr[1]のアドレス:%p, アドレス先の値:%.1f\n", &f_arr[1], *(&f_arr[1]));
    
    printf("double型ポインタ変数p_dbl:%p, アドレス先の値:%.1lf\n", p_dbl, *p_dbl);
    printf("double型配列要素d_arr[0]のアドレス:%p, アドレス先の値:%.1lf\n", &d_arr[0], *(&d_arr[0]));
    printf("double型ポインタ変数(p_dbl + 1):%p, アドレス先の値:%.1lf\n", p_dbl + 1, *(p_dbl + 1));
    printf("double型配列要素d_arr[1]のアドレス:%p, アドレス先の値:%.1lf\n", &d_arr[1], *(&d_arr[1]));
    
    return 0;
}

実行結果:

char型ポインタ変数p_chr:0x7fff3e4c1170, アドレス先の値:H
char型配列要素str[0]のアドレス:0x7fff3e4c1170, アドレス先の値:H
char型ポインタ変数(p_chr + 1):0x7fff3e4c1171, アドレス先の値:e
char型配列要素str[1]のアドレス:0x7fff3e4c1171, アドレス先の値:e
int型ポインタ変数p_int:0x7fff3e4c1100, アドレス先の値:0
int型配列要素i_arr[0]のアドレス:0x7fff3e4c1100, アドレス先の値:0
int型ポインタ変数(p_int + 1):0x7fff3e4c1104, アドレス先の値:1
int型配列要素i_arr[1]のアドレス:0x7fff3e4c1104, アドレス先の値:1
float型ポインタ変数p_flt:0x7fff3e4c1120, アドレス先の値:0.0
float型配列要素f_arr[0]のアドレス:0x7fff3e4c1120, アドレス先の値:0.0
float型ポインタ変数(p_flt + 1):0x7fff3e4c1124, アドレス先の値:0.1
float型配列要素f_arr[1]のアドレス:0x7fff3e4c1124, アドレス先の値:0.1
double型ポインタ変数p_dbl:0x7fff3e4c1140, アドレス先の値:0.0
double型配列要素d_arr[0]のアドレス:0x7fff3e4c1140, アドレス先の値:0.0
double型ポインタ変数(p_dbl + 1):0x7fff3e4c1148, アドレス先の値:0.1
double型配列要素d_arr[1]のアドレス:0x7fff3e4c1148, アドレス先の値:0.1

このサンプルコードではchar型、int型、float型、double型、それぞれの配列のポインタの演算結果と配列の要素を比較しています。

いずれの配列においても、ポインタ変数の値は配列の先頭のアドレスと一致しています。ポインタ変数の値に1加算すると配列の次の要素のアドレスと一致しています。

char型の配列はポインタ変数の値を1加算するとアドレスの値も1増えています。英文字1文字では1バイト(アドレス1個分)使用していることがわかります。

int型の配列はポインタ変数の値を1加算するとアドレスの値は4増えています。int型の変数1個では4バイト(アドレス4個分)使用していることがわかります。

float型の配列はポインタ変数の値を1加算するとアドレスの値は4増えています。float型の変数1個では4バイト(アドレス4個分)使用していることがわかります。

double型の配列はポインタ変数の値を1加算するとアドレスの値は8増えています。double型の変数1個では8バイト(アドレス8個分)使用していることがわかります。

ポインタのポインタについて

ポインタとは、変数のアドレスを記憶する変数のことでした。

ポインタもある種の変数です。

ということは、ポインタという変数のアドレスもメモリのどこかに存在し、アドレス番号が付けられています

ですので、ポインタにポインタのアドレスを格納することも可能です。これをポインタのポインタと言います。

ポインタのポインタを宣言するには、さらに「*」(アスタリスク)を付けます。

型名 **変数名;

サンプルコードで確認しましょう。

#include <stdio.h>
 
int main(void) {
    int num = 1; // int型変数
    int *p_num; // int型ポインタ変数
    p_num = &num; // ポインタ変数p_numに変数numアドレスを代入
    
    int **pp_num; // int型ポインタ変数のポインタ
    pp_num = &p_num; // ポインタ変数のポインタpp_numにポインタ変数p_numのアドレスを代入
    
    printf("int型変数numの値:%d\n", num);
    printf("int型ポインタ変数p_num:%p\n", p_num);
    printf("int型ポインタ変数のポインタpp_num:%p\n", pp_num);
    
    return 0;
}

実行結果:

int型変数numの値:1
int型ポインタ変数p_num:0x7ffef434156c
int型ポインタ変数のポインタpp_num:0x7ffef43415

このサンプルコードでは、int型の変数numのアドレスを格納するポインタ変数p_numをまず宣言、定義しています。

このポインタ変数p_numのアドレスを格納するために、ポインタ変数のポインタpp_numを次に宣言、定義しています。

配列でのポインタの使い方

配列でのポインタの使い方についてみていきましょう。

アドレスアクセスについて

配列の要素の値を取得する際にポインタを使ってアドレスから取得する方法があります。

使い方についてはこちらのサイトで詳しく解説していますので参考にしてくださいね!

【C言語入門】配列の使い方まとめ(初期化、代入、宣言、コピー)
更新日:2024年3月1日

2次元配列のポインタのポインタの使い方について

ポインタのポインタを使うことで2次元配列の要素の値を取得する方法があります。

使い方についてはこちらのサイトで詳しく解説していますので参考にしてくださいね!

【C言語入門】2次元配列の使い方まとめ
更新日:2024年3月1日

関数でのポインタの使い方

関数でのポインタの使い方について説明します。

引数での参照渡しの使い方

引数にポインタを利用すると値を参照渡しすることができて、2つ以上の複数の変数を処理することができます。

使い方についてはこちらで詳しく解説していますので参考にしてくださいね!

【C言語入門】関数の作り方、呼び出し方(宣言、引数、戻り値)
更新日:2024年3月1日

関数ポインタの使い方について

関数ポインタの使い方について説明します。

関数ポインタは関数が格納されたメモリアドレスを取得します。関数を呼び出す際に関数ポインタの内容を別の関数のアドレスに変更することで、呼び出す関数を変更することができます。

つまり、呼び出す関数を動的に変更することができます。ただし、引数の型と引数の数はそろえる必要があります。

「typedef」句を使って関数ポインタの型を宣言すると記述が簡潔になります。

typedef 戻り値の型 (*関数ポインタ型名)(引数);

この関数ポインタ型名のオブジェクトを生成して使用します。

関数ポインタ型名 オブジェクト名;

サンプルコードで確認しましょう。

#include <stdio.h>
 
// 関数ポインタの宣言
typedef int (*calc)(int i1, int i2);
 
/* 関数ポインタに代入する関数の定義
 * 関数ポインタの引数の型と引数の数をそろえる*/
int sum(int i1, int i2) {
    return i1 + i2;
}
 
int mul(int i1, int i2) {
    return i1 * i2;
}
 
int main(void) {
    int i1, i2, result;
    calc c; // calc型関数ポインタのオブジェクトを生成
    
    i1 = 2;
    i2 = 3;
    
    c = &sum; // 関数ポインタに関数アドレスを代入
    result = c(i1, i2); // 関数ポインタより関数の呼び出し
    printf("i1 + i2 = %d\n", result);
    
    c = &mul; // 関数ポインタに関数アドレスを代入
    result = c(i1, i2); // 関数ポインタより関数の呼び出し
    printf("i1 * i2 = %d\n", result);
 
    return 0;
}

実行結果:

i1 + i2 = 5
i1 * i2 = 6

このサンプルコードではcalc型の関数ポインタを宣言しています。これを「c」と定義してオブジェクトを生成し使用しています。

オブジェクト「c」に「sum」関数のアドレスを代入すると足し算が実行され、「mul」関数のアドレスを代入すると掛け算が実行されています。

このように関数ポインタを使うと処理内容を意図的に変えることができるので便利です。

構造体でのポインタの使い方

構造体でのポインタの使い方について説明します。

実体のポインタの使い方

実体にポインタを使うとすべてのメンバを含めた実体全体を操作することができるので便利です。

使い方についてはこちらで詳しく解説していますので参考にしてくださいね!

【C言語入門】構造体の使い方(struct、ポインタ、アロー演算子)
更新日:2024年3月1日

アロー演算子の使い方について

アロー演算子は構造体の実体のポインタからメンバを呼び出す際に使用します。

「->」記号を使います。

下記のように記述して使用します。

構造体の実体のポインタ変数名->メンバ名

サンプルコードで確認していきましょう。

#include <stdio.h>
 
typedef struct {
    int num;
    char *str;
} strct;
 
int main(void) {
    strct entity, *p;
    p = &entity; // strct型ポインタ変数にstrct型構造体のアドレスを代入
    
    // ポインタを使ってメンバの初期化
    p->num = 0;
    p->str = "Hello";
    
    printf("num:%d\n", entity.num);
    printf("str:%s\n", entity.str);
 
    return 0;
}

実行結果:

num:0
str:Hello

このサンプルコードでは、strct型の構造体を宣言しています。

strct型ポインタ変数にstrct型構造体のアドレスを代入しています。ポインタ変数からアロー演算子を使用して構造体のメンバを呼び出し値を代入しています。

strct型構造体の実体からメンバを呼び出し出力表示しています。

まとめ

ここでは、ポインタについて説明しました。

ポインタを使うとアドレス先から値を操作することができます。そうすることで配列、関数、構造体など複雑な構成を簡単な記述で扱うことができます。

ポインタはたくさんの用途で使えて便利ですので使いこなすことができるように、この記事を何度も参考にして下さいね!

この記事を書いた人

熊本在住のフリープログラマ兼ライターです。C/C++/C#、Java、Python、HTML/CSS、PHPを使ってプログラミングをしています。専門は画像処理で最近は機械学習、ディープラーニングにはまっています。幅広くやってきた経験を活かしてポイントをわかりやすくお伝えしようと思います。
お問合せはこちらでも受け付けています。
info@sss-lab.com

目次