負の整数を含む足し算(4ビット)

負の整数を2の補数を使って表現した数を普通に足せば引き算をしたことになります。

演算装置はいままでの正の整数とまったく同じに計算します。それを最高位のビットが1の時だけ2の補数を使って表現していることを考慮して負の整数と解釈するだけです。

ただし、計算結果が表現の範囲を越えると正しい答えになりません。

4ビットでは表現の範囲は -8 以上 7 以下です。

内部表現 符号つき整数
(十進数)
0 0 0 0 0
+) 0 0 0 0 0
計算値 正しい値

内部表現の薄い青の背景色部分のビットをクリックして数値を設定し、[加算実行]で和を求めます。

もう少し大きな数を扱いたいときは8ビットを見てください。

結果の表です。

コンピュータの
内部表現
計算値 正しい値
1110-214
1101-313
1100-412
1011-511
1010-610
1001-79
1000-88
011177
011066
010155
010044
001133
001022
000111
000000
1111-1-1
1110-2-2
1101-3-3
1100-4-4
1011-5-5
1010-6-6
1001-7-7
1000-8-8
01117-9
01106-10
01015-11
01004-12
00113-13
00102-14
00011-15
00000-16

解説

一番大切な所は、演算装置がすることが正の整数の足し算と全く同じということです。

正の整数の足し算に比べ計算可能範囲が狭いと感ずるかもしれませんが、半分を負の数に回した結果、正の数の範囲が半分になったのです。

コンピュータのCPUは32ビット、64ビットなどの桁数の計算を一度に行う演算装置を持っています。2進法1桁が1ビットに相当します。正の整数だけを使う場合10進法に直すと32ビットでは0から4294967295まで扱えましたが、負の数を使う場合は、-2147483648から2147483647までとなります。同様に64ビットで0から18446744073709551615までの整数が扱えたものが、-9223372036854775808から9223372036854775807までとなります。これで十分なときには問題ありません。しかし計算結果がこれを越えると正しい答えになりません。このことを理解するためにわざと4ビットしかない演算装置を用意しました。

足し算の結果が限界を超えることをオーバーフローした、溢れた、などといいますが、これを検出して計算を止めたり、複数回の計算をして64ビットの装置を128ビットの装置として使ったり、整数でなく浮動小数点数という形式で数を表して計算するなどの工夫で克服しています。

制作意図

ここに書いた「授業で使ったプログラム」は、負の整数の説明を先にして、その後加減算を説明していますが、最初に作成したときの教科書の影響です。コンピュータの歴史からいえば、引き算を効率的に行うために2の補数表現を使うようになったので、負の整数の説明をしてその先を省いては意味がありません。その上目的を見据えない知識は誤解を産みます。たとえば「2進法では負の数は補数で表す」と覚えるのは2進法を誤解します。「コンピュータで負の整数は2の補数を用いる」というのも、そう決まっているわけではなくて浮動小数点数の指数は一定数を加えて0を負の最大値とするバイアス表現を使うなど異なる方法もあることを隠してしまいます。

また「補数」ないしは「2の補数」というのも注意しないと正確な知識でなくなってしまいます。コンピュータで減算を加算で済ませる方法が数学で言う「補数」で理論的な裏付けができた、あるいはその応用のようにみなせるということであって、負の数は補数で表すといってしまうとちょっと違う。まだ確認できませんが「2の補数表現」がこの複雑な事情を含んだ適切な言葉なんだろうと思います。

この2の補数表現で「最上位ビットは符号を表し、0の時は正、1の時は負となる」というのも誤解のもとです。事実上そうなってはいますが、これは結果です。最上位ビットも含めて2の補数表現です。

使用していた教科書で説明の順序が決められましたので、引き算は負の数を足すことで行うということで説明し、加算の演算でできることを確認する形にしました。

プログラムなど

「正の整数の足し算(4ビット)」と同様に作業をステップに分けて説明していきます。最終的なものは最後にあります。

ステップ1

このステップは「正の整数の足し算(4ビット)」とまったく同じです。

