その4

Table of Contents

やること

関数を作ってみる

数学では関数がよく出てきますが,プログラミングででてくる関数は少し意味が異なり, 指定された方法で実行すると,決まった処理をして結果を返すというものです。

関数を使う意義

プログラミングでは,数学で言う関数 \( y = f(x) \) のパラメータ \(x\) のことを引数(ひきすう)と言います。 数学で言う関数 \(y\) の値 \(f(x)\) を,プログラミングでは 「関数 \(y\) が値 \(f(x)\) を返す」といいます。

ほとんど同じ処理がプログラム中で何回も出てくるとき, その処理を関数内で行うとプログラムの見通しが非常に良くなります。

例えば,セルシウス温度 \(T_d\) を絶対温度 \(T_K\) に変換する式は

\[ T_{K} = T_{d} + 273.15 \]

ですが,

double tempD1 = 25.5;
double tempK1 = tempD1 + 273.15;

double tempD2 = -77.0;
double tempK2 = tempD3 + 273.15;

double tempD3 = 500.0;
double tempK3 = tempD3 + 273.15;

と書くのはうっとうしいものです。

これを関数を使えば

double tempK1 = f(25.5);

だけで tempK1 に 298.65 が代入することができます。

書き方

という2つが必要です。 そう言われてもよくわからないと思うので,サンプル

#include <iostream>
using namespace std;

int sum( int val1, int val2 )
{
    return val1 + val2;
}

int main ()
{
    int value = sum( 7, 3 );
    cout << "value: " << value << endl;
    return 0;
}

を見ながら勉強していきましょう。

int main 以下

細かい理解は置いといて,

C/C++言語では最初にmain関数が実行される

ということを天下り的に受け入れてください。 では,main 関数の中身を見ていきましょう。

1行目にいきなり sum という名前の関数を使っています。

int value = sum( 7, 3 );

細かいとこはわからないと思いますが,この行の意図が 「int 型変数 value に sum という関数を使って 7+3 を代入している」 ということは想像できると思います。

では sum という関数はどんな関数なのでしょうか? これは main 関数だけを見てもわかりません。 コンピュータにとっても同じで,コンパイル時に sum という関数が main 関数よりも前に定義されていないと コンパイルできません。

sum については次の節で見るとして,main 関数残りをささっと確認しましょう。

cout << "value: " << value << endl;
return 0;

先ほど作った value という変数を画面に表示していますね。 最後の “return 0;” という部分は次の節で理解できるようになります。

int sum( int val1, int val2 )の部分

最初の行は

int sum( int val1, int val2 )

となっています。

これで関数の宣言をして,定義を始めています。 宣言だけなら

int sum( int val1, int val2 );

の用に,文末に ; を打つだけです。

これらの書式は

関数の返す型 関数名( 引数の型1 引数の名前1, 引数の型2 ... )

です。

つまり

書式 設定
関数名 sum
返す型 int
引数1 int 型 val1
引数2 int 型 val2

という関数の宣言を行っています。

関数を定義しているのが 2 行目以降です。

{
    return val1 + val2;
}

return は return 文というもので,値を返すときに使います。 ここでは val1 + val2 という値を返しています。

つまり sum という関数は int 型引数 val1 と val2 の和を 返す関数ということがわかりました。

3. まとめ

1, 2 節からサンプルコードの意味がわかったかと思います。

サンプルは非常に簡単なので,わざわざ関数を作る必要性を 感じないかもしれません。 ですが,似たような処理を繰り返せば繰り返すほど,処理が複雑であれば複雑であるほど,関数の威力は大きくなります。 なるべく使うように心がけましょう。

練習として,課題1, 課題2 をやってみましょう。

分割コンパイル

プログラムが複雑になるに連れて,ソースファイルも巨大化していきます。 数百,数千行のファイルを編集するのは非効率的でミスも生まれやすいので,ファイルを分割するのが定石です。

