見出し画像

C言語教室 第13回 - 文字コードと符号の取り扱い

試験期間がだいぶ長くなってしまいましたが、そろそろ次の課題に進もうと思います。予定では次は構造体をやる予定だったのですが、中間テストで「符号」の取り扱いが話題になったので、こちらを先に取り上げることにします。

話題になった投稿はこちら 【C言語】char が signed とは限らなかったという話 (C言語のおおらかな仕様に右往左往する日々)


C言語の代表的な型としては、主に文字を扱うための char、整数を扱う int、浮動小数点の float と double があります。int は符号付きで負の数は2の補数表現で実装されていると考えて構いません。

2の補数

ですから int が2バイトとして表現されている場合には、格納できる数値は -32768 ~ 32767 の 65536種類です。

C言語教室 解答編 第12回 と オーバーフローのはなし

で説明したように、C言語ではオーバーフローは無視されるので、32767 に 1 を加えると、何のエラーも起こさずに -32768 になります。このため最大値での大小判定には細心の注意が必要です。

#include <stdio.h>
#include <limits.h>

void main() {
  int i;

  for (i = INT_MAX -2; i <= INT_MAX; i++)
     printf("%d\n", i);
}

(ブラウザ環境では limits.h が読み込めないのですが、INT_MAX は使えますので #include の行を消してください)

何も不思議に思わないかもしれませんが、これを実行すると無限ループとなってしまいます。考えてみれば int は必ず INT_MAX より小さいのですから、ループ条件が偽になることはありません。

C++ をお使いであれば、聞いたことがあるかもしれませんが、イテレータとしてオブジェクトを舐める for の使い方の場合には、ループ条件には大小判定を持ち込まないようにすることが多いです。これはオブジェクトに大小判定の演算子が提供されていないことが多いのが理由です。大小ではなくて終了時(の次)の値との非等値判定を行うべしとされています。これはこれでループ変数がひとつずつ大小されている時は良いのですが、+2/-2だったりすると、やはりループを抜け損なうことがあるので油断大敵ですが。

先程のコードを、機能させるようにするためには for の条件節ではなく、ブロックの最後で等値判定を使って脱出条件を書くようにするか、気持ちが悪いのですが、以下のようにするとちゃんと動きます。

#include <stdio.h>
#include <limits.h>

void main() {
  int i;

  for (i = INT_MAX -2; i != INT_MIN; i++)
     printf("%d\n", i);
}

でも急に登場した整数の最小値と比較するなんて、これを見ただけで意味を理解するのは無理ですよね。


さて、本題に進みましょう。

float と double にはありませんが、char や int には符号なし型があります。これらを使うときには、char や int の前に unsigned というキーワードを付けます。

unsigned int u;

符号なしなので、負の数を扱うことができません。負の値を代入するとコンパイル時にわかれば警告がでることもあるかもしれませんが、2の補数表現の値がそのまま正の整数として解釈されます。整数が 2 バイトの時は -1 を代入すれば、65535 になってしまうわけです。

符号なし整数の最小値は 0 で、最大値は UINT_MAX で知ることが出来ます。もちろん符号なし型であってもオーバーフローでエラーは起きないので、先の例のように最大値で終了判定を行う時は、同じ問題が発生します。

なお、数値リテラルとして明示的に符号なしであることを表現したいときには、数値の末尾に”U”を付けます。

#define 32U

unsigned int が型として決められている時は、この指定をすることで型変換を避けられます。

注意点としては C言語の「暗黙の型変換」では、符号なし型が優先され、式の中に一度でも符号なしが現れると、それ以降の計算がすべて符号なしになります。最終的に符号あり型に代入されるとしても、その代入の際に符号あり型に変換されるだけです。例えば char から int への型変換も行われたとすると、符号のありなしで異なる値になるので、必要に応じて適切な位置に型変換を明示する必要が出ます。あまり使わないかもしれませんが、シフト演算子も符号のありなしで結果が異なります。


いよいよ問題山積の char についてです。char は元々は符号付きでした。多くのOSでは文字コード表現として ASCII または、その拡張された表現を使っていて、ASCII は 7 ビットを使って大小の英文字、数字、記号を表します。7 ビットですから範囲は 0 ~ 127 です。標準ライブラリの多くは、これを前提に設計されています。char を int に型変換して受け取るものも多く、戻り値として文字コードを返す場合には int で返し、負の場合にはエラーを示すものがあります。

char は当初から小さな整数を表現する場合にも使われることがあって、整数と同じように unsigned char がありました。日本に限らず世界の多くの国で独自の文字を入れるために ASCII が 8 ビットに拡張されて、char に 0 から 255 までのコードを使うようになったのですが、これが混乱の始まりです。

別にコードが正であっても負であっても構わないので、そのまま使い続けることも多かったのですが、最初に戻り値として -1 を返す関数で困るようになりました。文字コードに 255 が存在すると、そのコードが返ってきたのか -1 なのかの区別がつきません。幸い戻り値の型は int なので、文字を unsigned であると「定義」してしまえば、困らないようになります。このため、コンパイルする際に char は unsigned であるとして処理するという指定ができるようになりました。敢えて今までと同じ char が必要なときには、わざわざ signed という指定をすることで互換性を保つようになったのです。これで文字型には、char、unsigned char、signed char の 3 つの型が存在することになりました。

日本に限るとカナを扱えるようにするため 8ビット目を使っていることを明示するために unsigned char を使うことが多かったです。この型が使われていれば、カナが使えるんだなと思うわけです。実際、標準ライブラリではカナを含めた場合に都合が悪いことも多く、独自の unsigned char のライブラリが作られることがしばしばありました。

この混乱は漢字などを扱うようになったマルチバイト文字の時代になって、より深刻になりました。初期の頃によく使われたシフトJISコードは、ある程度 ASCII との互換性があり、今までの char のまま漢字が扱えました。たまたま漢字を表示すると英文字 2 文字分の大きさとなるので、(そのようなフォントが一般的だったので)見た目と文字列のバイト数が一致します。これで表示がレイアウトされていても崩れずに済みます。

良い事ずくめにも見えますが、この時代になっても char は符号付きなのかどうかは定まらず、OS毎に独自の文字型を typedef などで定義することで凌ぐようになりました。シフトJISに限れば、リテラルのエスケープに使われる \ が、漢字コードの 2 バイト目に使われていることから、これに対応する処理を行わないと、特定の漢字が化けてしまうという事故が頻発して、もう「ソ」は見たくもないということもありました。

0x5c問題

文字コードに関しては、まだまだいろいろあるので、ここまでにしておきます。

そろそろ、型のサイズを指定する short、long についても説明しておく必要がありますね。


今回の演習は、お休みです。

ヘッダ画像は、以下のページの素材からつくりました。

https://www.irasutoya.com/2014/10/blog-post_7.html

この記事が気に入ったらサポートをしてみませんか?