計算機の内部表現、文字コード


2進数と16進数

Rubyでのプログラミングではあまり計算機の中の都合を意識しなくてよいようになっている。しかし、凝ったプログラムを作ろうとしたり、Ruby以外で少し複雑な計算などを行なわせようとするとき、あるいはとくにコンピュータグラフィックスなどの基礎知識を習得したい場合には、計算機の内部構造、とくに2進数の性質を知らないとスムーズに理解できないことがある。

2進数

コンピュータ内部では、数字も文字も画像も音楽も全て 1 と 0 の2進数の状態で保存している。人間にとって分かりやすい10進数でなく2進数が使われるのは、計算機が何かを記憶するための格納場所であるメモリが、スイッチのように ON か OFF の2つの状態をもつトランジスタという半導体部品で構成されているからである。また、1 と 0 の2種類だけの値であれば、スイッチの ON/OFF だけでなく、磁極の向きや、光の点滅で表現することも可能となる。例えば、ハードディスクはS極とN極の磁化パターンにより1と0を記録しているし、CDやDVDはディスク表面に溝を彫り、そこにレーザー光を当てた際の反射光の拡散の有無により1と0を区別している。

上記の ON/OFF 値を何個も組み合わせてると一つのデータ格納領域になる。たとえば、ON と OFF の箱を8個用意して、次のようなデータを入れる。

OFFONOFFOFF OFFOFFOFFON

これは模式的な絵であるが、計算機のメモリの中では、電気が ON になっていたり、磁石がN極を向いている状態を想像するのが実状に近い。この ON と OFF の状態を2進数にしよう。

0100 0001

2進数の 01000001 になる。2進数を10進数に直すときは、各桁に以下のような倍率を掛けたものを足して行けばよい。

128倍64倍32倍16倍 8倍4倍2倍1倍
0100 0001

これは、64×1 + 1×1 = 65 となる。

ちなみに、情報科学では2進数の1桁のことをビット(bit)と言い、8ビットを並べた情報量の単位をバイト(byte)と呼ぶ。

16進数

2進数は2種類の数字のみで表現するため桁数が大きくなりやすくて、人間にとっては読みづらい。かといって、10進数に直すのは計算が面倒である。そのため、2進数4桁をまとめて16進数にして数値を表記することが多い。

2進数4桁をまとめると、2**4=16個の数を表すことができる。

 2進数  16進数  10進数
000000
000111
001022
001133
010044
010155
011066
011177
100088
100199
1010a10
1011b11
1100c12
1101d13
1110e14
1111f15

ON と OFF の箱で出てきた、

OFFONOFFOFF OFFOFFOFFON

つまり

0100 0001

は、2進数で 01000001 だが、これを16進数で表すと4桁毎に変換すればよいので

となる。2進数 01000001 は、16進数で41である。ちなみに41は文字コードでいえば、アルファベットの大文字の A に該当する。つまり、Aという文字を計算機の内部に記憶させたいときは以下のようにデータが格納されることになる。

0100 0001

16進数と10進数の変換

16進数を10進数へ

16進数を10進数に変換するには、各桁に16の倍数を掛けたものを足す。たとえば、16進数 56ce を10進数に直してみよう。

4096倍
(16**3)
256倍
(16**2)
16倍1倍
56ce

c の桁は10進数で 12 を意味することと、e の桁は10進数で 14 を意味することに注意して計算すると、以下のようになる。

4096*5 + 256*6 + 16*12 + 14 = 22222

10進数を16進数へ

10進数を16進数に変換するには、2つの方法がある。

  1. 10進数を直接16進数に変換する

    元の10進数を割れなくなるまで16で割り、割ったときの余りを逆に拾って行けばよい。たとえば、10進数 64206 を16進数に直してみよう。

    16) 64206    (余り)
    16)  4012 …… 14 → e
    16)   250 …… 12 → c
    16)    15 …… 10 → a
    16)     0 …… 15 → f  ↑下から上に読む
    

    割り算の余りを下から順に読んだ、face が16進の値となる。

  2. 10進数を2進数に変換してから16進数に変換する

    元の10進数を2進数に変換する(2で割り商を下に、剰余を右に書くのを、商が0になるまで繰り返し、剰余を下からの順に拾う)。

    2)  64206  (余り)
    2)  32103 …… 0
    2)  16051 …… 1
    2)   8025 …… 1
    2)   4012 …… 1
    2)   2006 …… 0
    2)   1003 …… 0
    2)    501 …… 1
    2)    250 …… 1
    2)    125 …… 0
    2)     62 …… 1
    2)     31 …… 0
    2)     15 …… 1
    2)      7 …… 1
    2)      3 …… 1
    2)      1 …… 1
    2)      0 …… 1   ↑ 下から上に読む
    

