ATLAS日本基礎ネットワーク C++トレーニングコース

第三回 手続きの記述

ここでの目標

目次

1.式と値

C++の基本的な構成要素は式です。式は文を構成できますし、条件など値が必要とされるところに記述することが出来ます。

もっとも単純な式はリテラルや変数そのもので、値を持つことが出来れば式を構成できます。

演算は一つ以上のリテラルや変数、あるいは式に対して作用し、値を返します。

文はC++での実行単位で、式を;で終端することで文を構成できます。

1;

これも文です。リテラル1が評価されて、値1を持ちます。その結果はどこにも反映しないので意味はありませんが。

;

これは空文を表します。何もないけれども文として終端されています。

1.1.演算

通常は四則演算などを使って、変数やリテラルの間で様々な計算をおこなうことになります。前回学習した変数などを用いて計算をおこなうのに演算が必要です。

C++では単一の式に対して作用する単項演算、二つの式の間で演算をおこない、その結果として値を返す二項演算などがあります。いずれの演算も結果となる値を必ず返します。順番に演算の種類を見ていきましょう。

1.2.代数演算

代数演算はいわゆる四則演算です。

単項演算として-があります。-に続く式の値の符号を反転し、その結果得られる値を持ちます。同様に+はそれに続く式の値の符号を変えず、そのままの値を持ちます。これらは前置演算子です。

単項演算の特殊なものに++--があります。特殊というのは、これらの演算子は前置にも後置にもなりえる点、そして、変数に作用し、その内容を変えてしまう点です。他の演算子は代入演算をのぞいて変数の値を変えてしまうものはありません。前置++はその直後に与えられた変数の内容を1増加し、その結果を値として持ちます。後置++はその直前に与えられた変数の内容を評価し、その値を持つとともに、その後内容を1増加します。--は逆に1減らします。

二項演算には+-*/%があります。+は演算子の両側の式の値を加算し、その結果得られた値を持ちます。同様に-は左辺の式の値から右辺の式の値を引き去り、その結果得られた値を持ちます。*は両側の式の値のかけ算をおこないその結果を持ちます。/は左辺の式の値を右辺の式の値で割り、その結果を持ちます。%は左辺の式を右辺の式で割り、そのあまりを値として持ちます。

実習1.

作業場所を用意しましょう。第三講目でl3です。

cd
cd tutorial/cplusplus
mkdir l3
cd l3

++--の例としてincdec.cxxを書きましょう。

#include <iostream>

main( ) {
    using namespace std;

    int i = 0;
    int j = 0;

    cout << "i++ " << i ++ << " ++j " << ++ j << endl;
    cout << "i-- " << i -- << " --j " << -- j << endl;
    cout << "i   " << i    << "   j " << j << endl;

}

走らせてみます。

g++ incdec.cxx
./a.out
i++ 0 ++j 1
i-- 1 --j 0
i 0 j 0

先に増えるのか後から増えるのか違いを確認しましょう。

1.3.論理演算と比較演算

論理演算はブール型(bool)の値(truefalse)を持つものです。

単項演算には!があります。それに続く式の持つ論理値を反転し、その結果得られたブール値を持ちます。

二項演算には&&||があります。 &&は両辺のブール値について論理積を取り、その結果を値として持ちます。||は両辺のブール値について論理和を取り、その結果を値として持ちます。

比較演算は二つの値の関係をブール値として返すものです。

二項演算には==!=<><=>=があります。==はその両辺の値が等しいときに真、そうでないときに偽のブール値を持ちます。!=はその両辺の値が等しくないときに真、そうでないときに偽を持ちます。<は左辺値が右辺値より小さいときに真、>は左辺値が右辺値より大きいときに真、<=は左辺値が右辺値より小さいか等しいときに真、>=は左辺値が右辺値より大きいか等しいとき真。

特殊な演算として

式1?式2:式3

という演算があります。これはまず式1の値を評価し、それが真であれば式2の値を持ちます。そうでなければ式3の値を持ちます。

k = k > kmax ? kmax : k;

この文は、まず k>kmax を評価します。これが真であれば?の直後の式の持つ値すなわちkmaxを、偽であれば:の後の式kを値として持ち、その結果をkに代入します。

実習2.

minmax.cxxを書いてみましょう。

#include <iostream>

main( ) {
    using namespace std;

    int kmax = 7;
    int kmin = 2;

    for( int k = 0; k < 10; k ++ ) {
        int ki = k < kmin ? kmin : k;
        ki = ki > kmax ? kmax : ki;
        cout << ki << endl;
    }

}

走らせます。

g++ minmax.cxx
./a.out
2
2
2
3
4
5
6
7
7
7

1.4.ビット演算とシフト演算

ビット単位で演算をおこないます。

単項演算としては~があります。これはこれに続く値をビット単位で反転しその結果得られた値を持ちます。

