Programming Serendipity

気まぐれに大まかに生きるブログ

C言語基本文法解説

C言語の基本文法をまとめるプロジェクトです。

1. Hello, World!

以下のコードを書いて実行してみてください。

#include <stdio.h>

int main(){
    printf("Hello, World!");
    return 0;
}

Visual Studioの場合、Ctrl+Shift+Bでビルドし、Ctrl+F5で実行します。 また、ソースを更新した状態でCtrl+F5で実行すると、同時にビルドも行われるので、これだけで保存→ビルド→実行までいっぺんにできます。 Xcodeの場合、左上の再生ボタンを押すと、ビルドと実行を同時に行ってくれます。

ビルドとは

コンパイル(ソースファイルごとに、ソースコードを実行可能なデータに変換すること)とリンク(コンパイルした複数のデータを一つにまとめて.exeのような実行可能ファイルにすること)をまとめてビルドといいます。

#include <stdio.h>とは

stdio.hというファイルをこのソースファイルに埋め込むという意味です。Visual Studioの場合、stdio.hの部分にカーソルを置き、Ctrl+Shift+Gを押してみてください。これによって開かれたファイルがstdio.hというファイルで、これをincludeすることでprintf()などデータの入出力を扱う関数が使えるようになります。 (ここでstdioとは、STandarD Input/Outputの略で、日本語では標準入出力などと呼ばれます。よくstudioと間違われるので注意してください。) (ちなみに、stdio.hの実装は例えばstdio.h setvbuf stderr SEEK_END define NULL FILE ifndef - Google 検索のように検索すると一例が見られます)

main()

C言語ではmain()関数からプログラムの実行を始めます。 このことをエントリポイントといいます。

printf()

