ホーム » 「ポインタ」タグがついた投稿

タグアーカイブ: ポインタ

2019年9月
« 8月    
1234567
891011121314
15161718192021
22232425262728
2930  

最近の投稿(電子情報)

アーカイブ

カテゴリー

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

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

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

// ポインタ加算の例
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 だけを記述すると、配列の先頭を意味することに注意。

構造体とポインタ

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

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 ) ; でも良い
   }
}

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

ポインタを使った処理

この後の授業で、ポインタを使ったプログラムが増えるので、ポインタの理解の確認

ポインタと引数

値渡し

// 値渡しのプログラム
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 が表示される。
でも、プログラムによっては、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() ;                   // 表示されない。
}

静的局所変数

大域変数を使わない方法としては、静的局所変数( static )を使う方法もある。

// 静的局所変数を使う場合
void foo() {
   static int x = 123 ; // x は foo の内部でのみ使用可
                        // x はプログラム起動時に作られ 123 で初期化される。
   x++ ;
   printf( "%d¥n" , x ) ;
}
void main() {
   foo() ;  // 124
   foo() ;  // 125
   // ここで x = 321 といった代入はできない
}

静的局所変数は、変数の寿命が大域変数と同じように、プログラム起動から終了までの間だが、参照できるスコープ所属するブロック内に限られる。

ポインタ渡し

C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。

// ポインタ渡しのプログラム
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++では、ポインタ渡しを使わないようにするために、参照渡しを利用する。

参照渡し

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

ポインタ渡しを使う処理

// ポインタ渡しのプログラム
void swap( int* pa , int* pb ) {
   int t = *pa ;
   *pa = *pb ;
   *pb = t ;
}
void main() {
   int a = 123 ;
   int b = 111 ;
   swap( &a , &b ) ;
   printf( "%d %d¥n" , a , b ) ;
}

ポインタと番地の理解

リスト構造とかのプログラミングでは、ポインタが使われるが、番地とポインタをうまく理解していないと、どのような処理をしているのか理解しづらいはず。

今回の補講では、ポインタを理解してもらう。

以下では、ポインタを使った処理(前半)を見て、ポインタの動きを考える。理解できていなければ、同じ処理をポインタ無し、番地を意識させる memory[] 配列による記述(後半)で、動きを追って2つのプログラムが同じ挙動を表している…という説明の繰り返しで、ポインタの理解を図る。

単純な変数の加算

プログラムで、「 c = a + b ; 」と書いてあったら、メモリの「変数aの番地の中身」と「変数bの番地の中身」を加えて、結果を「変数cの番地」に保存する。

// 変数 a と 変数b の加算
int a = 11 ;
int b = 22 ;
int c ;
c = a + b ;
// 同じ処理をメモリの番地のイメージを示す。
int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 0 , 0 } ;
#define ADDR_A 3
#define ADDR_B 4
#define ADDR_C 5
memory[ ADDR_C ] = memory[ ADDR_A ] + memory[ ADDR_B ] ;

ポインタのイメージ

// ポインタの処理
int a = 11 ;
int b = 22 ;
int*p ;
p = &a ;
(*p)++ ;
p = &b ;
(*p)++ ;
// 同じ処理をメモリ番地のイメージで
int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 0 , 0 } ;
#define ADDR_A 3
#define ADDR_B 4
int p ;              // int *p ;
p = ADDR_A ;         // p = &a ;
memory[ p ]++ ;      // (*p)++ ;
p = ADDR_B ;         // p = &b ;
memory[ p ]++ ;      // (*p)++ ;

ポインタ渡し

// ポインタ引数による値の交換
void swap( int*x , int*y ) {
   int tmp = *x ;
   *x = *y ;
   *y = tmp ;
}
void main() {
   int a = 11 ;
   int b = 22 ;
   swap( &a , &b ) ;
}
// 同じ処理をメモリ番地のイメージで。
int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 0 , 0 } ;
#define ADDR_A 3
#define ADDR_B 4
void swap( int x , int y ) {    // void swap( int*x , int*y ) {
   int tmp = memory[ x ] ;      //   int tmp = (*x) ;
   memory[ x ] = memory[ y ] ;  //   (*x) = (*y) ;
   memory[ y ] = tmp ;          //   (*y) = tmp ;
}                               // }
void main() {
   swap( ADDR_A , ADDR_B ) ;    // swap( &a , &b ) ;
}

