kentsu.dat

何かその時の興味でいろいろする人。最近はScala使ってる。

C言語 データ型、式、演算子

2章 データ型、式、演算子

前回の1章でざっくりとした入門を終えたので、今度はひとつひとつ掘り下げていきます。
はじめは、大抵どの入門書でもはじめに扱うデータ型、式、演算子というものです。

まず宣言と演算子を交えて…
宣言は変数を並べて、型を定義し、時には初期値を決める為に
演算子は変数に対して何を行うか、さらには式で変数や定数を結合し新しい値をもたせるのに使われる。

一方でそれらのオブジェクトが持つことが出来る値の集合と、どのような演算ができるかという事を示すのが型である


今回使用している、プログラミング言語CANSI規格では基本的な型と式に変更と追加が加えられ
型の中でも、特にすべての整数型にはsigned、unsignedという型があり符号なし定数や16進文字定数の記法ができた
浮動小数点型では、単精度での演算も可能で拡張精度のためのlong double型も存在する。
文字配列、つまり文字列の中でも特に定数はコンパイル時に連結が可能となった
オブジェクトではconstを付加することで変更不可に出来たり、算術間の自動変換規則ではより豊富なデータ型を扱える様に強化されている。

2.1 変数名

これは誰もが知っていることかもしれないが一応解説。
まず変数名と記号定数名にはいくつかの規則がある。
それは名前の1文字目は英字でないといけないということで、下線は英字に含まれる
そして、伝統的に変数名は小文字で
定数名、特に記号定数は大文字で記述することになっている(そうしなくても良い)

それ以外に、内部変数名の長さを31文字以内にすることや、言語で定義されているキーワードを使用しない
といったものがある
なので、変数名はその変数自体が持つ意味合いやそれに関係する名前にするのがいいと言われている
特に局所変数、内部変数には短い名前を、外部変数にはそれなりに長い名前をつけるのがいいとされている

2.2 データ型とサイズ

C言語には次のような数の基本的なデータ型があるだけ

データ型 意味合い
char 1バイト、ローカルな文字セット内に1文字保持する
int 整数、通常ホスト計算機の自然な整数サイズ
float 単精度浮動小数点数
double 倍精度浮動小数点数

これらの基本型には修飾子をつけることが可能で整数にはshortとlongというものがあり次の様に適応できる

short int c1;
long int c2;

こうした宣言ではintを省略できる。というか実際には省略されることがおおい
この意図は可能な限りshortとlongによって異なる長さの整数を扱える様にするべきというところにある
intは通常、特定の計算機にとって自然な大きさが取られていてshortは16bit、longは32bitであることが一般的である
具体的にいうとintは16bitか32bitのどちらかである。その点がハードによって違う
それらを考慮してshortはintより大きくてはいけない
またintはlongより長くてはいけないということがある

整数にはlong,shortがあるがcharにはsigned,unsignedがある
charが8bitの長さだとすると
signedは符号ありを示すもので-128から127の間の範囲である
unsignedは符号なし、つまり値が必ず0か正の数で0~255までの範囲である。これは印字可能な文字と関係があり
具体的には印字可能な文字を数値で表すと必ず正の整数なのでその点に関係する

一方、浮動小数点数にはlong doubleという拡張精度の浮動小数点数を表すものがある。
整数と同様にfloat,double,long doubleの長さは同じであることもあれば異なることもある

2.3 定数

1234のような整数定数はintとして扱われる。123456789Lのように末尾にLまたは小文字のlをつけるとそれはlongになる
それに加え大きすぎてintでは格納出来ない数字もlongとして扱われるがそのような使い方はおすすめしない
一方、符号なしのunsignedの場合定数には末尾にU,uをつけるため
unsigned long型だった場合、UL,ulを末尾につけるなどある

浮動小数点数は小数点(123.4)や指数部(1e-2)あるいはその両方を含むものであり
それらに接尾子が無い限りdoubleとして扱われf,Fを末尾につけるとfloatになる
さらにl,Lをつけた場合はlong doubleとして扱われる


整数では基本的に10進数を扱うことが多いが、8進数や16進数を標準で扱える
8進数の場合、先頭の数字が0
16進数の場合は先頭が0xもしくは0Xがつく

例として10進数で31を表現するなら
8進数で037
16進数で0x1fか0X1fと表現する
この時unsignedやlongを型にしているならulやULなどの接尾子を付加できる