まず、0,1をマウスでセットする部分です。説明のためサブセットを作りました。クリックしてみてください

0 0 0 0 0

htmlでは次のようになっています。0,1に変化する4ビットには onclick= が書かれています。イベントハンドラとよばれるもので、この要素がクリックされたらflipk1()という関数を呼び出しますが、引数にthisを入れると、関数側では引数からクリックされた要素を特定することができます。

<table class="kaisetsu">
<tr id="kaisetsuTr1">
<td></td>
<td onclick="flipk1(this)" class="bittd">0</td>
<td onclick="flipk1(this)" class="bittd">0</td>
<td onclick="flipk1(this)" class="bittd">0</td>
<td onclick="flipk1(this)" class="bittd">0</td>
<td>0</td>
</tr>
</table>

関数側では、.getElementById()で得られた要素を指す変数であるかのように使うことができます。

要素の値を調べて、0,1を反転させています。

<script>
function flipk1(e){
  var bit = e.firstChild.nodeValue;
  if ( bit == 0 ){
     bit = 1;
  }else{
     bit = 0;
  }
  e.firstChild.nodeValue = bit;
}
</script>

ステップ2

「正の整数の足し算(4ビット)」と異なるのは4つのビット列を10進法に換える部分です。na.toString(2)メソッドでやっていたものをsdnx(na,最上位ビット)関数で行います。演算は変わらずビット列の解釈が異なるということを示すためです。

ビットの値が変更されたら、4ビット全体での値を計算し、最後の欄に書き加えます。

trにidを設定し、そのtrの中のtdを配列で得て、0,1を読み出し、また書き込む欄もそれで指定します。

0 0 0 0 0

htmlは省略しますが、ステップ1とほぼ同じでonclick=の値がflipk1からflipk2に変わっているだけです。dispk2();を加えるため、別にしました。

<script>
function flipk2(e){
  var bit = e.firstChild.nodeValue;
  if ( bit == 0 ){
     bit = 1;
  }else{
     bit = 0;
  }
  e.firstChild.nodeValue = bit;
  dispk2();
}
var bitsk2 = 4;
var hok2 = 16;
function dispk2(){
   var tgt = document.getElementById("kaisetsuTr2");
   var tgttd = tgt.getElementsByTagName("td");
   var bstr = "";
   for (var i=1;bitsk2>=i;i++){ 
      bstr += tgttd[i].firstChild.nodeValue;
   }
   var na = parseInt(bstr,2);
   //tgttd[bitsk2+1].firstChild.nodeValue = na.toString(10);
   var sdna = sdnx(na,tgttd[1].firstChild.nodeValue);
   tgttd[bitsk2+1].firstChild.nodeValue = sdna;
}
function sdnx(nx,msb){
   if (msb==1){
      nx -=hok2;
   }
   return nx.toString(10);
}
</script>

グローバル変数としてbitsk2を宣言し値を4にしています。現代的なjavascriptとして推奨されるものかは、ちゃんと調べる必要を感じていますが、筋は通っているし、うまく動きます。本番ではdispk2()だけなく加算実行にも4ビットだと伝えなくてはなりません。8ビットのページを作る時にはここだけを変えようという魂胆です。

hok2は2の(bitsk2)乗です。Math.pow(2, bitsk2)でも 2**bitsk2 でも問題ないのですが、計算して直に書いています。

さて、入力された4ビットから読み取ったnaを10進法に換える部分、sdnx(na,最上位ビット)関数の話です。

2の補数表現になっている場合、 10進法の表現にするには、さらに2の補数に直してマイナス符号をつけます。普通はこの作業を2進数でやります。例えば、1110 の2の補数は 10000-1110=10 でマイナスをつけて -10 です。-10は見慣れないですか? なら10を十進に直して2としてから-2です。これを最初から十進数でやっても同じです。つまり、1110は14です。16-14=2 でマスナスをつけて-2です。

2の補数表現さらに2の補数にマイナスをつける
2進で111010000-1110=10-10
10進で1416-14=2-2