上記のポインタの説明では、番地をintで表現しているから、型の概念が曖昧になりそう。
本当は、以下のように pointer 型を使って説明したいけど、補講の学生に typedef は、混乱の元だろうな。ひとまず、ここまでのポインタのイメージを再学習するネタを見てもらってからなら、typedef int pointer してもいいかな?

typedef int pointer ;
void swap( pointer x , pointer y ) {
   int tmp = memory[ x ] ;
   memory[ x ] = memory[ y ] ;
   memory[ y ] = tmp ;
}

プログラミングでは、型の理解が重要。たとえ、Python,Ruby といった型宣言の無い言語でも、どんなデータなのかを意識して書く必要がある。

理解の確認

// 以下のプログラムの実行結果は?
void foo( int x ) {
   x++ ;
}
void bar( int*p ) {
   (*p)++ ;
}
void main() {
   int a = 111 ;
   foo( a ) ;  // a の中身は?
   bar( &a ) ; // a の中身は?
}
// 同じ処理を
typedef int pointer ;
int memory[ 1000 ] = { 0 , 0 , 0 , 111 , 0 , 0 } ;
#define ADDR_A 3
void foo( int x ) {
   _______________________ ;
}
void bar( pointer p ) {
   _______________________ ;
}
void main() {
   foo( ________________ ) ; // memory[ ADDR_A ] の中身は?
   bar( ________________ ) ;  // memory[ ADDR_A ] の中身は?
}

ポインタと配列

// ポインタの移動
int sum = 0 ;
int array[ 3 ] = { 11 , 22 , 33 } ;
int*p ;
p = array ;
sum += *p ;
p++ ;
sum += *p ;
p++ ;
sum += *p ;
// 同じ処理をメモリ番地のイメージで
typedef int pointer ;
int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 33 , 0 , 0 } ;
#define ADDR_SUM 2
#define ARRAY 3
pointer p ;                          // int*p ;
p = ARRAY ;                          // p = array ;
memory[ ADDR_SUM ] += memory[ p ] ;  // sum += (*p) ;
p++ ;                                // p++ ;
memory[ ADDR_SUM ] += memory[ p ] ;  // sum += (*p) ;
p++ ;                                // p++ ;
memory[ ADDR_SUM ] += memory[ p ] ;  // sum += (*p) ;

理解の確認

整数配列にデータが並んでいる。数字は0以上の数字で、データ列の後ろには必ず0が入っているものとする。配列の先頭から0を見つけるまでの値を合計する関数を作れ。

int sum( int*p ) {
   s = 0 ;
   for( __________ ; __________ ; __________ ) {
      ____________________ ;
   }
   return s ;
}
int array_a[ 4 ] = { 11 , 22 , 33 , 0 } ;
int array_b[ 5 ] = { 4 , 3 , 2 , 1 , 0 } ;
void main() {
   printf( "%d\n" , sum( array_a ) ) ; // 66 を表示
   printf( "%d\n" , sum( brray_b ) ) ; // 10 を表示
   printf( "%d\n" , sum( array_a + 1 ) ) ; // 何が表示される?
}

リスト構造

では、最後のシメということで、リスト構造でのポインタのイメージの確認。

// リストを次々たどる処理
struct List {
   int          data ;
   struct List* next ;
} ;
struct List* cons( int x , struct List* p ) {
   struct List* ans =
      (struct List*)malloc( sizeof( struct List ) ) ;
   if ( ans != NULL ) {
      ans->data = x ;
      ans->next = p ;
   }
   return ans ;
}
void main() {
   struct List* top =
      cons( 11 , cons( 22 , cons( 33 , NULL ) ) ) ;
   struct List* p ;
   for( p = top ; p != NULL ; p = p->next ) {
      printf( "%d¥n" , p->data ) ;
   }
}
// メモリのイメージで
typedef int pointer ;
int memory[ 1000 ] = {
   0  , 0 ,
   22 , 6 ,
   11 , 2 ,
   33 , 0 ,
   0  , 0 ,
} ;
#define OFFSET_DATA 0
#define OFFSET_NEXT 1
void main() {
   pointer p ;
   for( p = 4 ; p != 0 ; p = memory[ p + OFFSET_NEXT ] ) {
      printf( "%d¥n" , memory[ p + OFFSET_DATA ] ) ;
   }
}