ポインタ変数3

今回はポインタ変数の理解を深めるための復習と,関連するやや高度な話題について解説する.
ポインタ変数やアドレスの概念はすでに述べたとおりであるが,実際の場面でどのように役立てるかを中心にソースコードを読んでみよう.

ファイルの復習

500名分の試験の点数が書かれているファイル scores.txtをプログラムから読み込んで,

  1. 平均点(小数点第1位まで表示)
  2. 80点以上,90点未満( = 判定が A )の人数
  3. 60点未満( = 判定が F )の人数

を,画面に出力するプログラムを作成してみよう.
それぞれの処理は,(配列とその要素数を引数とする)関数にするとよい.
エラー処理などを適切に行うこと.

実行例(出力の値はこの通りになるとは限らない):
a.exe
Average = 51.7
Count of A is 130
Count of F is 100

目次

ポインタ演算子 * と,配列演算子[]

C言語の文法において「ポインタ」と「配列」は,異なる概念であるが,いずれも「アドレス」であるので,その使い方は非常によく似ている.

下記のように,配列を指すポインタに,配列演算子[ ] を使用することができる.
逆に,配列に整数を加算してアドレスの計算を行うこともできる.
この様に,ポインタと配列は相互に互換性があるような使い方が可能である.

違いは,配列名にポインタのように++,--して,そのアドレスを変更することはできない.
以下のソースコードで確かめてみよう.

#include <stdio.h>

int main(void)
{
    // Array
    char s[] = "Meiji University";

    s[0] = 'A';   // OK
    s++;          // NG! sは配列の先頭アドレスなので,変更不可

    // Pointer
    char *p = s;
    
    p[0] = 'A';   // OK
    p++;          // OK

    return 0;
}

コンパイル結果:
test.cpp:7:6: error: lvalue required as increment operand
    s++;

これには例外があり,以下のように,配列が関数の仮引数である場合は,ポインタと同じようにアドレスの増減ができてしまう.
つまり,関数の仮引数として配列を渡す場合,配列としても,ポインタ変数としても,区別なく受けとることができる

#include <stdio.h>

void func(char s[])    // s は関数の仮引数
{
    s[0] = 'A';   // OK
    s++;          // OK !!!

    char *p = s;
    p[0] = 'A';   // OK
    p++;          // OK
}

int main(void)
{
    char s[] = "Meiji University";
    func(s);
    return 0;
}

練習問題

上記プログラムをコンパイルしてみよう.仮引数としてのポインタと配列の違い(違わない?)を確認しよう.

#include <stdio.h>

