[前へ]   [目次へ]   [次へ]

ポインタ(7)(C/C++)


今回は、 const ポインタについてです。
まずは 第64回 で作った以下のソースを見てください。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
#include <stdio.h>

int StringLength(char *str){//(1)
   /******************************
   文字列のバイト数を数えて返す。
   
   戻り値:strのバイト数
   
   str:[入力]バイト数を数える対象の文字列
   ******************************/
   
int i;
   
for(i=0;str[i]!='\0';i++);//(2)
   
return i;
}

int main(void){
   
char str1[11]="abcde";
   
char str2[11]="あいうえお";
   
char str3[11]="1\n23\n456";
   
   printf(
"%sの長さ:%d\n\n",str1,StringLength(str1));//(3)
   
printf("%sの長さ:%d\n\n",str2,StringLength(str2));
   printf(
"%sの長さ:%d\n\n",str3,StringLength(str3));
   
   
//終了待ち
   
getchar();
   
return 0;
(1)3行目の StringLength 関数定義で引数 str は char* 型です。
そして仕様では str 引数の絶対IDは入力専用とされ、参照先は変更されないことになっています。
実際にこのコードは str 引数の参照先を変更しませんが、 第63回 の例と同じ方法で、
StringLength 関数は str 引数に渡された絶対IDを元に、呼び出し元の変数の内容を変更することができます。

ここで、もし StringLength 関数にバグがあり、
入力専用のはずの引数の絶対IDを使って呼び出し元の変数を変更されるとちょっと困ります。

↓ StringLength 関数にバグがあったらどうなりますか?
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
#include <stdio.h>

int StringLength(char *str){//(1)
   /******************************
   文字列のバイト数を数えて返す。
   
   戻り値:strのバイト数
   
   str:[入力]バイト数を数える対象の文字列
   ******************************/
   
int i;
   
for(i=0;str[i]!='\0';i++){
      str[i]=
'a';//(2)どうしてこんなコードが?とか突っ込んではいけません(笑)
   }
   
return i;
}

int main(void){
   
char str1[11]="abcde";
   
char str2[11]="あいうえお";
   
char str3[11]="1\n23\n456";
   
   printf(
"%sの長さ:%d\n\n",str1,StringLength(str1));
   printf(
"%sの長さ:%d\n\n",str2,StringLength(str2));
   printf(
"%sの長さ:%d\n\n",str3,StringLength(str3));
   
   
//終了待ち
   
getchar();
   
return 0;
実行結果:
aaaaaの長さ:5

aaaaaaaaaaの長さ:10

aaaaaaaaの長さ:8


分かりやすいように豪快にバグっていただきましたが、
これまで解説した仕様に従っているコードなので、コンパイラは何も言いません。

この講座をここまで読んでこられた方なら、 [ ] 演算子がポインタ型に使えることはお分かりかと思います。
ですが [ ] 演算子とポインタと配列の関係性はC/C++での混乱原因として有名なので、
もうちょっと詳細を見ていきます。

1回目の(2)13行目に注目します。

この時点までを実行した時の状況の一例
実体ID(絶対)実体の型保持値所属
0x0013FF5Cchar[11]"abcde"main(str1,line 17)
0x0013FF5Cchar97('a')main(str1[0],line 17)
0x0013FF5Dchar98('b')main(str1[1],line 17)
0x0013FF5Echar99('c')main(str1[2],line 17)
0x0013FF5Fchar100('d')main(str1[3],line 17)
0x0013FF60char101('e')main(str1[4],line 17)
0x0013FF61char0('\0')main(str1[5],line 17)
0x0013FF62char0('\0')main(str1[6],line 17)
0x0013FF63char0('\0')main(str1[7],line 17)
0x0013FF64char0('\0')main(str1[8],line 17)
0x0013FF65char0('\0')main(str1[9],line 17)
0x0013FF66char0('\0')main(str1[10],line 17)
0x0013FF68char[11]"あいうえお"main(str2,line 18)
0x0013FF74char[11]"1\n23\n456"main(str3,line 19)
0x0013FF08char*0x0013FF5C(main::str1)StringLength(str,line 3)
0x0013FEFCint0StringLength(i,line 11)
mainブロック 確保位置ID:0x0013FF5C
StringLength引数 確保位置ID:0x0013FF08
StringLengthブロック 確保位置ID:0x0013FEFC


A   B C B' D E
str [ i ]  = 'a'


通し記号種別
Achar*0x0013FF5C変数(str)(参照先:main::str1)
B演算子( [] )(その他、優先16、結合→)
Cint0変数(i)
B'演算子Bの終点
D演算子(=)(2項、算術、優先2、結合←)
Echar97('a')定数
優先順位から、演算子B(配列添字参照、左辺 変数A、座標 変数C)からです。
これまでの通り、この演算子は0x0013FF5C( str の格納値)+1( char 型のサイズ)*0( i の格納値)を計算し、
結果として0x0013FF5Cへの char 型参照を作成します。

A       B C
vtemp1  = 'a'


通し記号種別
Achar&97('a')一時変数(参照先:main::str1[0](0x0013FF5C))
B演算子(=)(2項、算術、優先2、結合←)
Cchar97('a')定数
この時の参照が代入することもできるのは単体変数への絶対IDを取っている場合と同じです。
配列は本質的には単に「絶対IDが等間隔で配置されている」変数の集合でしかないので、
絶対IDになると単体と配列の区別はありません。(隣接する要素へ計算でアクセスできるかどうかの違いはありますが)

なので、このバグ入り StringLength 関数は入ってきた文字をすべて a に変えてしまうという、アホみたいな動きをします。
でも意外とバカにはできません。
というのも、ここまであからさまでないにせよ、実際のコードが仕様と違う動きをするバグというのは、割とあるからです。


さて、バグの原因は見た目通り(2)13行の代入文なのですが。
いくら仕様で「これは入力専用です」と言っても、バグで出力されてはデバッグは困難になってしまいます。
というのも、どこで変な出力が出たかを果てしなく追いかけなければならなくなるからです。

なまじ変更する権限があるせいで、変なバグを埋め込む可能性と付き合わないといけないのは、むなしいですよね。
そこでC/C++はこの「変更権限」を無効にする方法があります。
もうお分かりかと思いますが、その方法こそ、今回の御題である「 const ポインタ」です。
const キーワードは以前、「変数の値を変更できなくする」という意味であるとして解説しました。
しかしそれは普通の変数に使った場合のことで、実は const はイロイロな意味があります。
全てにおいて共通するのは「変更不可属性を付ける」ということですが、使う対象によって様々な「変更不可属性」が付きます。
全体はそのうちやる予定ですが、今回は「ポインタ型に付けた場合」です。

さて、 const をポインタ型に付ける場合、3種類の記述があり、それぞれ意味が異なります。

●ポインタ型変数に格納されている絶対IDが示すオブジェクト(実体)を変更不可属性にする
注:属性変更はこのポインタ型変数から操作した場合のみで、元の変数は普通に操作できます。
const char*
char const*


●ポインタ型変数自体を変更不可属性にする(格納している絶対IDを変更できなくする)
char *const

●両方(格納している絶対IDもそれが示すオブジェクトも変更不可)
const char *const
char const*const


ポインタ型へのポインタ型に付けた場合、さらにややこしくなりますが、
そもそも「ポインタ型へのポインタ型」を作ること自体がそれほど多くないので詳しくはやりません。
最終的なオブジェクトを保護するには最初に const を付ければ大丈夫です。


それでは、さきほどのバグ入り StringLength 関数の引数 str を const ポインタ型にしてみましょう。

↓バグがあっても const ポインタなら・・・
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
#include <stdio.h>

int StringLength(const char *str){//(1)引数型をconst付きに
   /******************************
   文字列のバイト数を数えて返す。
   
   戻り値:strのバイト数
   
   str:[入力]バイト数を数える対象の文字列
   ******************************/
   
int i;
   
for(i=0;str[i]!='\0';i++){
      str[i]=
'a';//(2)どうしてこんなコードが?とか突っ込んではいけません(笑)
   }
   
return i;
}

int main(void){
   
char str1[11]="abcde";
   
char str2[11]="あいうえお";
   
char str3[11]="1\n23\n456";
   
   printf(
"%sの長さ:%d\n\n",str1,StringLength(str1));
   printf(
"%sの長さ:%d\n\n",str2,StringLength(str2));
   printf(
"%sの長さ:%d\n\n",str3,StringLength(str3));
   
   
//終了待ち
   
getchar();
   
return 0;
今度は、以下のような意味のエラーが出ます。

13行目:エラー:左辺値のオブジェクトは const 属性です。

変更不可属性を付けたので、
「 str に格納されている絶対IDが示す実体への変更はできない」というエラーをコンパイラが出しています。
実行して原因を探すまでもなく、コンパイラが行数付きで指摘してくれるので、とてもラクです。

なので、入力専用のポインタ型を作るときはいつも const 属性を付けておけば、
●間違って変更しようとしてもこのようにエラーにしてくれる
●仕様やソースだけ見ても即座に「入力専用」だと分かる
という、地味ながらとても便利な機能が提供されます。
使わないのはただのムダなので、積極的に利用しましょう。



ところで、 const 属性に関して、C/C++言語には一箇所とても変な仕様があります。
しかも何気に危険な仕様なので、関係する場合は要注意です。

それは、文字列定数に関してです。
以前、文字列定数( "abcde" とか)の型は「 const char の配列型」だと解説しました。
"abcde" だったら const char[6] ということですね。

そして、「配列型からポインタ型への暗黙的型変換は元になった型へのポインタ型」とも解説しました。
const char[6] からなら const char* ということですね。

ところが、文字列定数だけは例外的に char* に変化してしまいます。
型変換に関してうるさいはずのC++言語でさえ、この仕様になっています。
そのため、以下のような奇妙なことが起こります。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
int main(void){
   
const char *p;
   
char *p2;
   
   p=
"abcde";//const char[6]->const char*なので、これは問題ありません。
   
p2="abcde";//const char[6]->char*なのですが・・・?
   
p2=p;//やってることは同じ(pを介して"abcde"の絶対IDを代入)はずなのに、こっちは文句を言います。
   
return 0;
コンパイル結果:
   C言語としてコンパイルした場合:
      7行目:警告: const 属性が一致していません。
   C++言語としてコンパイルした場合:
      7行目:エラー: const char* から char* へは変換できません。

C言語だと7行目も警告止まりで通っちゃうんですが、どっちにしろ、6行目はスルーです。
しかしながら、文字列定数はあくまで const char の配列型ですから、変更したらマズイです。
実際、変更すると場合によってはアクセス違反で落ちます。(落ちなくても同じ文字列定数が以降化けたり、変なことになります)

本来なら6行目と7行目は同じエラーなり警告なりになるべきですが、
const が生まれる前の時代に作られたコードの救済のためにこうなってるんだそうです。
C++にする時にはぜひ切り捨ててほしかった仕様ですけど。


これで、ポインタに関する入門編は終了です。
次回からは簡単な単語当てゲームを作っていきます。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/12/05