●変数
変数はアセンブリ言語になく、ここで初めて登場します。C言語が高級言語らしい一面がここにあります。英語ではバリアブル(variable)と呼ばれます。値を記憶する入れ物とかとよく説明され、入るのは「値」です。
数学に出てくる変数とかわりなく
a=100
のaが変数にあたります。
アセンブリ言語では、値をメモリに保存する際、アドレスというものを使って、データの場所を示しました。この場合、どこにどんな値が入っているのか、すべてアドレス値を用いて把握する必要があるわけです。ある処理をするときにだけ、データを保存するメモリを使う場合であっても、どこが使われていないかを考えて、プログラマ自身がデータを保存するエリアを用意する必要があります。
この不便さをC言語では、変数という形を用いて解消しています。変数という形でメモリへアクセスすることでプログラマ自身がアドレス値を把握する必要なく、データの保存が可能になるようになっています。これはコンパイラが自動的にアドレスを割り当て、必要なときに必要なメモリを確保してくれるようになっているからです。また当然、アドレス値が必要であれば必要なときにアドレス値を得ることもできるようになっています。
変数は変数名と呼ばれる識別名を用いています。
a
といったような任意の変数名を使ってデータの出し入れができるようになるわけです。この際、プログラマはアドレスを意識する必要がなくなり、アセンブリ言語開発においても非常に有効であるわけです。
●変数と型指定
C言語での変数には”型”というものがあります。型なんて書くとピンとこないかもしれませんが、英語だと「タイプ(Type)」です。
アセンブリ言語ではメモリを使う場合、プログラマ自身が割り当てを考えるため、8ビット(1バイト)のエリアが必要であれば、8ビット分のメモリを使えます。そして、16ビット分必要であれば、2バイトのエリアを任意に使えます。
しかし、C言語ではメモリを使うのに変数を用いるため、まずコンパイラに変数をメモリ上へ割り当てさせる必要があります。その際、当然どれだけのエリアを必要とするかなどを指定する必要が出てきます。それが型(タイプ)の指定です。
そして、変数を割り当てるためには変数の宣言というものを行います。宣言とは「こういったものを使うよ、その準備をしてください」とコンパイラに伝えるメッセージのようなものです。
この型を指定して、その大きさの変数をメモリ上に確保させるための記述があり、それを”型指定子”と呼びます。この型指定子はかなり複雑であるため、慎重に理解してみてください。
型指定子 | 扱う数の種別 | 精度(型) | サイズ | アセンブリ言語での呼び方 |
char | 整数 | 文字型 | 1バイト | バイト |
int | 整数型 | 2(4)バイト | ワード(ダブルワード) | |
float | 浮動小数点 (実数) |
単精度型 | 4バイト | − |
double | 倍精度型 | 8バイト | − |
表の型指定子がC言語で使われる型指定子の一部です。サイズはその型指定の場合に準備(割り当て)られるメモリエリアのサイズです。最後のアセンブリの呼び方とはサイズが示す大きさをアセンブリ言語で一般的に呼んでいた名前です。1バイトはそのままバイト、2バイトは漢字1字を記憶するために2バイト必要なのでワード(語)そして、4バイトはその倍なのでダブルワードと通称呼ばれています。
charはキャラクタ(文字)型といいます。割り当てられるメモリは1バイト(8ビット)で、整数を扱う型です。
コンピュータでは1バイトで半角1文字を表現できます。初心者の人がこれの意味が分からないって言う人がよくいるため、簡単に説明しておきましょう。
通信で「モールス符号」っていうのは誰もが聞いたことがあるとおもいます。これは「ツーート」という感じの音で通信しますよね。これは長音という長い「ツーー」という音と、短音という
「ト」という短い音(正確には長音は短音の3倍だそうです)の組み合わせで通信します。当然、その組み合わせでどの文字を表すかが決められており、送る側とうける側がその決まりで送信、受信するため通信が成り立っているわけです。
コンピュータでも一緒で、1バイト(8ビット)の数値に一つ一つ、文字が決められています。この数値と文字の対応で有名なものにアスキーコードと呼ばれるものがあります。たとえば、"A"の文字だと41h、"B"は42hといったように数値と半角文字の対応が決められています。結局、メモリ上に41hっていうデータが存在したとして、そのデータを数値と判断するか、それとも文字と判断するかは自由であるわけです。文字と判断すれば、その値に対応した文字と判断して、画面に表示させればよいし、プリンタで印刷してもいいわけで、コンピュータ上では単なる「符号」のようなものなのです。
このように文字と見るか数値と見るかは任意であるいうことが、この後出てくる変数への代入などでも重要になってきます。
さて、以上のことから、char型は文字型といっているんですけど、大きさ的に文字型の大きさと言う意味で、別に文字を入れるためのだけの変数ではないことが重要です。
次にint型を説明しましょう。
intはinteger(インテジャー)の略で「イント」と呼びます。インテジャーとは整数という意味の英文ですから意味はそのままで、整数型と呼ばれます。先ほどcharは文字型といい1バイトで1文字を表せるため特に、文字型と呼んだわけですが、今度はその文字型より大きなエリア(バイト)を使いデータを整数で格納するための型をint型と呼びます。
ここから混乱するかもしれませんがあくまでint型とは文字型と対象に「整数」という名の型を意味するものであることが重要です。上の表でサイズが2(4)となっています。これはint型はコンピュータの種別(ハードウェアの特性)で違うためこのような表記にしています。たとえばOSがMS-DOSなどのPC系の場合、intは2バイト型の整数を表す型になり、UNIX系では4バイトを表します。これは基本的にCPUのレジスタが何ビットのサイズであるかにより変わるためです。
次にfloat型とdouble型です。これまでの型はすべて整数を扱うための型でした。そのため小数演算には使うことができません。そこでfloat型とdouble型という浮動小数点型(または実数型)と呼ばれる型を指定する事で小数点の値が扱える変数を指定できます。
float型は単精度浮動小数点(単精度実数)型、そしてdouble型は倍精度浮動小数点(倍精度実数)型とよばれ、単純にdouble型の方がfloat型の倍の精度で小数点の値を扱えることになります。
当然、倍精度の方が扱うためのメモリサイズが大きいため、どの程度の精度で値を扱うかににより
メモリの確保を行う指定をするわけです。
●型指定子の修飾
ここまでは、型指定子を説明してきましたが、これだけでは不十分です。型指定には「修飾」という形で型指定子を変化させます。こんな話をしているとなんだか、語学の文法のような世界ですが、C言語は本当に言語と言うだけあって、文法の世界です。
修飾子 | 修飾子種別 | サイズ |
short | 精度指定子 | 2バイト |
long | 4バイトもしくはそれ以上 | |
signed | 符号指定子 | 符号付き(正負の整数) |
unsigned | 符号なし(正の整数) |
さて、これらが修飾子の一部です。まず精度指定子からですが、これは本によっては修飾子として扱われていないことも多いのですが、曖昧にして混乱しないためにも、あえてこのワークショップでは修飾子として扱います。
short型は文字型でない整数型(int型など)としては最小のメモリ領域を確保すること指定する修飾子です。
そして、それに対し、long型は、型として最大サイズをメモリ確保の指定をする修飾子です。
かなりややこしいですがたとえば、
short int
とintをshortで修飾した場合、この整数型は文字型を除く最小サイズになるため2バイトのサイズのint型を意味する型指定子になります。
long int
と修飾した場合、この整数型はint型の最大サイズになるため、通常は4バイトのサイズのint型を意味する型指定子になります。
おまけに
long double
という修飾もできるようです。ただ、このばあいの意味は修飾のないdoubleと同じ精度がもしくはそれ以上の精度になる指定ということで、処理系によりどうなるかは不明です。
さて次は、符号指定の修飾子です。
signed char
unsigned char
のように修飾します。これらは整数と扱うcharとintなどに修飾し、実数型には修飾できません。
この場合signed charだと符号付きとなるため、格納する値の大きさは-128〜127までの値、
unsigned charでは符号なしの正の数0〜255までとなります。
当然これらの修飾子は相反しない限り(signed、unsignedとshort、longはそれぞれ相反してるので組み合わせられない)、組み合わせることができますので
unsigned short int
のような型指定子を作ることができます。
●型指定子などの省略
さて、ここまでのところで型指定子と、その修飾子を説明しました。C言語の文献を読んだことがある人なら、
int
とだけ型指定をしているのをよく見たことがあると思います。
ここまでで型指定子と、修飾子の説明で厳密にshortかlongか、signedかunsignedかを指定していたので、ただintってなんのことだかよく分からないと思います。
C言語では型指定しや、修飾子だけ記述したりしたとき、それが何かの略として解釈されるようになっています。
たとえば上のintだけを記述した場合、このint型の精度は処理系によりどうなるのかが決まっています。
一般的にPC系の場合だと
int = short int(2バイト)
と同じと解釈され、またUNIX系だと
int = long int(4バイト)
と同じが解釈されます。
また、符号指定子がない場合、一般的に符号ありのsignedと解釈されるため
char = signed char
int = signed short int(PC系)
int = signed long int(UNIX系)
として解釈されます。そのことから、intだけで型指定をしてもよいわけですが、これが実際にはどのような型になったのかを把握しておかないと、演算等を行うプログラムを作成する際、間違った型を指定してしまい、演算結果が正確に得られなくなってしまいます。
そして、まだまだややこしいのがあります。精度指定の修飾子だけを使って
short
long
といった型指定もできてしまいます。
また、signedとかunsignedだけでも指定でき、その場合は整数型の符号あり、なしを意味します。
結局これらをまとめると、PC系では
short = int = short int = signed int = signed short= signed short int
がすべて等価とみなされます。
また、PC系でlongの場合は
long = long int = signed long = signed long int
となるわけです。
これらのことからただintと書いた場合、処理系によりどのように認識されるかが見えると思います。
ここで、略記された型指定で実際に指定される型の一覧を表にまとめます。
型 | 修飾指定子 | 型指定子 | 指定される型 | |
文字型 | − | char | 符号付き1バイト signed char |
|
signed | char | |||
unsigned | char | 符号なし1バイト unsigned char |
||
整数型 | − | int | 符号付き2バイト signed short int |
|
short | − | |||
signed | int | |||
short | int | |||
signed short | − | |||
signed | − | |||
unsigned | int | 符号なし2バイト unsigned short int |
||
unsigned short | − | |||
unsigned | − | |||
long | − | 符号付き4バイト signed long int |
||
signed long | − | |||
long | int | |||
unsigned long | − | 符号なし4バイト unsigned long int |
||
浮動小数点型 (実数型) |
単精度型 | × | float | 4バイト |
倍精度型 | − | double | 8バイト | |
long | double | 8バイトもしくはそれ以上 |
●変数宣言と初期化子
変数を実際に使用したい場合、まず「宣言」というものを行います。これは先ほどの型指定子を使い、
基本構造: 型指定子 変数名=[初期化子]
実例: int name=0;
のような形で宣言されます。
さて、
int a;
と宣言しただけの時、aにはどんな値が保存されているでしょうか。宣言して新たに変数を作ったので、イメージ的に考えると、新しいから値は0でしょうか?
答えから言うと、この変数の初期値は不定(不明)です。
このような変数宣言のばあい、あくまで変数名をaしてメモリ上にint型(2バイト型)のデータエリアを確保しただけのことです。確保というのは、あるエリアをaのために使うように準備して、他の変数は別のエリアに割り当てられるよう、占領したにすぎないのです。ですから、もしそのエリアになにか数値があっても、当然そのままです。
そこに
a=0;
などと値を入れて初めて任意の数値がはいるわけです。
ですから初期化子を設定すればもちろん宣言と同時に値の設定が行えるわけです。このことはアセンブリ言語の説明からも分かると思います。
変数をどう使うかはプログラムする人が決めることで、コンパイラ側では、言われた通
りの動きをするのが当然です。これを見てもわかるように、アセンブリ言語でもそうですが、メモリ(変数)の使い方はプログラマ自身が決めることなのです。ですから、C言語がアセンブリ言語にものすごく近い言語であることがこれからもわかると思います。
そういったことで、
int a;
printf("%d\n",a);
とすれば、変数aが割り当てられたエリアに、前に入っていた値がわかるわけです。
●変数と代入式
たとえば
char a; /* アドレス2000hに割り当てと仮定 */
char b=100; /* アドレス2001hに割り当てと仮定 */
a=b;
という式があるとこれは代入を行う式ですよね。
イメージ的に僕たちは、「aにbの値を入れた」と考えます。
しかし、内部では「変数bの割り当てられたメモリのデータを変数aの割り当てられたエリアへコピーした」というのが実際の動きです。
上の式ではアドレスを使っていませんが、実際には
*((char *)0x2000))=*((char *)0x2001);
のように当然アドレスをつかって値の移動を行ってます。(上の式はまだ説明していないので、複雑であるということだけ理解してください)
でもわざわざアドレスを求めなくても変数の名前だけで代入できれば面倒くさくないわけですから、a=b;のように書けばいいわけです。
結局、扱うデータや変数などはすべてメモリ上でのやりとりが行われる訳で、すべてにアドレス表記させて扱うこともC言語では可能なのですが、それではアセンブリ言語になってしまうわけですよね。
逆に考えると、そこまで、アセンブリ言語に近い表記でも書けることが、C言語がポータブルアセンブラと呼ばれているゆえんでもあるのです。
ちなみに、ここで/* アドレス2000hに割り当てと仮定 */のように記述されていますが、これはコメントを入れるための記号で、ソースに/* */のように記述することでコンパイル時に無視してくれる文章をソースファイルに書くことができます。