void f1(int a[], int N)     // 配列で受ける
{
    for(int i=0; i<N; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");
}

void f2(int *p, int N)     // ポインタで受ける
{
    for(int i=0; i<N; i++) {
        printf("%d ", *(p+i));
    }
    printf("\n");
}

int main(void)
{
    const int N = 5;
    int a[N] = {10,20,30,40,50};
    f1(a,N);
    f2(a,N);
    return 0;
}

練習問題

上記のプログラムを実行してみよう.何が表示されるか?
次に,関数f1() のprintf内のa[i]の箇所を,ポインタ風に*(a+i)と書いてみよう.うまく動作するだろうか.

アドレス渡しの配列の変更を禁止する方法

関数に配列を渡すとアドレス渡しとなる,配列の中身を自由に変更できてしまう.
これを禁止したい場合は,仮引数に const をつけるとよい.

void f1(const int a[], int N)     // 変更を禁止する場合は,配列にconst をつける.
{
    for(int i=0; i<N; i++) {

        a[i] = 0;      // これはエラー.const で変更を禁止できる.

        printf("%d ", a[i]);
    }
}

void f2(const int *p, int N)     // ポインタにも const をつけることができる
{
    for(int i=0; i<N; i++) {
        *(p+i) = 0;      // const で変更を禁止できる.
    }
}

以下は文字列を関数に渡す方法である. 実行結果はどうなるか,確かめよ.
(文字列の場合はヌル文字 \0 で終端される約束であるため,先頭アドレスのみでOK.)

#include <stdio.h>

void who_made(const char *s)
{
    while(*s)
        printf("%c",(*s++)+1);  // アドレスをインクリメントしつつ,文字コードに1を加えて表示
    printf("\n");
}


int main(void)
{
    char str[] = "HAL";    // 人工知能を備えたコンピュータの名前
    who_made(str);         // HALを作ったのは誰?
    return 0;
}

練習問題

上記のプログラムを実行してみよう.何が表示されるか?(いにしえの人工知能コンピュータの名前です.)
*s++ は,どのような処理になるか分かりますか?(演算子の優先順位を調べよう)

このような暗号化をシーザー暗号と呼ばれる.

細かな注意点

* 演算子の使い分けに注意

文字・記号の限られたソースコードにはよくあるが,C言語の文法で「ポインタ変数の定義」,「ポインタの指す値を取り出す * 演算子」,および「乗算の演算子」と同じ記号を異なる意味で使用している.
文脈で判断する必要がある.

int n = 10;

int *p = &n;     // 1.「pは,ポインタ変数です」という定義

*p += 20;        // 2.ポインタ p の指す別の変数(ここではn)の値を取り出す「ポインタ演算子」

n = n * 2;       // 3.「掛け算」の記号

ポインタ演算子の優先順位

ポインタ変数にその都度,* がつくため,慣れないと判読が少々面倒である.

float *a = ...;
float *b = ...;

float x = *a + *b;   // floatの値同士の加算
float x = *a * *b;   // floatの値同士の乗算.*が続く・・・!

特に,乗算の場合は,ポインタ演算子と乗算が同じ記号であるため,やや読みにくい.
このような場合は,明示的に ( ) で括って,

float x = (*a) + (*b);
float x = (*a) * (*b);

などとするとわかりやすい.

C言語の各種演算子には優先順位が決められている.
そのなかで,ポインタ演算子 * は順位がかなり高いため,この例では()は不要である.
しかし,演算子の優先順位を明示的するために ( ) を適宜使用すると良い.

ポインタ配列

整数型変数の配列=整数配列,実数型変数の配列=実数配列と同様に,ポインタの配列=ポインタ配列を作ることができる.
これは,配列の各要素が,ポインタ変数であるものをいう.
「配列を指すポインタ」とは異なるので注意.

array of pointer
ポインタ配列.配列の各要素がポインタ変数であり,配列の各要素がそれぞれ別の変数や配列を指している.

#include <stdio.h>

int main(void)
{
    char* p[3];   // 文字型を指すポインタ変数を3個作る

    char h[] = "Hello";
    char w[] = " World";
    char e[] = "!.";

    // 2つの文字列 h,w の先頭アドレスを,ポインタ配列pに代入
    // アドレスを代入しているだけなので,strcpy() は不要.
    p[0] = h;    // p[0] はポインタ p[0]=&h[0]; でもOK
    p[1] = w;    // p[1] もポインタ
    p[2] = e;    // p[2] もポインタ

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

    return 0;
}

main()関数の引数

main()関数の引数は void でも良いが,正式には int main(int argc, char* argv[])と書く.
これは,コマンドラインから実行ファイル (a.exe) を実行させたとき,続く文字列をプログラムに渡す際に用いられ,非常に重要な機能である.
例えば,ソースファイルをコンパイルする際に,c++ test.cpp のように,引数としてファイル名を渡すあのやり方です.

main() の最初の引数 argc は引数の数,次の argv はその引数(文字列)を指すポインタの配列である.

#include <stdio.h>

int main(int argc, char* argv[])
// argc : 引数の個数
// argv : 引数(文字列)の先頭を指すポインタ配列
{
    printf("argc = %d\n", argc);

    int i;
    for(i=0; i<argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);    // argv[i] は,文字列の先頭を指すポインタ
    }
    return 0;
}

練習問題

