C言語を試してみよう

C言語を試してみましょう.

ハローワールド!(まずはここから)

新しくプログラミング言語を習うときには,多くの場合,文字列の出力の仕方から学びます.「Hello World!」と出力させることが多いので,ハローワールドと呼ばれることもあります. では,そのプログラムをみてみましょう.

3行目の int main() から説明しましょう.これはプログラムはここからはじまるという宣言です.そして,4行目の { から8行目の } までがプログラムのひとかたまりです.

5行目の printf(" ") は,ダブルクオーテーション" "で囲まれた部分を出力するという命令(関数)です(関数については後述します).そして \n は出力の後に改行をせよという命令です.これはあってもなくても一見して出力は変わりませんが,次の行よっては違いがみえてきます.

6行目の先頭の // はその行はコメントであり,プログラムを実行する際には無視されます(プログラミング言語によって,# だったり % だったりします).違いをみてみるために,// を外して実行してみましょう.5行目に \n を書いている場合と書いていない場合で出力が異なるはずです.

最後に1行目の #include <stdio.h> に戻ります.これは,printf を用いるための必要な宣言です.とりあえずのところは,おまじないと思っておけばよいでしょう(関数について説明するときに少し詳しく説明します).

なお,7行目の return 0 はプログラムが正常に終了したことをOSに伝えるものです.無くても動きます.但し,return 1 などとすると,別の指示を連続して実行できなくなったりします.ひとまず,「おまじない」と思っておいて問題ありません.

書式

次のプログラムです.

出力は

3/2 is 1.

となるはずです.はじめてこのようなプログラムをみると,printf の中の %d にぎょっとすると思います.これについて説明しましょう.

対応する部分を色分けして表示してみると次のようになります.

printf(" %d / %d is %d . \n", 3, 2, 3/2);

出力されるのは,上で学んだ通り," " で囲まれた部分です. まず,最初の%d には 3 が下の表が示す通り整数として10進法で表示されます. これが分かれば,次の %d には 2 が整数として10進法で表示されるのも明らかでしょう. 問題は最後の緑色の箇所です.ここでは,%d3/2 が整数として表示されるわけですが, 3/2 を整数とは?? と疑問に思いますよね.実際には,3/2 の小数点以下を切り捨てて 1 と表示されています.

書式指定 意味
%d 整数を10進法で表示
%f 実数を表示
%c 文字(' 'で囲まれた半角文字1個)を表示
%s 文字列(" "で囲まれた半角文字列)を表示

課題

出力が

    3/2 is 1.5.
    
となるようにプログラムを修正してみましょう.

注意

この課題の解答は一通りではありません.webや書籍で書式の細かい指定方法について調べていろいろ試してみましょう.
一方で,現時点で「書式」についてあまりよく理解できなくても,本講義のこれからの内容を理解するうえで大きな問題にはなりません. 例えば,科学技術計算ではmatlab(や近年では本講義でも扱うJulia)等がよく用いられますが,プログラムを書くときに書式を意識することはほとんどありません. 計算結果を「小数点何桁まで表示させたい」あるいは「指数表現で表示させたい」といったときに,「そういえば書式という概念があったな」と思い出し,適切に検索できればよいと思います.

条件分岐

プログラムを書く時,場合分けをしてコンピュータに処理をさせたいことがよくあります. こういうときに用いるのが 条件分岐,通称 if 文 です.

プログラム言語にも依りますが,基本的には次のような構造になります.

if (条件1)
    命令1 (←条件1が満たされていれば,命令1を実行)
    elseif (条件2)
        命令2 (←条件1が満たされておらず,条件2が満たされていれば,命令2を実行)
    else 
        命令3

if だけの場合,else if が複数ある場合,else が無い場合など様々なバリエーションがあります.

C言語の一例をみてみましょう.

まず最初の int a = 5 は,まさに「a は 5」という宣言です.直前の int については事項で説明しますので,一旦無視しておいてください.

