「学校で一通りC言語プログラミングを学んだ、次に学ぶべき言語はどれ?」
大学ではこういった質問を受けることがよくあります。もちろん、「広く浅く」をモットーに様々な言語に手を出していくのも一つのやり方。しかし、せっかくC言語を学んでいるのだから、講義レベルで終わりにするのも、もったいない。
そこで、「とりあえず(授業レベルの)C言語は一通り学んだよ!」という方向けに、次に学ぶとよいトピック(C言語以外のものを含めて)をいくつか挙げたいと思います。
※ レベル別になっている訳ではないので、興味のあるものから取り組むとよいでしょう。
すでに学んでいること
まず、C言語の授業ですでに学んだところを復習しましょう。例えば以下のようなトピックは十分理解できているでしょうか?(授業で扱っていない場合は初等的な本もしくはWaseda Course Channelで学んでください。)
- 変数・標準出力
- 繰り返し
- 配列・ポインタ
- 関数呼び出し
- 文字列
- 条件分岐
- switch文
- 構造体
- 数学関数
C言語の初歩的なトピックの理解を確認する10の問題で確認してください。
自分でゼロからプログラムをかけますか?
授業で与えられたプログラムを読むことは重要ですが、読むだけでなく自分でプログラムを書かなければ上達しません。そのプログラム、ちゃんと理解できているのであれば、自分でゼロから書き直せるはずですよね。
教科書
各言語には原典とも呼ばれるような有名な教科書があります。C言語の場合、それは間違いなく「プログラミング言語C (B.W.カーニハン/D.M.リッチー)」です。よくK&Rと呼ばれています。
少しとっつきにくいという方もいますが、手に入れて損はない長く使える本です。(第2版であれば、内容が古い/記法が古い、ということもありません)
この本について紹介したいことは山ほどありますが、本題からそれるので省略します。詳しくはWikipedia参照。
BNF記法
プログラミング言語の文法は何らかの手段で厳密に定義されていることが多く、そのための方法として最も有名なのがBNF記法です。
実際のところ、素のBNF記法がそのまま使われている例はあまり見ないのですが、そのエッセンスを汲んだ独自の方法で文法を記述することが多いため、基本を理解しておくと何かと役に立ちます。
例えば、先述した「プログラミング言語C」の「付録A. 参照マニュアル」では、C言語の文法が規定されています。その一部を示します。(純粋なBNF記法とは少し異なります)
asignment-expression:
conditional-expression
unary-expression assignment-operator assignment-expression
assignment-operator: one of
= *= /= %= += -= <<= >>= &= ^= |=
これが意味するところは、
- assignment-expressionは、「conditional-expression」または「unary-expression, assignment-operator, assignment-expressionを並べたもの」である
- assignment-operatorは、= *= /= %= += -= <<= >>= &= ^= |= のうちの1つである
となります。なお、conditional-expression, unary-expressionは本文中の他の部分で定義されています。
例えば、変数a
, b
, c
が定義されていて、a
がunary-expression, b || c
がconditional-expressionであるとすれば、
a /= b %= c = b || c
はassignment-expressionであることが確認できます。(確認してみてください)
ヒント: assignment-expressionの定義の中に再びassignment-expressionが入っていますね?(このような定義を右再帰的な定義といいます。)
演算子の優先順序
足し算、引き算より掛け算、割り算のほうが先に実行される、という初歩的な計算法則はC言語でも成り立ちます。例えば、
#include <stdio.h>
main() { printf("%d\n", 3 + 4 * 5); }
は23
を出力します。これを実現するのが演算子の優先順序です。表にまとめると以下になります。
優先順位 | 演算子 | 結合 |
---|---|---|
1 | 関数呼び出し() 添字[] . -> 後置++ 後置-- | 左結合 |
2 | 前置++ 前置-- sizeof 単項& 単項* 単項+ 単項- ~ ! | 右 |
3 | キャスト演算子 | 右 |
4 | * / % | 左 |
5 | + - | 左 |
6 | << >> | 左 |
7 | < <= > >= | 左 |
8 | == != | 左 |
9 | & | 左 |
10 | ^ | 左 |
11 | | | 左 |
12 | && | 左 |
13 | || | 左 |
14 | :? | 右 |
15 | = += -= *= /= %= <<= >>= &= ^= |= | 右 |
16 | , | 左 |
いくつか用語の説明をします。
-
単項 / 前置 / 後置について : 式を1つとる演算子を単項演算子といいます。特に、式の前に演算子を書く場合、それを前置演算子といい、後ろにかく場合は後置演算子といいます。例えば、
i++
の++
は後置演算子です。式を2つ取る演算子を二項演算子といいます。例えば1+2
の+
は二項演算子です。 -
結合について : 同じ優先度の演算子が並列に並んでいる場合の計算順序を規定します。例えば二項演算子の
-
は左結合なので、10-5-3
は(10-5)-3
と解釈されます。 -
キャスト演算子 は
(double)
のようなものです。
ビット演算
C言語で書かれたプログラムを実行するコンピュータは2進数を解釈する論理回路であるため、通常の計算(足し算など)の他にビット演算というものを効率よく実行することができます。
例えば、十進数で8
という値を持つオブジェクトは、論理回路上では1000
という2進数表現で扱われます。(このような2進数による表現のことを値の表現という)
この表現を1000
を1010
に変えたいと思ったとすると、10
は十進数では2
になるため、8 + 2
を計算すればよいです。しかしながら足し算を使わなくても、ビットOR演算を用いて 8 | 2
を計算することでも同様の効果が得られます。
詳しくは述べませんが、例えば以下のような演算子について学ぶとよいでしょう。
- ビットAND演算子 (bitwise-AND operator)
- ビットOR演算子 (bitwise-OR operator)
- ビット反転演算子
- ビットXOR(EOR)演算子
また、ビット演算の有用性を確認するために以下のプログラムを確認してみましょう。
#include <stdio.h>
const int APPLE = 1;
const int ORANGE = 2;
const int GRAPE= 4;
int cost(int flag) {
int ret = 0;
if (flag & APPLE) ret += 300;
if (flag & ORANGE) ret += 100;
if (flag & GRAPE) ret += 200;
return ret;
}
int main(void) {
printf("%d\n", cost(APPLE | GRAPE));
}
翻訳単位の分割(分割コンパイル)およびヘッダファイル
大きなプログラムを作る場合、コンパイル時間を短くしたい、等の理由からプログラムを複数のファイルに分けて書くことがあります。例えば、2つのファイルa.c
, b.c
について
#include <stdio.h>
void f(void) {
printf("hello\n");
}
void f(void);
int main(void) {
f();
}
と記述し、以下のようにコンパイルします。
$ gcc -o hello a.c b.c
また、さらに大きいプロジェクトでは各ファイルの先頭にvoid f(void);
のように関数のプロトタイプ宣言を記述するのは現実的でなくなるため、ヘッダファイルというものを用意してそこにプロトタイプ宣言をまとめて記述するのが普通です。上の例の場合、b.h
のプロトタイプ宣言をヘッダファイルに移すと以下のようになります。(コンパイルの仕方は変わりません)
void f(void);
#include "b.h"
int main(void) {
f();
}
暗黙の型変換
C言語では、数値型のオブジェクトは様々な局面で自由に使えるようになっています。例えば以下のコードを見てください。
unsigned long long a = 100ull;
int b = a;
a
はunsigned long long
型であり、b
はint
型なので、a
とb
の型が異なっていますが、C言語の場合は暗黙の型変換によって型変換が行われ、a
をb
に代入することができます。
また、通常の算術演算でも同様のことが起こります。
int a = -10;
unsigned int b = 10;
printf("%d\n", a / b);
このようにすると、a
, b
はどちらもunsigned int
に変換されて計算されます。(-1にならない)
参考
少し詳しい型変換の説明
副作用完了点
式には値を求める作用(正の作用)と、環境に対して何らかの影響を与える副作用が存在します。この副作用がどのタイミングで実行されるかを規定するのが副作用完了点です。
参考
副作用完了点について
型の読み方
C言語の特徴の1つとして、型の記述方法が複雑という点が挙げられます。例えば、以下に示す識別子a
は何を表すでしょうか。
char (*a(int, int))[10](int);
C言語の型の読み方を解説したサイトはいくつかあるため、ここでは割愛します。