上記のプログラムを実行する際,実行ファイル名に続いて以下のようにスペース区切りで単語を入力してみよう.

a.exe aaa bbb 123 456     (Windows)
./a.out aaa bbb 123 456     (Mac OS, UNIX)

以下のような出力になるはずである.
argcには引数の個数,argv[]には文字列を指すポインタが格納される.
argv[0]については,常に実行ファイルの名前が格納される.

argc = 5
argv[0] = a.exe
argv[1] = aaa
argv[2] = bbb
argv[3] = 123
argv[4] = 456

注:argv[]は,あくまで文字列であるので,数値として扱いたい場合は,atoi(), atof()などの文字列→数値変換ライブラリ関数を使用する必要がある.
例えば,argv[1]を整数に変換する場合は,int n = atoi(argv[1]);とする.

argv
ポインタ配列の各要素が,文字列の先頭を指している.

例えば,コマンドラインから処理をするファイル名を指定したければ,

#include <stdio.h>

int main(int argc, char* argv[])
{
    FILE* fp = fopen(argv[1], "r");
    ...

    return 0;
}

のように,argv[1]fopen()に渡せばよい.

こうしておけば,同じプログラムでいろいろなデータファイルを処理したい場合にも,いちいちソースファイル中に書かれたファイル名の文字列を書き直して,コンパイルし直さなくて良いので便利である.
(ソースコード中にファイル名や数値などを直接書き込んでしまうことを「ハードコーディング」と呼び,一般的にはあまりよくない方法とされる.)

ポインタへのポインタ(やや高度)

ポインタ変数も普通の変数と同様,メモリー上に記憶領域が確保されるので,その場所にはアドレスがあるはずである.
つまり,ポインタ変数の置かれているアドレスを指すポインタ変数,即ち,「ポインタへのポインタ」も定義可能である.

ptr of ptr
ポインタ変数を指す,ポインタ変数

この授業ではあまり使う機会はないが,ポインタのポインタは,「ポインタ変数」をアドレス渡しで関数に渡する場合に用いられる.
つまり,関数に渡した引数に,アドレス値を代入してもらいたい場合がこれにあたる.

さらに,(原理的には)ポインタのポインタのポインタや,ポインタのポインタのポインタのポインタ...なども定義できるが,あまり使われない.

// ポインタのポインタを使用する例 1:
#include <stdio.h>

int main(void)
{
    int i = 100;
    int *p = &i;         // p  は i のアドレス
    int **pp = &p;    // pp は p のアドレス
    
    printf("i    = %d, &i = %p \n", i, &i);

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

    printf("**pp = %d, *pp = %p, pp = %p \n", **pp, *pp, pp);
    
    return 0;
}

実行結果:(アドレスの数値は異なる場合がある)
i    = 100, &i = 000000000061FE14 
*p   = 100, p = 000000000061FE14
**pp = 100, *pp = 000000000061FE14, pp = 000000000061FE08
// ポインタのポインタを使用する例2:
#include <stdio.h>

void open_file_and_check(char fname[], FILE **fpp)   // fppはファイルポインタを指すポインタ!
{
    *fpp = fopen(fname, "r");    // fpp はポインタのポインタ,*fppはポインタ
    if( *fpp == NULL ) {
        // ファイルが開けなかった場合の error 処理
    }
	...
}

int main(void)
{
    FILE *p;
    open_file_and_check("sample.txt", &p);   // ポインタ変数のアドレスを渡す

    ...
    return 0;
}

関数へのポインタ(やや高度)

ポインタが指すものは,普通の変数,配列以外にも,関数・実行コードなどメモリ上に配置できるありとあらゆる物を指しうる.

したがって,データである「変数」ではなく,実行コード(機械語)である「関数」を指すポインタ変数も定義可能である.
これは「関数ポインタ」とも呼ばれる.

