ATLAS日本基礎ネットワーク C++トレーニングコース
ここでの目標
目次
C++の基本的な構成要素は式です。式は文を構成できますし、条件など値が必要とされるところに記述することが出来ます。
もっとも単純な式はリテラルや変数そのもので、値を持つことが出来れば式を構成できます。
演算は一つ以上のリテラルや変数、あるいは式に対して作用し、値を返します。
文はC++での実行単位で、式を;
で終端することで文を構成できます。
1;
これも文です。リテラル1が評価されて、値1を持ちます。その結果はどこにも反映しないので意味はありませんが。
;
これは空文を表します。何もないけれども文として終端されています。
通常は四則演算などを使って、変数やリテラルの間で様々な計算をおこなうことになります。前回学習した変数などを用いて計算をおこなうのに演算が必要です。
C++では単一の式に対して作用する単項演算、二つの式の間で演算をおこない、その結果として値を返す二項演算などがあります。いずれの演算も結果となる値を必ず返します。順番に演算の種類を見ていきましょう。
代数演算はいわゆる四則演算です。
単項演算として-
があります。-
に続く式の値の符号を反転し、その結果得られる値を持ちます。同様に+
はそれに続く式の値の符号を変えず、そのままの値を持ちます。これらは前置演算子です。
単項演算の特殊なものに++
と--
があります。特殊というのは、これらの演算子は前置にも後置にもなりえる点、そして、変数に作用し、その内容を変えてしまう点です。他の演算子は代入演算をのぞいて変数の値を変えてしまうものはありません。前置++
はその直後に与えられた変数の内容を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
先に増えるのか後から増えるのか違いを確認しましょう。
論理演算はブール型(bool
)の値(true
とfalse
)を持つものです。
単項演算には!
があります。それに続く式の持つ論理値を反転し、その結果得られたブール値を持ちます。
二項演算には&&
、||
があります。 &&
は両辺のブール値について論理積を取り、その結果を値として持ちます。||
は両辺のブール値について論理和を取り、その結果を値として持ちます。
比較演算は二つの値の関係をブール値として返すものです。
二項演算には==
、!=
、<
、>
、<=
、>=
があります。==
はその両辺の値が等しいときに真、そうでないときに偽のブール値を持ちます。!=
はその両辺の値が等しくないときに真、そうでないときに偽を持ちます。<
は左辺値が右辺値より小さいときに真、>
は左辺値が右辺値より大きいときに真、<=
は左辺値が右辺値より小さいか等しいときに真、>=
は左辺値が右辺値より大きいか等しいとき真。
特殊な演算として
式1?式2:式3
という演算があります。これはまず式1
の値を評価し、それが真であれば式2
の値を持ちます。そうでなければ式3
の値を持ちます。
k = k > kmax ? kmax : k;
この文は、まず k>kmax
を評価します。これが真であれば?
の直後の式の持つ値すなわちkmax
を、偽であれば:
の後の式k
を値として持ち、その結果をk
に代入します。
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<<式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) |
代入演算=
は左辺に変数など記憶域を与え、右辺の式の値をそれに代入するものです。左辺になれるものは変数、配列要素など変数としてデータの代入が可能な場所を表すもので左辺値と呼ばれます。
C++では代入演算として+=
、-=
、*=
、/=
、%=
、<<=
、>>=
、&=
、|=
、^=
があります。これらは=
の前についている演算を左辺の示す変数の値と右辺の式の間でおこなった上で、左辺に代入するものです。
比較演算の==
と代入演算の=
はもちろん別の演算ですが、誤って==
と書くべきところを=
としても構文上はエラーになりません。例えばif( a==b )
と書くべきところをif( a=b )
と書くなど。この場合は仕方がありませんが、例えば片方がリテラルの場合、if( a==3 )
と書くところをif( a=3 )
と書いてもエラーになりませんが、if( 3==a)
を誤ってif( 3=a )
と書くと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.out2 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
の文に含まれるdec
やhex
は整数の表現形式を指定するトークン(記号)です。cout.width(8)
はその直後の値の表示を8文字幅でさせるための手続きで、詳細は第9回で説明します。
すでにポインターのところで見たように、ポインター型変数が指し示す先の値を取り出す演算子は*
です。ポインターの初期化などで必要になる、変数のアドレスの取り出しには&
を用います。
配列で見たように要素を取り出すのには[]
を演算子として用います。
ポインター[式]
で、ポインターが
示す領域(配列)の式
の値が示す順番にある要素を取り出すことが出来ます。
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では使用しないことになっています。
二項演算の場合、二つの式の型が異なる場合、自動的に型変換が起こります。整数型、浮動小数点数型同士の場合は、より精度の高い方へ、整数型と浮動小数点数型の間の場合は浮動小数点数型の方へ型変換がされた上で(これをプロモートと呼びます)演算がおこなわれ、その結果もプロモートされた型の値を持ちます。最終的に代入演算がおこなわれるときにはその左辺が示す型に強制的に型変換がされることになります。
型変換はいろいろと問題を起こします。そのため、それが適当なものであるか、意図したものであるかをコンパイラがチェックできるように型変換を明示的に示すキャスト演算子が用意されています。(ATLASでは暗黙の変換は使わないことになっています。)
C言語から引き継がれているものとして(型)値の形式があります。
char c = 'a'; int i; i= (int)a;
これは古い書式で、関数型の表記もあります。
i = int(a);
これらはATLASのコーディングでは使わないことになっています。代わりに以下のものを使います。
プログラム中でより明確に変換の意図を示すために
i = static_cast<int>(a);
と記述します。
*
や/
の演算の方が+
や-
演算よりも優先順位が高いというように、演算の間には優先順位があります。まとめると別表のようになります。表に言う「変数」とは変数など記憶域を示すオブジェクト、左辺値です。(ここでは説明していないクラスやスコープ、関数等に関する演算子も含んでいます。「プログラミング言語C++第三版」159ページより引用)
同じ順位の演算子が式の中で使われている場合、どの順序でおこなわれるでしょうか。単項演算子と代入演算子は右結合、それ以外は左結合です。この意味は、
a=b=c
はa=(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
|
並び | 式, 式 |
優先順位の確認は重要です。自信がないときは()で優先順位を明示しましょう。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
が最初に計算されます。3
を4
ビットシフトします。16倍ですから48
になります。続いてi
を加えます。53
を得ます。それがcout
に出力されます。(1)の例では5
と3
が順に出力されたのですが、(4)の例では53
という数値が出力されました。だまされないように気をつけましょう。
プログラムの流れを制御する仕組みとして、条件分岐をおこなうためのif
文、switch
文と、繰り返しをおこなうためのfor
文、while
文、do
文があります。goto
文もありますが、使うなとC++の設計者自身が言っています。
これらの流れ制御では実行するかどうかを判断し、その上で必要であれば特定の文を実行します。その実行単位となる文に複数の手順を記述したくなります。そのために{}
で0個以上の文を取り囲むことによりそれらを一つの実行単位〜文として扱うように出来ます。複文と呼ばれます。
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;
実際には複文を伴う場合が多く、どこからどこまでが文の範囲かをわかりやすく示すことは重要です。インデント(段下げ)を用いて見やすくします。また、if
とelse
で片方を単文、もう一方を複文にすると対応を追いかけるのが難しくなります。片方が複文の場合、他が仮に単文で表現できても複文に入れるようにすることが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 ]; }
これがあまり繰り返すようだとアルゴリズムの設計が適切でないでしょう。
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
文は時として問題の元になります。プログラム構造をしっかり検討してから実装したいものです。
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
ラベルを用意することを要求しています。
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 -1
でmain
関数から戻ります。cerr
は標準エラー出力を表します。cout
がバッファされて出てくるのに対しcerr
はバッファされず直接出力されます。main
関数の戻り値はmake
などでも使用しますが、ジョブの実行状態を表し、正常終了の場合は0
を返します。今の場合異常終了なので適当な非零の値-1
を返しています。
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は見えない。エラーになる。
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<<" "
は実行されません。結果として偶数だけ表示されました。
while
文も反復を実装するのに用いられます。
while(式)文
式
を評価し、真であるかぎり文
を実行します。偽であればwhile
文を終了します。
int error = 0; while( ! error ) { .... //do something }
! error
が真である限り、error
が0である限り...
部分が実行され続けます。式
が最初から偽であれば文
は一度も実行されません。似た構文に
do 文 while(式);
というものがあります。この場合、無条件に文
を実行し、その後式
を評価します。真であれば文
を繰り返し実行します。偽であれば終了します。最初から式
が偽であっても一度は実行されます。
通常のプログラムではこれらの反復構文と条件分岐が複雑に絡み合うことになります。while
とdo while
を入れ子にするなど、ややこしい構造を持ち込むことはプログラムを読みにくくします。
繰り返し使われる一連の手続きを関数として定義することが出来ます。関数は戻り値を持つことが出来ますし、引数を渡すことが出来ます。
C++ではクラスメンバーとしての関数を書くことの方が圧倒的に多いわけですが、クラスに属さない大域的な関数と基本的なことは共通しています。また、UNIXのシステムコールやCの実行時間数など、Cの大域的な関数を使うことはよくあります。そういう意味でもよく理解しておく必要があります。
関数は次のような形で定義(definition)されます。
型 関数名(引数並び) { 文 }
この関数を利用する側では関数を宣言(declaration)する必要があります。
型 関数名(引数並び);
「宣言」は関数の呼び出し方を記述するもので、それが何をするかの「定義」は別のところで記述されていて構いません。
関数が呼び出され、その関数に含まれる文が実行されている間、本書では「制御が関数に移る」という言い方をしています。関数内の文の実行を終え、それを呼び出した側の文が実行されるようになることを呼び出した側に「制御が戻る」と言っています。
関数は戻り値を持つことが出来ます。その必要がないときはvoid型で定義することにより、値を返さなくても良くできます。
戻り値を持つ関数はその定義の中でreturn
文に値を持たすことにより値を返します。
int ifunc( ) { return 0; }
関数の戻り値はレジスタに収容可能なサイズの型のものはレジスタで返されますが、それより大きなものは一時的にコピーを作り、それを返します。ですから、非常に大きなサイズのオブジェクトを返すことは性能を劣化させます。可能であればポインターや参照で結果を返す方が効率的です。
関数が値を返すことを確認します。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
に与えなければなりません。
C++の関数の引数はいわゆる「値渡し」です。関数を呼び出す側が用意する引数リストには引数の値がコピーされて渡されます。通常引数リストはスタック上に作られ、関数から制御が戻るときに破棄されます。
//呼び出し側
配列のように大きなものを引数として渡したいときは直接渡すとそのコピーが引数リストに作られることになります。効率的な引き渡しをするにはポインターや参照を用いることになります。
int func( int isize, int iarray[1000] );
これは好ましくありません。
int func( int isize, int * ipointer );
ポインターや参照を渡した場合、関数の側でポインターの指し示す記憶域のデータを書き換えることが出来てしまいます。そのことを意図してポインターを渡す場合ももちろんありますが、関数側から書き換えられたくない場合はconst
なオブジェクトへのポインターとして引数を渡します。
int func( int isize, const int * ipointer );
引数として渡された値は関数定義の中では変数として使用することが出来ます。その内容を書き換えることも可能ですが、それはあくまでも引数リストに割り当てられたその引数の記憶域の内容が変化するだけであって、呼び出した側の変数とは何の関係もありません。しかし、そのようなコーディングが意味があるとは思えません。
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
は文字型へのポインターのポインターです。考え方によっては文字列の並びを示していると考えて良いでしょう。この例ではargc
は1
であり、argv
が指している内容は./a.out
というコマンドそのものを表す文字列でした。では
./a.out Hallo, World!
0: ./a.out
1: Hallo,
2: World!
コマンド./a.out
にHello, World!
という引数を与えました。今度はargc
の数が3
になり、argv
は./a.out
とHallo,
と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)
という記述ももちろん許されます。
関数定義の中だけで使用する変数(局所変数)を定義することが出来ます。そういった変数は引数リストなどと同様に、スタック上に一時的に生成され、関数から制御が戻るときに破棄されます。
int func( ) { int i = 0; //局所変数の宣言。関数の{}の中からのみ見える。 for( i = 0; i < 10; i++ ) { ... } //関数の中で自由に使える。 return i; //変数iの寿命はまもなく尽きるが、その値はコピーされて呼び出し側に伝わる。 }
関数定義の中に作った局所変数へのポインターや参照を関数の戻り値として与えることはそれ故間違いです。それが示す先の局所変数は呼び出し側がそれを使うときにはすでに破棄されてしまっています。
int * func( ) { int iarray[ 10 ]; //配列は局所的に作られた。この関数の中だけ存在する。 ... return iarray; //iarrayは整数型配列へのポインター。 }
文法的には許されるため、コンパイルは出来ますが、警告されます。ポインターの値はコピーされて呼び出した側にわたりますが、その指し示す先はすでに破棄され、意味のある情報を保持していません。
局所変数をstatic
と宣言することによって、記憶域を継続して確保することが出来ます。この場合、その変数は制御がその関数から離れても維持されており、再度制御がその関数に戻ってきたとき、その変数は最後に代入された値を維持しています。関数定義の中で初期化されていても、それは最初の呼び出しの時だけ有効です。
int func( ) { static int i = 0; i++; return i; }
この関数は、i
が0
に初期化されていますが、呼び出されるごとにインクリメントされているので関数func
が返す数は一つずつ大きくなります。
この手法を使えば、先ほどの
iarray
でも警告は出なくなります。しかし、この関数はいつどこから呼び出されるかわかりません。ポインターで参照している最中に別のところから関数が呼び出されてiarray
の内容が書き換わるかもしれません。
このようにして用意された関数を利用するためには、それを使用するプログラム中でその関数宣言を行う必要があります。関数宣言を行う部分のソースコードがヘッダーファイルとして用意されているので、それをインクルードします。例えばsqrt
を使ってみます。
#include <math.h> main( ) { double x = 2.0; double y = sqrt( x ); }
関数sqrt
はこのコンパイル単位中で宣言されていなければなりません。Linuxでは標準的な関数であり、manで見ることが出来ます。man sqrtとやってみてください。マンページの表示の中にmath.h
をインクルードする必要があることが示されています。このように、.h
ファイル(ヘッダーファイル)には関数の宣言が記述されており、それをインクルードすることによりその関数をプログラム中で利用できるようになります。
関数の宣言と定義を別の場所でおこなうことにより、分割コンパイルが可能になります。関数の利用者側は宣言だけを用いてコンパイルをおこない、実行可能イメージを作るときにあらかじめコンパイルされている関数のオブジェクトファイルを結合します。オブジェクトファイルは通常ライブラリと呼ばれるファイルにまとめられています。
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 ..
main
はt
、すなわちプログラムコード(テキストと呼ばれています)として存在します。一方、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.a
のXXX
の部分を-lに続けて記述します。-ll3とあるのは-lに続くl3を使ってlibl3.a
というファイルを指定したことになります。同じルールで、-lmとあるとlibm.a
が指定されたことになります。
関数もまたアドレスで与えられるため、関数へのポインターも定義できます。そのポインターを用いて関数を呼び出すことも出来ます。
関数のポインターは、その定義の中で引数も宣言する必要があります。
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 )と関数名と引数並びの型が同じになり区別が出来なくなりエラーになります。
次回はいよいよクラスについて説明します。