目次

ポインタ(1) 〜基礎〜

ポインタとは?

ポインタとは、「変数などのメモリアドレスを格納する変数」のことです。普通の変数と同じく、その値(=アドレス)を使うこともできますし、そのポインタが指すアドレスに格納されている値を使うこともできます。詳しい使い方等は後述します。

ポインタを使う

ポインタ変数を宣言するには、

型 *変数名;

とします。

また、ポインタ変数が指しているアドレスの中身を参照するには演算子「*」を。変数が格納されている場所のアドレスを参照するには演算子「&」を、それぞれ変数の前につけてやります。

言葉で説明してもイマイチよくわからないと思うので、例を見てみましょう。

int foo = 100;    /*整数型変数*/
int *bar;         /*整数に対するポインタ変数*/

bar = &foo;       /*変数fooのアドレスをポインタ変数barに格納*/

printf("fooの中身  :%d\n",foo);
printf("fooのアドレス:%p\n",&foo);
printf("barの中身  :%p\n",bar);
printf("barが指すアドレスの中身:%d\n",*bar);

「%p」というのはフォーマット指定子の一種です。整数を表示したいときに「%d」を使うように、アドレスを表示したいときにはこれを用います。

さて、上の例を実行すると…

fooの中身  :100
fooのアドレス:00123abc
barの中身  :00123abc
barが指すアドレスの中身:100

このような結果が表示されるはずです。「00123abc」は、アドレスを16進数で表示しているものです。

※勿論、アドレスは環境によって異なります。

このときのメモリの状態を図で表しておきましょう。(「0x」は後に続く数が16進数だという意味です)

上のソース実行時のメモリの状態簡易図

因みに、「scanf」関数などで、よく変数の前に「&」をつけるのは、scanf関数には「格納したい変数のポインタ」を渡すことになっているからです。

注意

ポインタ変数に何も代入しないままそのポインタが指す先を読み書きするとエラーの原因となります。必ず他の変数のアドレスを代入するか、後で解説するメモリの確保を行ってから操作するようにしましょう。

さて、ここまでは難なく理解している人も多いでしょう。しかしポインタが本領を発揮するのは、次の項で扱う関数との連携と、構造体を用いてさまざまなデータ構造を実現するときです。これで前置きは終わり。次項から、肝となる部分を説明していきます!

Point!

  • ポインタとは、メモリアドレスを格納する変数のこと
  • ポインタ型の宣言は、「型 *変数名」
  • ポインタが指す先の値を知りたいときは「*」演算子
  • ある変数のアドレスを知りたいときは「&」演算子

ポインタ(2) 〜値の変更〜

ポインタを使った変数の値の変更

ポインタを使うと、そのポインタが指す変数の値を書き換えることができます。

先ずは簡単な例から。変数fooの中身を、ポインタ変数barを通して置き換えます。

int foo = 100;
printf("foo = %d\n",foo);
int *bar = &foo;	/*fooのアドレスをbarに*/
*bar = 50;        /*ここで値を代入*/
printf("foo = %d\n",foo);

実行結果は、

foo = 100
foo = 50

となります。4行目で、barが指すアドレスの値(つまりfooの値)を50に変更しています。「*」演算子はこのように、ポインタが指すアドレスの中身を書き換えたい時にも使えます。

さてもう少し理解を深めてみましょう。下図を見てください。ポインタを使う場合(緑)と使わない場合(赤)のアクセスの仕方の違いです。

ポインタを使う場合とそうでない場合の、メモリアクセスのイメージ

図を見るとわかると思いますが、「変数とその値は1対1対応」しているわけではありません。変数に対応しているのはあくまでも「その変数に対応するメモリアドレス」であり、変数はそのアドレスへの案内役にすぎないのです。だからこそ、先の例(緑の矢印に相当)のように変数fooを全く使わずにfooの値を変更するなどということができたわけです。

関数間での変数の共有

さて、ここからが一つ目の山場となります。気合入れていきましょう!