二項演算としては&|^があります。&は左辺値と右辺値の間でビット単位での論理積を取ります。|は左辺値と右辺値の間でビット単位での論理和を取ります。^は左辺値と右辺値の間ではいた論理和を取ります。

シフト演算は値をビット列と見なし、それを右や左に算術シフトする二項演算です。

式1<<式2

これは式1の値をビット列と見なし、式2の値だけ左にシフトします。右からは0が追加されていきます。

式1>>式2

これは式1の値をビット列と見なし、式2の値だけ右に算術シフトします。正の数であれば0が、負の数であれば1が左から追加されていきます。

演算 aの値 bの値 演算結果
~a 00001101(13) 11110010(-14)
a&b 00001101(13) 00111001(57) 00001001(9)
a|b 00001101(13) 00111001(57) 00111101(61)
a^b 00001101(13) 00111001(57) 00110100(52)
a<<b 00001101(13) 00000011(3) 01101000(104)
a>>b 00001101(13) 00000011(3) 00000001(1)

1.5.代入演算

代入演算=は左辺に変数など記憶域を与え、右辺の式の値をそれに代入するものです。左辺になれるものは変数、配列要素など変数としてデータの代入が可能な場所を表すもので左辺値と呼ばれます。

C++では代入演算として+=-=*=/=%=<<=>>=&=|=^=があります。これらは=の前についている演算を左辺の示す変数の値と右辺の式の間でおこなった上で、左辺に代入するものです。

比較演算の==と代入演算の=はもちろん別の演算ですが、誤って==と書くべきところを=としても構文上はエラーになりません。例えばif( a==b )と書くべきところをif( a=b )と書くなど。この場合は仕方がありませんが、例えば片方がリテラルの場合、if( a==3 )と書くところをif( a=3 )と書いてもエラーになりませんが、if( 3==a)を誤ってif( 3=a )と書くと3は左辺値にはなれないのでエラーが検出できます。そういう間違いを検出しやすくするため、比較演算==の場合、左辺にはリテラルや式など左辺値になれないものを書く習慣をつけるようにしましょう。

実習3.

ビット演算と代入の例bitman.cxxを作ってみましょう。

#include <iostream>

main( ) {
    using namespace std;

    int id = 1;

    for( int i = 0; i < 16; i++ ) {
        id <<= 1;
        cout.width( 8 );
        cout << dec << id << " ";
        cout.width( 8 );
        cout << hex << id << endl;
    }
}
			

これを実行します。

g++ bitman.cxx
./a.out
       2        2
       4        4
       8        8
      16       10
      32       20
      64       40
     128       80
     256      100
     512      200
    1024      400
    2048      800
    4096     1000
    8192     2000
   16384     4000
   32768     8000
   65536    10000

coutの文に含まれるdechexは整数の表現形式を指定するトークン(記号)です。cout.width(8)はその直後の値の表示を8文字幅でさせるための手続きで、詳細は第9回で説明します。

1.6.ポインター演算

すでにポインターのところで見たように、ポインター型変数が指し示す先の値を取り出す演算子は*です。ポインターの初期化などで必要になる、変数のアドレスの取り出しには&を用います。

配列で見たように要素を取り出すのには[]を演算子として用います。

ポインター[式]

で、ポインターが示す領域(配列)のの値が示す順番にある要素を取り出すことが出来ます。

実習4.

pointerarith.cxxを書いてみましょう。

#include <iostream>

main( ) {
    using namespace std;

    int iarray[20];                 //(1)
    int * ip = & iarray[10];        //(2)

    for( int i = 0; i < 20; i++ )
        iarray[i] = 0;

    ip[5] = 5;                      //(3)
    for( int i = 0; i < 20; i++ )
        cout << iarray[i] << " ";
    cout << endl;

}
			

走らせます。

g++ pointerarith.cxx
./a.out
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 0 0 0

(1)で配列iarrayを定義しています。(2)では(1)で定義した配列の10番目の要素を取り出し、そのアドレスを&で取り出してポインターipに代入しています。ipは上記配列の10番目を指していることになります。(3)ではポインターに5オフセットをかけた場所ip[5]に数値5を代入しています。それはiarrayで言うと15番目になります。出力がそれを示しています。

このように同じ配列の一部を別の配列のように見せることもできますが、こういったことはプログラムを読みにくくするものでATLASでは使用しないことになっています。

1.7.型変換とプロモーション

二項演算の場合、二つの式の型が異なる場合、自動的に型変換が起こります。整数型、浮動小数点数型同士の場合は、より精度の高い方へ、整数型と浮動小数点数型の間の場合は浮動小数点数型の方へ型変換がされた上で(これをプロモートと呼びます)演算がおこなわれ、その結果もプロモートされた型の値を持ちます。最終的に代入演算がおこなわれるときにはその左辺が示す型に強制的に型変換がされることになります。

型変換はいろいろと問題を起こします。そのため、それが適当なものであるか、意図したものであるかをコンパイラがチェックできるように型変換を明示的に示すキャスト演算子が用意されています。(ATLASでは暗黙の変換は使わないことになっています。)

