プログラミングをすると、ほとんどの言語の配列などのインデックスを0から始まります。人間の立場では、数字を1からではなく0から数えるのが不自然だと思われます。
今回のポストでは、コンピュータが0から数字を数えるときに生じる利点についてご説明します。
インデックスを 0 から始める場合と 1 から始まる場合をそれぞれ C#とPascal で実現して違いを見てみましょう。ほとんどの言語が0からインデックスを開始するのとは異なり、Pascalは1から始める言語です。
0から始めるインデックスを使用する場合、1次元配列のインデックスを2次元配列のインデックスに変換することは、次のように行われます。
int[] oneDimensional = new int[100];int[,] twoDimensional = new int[10, 10];for (int i = 0; i < 100; i++){twoDimensional[i / 10, i % 10] = oneDimensional[i];}
2次元配列のインデックスを[x, y]としたとき、yは 1次元配列のインデックスが 10が増加するたびに1が増加されます。xの場合は0から9まで繰り返すことになります。
1次元-i: [ 0] [ 1] [ 2] [ 3] [ 4] [ 5] [ 6] [ 7] [ 8] [ 9] [10]2次元-x: [ 0] [ 1] [ 2] [ 3] [ 4] [ 5] [ 6] [ 7] [ 8] [ 9] [ 0]==> i % 102次元-y: [ 0] [ 0] [ 0] [ 0] [ 0] [ 0] [ 0] [ 0] [ 0] [ 0] [ 1]==> 1 / 10
1から始まるインデックスを使用する場合、1次元配列のインデックスを2次元配列のインデックスに変換するのは少し複雑です。追加の演算が必要になります。以下のPascalの例を見てみましょう。
vari: Integer;oneDimensional: array[1..100] of Integer;twoDimensional: array[1..10, 1..10] of Integer;beginfor i := 1 to 100 dotwoDimensional[(i - 1) div 10 + 1, (i - 1) mod 10 + 1] := oneDimensional[i];end.
Pascalの整数除算はdiv演算子を使用し、モジュロはmod演算子を使用します。下の図では都合上C#と同様に表現しています。
1次元-i: [ 1] [ 2] [ 3] [ 4] [ 5] [ 6] [ 7] [ 8] [ 9] [10] [11]2次元-x: [ 1] [ 2] [ 3] [ 4] [ 5] [ 6] [ 7] [ 8] [ 9] [10] [ 1] ==> (i-1)%10 + 12次元-y: [ 1] [ 1] [ 1] [ 1] [ 1] [ 1] [ 1] [ 1] [ 1] [ 1] [ 2] ==> (i-1)/10 + 1
0から始まるインデックスを使用する場合、2次元配列のインデックスを1次元配列のインデックスに変換することは、次のように行われます。
int[,] twoDimensional = new int[10, 10];int[] oneDimensional = new int[100];for (int i = 0; i < 10; i++) {for (int j = 0; j < 10; j++) {oneDimensional[i * 10 + j] = twoDimensional[i, j];}}
1から始まるインデックスを使用する場合、2次元配列のインデックスを1次元配列のインデックスに変換することは、次のように行われます。
vari, j: Integer;twoDimensional: array[1..10, 1..10] of Integer;oneDimensional: array[1..100] of Integer;beginfor i := 1 to 10 dofor j := 1 to 10 dooneDimensional[(i-1) * 10 + j] := twoDimensional[i, j];end.
この例からわかるように、1次元配列と2次元配列の切り替えは、0-based 索引付けがより簡単で直感的です。
一般的な状況では、プログラマは配列の次元遷移を気にする必要があることはあまりありません。しかし、プログラムが運営される内部状況では、これらの演算が頻繁に起こっています。したがって、0からインデックス開始するのがコンピュータの立場ではより自然的なことになります。
配列やポインタの基準点から各要素がどれだけ離れているかを把握することは、頻繁によく発生する演算です。特にポインタでは、特定の位置のデータに直接アクセスしたい場合が多いです。このような場合はoffsetを使用します。offsetは、ベースアドレスからどれだけ離れているかを示す値です。
ここで、offsetが0または1で始まる場合を比較して説明します。
オフセットが1で始まる場合は、最初のバイト(または要素)にアクセスするために-1を指定する必要があります。これは、offset 値が実際に最初のバイトより 1 つ上にあるためです。
char *data = (char *)malloc(1024);// 最初のデータという意味で1を使用int offset = 1;// 最初のバイトにアクセスするためにoffsetから1を引いてください。char *theFirstByte = data + offset - 1;
オフセットが0で始まる場合は、最初のバイトに直接アクセスできます。これは、追加の操作なしでポインタがすでにデータの始点を指しているためです。
char *data = (char *)malloc(1024);// 指す位置をすぐに参照するという意味int offset = 0;// 最初のバイトに直接アクセスします。char *theFirstByte = data + offset;
上記の2つの例を比較すると、offsetが0の場合がはるかに直感的で簡単であることがわかります。 実際に多くのプログラミング言語とシステムは0ベースのインデックスを使用します。これは、メモリアドレスの操作を簡素化し、コードの読みやすさを向上させるためです。
今回は、範囲を計算する場合の違いを以下のように2つのケースをそれぞれ見ながら、0から始めたときと1から始めたときの違いを説明します。
(a) 0 ≤ i < 10(b) 1 ≤ i ≤ 10
配列の最初の要素のメモリアドレスをベースアドレスと考えると、0 ≤ i < 10の場合、追加の操作なしで直接その要素にアクセスできます。1 ≤ i ≤ 10では、メモリアドレスを計算するときに1を引く必要がある追加の操作が必要です。
0 ≤ i < 10 の場合、配列または文字列の先頭から特定の位置までスライスするときに、開始インデックスを指定する必要はありません。1≤i≤10では、開始位置の1を常に指定する必要があります。
0 ≤ i < 10と10 ≤ i < 20のように、連続した範囲は前の範囲の最後の値からすぐに始まります。一方、1 ≤ i ≤ 10 の後の連続範囲は11 ≤ i ≤ 20となり、開始値と終了値の両方を調整する必要があります。
要約すると、0≦i<10のような0から始まる索引付けは、プログラミングにおける演算の簡潔さ、直感性、及びメモリアクセスの効率を提供します。一方、1 ≤ i ≤ 10などの1から始まる索引付けは、追加の操作または明示的な初期化を必要とし、連続した範囲表現が複雑になる可能性があります。