早速ですが例を見てください。main関数のfooという変数の値を、hoge関数の中で書き換えています。

void hoge(int *bar){
    *bar = 50;     /*「bar」の指す先の値を50に*/
}

int main(){
    int foo=100;
    printf("%d\n",foo);
    hoge(&foo);    /*「foo」のアドレスをhogeに渡す*/
    printf("%d\n",foo);
    return 0;
}

実行結果は

100
50

となります。fooはhoge関数のスコープ外にありますから、hoge関数内からは「foo」という名前でmain関数内のfooにアクセスすることはできません

しかし、変数はスコープ外でもメモリの同じ位置にあり続けますから、hoge関数にfooのアドレスを渡せば、それを用いて値を書き換えられるというわけです。そしてこれは前項で行ったことを、関数をまたいで行っているだけのことです。

Point!

  • 「*ポインタ変数」に代入すると、ポインタが指す先の値を変更できる
  • 変数はスコープの外にでてもメモリ中で同じ位置にある
  • そのためアドレスを用いると、スコープ外の変数にアクセスすることができる

この項で紹介した事実が、ポインタを用いた有用なテクニックの基礎となっています。後ほど、これらを基にしたもうすこし具体的な技術を紹介します。

ポインタ(3) 〜ポインタと配列〜

ポインタのもう一つの魅力は、配列と深い関係にあることです。配列を利用することで、より強力なプログラムを作ることができます。

ポインタへの加算

配列に入る前に、もう一つ、基本事項の紹介です。

ポインタに対して加算を行うとどうなるでしょうか?例を見てみましょう。ポインタ変数barに1を足す前と後の値を表示するプログラムです。

int main(){
    int foo;
    int *bar = &foo;
    printf("%p\n",bar);
    bar++;               /*bar = bar+1と同義*/
    printf("%p\n",bar);
}

実行すると、

00123abc
00123ac0

ポインタの値は4増えています。これは、ポインタへの加算が、元となる型のサイズを基準としているからだと考えてください。int型はメモリの4バイト分の領域を使用するので、int型に対するポインタに対して整数を足すと、その4倍の数値が加算されるのです。

配列の正体

配列とは、メモリ中の連続した領域に配置される変数の群のことです。

int foo[5];

という宣言の場合、メモリ中で配列fooは下図のようになっています。

配列のメモリ中での姿

図に記してありますが、配列の添え字を取ったもの(ここでは「foo」)は、配列の先頭を指すポインタとなっています。

2次元以上の配列は後ほど。

配列の要素へアクセス

さて、もうピンと来た人もいるかもしれませんが、これは、配列の先頭のアドレスが解れば、ポインタに加算を施すことで配列の各要素にアクセスできるということです。例えば、

int foo[5];

と宣言されているときには、

となります(下図も参照)。添え字と足す数が同じなのがうれしいところ。

ポインタと添え字を使った配列へのアクセスのイメージ

また、

int foo[5];
int *bar = foo;    /*配列の先頭アドレスをbarに*/

このようになっているとき、

foo[n] = *(foo+n) = *(bar+n)= bar[n]

が成り立ちます。ポインタは、配列名として扱うことができる。ということです。ポインタによるアクセスより、配列としてアクセスした方がわかりやすいですよね。

※2次元以上の配列になると、少し勝手が違ってきます。

これで、関数をまたいで配列を使用することができるようになります。例を見てみましょう。

#define ARRAY_LENGTH 3

void add_arr(int *arr,int arrlen){    /*arrlenは配列の長さ*/
    for(int i=0;i<arrlen;i++){
        arr[i] += 1;                 /*ポインタを配列として使用*/
    }
}

int main(){
    int array[ARRAY_LENGTH]={
        1,2,3,
    };
    printf("add_arrの前\n");
    for(int i=0;i<ARRAY_LENGTH;i++){      /*arrayの中身を全て表示*/
        printf("array[%d] = %d\n",i,array[i]);
    }

    add_arr(array,ARRAY_LENGTH);    /*配列arrayの先頭ポインタを渡す*/

    printf("add_arrの後\n");
    for(int i=0;i<ARRAY_LENGTH;i++){      /*arrayの中身を全て表示*/
        printf("array[%d] = %d\n",i,array[i]);
    }

    return 0;
}