C言語から引き継がれているものとして(型)値の形式があります。

char c = 'a';
int i;
i= (int)a;

これは古い書式で、関数型の表記もあります。

i = int(a);

これらはATLASのコーディングでは使わないことになっています。代わりに以下のものを使います。

プログラム中でより明確に変換の意図を示すために

i = static_cast<int>(a);

と記述します。

1.8.演算の優先順位

*/の演算の方が+-演算よりも優先順位が高いというように、演算の間には優先順位があります。まとめると別表のようになります。表に言う「変数」とは変数など記憶域を示すオブジェクト、左辺値です。(ここでは説明していないクラスやスコープ、関数等に関する演算子も含んでいます。「プログラミング言語C++第三版」159ページより引用)

同じ順位の演算子が式の中で使われている場合、どの順序でおこなわれるでしょうか。単項演算子と代入演算子は右結合、それ以外は左結合です。この意味は、

a=b=ca=(b=c)

a+b+c(a+b)+c

という順序で結合されるという意味です。

演算の優先順位を変えるには()を使います。

優先順位 演算 表記
18
スコープ解決 クラス名::メンバー
スコープ解決 名前空間名::メンバー
大域解決 ::名前
17
メンバー選択 オブジェクト.メンバー
メンバー選択 ポインター->メンバー
要素取りだし ポインター[]
関数呼び出し 表現(式の並び)
値の構築 (式の並び)
後置インクリメント 変数++
後置デクレメント 変数--
型識別 typeid()
実行時型識別 typeid()
実行時チェックつき型変換 dynamic_cast<>()
コンパイル時チェックつき型変換 static_cast<>()
チェック無し型変換 reinterpret_cast<>()
const変換 const_cast<>()
16
オブジェクトサイズ sizeof
型のサイズ sizeof()
前置インクリメント ++変数
前置デクレメント --変数
1の補数(ビット単位反転) ^
論理否定 !
単項マイナス -
単項プラス +
アドレス取り出し &変数
間接参照 *ポインター式
構築(領域確保) new
構築(領域確保と初期設定) new (式の並び)
構築(配置) new (式の並び)
構築(配置と初期設定) new (式の並び) (式の並び)
破棄(領域解放) delete ポインター
配列の破棄 delete [] ポインター
型変換 ()
15
メンバー選択 オブジェクト.*メンバーへのポインター
メンバー選択 ポインター->*メンバーへのポインター
14
乗算 *
除算 /
剰余 %
13
加算 +
減算 -
12
左シフト <<
右シフト >>
11
未満 <
以下 <=
より大 >
以上 >=
10
等しい ==
不等 !=
9
ビット単位論理積 &
8
ビット単位排他論理和 ^
7
ビット単位論理和 |
6
論理積 &&
5
論理和 ||
4
条件式 ?:
3
代入 変数=
演算して代入 変数*=式、変数/=式、変数%=式、変数+=式、
変数-=式、変数<<=式、変数>>=式、
変数&=式、変数|=式、変数^=
2
例外投擲 throw
1
並び ,

実習5.

優先順位の確認は重要です。自信がないときは()で優先順位を明示しましょう。priority.cxxを書いてみます。

#include <iostream>

main( ) {
    using namespace std;

    int i = 5;
    int j = 3;

    cout << i << j << endl;                //(1)
    cout << ( i << j ) << endl;            //(2)

    int k = 4;

    cout << ( i + j << k ) << endl;        //(3)
    cout << ( i + ( j << k )) << endl;     //(4)

}

実行します。

g++ priority.cxx
./a.out
53
40
128
53

まず(1)では、<<の結合則が左から右なので、まずcout<<iが実行されます。つまり数値5が出力されます。cout<<iという演算の結果はcoutを返しますので続いてcout<<jが実行されます。その結果3が出力され、空白が間に入らないので53と表示されることになります。

(2)では結合則の順序を変えるため()で変えます。まずi<<jが実行されます。これはともに整数なのでビットシフトが起こり5を左に3ビットシフト、つまり8倍します。40という値を得ます。これが次にcoutに出力されます。その結果40と表示されます。

(3)ではさらにkが登場します。括弧によりcoutへの出力は優先順位が後になります。i+j<<kが実行される訳ですが、+演算は<<演算より優先度が高いのでi+jが先に計算されます。その結果8となります。それに続いて<<kが実行されます。4ビット左シフトしますので16倍することになります。その結果128を得ます。これをcoutに出力します。

(4)ではさらに演算順序を変えます。j<<kが最初に計算されます。34ビットシフトします。16倍ですから48になります。続いてiを加えます。53を得ます。それがcoutに出力されます。(1)の例では53が順に出力されたのですが、(4)の例では53という数値が出力されました。だまされないように気をつけましょう。

2.条件分岐と繰り返し

