今回は、「関数と受け取る値(引数)」の関係を詳しく説明します。関数はプログラム内で次のような役割を果たします。
"関数とは、ある値を受け取り、必要な処理を行った上で、その結果を返す"
これはありきたりの関数定義です(物理学、電磁気学、あるいは、力学などに見られる計算式そのものです)。しかし、この定義文からも分かるように、関数の果たす役割はきわめて単純です。物理学者ならその物理学的な世界観(仮定)を数式化し、関数として表現することになるでしょう。科学者ならその科学的な認識内容を数式化し、関数として表現することになります。
プログラミング言語Cを設計したDennis Ritchie氏は、若いとき物理学者を目指していました(本「IT談話館」の参考連載)。物体と物体の間にある微妙な関係を理解したい場合、高度な数式が登場することになりますが、その計算を自らの手で行うのではなく、計算機に任せたいと誰でも願います。質量、移動速度、抵抗、形状、移動位置などの立証計算はかなり面倒なものです。おそらく、プログラミング言語C誕生の根はこのようなところにあったのでしょう。物理学者とはいえ、さすがにアセンブラ(機械語)で計算式を表現するのはかなり面倒なことだったのではないかと想像できます。
数式の表現。筆者が知る限り、Ritchie氏、Java設計者のJames Gosling氏、C++設計者のBjarne Stroustrup氏などは数式の表現という角度からコンピュータの世界に入ってきています。Ritchie氏は晩年になっても物理学への関心を示し、おそらく、「数式をコンピュータを使って表現し、その場で解を得る」という発想から抜け出ていないと思われます。Gosling氏も同じように、数値計算に強い関心を示し、組み込み制御分野に視線が向いています。
一方、Stroustrup氏は、プログラミングは数式の表現という発想から抜け出し、"コンピュータを使った人間の表現"へ、と一歩踏み出している印象を受けます。同氏は、"数学は数学オタクのためにあるのではなく、人のために役に立つものでなければならない"、というニュアンスのことを述べています。Stroustrup氏は、人の経験を何よりも重んじ、観念論を激しく攻撃します。同氏のこの世界観や人生観は、現場で働くプログラマの尊重、高級なマシンを持たない平均的な開発者の尊重へ、と成長します。
前回は、次のようなサンプルプログラムを紹介しました。
#include <stdio.h>
int main()
{
printf ("I don't know anything about programming!\n");
return 0;
}
このサンプルプログラムは、CとC++の最新標準仕様に準拠するものです。つまり、最近のCとC++コンパイラ・リンカを使用すれば、問題なく、目的の実行プログラムが完成します。さらにこのコードは、C++から見れば、"CはC++のサブセット"、という一面をはっきり示しています。
今回は、このサンプルサンプルプログラムのprintf関数の意味を詳しく説明することにします。
printfは、(標準)関数の一つです。この関数は、C標準化委員会が定めたものであり、「stdio.h」というファイル内で次のように定義されています。
int printf(const char *, ...);
このような情報は一般には、「関数の宣言」、と呼ばれています。プログラミング言語Cをこれから学習しようとする人は、本来なら、この関数宣言の意味や位置付けをきちんと理解し、その基礎知識を活かす形で、printf関数を使用できるようにならなければなりません。しかし、この関数宣言を見て分かるように、たいへん難しいものです。この関数宣言から、なぜ、次のようなコードが記述できることになるのでしょうか。
printf ("I don't know anything about programming!\n");
関数宣言の「const char *, ...」の部分に、なぜ、"I don't know anything about programming!\n"というコードが記述できるのでしょう。そして、なぜ、このコードはなんの問題もなく、コンパイル・リンクできるのでしょう。考えてみれば本当に不思議です。
関数宣言内に含まれているcharというのは、characterの略語であり、「文字」という意味を持っています。一方、printf関数が実際に受け取っている"I don't know anything about programming!\n"は、文字ではなく、(複数の文字を連ねた)文字列、というものです。先の関数宣言は、printf関数が「文字」を受け取る関数であるといっていますが、実際には「文字列」を受け取っています。これはどう考えても辻褄が合わず、論理的に破綻している印象を受けます。しかし、プログラムは何の問題もなく完成してしまいます。考えれば考えるほどまったく不思議です!
「文字」を受け取るべき関数が、複数の文字で構成される「文字列」を平気で受け取っても正常に動作してしまう現実。これは明らかに論理的な整合性を欠いているように見えます。プログラミング言語Cを設計したRitchie氏は、機械語や電子回路に精通しています。同氏は、文字、文字列、計算機の関係を知り尽くしています。計算機から見た場合、文字と文字列の間には共通点があります。それは、いずれもメモリに格納される、ということです。
関数宣言内には、charに続けて、「星印(*)」と「...」がこっそり置かれています。この星印こそ、メモリ(「...」はメモリの連続性を示す)を意味するものなのです。自分の考えていることを計算機で素直に表現したいと考えている私たちですが、その前に、星印の意味を理解する必要があります。現代人である私たちは、printf関数宣言から、表現者の立場を捨て、関数の奴隷になることを強要されているような錯覚さえ覚えます。
文字と文字列の共通点はいずれもメモリに格納されることです。これは間違いありません。しかし、簡単なあいさつ文を画面に表示するために、メモリを意識しなければならない現実はたいへん厳しいものです。さらにいえば、「char *」というコードは、誰が見ても不自然で、しかも、たいへん醜いものです。
「char *」は、私たちが表示しようとしている文字列ではなく、それを記憶するメモリを表しています。このprintf関数宣言に沿って、先のサンプルプログラムを忠実に書き換えると、次のようになります。
#include <stdio.h>
int main()
{
const char* aisatsu = "I don't know anything about programming!\n";
printf (aisatsu);
return 0;
}
ちょっとした変更を加えています。プログラミング初心者の方は、2つのサンプルコードを何度も比較し、加えられた変更をしっかり理解するようにしてください。関数の宣言をよく理解し、宣言に従って素直にコードを記述していけば、その関数を簡単に使用できます。関数宣言は、関数の使い方を教えてくれる貴重な情報です。
関数宣言では、constというコードも使われています。constはconstantの略語で、変更されることのない一定の値、という意味です。このため、aisatsu文を変更する次のコードはコンパイルされません。
#include <stdio.h>
int main()
{
const char* aisatsu = "I don't know anything about programming!\n"; // 文字列を変更しないと宣言
*aisatsu = "I am a good programmer!\n"; // しかし、変更しようとしている
printf (aisatsu);
return 0;
}
このコードは、aisatus文を変更しない(const)といっておきながら、その直後で変更しています。これでは論理的に矛盾しています。
私たちは、知人の結婚式に祝電を送ることがよくあります。結婚式の司会者はその祝電を変更せずにそのまま読み上げるのが普通です。printf関数は、結婚式の司会者に相当すると考えると分かりやすいと思います。このような事実を総合すると、プログラミングと私たちの日常生活は、それほどかけ離れていないことが分かると思います。C++以降のプログラミング言語はさらに人間社会を反映するようになります。
先ほど、printf関数は文字列ではなく、実際には、その文字列が記憶されたメモリ(の位置、つまり、アドレス)情報を受け取っていると述べました。この視点に立つと、いろいろな(ある意味では、滅茶苦茶な)コードが書けるようになります。たとえば、次のコードを見てください。
#include <stdio.h>
int main()
{
const char ch1 [] = "I don't know anything about programming!\n";
const char* ch2 = "I don't know anything about programming!\n";
printf (&ch1[0+3]);
printf (&ch2[5]);
}
このコードでは、配列、ポインタ、文字列、アドレス演算子(&)、添え字([])という概念が使われています。これらのすべての概念を覚える必要はありませんが、次の2つのコードに注目してください。
printf (&ch1[0+3]);
printf(&ch2[5]);
これら2つのprintf関数は、メモリアドレスを露骨に受け取っています(&ch2[5]の&はアドレスを示しています)。私たちは簡単な文字列を画面に表示したいと考えていましたが、ご覧のように、メモリというハードウェア部品の知識を要求されています。「const char*」は文字列ではなく、実際には、メモリを表現しているのです。上のコードは、メモリは表現道具である、ということではなく、どちらかといえば、直接操作できる対象(電子部品)である、という事実(Cの設計思想)を見事に示しています。
今回は、Cプログラムを作成する際には、関数に加え、メモリというものを理解する必要があることを学びました。かなり厳しい現実ですが、プログラミング言語Cの誕生の原動力は、「電子回路を使って効率的に算術計算を行いたい!」、あるいは、「難しい複雑な計算を楽して済ませたい!」、という物理学者たちの夢であったと考えてしまうと分かりやすいかもしれません。