というわけで、16からnaを引いてマイナスをつけるわけですが、-(16-na)=na-16ですから、これが function sdnx() 内のnx-=hok2の根拠です。もっとも、na=parseInt(bstr,2) でビット列(01の文字列)を2進数として解釈して数値化し、naに代入していますから、我々はnaを10進数と考えていますが、コンピュータ内では2進数です。数値naを我々は10進数として把握しますが、コンピュータは2進数として扱うというだけで、もともと同じ数値です。

全部の数値を2の補数表現と見てしまうと正の数がなくなりますから、最高位のビットで判断することになっています。それが、tgttd[1].firstChild.nodeValueを引数にしている理由です。tgttd[0]でなくtgttd[1]なのは、本番の表のこの列に +)を入れている事情によります。

nx.toString(10)のtoString(10)はなくても動きますが、nodeValueとしてテキストノードに代入するので律儀に加えています。

解説のプログラムは以上ですが、本番のプログラムでは、2つの数についてこれを行ないます。また、加算結果の欄もクリアしています。本当はクリックされた方の数だけ表示し直すので良いし、加算結果をクリアするのは1回でよいのですが、1ビット変更するたびに全部繰り返しています。必要か不必要かの判断の手間・手数と余計なことをする手数の量を比較して無駄でも全部書きなおすことにしました。

ステップ3

「正の整数の足し算(4ビット)」に合わせてステップ3を作りました。同様のプログラムでいいと示したかったのですが、わかりづらくなっています。色々加えると本番環境度同じになるので、これでやめておきます。

[乱数の発生]のボタンを押すと、2つの乱数を発生させて、足し算をします。

2進法4桁で表現できる数は-8〜7なのですが、8〜15を-8〜-1に読み換えているだけなので、0〜15の乱数を発生させています。加えると-16〜14になりますが、くどいようですが、4ビットのパターンは0〜30と同じです。

2進法では1桁から5桁の数字です。末尾の4桁を書き出したいのですが、不足の時は0を補う必要があります。新しいJavascriptの規格ではぴったりな関数があるのですが、全部のブラウザで動くようになっていません。"000"を前に連結して後ろ4桁を切り出します。

演算が共通なので、na,nbには正の整数と同様に0〜15までの数値にしておきます。関数sdnx()を使ってna,nbがそれぞれ7より大きい時には対応する負の整数を求め、変数sdna,sdnbに格納しておきます。

切り出した文字列を1桁ずつ書き、10進法で「末尾4桁」の欄に書き、書き出した4ビットも、関数sdnx()を使って負数を含む値に変換して「末尾 4桁」の欄に書きます。

sdna+sdnbを計算して「正しい値」の欄に書きます。

「末尾 4桁」と「正しい値」が異なるときは、背景色を薄い赤にします。

"000 "+乱数 10進表現
末尾 4桁 正しい値

-16,-9,-8といったボタンは、足したらその値になる2数を乱数の代わりに使って計算します。2数の和が-8〜7の範囲になる時には「正しい値」と一致するということを確認できます。

//sd追加と書いてある部分が、負の整数を扱うために追加したところです。

