ポインタ変数2

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

目次

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

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;
}

練習問題

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

細かな注意点

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

文字・記号の限られたソースコードにはよくあるが,C言語の文法では*記号は

  1. ポインタ変数の定義
  2. ポインタの指す値を取り出すポインタ演算子
  3. 乗算の演算子

のように,同じ記号が異なる意味で使用される
したがって,その意味は文脈で判断する必要がある.

int n = 10;

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

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

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

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

以下のような出力になるはずである.

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
ポインタ配列の各要素が,文字列の先頭を指している.

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

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

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

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

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

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

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

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

#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;
}