えーっと!専門家に見てもらったら案の定このページの内容は大間違いだということがわかりましたー!(泣 そんなわけで再びお勉強&修正開始〜!
C言語の変数っていろいろ型がある。で、それらを相互に変換することを型キャストあるいは単純にキャストと言う。
変数の型
そもそも、「型」ってなに??
普通のコンピュータの素子は1bit(ビット)。0と1しか記憶できない。で、それを8個集めた単位が1byte(バイト)。(つまり8bit=1byteになる)
1バイト | |||||||
0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
経験則だが、普通多くの言語はこの1バイトを最低単位とする。それ以下のビットは特殊な用途で使用することが多い。ちょうどお金の円と銭みたいな感じ
C言語でも、基本的には変数の最低単位はこの1バイトになる。
1バイトは2進数で00000000〜11111111。10進数に直すと0〜255までとなる。逆に言えば、たった256種類しか表現できない困った単位ともいえる。
でもこれじゃ不便なので、複数のバイトで1個という単位の変数が生まれた。
C言語では大体こんな感じ。
型名 | バイト数 | 用途 | 範囲(10進数) | 範囲(16進数) | 備考 |
char型 | 1バイト | 文字(一文字単位) | -127〜127 | 0x00〜0xFF | 「A」だの「i」だの「#」だの |
short型 | 2バイト | すこし大きな整数 | -32768〜32767 | 0x0000〜0xFFFF | |
long型 | 4バイト | かなり大きな整数 | -214783648〜214783647 | 0x00000000〜0xFFFFFFFF | |
int型 | 4バイト | かなり大きな整数 | -214783648〜214783647 | 0x00000000〜0xFFFFFFFF | どうも通常は動作している環境って単位みたい。つまり16bitCPUなら16bit、32bitCPUなら32bitとか(Cコンパイラに依存?) |
float型 | 4バイト | 少数を扱える | 小数を扱いたい場合使うらしい | ||
double型 | 8バイト | より桁数が多い小数 | |||
ポインタ型 | 4バイト | アドレス変数 | アドレスを記憶する |
・・・こうして見ると、intやfloat、ポインタはみな同じバイト数である。なのでおそらく、実際にメモリ内に記憶されている状態ではまったく同じ状態だと思われる。
int a; float b; char *c; a=0x00FF00FF; b=0x00FF00FF; c=0x00FF00FF; /*この状態で各変数の実際の場所を覗いてみるときっとまったく同じだと思われる*/
しかし、C言語上ではおのおのは型(タイプ)が違う変数という解釈がされるのだろう。
豆知識〜
枝豆ってなぁ・・・大豆やねんでぇ?
- たとえばchar型。1バイトである。二進数でいえば8桁。扱える範囲は 00000000〜11111111までだ。これを16進数に直すと0x00〜0xFF。10進数だと0〜255までの値を使えるということになる。
ヨミちゃんアカンわ・・・・
- しかしこれは間違い。C言語の通常の考え方では、頭の(一番左)のビットは符号用の印として扱うようだ。頭の1ビットが0なら正数。1なら負の数ということ。っていうことは、char型が数字の値として扱えるのは残りの7ビットだけということになるわけ。7ビット(0000000〜1111111)だと、0〜127まで。で、符号があるから、char型が扱える数値の範囲は -127〜127 ということになる。ポインタを除く普通の変数はみなこの法則になる。
それは豆知識やのーて豆の知識や〜て言わなアカンやろ
- しかしそれだと不便だという場合もあるよね?。50+40+80なんて計算をしたい場合、char型では正の数は最大で128。たかが170なんて値を計算したのにint型(4バイト)を使わなくてはいけなくなる。これじゃメモリの無駄だよね。なので、unsignedというオプションがある。ただの char a; だと、変数aは-127〜127という範囲になるけど、unsigned char a; って宣言すると、最初の1ビットを符号とするのをやめてくれる。なので変数aは、 0〜255 を扱える変数になるわけ。
多くのコンピュータの変数等は「0」という値をもっている。なので大抵0〜xxxという感じになる。16進数で0x00〜0xFFは、10進数で0〜255となる。最大の値は255だが、0も1個と数えられるので、個数・種類という目でみると1個増える。
- 例: 1バイトは0〜255までの数字を扱える。なので、256種類の色を表現できる
違う型同士の演算
ここは厳密には間違っているが、概念の理解のため大雑把に端折って考える
当たり前だが現実の世界の計算でも、型やタイプがバラバラなもの同士の演算はできないし、困る。
X = sing60(ピタゴラス+マジンガーZ)
とか。
同じようにC言語の変数でも、違う型同士だと演算や代入はできない。intとfloatがたまたま同じ4バイトだからと言っても、とにかく型が違うんだからできない。ましてやchar型+int型なんて、バイト数も違うんだからなおさらである。
int a,aa,aaa; unsegned int b; char c; float d; double e; aaa = aa + a; /*同じint同士。足し算も代入もOK。*/ a = b - aa; /*おいおい! bとaaは型が違うだろ同じ4バイトでも!*/ aaa = e; /*うわ!aaaとeじゃ型が違うしそもそもバイト数も・・・*/ aa = e + d -b; /*もうなにがなんだか・・・*/
そこで!!!
型キャスト(型の変換)が必要になってくるというわけ。
単純なもの
こんな式を妄想してみます。
char a; int b; int c: a = 18; b = 38632; c = a + b;
さて、これは問題です。まず、 a+b の時点で、char型+int型です。型が合いません。
これを暗算すると、38650になります。計算の結果はint型の変数cに収まります。代入はまあ、問題ないでしょう。
じゃあどうしたらいいかというと、こんなふうにしてchar型のaをint型に変換します。
char a; int b,ab; int c: a = 18; b = 38632; ab = (int)a; /*こうすると、char型aの値(18)はそのままに、int型に変換してくれる*/ c = ab + b; /*当然int同士の足し算。そして結果をint型に代入なので、問題なし!*/
さて、上の式を見ると、変数abは一時的に型キャストした変数aを収めているだけですね。こんなことでいちいち変数を使うのはもったいないし行数も増える。ちょっといや〜ん。
そこでこんなふうに書くことができます。
char a; int b; int c: a = 18; b = 38632; c = (int)a + b;
なるほど!。簡単で単純ですね!
・・・ちなみに、この型キャストはあくまでも一時的に式の内部で変換をしただけです。なので、変数aそのものがint型に変化したわけではないことに注意。
ポインタ型のキャスト
これがややこしいですね。整理して単純化して考えてみます。
たとえばですが、メモリの0x0010番地〜0x0014番地になんらかの方法で22〜26までの値を代入してあるとします。(どうやるかはちょっとここでは考えない)
とりあえず今はこういう状態だと仮定する。 : : ├──────┤ 0x0010番地 │ 22 (0x16)│ ├──────┤ 0x0011番地 │ 23 (0x17)│ ├──────┤ 0x0012番地 │ 24 (0x18)│ ├──────┤ 0x0013番地 │ 25 (0x19)│ ├──────┤ 0x0014番地 │ 26 (0x1A)│ ├──────┤ : :
ここでたとえば、「0x0010番地〜0x0014番地に入っている値を表示しなさい!」という指令を受けたとします。どういうプログラムになるでしょう?
char *p; char a; int i; for(i = 0x0010; i = 0x0014; i = i+1) { p = i; printf("データは %d 16進数だと %x だよーん!\n",*p); }
これで、0x0010番地〜0x0014番地の内容が次々と表示されるはずです。
データは 22 16進数だと 0x16 だよーん! データは 23 16進数だと 0x17 だよーん! データは 24 16進数だと 0x18 だよーん! データは 25 16進数だと 0x19 だよーん! データは 26 16進数だと 0x1A だよーん!
さて、このプログラム。コンパイルするとエラーというか、警告が出てきます。型が合わないんです。いったいどこが????
p = i;
なんと!ここです。おかしいですね?。pはアドレス変数として宣言しています。pに数値を入れればその数値の番地の内容が*pで取り出せるはずです。だからアドレス変数pに、変数i(0x0010〜0x0014まで変化してくれる値)を代入したのですが・・・
実はC言語では、同じ4バイトの変数や値とアドレス変数というのはまったく別で特別なものと考えられているようです。
普通の型の変数でもバイト数が違ったりすると問題なのはわかりますよね?。アドレス変数はもっとまったく別で、数値ですらないくらいまったく別なんです。
こんなふうに考えるとわかります。アドレス。文字通り、住所・番地です。
- たとえばアナタの住んでいる所。住所がありますよね?。で、その住所。数字も使われているでしょう?
- 日本国 東京都 葛飾区 亀有 5丁目 18番地 とか。
- こういう住所の数字に対して、「なあ?お前の家の住所の三倍ってなに?」ともし聞かれたらどう答えますか?
- 「まずよく寝ろ!話はそれからだ」が正しいですね? <はぁ???
- つまり、住所にはたまたま数字が使われていますが、これは倍にしたり割り算したりするようなものではない。つまり、数字は使っているけど数値ではないとなります。
上記の例がエラーになるのはこれが理由です。int(4バイト)だろうがchar(1バイト)だろうが、これらは数値なのです。なので、「住所の番地に数を代入しようとしてるぞ!おかしいぞ!」と、こういうことでエラーになってしまうのです。
そのため、こういう代入をしたい場合は、数値であるiを型キャストして、一時的にアドレス変数タイプに転換してから代入してやる必要があります。
この場合、何型にキャストしてあげればいいでしょう。pと同じにしてあげればいいのです。変数pはchar型です。しかも、*がついています。char型のアドレス変数(ポインタ)です。なので、
char *p; char a; int i; for(i = 0x0010; i = 0x0014; i = i+1) { p = (char *)i; /* ←ここに注目!*/ printf("データは %d 16進数だと %x だよーん!\n",*p); }
こうしてあげれば、int変数であるiは一時的にアドレス変数pと同じ型に変化します。よって代入が可能になり、エラーが出なくなります。
もう一度書きますが、アドレス変数(ポインタ)は、たまたま数字を使っているだけの住所番地です。よって、足したり割ったり掛け算したりできる数値とはまったく違う型なのです。
- ちょっとまて!それはおかしいぞ! この状態で p = p + 1 等の演算をしてもエラーにならないはず。アドレス変数だって計算が出来る数値じゃないのか??
- 大変すばらしい問いです。ほんとですね。アドレス変数であるpに対して、足し算・引き算を行うことはできますしエラーにはなりません。やっぱりアドレス変数も数値なのでしょうか???
- では、実験してください。p = p * 3 等、掛け算や割り算をしてみてください。ほらね?エラーになるでしょう??。これはいったいどういうことなのでしょうか?
- 難しく考えることじゃありません。たとえば最初に出した、実際の住所・番地で考えてください。
- あなたの住所は「日本国 東京都 葛飾区 亀有 5丁目 18番地」です。この住所に対して、「お前の住所の2倍」というのは成り立ちません。
- しかし、「お前の住所の次」はどうですか?
- 「お前の住所の3軒前の家」ならどうでしょうか?。
- そう。成り立ちますね?
- つまり、アドレス変数が足し算引き算はできても掛け算割り算ができないのはこういう理由なのです。そしてこの理屈なら、足し算ができたってやっぱりアドレス変数は数値ではないということも矛盾しないでしょう?
- もっと言えば、アドレス変数(ポインタ)を p = p + 2 などとしているのは「2軒後のお家」と言っているだけで、そもそも足し算ではないとさえ言えます
数値の型キャスト
たとえばこんな式を考えます。
char z; char c; z = 0x44; c = 0x22; c = c + z; printf("data is %x\n",c);
答えは暗算できますね。結果は0x66になります。もちろんエラーも出ません。同じ型同士の足し算を同じ型に代入したのですから当然ですね。
ではこんな式はどうでしょう?
int i; char z; char c; z = 0x44; c = 0x22; i = c + z; printf("data is %x\n",i);
これまで説明してきた「概念」ではこれはおかしいはずです。だって、変数zと変数cはchar型。しかし代入しているiはint型。型が合わないはずです。ところが・・・
あれれれ〜? これをコンパイルすると、ちゃんとエラーもなく、さらに実行してもちゃんと答えが出てきます。どういうこと???
実はこれは、厳密には概念通り、型が合わないのです。しかしCコンパイラは、「この程度ならよく使うし、いちいち型キャストの警告出しててもうっとぉしいだろうなぁ。この程度は容認してあげよう」と考えるように調整されているのです。
- もちろん、 i = (int)c + (int)z; と書いても当然エラーにはなりません。むしろこれが正しいのです。本来の概念で言えば。
もちろん「許容」にもちゃんと法則があります。
許容の法則は一般にちょっとややこしい説明がされていることが多いのですが、こんな風に分けて考えればそうむずかしくありません。
まず、左辺と右辺に分けて考えるといいです。
- まず右辺内
- 混在する一番大きな型に合わせて自動調整される
- ただし、int以下のものは全てintに強制変換される
- 以上の法則で、「とりあえず右辺内」の値が出来上がる
- 次に左辺
- 右辺の値や型がなんだろうと左辺の型に強制変換される
- もしはみ出た場合は切り捨てられる
基本的にはこれを上から順番に行い、最後に左辺の値が決まると考えてもらってOKです。
実際の場合を思考実験してみましょう。たとえばこんな式の場合。
char c; int i; c = 0xf3; i = 0x12; c = c + i; printf("data is 0x%X\n",c);
これは暗算できますね?。答えは0x105になるはずです。しかしこれを実際にコンパイルして実行すると、答えは0x5になります。はて???
c = c + i; /* ← ここがどう処理されているのか? */
- まず右辺。
- おぉ? iはintだけど、cはcharじゃないか!。型が違うぞ!
- 仕方がない。自動的に許容しよう。
- 右辺で一番大きいのは・・・int型(変数i)だな!
- よし、全ての変数を無理やりint型として考えよう!
- 結果、この式は、c = 0x000000f3(int型。4バイト) + 0x00000012 (int型。4バイト)に変換だ!
- よし!計算できたぞ!。結果はこうだ! c = 0x00000105(int型。4バイト) だ!。
- これで右辺は終了だ!。今度は左辺への代入処理だ!
- 左辺への代入の場合、右辺が何だろうと左辺の型に強制変換だ!
- むむむ! なんていうことだ! 左辺はchar型。右辺はint型。左辺の型のほうが小さいぞ!
- ・・・しかし、「右辺が何だろうと左辺の型に強制変換」は決まっている!。これは崩せない!
- 仕方がない。はみ出す部分は残念だが切捨ててしまおう!
- 右辺をこう切り捨てる。 c = 0x05 (右辺の頭の3バイトが切り捨てられた)
- よし! 左辺 c に代入できるぞ。代入!
こういう内部処理が行われて、結果、暗算の結果である0x105は0x05に切り捨てられ、cに代入が完成し、計算が終わるのです。
この、自動型調整はややこしいといわれますが、とにかくポイントは、上記の8番の部分。「式の処理は、一旦右辺の中で全部解決して、それが終わったら改めて左辺への代入に移る」というところでしょうか?
- 少数の場合はどうなるの???
- ・・・いや申し訳ない。少数を扱う場合も勉強してみたのですが、どうも整理しきれませんでした。またここはオイラがOSを作るために勉強して整理しているサイトなので、このページとしては少数型はこれ以上はツッこまないことにしようかと・・・
現実の型キャスト(ポインタ)
これは「現実」とは言っても、ほぼ概念通りと考えてもらってかまわないと思います。
アドレス変数(ポインタ)は特別にして唯一無比の型なので、とにかくこれに代入しようとした場合はほぼかならず型キャストして代入するで問題ないでしょう。
- 少ない桁数の変数から型キャストした場合どうなるのか?
こういうのは実験しちゃうのが早いですよ?
int main(void){ char *p; char c; c = 0x33; p = c; printf("data is 0x%X\n",p); }
さて、これをコンパイルすると・・・
おっと!いけない!型キャストを忘れていました。
int main(void){ char *p; char c; c = 0x33; p = (char *)c; printf("data is 0x%X\n",p); }
・・・実は、これもエラーが出ます。「キャストはいいけどサイズが合わんぞ。サイズくらい合わせろよ!」ってとこでしょうか?
しかし警告エラーは無視してコンパイルし、これを実行すると、答えは0x33と表示されます。おそらくこういう場合、足りないケタ数は0で埋められてしまうようです。
では、char型の変数の値をアドレス変数に当て込むにはどういう手法が正しいのでしょうか?
正しい呼び名はわかりませんが、おそらくこういう「二段キャスト」でよいはずです。
p = (char *)(int)c;
上記。理解のため無理にchar型を使っていますが、やっぱり理想は、せめてint型にしてから、あるいはアドレス変数への代入が予想されるような値は最初からint型で定義して使うというのが本流なのでしょう。きっと。