<script>
var na,nb;
var sdna,sdnb;
function doaddk3n(nn1,nn2){
   na = nn1;
   sdna = sdnx(na,na>7?'1':'0');//sd追加
   nb = nn2;
   sdnb = sdnx(nb,nb>7?'1':'0');//sd追加
   doaddk3();
}
function rand4doaddk3(){
   na = Math.floor(Math.random()*16); //k3追加
   sdna = sdnx(na,na>7?'1':'0');//sd追加
   nb = Math.floor(Math.random()*16); //k3追加
   sdnb = sdnx(nb,nb>7?'1':'0');//sd追加
   doaddk3();
}
function doaddk3(){
   var nsum = na + nb;
   var bstrtd = document.getElementById("bstr"); //k3追加
   var tgt = document.getElementById("kaisetsuTr3");
   var tgttd = tgt.getElementsByTagName("td");
   //tgttd[0].firstChild.nodeValue = na.toString(10)+" "+nb.toString(10)+":"+sdna+" "+sdnb;
   bstrtd.firstChild.nodeValue = "000 " + nsum.toString(2); //k3追加
   var bstr = "000" + nsum.toString(2);
   bstr = bstr.substring(bstr.length-bits);
   for (var i=1;bits>=i;i++){ 
      tgttd[i].firstChild.nodeValue = bstr.substring(i-1,i);
   }
   var bgcolor = "#ffffff";
   var nc = parseInt(bstr,2);
   var sdnc = sdnx(nc,tgttd[1].firstChild.nodeValue);//sd追加
   var truthsum = parseInt(sdna,10)+parseInt(sdnb,10);//sd追加
   if (truthsum != sdnc){
      bgcolor = "#ffcccc";
   }
   tgttd[bits+1].firstChild.nodeValue = sdnc.toString(10);
   tgttd[bits+1].style.backgroundColor = bgcolor;
   tgttd[bits+2].firstChild.nodeValue = truthsum.toString(10);
}
</script>

ステップ4

繰り返そうと思ったのですが、さすがに同じなので、正の整数の足し算(4ビット)のステップ4をご覧ください。

本番部分のプログラム

Javascript

<script>
var na=0;
var sdna=0;
var nb=0;
var sdnb=0;
var bits = 4;
var ho = 16;//2^4
function flip(e){
  var bit = e.firstChild.nodeValue;
  if ( bit == 0 ){
     bit = 1;
  }else{
     bit = 0;
  }
  e.firstChild.nodeValue = bit;
  disp();
}
function sdnx(nx,msb){
   if (msb==1){
      nx -=ho;
   }
   return nx.toString(10);
}
function disp(){
   var tgt = document.getElementById("cala");
   var tgttd = tgt.getElementsByTagName("td");
   var bstr = "";
   for (var i=1;bits>=i;i++){ 
      bstr += tgttd[i].firstChild.nodeValue;
   }
   na = parseInt(bstr,2);
   sdna = sdnx(na,tgttd[1].firstChild.nodeValue);
   tgttd[bits+1].firstChild.nodeValue = sdna;
   tgt = document.getElementById("calb");
   tgttd = tgt.getElementsByTagName("td");
   bstr = "";
   for (var i=1;bits>=i;i++){ 
      bstr += tgttd[i].firstChild.nodeValue;
   }
   nb = parseInt(bstr,2);
   sdnb = sdnx(nb,tgttd[1].firstChild.nodeValue);
   tgttd[bits+1].firstChild.nodeValue = sdnb;
   tgt = document.getElementById("calc");
   tgttd = tgt.getElementsByTagName("td");
   bstr = "";
   for (var i=1;tgttd.length>i;i++){ 
      tgttd[i].firstChild.nodeValue = " ";
      tgttd[bits+1].style.backgroundColor = "#ffffff";
   }
}

function doadd(){
   var nsum = na + nb;
   var tgt = document.getElementById("calc");
   var tgttd = tgt.getElementsByTagName("td");
   var bstr = "000" + nsum.toString(2);
   bstr = bstr.substring(bstr.length-bits);
   //tgttd[0].firstChild.nodeValue = bstr;
   for (var i=1;bits>=i;i++){ 
      tgttd[i].firstChild.nodeValue = bstr.substring(i-1,i);
   }
   var nc = parseInt(bstr,2);
   var sdnc = sdnx(nc,tgttd[1].firstChild.nodeValue);
   var truthsum = parseInt(sdna,10)+parseInt(sdnb,10);
   var bgcolor = "#ffffff";
   if (sdnc != truthsum){
      bgcolor = "#ffcccc";
   }
   tgttd[bits+1].firstChild.nodeValue = sdnc.toString(10);
   tgttd[bits+1].style.backgroundColor = bgcolor;
   tgttd[bits+2].firstChild.nodeValue = truthsum.toString(10);
}
window.onload = function() {
  setlistner();
}
function setlistner(){
   var tgt = document.getElementById("cala");
   var tgttd = tgt.getElementsByTagName("td");
   for (var i=1;bits>=i;i++){ 
      tgttd[i].onclick = function(){flip(this);}
      tgttd[i].className = "bittd";
   }
   tgt = document.getElementById("calb");
   tgttd = tgt.getElementsByTagName("td");
   for (var i=1;bits>=i;i++){ 
      tgttd[i].onclick = function(){flip(this);}
      tgttd[i].className = "bittd";
   }
   tgt = document.getElementById("calc");
   tgttd = tgt.getElementsByTagName("td");
   for (var i=1;bits>=i;i++){ 
      tgttd[i].className = "anstd";
   }
}
</script>

