Web Analytics Made Easy - StatCounter

工業大学生ももやまのうさぎ塾

うさぎでもわかるをモットーに大学レベルの数学・情報科目をわかりやすく解説! 数式が読み込まれない場合は1回再読み込みしてみてください。

うさぎでもわかるC言語のポインタ講座

C言語に出てくるポインタって難しいですよね。

おそらく、C言語を学ぶ上で大きな壁だと思います。


そこで、今回はポインタについて簡単にですが、うさぎでもわかるようにわかりやすくまとめていきたいと思います。

1.ポインタとは

ポインタとは、アドレスを示す変数のことを表します!

と言われてもわかりにくいので補足をしていきましょう。


プログラムを書く際に宣言した変数は必ずメモリのどこかに保存されます。

アドレスとは、宣言した変数が保存されているメモリの場所を表します。

住所みたいなもんですね!


実際に宣言したメモリがどこに保存されているか見てみましょう。

宣言した変数に & を付けるとアドレスを見ることができます。例えば変数 a のアドレスは &a でみることができます。

#include <stdio.h>

int main(void){
    int a = 10; // 変数aを初期値10で宣言
    
    printf("aの値:%d\n",a);
    printf("aのアドレス:%d\n",&a); // 変数に & を付けるとアドレスになる
}

実行してみると、

aの値:10
aのアドレス:1471802164

となります。つまり、a のアドレスは 1471802164 番地にあることがわかりますね。

f:id:momoyama1192:20200111085906g:plain


今度は実際にポインタを使ってみましょう。

ポインタを使う際には、変数の前に * をつけます。

例えば b をポインタで宣言するときには int *b; と書きます。

#include <stdio.h>
int main(void){
    int a = 10; // 変数aを初期値10で宣言
    
    int *b; // ポインタ変数bを宣言
    
    b = &a; // ポインタ変数bに通常の変数aのアドレスを代入
    
    printf("aのアドレス:%d\n",&a); //通常の変数のアドレスは & をつける 
    printf("bのアドレス:%d\n",b);  //ポインタ変数のアドレスはなにもつけない

    printf("aの値:%d\n",a);   
    printf("bの値:%d\n",*b);  //ポインタ変数の中身を見るときは * をつける
}

実行してみると、

aのアドレス:-185129860
bのアドレス:-185129860
aの値:10
bの値:10

となり、確かにポインタ変数にアドレスが入っていますね!

f:id:momoyama1192:20200111085911g:plain


もう少し実用的なプログラムを見てみましょう。

ポインタ変数に値を代入する際には、変数の前に * を付けます。

#include <stdio.h>
int main(void){
    int a = 10; // 変数aを初期値10で宣言
    
    int *b; // ポインタ変数bを宣言
    
    b = &a; // ポインタ変数bにaのアドレスを代入
    *b = 30; // ポインタ変数bの「値」を30に
    
    printf("aの値:%d\n",a);   
    printf("bの値:%d\n",*b);  //ポインタ変数の中身を見るときは * をつける

}

このようにポインタ変数の値を変えると、

aの値:30
bの値:30

もとの変数 a の値も変わりますね!

f:id:momoyama1192:20200111085915g:plain


ここで、普通の変数とポインタ変数の宣言、および値・アドレスの代入の方法を表で確認しておきましょう。

使用法 普通の変数 ポインタ
宣言 / 引数 a *a
値代入 a *a
アドレス代入 &a a

宣言や関数の引数で使う *a と値の代入で使う *a は異なるものなのでごちゃごちゃにならないようにしましょう。

2.値渡しと参照渡し

普通の変数とポインタ変数の大きな違いとして、値渡しと参照渡しがあります。

2つの大きな違いは関数内で引数の値を変えたときにmain関数に影響するかしないかです。

(1) 値渡し

値渡しでは、main関数の変数をコピーしてから関数の引数に渡します。

C言語では、ポインタではない普通の変数の引数が値渡しで渡されます。

f:id:momoyama1192:20200111085923g:plain

変数をコピーしてから渡すので、関数内で値を変更してもmain関数には一切影響しません

1つプログラムを見てみましょう。

#include <stdio.h>

// 値渡しの関数(引数は普通の変数)
int func1(int a) {
    a += 10;
    return a;
}

int main(void){
    int a,b;
    
    a = 10;
    
    b = func1(a);
    printf("aの値:%d\n",a);
    printf("bの値:%d\n",b);  
}

上のプログラムを実行すると、

aの値:10
bの値:20

となり、関数 func1 内で a の値を変更しても一切main関数の a の値には響いていませんね。

値渡しでは、基本的に値を返すreturn する)ことで計算結果をmain関数に持ち込みます。

(2) 参照渡し

参照渡しでは、main関数のアドレスをコピーしてから関数の引数に渡します。