プログラムの流れを制御する仕組みとして、条件分岐をおこなうためのif文、switch文と、繰り返しをおこなうためのfor文、while文、do文があります。goto文もありますが、使うなとC++の設計者自身が言っています。

これらの流れ制御では実行するかどうかを判断し、その上で必要であれば特定の文を実行します。その実行単位となる文に複数の手順を記述したくなります。そのために{}で0個以上の文を取り囲むことによりそれらを一つの実行単位〜文として扱うように出来ます。複文と呼ばれます。

2.1.if文

if文は次の形を取ります。

if(式) 文

式はブール値として評価されます。0の時偽、それ以外は真です。は上に述べたように単文でも複文でも構いません。例として

if( i > 10 ) i = 10;

これはi > 10という比較演算がおこなわれ、その結果が真であればi=10;という文を実行します。

if( error ) {
    std::cerr << "Error" << std::endl;
    return -1;
}

この場合は{}の内部がerrorという表現が真の時に実行されます。

もう一つの表現として、偽の時に実行する部分も指定できます。

if(式) 文1 else 文2

この場合、式が評価されてその結果が真ならば文1、偽ならば文2が実行されます。例えば

if( i < 10 ) k = iarray[ i ]; else k = 0;

実際には複文を伴う場合が多く、どこからどこまでが文の範囲かをわかりやすく示すことは重要です。インデント(段下げ)を用いて見やすくします。また、ifelseで片方を単文、もう一方を複文にすると対応を追いかけるのが難しくなります。片方が複文の場合、他が仮に単文で表現できても複文に入れるようにすることがATLASでは求められています。

if( i >= 0 && i < 10 ) {
    k  = iarray[ i ];
} else {
    std::cerr << "Index out of bounds. Quit" << std::endl;
    return -1;
}

この例ではiが範囲外の場合エラーメッセージを表示して抜けます。必ず複文が必要になります。iが範囲内の場合は単文でも良いのですが、ATLASのルールに従って複文にし、{}に入れてあります。

それから複文の中に含まれている文はそれぞれ文頭を段下げすることで{}の中にいることがわかりやすくなります。

もちろんif文のelseの後の文に再度if文が来ることもよくあります。

if( i > 10 ) {
    std::cerr << "Index too large. use default." << std::endl;
    k = 0;
} else if( i < 0 ) {
    std::cerr << "Negative index. use default." << std::endl;
    k = 0;
} else {
    k = iarray[ i ];
}

これがあまり繰り返すようだとアルゴリズムの設計が適切でないでしょう。

実習6.

if.cxxを書いてみましょう。

#include <iostream>

main( ) {
    using namespace std;

    int idx = 0;
    int idy = 0;

    if( idx ) {
        if( idy ) {
            cout << "Both idx and idy are true." << endl;
        }
        else {
            cout << "idx is true and idy is false." << endl;
        }
    }
    else if( idy ) {
        if( idx ) {
            cout << "Both idx and idy are true. Ummm." << endl;
        }
        else {
            cout << "idx is false and idy is true." << endl;
        }
    }
    else if( idx == idy ) {
        cout << "Neither idx nor idy is true." << endl;
    }
    else {
        cout << "Neither idx nor idy is true,"
             << "but still idx is different from idy. Never!" <<endl;
    }
}

実行します。

g++ if.cxx
./a.out
Neither idx nor idy is true.

if文の組み合わせてでできています。この例で論理的に決して実行されることのない部分があることがわかりますね。複雑な入れ子の関係になったif文は時として問題の元になります。プログラム構造をしっかり検討してから実装したいものです。

2.2.switch文

switch文は条件分岐を実装したもう一つの方法です。

switch(式) {
case 値: 文
case 値: 文
...
default: 文
}

式の値が評価され、その値がcaseに書かれた値と一致したとき、そのcaseの位置から文を実行します。いずれの値にも一致しなかった場合defaultと書かれたラベルがあればそこから実行を開始します。もしdefaultラベルがない場合は何も実行せずswitch文を抜けます。break文は複文の中から脱出する役割をします。

enum MACHINESTATE {
    INITIAL,
    STANDBY,
    RUNNING
};

列挙MACHINESTATEを定義しました。

switch( istep ) {
case INITIAL:
    cout << "Initializing..." << endl;
    break;
case STANDBY:
    cout << "Standby." << endl;
    break;
case RUNNING:
    cout << "Running..." << endl;
    break;
default:
    cerr << "Undefind state. " << istep << endl;
    return -1;
}

istepにはMACHINESTATEが記録されていると思ってください。それぞれの値に対応してcaseラベルが用意されています。これはあくまでもラベルなので、その位置に制御がわたったらそこから先に並んだ文をすべて順番に実行します。もしcase INITIALの後のbreak文がないと、そのままcase STANDBYに進んでいってしまいます。意図してそのように書く場合も考えられますが、一般には誤解しやすく読みにくいプログラムになってしまいます。必ず一つのcaseラベルにはbreakを割り当てて、制御の流れがわかりやすくなるようにしましょう。

