ホーム » スタッフ » 斉藤徹 » 講義録 » 情報構造論 » 値渡しと参照渡しとポインター

2024年5月
 1234
567891011
12131415161718
19202122232425
262728293031  

検索・リンク

値渡しと参照渡しとポインター

Javaでの引数に対する副作用

Javaでのプログラムにおいて、下記のように関数に引数でデータが渡された場合、呼び出し元の変数が変化する/変化しないの違いが分かるであろうか?

import java.util.*;

class A {
    private int a ;
    public A( int x ) { a = x ; }
    public void set( int x ) { a = x ; }
    public int get() { return a ; }
}

public class Main {
    public static void foo( int x , Integer y , String s , int z[] , A a ) {
        x = 12345 ;        // プリミティブな引数の書き換え
        y = 23456 ;        // イミュータブルな引数の書き換え
        s = "hoge" ;
        z[0] = 34567 ;     // 参照で渡されたオブジェクトの書き換え
        a.set( 45678 ) ;
    }
    public static void main(String[] args) throws Exception {
        int     mx = 11111 ;        // プリミティブなデータ
        Integer my = 22222 ;        // イミュータブルなオブジェクト
        String  ms = "aaa" ;
        int     mz[] = { 33333 } ;  // それ以外のオブジェクト
        A       ma = new A( 44444 ) ;

        foo( mx , my , ms , mz , ma ) ;

        System.out.println( "mx="+mx+",my="+my+",ms="+ms+",mz[0]="+mz[0]+",ma="+ma.get() );
    }
}

上記のプログラムでは、foo() の第1引数 mx は、プリミティブ型なので関数の引数に渡される際には、コピーが生成されて渡されるため、呼び出し元の変数 mx の値は変化していない。

Javaでは、プリミティブ型以外のデータは、ヒープ領域に実体が保存され、そのデータの場所(ポインタ)によって管理される。

しかし、Integer型のオブジェクト my や、String型のオブジェクト ms は、参照(データの場所)が渡されるが、イミュータブルな(変更できない)オブジェクトなので、値の代入が発生すると新しいオブジェクトが生成され、そのアドレスが参照を保存している変数(ポインタ)に代入される。このため、呼び出し元の my や ms は値が変化しない。

これに対し、配列 mz や クラスオブジェクト ma は、オブジェクトの中身を関数 foo で値を変更すると、呼び出し元の変数の内容が変更される。こういった関数やメソッドの呼び出しによって、呼び出し元の値が変化することは「副作用」と呼ばれる。

こういった参照のメカニズムは、データの管理の仕方を正しく理解する必要があることから、もっと原始的な C 言語にて理解を目指す。

C言語の基礎

#include <stdio.h>

int main() {
   int n ;
   scanf( "%d" , &n ) ;  // 標準入力から整数をnに保存
   int m = 1 ;
   for( int i = 1 ; i <= n ; i++ )
      m *= i ;
   printf( "%d! = %d\n" , n , m ) ; // 
   return 0 ;
}

printf の最初の引数は、表示する際のフォーマットであり、%d の部分には対応する引数の値に置き換えて表示される。

   型                |   基数             |   型            |   表示方式
  long int      %ld  |  10進数        %d  |  double    %lf  |  固定小数点表示 %f  12.34
  int           %d   |  16進数        %x  |  float     %f   |  指数小数点表示 %e  1.234e+1
  short int     %hd  |   8進数        %o  |                 |  固定/指数自動  %g
  char          %c   |                    |  printf( "%5.2f" , 1.2345 ) ; □1.23
  char[], char* %s   |                    |
// Compile by C++
#include <stdio.h>

int main(void) {
    long int  x = 123456789L ;
    int       y = 1234567 ;
    short int z = 32767 ;
    printf( "%ld %d %hd\n" , x , y , z ) ;
    //      123456789 1234567 32767
    printf( "%d %x %o\n" , 0x1000 , 32767 , 32767 ) ;
    //      4096 7fff 77777
  
    double    p = 123.45678L ;
    float     q = 12.345 ;

    printf( "%lf %f\n" , p , q ) ;
    //      123.456780 12.345000
    printf( "(%lf) (%8.3lf) (%le)\n" , p , p , p ) ;
    //      (123.456780) ( 123.457) (1.234568e+02)   

    char      c = 0x41 ;
    char      s[] = "ABCDE" ;
    char      t[] = { 0x41 , 0x42 , 0x43 , 0x0 } ; // C言語の文字列の末尾には'
// Compile by C++
#include <stdio.h>

int main(void) {
    long int  x = 123456789L ;
    int       y = 1234567 ;
    short int z = 32767 ;
    printf( "%ld %d %hd\n" , x , y , z ) ;
    //      123456789 1234567 32767
    printf( "%d %x %o\n" , 0x1000 , 32767 , 32767 ) ;
    //      4096 7fff 77777
  
    double    p = 123.45678L ;
    float     q = 12.345 ;

    printf( "%lf %f\n" , p , q ) ;
    //      123.456780 12.345000
    printf( "(%lf) (%8.3lf) (%le)\n" , p , p , p ) ;
    //      (123.456780) ( 123.457) (1.234568e+02)   

    char      c = 0x41 ;
    char      s[] = "ABCDE" ;
    char      t[] = { 0x41 , 0x42 , 0x43 , 0x0 } ; // C言語の文字列の末尾には'\0'が必要

    printf( "(%c) (%s) (%s)\n" , c , s , t ) ;   
    //       (A) (ABCDE) (ABC)
    return 0 ;
}
'が必要 printf( "(%c) (%s) (%s)\n" , c , s , t ) ; // (A) (ABCDE) (ABC) return 0 ; }

C言語でのポインター

#include <stdio.h>

int main() {
    int  x  = 123 ; //                             px [ 〇 ]
    int* px ;       // px はポインタ                      ↓
    px = &x ;       // x の変数の番地を px に代入      x [ 123 ]
    *px = 321 ;     // px の指し示す場所に 321 を代入
    printf( "%d\n" , x ) ; // 321 を出力
    return 0 ;
}

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

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

// ポインタ加算の例
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 ] != '
// 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 ;
}
' ; 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 != '
// 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++ ) ; でも良い
      ;
}
' ) { *p = *q ; p++ ; q++ ; } *p = '
// 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++ ) ; でも良い
      ;
}
' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '
// 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++ ) ; でも良い
      ;
}
' ) *p++ = *q++ ; // *(p++) = *(q++) } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '
// 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++ ) ; でも良い
      ;
}
' ) // while( *p++ = *q++ ) ; でも良い ; }