値渡しのようにそのまま値は渡されません。場所のみが渡されます


C言語では、ポインタ変数、および配列で参照渡しとなります。

f:id:momoyama1192:20200111085927g:plain

変数がある場所(アドレス)のみを渡すので、関数内で値を変更するとmain関数の値も変化します

1つプログラムを見てみましょう。

#include <stdio.h>

// 参照渡しの関数(引数はポインタ変数)
int func2(int *a) {
    *a += 10; // ポインタ変数の値は * をつける 
    return *a; 
}

int main(void){
    int a,b;
    
    a = 10;
    
    b = func2(&a); // aのアドレスを渡すので &a
    printf("aの値:%d\n",a);
    printf("bの値:%d\n",b);  
}

上のプログラムを実行すると、

aの値:20
bの値:20

となり、関数 func2 内で a の値を変更するとmain関数の a の値も変化します。


参照渡しの実用例を1つ見てみましょう。

参照渡しでは、値が変化するとmain関数の値を変化させられるので、次のような2つの変数を入れ替える関数を作ることができます。

//ポインタで渡された変数 a, b の値を入れ替える関数
void swap(int *a, int *b)
{
    int tmp = *a; // a の値を代入するので * を付ける
    *a = *b; // 値代入なので両方 * を付ける
    *b = tmp;
}


値渡し・参照渡し
  • 値渡し(引数がポインタではない普通の変数のとき)
    → 関数内で値を変更してもmain関数に影響しない
  • 参照渡し(引数がポインタ変数・配列のとき)
    → 関数内で値を変更するとmain関数の値も変化する

では、ここで値渡しと参照渡しが理解できているかの簡単な練習をしてみましょう。

練習

次のプログラムを実行したときの実行結果を答えなさい。

#include <stdio.h>

int func(int a, int *b) {
    *b += 5;
    a += *b; 
    return a; 
}

int main(void){
    int a = 10, b = 20, c = 30;
    
    c = func(a,&b);
    printf("(a,b,c) = (%2d,%2d,%2d)\n",a,b,c);
    
    c = func(b,&a);
    printf("(a,b,c) = (%2d,%2d,%2d)\n",a,b,c);
}

解答

(a,b,c) = (10,25,35)
(a,b,c) = (15,25,40)

関数 func は、1つ目の引数 a は普通の変数なので値渡し、2つ目の引数 b* がついているポインタ変数なので参照渡しとなります。

まず、最初に

// この時点では a = 10, b = 20, c = 30
    c = func(a,&b);
    printf("(a,b,c) = (%2d,%2d,%2d)\n",a,b,c);

の部分を見ていきましょう。

1つ目の引数 a は値渡しされるので10のままですね。

一方2つ目の引数 b は参照渡しされるので値が変化します。関数 func

int func(int a, int *b) {
    *b += 5; // b に5加算
    a += *b;  // ここではbの値変化なし
    return a; 
}

を実行すると b は5加算されます。なので b は25となります。

最後に c には関数の返り値が変えるので、プログラムをたどると35が返されることがわかります。

よって、(a,b,c) = (10,25,35) となります。


つぎに

// この時点では a = 10, b = 25, c = 35
    c = func(b,&a);
    printf("(a,b,c) = (%2d,%2d,%2d)\n",a,b,c);

の部分を見ていきましょう。

1つ目の引数 b は値渡しされるので25のままですね。

一方2つ目の引数 a は参照渡しされるので値が変化します。関数 func

int func(int a, int *b) {
    *b += 5; // b(mainのa)に5加算
    a += *b;  // ここではb(mainのa)の値変化なし
    return a; 
}

を実行すると a は5加算されますね。なので a は15となります。

最後に c には関数の返り値が変えるので、プログラムをたどると40が返されることがわかります。

よって、(a,b,c) = (15,25,40) となります。

3.配列とポインタ

配列とポインタには密接な関係があります。

ポインタと同じく、配列も参照渡しされます。


早速ですが下のプログラムを見てください。

#include <stdio.h>

int main() {
    int array[3] = {15,25,35}; // 配列宣言
    int *array_p; // ポインタ宣言
    array_p = array; // array 配列の先頭(array[0])番地をコピー
    // array_p = &array[0]; と同じ

    printf("配列  :(%d,%d,%d)\n" , array[0] , array[1] , array[2]);
    printf("ポインタ:(%d,%d,%d)\n", *array_p , *(array_p + 1) , *(array_p + 2));
        
    return 0;
}

下のプログラムを実行するとなんと、

配列  :(15,25,35)
ポインタ:(15,25,35)

と普通に実行できてしまいます。


実は配列 a とポインタ *p を宣言し、

p = a; // p = &a[0]; と同じ

と書くと、ポインタ変数 *p に配列 a の先頭アドレス(つまり &a[0])を代入することができます。