次に文字定数について、文字定数は宣言するときにシングルクォーテーションで対象を囲む
例えば、Aという文字を文字定数として扱いたいなら'A'とする。

そして文字定数、文字とは数値であるため'A'を格納する定数は実質的に65という数値を格納している。
なのでC言語には数値型しかないと言われる。
またこの性質を使うと上の例で65を定義するときに'A'と書くとそれだけ特定の値と独立しコードが見やすくなる
ただむやみに、その性質を乱用するべきではない
そして然るべき時に使うことで、文字とは数値と同様の意味合いを持つ


この性質が一番よく使われるときは文字と文字の比較の時で、文字または文字列においてある種の文字は\nの様にエスケープ系列で表現される
これらは2文字のように見えるがそれが表すのは一文字である

文字を定数として定義するときはシングルクォートで囲んだが文字列を定数として定義するときはダブルクォーテーションで囲む

'A'//文字
"A"//文字列

ダブルクォーテーションで囲まれたものは文字列として扱われるため、例え1文字であっても文字列として扱われる


またこれら定数は数値と同様にdefineを用いて定数化することも出来る
定数式はコンパイル時に評価されるため以下の様に使用できる

#define MAX 100;
char str[MAX+1];

ここまでは簡単、でもこれまで紹介したそれらより少しめんどくさい文字列
文字列は技術的に言うと、各要素が1文字の配列である
また文字列の最後にはヌル文字('\0')という文字列の終わりを示す文字が内部表現として追加されるので物理的な容量としてダブルクォーテーションで囲まれたものより1多い
これは文字列の長さに制限がないことを示すが、プログラム上でその長さを求めるためには最後までスキャンする必要がある

その例として、文字列の長さを返すstrlen関数を例に上げる
strlen関数はこのようになっている

int strlen(char s[]){
    int i;
    while(s[i] != '\0'){  //null文字でないなら
        ++i;
    }
    return i;
}

そもそもstrlen関数とはstring.hという標準ヘッダで制限されている関数である
そしてこの関数は内部表現として追加されたnull文字を除く文字列の長さを返している

この他に列挙定数というものがあり、enumを用いて以下のように宣言する

enum boolean{ YES,NO }

この場合、YES,NOの値を明示的に宣言していないので
YES = 0, NO = 1という値が暗黙的に宣言されている

それを利用して以下の様に書くことも出来る

enum months{
    JAN = 1,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
}

この場合先頭の定数が1という値を保持しているのでFEB = 2,MAR = 3というようになる
また列挙では名前が異なっている必要があるが、値は異なる必要は無い

列挙は名前と定数値を結びつけるのに便利で、defineの代用として使用もできるし
上のコードの様に、値が自動生成されるという点でも利点がある。

2.4 宣言

すべての変数は宣言を必要とする。
宣言は同じ型をまとめて1行で宣言したり、すべてバラバラに宣言したりできる

int i,max,min;
char str[MAX];

int i;
int max;
int min;
chsr str[MAX];
//上下で同じ意味

前者はコードを簡潔に出来る
後者はコメントなどが挿入しやすく、変更も容易である
また宣言と初期化を同時に行うことも可能
ただ初期は通例として一回行われるとき、式に定数を含むのが望ましい

さらに変数にconstという修飾子をつけることで変更不可な変数を生成することが出来る

const int i = 10;
i = 11;//これはできない

変更不可というよりは初期化後に再代入できないため、関数の引数にconstをつけることもでき配列にも適応出来る

2.5 算術演算子

C言語の算術演算子に2種類あり

  1. ,-,*,/の二項演算子

%のモジュロ演算子がある
これらには実行優先度というものがあり+,-は算術演算子の中では同じ優先度であるがその他の算術演算子より優先度が低い
ざっくり言えば数学の四則演算と同じ優先度になっている

少し注意が必要なのが/,%の2つ

まずモジュロ演算子の%は浮動小数点に適応できない
さらに/は小数が切り捨てされるがそれらの方向は機種に依存する、負の演算についても同様
加えてオーバーフローやアンダーフローも機種に依存する

2.6 関係演算子と論理演算子

まず関係演算子として
すべて優先順位が等しい<,<= ,> ,>=があるこれは右辺左辺の大小を評価するものである
一つ下の優先順位には等値演算子と呼ばれる != ,==がある

さらに関係演算子は算術演算子よりも優先度が低い。これには注意が必要である

