Lecture 12 ポインタ(3)〜復習

アスタリスク(*)とアンパサンド(&)

int a = 0;	// これは普通のint型の変数.初期化を忘れないように
int* p = &a;	// これはaのアドレスを指し示すポインタ変数 p の宣言.
		// aのアドレスで初期化している.&は参照(アドレス)を示す.
既に解説している通り,Cには,アスタリスク(*)とアンパサンド(&)という2つのポインタ演算子がある.

  • &演算子は,その後に続く変数のアドレスを返す.
  • *演算子は,後に続く「アドレス(場所)に格納された値」を返す.

【ヒント】ポインタ演算子は,乗算の演算子と同じアスタリスクを使うが,意味が全く異なる.混同しないように.

ポインタの利用例:
#include <stdio.h>
int main(void){
    int a = 123;
    int* p = &a;           // ポインタの宣言と初期化(代入).
    p = &a;                // ポインタの代入.上の代入と同じ内容.
    printf("a = %d\n", *p);// ポインタが示す先に格納された「値」を表示
}

仮引数としてのポインタ

文字列を引数として関数に渡すときは,文字列全体のコピーを作るのではなく, その先頭アドレスが渡される.これをポインタで受けると, アドレスのスキャン(順次変更していくこと)ができる.
以下のプログラムの実行結果はどうなるか,確かめよ.
//
// 映画「2001年宇宙の旅」より
// (劇中に登場する,反乱する人工知能HAL9000は誰が作ったのか?
//  の謎を解くプログラム)
//
void who_made(const char *s){  // 先頭アドレスをポインタで受ける
    while(*s)
        printf("%c",(*s++)+1);  // アドレスをインクリメント
    printf("\n");
}

int main(void){
    who_made("HAL");  // HALを作ったのは誰?
}

ポインタ変数同士の算術演算

ポインタが指し示す変数同士の演算ももちろん可能である.ただし,アスタリスクがつくため,少々見にくい.
    float* a = ...;
    float* b = ...;
    float x = *a * *b;  // floatの値同士の掛け算.星,星,星・・・!
かなり見苦しいので,カッコでくくって,
    float x = (*a) * (*b);  // floatの値同士の掛け算.
などとすると良い.

ポインタのアドレス計算

ポインタが優れていることは,ポインタのアドレス計算が,型の大きさに応じて自動的になされることである.
    short int si[100];
    short int* sip = si;
    *sip++;	// この結果は,先頭より2バイト先,si[1]を示す.
これによって,以下のようなことも可能である.
    char c[sizeof(long)*100];	// long 100個分の領域を確保
    long* x = (long*)c;	// char 配列の先頭にlongポインタをセット
    for(int i=0; i<100; i++)
        *x++ = i;	// 0から99まで値を代入.格納されるのはchar配列の中
この時,値が実際に格納されるのは,はじめに宣言した char配列の中だが, long型のポインタでアクセスしているので,アドレスは4バイト毎にインクリメントされ, long型としての整合性を保ち続ける.

ポインタ式の制限

ポインタの値そのものはアドレスである.ポインタには加減算を施すことができる.この場合,変更されるのは「アドレス」である.
    char s[] = "Meiji University";
    char* p = s; // 文字列の先頭アドレスを示すように初期化
    p += 3;      // この演算の後,*p は文字'j'を示す

【ヒント】[ ]の無い配列名は,配列の先頭アドレスを示す.&s[0]と同じ.
【ヒント】ポインタの演算は,整数の加減算以外は実行できない.

では,ポインタが示すアドレスに格納された値を変更するにはどうすればいいだろうか?
    char s[] = "Meiji University";
    char* p = s;    // 文字列の先頭アドレスに設定
    (*p) += 3;      // 文字'M'が'P'に変更される.
    *p += 3;        // *p は文字'j'を示す!!!
数学で,加減算よりも乗除算が先に実行されるように,C言語の演算子にも演算の順序がある. ポインタ演算子は,加減算よりも後に実行されるため,上記の最後の行のような結果になることに注意.

配列とポインタ,文字列定数とポインタ

既に,上記の文字配列をポインタで示した通り,配列のポインタとは親和性が高く, 配列の要素にポインタでアクセスすることができる.
その一方で,ポインタに配列演算子をつけて,配列と同じに使うこともできる.
    char* p = "Meiji University";
    p[3];    // ポインタに配列の添字をつけることができる.
ただし,配列名をポインタの様にアドレス変更することは出来ない.
    char s[] = "Meiji University";
    s++;    // エラー!! sは配列の先頭ラベルで,アドレスを変更することは出来ない.
    char* p = s;
    p++;    // もちろん,これは可能.

