ホーム » スタッフ » 斉藤徹 » 講義録 » 情報構造論 » ポインタ処理

2023年5月
 123456
78910111213
14151617181920
21222324252627
28293031  

検索・リンク

ポインタ処理

ここからは、次のメモリの消費を考慮したプログラムの説明を行うが、データ保存場所としてのポインタを多用するので、ポインタの処理に慣れない人のために説明。

値渡しとポインタ渡し

大きなプログラムを作成する場合、変数名の使い方には注意が必要となる。大域変数は、どこでも利用できるが、間違った使い方をすると値が予想外の変化があったりするため危険である。一方で、局所変数を使うと、関数呼び出しでデータの受け渡しに注意が必要となる。

値渡し(call by value)

// 値渡しのプログラム
void foo( int x ) {  // x は局所変数(仮引数は呼出時に
                     // 対応する実引数で初期化される。
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   int a = 123 ;
   foo( a ) ;  // 124
               // 処理後も main::a は 123 のまま。
   foo( a ) ;  // 124
}

このプログラムでは、aの値は変化せずに、124,124 が表示される。

言い方を変えるなら、呼び出し側main() では、関数の foo() の処理の影響を受けない。このように、関数には仮引数の値を渡すことを、値渡し(call by value)と言う。実引数の値は、仮引数の変数に copy し代入される。

でも、プログラムによっては、124,125 と変化して欲しい場合もある。
どのように記述すべきだろうか?

// 大域変数を使う場合
int x ;
void foo() {
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   x = 123 ;
   foo() ;  // 124
   foo() ;  // 125
}

しかし、このプログラムは大域変数を使うために、間違いを引き起こしやすい。

// 大域変数が原因で予想外の挙動をしめす簡単な例
int i ;
void foo() {
   for( i = 0 ; i < 2 ; i++ )
      printf( "A" ) ;
}
void main() {
   for( i = 0 ; i < 3 ; i++ )  // このプログラムでは、AA AA AA と
      foo() ;                   // 表示されない。
}

ポインタ渡し(call by pointer)

C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、(引数を経由して関数の副作用を受け取るには)、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。このような値の受け渡し方法は、ポインタ渡し(call by pointer)と呼ぶ。

// ポインタ渡しのプログラム
void foo( int* p ) {  // p はポインタ
   (*p)++ ;
   printf( "%d¥n" , *p ) ;
}
void main() {
   int a = 123 ;
   foo( &a ) ;  // 124
                // 処理後 main::a は 124 に増えている。
   foo( &a ) ;  // 124
}               // さらに125と増える。

C言語では、関数から結果をもらうには、通常は関数の返り値を使う。しかし、返り値は1つの値しか受け取ることができないので、上記のようにポインタを使って、呼び出し側は:結果を入れてもらう場所を伝え、関数側は:指定されたアドレスに結果を書き込む。

変数の寿命とスコープ

変数の管理では、変数の寿命スコープの理解が重要。

静的変数:変数は、プログラムの起動時に初期化、プログラムの終了時に廃棄。
動的変数:変数は、関数に入るときに初期化、関数を抜けるときに廃棄。
もしくは、ブロックに入るときに初期化、ブロックを抜けるときに廃棄。

大域変数:大域変数は、プログラム全体で参照できる。
局所変数:関数の中 or そのブロックの中でのみ参照できる。

ブロックの中で変数が宣言されると、そのブロックの外の変数とは別の入れ物となる。そのブロックの中では、新たに宣言された変数が使われる。

int i = 111 ;    // 静的大域変数

void foo() {
   int i = 222 ; // 動的局所変数
   i++ ;
   printf( "%d\n" , i ) ;
}
void bar() {
   static int i = 333 ; // 静的局所変数(プログラム起動時に初期化)
   i++ ;
   printf( "%d\n" , i ) ;
}
void hoge( int x ) {    // x: 動的局所変数(値渡し)
   x++ ;
   printf( "%d\n" , x ) ;
}
void fuga( int* p ) {   // p: 動的局所変数(ポインタ渡し)
   (*p)++ ;
   printf( "%d\n" , (*p) ) ;
}
int main() {
   int i = 444 , j = 555 ;
   foo() ;      // 223 (副作用ナシ)
   bar() ;      // 334
   hoge( i ) ;  // 445 (副作用ナシ)
   fuga( &j ) ; // 556
   printf( "%d\n" , i ) ;
   foo() ;      // 223 (副作用ナシ)
   bar() ;      // 335
   hoge( i ) ;  // 445 (副作用ナシ)
   fuga( &j ) ; // 557

   printf( "%d\n" , i ) ;                     // 444
   for( int i = 0 ; i < 2 ; i++ ) { // (a)    // A:0
      printf( "A:%d\n" , i ) ;                // B:0
      for( int i = 0 ; i < 2 ; i++ ) { // (b) // B:1
         printf( "B:%d\n" , i ) ;             // A:1
      }                                       // B:0
   }                                          // B:1

   printf( "%d\n" , i ) ;  // 333 ← 要注意C言語のバージョンによっては
                           //       2 になる場合あり。(a)の変数iの値
   return 0 ;
}