print formattedの略で、ダブルクオーテーション(")で囲まれた文字列を出力します。 %dは書式指定子で、整数を表し、文字列の後に対応する変数を指定します。 複数の変数を指定することもできます。

int a = 10, b = 20;
printf("%d, %d\n", a, b); // 10, 20

それ以外の書式指定子は別表にまとめています。 \nのように\+特定のアルファベット1文字の組み合わせはエスケープシーケンスといい、\nは改行を意味します。 バックスラッシュと円マークは表示が異なるだけで同じ意味です。
ref: printf - Wikipedia

return 0;

main()関数では、プログラムの終了を意味します。

2. 計算

型の種類

C言語にはおおまかにbool char int short long float doubleの7種類の型があります。他の言語によくあるstringのような文字列型は一般に文字を入れるchar型の配列として扱われます(配列については後述します)。

bool  a = true; // trueまたはfalseの2値のどちらかが入る。条件分岐やフラグ管理などに使われる。本来はC言語ではなくC++にしかないが、純粋なC言語環境でも#include <stdbool.h>とすると使える。
char b = 'a'; // 「1文字」を入れるための型。''シングルクオーテーションで囲むことで1文字として利用できる。文字列をあらわす""ダブルクオーテーションとは役割が違うため注意。
printf("%\n", b); // char型の書式指定子である%cを使うことで文字を表示できる。
char c = '\n'; // のようにエスケープシーケンスを代入することもできる。char型も内部的には数値で管理されているので、
printf("%d\n", c); // のように整数として表示しようとすると暗黙的にint型に変換されて数値が表示される。
int d = 0; // 整数型で、現在の多くの環境では32bitを使った2進数で-21億~+21億くらいまでを表現できる。組み込み機器など、環境によっては-32768~+32767などの場合もある。
// というのも、C言語の規格では、環境ごとに各々最適なサイズを決められるように、int型は「-32768~+32767より広い範囲」としか定義されていないので、環境によってサイズが異なるので、ガチで使うときは#include <stdint.h>として、
int32_t e = 0; // のようにサイズを明確に指定するほうが安全なのですが、まあ学習段階ではintでいいでしょう。
short f = 0; // 一般的に16bit -32768~+32767
long g = 0; // 一般的に32bit -21億~+21億くらい
float h = 0; // 小数点以下も表現できる浮動小数点型と呼ばれる型。要は実数型。一番大きい桁から7桁目までは正確に保持される。
double i = 0; // floatの2倍まで詳細に値が記録できる型。一般的にはこちらを使う。
unsigned int j = 0; // のように型名の前にunsignedを付けると、マイナスの値が表現できなくなる代わりにプラス方向に2倍の値が表現できるようになります。

浮動小数点型について、なぜ全ての桁が正確に保存されないのかなどの詳細は浮動小数点数型と誤差を参照してください。

基本的な計算

四則演算、剰余などができます。 その他の操作は#include <math.h>すると数学系の関数が多く使用できます。

int a = 22;  
int b = 10;  
int c = a + b;  // 足し算
int d = a - b;  // 引き算
int e = a * b;  // 掛け算
int f = a / b;  // 割り算(※整数割る整数は小数点以下が切り捨てられるので注意)
printf("a: %d, b: %d, c: %d, d: %d, e: %d, f: %d\n", a, b, c, d, e, f);  

a++;  // インクリメント(1を足す)
b--;  // デクリメント(1を引く)
printf("a: %d, b: %d\n", a, b);  

a += b;  // a = a + b;
b -= c;  // b = b - c;
c *= d;  // c = c * d;
d /= e;  // d = d / e;
f = e % 5;  // 変数eを5で割った余り(整数型のみ使用可)
printf("a: %d, b: %d, c: %d, d: %d, e: %d, f: %d\n", a, b, c, d, e, f);  

キャスト

上の結果からわかるとおり、整数同士の割り算は余りが切り捨てられ、たとえば1/70になってしまいます。なので、厳密に小数点以下も含めて答えが欲しい場合は、値を別の型として解釈させるキャストを使います。ここではdouble型にキャストする必要があります。

// 以下は全て同じ結果になる
(double)1 / (double)7
(double)1 / 7  
1 / (double)7  
1.0 / 7  
1 / 7.0  

このように、値の前に(型名)の形で書くとキャストになります。これらはすべて0.142857など、実数で計算結果を得られます。直接の値の場合、括弧によるキャストをしなくても、小数点以下まで明示的に書けば自動的にdouble型になります。また、数値の最後にfをつけるとfloat型になりますが、以下のような書き方もできます。

40.0f  
40.f  
.5f  

ただし、C#でよくある6fのような書き方はできません。そのほか、型の表し方がいくつかあります。

16l // long型
78u // unsigned int型
234ul // unsigned long型

なお、これらの数値の後ろにつけるアルファベットは大文字でもOKです。

3. 乱数

コンピュータは完全な乱数を生み出すことはできませんので、計算して乱数のようなもの(疑似乱数)を作ります。 これは例えば、何かの値に12345で掛けて83769を引くとか、そういう計算で算出する数値のことです。 rand()で疑似乱数を生成できます。rand()関数を使用するには#include <stdlib.h>が必要です。

srand()

しかし、rand()だけだと何度実行しても同じ乱数の出方になると思います。 これは計算のもとになる最初の数値(シード値)が同じであるため、そこから計算される数値も毎回同じものになっているためです。 このシード値を変更する関数がsrand()関数です(ちなみにsはseedのsです)。 srand(1414287);などのように数値を指定しておくと、その数値から疑似乱数計算をスタートさせるので、異なる出方になります。

time(NULL)

#include <time.h>を記述して使用できるようになるこのtime()関数は、time(NULL)と記述することで1970年1月1日からの経過秒数(UNIX時間)を取得できます。

参考:UNIX時間 - Wikipedia

srand((unsigned)time(NULL))

これを使用し、srand(time(NULL))とすると、実行するタイミングによって毎回異なるシード値が設定できます。ただし、秒数なので1秒の間に何度も実行すると同じ出方になってしまいます。が、学習段階では十分でしょう。(unsigned)のunsignedはunsigned intの略です。srand()関数はunsigned int型を使って呼び出す必要があるので、通常のint型を使おうとすると処理系によっては警告を出すことがあるので、time()で取得した値をそのunsigned int型に変換することでその警告をなくすことができます。

4. for

ループを記述します。基本的な使い方は以下の通りです。

for(int i = 0; i < 10; i++){
    // 何かの処理
}

このように記述すると、ブロックの中の処理が10回実行されます。セミコロンで区切られ、3つの文があるのがわかると思います。

第1文

1つ目の文はこのfor文に入って1回だけ実行する文です。一般的には主にループの制御に使う変数(ループカウンタ)の初期化などを行いますが、何でも書くことができます。

第2文

2つ目の文には条件式を書き、この条件に合っていたらfor文の中身を実行する、という意味です。ここでは変数iが10未満であれば実行する、ということになります。

第3文

3つ目の文はfor文を実行した後に実行される文です。主にループカウンタの加算などを行います。ここでは、変数iの中身を1増やす、ということをしています。

まとめ

まとめると、ここでは、変数iを宣言して0で初期化し、条件を判定して10未満なので実行、forの中身を実行し、終わったらiを1増やし、条件を判定してi(中身は1)はまだ10未満なので実行、終わったらiを1増やし、条件の判定、(中身は2)、実行したら1増やし、ということを繰り返し、中身が9までの時は繰り返されますが、中身が10になると10<10は成立しないのでfor文の実行が終了される、という形になります。変数iが0,1,2,3,4,5,6,7,8,9のときに実行されるので10回実行される、となります。

なぜ0からなのか

1から始まらずに0から始めるのは、他の操作との相性が良いためです。特にポインタとの関係で実感します。後で出てきます。 もちろん、for(int i = 1; i <= 10; i++)でも正常に実行されますし、そうしたほうがいいケースもあります。

その他

  • 全ての文はそれぞれ省略できます。特に、第2文を省略すると無限ループになります。

  • よくある間違いとして、 for(int i = 0, i < 10, i++)のように、セミコロンでなくカンマを書いてしまうことがあります。こうすると第1文に3つ書いていることになります。具体的には、まずint型のiという変数に0が代入され、次にi < 10が評価されてtrueになりますが特に意味はなく、次にi++が評価されてiは1になりますが、あくまでfor文の第1文は最初にfor文に入った時に実行されるだけなので、第2文・第3文が存在しないとみなされ、無限ループになります。

  • インクリメントは前置のほうがいいです。前置インクリメントはその場で直接1加算するのに対し、後置インクリメントは一度1加算した値を別に保存しておいて、次のセミコロンに到達したらその数値を変数に代入することをしているため余分な操作になり、パフォーマンス的にマイナスです。必要なければ前置インクリメントを使いましょう。ループカウンタの加算程度ならコンパイラが最適化して前置インクリメントしたのと同じことになることも多いですが、遅くなる可能性のある書き方をする必要はありません。基本は前置インクリメントと覚えましょう。

  • (ではなぜ後置インクリメントが存在するのか?)以下のようなケースでは後置インクリメントのほうがいいです。
char str[1024] = {};
char* p = str;
*p++ = '@'; // この処理が...
puts(str);

このプログラムは、strのなかに@(アットマーク)を入れる処理をしますが、前置インクリメントだけで書くと以下のようになります。

char str[1024] = {};
char* p = str;
*p = '@'; // こちらでは
++p; // 2行必要
puts(str);

両者の処理と結果は同じですが、前置インクリメントでは行数が1行多くなっています。この場合は、「その文ではすぐに加算せずに、セミコロンに到達したら加算する」という後置インクリメントの性質を利用したほうがソースコードが簡単になりますね。

5. 配列

配列は、複数の変数をひとかたまりとみなして扱う概念です。具体例を見たほうがわかりやすいでしょう。例えば、12か月の自分の体重を記録しておいたとします。それらを順に表示させるプログラムは以下のようになります。

double myWeights[12] = {65.2, 64.9, 64.8, 65.0, 64.4, 63.9, 63.6, 62.7, 63.3, 62.8, 63.8, 66.0};

for(int i = 0; i < 12; ++i){  
    printf("%2d月:%fkg\n", i + 1, myWeights[i]);  
}

ここで、myWeightsが配列で、配列はmyWeights[0]のように0から始まる数字でアクセスすることができます。なぜ0から始まるのかというのは、for文のループカウンタが0から始まる理由同様、これもポインタの演算と関係しています。その時に説明しましょう。

char型配列

C言語には、string型(文字列型)というのがありません(C++にはstd::stringがあります)。 なので、文字列を表現するときはchar型の配列を用います。 以下のコードをコンパイルして実行してみてください。

char str[6 + 1] = {'H', 'E', 'L', 'L', 'O', '!', '\0'};  
printf("%s\n", str);

シングルクオーテーション(')で囲った文字はchar型として扱われ、1文字を表します。Python, PHPなど、シングル・ダブルクオーテーションがどちらも文字列を表す言語がいくつかありますが、C言語(やJava, C#など)ではダブルのほうは文字列、シングルのほうは1文字であることに注意してください。

また、char型は数値とも互換性があり、例えば'A'65と同じ、'B'66と同じように扱うことができます。これは、ASCIIキャラクターコードの表にある数字に対応します。

また、上のプログラムのように%sとして文字列を出力する場合、配列を先頭から順番に見ていって、'\0'(ヌル文字。数字では0に相当)に当たるまで表示するという動作をします。なので、配列の最後の'\0'を忘れると予測できないメモリの先のほうまでデータを文字として読み取って、偶然0になっているバイトにたどり着くまで変な文字を表示し続けてしまいます。char型配列を文字列として扱うには、最後にヌル文字がある(ヌル終端されている=null terminated)必要があります。すなわち、上のように6文字格納するにはそれよりもう1文字多く7文字分の要素を確保しなければいけません。

初期化しなかった場合

int nums[10];のように、特に初期値を入れずに配列を宣言した場合、中には何が入っているかはわからない状態になっています。これは、言語側で自動的に初期化などを行わず、配列を確保した場所にあるメモリのデータがそのまま残っているためです。「配列はすべて0で初期化する」という言語もありますが、「0で埋める」という処理にもほんのわずかながら時間がかかるため、その時間すらも惜しいというケースを想定し、C言語では自動的に初期化などは行っていません。

配列の要素をすべて0で初期化する

とはいえ、0で初期化したいケースは多々あります。そこで、int nums[10] = {0};というようなコードをよく見かけます。{}を使って初期化するときは、省略した要素にはすべて0が入るので、0番目の要素に0を入れて、残りの9つの省略された要素には自動的に0が入るので、これで確かにすべて0で初期化されるのですが、もっと省略してint nums[10] = {};でも同じ効果が得られます。

素数省略

配列の要素数は省略することができます。その場合、{}を使って初期化された要素の数が自動的に入ります。int nums[] = {1, 2, 3}; // 要素数は3

2次元配列

[]を重ねることで多次元配列が作れます。 int nums[4][5] = { };

多次元配列の要素数の省略

多次元配列は、最初の(一番左の)要素数のみ省略することができます。 int foo[][4][7][3];

6. プリプロセッサ

プリプロセッサとは、プリプロセス、つまりコンパイラがコードをコンパイルする前に行う処理のことで、C言語では#で始まる行が該当します。 #include, #define, #undef, #if, #else, #elif, #endif, #pragmaなどが該当します。

#includeは、「含める」という意味の通り、指定したファイルをそのまま埋め込む操作をします。特に文法的に特殊な意味合いを持っておらずべたっとファイルを貼り付けるだけなので、

int a[10] = {  
#include "SomeHeader.h"  
};  

というようなことをすることも可能です(普通しませんが)。

#include <stdio.h>
#include "something.h"

と、角かっこと二重引用符の違いは、角かっこで囲むほうは標準のインクルードパスのみ検索し、二重引用符で囲むほうは開発に使用しているディレクトリ(Visual Studioであれば.csprojファイルがあるディレクトリ)も含めてファイルを探します。 なので、<>でインクルードできるファイルは""でもインクルードできますが、逆はできない場合があります。 また、インクルードするファイルはヘッダでなければいけないという決まりもなく、.cppをインクルードしても問題ありません。

#defineは文字の置き換えを行います。例えば#define MAX_LINE 10とし、それ以降の行でprintf("%d", MAX_LINE);とすると、10が表示されます。 また、よく使われるのが#define DEBUGという定義で、これをしておいて

#if defined(DEBUG)
printf("Test Output...\n");
#endif

というようにして、DEBUGが定義されている時のみ実行されるコードを書くことができます。これは、コンパイルの最初の段階のプリプロセスの段階で判別されて除去されるので、実行時のパフォーマンスにも影響を与えません。

  • 「定義されていなければ」という条件にしたい場合は #if !defined(DEBUG)のようにします。
  • #ifの条件の判定対象1個であれば#ifdef DEBUGと省略して書くこともできます。
  • 複数の条件を使用したいときも、一般的なif文と同じように#if defined(DEBUG) && MAX_LINE > 5のように書くことができます。
  • #if MAX_LINEのように定義だけ書かれている場合も、これもやはり通常のif文と同じように!=0が補われており、すなわち #if MAX_LINE != 0と同じ意味になります。
  • #ifdefの逆バージョンは#ifndefです。

7.入力を受け取る

プログラムの実行中に値を受け取るには、scanf()を使います。 (Visual Studioの場合、#define _CRT_SECURE_NO_WARNINGS#includeの行よりも上の一番上に書いてください。)

例えば、入力した数値を変数に受け取るには、以下のようにします。

int num;
puts("input a number:");
scanf("%d", &num);
printf("you entered %d\n", num);

これは、printf()が文字列の書式に従って右側の引数を出力するのとは逆の動作で、scanf()は文字列の書式に従って右側の引数に値を受け渡す動作をします。これを実行させると、scanf()の直前まで実行された後待機状態になり、そこで数値を入力するとnumに入力した数値が入り、続くprintf()でnumの中身が出力されるという動作になります。

8. if

if文は条件によって処理を分岐させることができます。elseは条件に合致しなかったとき、 else ifは上のifに合致しなくて今回の条件に合致するときに実行されます。

int num = 30;
if(num > 10){
    printf("num is bigger than 10.\n");
}
else if(num == 10){
    printf("num is 10!\n");
}
else{
    printf("num is smaller than 10\n");
}

比較には、<><=>===!=の6種類があります。不等号は必ずイコールの左側に書きます。

比較演算子の省略

if(num)のような、変数をどれとも比較せずに直接if文の中に書いているコードがあります。これは、C言語では0のみがfalseでそれ以外が全てtrueであることを利用した文で、if(num != 0)と同じ意味になります。

代入と間違える

同じかどうかを確かめる演算子==ですが、慣れない(数学の癖が残ってる)うちは、if(num = 10)と書いてしまうことがあります(自分もよくやりました)。これはC言語ではエラーになりません(警告を出してくれるコンパイラもありますが)。このコードは別の意味を持つことになり、「numに10を代入して、その代入された後のnumを使って num != 0を比較する」という意味になります。これでは常にtrueになってしまいますね。このようなミスを防ぐため、if文の中で数値と比較するときは、固定値を左側に書くという人もいるくらいです(if(10 == num))。このように間違えやすいので、if文の中での代入は避けたほうが良いでしょう。

switch

条件分岐の文法として、もうひとつswitchというのがあります。

int num = 0;
switch(num){
    case 0:
        printf("num is 0.\n");
        break;
    case 1:
        printf("num is 1.\n");
        break;
    case 2:
        printf("num is 2.\n");
        break;
    default:
        printf("num is not 0, 1, or 2.\n");
}

これは、以下のようにif文で書いても同じです。

int num = 0;
if(num == 0){
    printf("num is 0.\n");
}
else if(num == 1){
    printf("num is 1.\n");
}
else if(num == 2){
    printf("num is 2.\n");
}
else{
    printf("num is not 0, 1, or 2.\n");
}

switchの使いどころとしては、条件分岐が3つ以上の時に使われます。

breakの書き忘れ

よくあるミスとして、breakを書き忘れるというのがあります。例えば、以下のコードはbreakを書き忘れています。

int num = 0;
switch(num){
    case 0:
        printf("num is 0.\n");
    case 1:
        printf("num is 1.\n");
    case 2:
        printf("num is 2.\n");
    default:
        printf("num is not 0, 1, or 2.\n");
}

このコードを実行すると、

num is 0.
num is 1.
num is 2.
num is not 0, 1, or 2.

と表示されてしまいます。これは、本来breakでswitch文を抜けるべきところで処理が終わらず、その下まで処理が続いてしまっていることが原因です。breakを忘れないようにしましょう。

あえてbreakを書かずに処理を抜けさせる場合もありますが、滅多にない(仕事でも1年に1度あるかないか)ので、基本的にはbreakをつけていきましょう。

(条件にかけられるのは整数型のみです。floatや配列・ポインタなどは使えません(C#のswitchは文字列も比較できます)。)

9. ビット演算

コンピュータは内部的に数値をすべて2進数で持っています。その2進数のそれぞれの桁のことをビットといいますが、このビットを直接操作することで通常の演算よりも高速に処理を行うことができます。

まずはソースファイル全体を以下のようにして準備してください。これは整数値のビットの状態を分かりやすくするための関数を用意した状態です。

#include <stdio.h>

void print_bits(int num) {
    printf("%8d : ", num);
    for (int i = 31; i >= 0; --i) {
        printf("%d", (num & (1 << i)) != 0);
    }
    puts("");
}

int main() {
    int a = 1;
    print_bits(a);
}

この状態で実行するとint型変数aをビットで表した時の状態を見ることができます。

ビット演算子には、<<, >>, &, |, ^, ~の6種類があります。それぞれ実践的に見ていきましょう。

シフト演算

<<はビットを左にシフトする演算子です。↑のコード中のmain()関数の最後にさらに

a = a << 1;
print_bits(a);

と追加して実行してみてください。すると、aの内部の終わりが2進数で10になっているのがわかると思います。最初は右端にあった1が左に1つずれていますね。これは10進数に直すと2です。では、a << 1a << 2にしてみてください。こうすると今度は100になっているのが確認できると思います。これは10進数では4を表します。さらに、a << 3とすると、1000(8)になっているはずです。これを見るとわかるように、ひとつビットをずらすたびに値が2倍になっていきます。(昔は、これを利用して2,4,8倍のような計算は掛け算より高速なビット演算でやっていた時代もあったようですが、今はコンパイラが優秀なので、そのような演算を見ると自動的にビット演算に変換してくれますので、こちら側でそうする必要はありません)

>>は逆に、ビットを右にずらす演算です。a >> 1a / 2と同じ意味合いになり、a >> 2a / 4と、a >> 3a / 8と同じ意味になります(それ以降も同じ)。

AND演算

&(and)は、「両方ともビットが立っていたらそのビットを立てる」演算です。以下のような関係性があります。

1 & 1 // 1
1 & 0 // 0
0 & 1 // 0
0 & 0 // 0

片方でも0のビットがあれば、そのビットは0になります。 この性質は「ビットマスク」というものを実現するのによくつかわれます。たとえば、a & 7とすれば、7の2進数は111ですから、それ以上の桁は0とのアンドなので全て0になり、最後の3ケタだけは変数のビットがそのまま保持されます。これを使って、

for(int i = 0; i < 100; ++i){
    printf("%d\n", i & 7);
}

とすると、0から7まで進むシーケンスが連続しているのが確認できます。これは2n-1の数(3, 7, 15, 31, 63など…)なら同様のことができます。

OR演算

|(or)は、「片方でもビットが立っていたらそのビットを立てる」演算子です。以下のような関係性があります。

1 | 1 // 1
1 | 0 // 1
0 | 1 // 1
0 | 0 // 0

片方でも1のビットがあれば、そのビットは1になります。

これは、数多くのオンオフの設定をより少ないメモリで設定するのによく使われます。

例えば、C++のiostreamでは、マニピュレータという、標準出力の形式を設定する関数があり、そのフラグ管理にor演算が使われています。http://www.cplusplus.com/reference/ios/ios_base/flags/ にあるサンプルでは、以下のように使われています。

  std::cout.flags(std::ios::right | std::ios::hex | std::ios::showbase);

これは、「右詰めで、16進数表示で、基数を表示する」設定を意味しますが、right, hex, showbaseなどは数値であり、VSでは以下のように設定されています。

 #define _IOSskipws     0x0001
 #define _IOSunitbuf   0x0002
 #define _IOSuppercase 0x0004
 #define _IOSshowbase  0x0008
 #define _IOSshowpoint 0x0010
 #define _IOSshowpos   0x0020
 #define _IOSleft      0x0040
 #define _IOSright     0x0080
 #define _IOSinternal  0x0100
 #define _IOSdec       0x0200
 #define _IOSoct       0x0400
 #define _IOShex       0x0800
 #define _IOSscientific    0x1000
 #define _IOSfixed     0x2000

最初の例では、これをそれぞれorすることでここでは0x888(ビットでは100010001000)となり、この値を受け取った側で各ビットごとにand演算をし、ビットが立っていたらその設定をオンにする、という処理がなされていることが想像できます。これで、1つ1つbool型の変数を用意してそれぞれにtrueを代入してという事をするよりもメモリは少なくて済みます。(boolは変数ごとに1バイト必要ですが、1バイトは8bitなのでそれぞれのビットをフラグとして使えば1バイトで8つのフラグを保持できます)。無論、読みにくさは増してしまうので、使い時は考える必要はあります。

XOR演算

^は「両方のビットが異なっていた時にビットを立て、両方のビットが同じだった場合はビットを下げる」演算子です。以下のような関係性があります。

1 ^ 1 // 0
1 ^ 0 // 1
0 ^ 1 // 1
0 ^ 0 // 0

0と1の組み合わせの時に1になり、1と1,0と0は0になっています。

この性質はスイッチを実現するのによく使われます。例えば、aという変数が0だったとしてa = a ^ 1とすると、1回目は0 ^ 1であり、これは1になります。ここで、もう一度やるとさっきaは1になりましたから1 ^ 1となり、今度は0が得られます。これは他のビット演算ではできない演算です。

また、同じ数値で2度XORすると元の値が復元されるということから、簡易的な暗号にも使われます。以下のコードを実行してみてください。

int a = 38756832;
int b = 96923840;
printf("a: %d\n", a);
a = a ^ b;
printf("a: %d\n", a);
a = a ^ b;
printf("a: %d\n", a);

aとbがどの値になっていてもaが復元されるのがわかると思います。ただしこれはbの値を総当たりで試せばいつか暗号がばれてしまうので、個人情報のような大事な情報の暗号化には適していません。

否定

~は、全てのビットをひっくり返します。001010という数値があったら110101になります。

int c = 428759823;
print_bits(c);
c = ~c;
print_bits(c);

これを利用して、変数の中の特定のビットだけ下げるということができます。ちなみに、特定のビットだけ立てる場合は簡単で、例えば3ビット目(ビットの桁の数え方は0始まりなので右から4つ目の意味)を立てるには、a | 0x8だけでいいですね。これを下げたいときはa & ~0x8のようにできます。これはまず、0x8(00001000)の反転(11110111)とaとのAND演算になるので、0x8で指定したビットだけは強制的に0になり、他はaが持っていたビットがそのまま保持されます。

複合

これらの演算子は、四則演算と同様に演算と代入をいっぺんに行うことができます。

int d = 10;
d = d + 10;
d += 10; // 同じ
d = d << 2;
d <<= 2; // 同じ

d >>= 2;
d &= 7;
d |= 0x5555;
d ^= 0xff;
d = ~d;

否定~だけは単項演算子なので、複合はありません。

(この記事は書きかけです。指摘・意見・要望・提案などもらえると嬉しいです。)