さて,肝心の if ( ) の括弧の中身をみてみましょう.a%2 は「aを2で割った余り」を意味します.そして,括弧の中は「aを2で割った余りが0」という条件になります(「何かが何かに等しい」という条件を表すときには多くの言語でイコールを2つ書きます).これが条件が満たされていれば,if の直後の命令が実行され,満たされていなければ elseの直後の命令が実行されます.いま,a = 5 ですから,この条件は満たされていません,従って,else の直後の命令が実行され,5 is odd. と出力されるわけです.

なお,if に関する中括弧 { } を省略してもOKです(ただし,実行する行が一行の場合に限ります).

改行,インデント

上のプログラムでは, int main() の直後に(同じ行に) { が書かれています. これまでは一旦改行してから書かれていました. 実は,C言語では,改行や空白はあってもなくてもプログラムとして違いはありません. しかし,例えばプログラムを1行で書いてしまうと,どこからどこまでがひとかたまりで,全体がどういう構造になっているのか,他者が見たときに分かりません. おそらく,書いた本人も翌日になると混乱するでしょう. なお,いまのところは数行のプログラムですが,大きなプログラムになると,数十行,数百行,数千行,数万行になることも珍しくありません. 従って,適切に改行と空白(インデントといいます),さらにはコメントを入れたプログラムを書くことが重要です. さらに,改行や空白が必ず必要な言語もありますので,C言語のような言語を扱うときも改行や空白を適切に挿入する習慣をつけておくことをおすすめします(C言語は他の言語よりプログラム全体が長くなる傾向があるのでなおさらです).

型:計算機の中の数の数の体系を理解しよう

計算機の中では無限は扱えません.たとえば,円の面積を計算したいときに,半径が整数ならば「半径 x 半径」の部分は厳密に計算できても,円周率をかけるときはどうしても有限桁で打ち切った近似計算になってしまいます.では,計算機の中で,整数や実数(あるいは文字)はどのように表現されているのでしょうか.

ビット,バイト,...

計算機の中は二進法であるという話は聞いたことがあると思います.また,スマートフォンやPCを購入する時,ギガバイトやテラバイトという言葉を聞いたことがあると思います.これらについての理解からはじめましょう.

コンピュータの中で,情報(の最小単位)は0か1で表現されます.そこで,2通りの情報を表現する最小単位をビットと言います.「ボールを一つ入れることができる箱1つ」というイメージです.すなわち,その箱にはボールが入っているか入っていないかの2通りがあります.そして,コンピュータの中にはこの小さな箱が非常にたくさんあるとイメージすればよいでしょう.もっとも,実際には,コンピュータの中に箱があるわけではなく,電圧の高低でボールが入っているか入っていないかを区別します.

この最小単位の小さな箱が8個集まったものが1バイトと呼ばれます.すなわち「1バイト=8ビット」です.箱が8個あるわけですから,2^8 = 256 通りの状態が表現できるわけです.これら8個の箱で整数を表そうとすると,0以上の整数ならば,0〜255まで区別できます.負の数も含めると-128〜127まで区別できます.

その次の単位は,「1キロバイト=1000バイト」です.よく 1KB と表現されます.テキストファイルや小さなワードファイルなどでよくみかけると思います.

その次の単位は.「1メガバイト=1000キロバイト」です.よく 1MB と表現されます.スマートフォンで撮影した写真がだいたい数百KB〜数MBくらいです(後述します).

その次の単位は,「1ギガバイト=1000メガバイト」です.よく 1GB と表現されます.ノートPCのメモリが8GB〜32GB,ハードディスクやSSDが数百GBからその次の単位である数TBです.

その次の単位は,「1テラバイト=1000ギガバイト」です.よく 1TB と表現されます.日常生活で目にするのはこのあたりまでですが,もちろん更に大きな単位もあります.

余談:写真の情報量

例えば,iPhone7で写真を撮ると,縦3,920 x 横2,940 の各点に(赤,緑,青)の情報がそれぞれ256段階で表現されます.すなわち,各点の色を表現するのに 3B(バイト)使います.従って,写真全体では 3,920 x 横2,940 x 3B でおおよそ 32MB ということになります.ということは,30枚くらいの写真を保存するのに1GB必要で,300枚くらいになると10GB必要ということになります.しかし,みなさんの使っているスマートフォンの容量とその中に保存されている写真(や動画)の枚数を考えると,計算が合っていないように感じるでしょう(実際にはもっとたくさんの写真が保存できているはずです).実際,それぞれの写真のファイルサイズを調べてみると,数百KBから数MBのことがほとんどです.「説明と矛盾しているではないか!?」と思われるかもしれませんが,そんなことはなくて,実は,写真を保存するときには圧縮という技術が使われています.そこではそれなりに数学的に高度な技術が使われています.この講義の後半(7月頃)に,圧縮(ただし,実用的に使われているものではなく,1年生の線形代数で理解できる非常に初歩的な方法)についても解説する予定です.

さて,ここからが本題で,数字をどのように(何バイトで)表現するのかについて説明しましょう.ここでは,まず代表的なものを3種類紹介します.

型の名前 意味 バイト数(ビット数) 入る値のおおまかな範囲
int 整数 4 (32) \(-2,147,483,648\sim 2,147,483,647\)
float 実数 4 (32) \(-3.4\times 10^{38} \sim 3.4\times 10^{38}\)
double 実数 8 (64) \(-1.7\times 10^{308} \sim 1.7\times 10^{308}\)

整数を int 型 で表すとき,4バイト使うことになります(箱32個).従って,int 型で表現可能な整数には上限と下限があります.float 型double 型 はどちらも実数の型ですが,使用するバイト数が異なります.double型の方が多くの箱を使うわけですから,当然表現能力も高いわけです.すぐに,double 型 を例にどのように実数が表されるのか説明しますが,感覚として,float 型は有効数字7桁程度の実数,double 型は有効数字15桁程度の実数,というふうに認識しておくとよいでしょう.

では,double 型でどのように実数を表現するのかについて説明します.まず,64個の箱の使い方を理解する必要があります.double 型では,64個の箱を「符号部」,「仮数部」,「指数部」の3つに役割分担します.

たとえば,実数を

$$- 1.234567 \times 10 ^{-5}$$

と表現するとき,「\(-\)」を「符号部」,「\(1.234567\)」を「仮数部」,「\(10^{-5}\)(の特に\(-5\)の部分)」を指数部といいます. 64ビットの内訳は

ビット数
符号部 1
仮数部 52
指数部 11

と決められています.

符号部は,\(+\) と \(-\) を区別できればよいので,箱は一つで十分であることは明らかでしょう.

仮数部には 52 個の箱を使えるわけですから,\(2^{52}\approx 10^{15.65}\) 通りを区別できます.すなわち,10 進法では有効数字が \(15\sim 16\) 桁ということになります.

指数部には11個の箱を使います.つまり,\(2^{11} = 2048\) 通りを区別できます.通常,\(2048\) を \(-1022\sim 1023\) の \(2046\) 個の整数と残り \(2\) 個に割り当て, 指数部としては,\(2^{-1022}\) から \(2^{1023}\) までを表現できます.残りの\(2\)個分は,\(0\) と \(\mathrm{NAN}\) を区別するために利用します.

ここまで,コンピュータの中では実数は有限桁でしか表現できないことを説明しました. 別の見方をすれば,コンピュータが扱える数は 有限個 ということになります. これらを全て集めてきた集合を \(\mathbb{F}\) とすれば,\(\mathbb{F} \subset \mathbb{R}\) が成り立ちます.

誤差

以上では,コンピュータの中では実数は有限桁でしか表現できないことを説明しました. このことにより,計算の中で様々な誤差が混入してしまいます. 従って,コンピュータで計算する際には,「どのような誤差があるかを理解すること」,「混入する誤差をできる限り小さくすること(そのようにプログラムを工夫すること)」,「全体の計算結果を,誤差があることを前提に理解・解釈すること」などが重要になります.

ここでは代表的な誤差を 3 種類紹介します.

1つ目は,「丸め誤差」です. たとえば,\(a, b\in \mathbb{F}\) に対して,\(a+b\) は \(\mathbb{F}\) に含まれているとは限りません(計算機で表現可能とは限りません). 従って,\(a+b \not\in \mathbb{F}\) のときは,\(\mathbb{F}\) の中で \(a+b\) に近いもので代用します. その操作を「丸める」といい,その時の誤差を「丸め誤差」といいます. 従って,ほぼ全ての演算で丸め誤差が混入すると言っても過言ではありません. 一つ一つの演算の丸め誤差は小さくても,何万回,何億回と演算を行うと,「塵も積もれば山となる」という状況になりかねないので,丸め誤差には十分注意する必要があります(丸め誤差を抑制するような演算方法も知られています).

2つ目は,「積み残し」です. 絶対値の大きい数と小さい数を足し引きする場合,有効桁数の問題から,小さい数が無視されることがあります.一例を挙げましょう. 例えば, $$\sum_{k=1}^\infty \frac{1}{k^2} = \frac{\pi^2}{6} \ (= \ 1.64493406684822\cdots)$$ ですが,matlabというソフトウェアで倍精度で数値計算すると,\(k=94906266\) 以降は \(\frac{1}{k^2}\) は小さすぎて無視されます. 実際,\(1/94906266^2 \approx 1.110223015834071 \times 10^{-16}\) であり,これより小さい数を足し込んでも全くカウントされません. \(10^{-16}\) 程度の数なので,その影響は大きくないと想像するかもしれませんが,この時点での部分和は \(S_{94906266} = 1.644934057834575\) であり,正しい収束先と比較すると,倍精度で計算したにも関わらず,\(8\) 桁しか合っていない. このような減少を避けるための簡単な処方箋として,「小さい方から足していく(ただし,どの程度小さいところから足していけばよいかは自明ではない)」といったことが考えられる.

3つ目は,「桁落ち」です. 近い数の引き算をする際,有効桁数が著しく減少することがあり,この減少を「桁落ち」といいます. たとえば, $$1.0000002 \times 10^2 − 1.0000000 \times 10^2 = 2 \times 10^{-5}$$ であり,有効数字 7 桁同士の数同士を引き算した結果,有効数字が 1 桁になってしまっています. 倍精度の演算であっても,一回の演算で一気に10桁近く有効数字の桁数が減少することがあり注意する必要があります(一旦桁落ちが起こると,基本的にその回復はできません). 言い換えれば,計算結果の仮数部は常に(10進法で)\(15\sim 16\)桁ですが,(桁落ちが生じると)その中で本当に意味を持つ値は最初の数桁,あるいは最初の一桁すら意味を持たないといったことが起こる可能性があります. 桁落ちは「知らないうちに発生する」ことが多く,常に注意を払う必要があります. 簡単な一例を見てみましょう. 2次方程式 \(ax^2 + bx + c = 0\) の解は $$x_\pm = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$$ ですが,\(b^2 \gg 4ac\) のとき分子はおおよそ \(-b \pm |b|\) であり,\(b\) の符号によって \(\pm\) のどちらかは非常に近い数同士の引き算に対応します. そして著しい有効数字の減少が起きてしまいます. これを避けるための処方箋も知られています. たとえば,\(b>0\) の場合は \(\pm\) のうち \(+\) の計算が問題になりますが,有理化をして $$x_+ = \frac{-2c}{b + \sqrt{b-4ac}}$$ を計算すれば桁落ちは避けられます(分子は非常に近い数同士の「足し算」になっています).