JavaScriptのvarとletのスコープ

C言語でのスコープと寿命と、JavaScriptのvar宣言では考え方が違うので要注意。
C言語では、変数はその宣言された場所がブロックの中であれば、その中でのみ使用できる局所変数となる。JavaScript の let 宣言も基本は同じ考え方。ただし、JavaScript の var 宣言は、その変数が使われる関数ブロックの中(もしくはグローバルスコープ)に関連付けられる。この変数の宣言部分がもっとも近い関数の先頭に移動しているように見える動作のことを変数の巻き上げ(hoisting)と呼ばれる。

C言語の局所変数のスコープ。

JavaScript の let 宣言は、C言語のスコープと同じ考え方。

JavaScript の var 宣言は、関数スコープであり、ブロック{} 内で新しく宣言があっても、関数スコープまで巻き上げられる。

ポインタの加算と配列アドレス

ポインタに整数値を加えることは、アクセスする場所が、指定された分だけ後ろにずれることを意味する。

// ポインタ加算の例
int a[ 5 ] = { 11 , 22 , 33 , 44 , 55 } ;

void main() {
   int* p ;
                               //            p∇
   p = &a[2] ;                 // a[] : 11,22,33,44,55
                               //       -2    +0 +1
   printf( "%d¥n" , *p ) ;     // 33  p[0]
   printf( "%d¥n" , *(p+1) ) ; // 44  p[1]
   printf( "%d¥n" , *(p-2) ) ; // 11  p[-2]

   p = a ;                  //      p∇
   printf( "%d¥n" , *p ) ;  // a[] : 11,22,33,44,55
   p++ ;                    //       → p∇
   printf( "%d¥n" , *p ) ;  // a[] : 11,22,33,44,55
   p += 2 ;                 //           → → p∇
   printf( "%d¥n" , *p ) ;  // a[] : 11,22,33,44,55
}

ここで、注意すべき点は、ポインタの加算した場所の参照と、配列の参照は同じ意味となる。

*(p + 整数式)p[ 整数式 ] は同じ意味 (参照”悪趣味なプログラム”)

特に配列 a[] の a だけを記述すると、配列の先頭を意味することに注意。

ポインタインクリメントと式

C言語では、ポインタを動かしながら処理を行う場合に以下のようなプログラムもよくでてくる。

// string copy 配列のイメージで記載
void strcpy( char d[] , char s[] ) {
   int i ;
   for( i = 0 ; s[ i ] != '¥0' ; i++ )
      d[ i ] = s[ i ] ;
   d[ i ] = '¥0' ;
}

int main() {
   char a[] = "abcde" ;
   char b[ 10 ] ;
   strcpy( b , a ) ;
   printf( "%s¥n" , b ) ;
   return 0 ;
}

しかし、この strcpy は、ポインタを使って書くと以下のように書ける。

// string copy ポインタのイメージで記載
void strcpy( char* p , char* q ) {
   while( *q != '¥0' ) {
      *p = *q ;
      p++ ;
      q++ ;
   }
   *p = '¥0' ;
}
// ポインタ加算と代入を一度に書く
void strcpy( char* p , char* q ) {
   while( *q != '¥0' )
      *p++ = *q++ ;    // *(p++) = *(q++)
}
// ポインタ加算と代入と'¥0'判定を一度に書く
void strcpy( char* p , char* q ) {
   while( (*p++ = *q++) != '¥0' )   // while( *p++ = *q++ ) ; でも良い
      ;
}

構造体とポインタ

構造体を関数に渡して処理を行う例を示す。

struct Person {
   char name[ 10 ] ;
   int  age ;
} ;
struct Person table[3] = {
   { "t-saitoh" , 55 } ,
   { "tomoko" ,   44 } ,
   { "mitsuki" ,  19 } ,
} ;

void print_Person( struct Person* p ) {
   printf( "%s %d\n" ,
           (*p).name , // * と . では . の方が優先順位が高い
                       // p->name と簡単に書ける。
           p->age ) ;  // (*p).age の簡単な書き方
}

void main() {
   for( int i = 0 ; i < 3 ; i++ ) {
      print_Person( &(table[i]) ) ;
   // print_Person( table + i ) ; でも良い
   }
}

構造体へのポインタの中の要素を参照する時には、アロー演算子 -> を使う。

練習問題(2018年度中間試験問題より)

printf() に慣れていない人もいるので…ヒント:%d 引数を10進数で表示、%s 引数の文字列として表示(文字列の先頭アドレスから’\0’までの文字を表示)、%c 引数を文字(char型)として表示。