lecture 7
(本稿は Ellis Horowitz, Fundamentals of programming languages によるところが多い)
データ型(data type)とは、対象(object)とその上で許される演算の組み合わせである。 データ型そのものはいわば型紙であって実質的な存在ではない(プログラム言語でいえば、 データ型は基本的にはコンパイラが用いるものである。実行プログラムは、もう意識しないか (目的プログラムに反映されているため)せいぜい、実行時チェックのため参照するだけである)。 演算は実際には、その対象のインスタンス(instance)に対して施されるものであって、 新たなインスタンスを作成したり、廃棄したり、変更したり、するものである。
命令型言語には、通常、組み込みデータ型(built-in data types)がある。 言語がはじめから用意しているものである。整数型とか浮動小数点型といった ものであり、一般には、計算機のハードウェア実装に深く関わっている(すべての組み込みデータ型がそうだと 言っているわけではない)。そのため、実行速度は十分に考慮されているが、 (計算機のハードウェアの違いを超えた)汎用性に欠けるきらいがある。最近は MPU の defacto standard 化と これまでの試行錯誤や IEEE 等の標準化努力の結果、いくつかのパターンに分けられるようになってきている。
なお,この講義のように情報科学系の学生向けの講義ではあまり言及しない事実に「基本的なデータ型には しばしば10進数が用意されている」ということがある.ビジネスの世界では,100万円を日歩1厘の利子で借りた 時の一日の利息が 99.99 円では困ることもあるのである.
過去の主な命令型言語の組み込みデータ型を記す
FORTRAN IV FORTRAN77 Algol60 Pascal C --------------------------------------------------------------------------------------- INTEGER INTEGER integer integer int (INTEGER*32, INTEGER*2, LONG) long REAL REAL real real float LOGICAL LOGICAL Boolean Boolean Boolean CHARACTER char char DOUBLE DOUBLE double PRECISION PRECISION COMPLEX COMPLEX
上でも述べたように、こうした組み込みデータ型の問題点は、桁数、精度、指数の範囲、 オーバフローの取り扱い、桁落ちの取り扱い、符号の取り扱い等にハードウェア依存の部分があるため、 言語間の単純な書き換えでは同一の結果が得られないことがあるのみでなく、 同一言語であっても実行する計算機を変えると結果が変わるということもある。
通常一つの演算子は対象とするデータのデータ型は一組に特定して定義される。 しかし、少なくとも数値演算に関してはこれは非常に不便なため(integer 同士の加算と real 同士の加算の 記号が異なるといった事態は受け入れられるものではない)。 命令型の言語の数値データのデータ型に関しては原則として一つの演算子が複数の型に対する演算を 表現するようになっている。これを多型(polymorphic)又はオーバローディング(overloading)という。 オブジェクト指向言語では、メソッド(サブルーチンと考えてよい)を多型とすることが許されているし、 Prolog では自動的にすべての述語(サブルーチンと考えてよい)が多型となっている。 Ada ではもっと自由に多型を用いることができる。
組み込みデータ型の間の数値演算の多型はたいていは単純で理解しやすい。 二つの演算子の型が同一であればその型で計算される(なお、演算結果が同じ型になるものと、 より有効桁の長い型になるものとがある)。両者の型が同一でないときには、 表現できる数値範囲が広い方に変換されて計算されると理解すればよいからである(下表参照)。 例外は、固定小数点型数やそれ類似の表現(byte 等)で、符号ビットの扱いが絡む場合や、 固定小数点型から浮動小数点型に変換する場合で、有効桁を少なくせざるを得ない場合である (64 ビット固定小数点数から 64 ビット浮動小数点数へと変換しようとすると有効桁が減少する)。
+ integer real double complex ---------------------------------------------------------------------------------------- integer integer real double complex real real real double complex double double double double complex complex comples complex complex complex
文字や文字列の取り扱いは言語によって大きく異なっている。 ここでは述べる余裕がないので省略するが使用する時には言語仕様を 読む必要がある(山勘が働かない)。
ポインタ型は Fortran, Cobol, Algol 等では存在しなかった。 PL/I で導入され、Pascal や Ada で洗練された。 ポインタ型は非常に強力な機能(非常に複雑なしかし便利なデータ構造を構成することができる) であるが、同時に訳の分からないエラーや破壊的な結果を引き起こすエラーを埋め込む原因ともなりやすい。 この反省に立って、Java では参照型としてより制限された形でポインタ型の導入を図った。 なお、LISP のデータ構造はある意味でポインタの集まりであるが、「手で」ポインタを付け替えるような 無茶をしない限り破壊的な結果を引き起こすことはない。
ポインタ型の変数に許された演算は、代入、比較、dereferencing である。
但し、C においては、加減算や increment/decrement が許されている。
Pascal を例に取り上げてみる。
この型を持った変数の宣言は、例えば、次の様になる。
type nextnode = ↑node ;
node = record
number: integer;
next: nextnode
end ;
この後、 var x, y: nextnode;
new( x )
を実行したものとする。
この時、 x
に関連して次の様な表記がある。
x
: 型は nextnode, 値( contents(x)
)は node 型の変数へのポインタである。
初期値は、通常、 nil
と呼ばれる特殊な値となる(未定義ではなく)
x↑
: 型は node, 値( contents(x↑)
)はある構造体である。
x↑.next
: 型は nextnode, 値( contents(x↑)
)は再びnode 型の変数へのポインタである。
従って、 new( x↑.next )
を実行すれば、再び node 型変数の領域が生成され、それへのポインタがここに
代入される。
C はもともと,OS 等を効率よく作成するための高級言語として設計されただけあって,細かいところまで 操作できるようになっている.ポインタ型はその良い例であり,他の言語にない融通性を持っている. しかし,その表現はわかりやすいとはいえず,誤りの原因となり易い. いくつか例を挙げておく.
char *pchar;
int *pint;
strut x *pstrutx;
となり,Pascal における宣言の仕方( var pchar: ↑char; pint: ↑int; pstrutx: ↑x
)と
逆となっている.尤も,利用する時は *pchr
や *pint
となるので,宣言文との
字面上での整合性はよい.なお,ある構造体に「型名」(たとえば, x
)を与えても,
ポインタ型変数の宣言で引用するときには, strut x
とかかなければならない.
pchar + 1
は, pchar
の値(物理的に主記憶装置上の番地を値とする)
に 1 を加えたものを表すが, pint + 1
は,(int 型変数が2バイトを占めるなら) pint
の値に
2 を加えたものを表し, pstrutx + 1
は,(構造体 x が lx バイトを占めるなら) pstrutx
の値に
lx を加えたものを表す.
a[i]
は,
a
が一次元配列型の変数であれば a
の i
番目の配列要素の値を現すが,
a
が二次元配列型の変数であれば a [i][0]
の先頭番地を現すポインタ型変数となっている.
Pascal で導入され ANSI C から C にも取り入れられた型である。 本稿では省略する。
命令型言語においては、少なくとも二種類の方法で、二個以上のデータからなる集団を一つに纏めて扱うことができる。 一つは配列型であり、もう一つは record 型である。 集団を構成する要素がすべて同一の型であって、ある番号を用いて各要素を参照することができるデータ型が、配列型である。 集団を構成する要素が必ずしも同一の型ではない時に、名前を用いて各要素を参照することができるデータ型が、record 型である。
配列を特徴づけるものは、名称、次元、各要素の型、添え字集合の型と値の範囲である。 添え字集合は、普通、連続する整数(必ずしも正整数とは限らない。しかし、0以上の整数か1以上の整数とする ことが最も普通に行われている)の集合である。 添え字集合が整数であれば、各次元は、添え字値の下限と上限を指定することにより定義される。 配列の添え字を表現するのに、Fortran, PL/I, Ada では丸括弧(parenthesis)を用いているが、 Algol60, Pascal, C 等では角括弧(square bracket)が用いられる。 後者には、関数呼び出しとの混同がないという利点がある。
配列の大きさは Fortran のように静的な領域割付を行う言語では、固定値にするのが当然であり、 従って、添え字値の範囲も固定値とした。 一方、Algol60 のように動的割付を基本とする言語では、変数を許すのも納得できる故、 添え字値の範囲も動的となった。 Pascal においては、Wirth が熟考の末、添え字値の範囲を動的にすることによって得られるメリットとそれによって 失われる効率とを天秤にかけ、結局動的な範囲指定を許さなかった。 しかし、後年これは、問題となった。それは、配列を引数として受け渡ししたい場合である。 サブルーチン側は、どんな添え字範囲であっても動くプログラムとしたい。 そうでないとすれば、ライブラリ化することができず、利用しようと思う毎にコンパイルしないといけないからである。 そこで、実際に Pascal の実装では、動的に添え字値範囲を定義できるものが現れた。 なお、Fortran においても、``adjustabel dimension'' というものがあり、引数として配列を受け渡す時には、 (Fortran においては下限は常に1なので、上限のみだるが)添え字値範囲を呼び出しが我が指定できるようになっている (実際には、Fortran コンパイラの多くは配列の上限・下限尾チェックを行わないため、この adjustabel dimension がなくとも 一次元配列であれば、困らない)。
現在の計算機の主記憶は、例外なく一次元配列相当の番地が付与されている。
従って、2次元以上の配列をここに割り付けるためには、何らかの一次元化をしなければならない。
その方法には大きく分けて二つの方法がある(他にも方法は考えられるが大して意味はない)。
一つは、``row major order'' と呼ばれる方法であり、例えば、次の順番で並ぶ。
これは、Algol60, Pascal, C 等で用いられている方法である。
もう一つは、``column major order'' と呼ばれる方法であり、例えば、次の順番で並ぶ。
a[0,0], a[0,1],..., a[0,6], a[1,0], a[1,1],..., a[1,6], ......... a[8,0],...,a[8,6]
これは Fortran で用いられている方法である( Fortran であれば、
a[0,0], a[1,0],..., a[8,0], a[0,1], a[1,1],..., a[8,1], ......... a[0,6],...,a[8,6]
と書くべきであろう)。
A(1,1), A(2,1),..., A(9,1), A(1,2), A(2,2),..., A(9,2), ......... A(1,7),....,A(9,7)
配列型は現在のプログラム言語の標準的な部品であるにも関わらず、言語毎に異なる点が非常に多い。 例えば、
レコードの概念は COBOL にまで溯る。そこでは、レコードは伝票の概念の一般化である。 例えば、
01 URIKAKEKIN-DENPYOU.
10 HIZUKE PIC X(4).
1O KOKYAKU.
20 KOKYAKU-KOUDO PIC X(6).
20 NAMAE PIC X(12).
10 URIKAKE-KIN PIC S9(8).
といった形で記述される。Algol60 や Fortran IV においては導入されなかった。
PL/I においては structure と呼び
DECLARE 1 A,
2 B,
3 C CHARACTER(3),
3 D FIXED DECIMAL(2),
2 E FIXED DECIAML(5);
といった形で記述される。Pascal においては、記述は洗練され、
type stock =
record
name: array [1..30] of char;
price: array [1..365] of real;
dividend: real;
volume: array [1..365] of integer;
exchange: (nyse, amex, nasdaq)
end
といった形になった。このように、言語によって記述法は(配列と同様に)さまざまである。
レコードの要素の指定の方法もいろいろある.代表的には次の2種類がある.
上記の例で ibm
が stock
型の変数であるとする.
name( ibm )
や price( ibm ) [45]
のように関数呼び出しの形で記す方法
ibm.name
や ibm.price[45]
のように修飾語を前方(左方)に記す方法