第41回 プログラミングについて『キャストの話』

今回は cast について考えてみましょう。C言語で型を変換するときに cast を使います。通常、代入や比較時にはコンパイラが自動的に型の変換を行なうようにしてくれます。
int i;
i=10.0;
こう記述すると、double 型の定数 10.0 を int 型に変換した後 i に代入します。
printf("%d\n",10.0);
の場合はコンパイラは 10.0 は double の型のまま引数として関数に渡しますので、思ったような表示はされません。こういうときには、
printf("%d\n",(int)10.0);
とすれば結果は正しくなります(このように実数の定数をわざわざキャストすることはまずありません。こういうときには最初からキャスト後の型で定数を書くのが普通です)。
上記のように単なる値の型を変更するのはあまり難しいものではありません。値そのものは変えないけれども型を変えたいときがときどきあります。
char c[4];
long l;
c[0]=1;
c[1]=2;
c[2]=3;
c[3]=4;
このようなとき、l に c の4バイトをそのまま代入するにはどうしたらいいでしょうか。
l=c;
と書いたのでは、c は配列で l はロングワードの整数ですので型が違うという理由でコンパイラに叱られてしまいます。それでは、
l=(long)c;
としたらどうなるでしょうか。この場合は c のアドレスをロングワードに変換してからl に代入するので l には c の値は代入されません。問題は値を値にキャストしたときには値そのものの変換が行われてしまうということです。 値そのものを変えないようにするには、ポインタの型を変えることです。
そこで、
l= *(long *)c;
と書いてみると、思いどおりに代入することができます。一見 l=(long)c; と似ているようですが、実際は似て非なるものです。
上の式をもう少し詳しく分析してみましょう。
l= *(long *)c;
| | | |
| | | 1
| | 2
| 3
4
1 は c は配列ですのでアドレス値、すなわち文字配列へのポインタ char * です。
2 は 1 のアドレスの解釈をロングワードへのポインタ long * にポインタの型を変更します。この変更はアドレス値そのものは同じなのですが、コンパイラに対してそのアドレスにあるものが指し示すものは long * だよと解釈を代えさせるだけなのです。
3 はそのアドレスの示しているものですから、long * が示しているものは当然 longということになります。
今度は l のほうをキャストして同じ事を行なってみましょう。
*((char *)&l+0)=c[0];
*((char *)&l+1)=c[1];
*((char *)&l+2)=c[2];
*((char *)&l+3)=c[3];
このような書き方になります。なんだか面倒くさいのですがこれを
*(char *)(&l+0)=c[0];
*(char *)(&l+1)=c[1];
*(char *)(&l+2)=c[2];
*(char *)(&l+3)=c[3];
と書いてはとんでもないことになります。
*((char *)&l+3)=c[3];
| | | | |
| | | | 5
| | 1 |
| 2 |
| 3
4
この式を解析してみましょう。
1 は l のアドレスですので long * 型です。
2 は 1 の型を char * にキャストします。ポインタのキャストですから値そのもの(l のアドレス)には変換はありません。
3 は char * 型の3個目の要素(最初を0個目とします)のアドレスとなります。
4 は 3 のポインタが示すものを指しますので、l の位置から3バイト進んだ位置になりますので char 型です。
5 で char 型の値を char 型 に代入します。
次に、
*(char *)(&l+3)=c[3];
| | | | |
| | 1 | |
| | 2 |
| 3 |
4 |
5
1 は l のアドレスですので long * 型です。
2 は long * 型の3個目の要素(最初を0個目とします)のアドレスとなります。ということは 3x4=12 で l の位置から12バイト進んだ位置になります。
3 は 2 の型を char * にキャストします。
4 は char * の指し示すものですので char 型です。
5 で char 型の値を char 型 に代入します。
このようにカッコの位置でまったく解釈が変ってしまいとんでもないところに値を代入してしまうのです。
型をキャストするのは値かポインタの型に対して行なうことしかないのですが、私はある処理を行なう関数群との間でのポインタのやり取りに void * として行なうことがあります。
いま、ある関数群(1つのオブジェクトと考えて下さい)が複数のテキストを保持しているとします。その関数群から現在のテキストのIDを取得し、そのIDのテキストを取得したいとします。
//関数の宣言。
void *text_manager__get_current_id();
char *text_manager__get_text(void *id);
:
:
sub()
{
void *id;
id=text_manager__get_current_id();
printf("%s\n",text_manager__get_text(id));
:
:
}
この呼び出し側に対し、その関数群(text_manager)を、
struct text_data_strct{
char *text;
struct text_data_strct *next;
};
static struct text_data_strct *text_data_top;
static struct text_data_strct *text_data_current;
:
:
void *text_manager__get_current_id()
{
return (void *)text_data_current;
}
char *text_manager__get_text(void *id)
{
return ((struct text_data_strct *)id)->text;
}
のようにします。関数群(text_manager)は呼び出し側とは別のソースファイルにしてコンパイルし、呼び出し側からはこの関数の内部構造は見せないようにします。このようにすると、この関数群のデータは外部にはIDとしてやり取りされる void * 型の値のみですので、呼び出し側は関数群のデータ構造は一切分かりません。本来このIDは関数群内のデータのポインタなのですが、その構造を秘匿することで関数群と他の関数とは独立(関係が疎)していると考えることができます。もし関数群内のデータの構造を外部に公開してしまうと、関数群から受け取ったIDを、
struct text_data_strct *td;
td=(struct text_data_strct *)text_manager__get_current_id();
strcpy(td->text,"abc");
などと、本来の型にキャストして勝手にデータを変更できますので、データ構造が変更になったりバグが発生したときになどに、誰がいつどこでデータを操作しているのかがハッキリしなくなった上、大掛かりな作業になってしまいます。
このようにして考えてみると、値を返さない関数は、
void function();
で結構つまらないものなのですが、型のないポインタを返す関数は大変重要な使い方ができる可能性がある訳です。
型のキャストは必要に迫られて行なうときと意識的に行なうときがあります。今回紹介した以外にも面白い使い方があるかも知れませんので研究して下さい。
それではまた次回。