この例では、本来はcaseラベルのいずれかにistepの値はマッチするはずなのでdefaultラベルは不必要かもしれません。しかし、何かのバグのせいで変な値がistepに与えられたとき、もしdeafultがないとこのswitch文の中では何もしないことになって、エラー検出の機会を一つ失うことになります。そういう意味で、ATLASでは必ずdefaultラベルを用意することを要求しています。

実習7.

switch文を試してみましょう。switch.cxxを書きます。

#include <iostream>

main( ) {
    using namespace std;

    enum MACHINESTATE {
        INITIAL,
        STANDBY,
        RUNNING
    };

    int istep = INITIAL;

    while( true ) {
        switch( istep ) {
        case INITIAL:
            cout << "Initializing..." << endl;
            istep = STANDBY;
            break;
        case STANDBY:
            cout << "Standing by..." << endl;
            istep = RUNNING;
            break;
        case RUNNING:
            cout << "Running..." << endl;
            istep ++;
            break;
        default:
            cerr << "Undefined state. " << istep << endl;
            return -1;
        }
    }
}

実行します。

g++ switch.cxx
./a.out
Initializing...
Standing by...
Running...
Undefined state. 3

enumでシンボルを定義します。istepがその状態を保持します。while(true)は無限に繰り返すことを指定しています。そうなると困るのですがちゃんと抜け道が用意されています。RUNNINGになったときitep++でインクリメントしてistepを未定義の値にします。その結果defaultラベルに制御が移りcerrにメッセージを出力してreturn -1main関数から戻ります。cerrは標準エラー出力を表します。coutがバッファされて出てくるのに対しcerrはバッファされず直接出力されます。main関数の戻り値はmakeなどでも使用しますが、ジョブの実行状態を表し、正常終了の場合は0を返します。今の場合異常終了なので適当な非零の値-1を返しています。

2.3.for文

for文は次の形式で記述されます。

for( 初期化式; 判定式; 更新式 ) 実行文

ここで初期化式for文で最初に一度だけ実行評価されます。判定式は次に実行評価され、その値が真であれば実行文を実行します。偽であればfor文を終了します。真の場合、実行文を実行した後更新式を実行評価した後、再度判定式を実行評価します。判定式の値が真であり続ける限り実行文が繰り返し実行されます。

int i;
for( i = 0; i < 10; i++ )
    cout << i << " = " << iarray[ i ] << endl;

初期化式判定式更新式は空式でも構いません。判定式が空式の場合はbreak文により実行文から脱出できるように用意する必要があります。

continue文により実行分のそれ以後の部分をスキップし次の更新に移ることもできます。

for文の初期化式の中で変数を定義することが出来ます。この場合、その変数は実行文の中からしか見えないことに注意してください。

for( int i = 0; i < 10; i ++ )
    cout << i << endl;    //ここは実行文の中なのでiは見える。
cout << i << endl;    //ここは実行文の外なのでiは見えない。エラーになる。

実習8.

for文を使ってみましょう。for.cxxです。

#include <iostream>

main( ) {
    using namespace std;

    for( int i = 0; i < 10; i++ ) {
        if( i % 2 ) continue;
        cout << i << " ";
    }
    cout << endl;

}

実行します。

g++ for.cxx
./a.out
0 2 4 6 8

この例の中に出てくるcontinueというのはループの最後に飛ぶことを表します。それ故次の行cout<<i<<" "は実行されません。結果として偶数だけ表示されました。

2.4.while文

while文も反復を実装するのに用いられます。

while(式)文

を評価し、真であるかぎりを実行します。偽であればwhile文を終了します。

int error = 0;
while( ! error ) {
    ....  //do something
}

! error が真である限り、errorが0である限り...部分が実行され続けます。が最初から偽であればは一度も実行されません。似た構文に

do 文 while(式);

というものがあります。この場合、無条件にを実行し、その後を評価します。真であればを繰り返し実行します。偽であれば終了します。最初からが偽であっても一度は実行されます。

通常のプログラムではこれらの反復構文と条件分岐が複雑に絡み合うことになります。whiledo whileを入れ子にするなど、ややこしい構造を持ち込むことはプログラムを読みにくくします。

3.関数

繰り返し使われる一連の手続きを関数として定義することが出来ます。関数は戻り値を持つことが出来ますし、引数を渡すことが出来ます。

C++ではクラスメンバーとしての関数を書くことの方が圧倒的に多いわけですが、クラスに属さない大域的な関数と基本的なことは共通しています。また、UNIXのシステムコールやCの実行時間数など、Cの大域的な関数を使うことはよくあります。そういう意味でもよく理解しておく必要があります。

関数は次のような形で定義(definition)されます。

型 関数名(引数並び) { 文 }

この関数を利用する側では関数を宣言(declaration)する必要があります。

型 関数名(引数並び);

「宣言」は関数の呼び出し方を記述するもので、それが何をするかの「定義」は別のところで記述されていて構いません。