ポインタなら,アドレスを変更することができるため,この機能を使って以下のようなプログラムを作ることができる.
    #include <stdio.h>
    int main(void){
        char* p = "Meiji University";
        while(*p)
            printf("%c", *p++);
    }
最初,pは文字列定数の先頭'M'の位置を示している.printfによって,一文字ずつ表示しながら,アドレスをインクリメントすることによって,文字列中の文字を次々に表示していく. 文字列定数の最後は必ず「ヌル文字」であるため,while(*p) は false となってループが終わる. このように,ポインタを用いれば,文字をスキャンする時にループカウンタは必要ない.

ポインタ配列

ポインタも他の変数と同様に配列を作ることができる.データベースなどを作るときに有効. 以下は名簿へのアクセス例.
    char* p[] = {
        "きかい はなこ",
        "めいじ たろう",
        "りこう いちろう",
        ""
    };
    
    void meibo_print(int shusseki_banngou){
    	printf("%s", p[shusseki_bangou]);
    }


Quiz 12

以下の問それぞれに対応するプログラムを作成しなさい.

    1. 以下のプログラムを実行し,以下を確かめてみよう.
        1. ポインタがどのように変化するか?
        2. 値をインクリメントするにはどうするか?
        3. 変数の型が変わるとどうなるか?
      #include 
      
      int main(void)
      {
          int a[] = {10, 20, 30};    // 配列の宣言と初期化
      
          int *p = a;          // 配列の先頭アドレスを示すようにポインタを初期化
                               // p = &a[0]; と等価.
      
          printf("Address of array : %p %p %p \n", &a[0], &a[1], &a[2]);
      
          printf("initial   : p = %p, *p = %d \n", p, *p);        // p と p の指す相手の値(*p)を表示
      
          p++;       // p に 1 を加える?
          printf("p++       : p = %p, *p = %d \n", p, *p);
      
          p++;
          printf("p++ again : p = %p, *p = %d \n", p, *p);
      
          return 0;
      }
      
      ソースコード提供:川口達也先生


    2. ポインタの配列
      以下のコードのように,複数の文字列(リスト)を扱う時によく用いられます. ポインタが指した先が文字列(=1次元配列)になっているため,二次元配列的ですが, 各文字列の長さは異なっていても良いので,きれいな長方形の行列になるわけではありません. あくまで,「ポインタの一次元配列」なのです.
      #include 
      
      int main(void)
      {
          char* p[10];   // 文字型を指すポインタ変数を10個作る
      
          p[0] = "Hello";    // p[0] はポインタ!
          p[1] = "World!";
      
          printf("%s %s\n", p[0], p[1]);
          return 0;
      }
      
        1. 上のソースコードを改造して,5個の文字列を入力,格納し,任意の番号の文字列を出力するプログラムを作ってみよう.



    3. 2つの文字列を連結して一つの文字列にする関数
      char* my_strcat(char* s1, const char* s2)
      を作成し,キーボードから入力した任意の2つの文字列を連結して表示するプログラムを作成せよ. 但し,当該関数は,文字列s1の最後に文字列s2を連結し, その結果の文字列へのアドレスを返すこととする.
      必要に応じて,strlen()関数も作成してみよ.
      また,以下のmain関数を利用せよ.
      int main(void)
      {
          printf("Type string #1: ");
          char s1[100];
          scanf("%[^\n]%*c",s1);	// 改行コード以外をs1へ,
                                	// 最後の改行コードは読み飛ばす
      
          printf("Type string #2: ");
          char s2[100];
          scanf("%[^\n]",s2);		// 改行コード以外をs2へ.
      
          printf("result: %s\n", my_strcat(s1,s2));
      }
      

      解答例 Q12-3


      【ヒント】文字列の最後尾に別の文字列を追加するために,文字列の長さを返す関数 strlen() を使っていますが, ここでは strlen() も自作してみました.



Assignment(課題)12

課題の提出は Online Judge にて行います.

  • 提出期限があります.できるだけ早めにやりましょう.
  • Online Judge は普通の課題提出方法とは異なり,良い点が取れるまで何度でも挑戦できます.良い点が取れるまで頑張ってやろう.
  • わからないことは,早めに Teams で聞こう.問題は自分で解決すること.
  • それでもどうしてもわからないときは,毎週月曜日の2時限目にZoomによるオフィスアワーを用意しています. ZoomのリンクはOh-o! meijiで確認してください.
  • Online Judge のページはこちら