下記のように,「関数名」はその関数のアドレスを格納しており,その場所を先頭に実行コード(機械語)が書かれていると解釈される.
したがって,関数名に()をつけることにより,そのアドレスにある実行コードを処理せよと,いう意味になる.
(同様に,配列名+[]をつけることにより,データを取り出せる事に似ている.)

なお,ここでは説明しないが「関数ポインタの配列」も作ることができる.
これは,ソーティングなど同じデータに対して異なるアルゴリズムの関数を実行時に切り替えるなどの処理に重宝する. 興味があれば調べてみよう.

#include <stdio.h>

void SayHello(void)
{
    printf("Hello!\n");
    return;
}

void SayGoodBye(void)
{
    printf("GoodBye!\n");
    return;
}

int main(void)
{
    printf("*** call normal functions ***\n");
    SayHello();                  // 普通に関数の呼び出し
    SayGoodBye();                // 普通に関数の呼び出し

    printf("\naddress of SayHello = %p , SayGoodBye = %p\n\n", SayHello, SayGoodBye );  // 関数名に() がつけられていないことに注目!

    void (*funcp) (void);        // 関数ポインタの定義(難).
                                 // funcpは「引数void, 戻り値void」なる関数へのポインタ

    printf("*** call by function pointers ***\n");
    funcp = SayHello;            // ポインタの指す相手をSayHello関数に設定
    (*funcp)();                  // 関数ポインタを使った関数の呼び出し

    funcp = SayGoodBye;          // ポインタの指す相手をSayGoodBye関数に設定
    (*funcp)();                  // 関数ポインタを使った関数の呼び出し

    return 0;
}

練習問題

上記プログラムを実行して,関数をポインタ経由で呼び出してみよう.
関数ポインタの定義方法や,呼び出し方に注目.
(かなり癖のある文法)

実行結果:(アドレスの数値は異なる場合がある)
*** call normal functions ***
Hello!         <-ふつうの関数の呼び出し
GoodBye!       <-ふつうの関数の呼び出し

address = 0000000000401550 , 000000000040156B

*** call by function pointers ***
Hello!         <-関数ポインタを使った関数の呼び出し
GoodBye!       <-関数ポインタを使った関数の呼び出し