ここでなぜ配列も参照渡しされるかの理由を説明したいと思います。

下のような配列を引数にするような関数があるとします。

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

皆さんが上の関数 disp を呼び出すとき、

disp(a,n); // disp(&a[0],n); と同じ

と書きますよね。

実はこの操作、配列 a の先頭アドレス (&a[0]) を関数に渡していますよね。

f:id:momoyama1192:20200111085901g:plain

配列もポインタと同じくアドレスが渡される(値は渡されない)ので、配列もポインタと同じく参照渡しとなるのです!

そのため、 void disp(int a[], int n) の部分を

void disp(int *a, int n)

と書き換えても全く同じように実行することができます!


また、配列は下のように同じ型のデータが一定番地間隔に並んでいます。

f:id:momoyama1192:20200111085857g:plain

なので、例えば a[2] のデータを *(p+2) のように表すことができます*1


下のプログラムは、(1)〜(3)それぞれの方法で配列の中身を出力する操作を表しています。

#include <stdio.h>
#define N 9

int main() {
    int a[9] = {1,2,3,4,5,6,7,8,9};
    int *p;
    p = a;

    // (1) 配列を使う
    for(int i = 0; i < N; i += 1) {
        printf("%2d ",a[i]);
    }    
    printf("\n");
    
    // (2) ポインタを使い、pの番地自身は変化させない(相対的)
    for(int i = 0; i < N; i += 1) {
        printf("%2d ",*(p+i)); 
    }    
    printf("\n");
    
    // (3) ポインタを使い、pの番地自身を変化させる(絶対的)
    for(int i = 0; i < N; i += 1) {
        printf("%2d ",*p);
        p += 1;
    }    
    printf("\n");
        
    return 0;
}

表記は異なりますが、(1)~(3)の操作はどれも全く同じ操作です!

4.ポインタと構造体

構造体でもポインタを使うことができます。

例えば、重要なデータ構造の1つであるリスト構造*2は、

struct LIST {
    int data;  // 要素
    struct LIST *next; // 次のデータの場所(ポインタ)
} ;  // 意外とこのセミコロン忘れやすい

のように宣言します。

ドット演算子とアロー演算子

ドット演算子とアロー演算子はともに構造体の中の変数(メンバ)を参照するときに使うのですが、構造体自体の変数が「普通の変数」か「ポインタ変数」かによって使う演算子が変わってきます

ドット演算子

ドット演算子 . は、ポインタではない普通の構造体に対して使います。

例えば、下のような構造体

struct VEC {
    int x; 
    int y;
} ;  // セミコロン忘れずに

があるとします。

このとき、構造体 VEC 型の中の変数(メンバと呼びます) x, y の値を使いたいときには、

struct VEC a;

a.x = 30;
a.y = 40;

のように 変数名.メンバ名 の形で書きますね。この .(ピリオド)のことをドット演算子と呼びます。

アロー演算子

一方アロー演算子 -> は、ポインタで定義された構造体に対して使います。

例えば、VEC 型のポインタ変数 *b を宣言するとします。

このとき、宣言したポインタ変数のメンバの中身を操作する際に

(*b).x = 30;
(*b).y = 40;

のように少し複雑な表記をしなければなりません。そこで、C言語では

b->x = 30; // (*b).x = 30; と同じ
b->y = 40; // (*b).y = 40; と同じ

のように省略して書くことができます。これをアロー演算子と呼びます。


アロー演算子を使うことで先程紹介したリストの構造体

struct LIST {
    int data;  // 要素
    struct LIST *next; // 次のデータの場所(ポインタ)
} ;  // 意外とこのセミコロン忘れやすい

をより直感的に使うことができます。

例えば、リストの先頭を表している *top の2つ先のデータの要素を見るときに、

top->next->next->data; // top の次の次のデータの要素

のような書き方をしたり、3つ先のリストの場所を指すときに、

top->next->next->next; // topの次の次の次のデータ

のような書き方ができます。

f:id:momoyama1192:20200111170614g:plain

5.まとめ(さいごに)

今回は、

  • ポインタは変数のアドレスを示すときに使う変数
  • ポインタではない普通の関数では値渡し、ポインタ変数と配列では参照渡しが行われている
  • 配列とポインタは密接な関係あり
  • 構造体の2つのメンバの表現法(ドット演算子とアロー演算子)

についてまとめました。


この記事を読んでポインタを少しでも理解していただけたら幸いです。

では今日の授業はここまで。また次回!

*1:もちろん p = a (p = &a[0]) をしていないとできませんよ。

*2:リストは「要素」と「次のデータを指し示すポインタ(場所)」の2つからなるデータが数珠のようにつながっているデータ構造です。詳しくは次回の記事で説明したいと思います。