より興味深いのは論理演算子である&&,||である
これら論理演算子で結び付いている式は、左から右へ評価が行われその評価は対象の式が真か偽か判定出来た時点で終了する。

なので1章で示したgetline関数を例に示すと

for(i = 0; i < lim-1 && (c = getchar()) != '\n' && c != EOF; i++){
}

この処理ではまず i < lim -1が判定されなければならない。なぜならこの評価が偽であるなら先に進んで文字を入力しては行けないからだ
同様に!= EOFをgetcharの前に呼び出してはいけない
一方、&&の優先順位は||より高く、これらは関係演算子や等値演算子より低い


最後に単項の否定演算子!は0ではない真の被演算数の値を0にし
0つまり偽の被演算数の値を1にする

if(!valid)

というのは、つまりは

if(valid == 0)

とおなじであるということだ

2.7 型変換

異なる型の被演算数を一緒に計算すると、少数の規則によって共通の型に変換される
一般的には浮動小数点と整数を一緒に計算するときに、整数を浮動小数点に変換したり
それ以外で情報が欠損しない範囲でより広い型に変換されたりすることがある
ただし、情報の欠損があるとしても大きい型の値を狭い型へ変換や代入を行うことは警告が出るかもしれないが文法違反ではない

まずcharから
charは単に小さい整数だから算術式の中で自由に扱える
これは文字変換に対し、ある種の柔軟性を生む

例として文字を整数に変換するatoi関数の単純なものを紹介する

int atoi(char s[]){
    int i,n;
    n=0;
    for(i = 0; s[i] >= '0' && s[i] <= '9';++i){
        n = 10*n+(s[i] - '0');
    }
    return n;
}

この例でs[i] - '0'が計算可能なのは文字が数値、厳密には整数だからである

charからintへの変換例として、ASCII文字セットに対してのみ1文字を小文字に変換する関数lowerがある

int lower(int c){
    if(c >= 'A' && c <= 'Z'){
        return c+'a'-'A';
    }else{
        return c;
    }
}

小文字は数値として一定の距離内にあり、かつ各文字は連続していて、A〜Zの間には文字しかなく
すべて数値として扱えるためASCII文字セットにのみ適応できる


ところでctype.hという標準ヘッダをしっているだろうか
これらに含まれる関数は文字セットとは独立した形で、検査変換などを行う関数の一群で
例えばtolower(c)はcが大文字であればcの小文字の値を返すからtolowerはlower関数の移植可能な代替版となる
またisdigit(c)という関数は、 c >= '0' && c <= '9'という検査の代替として使えるため今後はそれをつかっていく


次に文字から整数の変換で起こる微妙な点についてまとめてみる
それは文字を整数に変換した時にその数がsigned,unsignedであるか、つまり符号があるかないかという点である
これは計算機のアーキテクチャによって違いがでるもののC言語の定義では、すべて正の数であることが保証されている
しかし文字変数のビットパターンが正か負かという判断は計算機、ハードウェアに左右されるのでsigned,unsignedを明記するのがいいとされている


ここまで少し書いてきた型の暗黙的変換は大抵プログラマーの予想どおりの変換になる
式の中に、異なる型が混ざっているなら低い型は高い型に変換され、結果として式が返す結果は高い型となる
この変換にはまだ規則があるがそれは後述、unsignedの被演算数がなければ大まかな規則だけ知っていれば十分


基本的に暗黙的な型変換は
被演算数がlong doubleを含む場合は他方をlong doubleに変換し
それではなく被演算数がdoubleを含むなら他方をdoubleに
それではなく被演算数がfloatを含むならなら他方をfloatに
そうでないならcharとshortをintに変換する
そして、被演算数がlongを含むなら他方をlongに変換する

ここで注意すべきことはfloatが自動的にdoubleに変換されることはないこと
しかし、一般的にmath.hにあるような数学関数で倍精度が使われる
floatを使う理由は大きな配列の容量を減らすことであったり、計算時間を短くすることにある


ところでunsignedな被演算数が入ってくると変換の規則がもっと複雑になる。
問題はsigned,unsignedの比較が様々な整数の型に影響されるため機種に依存することである。
例として、
int->16bit
long->32bit
とすると

  • 1L<1U

となる
何故ならintである1Uがsigned longに格上げされるためである
しかし
1UL<-1U
も成立していまう
これは、-1Lがunsigned long に格上げされるためである

