ビット演算について
補足資料です。
ビット演算
このページは、講義資料ではありません。講義では最低限覚えなければならないことを教えていますが、こちらで紹介する物は必ずしも覚えなくてもよい、しかし使えるとちょっと役に立つ機能を紹介します。マニア向け。
ビット演算は、整数型変数の各ビットに対して、論理演算やシフト演算などをします。ビット演算ができるのは符号付整数型、符号なし整数型のオペランドに対してのみであり、浮動小数点型などのオペランドには使えません。
コンピュータで使われる記憶素子は2通りの値をとります。それをn個並べることで、2n通りの情報を表現することができます。この記憶素子1個分の情報量をビット(bit)といいます。また、このビットをいくらかまとめた同次元の単位としてバイト(byte)があります。ビットとバイトの間の関係は、実際には定義されていませんが、多くのコンピュータでは1バイト=8ビットとして扱っています。このビットを使って数値を表現する方法として2進数があります。
ビット演算は、整数型変数の各ビットに対して、論理演算やシフト演算などをします。ビット演算ができるのは符号付整数型、符号なし整数型のオペランドに対してのみであり、浮動小数点型などのオペランドには使えません。
コンピュータで使われる記憶素子は2通りの値をとります。それをn個並べることで、2n通りの情報を表現することができます。この記憶素子1個分の情報量をビット(bit)といいます。また、このビットをいくらかまとめた同次元の単位としてバイト(byte)があります。ビットとバイトの間の関係は、実際には定義されていませんが、多くのコンピュータでは1バイト=8ビットとして扱っています。このビットを使って数値を表現する方法として2進数があります。
C言語では、普通に数字列を書くと10進数として解釈されます。数字列の先頭に次の文字列を添えることで、次の基数で数値を表現することができます。
0b 2進数 0 8進数 0x 16進数
例えば、次のように使います。
#include <stdio.h>
int main(int argc, char *argv[]){
int b = 0b10, o = 010, d = 10, x = 0x10;
puts("\t%d\t%x");
printf("b\t%d\t%x\n", b, b); /* %d は整数型を符号付10進整数として、 %x は整数型を符号なし16進整数として文字列に変換します。 */
printf("o\t%d\t%x\n", o, o);
printf("d\t%d\t%x\n", d, d);
printf("x\t%d\t%x\n", x, x);
return 0;
}
結果
%d %x b 2 2 o 8 8 d 10 a x 16 10
論理演算子について
論理演算子は、整数型オペランドの各ビットに対して論理演算を行います。論理演算子は以下のものがあります。
& a & b aとbの論理積 | a | b aとbの論理和 ^ a ^ b aとbの排他的論理和 ~ ~a aの各ビットを反転
論理演算子も算術演算子と同様に次のような省略表現ができます。
a = a 演算子 b ⇔ a 演算子= b a = a & b ⇔ a &= b a = a | b ⇔ a |= b a = a ^ b ⇔ a ^= b
サンプル
#include <stdio.h>
int main (int argc, char *argv[]){
unsigned int a = 0xc, b = 0xa; /* 0xc == 0b1100, 0xa == 0b1010 */
printf("0x%x & 0x%x = 0x%x\n", a, b, a & b); /* 0x8 == 0b1000 となる */
printf("0x%x | 0x%x = 0x%x\n", a, b, a | b); /* 0xe == 0b1110 となる */
printf("0x%x ^ 0x%x = 0x%x\n", a, b, a ^ b); /* 0x6 == 0b110 となる */
printf("~0x%x = 0x%x\n", a, ~a); /* 32ビット整数の場合、 0xfffffff3 == 0b111...11110101 となる */
return 0;
}
シフト演算子について
シフト演算子は整数型のビットを左または右にずらします。ただし、データ型が符号ありか符号なしかで機能が変わるので注意が必要です。符号なし整数に対するシフトを論理シフト、符号あり整数に対するシフトを算術シフトといいます。シフト演算子は以下の通りです。
<< a << n aのビットを左へnビットシフト。右には0が詰められる。 >> a >> n aのビットを右へnビットシフト。左には論理シフトなら0が、算術シフトなら正の場合0が、負の場合1が詰められる。
シフト演算子も算術演算子と同様に省略表現ができます。
a = a 演算子 b ⇔ a 演算子= b a = a << n ⇔ a <<= n a = a >> n ⇔ a >>= n
また、次の表の左と右は恒等的に等しいです。
| a << n | = | a×2n |
| a >> n | = | a×2-n |
サンプル
#include <stdio.h>
int main(int argc, char *argv[]){
unsigned int a = 0x4, n = 2;
printf("0x%x << %u = 0x%x\n", a, n, a << n);
printf("0x%x >> %u = 0x%x\n", a, n, a >> n);
return 0;
}
typedefについて
ビット演算とは関係ありませんが、わざわざページを作ってまで紹介することでもないのでここで書きます。
typedefはデータ型に名前を付けます。それだけです。使い方は
typedefはデータ型に名前を付けます。それだけです。使い方は
typedef データ型 新しい名前
とやります。変数の宣言と似ていますね。
#include <stdio.h>
#include <string.h>
typedef unsigned int uint; /* 長ったらしいデータ型を短くしたり (これは uint が新しいデータ型名) */
typedef __int64 integer_t; /* 将来データ型を変更するかもしれないものを一気に変えることができたり (integer_t が新データ型名) */
typedef struct person_st{
int no;
char name[16];
}person_t; /* 構造体などにも名前を付けることができます。 (person_t が新データ型名) */
int main(int argc, char *argv[]){
uint n; /* unsigned int 型の変数 n を宣言。 */
integer_t i; /* __int64 型の変数 i を宣言。 (__int64 は処理系依存なので直接指定するよりは安全じゃないかな? かな?) */
person_t tarou; /* 構造体 person_st 型の変数 tarou を宣言。 */
n = 0;
--n; /* アンダフローを起こす。
値としては UINT_MAX (limits.h で定義されている) となる。 */
printf("n = %u = 0x%x\n", n, n);
i = (integer_t)1 << 40; /* 64 ビット符号あり整数なので最大 (2 の 63 乗) - 1 の整数を扱える。
(執筆当時 (2010 年現在) 、 CPU ビット幅の主流は 32 ビットから 64 ビットへ移行する境目の時期であった。) */
printf("i = %lld\n", i);
tarou.no = 3;
strncpy(tarou.name, "TAROU WORLD", 16);
puts("No\tName");
printf("%d\t%s\n", tarou.no, tarou.name);
return 0;
}
結果
n = 4294967295 = 0xffffffff i = 1099511627776 No Name 3 TAROU WORLD
型変換について
型変換とは、あるデータ型のオペランドを別のデータ型に変換することです。
特に明記しない場合でも、違うデータ型同士の演算や、違うデータ型の変数への代入などを行うことで、コンパイラが自動的に型変換を行うことがあります。これをWikipediaでは暗黙の型変換と呼んで明示的なものと区別しているようです。といっても、区別する目的でなければ、普段は型変換と呼んでいます。
型変換を明示的に行うこともできます。それを明示的型変換 (キャスト) といいます。これはよく使う言葉です。使い方は、
特に明記しない場合でも、違うデータ型同士の演算や、違うデータ型の変数への代入などを行うことで、コンパイラが自動的に型変換を行うことがあります。これをWikipediaでは暗黙の型変換と呼んで明示的なものと区別しているようです。といっても、区別する目的でなければ、普段は型変換と呼んでいます。
型変換を明示的に行うこともできます。それを明示的型変換 (キャスト) といいます。これはよく使う言葉です。使い方は、
(変換先のデータ型) 変換されるオペランド
とやります。
/* 暗黙の型変換だけテスト */
#include <stdio.h>
int main(int argc, char *argv[]){
double realX;
int intX;
long long llX; /* 筆者の環境では 64 ビット符号あり整数 */
realX = 3.14159265358979323846e+3; /* ○.○○e□△△は○.○○×(10の□△△乗)を表わす。□は符号。 */
printf("realX = %.15g\n", realX);
intX = realX; /* 整数型変数に浮動小数点型の値を入れることはできないので、浮動小数点型オペランドの少数第一以下を切捨てて整数型に変換してから代入される。 */
printf("intX = %d\n", intX);
realX = intX; /* 浮動小数点型変数に整数型の値を入れることはできないので、整数型オペランドを指数形式で表し正規化して浮動小数点型に変換してから代入される。 */
printf("realX = %.15g\n\n", realX);
llX = 12345678901234567; /* 16 桁の精度を持っている。 */
printf("llX = %lld\n", llX);
realX = llX; /* double 型の精度は 15 桁なので丸め誤差が生じる。 */
printf("realX = %.15g\n", realX);
llX = realX; /* 精度が下がったものを入れると… */
printf("llX = %lld\n", llX);
return 0;
}
結果
realX = 3141.59265358979 intX = 3141 realX = 3141 llX = 12345678901234567 realX = 1.23456789012346e+016 llX = 12345678901234568
次に明示的型変換を含んだサンプルを示します。
#include <stdio.h>
int main(int argc, char *argv[]){
double x;
int n;
x = 9 / 2 * 2; /* 9 となるのが理想だが… */
printf("[1]\t9 / 2 * 2 = %g\n", x);
x = (double)9 / 2 * 2; /* 最初の 9 を double 型に */
printf("[2]\t(double)9 / 2 * 2 = %g\n", x);
x = 9 / (double)2 * 2; /* 真ん中の 2 を double 型に */
printf("[3]\t9 / (double)2 * 2 = %g\n", x);
x = 9 / 2 * (double)2; /* 右端の 2 を double 型に */
printf("[4]\t9 / 2 * (double)2 = %g\n", x);
x = (double)(9 / 2 * 2); /* 9/2 を double 型に */
printf("[5]\t(double)(9 / 2) * 2 = %g\n", x);
x = 9. / 2. * 2.; /* 書式を変えただけだが… */
printf("[6]\t9. / 2. * 2. = %g\n", x);
}
結果
[1] 9 / 2 * 2 = 8 [2] (double)9 / 2 * 2 = 9 [3] 9 / (double)2 * 2 = 9 [4] 9 / 2 * (double)2 = 8 [5] (double)(9 / 2) * 2 = 8 [6] 9. / 2. * 2. = 9
この結果の原因を考えます。この計算の計算の順番はいずれも、まず9を2で割り、その結果に2を掛けるというものです。違いはキャストを行うか行わないかです。ここで、9と2と2はいずれもint型のオペランドとして扱われます。オペレータはそのオペレータが演算する対象のすべてのオペランドの中で最も位の高いデータ型の演算規則で演算するため、その中で最も位の高いデータ型でないものを最も位の高いデータ型に暗黙の型変換します。ちなみに、データ型の順位は以下の通りです。
double > float > int > char
本題に戻ります。/は2個のオペランドを受け取り、結果として1個のオペランド (型は演算したオペランドの中の最高位の型) を返します。*も2個のオペランドを受け取り、結果として1個のオペランドを返します。=は右側のオペランドを左側の変数に代入するため、データ型が異なれば右側のオペランドを強制的に変数のデータ型に暗黙の型変換します。
[1]ではまず9/2を演算しますが、最高位の型であるint型の演算規則で演算し、演算結果の4 (int型のオペランド) を返します。次に、計算結果の4と2を掛けます。なので、これはint型の8というオペランドを返します。残るオペレータは代入の=ですので、オペランド8をdouble型に型変換し、変数xに書き込みます。
[2]ではまずint型の9をdouble型にキャストします。次に9/2を演算しますが、最高位の型であるdouble型の演算規則で演算するため、2をdouble型に暗黙の型変換し、演算結果の4.5 (double型のオペランド) を返します。次に、演算結果の4.5と2を掛けます。なので、これはdouble型の9というオペランドを返します。残るオペレータは代入の=ですので、両方の型が等しいことから、そのまま右側のオペランドを変数xに書き込みます。
これだけ説明すれば[3]~[5]もわかるでしょう。わからなかったら掲示板でも使って「異議あり!」とか言ってください。
[6]は、数値オペランドの表現方法を変えただけですが、計算結果が[1]と違います。小数点を含むオペランドは浮動小数点型であると解釈されるので、9.や2.はdouble型のオペランドということになります。
[1]ではまず9/2を演算しますが、最高位の型であるint型の演算規則で演算し、演算結果の4 (int型のオペランド) を返します。次に、計算結果の4と2を掛けます。なので、これはint型の8というオペランドを返します。残るオペレータは代入の=ですので、オペランド8をdouble型に型変換し、変数xに書き込みます。
[2]ではまずint型の9をdouble型にキャストします。次に9/2を演算しますが、最高位の型であるdouble型の演算規則で演算するため、2をdouble型に暗黙の型変換し、演算結果の4.5 (double型のオペランド) を返します。次に、演算結果の4.5と2を掛けます。なので、これはdouble型の9というオペランドを返します。残るオペレータは代入の=ですので、両方の型が等しいことから、そのまま右側のオペランドを変数xに書き込みます。
これだけ説明すれば[3]~[5]もわかるでしょう。わからなかったら掲示板でも使って「異議あり!」とか言ってください。
[6]は、数値オペランドの表現方法を変えただけですが、計算結果が[1]と違います。小数点を含むオペランドは浮動小数点型であると解釈されるので、9.や2.はdouble型のオペランドということになります。
sizeof演算子について
sizeof演算子は、データ型やオペランドがメモリ上でどれだけの空間を占有しているのかをバイト単位で返す演算子です。1個のオペランドを受け取り、結果としてsize_t型のオペランドを返します。使い方は
sizeof(データ型) sizeof(オペランド) sizeof データ型 sizeof オペランド
とやります。()は任意ですが、付けた方が見た目が関数と似ててわかりやすいんじゃないかな? かな?
#include <stdio.h>
struct student_t{
char id[10];
char name[20];
};
int main(int argc, char *argv[]){
int n, arr[16];
char ch, *cp;
printf("sizeof(double) = %u\n", sizeof(double));
printf("sizeof(float) = %u\n", sizeof(float));
printf("sizeof(struct student_t) = %u\n", sizeof(struct student_t));
printf("sizeof(n) = %u\n", sizeof(n));
printf("sizeof(arr) = %u\n", sizeof(arr));
printf("sizeof(arr[0]) = %u\n", sizeof(arr[0]));
printf("sizeof(999) = %u\n", sizeof(999));
printf("sizeof(999.) = %u\n", sizeof(999.));
printf("sizeof('a') = %u\n", sizeof('a'));
printf("sizeof(\"ahya\") = %u\n", sizeof("ahya"));
printf("sizeof(ch) = %u\n", sizeof(ch));
printf("sizeof(&ch) = %u\n", sizeof(&ch));
printf("sizeof(cp) = %u\n", sizeof(cp));
printf("sizeof(struct student_t *) = %u\n", sizeof(struct student_t *));
return 0;
}
結果
sizeof(double) = 8
sizeof(float) = 4
sizeof(struct student_t) = 30
sizeof(n) = 4
sizeof(arr) = 64
sizeof(arr[0]) = 4
sizeof(19) = 4
sizeof(19.) = 8
sizeof('a') = 4
sizeof("ahya") = 5
sizeof(ch) = 1
sizeof(&ch) = 4
sizeof(cp) = 4
sizeof(struct student_t *) = 4
予想と結果はどんな感じでしたか? 私は'a'だけ予想外でした(予想では1かなと)。もちろんこの結果は処理系依存です。
条件演算子について
条件演算子は、式の中で使えるif文と思ってください。書式は
条件 ? 条件が真のときの式 : 条件が偽のときの式
となります。3個の項からなる演算子なので、条件演算子は三項演算子です。
サンプル
#include <stdio.h>
int main(int argc, char *argv[]){
int a, b, max, min;
scanf("%d", &a); // a の値を入力
scanf("%d", &b); // b の値を入力
max = (a > b) ? a : b; // a と b のうち大きいほうを max に代入
min = (a < b) ? a : b; // a と b のうち小さいほうを min に代入
printf("大きいのは %d\n", a);
printf("小さいのは %d\n", b);
return 0;
}