余りを下から上に読むと、

1111101011001110

となるが、これを1の位(右側)から4桁毎に区切る。

              ← 右から4桁毎に区切る
1111,1010,1100,1110

区切った2進数を16進数に直していく。

1111,1010,1100,1110
 ↑   ↑    ↑    ↑
 f    a    c    e   (16進数)

Rubyプログラムでの扱い

Rubyでは、プログラム中に書く数値の先頭に付ける記号(接頭辞)でその値が何進数であるかを指定する。

接頭辞 意味
なし 10進数
0b 2進数
0 8進数
0x 16進数

たとえば、0b1010 は、2進数の 1010 (10進数でいえば 10 )を表すことになり、0x10f0 は、16進数の 10F0 (10進数でいえば 4336 )を表すことになる。

プログラム中に、0b1010 と書こうが 10 と書こうが、0xa と書こうが、一度読み込まれれば計算機の内部ではどれも10進数でいうところの 10 として扱われ、どれも全く同じである。また、以下のプログラムで確認できるように、特に何も指定せず数値を出力するともとは2進数や16進数であったとしてもデフォルトでは10進数として表示される。

 練習問題  dec_bin_hex.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

puts 64206
puts 0b1111101011001110
puts 0xface

結果:

64206
64206
64206

数値を出力するときに何進法にするかは printf の%つきフォーマットで選択することができる。以下は、printf の%フォーマットで数値を何進法で出すかの意味を示す。

%フォーマット 何進法で出力されるか
%b 2進法
%o 8進法
%d 10進法
%x 16進法

これを利用すると、16進数を10進数に直したり、その逆をしたりすることをRubyプログラムに任せることができる。

 練習問題  convert_nums.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

printf("16進の%xは10進で%dです\n", 0x555, 0x555)
printf("10進の%dは2進で%bです\n", 1365, 1365)
printf("10進の%dは16進で%xです\n", 0b10101010101, 0b10101010101)

結果:

16進の555は10進で1365です
10進の1365は2進で10101010101です
10進の1365は16進で555です

また、getsによってキーボードやファイルから10進数以外の数値を読み込みする場合は、読み込みされた文字列を数値に変換するto_iメソッドの引数として、何進数であるかを指定する。

例:

 練習問題 
% irb
irb(main):001:0> gets.to_i(2)  # 2進数の数値を入力
1101
=> 13
irb(main):001:0> gets.to_i(16)  # 16進数の数値を入力
f
=> 15

解説:=> の後ろに表示される結果は入力された数値を10進数に変換した結果である。


文字コード

コンピュータ内では全てのデータが 1 と 0 の組み合わせとして保管されていると説明したが、文字についても同じである。計算機で文字を扱う場合、内部では文字に付けた番号を元に管理している。計算機で利用できる文字には全て番号がついている。この番号のことを文字コードという。

ASCIIコード

abcdefg... ABCD..., 0123..., !@#$ () などの、日本語入力OFFのまま入力できる文字や記号には全てASCIIコード(American Standard Code for Information Interchange)が割り当てられている。

ASCIIコード表を以下に示す。