実行結果は以下の通り。

add_arrの前
array[0] = 1
array[1] = 2
array[2] = 3
add_arrの後
array[0] = 2
array[1] = 3
array[2] = 4

Point!

  • ポインタに1を足すと、元となる型のサイズ分だけ値が増える
  • 配列はメモリ中で連続した領域を占めている
  • 配列の添え字を取ったものは配列の先頭へのポインタ
  • 以上より、 配列名[n] は *(配列名+n)と書ける。
  • 配列の先頭アドレスを持ったポインタは、配列名として扱える。

ポインタ(4) 〜メモリの動的割り当て〜

ここでは先ず「メモリの動的割り当て」と呼ばれる技術を説明して、関数や配列を使った例を紹介します。

変数や配列を宣言してメモリの領域を確保するのは、言わば静的なメモリ割り当てです。ここではプログラム中で好きなときに、好きな分だけ(上限はありますが…)メモリを確保できる方法について説明します。

malloc関数の使い方

mallocという関数について説明します。この関数は、必要なメモリ領域を自動で見つけて確保し、その先頭アドレスを返す関数です。

#include <stdio.h>
#include <stdlib.h>

int main(){
    int *foo;
    foo = (int *)malloc(sizeof(int))
    ........
    printf("%d",*foo);
    ........
    free(foo);
    ........
    return 0;
}

malloc関数を使うにはstdlib.hのインクルードが必要です。

以下、太字部分について説明していきます。

foo = ...

として、返り値(確保した領域の先頭アドレス)をポインタに代入してやります。

(int *)malloc...

C++コンパイラを用いる場合、malloc関数は返す型をこちらで決めてやらなければいけません(これをキャストと言います)。「(int *)」は、そのために記述しており、今回の場合「intに対するポインタを返す」という意味です。

Cコンパイラを使う場合には、キャストは行わない方が良いようそうです(cf:malloc -Wikipedia)が、開発環境が変わって、C++コンパイラを使うことになった場合はエラーが出るようなので注意です。

malloc(sizeof(int))

sizeof関数は、その型に必要なメモリの容量を返します。mallocの引数は、確保したいメモリサイズであり、この場合「int型の大きさ分の領域」を確保します。

free(foo);

free関数はmalloc関数などで確保した領域を開放する関数で、引数はポインタです。通常、プログラム終了時に自動的に開放されますが、異常終了やメモリの容量なども考えて、使わない領域は開放してやるといいでしょう。

※通常の変数などにfreeを使ってはいけません。エラーが発生します。

さて、これでポインタfooはint型変数を格納できる領域を指すようになりました。つまり、先ほどまでと同じようにfooを使って値を読み書きできるということです。

注意!

メモリの確保は時々失敗し、そのときmalloc関数はNULL(無効)を返します。そして、このままプログラムを実行させ続けると多くの場合エラーが起きてしまいます。このエラーはパソコン自体に悪影響を及ぼしかねないので、普通はチェックを行って、強制終了させます。

int *foo;
foo = (int *)malloc(sizeof(int))
if(!foo){
    printf("エラー:メモリの割り当てに失敗しました。");
    exit(-1);
}

fooがNULL(=0)のときに強制終了させています。exit関数は、プログラムを終了させる関数です。異常終了の場合、引数に1以外の値を指定します。(私は-1が好きなだけです)

配列の大きさを指定する

皆さんはユーザーに配列の長さを入力してもらうために下のようなコードを書いてエラーを起こしたことはありませんか?

int len;        //配列の長さ
printf("配列の長さを入力してください:");
scanf("%d",&len);

int arr[len];
...

配列の宣言をするときにその長さを変数にしています。長さは静的でないといけないので、エラーになってしまう訳ですね。

