C言語とは
C言語とは、現在のモダンな言語のベースとなったといっても過言ではない言語です。
拡張言語として登場したC++やObjective-Cはもちろんのこと、
C#やJava、Goといった言語はこのC言語の作法に則っています。
初学者にC言語をおすすめするということ
タイトルにすごいことを書いていますが、やはり初学者がC言語からスタートすることは反対です。
初学者はRubyやPythonなどでWeb、もしくはKotlinやSwiftでモバイルなどの画面作ったほうが楽しいです。
Javaも個人的には今はおすすめしません。好んでJavaを使うのなんてもうSIerだけでは?
さて、本題です。
C言語は言ってしまえば古い言語です。
コンソールアプリのHello Worldは以下のコードで書けるのでとても簡単です。
#include <stdio.h>
int main(void) {
printf("Hello World!\n");
}
基本構文のif, for, whileといった構文は、ほかの言語と同様ですので、
プログラムの基礎中の基礎を身に着けるには適しているかもしれません。
しかし、初学者はいかにつまずくことが非常に多いです。
- 文字列
- 配列
- 構造体
- ポインタ
特に最後の「ポインタ」という概念です。
というのも、文字列も配列も実質的には裏でポインタを使っているので、
実質的には上記の点すべてがポインタに起因してしまいます。
なので、C言語をまともに使うためにはこのポインタの概念を知らないと何もできないのです。
ポインタとは何か
C言語を知らない方のために説明すると、このポインタは「データのアドレス(格納場所)」を指します。
プログラムで使用するデータ(本体を含む)は、必ずメモリ領域のどこかに展開されます。
この時、そのメモリ領域のどこを指すのかがアドレスです。
例えば、以下のコードを実行した場合、intの大きさ4バイト分のメモリ領域が確保され、
そのアドレスが発行されます。
int num = 1;
C言語では、この変数のアドレスを取得し、操作することができます。
例えば、JavaScriptで以下のコードを書いたとします。
function multiple(a) {
a = a * a;
}
let a = 3;
multiple(a)
console.log(a);
この時、実行結果は3になります。
同じようにC言語で書いても、同じ結果を得られます。
#include <stdio.h>
void multiple(int a) {
a = a * a;
}
int main(void){
int a = 3;
multiple(a);
printf("%d", a);
}
様々なプログラミング言語の入門書で、同様のことが記載されているかと思います。
これは、int/number(整数)がプリミティブ型であるためです。
プリミティブ型は、関数へ値をコピーして読み出します。これを値渡しと言います。
値がコピーされて渡すので、元々宣言されているaという変数の中身は変更されません。
それに対して、オブジェクトなどは参照渡しされます。
これについては後に解説します。
C言語(ポインタ)を使ってできること
さて、ここからがC言語の真骨頂です。
C言語では、このプリミティブ型を参照渡しすることができます。
#include <stdio.h>
void multiple(int *a) {
*a = *a * *a;
}
int main(void){
int a = 3;
multiple(&a);
printf("%d", a);
}
これを実行すると、9が出力されます。
これは、aに入っている3という数字ではなく、
3という数字が入っているaのアドレスを渡しています。
これを**参照渡し(※1)**と言い、関数とその呼び元で同じ値を参照することができます。
C言語以外でも参照渡しという概念がありますが、内部的には同じ仕組みです。
※1・・・2020/12/11 追記
本来の参照渡しはC++やC#などの言語でしか存在せず、CやJavaなどでは厳密には参照を渡していません。
変数のポインタを渡していますので、「ポインタ渡し」というそうです。
ただ、Javaとかではポインタの概念が隠蔽されている関係で、「値の参照渡し」と表記すると間違いはなさそうです。
C言語では表現を気を付けないといけませんね。
@shiracamus さま、ありがとうございます!
参照とは、データのアドレス(ポインタ)にアクセスするという意味合いになります。
例えば、文書での参照も、別なページへの誘導を行う用途です。
特定の箇所に対し、ほかの箇所からたどり着けるようにする
ということになりますので、意味は同じです。
なぜC言語を勉強するのか
さて、「初学者にはC言語は反対」と大見えきって書きましたが、
C言語自体は勉強してほしい言語の中でもナンバーワンだと考えています。
ちなみに、次点がJavaScriptですが、これは別の機会に。
C言語理解したマンの何が良いかというと、
ここまで長々と解説してきた「ポインタ」を理解していることです。
なんとなく使うことはできるかもしれませんが、
ポインタが理解できないとまともにコードが組めません。それがC言語の利点であり欠点です。
というわけで本題
さて、実はここからが本題です。
本稿は「プログラミング技術の変化で得られた知見・苦労話」のアドベントカレンダーですので、
「C言語を学んだおかげで原因がすぐ分かった・解決できた」という話をしたいと思います。
CASE1: 消えた参照渡し
これは、とあるJava現場のお話です。
参照渡しをしているはずのJavaBeansの内容が消えたと言われたのです。
さて、問題のあったコードを見てみましょう。
/**
* Bean内容の初期化
* @param 書き換え対象のBean
*/
public void initBean (XXXBean bean) {
bean = new XXXBean();
bean.setValue1("");
bean.setValue2(0);
}
// ~~~メソッドの呼び出し部分
bean.setValue2(3);
// 内容を初期化
initBean();
System.out.println(bean.getValue2()); // 0が主力されてほしい
この実行結果は3でした。
さて、ここで0が代入されたはずのbean.value2に0が入らなかったのはなぜでしょうか。
問題のポイントはここです。
bean = new XXXBean();
この行を削除して実行することで、正常に動作するようになりました。
なぜ初期化されないのか
削除した行でbeanを初期化してしまったため、beanに対するアドレスが変わってしまったのです。
実は、C言語に限らずほとんどの言語は、インスタンスのタイミングでアドレスが確保されます。
そのため、定義した変数の実態は、そのインスタンスが格納ているメモリ空間のアドレスです。
オブジェクト指向型言語の多数では、インスタンス生成タイミングでメモリ領域を確保し、
変数に対してアドレスを代入します。
// この時点では何も入っていない = どのアドレスも参照していない
XXXBean bean = null;
// この時点でメモリ領域が確保され、そのアドレスが変数に格納される
bean = new XXXBean();
実は、よく目にする「NullPointerException」にもポインタの名前が入っていますね。
つまり、ポインタ参照先がnull(指定アドレスが存在しない)ことが原因なんです。
コーディングする人はポインタをよく知らなくても、実はしょっちゅう目にしているんです。
余談ですが、Javaにおけるnullは「値なし」ではなく「アドレスなし」が正しい表現になります。
プリミティブ型は実態がアドレスではないため、nullを代入することができません。
nullを許容したい場合には、ラッパークラスを使うことで実現できます。
// これはコンパイルエラーになる
int a = null;
// これなら動く
Integer a = null;
Javaの参照渡しとアドレス書き替え
Javaでは、プリミティブ型以外の引数で参照渡しで渡されます。
Javaの参照渡しも、C言語同様にインスタンスのアドレスを渡しているだけなのです。
下記のように new XXXBean()
をすると、新しいメモリ領域が確保され、
beanという変数にアドレスが書き込まれます。
public void initBean (XXXBean bean) {
// この時点で0x0001を参照している
bean = new XXXBean(); // - (1)
// この時点で、beanは0x0231を参照している
bean.setValue1(""); // - (2)
bean.setValue2(0);
}
※コメント内のアドレス表記は適当です。
さて、ここで気を付けないといけないのが、
前述の通り厳密にはJavaで言われている参照渡しは本当の参照渡しではなく、**「アドレス番号を値渡ししている」**ということです。
そのため、(1)の時点で違うアドレスが引数として宣言されている変数に設定されてしまい、
ちがうインスタンスを操作することになってしまうのです。
このように、参照渡しを勘違いすることでバグを仕込んでしまうことはよくあります。
ただ漠然と
「引数として受け取ったインスタンスは初期化してはいけない」
ではなく、ポインタとアドレスの概念を知ることで
「アドレスが書き換わるから参照渡しができなくなる」
ということが判断できるようになります。
CASE2: 渡されないJavaScript引数
次はJavaScriptの話です。
ボタンクリックのイベントをJavaScriptから設定しようと考えています。
イベントの処理は関数で定義したいため、onClickedという名前で関数を作成しています。
function onClicked(e) {
e.stopPropagation();
console.log('clicked.');
}
window.onload = function() {
var btn = document.getElementById('btn');
btn.onclick = onClicked();
}
<button id="btn">イベントが発火するよ</button>
これを実行しようとしたところ、以下のエラーが発生しました。
Uncaught TypeError: Cannot read property 'stopPropagation' of undefined
ちなみに蛇足となりますが、
e.StopPropagation()
はクリックイベントを伝搬させない場合のテクニックです。
普段からJavaScriptを書いている方々は違和感に気づくと思いますが、
ここでポイントとなるのは、以下の行です。
btn.onclick = onClicked();
これでは、onClicked関数の実行結果をbtn.onclickに代入してしまいます。
今回は呼び出す関数を定義したいので、以下のように修正することで想定通りの動きになります。
btn.onclick = onClicked;
このように、JavaScriptでは関数名を代入することでその関数を自在に扱うことができます。
実は、このテクニックはC言語ではよくつかわれるもので、関数ポインタと呼ばれています。
関数ポインタは、その名の通り「関数のアドレス」を表します。
C言語は、以下のように関数自体を変数として定義することができます。
#include <stdio.h>
/* 足し算 */
int add(int a) {
return a + a;
}
/* 掛け算 */
int multiple(int a) {
return a * a;
}
int main(void) {
/* ポインタ関数の変数を定義 */
int (*fn)(int);
int a = 3;
fn = add; /* 足し算 */
printf("%d\n", fn(a)); /* -> 6 */
fn = multiple; /* 掛け算 */
printf("%d\n", fn(a)); /* -> 9 */
}
JavaScriptでは無名関数をコールバック引数として渡すことが多々あります。
先ほどのコードは明らかにレガシーコードですが、通信を行うfetchや、StreamAPIもコールバック関数を多用します。
const arr = [2, 3, 5, 8, 9, 12];
const found = arr.filter(elem => elem % 2 === 0);
console.log(found); // -> 2, 8, 12
なお、上記のコードは次のように書き換えることができます。
function getEven(elem) {
return elem % 2 === 0;
}
const arr = [2, 3, 5, 8, 9, 12];
const found = arr.filter(getEven);
console.log(found); // -> 2, 8, 12
引数として関数の定義を渡しているのですが、
内部処理としては関数の定義(ポインタ)を渡していることになります。
JavaScriptは一見簡単そうに見えますが、
型定義があいまいなため、なんでも変数にぶち込むことができます。
ソースコードを読んでも何をしているのか(その変数の型が何なのか、実態は何なのか)が分かりにくくなっています。
ES2015対応のJavaScriptを学習するうえで、この関数定義が難しいと感じる方が多いようですが、
内部的には関数ポインタを渡すことになります。
ポインタの概念さえ知っていれば簡単に理解ができるでます。
そういう意味では、JavaScriptはC言語に近い言語なのかもしれませんね。
さいごに
今回は、JavaとJavaScriptを例として挙げましたが、ほかの言語でも似たようなことが起きえます。
思いもしない挙動が目の前で起こったとき、それはポインタが原因かもしれません。
いきなりC言語を勉強することはおすすめしません。難しいので。
しかしながら、体験談から話すと
C言語を知っていたおかげで現象をすぐ理解したり、
言語仕様の意味を理解できたことは結構多かったです。
そう、私はC言語に助けられてここまできたのです。
モダンな言語を使うのはもちろん大事ですし、常にキャッチアップが必要ですが、
何か起きたときにはやはりベースとなる低レイヤーの知識が必要になることがいつか出てきます。
すべての言語が最終的にはすべて機械語に変換されるように、
コードを含むデータはすべてメモリ空間のアドレスで管理されるのです。
もし、今のモダン言語をマスターしたと思ったら、一度C言語のポインタで遊んでみてはいかがでしょうか。
余談
そう考えると、Objective-Cってポインタバリバリ使うし、案外楽しかったかもなぁ。
Swiftが楽すぎるのでもう戻りたくないけど。