0123456789ABCDEF
0NULSOHSTXETXEOTENQACKBELBSHTLFVTFFCRSOSI
1DLEDC1DC2DC3DC4NAKSYNETBCANEMSUBESCFSGSRSUS
2 !"#$%&'()*+,-./
30123456789:;<=>?
4@ABCDEFGHIJKLMNO
5PQRSTUVWXYZ[\]^_
6`abcdefghijklmno
7pqrstuvwxyz{|}~DEL

この表は16進数での文字コード表記となっている。最左列の縦に並ぶ0-7が16進数の10の位で、最上行の横に並ぶ0-Fが16進数の1の位である。たとえば、アルファベットの大文字 A は、「4の行」の「1の列」にあるので、16進数の「41」が文字コードとなる。以下、16進数の数字を表すときは、先頭に 0x を付けて表す。「16進数41」==「0x41」

表中、文字コード 0x00 - 0x1F は、制御文字といい主に端末を制御するための文字コードが並んでいる。代表的なものとしては、

などがある。これらの制御文字をキーボードから入力するときは、制御文字の文字コードに 0x40 を足した文字を、Ctrlキーを押しながらタイプする。つまり、0x09 の HT は、0x40 を足した 0x49 が I なので、C-i が対応するキーとなる。また、慣習的に 0x00 の NUL は C-SPC (Ctrlを押しながらスペース)に対応させることが多い。まとめると

0x00 NUL
C-@ (または C-SPC)
0x01 SOH - 0x1A SUB
C-a - C-z
0x1B ESC - 0x1E RS
C-[ - C-^
0x1F US
C-_ または C-/

という風にキー入力できる。ただし、現在ではこれらの制御文字を直接画面に出すことはほとんどないので、C-英字 などのキー入力にはカーソル移動したり、画面を消したりするなどの機能が割り当てられていることが多い。

実際に、これらの制御文字そのものを入力したいときには

をタイプする。ためしに、BELコードを使って、端末(KTerm)のベルを鳴らしてみよう。KTermで次のようにタイプする。

 練習問題 
% echo "^G"
  (^Gの部分は C-v C-g とタイプする)

KTermで順に e c h o スペース " C-v C-g " とタイプすると、画面に echo "^G" と出ているはずなので、それを確認したら最後に [Return] を押す。

ピッ

と音が出るはずである。ここで利用した echo というコマンドは、後続する引数をそのまま出力するものである(シェルの内部コマンド)。

また、0x1B の ESC コードも端末制御をするのによく用いられる。ESC コードに続いて一連の決まった文字を端末に出力すると、端末の文字の種類を変えたりすることができる。有名なものに、文字色を変えるエスケープシーケンスがある。

ためしに、画面に黄色の文字を出してみよう。ESC コードはシェル上とRubyプログラムでは \e で表せる。 C-v ESC とタイプしよう。

 練習問題 
% echo "Hello \e[35mworld"
Hello world

のように色の変わったメッセージが表れる。

Rubyプログラムでの文字コードの扱い

他のプログラミング言語と比較すると、Rubyでは文字コードのことを意識しなくてもプログラミングできる範囲が広くなっている。上記のように画面に出すメッセージの色を変えたいようなときは文字コードを意識する必要がある。

ある文字コードに該当する文字を画面に出力したい場合は、printf のフォーマット文字列のところに %c を指定する。たとえば、

printf("%c", 0x07)

とすると、文字コード 0x07 つまり BEL が出力され、端末のベルが鳴る。やってみよう。

 練習問題 
% irb
irb(main):001:0> printf("%c", 0x07)

文字コードを実際の文字に変換するのが %c であるならばその逆は何だろう。ある文字の文字コードを得るにはStringクラスに備わっているordメソッドを使う。プログラム中に "a".ord と書けば、それは 文字 'a' の文字コード(つまり0x61)と書いたのと同じことになる。"9".ord と書けば '9' の文字コード(つまり0x39)と書いたのと同じことになる。次のプログラム ascii.rb は、ASCIIコード表のうち、'!'(0x21) - 'z'(0x7A) の範囲の文字全てを出力するものである。

 練習問題  ascii.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

char = "!".ord	# ! の文字コード(==0x21)

while char <= "Z".ord
  printf("ascii-code %3d(0x%02x): %c\n", char, char, char)
  char += 1
end

文字列と文字コード

Rubyプログラム中の文字列は、あたかも文字コードの数値が並んで入っている配列のように考えることができる。たとえば、"Hello" という文字列を考えよう。各文字の文字コードを調べると、H は 0x48、e は 0x65、l は 0x6c、o は 0x6f となる。これらの数値が並んで配列に入ったものをイメージしよう。

0 1 2 3 4
H e l l o
0x48 0x65 0x6c 0x6c 0x6f

1行目に示されている0-4の数字はそれぞれの文字のインデックスである(配列と同じように0からはじまる)。文字列 "Hello" の特定の文字を取り出すときは、配列のときと同様に大括弧 [ ] を利用してその中には文字のインデックスを入れる。

 練習問題 
% irb
irb(main):001:0> printf("%c\n", "Hello"[0])
H
=> nil
irb(main):002:0> printf("%x\n", "Hello"[0].ord)  # .ordにより16進数の文字コードが得られる
48
=> nil
irb(main):002:0> printf("%x\n", "Hello"[1].ord)
65
=> nil

これを文字列の先頭から、末尾まで繰り返すプログラムhellocode.rbは以下のようになる。

 練習問題  hellocode.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

hello = "Hello"
i = 0

while i < hello.length
  printf("%c → 0x%x\n", hello[i], hello[i].ord)
  i += 1
end

結果:

H → 0x48
e → 0x65
l → 0x6c
l → 0x6c
o → 0x6f

文字列のエンコーディング

計算機でASCII文字以外を含む文字列(たとえば日本語文字列)を表現する方法は複数ある。その方式のことをエンコーディングという。

代表的なエンコーディング

日本語の処理をする場合,以下のエンコーディングを使い分ける必要がある。

UTF-8コード(utf-8)

Unicodeという国際的な文字コードの規格で定められたエンコーディングで、多国語を同時表現できる。

JISコード(iso-2022-jp)

主に電子メールで用いられる。

日本語EUCコード(euc-jp)

日本語に特化したプログラム処理を作成する場合に主に用いられる。

(シフトJIS)(shift_jis)

Windows 系で用いられる。

同じ文字であっても、使用したエンコーディングによってその文字を表す数値が異なる。

文字 utf-8 iso-2022-jp euc-jp shift_jis
あ E38182 2422 A4A2 82A0

また逆に、エンコーディングによって同じ数値がまったく違う文字として表示される。

数値 euc-jp shift_jis
E0D1 猾 獏

エンコーディングの指定と変換

この授業では、プログラムの最初に

# -*- coding: utf-8 -*-

というコメントを記述することによってプログラムのエンコーディングを指定している。このコメントはマジックコメントという。

open によってRubyプログラムで読み込む(ASCII文字以外の)データが utf-8 以外のエンコーディングになっている場合、そのまま開こうとすると文字化けしてしまう。euc-jpエンコーディングで作成されたファイル eucjp_data.txt を使って確かめよう。

eucjp_data.txt

 練習問題  read_eucjp_data.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

open("eucjp_data.txt", "r") do |f|
  while line = f.gets
    puts line
  end
end

結果:

ASCII characters display normally
���������ܸ�ϡ����󥳡��ǥ��󥰤�
���������ꤷ�ʤ��ȡ�ʸ�������ˤʤ�

このようなデータを正しく処理するためには、以下のようにエンコーディングを指定してオープンする必要がある。

open(ファイル, "モード:エンコーディング")

なお、ターミナルのエンコーディング設定も utf-8 になっているので、出力する時それに変換しないとやはり文字化けしてしまう。文字列のエンコーディング変換にはencodeメソッドを用いる。

文字列.encode("utf-8") 元の文字列をUTF-8コードに変換した文字列を返す
文字列.encode("iso-2022-jp") 元の文字列をJISコードに変換した文字列を返す
文字列.encode("euc-jp") 元の文字列を日本語EUCコードに変換した文字列を返す
文字列.encode("shift_jis") 元の文字列をシフトJISコードに変換した文字列を返す

上記のプログラムを修正すると以下のようになる。

 練習問題  read_eucjp_data2.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

open("eucjp_data.txt", "r:euc-jp") do |f|
  while line = f.gets
    puts line.encode("utf-8")
  end
end

結果:

ASCII characters display normally
しかし日本語は、エンコーディングを
正しく設定しないと、文字化けになる

また、Rubyプログラムで読み込むデータに対して、ASCII文字以外の文字を含む正規表現を使用したい場合は、正規表現マッチングの対象になる文字列とプログラムのエンコーディングを合わせる必要がある(そうしないとエラーになる)。

 練習問題  read_eucjp_data3.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

open("eucjp_data.txt", "r:euc-jp") do |f|
  while line = f.gets  # line のエンコーディングは euc-jp になっている
    line_utf8 = line.encode("utf-8")
    if /し/ =~ line_utf8
      puts line_utf8
    end
  end
end

結果:

しかし日本語は、エンコーディングを
正しく設定しないと、文字化けになる

プログラムのエンコーディング(utf-8)以外のエンコーディングを使用してファイルに書き込みをしたい場合も同じように指定できる。

 練習問題  write_eucjp_data.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

open("eucjp_data.txt", "w:euc-jp") do |f|
  f.puts("日本語の文字列です")
end

本日の課題

 基本課題 

ユーザーが入力した数値の進数変換を行うプログラムnum_converter.rbを作成せよ。

プログラム動作のながれ:
1)ユーザーが、入力数値の進法を指定する(選択肢:2進数, 10進数, 16進数)
2)ユーザーが、指定された進法の数値を入力する
3)ユーザーが、出力の進法を指定する(選択肢:2進数, 10進数, 16進数)
4)指定された進法で数値が出力される。

実行結果の例:

sime{c11xxxx}% ruby num_converter.rb
何進数の数値を入力しますか(1:2進数、2:10進数、3:16進数)
2
指定された進法で好きな数値を入力してください
1365
何進数に変換しますか(1:2進数、2:10進数、3:16進数)
3
16進数に変換すると 555 です。

本文は下記の通り記入してください.

氏名: 苗字名前
学籍番号: C11xxxxx

ソースコード:
...

実行結果:
...

 発展課題 

  1. ユーザーが入力した数値の進数変換を行う際に printf を使うのではなく上記で説明した変換の手順(例えば2進数の各桁に決まった倍率を掛けて総和をとることで10進数を得るという方法や10進数を16で割り続けることで16進数を得るという方法)を行うプログラム num_converter2.rb を実装してみよう。

目次