ですが、このままでは配列の大きさをユーザーが決定することは出来ません。ではどうすればよいのかと言うと、メモリの動的確保を応用するわけです。

上のコードを少し書き換えます。(太字部分が変更部分です)

int len;        //配列の長さ
int *arr;      //配列の先頭を指すポインタ
printf("配列の長さを入力してください:");
scanf("%d",&len);

arr = (int *)malloc((sizeof(int)) * len)
...

これで、長さがint型の変数「len」個分の連続領域つまりサイズがlenの配列を確保することができました。arrはそれらの領域の先頭ポインタですから、前項と同じように配列arrとして扱うことができるわけです。

Point!

  • malloc関数は、指定したサイズ分の連続したメモリ領域を確保する関数
  • stdlib.hが必要
  • プログラムの途中で領域を使い終わったらfreeで開放しておく
  • ブログラムのクラッシュを防ぐために、エラーチェックをしておく
  • 「ポインタ = (型 *)malloc(sizeof(型)*サイズ)」という形で配列を確保できる

例題1

では、ちょっとしたプログラムを組んでみましょう。全く実用的なプログラムではありませんが、仕様は、

としましょう。以下に実行結果の例を示します。

nの値を入力してください(半角自然数):10
stepの値を入力してください(半角整数):2
0,2,4,6,8,10,12,14,16,18
合計:90

別ページで詳しく解答をしています。解らなければ見てみてください。

解答ページへ

補足 〜2次元配列〜

ここからの内容は難解な部分が多いので、ザックリ説明していきます。余裕のある方だけどうぞ。