例えば 課題1 で作成した main 関数と Deg2Rad 関数を2つのファイルに分割してみましょう。 おそらく

#include /*省略*/

double Deg2Rad(/*省略*/)
{
 /*省略*/
}

int main()
{
 /*省略*/
}

という形になっていると思います。 これを分割していきましょう。

流れ

分割コンパイルは

  1. 各ファイルごとにコンパイルし,オブジェクトファイル (*.o) を作成する
  2. 各オブジェクトファイルをくっつけ,一つの実行ファイルを作成する

というものです。

やってみる

上のソースを main.cc と Deg2Rad.cc の2つに分けましょう。

Deg2Rad.cc

#include /*省略*/

double Deg2Rad(/*省略*/)
{
 /*省略*/
}

簡単ですね。

main.cc

#include /*省略*/

double Deg2Rad( /*省略*/ );

int main()
{
 /*省略*/
}

注意すべき点は,main 関数に Deg2Rad 関数の存在を教える必要があるということです。 そうしないと,main 関数の中で,Deg2Rad 関数をどう使えばいいかわからなくて,コンパイラが困ってしまいます。 3 行目がこれに当たります。 関数の具体的な記述は省き,宣言だけを記述しています。

コンパイルしてみる

ソースごと

g++ の -c オプションを使い,以下のようにします。

# ディレクトリの中身
mail:temp genki$ ls
Deg2Rad.cc main.cc

# main.cc から main.o を作ってみる
mail:temp genki$ g++ -c main.cc

# 様子見
mail:temp genki$ ls
Deg2Rad.cc main.cc    main.o

# Deg2Rad.cc から Deg2Rad.o を作ってみる
mail:temp genki$ g++ -c Deg2Rad.cc

# 様子見
mail:temp genki$ ls
Deg2Rad.cc Deg2Rad.o  main.cc    main.o

もし,main 関数で Deg2Rad 関数の宣言がないときは

mail:temp genki$ g++ -c main.cc
main.cc: In function ‘int main()’:
main.cc:11: error: ‘Deg2Rad’ was not declared in this scope

というエラーが帰ってきます。

くっつける

g++ で全オブジェクトファイルをコンパイルします。

mail:temp genki$ g++ main.o Deg2Rad.o
mail:temp genki$ ls
Deg2Rad.cc Deg2Rad.o  a.out      main.cc    main.o
mail:temp genki$ ./a.out
1 degree is 0.0174533

こういう感じです。

宣言をヘッダーファイルにまとめる

上では main.cc に Deg2Rad 関数の宣言を書きましたが,この方法には問題があります。 例えば

のような状況になると,上の方法は非常に煩雑になってしまいます。 type-a の関数を一つだけ以下のように少し変更したとき

int function() -> int function( int number )

この関数に依存するファイルにある全ての宣言を修正する必要があります。 このように,面倒なことが起こり,余計な手間やミスの温床になります。

これを回避するには,宣言を書く場所を関数ごとに 1 ヶ所だけにし,依存するファイルがこの宣言を読み込めばいいのです。 実践してみましょう。

Deg2Rad.hh

Deg2Rad.cc に対応するヘッダーファイル Deg2Rad.hh を以下のように作成しましょう。

#include /*省略*/
double Deg2Rad( /*省略*/);

これだけです。

Deg2Rad.cc

.cc ファイルも少し改造しましょう。

#include "Deg2Rad.hh"

double Deg2Rad(/*省略*/)
{
 /*省略*/
}

変更点は include の部分です。 include するものもヘッダーファイルに書いてしまいしょう。

main.cc

作成した Deg2Rad.hh をインクルードしましょう。

#include "Deg2Rad.hh"
#include /*省略*/ // その他必要なもの

int main()
{
 /*省略*/
}

前には書いていた Deg2Rad 関数の宣言は消しました。

またコンパイル

ヘッダーファイルはコンパイルすつ必要はありません。 Deg2Rad.cc, main.cc をそれぞれコンパイルし,main.o, Deg2Rad.o をくっつけましょう。

