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

2024年5月
 1234
567891011
12131415161718
19202122232425
262728293031  

検索・リンク

ポインタと文字列処理

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

値渡し(pass by value)

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

このプログラムでは、aの値は変化せずに、124,124 が表示される。
でも、プログラムによっては、124,125 と変化して欲しい場合もある。
どのように記述すべきだろうか?

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

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

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

ポインタ渡し(pass by pointer)

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

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

ポインタを利用して引数に副作用を与える方法は、ポインタを正しく理解していないプログラマーでは、危険な操作となる。C++では、ポインタ渡しを極力使わないようにするために、参照渡しを利用する。ただし、ポインタ渡しも参照渡しも、機械語レベルでは同じ処理にすぎない。

参照渡し(pass by reference)

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

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

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

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

ポインタと文字列処理

#include <stdio.h>

void my_tolower( char d[] , char s[] ) {
    int i ;
    for( i = 0 ; s[i] != '\0' ; i++ )
        if ( 'A' <= s[i] && s[i] <= 'Z' )
            d[i] = s[i] - 'A' + 'a' ;
        else
            d[i] = s[i] ;
    d[i] = '\0' ;
}

int main(void){
    char str[ 20 ] ;

    my_tolower( str , "AaBcDeF Hoge" ) ;
    printf( "%s\n" , str ) ;
    return 0 ;
}

間違ったプログラム

C言語の面倒な点は、データがどのように格納されるのかを考えないと正しく動かない所であろう。

下記のプログラムの問題点がわかるだろうか?

#include <stdio.h>

// 前述の my_tolower と同じ
void my_tolower( char d[] , char s[] ) {
    int i ;
    for( i = 0 ; s[i] != '\0' ; i++ )
        if ( 'A' <= s[i] && s[i] <= 'Z' )
            d[i] = s[i] - 'A' + 'a' ;
        else
            d[i] = s[i] ;
    d[i] = '\0' ;
}

// 引数に副作用のある my_tolower
char* my_tolower_1( char s[] ) {
    for( int i = 0 ; s[i] != '\0' ; i++ )
        if ( 'A' <= s[i] && s[i] <= 'Z' )
            s[i] = s[i] - 'A' + 'a' ;
    return s ;
}

// 局所変数のメモリを帰してはダメ
char* my_tolower_2( char s[] ) {
    char str[ 20 ] ;
    int  i ;
    for( i = 0 ; s[i] != '\0' ; i++ )
        if ( 'A' <= s[i] && s[i] <= 'Z' )
            str[i] = s[i] - 'A' + 'a' ;
        else
            str[i] = s[i] ;
    str[i] = '\0' ;
    // printf( "in my_tolower_2 : %s\n" , str ) ;
    return str ;    
}

int main(void) {
    char str[ 20 ] = "Hoge" ; ;

    // case-1
    char* ptr ;
    my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped)

    // case-2
    printf( "%s\n" , my_tolower_1( str ) ) ;
    printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない

    // csse-3
    printf( "%s\n" , my_tolower_2( "foo" ) ) ;  // ゴミが表示される
    return 0 ;
}

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

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