234 13.2 ユオブジェクトの型を知らずにオブジェクトを受け取る 第 13 章継承と動的結合を使う compare 関数は、引数に Grad オプジェクトを渡されても、 name 関数は Grad と core の両方にあると考えら れるので問題は起こりません。しかし、学生を名前ではなく最終成績で比較したい場合はどうでしよう。たとえ ば、学生を名前順に並べてその最終成績を出力するのではなく、最終成績順に学生を並べなおす必要も出てくる でしよう。 そこで、 c 。 mpare に似た関数を書いてこの問題を解決してみましよう。 b001 compare—grades(const Core& cl , const Core& c2) return cl . grade ( ) く c2. grade() ; compare との違いは、内部で呼び出しているのが n e ではなく grade 関数というだけです。しかし、このわ ずかな違いが大きな違いになるのです。 違いは Grad クラスが grade 関数を再定義していることから来ます。私たちは、 Grad と core の同名の関数 を区別するようなことはしませんでした。 compare-grades 関数を実行すると、 compare が Core::name を実 行するように compare-grades は Core::grade を実行します。そこで、もし Grad オプジェクトについてこ の関数を実行すると、 core の grade が実行され、間違った成績で比較が行われてしまうのです。 grade 関数は core と Grad で違うからです。 Grad オプジェクトについては thesis まで計算に入れた Grad::grade を実行 しなければならないのです。 こで、 compare-grades では、渡されたオプジェクトの実際の型の grade を実行するようにしたいと考え ます。つまり、 cl 、 c2 が Grad オプジェクトの参照なら、 compare_grades の中では Grad の grade を実行し たいはずです。そして、もちろん、オプジェクトが core なら、実行されるべきなのは c 。 re の grade です。 のような判断を実行時に行いたいということです。言い換えれば、関数に渡されるオプジェクトの実際の型に基 づいて正しい関数が実行されるようにしたいのです。ただし、渡されるオプジェクトの型は実行時までわからな いことに注意してください。 このような実行時の選択をサポートするために、 C ト + には virt Ⅱな関数 ( 仮想関数 ) というものがあります。 class Core { public : virtual double grade() const ; / / vi て tual というキーワードを付けた 上のようにすることで grade を virtual 関数にできるのです。このようにして compare_grades を実行する と、参照 cl 、 c2 の指すオプジェクトの実際の型を見て、正しいバージョンの grade が呼ばれるようになるので す。これは引数に渡されたオプジェクトを調べて、実行すべき関数を決めているということです。もし引数が Grad オプジェクトを指すなら、 Grad::grade 関数が実行され、 core なら c 。 re : : grade 関数が実行されます。 この virtual というキーワードは、クラス定義の中でだけ使われます。関数の定義がその宣言とは別にク ラス定義の外にある場合、定義のところで virtual を繰り返しません。そのため、 core::grade の定義は変 わらないのです。また、同じように、 virtual という性質は継承されるので、 Grad クラスの grade の宣言に
112 第 6 章ライブラリのアルゴリズムを使う たとえば、最初のメジアンを使う成績処理の方法は median ー analysis という関数で実行されるとし、 2 つの学 生グループの成績処理の結果を出力する関数を write-analysis とすると、その実行は次のようになります。 write—analysis (cout , "median" , median-analysis , did, didnt) ; この write-analysis を書く前に、 median-analysis を書いてみましよう。この関数は学生のデータである vect 。 r を引数に取り、始めに決めたメジアンの方法で成績処理を行い、これらの学生の成績のメジアンを戻す ことにします。 / / この関数は実はうまく働かない double median—analysis (const vector く Student—info>& students) vector く double> grades ; transform(students . begin() , students. end() , back-inserter(grades) , grade) ; return median(grades) ; この関数は一見複雑そうですが、実は、新しいものは transf 。 rm 関数だけです。この関数は、引数に 3 つの反 復子と 1 つの関数を取ります。最初の 2 つの反復子は変換 (transform) する要素の範囲を表します。そして、 3 番目の反復子はこの関数の実行結果を書き出す場所を表しているのです。 この transf orm 関数を呼び出すときには、入力シーケンスからの値を書き出す場所が 3 番目の引数の反復子 のところにあるようにしなければなりません。今の場合は問題ありません。 back-inserter 6.1 ) を使って いるので、 transform の結果は grades に追加されていくことになります。この場合、 grades は必要なだけ拡 張されるのです。 transf orm の 4 番目の引数は、 transf orm が入力シーケンスに順番に適用し、出力シーケンスを作っていく 関数です。そのため、今の例では、 transform が呼ばれると students の各要素に grade が適用され、その成 績が grades という vector に入れられていくわけです。このようにして全学生の成績が求まったら、 4.1.1 で 定義した median を使って、メジアンを計算すればよいわけです。 しかし、 1 つだけ問題があります。コメントに書いたように、この関数はうまく働かないのです。 その 1 つの理由は、 grade にはいくつものオーバーロードされたバージョンがあるからです。 transform の 引数としての grade には引数を付けないので、コンパイラが判断できないのです。これは、もちろん 4.2.2 の バージョンを使いたいのですが、そのようにコンパイラに知らせる必要があるのです。 もう 1 つの理由は、 grade 関数は学生がまったく宿題を提出していない場合に例外を投げる ( スローする ) の 、 transform 関数は例外について何もしないからです。もし例外が起こると、 tra Ⅱ sf 。 rm 関数は例外の発生 時点でストップし、制御を median_analysis に戻します。しかし、 median_analysis も例外を処理しないの で、例外はさらにその外へ投げられます。その結果、関数は途中で止められ、その関数を呼び出した関数に例外 が投げられ、その関数も止められ ... が、 cat ch 節に到達するまで続いていくのです。もし、適当な cat ch がな ければ、実際今の場合はそうなっていそうですが、プログラムそのものが止められ、メッセージが出力されるで しよう ( これは環境によりますが ) 。 この問題は、 grade 関数を try というプロックで使い、例外を処理する補助的な関数を作ることで解決でき ます。その場合、 grade は引数として渡されるのではなく、その関数内で実際に使われるので、コンパイラはど のバージョンを使うべきか判断できるのです。
6.2 成績処理の仕組みを比べる 115 この accumulate を使うことですべての要素の合計が計算できるので、後はそれを全要素数である v. size() で割って平均を出しています。そして、これを関数の呼び出し側に戻しているわけです。 average 関数を定義したので、平均の方法を使った成績を計算する average-grade を書くことができます。 return median(grades) ; back—inserter (grades) , average-grade) ; transform(students . begin() , students . end() , vector く double> grades ; double average—analysis (const vector く Student—info>& students) こまでの下準備で、 average-analysis を簡単に書くことができます。 を使っています。 この関数は宿題の総合点数を出すのに average を使い、それから最終成績を出すために 4.1 で定義した grade return grade (s . midterm, s . final , average(s . homework) ) ; double average-grade (const Student—inf0& s) 最終成績を付けるもので、こうして実際に提出された宿題のメジアンを使って成績を出すことになります。 して 0 でない宿題の成績を取り出したら、碆4.1 の grade を使います。この関数は、試験と宿題の総合点数から この関数はまず homework から 0 でない要素を取り出し、それを nonzero という vector に格納します。こう return grade(s . midterm, S . final , median(nonzero) ) ; else return grade(s . midterm, s . final , 0) ; if (nonzero . empty() ) back—inserter(nonzero) , 0 ) ; remove—copy(s . home 0 て k . begin() , s . homework. end() , vector く double> nonzero; double optimistic—median(const Student—info& s) / / 要素数が 0 でない場合はメジアンを戻し、要素がないときは 0 を戻す その場合は、宿題の成績は 0 点とすることにします。 る optimistic-median 関数を書いてみましよう。もちろん、宿題をまったく提出しない学生もいるわけです。 基づいて、提出した宿題のメジアンをもって宿題の総合点数にするわけです。まず、この楽観的な方法を実現す 題の成績は、提出した宿題とほば同じだろうという楽観的な考えに従っていることを表しています。この仮定に 最後の方法を表す関数は、 optimistic-median ( 楽観的なメジアン ) です。この名前は、提出しなかった宿 6.2.4 提出した宿題のメジアン 使うかの違いだけです。 median-analysis と average—analysis の違いは、名前そのものと grade-aux を使うか average-grade を
13.2 多態性 ( ポリモルフィズム ) と仮想関数 235 virtual を付ける必要もありません。ただし、新しい core クラスの定義のコードはコンパイルし直す必要があ ります。そうすると、基底クラスの関数で virtual と指定したことになり、コードは望んだ振る舞いをするよ うになります。 13.2.2 動的結合 virtual な関数 ( 仮想関数 ) のこのような実行時の選択は、参照かポインタを通して関数が呼ばれるときだけ 行われます。仮想関数であっても、 ( 参照やポインタではなく ) オプジェクトに対して実行されるなら、その型 はコンパイル時に決まっているのであり、実行時に変わることはありません。一方、参照やポインタは基底クラ スでも派生クラスでも指すことができ、したがって、それらの指すオプジェクトの型は実行時に決められること になります。 virtual の仕組みが異なる結果を出すのはこのような場合なのです。 たとえば、 compare-grades を次のように書いたとします。 / / 誤った実装 b001 compare—grades (Core cl , Core c2) return cl . grade ( ) く c2. grade() ; パラメータはオプジェクトであって参照ではありません。この場合、 c 1 と c2 の型はいつも core とわかってい るのです。このような定義でも引数に Grad オプジェクトを使うことができます。しかし、このとき、 Grad の型 は使われません。その場合、オプジェクトの core 部分だけが渡されるのです。つまり、 Grad オプジェクトの core にない部分は、切り落 (cutdown) とされてしまい、 core の部分だけがコピーされて compare-grades に渡されることになります。パラメータが core オプジェクトであるため、この関数内の grade 呼び出しは、 ( コンパイル時に ) core::grade になります。このようにコンパイル時に呼び出される関数が決められているこ とを、静的結合 (statically bound) と言います。 参照やポインタを使う関数呼び出しの場合は動的結△ , 、ロロ (dynamic binding) とよばれますが、これと静的結合 の違いは、 C+ + がどのように OOP をサポートするかを理解する上で、非常に重要です。静的結合という言葉が 「呼び出す関数がコンパイル時に決められる」を意味するのに対し、動的結合という言葉は「関数の呼び出しが 実行時に決められる」という意味です。仮想関数であっても、オプジェクトに対して呼び出すなら、その呼び出 しは静的結合です。そのオプジェクトがコンパイル時と実行時に異なる型になることはないからです。一方、ポ インタや参照に対しては、仮想関数は、実行時に内容が決められる、動的結合になります。仮想関数は、参照や ポインタが実行時に指す型によって、異なるバージョンのものが使われるのです。 Core& r = g; Core* p; Grad g; Core C ; c . grade(); g. grade(); p->grade ( ) r ・ grade(); / / core: : grade ( ) に静的に結合 / / Gore: : grade ( ) に静的に結△ ; / / 動的な結合 : p の指すオプジェクトの型によって異なる / / 動的な結合 : r の指すオプジェクトの型によって異なる
6.2 成績処理の仕組みを比べる double grade—aux(const Student-inf0& s) try { return grade(s) ; } catch (domain—error) { return grade(s . midterm, s . final , 0 ) ; 113 この関数は 4.2.2 の grade を呼びます。例外が起こると、 catch のプロックが実行され、引数に 2 つの試験結 果と宿題の合計 3 つの d 。 uble 値を取る 4.1 で定義した grade が呼ばれます。これにより、宿題を提出してい ない学生の宿題の点数は 0 点とし、中間と期末の試験結果のみを使うことになるわけです。 const vector く Student—info>& didnt) const vector く Student_info>& did, double analysis (const vector く Student—inf0>&) , void write—analysis (ostream& out , const string& name , の学生グループそれぞれに使うものです。 成績処理を見たので、次は、結果を書き出す write ー analysis を書きましよう。これは、成績処理の関数を 2 つ return median(grades) ; back—inserter(grades) , grade—aux) ; transform(students . begin() , students . end() , vector く double> grades ; double median—analysis (const vector く Student—info>& students) / / 今度はうまくいく これをもとに成績処理関数は次のように書き換えられます。 out くく name くく median(did) median(didnt) くく analysis(did) くく くく analysis(didnt) くく endl ; この関数も驚くほど短いものですが、新しいことを 2 つ使っています。まず、関数を表すパラメータの定義の仕 方です。パラメータの analysis の定義は、 4.3 の関数の宣言と同じ形をしています。 ( 実際、 10.1.2 で説明 しますが、もう少し見た目以上のものがあります。しかし、それはここでは問題になりません。 ) もう 1 つの新しいものは、 vo id です。この組み込み型は特別な場合にのみ使われるのですが、その 1 例が、 上のように戻り値の型としての void です。このように関数が void を「戻す」とすると、この関数は何も戻さ ないということになります。値を戻さない関数の実行を終了するには、値なしで return を使います。つまり、 こで、プログラムの main を書くことができます。 す。そのようなことは普通はできないのですが、 v 。 id を戻す関数ならよいのです。 となるのです。あるいは、上の例がそうであるように、途中に return がなければ、関数は最後まで実行されま return ;
238 cout くく students . name ( ) くく string(maxlen 十 1 students [i] . name . size ( ) , try { 第 13 章 継承と動的結合を使う students [i] . grade() ; double final—grade cout . precision() ; streamsize cout くく setprecision(3) くく final_grade くく setprecision(prec) くく end1 ; } catch (domain-error e) { cout くく e . what() くく endl; / / Core : : grade return 0 ; Grad に対しても、型の定義を変えれば同じプログラムが使えます。 int main ( ) vector く Grad> students ; Grad record; / / vect 。 r の要素の型が異なる / / 異なる型のオプジェクトへの読み込み string: : size—type maxlen = 0 ; / / データを読み込み、保持する / / c 。て e ではなく Grad から読み込む while (record. read(cin)) { 0 ; i ! = students . size() ; try { students [i] . name . size() , くく string(maxlen 十 1 cout くく students [i] . name ( ) for (vector く Grad> : : size—type i / / name と grades を出力する sort (students . begin() , students. end() , compare) ; / / 学生データを名前川頁に整列させる students . push—back (record) ; maxlen = max(maxlen, record. name() . size() ) ; return 0 ; / / Grad: :grade cout くく e . what ( ) くく endl ; } catch (domain—error e) { くく setprecision(prec) くく endl; cout くく setprecision(3) くく final—grade streamsize prec cout . precision() ; double final—grade students Ci] . grade ( ) ; のため、 record は core であるか Grad であるか最初から固定していて、 record. read(cin) でどのバージョ び出しているのであって、オプジェクトを指すポインタや参照に対して呼び出しているのではないからです。そ ーでは read 関数の virtual な性質は何も関係ないのです。関数をオプジェクトに対して呼 るということに 型によって決まるわけです。この関数呼び出しは静的結合であることに注意してください。 re cord の型で決ま とえば、ある工クスプレッションで core : : read 関数と Grad : : read 関数のどちらを呼び出すかは、 record の もちろん、それぞれのプログラムで実行される関数は、 re cord と ve ctor の要素の型によって決まります。た
328 continue, cout, 3 , 7 , 310 Core, 227 , 232 , 235 , 246 copy, 102 , 121 , 146 , 225 , 319 , 321 308 duplicate—chars , 185 do while, 135 , 136 double, 36 , 295 , 301 domain_error, 53 , 72 do, 307 display, 272 , 283 did-all_hw, 110 destroy, 203 delete ロ , 184 , 186 delete, 183 , 186 def ine ディレクテイプ , 67 deallocate, 203 data, 189 , 223 , 225 , 319 ( 亟〕 , 36 く cstring>, 176 c_str, 181 , 224 , 225 く cstdlib>, 134 , 137 く cstddef>, 174 , 175 create, 205 , 2 ( ) 6 erase, 77 , 82 , 83 , 96 , 314 , 315 equal, 105 , 321 eof, 312 enum, 303 endl, 3 , 5 , 36 , 311 end-of-file, 36 end, 44 , 80 , 95 , 137 , 142 , 193 , 205 , 313 emptyvec ( ) , 58 empty, 96 , 111 , 313 fill, 321 fgrade, 75 false, 19 , 32 , 299 fail, 312 \f, 301 extract_fails, 76 , 77 , 85 , 117 , 119 extern, 295 explicit, 190 , 220 Estragon, 12 , 17 find, 107 , 121 , 137 , 144 , 145 , 315 , 320 , 321 find_if, 104 , 121 , 173 , 321 grade-aux, 113 く fstream>, 186 , 311 front_inserter, 121 front, 314 friend, 217 , 219 , 224 , 247 , 251 , 295 Frame—Pic, 275 , 284 frame, 92 , 271 , 278 for ヘッダ , 27 for の中身 , 27 for, 26 , 307 flush, 311 float, 295 , 301 first, 125 , 137 , 319 find_urls , 106 , 314 索引 gen-aux, 133 gen-sentence, 132 get, 216 , 310 get-analysis-ptr, 293 , 298 inline, 72 , 295 include ディレクテイプ , 2 , 66 ifstream, 186 , 311 ifndef ディレクテイプ , 67 if, 23 , 307 histogram, 290 He110 , world ! プログラム , 1 height, 273 , 280 HCat-Pic, 270 , 277 , 283 hcat, 94 , 271 , 279 Hand1e, 254 GUARD, 67 grow, 207 Grammar , 131 grade, 52 , 54 , 62 , 69 , 156 , 158 , 159 , 161 , 231 Grad, 228 , 231 , 232 , 246 goto, 308 good, 312 getline, 90 , 97 , 318 istream—iterator, 150 , 310 istream, 150 , 311 isspace, 88 , 97 , 104 ispunct, 97 is-palindrome , 105 is-negative, 173 islower, 97 isdigit, 97 isalpha, 97 isalnum, 97 , 107 く iostream>, 2 , 310 く ios>, 36 , 311 く iomanip>, 36 , 311 invalid—pointer, 182 invalid—argument , 72 int, 2 , 22 , 295 , 296 , 298 inserter, 121 , 314 insert, 93 , 96 , 313 , 315 , 320 10 Ⅱ g int, 298 10 Ⅱ g double, 301 long, 32 , 295 logic—error, 72 く list>, 317 list, 84 , 86 limit, 189 lexicographical—compare , letter-grade, 178 length—error, 72 1 , 299 key-type, 315 く iterator>, 102 , 310 iterator, 80 , 95 , 191 , 205 , 312 isupper, 97 321
75 第 5 章 シーケンシャルコンテナと string の解析 こまで、 C + + 言語そのものを学ぶのによいスタートだったと思います。また、 string と vector も学びまし た。これらを使うだけで多くの問題を解決することができます。 この章では、興味の中心をその先に向け、ライプラリの使用についてもう少し深く考えていくことにします。 ライプラリのツールを使うと、これまで以上に面倒な問題を解決できることがわかるでしよう。 標準ライプラリは有用なデータ構造や関数を提供するだけでなく、一貫したアーキテクチャー ( 構造 ) を持っ ています。あるコンテナの振る舞いを理解したなら、別のコンテナを理解することも容易になるのです。 たとえば、この章の後半で見ますが、 string をあたかも vect 。 r のように扱うこともできるのです。このよ うにライプラリのある型の多くの有用な操作が、論理的には他の型の操作と同じになるのです。ライプラリは、 異なる型に対しても、相当する操作は同様にできるよう設計されているからです。 5 ユ学生をカテゴリに分類する 学生に成績をつける問題 ( 4.2 ) をもう 1 度考えてみましよう。ただし、今度は最終成績をつけるだけでな く、どの学生が不合格になったかを判定するようにしたいと思います。まず、 student-info の vector に学生 のデータがあると仮定し、そこから不合格の学生を抜き出し、別の ve ct or に記録するとします。また、不合格 の学生はもとの vect 。 r から削除し、この vect 。 r は合格者だけの記録にします。 次に示す、学生が不合格かどうかを判定する簡単な関数から始めます。 / / 学生が不合格かどうかを判定する関数 b001 fgrade (const Student-inf0& s) return grade (s) く 60 ; こで成績を計算するのに 4.2.2 の grade 関数を使い、 60 点未満は不合格としました。 望みのコードを書くのに、もっともストレートなやり方は、合格者用の vector と不合格者用の vector を 作っておいて、学生 1 人ひとりのデータをチェックしながら、それをどちらかに振り分けていくというものです。 / / 最初の案 : 合格者 (pass) と不合格者 (fail) を分ける vector く Student—info> extract—fails(vector く Student—info>& students) vector く Student—info> pass, fail ; for (vector く Student_info>: :size—type i
4.4 成績処理プログラムの分割 かチェックさせることになるからです。ただし、 ではありません。 なので、ヘッダファイルをインクルードしたソースファイルをチェックしておくことはとても有益ですが、完全 このようなチェックは本来プログラム全体に行われるべきもの 69 double と int の 2 種類の値を戻せないからです。しかし、次のように宣言してしまうと これをコンパイラは、コンパイル時にエラーとします。というのは、 median (vector く double>) は同時に int median(std: : vector く double>) ; / / 戻り値が d 。 uble でない median に対し、誤って次のように不完全な宣言をしたとします。 ると判断し、見つからない定義はどこか別のところにあると仮定するからです。たとえば、 4.1.1 で定義した れは、もし宣言と定義が一致していなければ、コンパイラはこれらがオーバーロードにより異なる形をしてい クが完全であることを意味していますが、それがどうしてチェックの不完全さにつながるかわかりますか。そ と定義は戻り値とパラメータリストまで含めて一致していなければならないのです。これはコンパイラのチェッ チェックが有益である理由とそれが完全ではない理由には共通の原因があります。 CH - + 言語では、関数の宣言 double median(double) ; / / 引数の型が vect 。 r く d 。 uble > でない コンパイラは ( これ自身は ) 工ラーとは判断しません。それは median (double ) という関数がどこかに定義さ れているかもしれないからです。この関数を使うときに初めて、その定義が探され、定義がなければその時点で のみ工ラーになるのです。 ・をすることに問題はありません。ヘッダファイルと違い、ソースファイルはこ ソースファイルで using 亘 ーを使うかどうかは、純粋 れらの関数を使うプログラムに何の影響も与えないからです。したがって、 using 宣新 にこのファイルだけの問題になるのです。 最後に残ったのは、いくつかのオーバーロードバージョンのある grade 関数のヘッダファイルを書くという #endif double grade (const Student-inf0t) ; double grade(double , double , const std: :vector く double>&) ; double grade (double , double , double) ; #include "Student_info . h" #include く vector> / / grade . h #def ine GUARD-grade-h #ifndef GUARD-grade—h ことです。 このようにオーバーロードされた関数の宣言を一箇所にまとめておくと、それぞれの違いが見やすいことにも注 ファイルの名前は使用するコンパイラにあわせて、 grade ・ cpp 、 grade. C 、 grade. c などにします。 意してください。これらは相互に関連しているので、その定義は 1 つのファイルにまとめることにしましよう。 #include #include #include #include く stdexcept> く vector> grade .
158 第 9 章新しい型を定義する 関数名にある : : は 0.7 から出てきていますが、標準ライプラリ内で定義されている名前を使うた めにあるスコープ演算子と同じものです。たとえば、 string のメンバである size_type という名前を string: :type—size と書きました。同様に、 Student-info: :read で、「 Student-info のメンバである name という名前の関数」ということになります。 このメンバ関数は istream& パラメータのみを持ちます。この理由は、前のバージョンで必要だった student-info& は明示的でなくても、いつもあると考えることができるからです。実は、 string や vector の メンバ関数を呼び出すときには、どの string オプジェクトに対してか、どの vector オプジェクトに対してか を指定していました。たとえば、 string オプジェクト s に対して size を使うには、 s . size ( ) としました。逆 に string オプジェクトを指定しないで string のメンバ関数を呼び出すことはできないのです。このように 新しい read 関数を呼び出すときも、読み込みをしている student-info オプジェクトをはっきり示すことにな るのです。この場合、そのオプジェクトを read 関数の中で使うことができます。 read の中で他のメンバを使うときには修飾を付けません。というのは、それらが現在操作しているオプジェ クトのメンバであるからです。言葉を換えると次のようにも言えます。もし、 student_info オプジェクトで ある s に対して s . read ( cin) という関数呼び出しをすれば、それは s の操作であるわけです。したがって、 read カ smidterm 、 final 、 homework を使うとき、それは s . midterm 、 s . final 、 s . homework を意味するの それでは、別のメンバ関数 grade 見てみましよう。 double Student-inf0 : :grade ( ) const return : : grade(midterm, final , homework) ; これは 4.2.2 のものに似ています。そしてその違いは read のときと同様です。 grade は student_info のメ ンバ関数として定義され、そのため、引数という形で student_info オプジェクトを受け取ることなくそのメ ンバにアクセスでき、しかも他のメンバにアクセスするのに修飾を使わないのです。 ただ、上のコードは read に比べて、 2 つの重要な違いを含んでいます。まず、 ::grade という関数呼び出 しに注意してください。 : を前に付けるのは、それによって、これはメンバではないことを示しているのです。 今の場合、 4.1.2 で定義した d 。 uble が 2 っと vecotr が 1 つの引数を取る同名の関数 grade を呼び出した いので、この : : は必要なのです。もし、これを入れないと、コンパイラは student_info 内の grade を再び student-info : : grade と解釈し、「引数を 3 つも持っことはできない」というエラーになってしまうでしよう。 もう 1 つの重要な違いは、 grade のすぐあとに const を付けているところです。これは新しい関数と前の関 数の宣言を見比べるとわかりやすいと思います。 double Student—inf0 : : grade() const { double grade (const Student-info&) { / / メンバ関数バージョン / / 4 . 2 . 2 のオリジナルバージョン 前のバージョンではパラメータリストの stude Ⅱ t ー info の参照に const を付けました。こうすることで、 grade のパラメータが const Student_inf 。オプジェクトになり、これを関数内で変更しようとするとコンパイラが工 ラーと判断してくれます。 メンバ関数を呼ぶときには、そのメンバ関数の属するオプジェクトを引数に渡すことはありません。そのた め、 const を付けるべきものが、パラメータリストには何もありません。その代わり、関数自身に修飾を付け、