html部分

<table id="addtbl">
<tr>
<th></th>
<th colspan="4">内部表現</th>
<th>符号つき整数<br>(十進数)</th>
<th></th>
</tr>
<tr id="cala">
<td></td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td> </td>
</tr>
<tr id="calb">
<td>+)</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td> </td>
</tr>
<tr id="calc">
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
</tr>
<tr>
<td> </td>
<td class="incdec" colspan="4">
<button type="button" onclick="doadd()">加算実行</button>
</td>
<td class="info">計算値</td>
<td class="info">正しい値</td>
</tr>
</table>

firstChild.nodeValueで値を入れる要素にはあらかじめテキストが入っている必要があります。テキストノードがfirstChildになるからです。ここでは0または半角スペースが入っています。不用意に削除してはいけません。

css部分

#addtbl{ 
  margin:1% 2%;
  border:none;
  border-collapse:collapse;
}
#addtbl td, #addtbl th{
  border:none;
}
#cala td, #calb td, #calc td{
  font-family:courier,monospace;
  font-size:200%;
  text-align :right;
  padding:2px 8px;
  height:1.5em;
}
td.incdec{
  padding:16px 2px 1px 2px;
  text-align :center;
}
td.info{
  padding-left:1em;
  text-align :right;
}
#addtbl td.bittd{
  cursor:pointer;
  border:solid 1px #999999;
  background-color:#f0f0ff;
}
#addtbl td.anstd{
  border:solid 1px #999999;
}
#addtbl tr#calc{
  border-top:solid 1px #999999;
}
button{
  cursor:pointer;
  min-width:5em;
}

正しく計算できる範囲の一覧

4ビットの正の整数の足し算、負の整数を含む足し算で正しく計算できる組み合わせの範囲を一覧にしたものです。

負の整数を含む方が正しく計算できる範囲が広くなっています。

正の整数の足し算

0123456789101112131415
00123456789101112131415
112345678910111213141516
2234567891011121314151617
33456789101112131415161718
445678910111213141516171819
5567891011121314151617181920
66789101112131415161718192021
778910111213141516171819202122
8891011121314151617181920212223
99101112131415161718192021222324
1010111213141516171819202122232425
1111121314151617181920212223242526
1212131415161718192021222324252627
1313141516171819202122232425262728
1414151617181920212223242526272829
1515161718192021222324252627282930

負の整数を含む足し算

-8-7-6-5-4-3-2-101234567
-8-16-15-14-13-12-11-10-9-8-7-6-5-4-3-2-1
-7-15-14-13-12-11-10-9-8-7-6-5-4-3-2-10
-6-14-13-12-11-10-9-8-7-6-5-4-3-2-101
-5-13-12-11-10-9-8-7-6-5-4-3-2-1012
-4-12-11-10-9-8-7-6-5-4-3-2-10123
-3-11-10-9-8-7-6-5-4-3-2-101234
-2-10-9-8-7-6-5-4-3-2-1012345
-1-9-8-7-6-5-4-3-2-10123456
0-8-7-6-5-4-3-2-101234567
1-7-6-5-4-3-2-1012345678
2-6-5-4-3-2-10123456789
3-5-4-3-2-1012345678910
4-4-3-2-101234567891011
5-3-2-10123456789101112
6-2-1012345678910111213
7-101234567891011121314