配列が多次元になると、扱いが一気に変わってきますので、補足しておきます。なお、3次元以上は容易に考えが拡張できるので、簡単のため2次元配列のみを扱います。(数学の教科書風に逃

メモリ中での姿

いきなりですが、図を見てもらいましょう。

int foo[3][2];

と宣言されているとすると、メモリ中では

2次元配列のメモリ中での姿

となっています。一応、このように規則正しく並んでいますが、ポインタでアクセスしようとすると、なかなか上手く行きません。

上の2次元配列は、foo[0]、foo[1]、foo[2]という長さ2の1次元配列3つからできています。そして、「foo」は配列foo(1次元ポインタ配列)の先頭つまり、&foo[0]を、またfoo+1は&foo[1]を…指しています。因みに、foo[0]は&foo[0][0]を、foo[0]+1は&foo[0][1]を…指しています。つまり、

2次元配列名とポインタ

※この図は厳密には正しくありません。興味のある人は&foo,&foo[0],&foo[0][0]辺りを調べてみるといいと思います。

このようなイメージなので、ポインタを使うと…

などという、非常にわかりにくいものとなってしまいます。

2次元配列と関数(1) 〜失敗例〜

2次元配列は、マップ(表)として使われることが多く、関数を介して受け渡しできると非常に便利です。しかし、1次元のときと違い、単にポインタを渡すだけでは上手くいかないことが多いのです。例えば次のような場合には上手く動きません。

void hoge(int *foo){   /* 添え字の合計を代入。例:foo[1][2]=(1+2=)3 */
    int i=0,j=0;
    for (i=0;i<3;i++){
        for(j=0;j<4;j++){
            foo[i][j] = i+j;
        }
    }
}

int main(){
    int foo[3][4];
    hoge(foo);
    ...
    return 0;
}

原因は、「hogeから見るとfooがどんな構造の配列なのかわからないから」です。見ると、確かにhogeにおけるfooは、単なるint型のポインタでしかありません。

これを解決するにはいくつか手段があります。

2次元配列と関数(2) 〜静的な処理〜

1つは、「この配列がどこで区切れるか」をhogeに教えることです。hogeの宣言を以下のようにします。

void hoge(int foo[][4])

前の括弧にも数値を入れて構いませんが、こちらの方が柔軟性があります。これで、fooが、「●●×4の配列」だとhogeに伝わるので、正しく動くようになります。

2次元配列と関数(3) 〜ポインタ〜

さて、次の方法です。今度は、引数として配列のサイズをhogeに与えてやります。

void hoge(int *foo,int m,int n){   /*配列はm×n*/
    int i=0,j=0;
    for (i=0;i<m;i++){
        for(j=0;j<n;j++){
            *(foo + n*i + j) = i+j;
        }
    }
}

int main(){
    int foo[3][4];
    hoge(&foo[0][0],3,4);    /*&foo[0][0]は「foo」でも大丈夫。*/
    ...
    return 0;
}

アクセスにはポインタを用いています。hogeからでは、fooはただのポインタにしか見えないので1次元配列のときと同じようにようにアクセスできるのです。この方法だと、配列のサイズが動的な場合にも対処できます。

しかしこの場合、"配列名[添え字][添え字]"とすることができないため、ソースが大変見づらくなってしまいます。

2次元配列と関数(4) 〜ポインタのポインタ〜

そこで、次のような方法を考えてみます。

void hoge(int **foo,int m,int n){   /*配列はm×n*/
    int i=0,j=0;
    for (i=0;i<n;i++){
        for(j=0;j<m;j++){
            foo[i][j] = i+j;
        }
    }
}

int main(){
    int *foo[3];    /*ポインタ配列の宣言*/
    int i;
    for(i=0;i<3;i++){ /*fooの各要素に配列の先頭アドレスを代入。*/
        if(!(foo[i] = (int *)malloc(sizeof(int)*4))){
            printf("error:割り当てに失敗しました");
            exit(-1);
        }
    }
    
    hoge(foo,3,4);
    ...
    return 0;
}

長さ3のポインタ配列fooを確保。さらに長さ4のint型配列の先頭アドレスをfooの各要素に代入して、2次元配列を実装しています

強調してある部分ですが、fooは、ポインタのポインタと呼ばれるものです。fooが「ポインタ変数のアドレス」を指す場合に使います。先の図を見てもわかりますね。main関数のfooも、明示はされていませんが同様です。

因みにこの場合、通常の2次元配列のように全ての要素が連続した領域に配置されるわけではありません。例えばm×n配列であれば、長さnの配列をm個ばらばらに確保します。

Point!

  • m×nの2次元配列は、長さnの1次元配列m個からできている
  • 2次元配列の要素の後ろの添え字を取ったものは、その1次元配列へのポインタ
  • 関数に2次元配列を渡すときは、なんらかの方法で関数にその構造を伝えなければならない

補足 〜文字列とポインタ〜

C言語では文字列は配列として実装されています。が、実は文字列を保持する変数には2種類あるのです

文字配列

一つは、文字列を、ごく普通に配列として確保する方法です。なじみが深いのはこちらの方法ではないでしょうか?

char str[] = "hoge";

宣言は上のような形です。

しかし、実はメモリでは意外な操作が行われています。下の図を見てみましょう。

文字配列のメモリ中での姿

プログラム中で文字列は一旦文字列専用の領域に書き込まれます。文字配列は、文字列を専用の領域からコピーすることで実装されているのです。

文字ポインタ

さて、こちらは先ほどとは少し様子が違って、文字列の先頭へのポインタを得ることで文字列を保持します。文字ポインタの宣言は次のようにします。

char *str = "hoge";

こちらも図を見てみましょう。

文字ポインタのメモリ中での姿

こちらは、文字列をコピーすることはなく、専用の領域にあるその文字列の先頭のポインタを直接得ています。

これら二つの方法は、普段はそこまで大きく違わないのですが、意識しておかないとプログラムが正常に動作してくれない場合があります。

特にソートなど、文字列をさらに配列にするときは特に注意しましょう。

Point!

  • 配列による文字列は、別領域に確保されている文字列を配列の領域にコピーして得られている
  • 対して文字ポインタは、文字列が確保されている領域を直接指すポインタである。

隠蔽されてしまったポインタ達の事、時々d(ry

inserted by FC2 system