変換は代入時にも行われ、右辺の値が左辺の型に変換され、それが結果の型となる
ここまでで述べてきた様に、文字は符号拡張されたり、されなかったりして整数に変換される
より長い整数は余分な上位ビットが捨てられて、より短い整数に変換される
つまり以下の例で

int i;
char c;
i = c;
c = i;

cの値は不変になる
これは符号拡張の有無に関係なく言えることである。
しかし、代入の順序を逆にすると情報の欠損が起こる場合がある
ちなみにfloatからintへの変換では、小数部が切り捨てになり、doubleからfloatではまるめられるか切り捨てかは機種に依存する

最後にキャストについて
キャストとは明示的な型変換(強制的でもある)で、任意の式に適応できる
例えば、math.hに含まれるsqrt関数では引数にdoubleを仮定しているからnを整数とするなら

sqrt((double)n);

とすればdoubleに変換され引数として渡される
この時、元の変数や値が変わることはない
また実際はある程度の大きさまでは、暗黙的型変換が適応される。

2.8 インクリメントとデクリメントの演算子

C言語にはインクリメントとデクリメント演算子として、ここまではっきりとは紹介してこなかった演算子がある
まず、被演算数に1加える++のインクリメント演算子
そして、被演算数に-1するデクリメント演算子
実際には、既に使用していて

if(c == '\n'){
    ++nl;
}

の様な形で使用していた
見慣れない点はこれらの演算子が変数の前にある、前置演算子(++n,--n)の場合と変数の後ろにある後置演算子(n++,n--)の場合がある点で
例で言えば、どちらともnの値を±1できるが前置演算子ではnを先に±1し、後置演算子ではnを後で±1する
なのでこうなる

int x;
int y;
int n = 5;
int m = 5;

x = n++;
y = ++m;

上の例で、xにはnの5が代入されてからnは6になる
またyには先にmが+1されて6が代入されmも6となる

2.9 ビットごとの論理演算子

C言語には6つのビット処理演算子を持っていて、これはunsigned,signedに関らず整数にのみ適応できる。

演算子 意味
& ビットごとのAND
ビットごとのOR
^ ビットごとの排他的OR
<< 左シフト
>> 右シフト
~ 1の補数(単項演算子)

はてな記法上の問題でビット毎のORについてはかけなかったが「|」の記号を用いる
まず&演算子に付いてはビットをマスクするのによく使われ

n = n & 0117;

とあるなら、低位7ビット以外を0にする

演算子はビットをオンにする役目を持ち

x = x | SET_ON;

とあるならSET_ON中で1になってるビットに対応するxのビットが1になる
ビットごとの排他的ORの^演算子では、対応するビットが異なる時にそのビットは1にされ
同じビットであるときは0にセットされる

ビットごとの演算子&,|は論理演算子&&,||と違うことに注意しなければならない
それは後者が左から右へ評価していくためである
なので x&yが0なら x&&yは1となる

シフト演算子は左被演算数と右被演算数で指定したビット数だけシフトするのに使われる

x << 2;

とあるならxを左へ2ビットシフトすることを意味し、シフトし出来た空白は0で埋められるため
この場合、左から2ビットが0になる

単項演算子~は1の補数を求めるのに使用される
これは各ビットが1なら0に0なら1に反転させるからである。
例えば

x = x ~ 0117;

とあるなら
xの下6ビットを0にする意味をもつ

2.10 代入演算子と式

次の様な式

i = i + 2;

では左辺が右辺に再び現れる
これは次の様にまとめて書くことが出来る

i += 2;

このような+=を代入演算子という
大抵の2項演算子はそれに対応する代入演算子をもつ

しかしこれには注意が必要である
以下の例で

x += y+1;

とするとこれはどう解釈できるだろうか

x = x + (y+1);

だろうか、それとも

x = x + y + 1;

正解は前者である
またこの代入演算子は様々な式に適応できて
例えば以下のようなfor文もかける

int i;
for(i = 0; i < 100; i+=2){
}

こうあるならiを+1ではなく+2しながらループを回すことができる

2.11 条件式

以下のプログラムは最大値を代入するものである

if(b < a){
    max = a;
}else{
    max = b;
}

これは、3項演算子を用いて以下の様に書ける

x = (b < a)? a : b;

この二つの文は全く同じことをしている
またこれは、必ずしも代入する必要はなくprintfなど関数に値を渡す場合にもしようでき

//最大値を表示する
printf("%d\n",(b < a)? a : b);

とも書ける