改善点(多重読み込み対策)

ヘッダーファイルを自分で作成すると,インクルードの無限ループを作る可能性があります。 つまり

  1. header1.hh で header2.hh を読み込む
  2. header2.hh で header1.hh を読み込む
  3. header1.hh で header2.hh を読み込む
  4. (無限に続く)

これを避けるのがインクルードと呼ばれるテクニックです。 やり方は簡単で,ファイルの先頭に

#pragma once

を書くだけです。

古いやり方では,

#ifndef __header_hh__
#define __header_hh__

/* 省略 */

#endif

のようにファイルをくくります。 ここで "__header_hh__ は任意の文字列を使えます。

Makefile と make を使ってお手軽に分割コンパイルをする

(あとで書く)

課題

課題1

角度をラジアンに変換する関数を作りましょう。 関数名はなんでもいいですが,例えば “DegreeToRadian”, “Deg2Rad”, “degToRad” なんかがいいと思います。 \(\pi\) の値は自分で書いてもいいですが,<cmath> ヘッダー[1] をインクルードすると

M_PI

という定数が使えるようになります。 他にもいろいろな定数・関数が定義されています(リンク)。

答え

課題2

鉛直投げ上げの式 \[ y(t) = y_{0} + v_{0}t - gt^{2}/2 \] の関数を作りましょう。 ここで \( y_{0} = 0,\ \ v_{0} = 1,\ \ g=-10 \) とします。 また,変数はすべて double にしてください。

答え を実行すると以下のような結果が得られます。

mail:answer genki$ g++ cpp_test4_answer2.cc
mail:answer genki$ ./a.out
0.00 sec:  0.00 (m)
0.05 sec:  0.04 (m)
0.10 sec:  0.05 (m)
0.15 sec:  0.04 (m)
0.20 sec: -0.00 (m)
0.25 sec: -0.06 (m)
0.30 sec: -0.15 (m)
0.35 sec: -0.26 (m)
0.40 sec: -0.40 (m)
0.45 sec: -0.56 (m)
0.50 sec: -0.75 (m)
0.55 sec: -0.96 (m)
0.60 sec: -1.20 (m)
0.65 sec: -1.46 (m)
0.70 sec: -1.75 (m)
0.75 sec: -2.06 (m)
0.80 sec: -2.40 (m)
0.85 sec: -2.76 (m)
0.90 sec: -3.15 (m)
0.95 sec: -3.56 (m)

課題3

再帰的な関数で階乗を求めてみましょう。 再帰的な関数とは,関数の中で自分を呼び出しているもので

int func( int num ){
    int aaa = func( num - 1 ); /* 処理で自分を呼び出す */
    return aaa;
}

のような感じです。 結果は

mail:answer genki$ ./a.out
0                     1
1                     1
2                     2
3                     6
4                    24
5                   120
6                   720
7                  5040
8                 40320
9                362880
10              3628800
11             39916800
12            479001600
13           6227020800
14          87178291200
15        1307674368000
16       20922789888000
17      355687428096000
18     6402373705728000
19   121645100408832000

になりました。

回答案

課題4

このウェブページを再読込みすると配色が変わるように仕組んでいます。 これは,読み込みをしたときの時刻を使ってメインの色を決め,色の彩度や明度を変えてサブの色やリンクの色を生成しています。 このときに HSV色空間 を利用しています。

HSV では hue(色相), saturation(彩度), value(明度) を使って色を指定します。 同じ色相で彩度・明度を変えると,メインの色より淡い色やくすんだ色を簡単に作れて便利です。 一方でウェブページの色の指定は・・・・・・

気が変わったので,明日書きます。

答え

答え (C/C++ではなく Javascript で書いてある。このページで使用している。)


  1. C言語の場合,<math.h> ヘッダーをインクルードしましょう。cmath とは std 名前空間を使っているかが異なります。  ↩