関数が呼び出され、その関数に含まれる文が実行されている間、本書では「制御が関数に移る」という言い方をしています。関数内の文の実行を終え、それを呼び出した側の文が実行されるようになることを呼び出した側に「制御が戻る」と言っています。

3.1.関数の戻り値

関数は戻り値を持つことが出来ます。その必要がないときはvoid型で定義することにより、値を返さなくても良くできます。

戻り値を持つ関数はその定義の中でreturn文に値を持たすことにより値を返します。

int ifunc( ) {
    return 0;
}

関数の戻り値はレジスタに収容可能なサイズの型のものはレジスタで返されますが、それより大きなものは一時的にコピーを作り、それを返します。ですから、非常に大きなサイズのオブジェクトを返すことは性能を劣化させます。可能であればポインターや参照で結果を返す方が効率的です。

実習9.

関数が値を返すことを確認します。func.cxxを書きます。

#include <iostream>

int func( ) {

}

main( ) {
    using namespace std;

    cout << "func returns " << func( ) << endl;

}

この例を実行しましょう。

g++ func.cxx
./a.out
func returns 1895370624

関数funcは明示的には値を返していません。そのため、この例では変な値を返しています。

この関数を次のように修正します。

int func( ) {

return;

}

これをコンパイルしようとすると