練習問題(授業時間内に実習)

  1. main()関数の引数 argvを利用して,コマンドラインからテキストファイルのファイル名を指定して,その内容を画面に表示してみよう.
    例えば,いま作成しているソースファイル名を指定すると,そのファイルの内容を表示する.
    (全角文字を含まないテキストファイルを指定する.)

    ヒント:ファイルのオープンや読み込みは,ファイルの回を参照.
    先頭に行番号をつけると見やすくなるかもしれない.

    a.exe test.cpp    (Windowsの場合)
    ./a.out test.cpp      (Mac OS の場合)
    
    実行結果:
    #include <stdio.h>
    int main(int argc, char* argv[])
    {
    ...
    }
    
    実行結果(4桁の行番号付き):
    0000 #include <stdio.h>
    0001 int main(int argc, char* argv[])
    0002 {
             ...
    0099 }

  2. 8桁の2進数を,10進数に変換してみよう.
    2進数は8文字の文字列で与えられるものとし,以下のような変換する関数を作成しよう.
    (数値の後につく()内の数値は,何進数かを表す.)

    ヒント:
    00000001(2) = 1(10)
    00000010(2) = 2(10)
    00000100(2) = 4(10)
    00001000(2) = 8(10)
    00010000(2) = 16(10)
    00100000(2) = 32(10)
    01000000(2) = 64(10)
    10000000(2) = 128(10)

    #include <stdio.h>
    
    int binary2dec(char str[])   // 引数は char *p でもOK
    {
    	...
    }
    
    int main(void)
    {
        char data[9] = "11011011";   // 8文字+ヌル文字=9個.値を変えても正しく動作すること.
        printf("%s(2) = %d(10)\n", data, binary2dec(data));
        return 0;
    }

  3. 要素数N個の整数配列の,ある一部分を並べ替える「部分ソーティング」を行う関数 partial_sort_bubb()を作成しよう.
    並べ替えは昇順(=小さい順)として,添え字の範囲はキーボードから入力すること.

    ソーティングにはいろいろな方法がある. ここではバブルソートを実装してみよう.
    バブルソートは,先頭から末尾まで順に常に隣同士を比較して,所望の大小関係(ここでは小→大)となるよう入れ替える. この繰り返しを,入れ替え操作が不要になるまで繰り返す.

    バブルソートのアルゴリズム:

    1. 先頭の値と,隣の値とを比較し,大→小の順になっていたら入れ替え(swap)て,小→大の順にする.
    2. 同様に,次の値と,その次の値を比較し,大→小の順になっていたら入れ替える(swap).
    3. これを終端まで繰り返す.
    4. 上記1-3の操作(先頭から終端までを一巡とする)を,入れ替えが起こらなくなる(即ち,swapの回数==0 回)まで繰り返す.

    可能であれば,配列の範囲指定を(scanf() ではなく)コマンドラインから直接引数として与えるようにしてみよう.
    また,入力された整数の範囲が,配列の添え字の範囲を超えていないかどうかチェックすること.もちろん大小関係も要チェック.

    #include <stdio.h>
    
    void swap(...)
    {
        // おなじみの入れ替え関数
    }
    
    void partial_sort_bubb(int *p_beg, int *p_end)
    // 並べ替えを行う部分配列の先頭・終端アドレスを受け取る
    {
        int swapcount; // 1巡するうちに,入れ替えが発生した回数
    
        do {
    
            swapcount=???;   //  毎ループごとに初期化
            
            // 先頭から順に隣接値を比較
            // 大小関係を比較して条件によってswap
            int *p;
            for( p = p_beg ; ??? ; p++) {    // ポインタ変数でループ?
    
                if(???) {
                    ...
                    swapcount++;  // 入れ替え発生! カウンタをインクリメント
                }
            }
    
        } while(???);  // 入替が発生しているうちは繰り返す.
    
    }
    
    int main(int argc, char* argv[])
    {
        const int N = 20;
        int data[N] = {3, 9, 8, 4, 5, 1, 10, 3, 4, 7, 2, 8, 4, 5, 8, 2, 7, 4, 9, 0};
    
        // 並べ替え範囲を入力
        // できれば argv[1], argv[2] を数値に変換して使用する
        int beg, end;
        scanf(...);  
    
        // 入力されたbeg, end が,配列の範囲を超えていないかチェック.
        if(???) {
            printf("Out of range in input number(s)\n");
            return 0;
        }
    
        // 入力されたbeg, end の順序をチェック
        if(???) {
            printf("Wrong order of input numbers\n");
            return 0;
        }
    
        int i;
        printf("before:");
        for(i=0; i<N; i++) {
            printf("%d ", data[i]);
        }
        printf("\n");
    
        partial_sort_bubb(data+beg, data+end);      // 並べ替え区間の始めと終わりを,アドレスで渡す
    
        printf("after :");
        for(i=0; i<N; i++) {
            printf("%d ", data[i]);
        }
        printf("\n");
    
        return 0;
    }
    
    実行例 1 : scanf()で範囲を入力
    
    5 10     <-添え字の範囲を入力
    before:3 9 8 4 5 1 10 3 4 7 2 8 4 5 8 2 7 4 9 0
    after :3 9 8 4 5 1 2 3 4 7 10 8 4 5 8 2 7 4 9 0
    
    実行例 2 : a.exeに続いて範囲を入力.
    
    a.exe 5 10     <- a.exeに続いて添え字の範囲を入力
    before:3 9 8 4 5 1 10 3 4 7 2 8 4 5 8 2 7 4 9 0
    after :3 9 8 4 5 1 2 3 4 7 10 8 4 5 8 2 7 4 9 0
    
    実行例 3 : 範囲エラー.
    
    a.exe 5 50     <- a.exeに続いて添え字の範囲を入力
    Out of range in input number(s)