g++ func.cxx
func.cxx: In function `int func()':
func.cxx:5: error: return-statement with no value, in function returning 'int'

とエラーになります。必ず戻り値の型の値をreturnに与えなければなりません。

3.2.関数の引数

C++の関数の引数はいわゆる「値渡し」です。関数を呼び出す側が用意する引数リストには引数の値がコピーされて渡されます。通常引数リストはスタック上に作られ、関数から制御が戻るときに破棄されます。

//呼び出し側

配列のように大きなものを引数として渡したいときは直接渡すとそのコピーが引数リストに作られることになります。効率的な引き渡しをするにはポインターや参照を用いることになります。

int func( int isize, int iarray[1000] );

これは好ましくありません。

int func( int isize, int * ipointer );

ポインターや参照を渡した場合、関数の側でポインターの指し示す記憶域のデータを書き換えることが出来てしまいます。そのことを意図してポインターを渡す場合ももちろんありますが、関数側から書き換えられたくない場合はconstなオブジェクトへのポインターとして引数を渡します。

int func( int isize, const int * ipointer );

引数として渡された値は関数定義の中では変数として使用することが出来ます。その内容を書き換えることも可能ですが、それはあくまでも引数リストに割り当てられたその引数の記憶域の内容が変化するだけであって、呼び出した側の変数とは何の関係もありません。しかし、そのようなコーディングが意味があるとは思えません。

実習10.

mainも関数です。後に述べる関数の多重定義により、引数を持ったmain関数もあります。コマンドとしてプログラムを実行するときにコマンド引数として与えた文字列をプログラム中で使えるようになります。

args.cxxを書いてみましょう。

#include <iostream>

main( int argc, char ** argv ) {
    using namespace std;

    char ** ap = argv;

    for( int i = 0; i < argc; i ++ ) {
        cout << i << ": " << * ap ++ << endl;
    }

}

これを実行します。

g++ args.cxx
./a.out
0: ./a.out

char ** argvとあるように、argvは文字型へのポインターのポインターです。考え方によっては文字列の並びを示していると考えて良いでしょう。この例ではargc1であり、argvが指している内容は./a.outというコマンドそのものを表す文字列でした。では

./a.out Hallo, World!
0: ./a.out
1: Hallo,
2: World!

コマンド./a.outHello, World!という引数を与えました。今度はargcの数が3になり、argv./a.outHallo,World!という文字列をそれぞれ指し示しています。コマンド引数は空白文字で区切られていると考えます。空白文字も引数に入れたいときは

./a.out 'Hello, World!'
0: ./a.out
1: Hello, World!

文字列全体を'シングルクォートで囲みました。これは"ダブルクォートではうまくいきません。シェルの文法を参照してください。"ダブルクォートはコマンドラインで制御文字として使われるためです。

引数のデフォルト値

引数にデフォルト値を与えることが出来ます。関数定義/宣言の中で引数宣言にデフォルト値を加えます。これはその引数が省略されたときに使われる値になります。複数の引数を持つ場合、後から出現する引数の側にデフォルトを与えます。最初の引数が省略され、その次の引数が省略されないなどと言うことは許されないからです。

int func( int istart, int istop, int istep = 1 );

この場合、func(0,10)とするとistep=1が使われます。func(0,10,2)という記述ももちろん許されます。

3.3.局所変数

関数定義の中だけで使用する変数(局所変数)を定義することが出来ます。そういった変数は引数リストなどと同様に、スタック上に一時的に生成され、関数から制御が戻るときに破棄されます。

int func( ) {
    int i = 0;    //局所変数の宣言。関数の{}の中からのみ見える。
    for( i = 0; i < 10; i++ ) { ... }    //関数の中で自由に使える。
    return i;    //変数iの寿命はまもなく尽きるが、その値はコピーされて呼び出し側に伝わる。
}

関数定義の中に作った局所変数へのポインターや参照を関数の戻り値として与えることはそれ故間違いです。それが示す先の局所変数は呼び出し側がそれを使うときにはすでに破棄されてしまっています。

int * func( ) {
    int iarray[ 10 ];    //配列は局所的に作られた。この関数の中だけ存在する。
    ...
    return iarray;    //iarrayは整数型配列へのポインター。
}

文法的には許されるため、コンパイルは出来ますが、警告されます。ポインターの値はコピーされて呼び出した側にわたりますが、その指し示す先はすでに破棄され、意味のある情報を保持していません。

staticな変数

局所変数をstaticと宣言することによって、記憶域を継続して確保することが出来ます。この場合、その変数は制御がその関数から離れても維持されており、再度制御がその関数に戻ってきたとき、その変数は最後に代入された値を維持しています。関数定義の中で初期化されていても、それは最初の呼び出しの時だけ有効です。

int func( ) {
    static int i = 0;
    i++;
    return i;
}

この関数は、i0に初期化されていますが、呼び出されるごとにインクリメントされているので関数funcが返す数は一つずつ大きくなります。

この手法を使えば、先ほどのiarrayでも警告は出なくなります。しかし、この関数はいつどこから呼び出されるかわかりません。ポインターで参照している最中に別のところから関数が呼び出されてiarrayの内容が書き換わるかもしれません。

3.4.関数の利用・ヘッダーファイル

このようにして用意された関数を利用するためには、それを使用するプログラム中でその関数宣言を行う必要があります。関数宣言を行う部分のソースコードがヘッダーファイルとして用意されているので、それをインクルードします。例えばsqrtを使ってみます。

#include <math.h>
main( ) {
    double x = 2.0;
    double y = sqrt( x );
}

関数sqrtはこのコンパイル単位中で宣言されていなければなりません。Linuxでは標準的な関数であり、manで見ることが出来ます。man sqrtとやってみてください。マンページの表示の中にmath.hをインクルードする必要があることが示されています。このように、.hファイル(ヘッダーファイル)には関数の宣言が記述されており、それをインクルードすることによりその関数をプログラム中で利用できるようになります。

3.5.分割コンパイル

関数の宣言と定義を別の場所でおこなうことにより、分割コンパイルが可能になります。関数の利用者側は宣言だけを用いてコンパイルをおこない、実行可能イメージを作るときにあらかじめコンパイルされている関数のオブジェクトファイルを結合します。オブジェクトファイルは通常ライブラリと呼ばれるファイルにまとめられています。

実習11.

main.cxxを書いてみましょう。内容は次のように簡単なもの。

#include <iostream>
int func3( ) {  return 3;  }
main( ) {
    int i = func3( );
    std::cout << "i = " << i << std;;endl;
}

この非常に短い例の中で、関数funcが定義され、使用されています。このfuncをいろいろなケースで使いたいので、別のファイルで定義します。定義を宣言に替えます。

#include <iostream>
int func( );
main( ) {
    int i = func( );
    std::cout << "i = " << i << std;;endl;
}

これをコンパイルしてみましょう。

g++ main.cxx
...
: undefined reference to 'func3()'

func3がありません。別のファイルでfunc3()を定義したいので、func3.cxxを用意します。

int func3( ) {
    return 3;
}

これをこれまで通りコンパイルすると

g++ func.cxx
...
: undefined reference to 'main'

その通り、このファイルにはmain関数が含まれていません。これまでg++を使ってきたのは、コンパイルという手順とリンクという手順の両方を続けてやってきていました。

コンパイル
ソースコードを解読してマシンコードに変換します。出来るファイルはオブジェクトファイルと呼ばれ、マシンコードを含んでいますがそのままでは実行できません。オブジェクトファイルは通常.oというサフィックスがつけられます。
リンク
オブジェクトファイルを解読し、必要なオブジェクトモジュールをライブラリーから取り出し結合します。その結果できあがるファイルが実行可能イメージと呼ばれ、a.outというデフォルトの名前が付けられます。

分割コンパイルを行う場合、まずコンパイルを個別にやっておき、それからリンクを行って実行可能イメージを作ります。今の例では

g++ -c main.cxx

これでmain.oが作られました。-cはコンパイルで止め、リンクには行きなさんなという意味で、デフォルトで後の.cxx.oに替えたファイルが作られます。このファイルはまだ不足しているオブジェクトモジュールを抱えています。nmというコマンドを使って、オブジェクトファイル中の情報を見てみましょう。

nm main.o
...
0000000000000080 t _GLOBAL__I_main
 		    ...
                 U _Z5func3v 
 		    ..

maint、すなわちプログラムコード(テキストと呼ばれています)として存在します。一方、Uで始まるものは未定義。このオブジェクトファイルの中にはないものです。次に

g++ -c func.cxx

これでfunc.oが出来ました。同様にnmをやってみましょう。

nm func.o
0000000000000000 T _Z5func3v

ここにはちゃんと出来ています。名前がfunc3でなく_Z5func3vとなっているのは、名前が同じでも引数の種類が違うと別の関数定義ができる「関数の多重定義」のためにこうなっています。

それぞれのT/tの前に00000000とあるのは、その関数がオブジェクトファイル内のどこから始まるかを示しており、いずれも0からです。リンク時に適当な位置に配置されます。

次にリンクしましょう。g++コマンドに.oファイルだけを与えることで、リンクのみを行うことが伝わります。

g++ main.o func.o

これでこれまでと同じようにa.outが出来ました。nm a.outとやってみてください。main_Z5func2vにそれらしいアドレスが割り振られていることがわかるでしょう。

これまで、実行可能イメージとしてa.outを使ってきましたが、他の名前にしたいでしょう。

g++ -o funcdemo.exe main.o func.o

-oで出力ファイルの名前を指定することが出来ます。今の場合funcdemo.exeがそれです。

次にライブラリを作りましょう。たくさんのオブジェクトファイルが出来た場合、コマンドラインにそれらを一つずつ記述するのは大変です。代わりにライブラリを与えることで、その中から必要なオブジェクトモジュールを取り出してくれます。今の例ではfunc3.oしかありませんのでfunc4.cxxを用意しましょう。

int func4( ) {
    return 4;
}

g++ -c func4.cxx

これでfunc4.oも出来ました。ライブラリを作るのにはarコマンドを使います。archiveです。rオプション(replace)をつけて、最初にライブラリファイル名、続けてそこに含ませたいオブジェクトファイルを続けて書きます。

ar r libl3.a fun3c.o func4.o
ar: creating libl3.a

この方法で作るライブラリの名前は必ずlibXXX.aという形でなければなりません。今回の例はレッスン3なのでl3としました。試しにまたnmをやってみましょう。

nm libl3.a
func3.o
00000000 T _Z5func2v

func4.o
00000000 T _Z5func4v

ちゃんと登録されていることがわかります。このライブラリを使って実行可能イメージを作りましょう。

g++ -o funcdemo.exe main.o -L. -ll3

少しコマンドが長くなってきました。-o funcdemo.exeは、出力ファイルの名前の指定です。main.oがエントリーポイントです。このままですと、mainが呼んでいるfunc3が未解決になるのでライブラリを読むように指定します。ライブラリを指定するには、そのライブラリがあるディレクトリと、そのライブラリのファイル名を指定する必要があります。

-L.はこのライブラリファイルを探すパスを指定します。.(ピリオド)が与えられていますので現在のディレクトリを探すことになります。

リンカー(g++のことですが、リンクを行うプログラムをリンカーと呼びます。)は、-Lオプションで指定されているディレクトリを探します。続いて、LD_LIBRARY_PATH環境変数が設定されていると、そこに含まれているパス(ディレクトリ)を順番に探します。さらに見つからなければその言語コンパイラがインストールされた標準のライブラリのあるディレクトリを探します。/lib/usr/lib等です。

-lオプションはライブラリファイルの名前を指定します。libXXX.aXXXの部分を-lに続けて記述します。-ll3とあるのは-lに続くl3を使ってlibl3.aというファイルを指定したことになります。同じルールで、-lmとあるとlibm.aが指定されたことになります。

3.6.関数に関するテクニック

関数へのポインター

関数もまたアドレスで与えられるため、関数へのポインターも定義できます。そのポインターを用いて関数を呼び出すことも出来ます。

関数のポインターは、その定義の中で引数も宣言する必要があります。

int ifunc( int iarg );
int ( * pfunc ) ( int iarg );
pfunc = ifunc;
int ist = ( * pfunc ) ( 123 );

関数ポインターは戻り値と引数並びが一致する関数へのポインターになります。

実行時に呼び出される関数を切り替える方法として関数ポインターは使えますが、オブジェクト指向技術としては仮想関数がまさにその役割をしてくれるため、使う機会は少ないでしょう。関数の名前だけが現れたときはその関数のアドレスとして使われているということは理解しておいてください。(上記の例の三行目)

関数の多重定義(オーバーロード)

同じ名前の関数であっても、引数並びが違うものを定義することが出来ます。これを関数の多重定義と呼びます。同じ関数名にたくさんの実装が上乗せされます。(オーバーロード)

実際には我々はそれを見慣れています。+という演算子は整数にも浮動小数点数にも作用します。同じ演算子が異なる引数に作用できるように仕組みが作られているわけです。

int func( int iarg );
double func( double darg );

ただし、戻り値は区別されないので、

double func( int iarg );

とやるとint func( int iarg )と関数名と引数並びの型が同じになり区別が出来なくなりエラーになります。

次回はいよいよクラスについて説明します。


前へ 上へ 次へ


2017年7月21日