コラムその3 〜変数の初期値〜
解説
第三回の課題プログラム3番ではなにが表示されたでしょうか。運のいい人は普通に「0」と表示されて「何だちくしょー代入なんてしなくても良かったんじゃないかー騙しやがってー」と怠惰の道に進まれてしまった方も残念ながらいらっしゃると思います。他方で驚愕の事実に出会い「どういう……ことだ……」と活動日誌を確認してもいつまでもコラムが投稿されず怒り心頭の方もいらっしゃると思います。ごめんなさい。さて、ということで変数に初期値を代入しないとどのような事態が起こるのかを確認してみましょう。
変数を代入せずにプログラムを組んでもコンパイルは通ります。そして
はい、こうなってしまいます。
このように数値を代入しないと9445848になる……というわけではありません。皆さんが試してみればわかると思いますが、9445848とは別の値になるはずです。
では変数に初期値を代入しないといったい何が変数の中に入っているのでしょうか。実はこれは「何が入っているのかわからない」というのが結論になります。何故こんなことになってしまうのかというと、コンピューターの仕組みから解説しなければいけないくなってしまい逆にわかりにくくなると思いますので、これに関してはそういうものだと思って何も考えずに覚えてください。
ということで、0だと思ったらこのような変な数値になっていてプログラム自体が崩壊してしまわないように、皆さんくれぐれも変数の初期化は忘れないようにしてください。
ミニ課題
変数に初期値を代入しないプログラムを作成し、値を確かめる。その後翌日などコンピューターを一度シャットダウンした後にもう一度同じプログラムを一から作成し、そのときの値を確かめる。コラムその2 〜変数の限界値〜
解説
第三回の課題プログラム2番を作っていたときに、奇妙な現象に出会った方がいらっしゃるかもしれません。たとえば
のようなプログラムを作った際、何故か実行結果が
になってしまう。
この原因は何なのでしょうか。
第三回の講座では内容が多くなりすぎるので触れなかったのですが、変数には代入できる最大値、最小値が決まっています。これは実際にはいろんな場合によって異なるのですが、基本的には変数には「-2147483648〜2147483647」までの数値しか入りません。約20億です。これを超えてしまうと「一周」してしまうのです。どういうことかといいますと、最大値と最小値が繋がって、2147483648を変数に入れた瞬間に-2147483648になってしまうのです。
この「一周」してしまう現象のことをオーバーフローと呼びます。
実際にどのようになるのか見てみましょう。
「2147483647→2147483647」
「2147483648→-2147483648」
このように、実際にちゃんとオーバーフローしました。
この後は「2147483649→-2147483647」「2147483650→-2147483646」「2147483651→-2147483645」のように代入する値を大きくする後とに表示される数値も大きくなり、また2147483647に達すると-2147483648に戻ります。
ミニ課題
上の画像で上げたプログラムをもう一度一から自分で作成し、値が変化するのを確認する。データ構造 第03講「データの数がいくつになるか分からない!」
目次→美祢『データ構造』 目次 - 武蔵高中 CPU愛好会 活動日誌
お詫び
他の部活の合宿との兼ね合いにより、金曜日に更新する事ができませんでした、申し訳ありません。
前回までは
指定した数のデータの集まり、「配列」を学びました。ただし、要素数は定数(の正整数)でなければならない、という制約がありました。
今回は、要素数を増減させられる「動的配列(vector)」について学びましょう。
vectorの宣言の仕方
#include <vector> int main(){ std::vector<int> vi; std::vector<char> vc; std::vector<float> vf; }
みたいな風に、「<>」の中に型の名前を入れて宣言します。また、「
※「::」の意味については本講座では詳しくはやりません、分からない方は「stdっていう偉いまとまりの中にvectorが入ってる」ぐらいの認識で大丈夫です。
#include <vector> int main(){ std::vector<int> vi(32); }
という風に、コンストラクタを呼び出すことで、最初に確保するメモリの大きさを指定する事ができます。(こうすることの意味は、“性質と注意”の項で説明します)
#include <vector> int main(){ std::vector<int> vi(50,0); }
とすると、「int vi=[50]={0};」の様に、全部中身が「0」の、要素数50の配列が初期状態としてできます。
vectorの使い方
00 #include <vector> 01 int main(){ 02 std::vector<int> vi; 03 vi.push_back(0); 04 vi.push_back(1); 05 vi.push_back(2); 06 vi.push_back(3); 07 vi.pop_back(); 08 vi.push_back(4); 09 vi.pop_back(); 10 vi.push_back(5); 11 printf("size=%d\n",vi.size()); 12 for(int i=0;i<vi.size();i++){ 13 printf("%d\n",vi[i]); 14 } 15 vi.clear(); 16 printf("size=%d\n",vi.size()); 17 }
まず先にこのプログラムの出力を覗いてみましょう。
size=4 0 1 2 5 size=0
では解説。
02行目でint型を収めるvector(以下、配列と書く)であるviを宣言しています。
03行目では、push_back()関数を使う事で、配列の最後尾に要素を追加しています。引数の型は、vectorを宣言した時の「<>」の中の型と同じ型です。
07行目では、pop_back()関数を使うことで、配列の最後尾の要素を削除しています。
11行目のsize()関数は、配列の要素数を返します。この例では、「0」→「0,1」→「0,1,2」→「0,1,2,3」→「0,1,2」→「0,1,2,4」→「0,1,2」→「0,1,2,5」となっているので、この時点での要素数は4です。
13行目の様に、配列と同じ「[]」を用いて要素の番号を指定し、その中身を得ることができます。
15行目のclear()関数を使うと、全ての要素を削除します。要素数は0になります。ある意味で初期化とも言えるでしょう。
※その他に、「イテレータ」という物を使う関数がありますが、それについては次回解説します。
vectorの性質と注意点
vectorには、配列と同じように、「順番に並んでいる」(間に他の変数が入らない)というメモリ内の置かれ方をします。そのため、push_back()や、次回解説する関数のinsert()をした後、既に並んでる領域に入り切らない場合には、新たにメモリの「順番に並べるのに十分に空いている場所」を探して、そちらにコピーされます。なので、元々「&vi[3]」であった所がpush_back()をした後にも同じ場所であるという保証がありません。つまり
#include <vector> int main(){ std::vector<int> vi; for(int i=0;i<50;i++){ vi.push_back(i); } int *seisuuP=&vi[24]; for(int i=0;i<50;i++){ vi.puch_back(i+50); } int seisuu=*seisuuP; printf("%d",seisuu); }
というプログラムを書いたとして出力が「24」である保証はなく、そうすることは危険であると言えます。
次回解説するイテレータ(これもポインタみたいなもの)を使うときには特に気をつけてください。
今回の終わりに
今回は、要素数を変えることのできる配列、vectorについて学びました。次回は、vectorの様な動的な構造においてそのある特定の要素が入っているメモリ上の位置を指すポインタである、「イテレータ」と、それを理解するために必要な「リスト」という概念を説明する予定です。
参考
C++とプログラム全般「配列よりSTL」http://www.geocities.jp/bleis_tift/cpp/stl.html
C/C++リファレンス「C++ ベクタ」http://www.cppll.jp/cppreference/cppvector.html
Programing Place「C++編(標準ライブラリ) 第2章 vector」http://www.geocities.jp/ky_webid/cpp/library/002.html
第03回「変数とは?」
今回の理解目標
- 変数とは何なのか
- 変数の内容を確認する
今までこの講座では「数字」というものを扱ってきませんでしたが、本来プログラミングは数字の操作あってこそです。前回の文字列表示に使った「printf()」なども、コンピューターの内部で数字をいろいろと操作してその結果文字列を画面に表示するにいたっているのです。
よって、今回は数字を操作する方法、そしてその操作に必要な「変数」について学んで生きたいと思います。
変数を作る
まず、変数とは何なのかですが、「数字を入れておく箱」のように思ってください。この箱である変数の中にまず数字を入れて、そこに外部からいろいろと操作を加えて中にある数字を変えたり、時には中にある数字を追い出して新しい数字を入れるなど、いろいろな使い方をします。語源としては数学で使われる「変数」という語なので、そちらのイメージを持っていただいてもかまいません。
では、まず変数を作成してみましょう。変数の作成は
int bluster;
のようにして行います。
左から、「int」の部分がこれから変数を作成しますという記号、そして文字が繋がってしまわないように空白をひとつ入れた後に次の部分へと続き、「bluster」の部分が作成する変数の名前(「温州みかん」のような箱についている名前、と考えてください)、「;」の部分がひとつの処理、つまり変数宣言の終わりを示すセミコロンです。
これで変数の作成は完了です。実際のプログラムの中では
main()
{
int garbage;
}
のようにして作成します。このように変数を作成することを変数の宣言といいます。
では、変数の宣言が完了したので、次はこの変数の中に数値を入れる方法を入れる方法を学びましょう。
変数に数値を代入する
数学を少しやったことのある方ならわかると思いますが、変数の中に数字を入れることを「数値を代入する」といいます。変数を作成しただけではそれはただの空箱……どころか中に何が入っているのかわからない危険なブラックボックス状態です。ということで変数を作成したらまずは必ず「代入」処理によって数値を入れて、箱の中を掃除してやらなければいけません。さて、この代入処理ですが
garbage = 53;
のように行います。
左に数字を代入したい変数の名前を書いた後に代入を表す記号「=」でつなぎ、右に代入したい数値を書きます。そして最後は処理の終了を示すセミコロンです。
さて、何故代入処理なのにイコール記号を使ったのか疑問に思った方もいらっしゃると思います。矢印とか他にもっとわかりやすい物があるだろう、とか。実はそれは製作者側もわかっていたと思います、ただ仕方なく行った苦肉の策だったのです。
皆さんが使っているキーボードに目を落としてみてください。qawsedrftgyhujikolp;@:[]など半角で入力できる文字がいくつも書いてあると思います。しかしよく探してみると、矢印のような「代入っぽい記号」はどこにもありません(矢印の書いてあるキーボードを持っている方もいらっしゃるかと思いますが、そのキーを押してみても矢印は画面に表示されないはずです)。残念ながらイコールのほかに使えそうな記号がなかったのです。そこで仕方なく代入記号にイコールを使っているわけです。
じゃあ本来の意味でイコール記号を使いたいという場合はどうなるんだ、ということになりますが、それに関してはまだ別の回でお話します。
「<-」で矢印みたいな記号が作れるじゃんって言うのは禁句です。
さて、この変数への初期値代入ですが、実は変数の宣言と同時に行うことができます。方法は簡単で、
int garbage = 53;
と、このように変数の宣言と代入をそのままつなげたような形となります。
これで変数に数値を代入するところまでできました。しかし、これではまだ実際に変数に何が入っているのか頭ではわかっていても実際に確認する術はありません。ということで、次は変数の中身を画面に表示する方法について学んでいきましょう。
変数の値を表示する
変数の中身を画面に表示するにはどうしたらいいのでしょうか。前回学習した関数は文字列を画面に表示する関数でした。今回もこれを使って画面に変数を表示しそうなものですが、ただ単に
puts(garbage);
のように書いてもただコンパイルエラーになるだけで画面に変数の中に入っている数字を表示することはできません。
ではどうするのか。
実はここで前回紹介したputs関数とprintf関数の最大の差が現れてきます。変数の中の数値を画面に表示する場合はprintf関数を使います。puts関数は使えません。なぜなら、puts関数は「文字列を表示する」だけの関数なのに対し、printf関数は「文字列を表示する+α」の機能を持っている関数だからです。その「+α」機能があるために、主にプログラミングにおいてはputs関数ではなくprintf関数が使われるのです。
ただし、printf関数でも同様に()の中に変数の名前を書いたところでコンパイルエラーです。printf関数で変数の中身を表示するには以下のように書きます。
printf("%d",garbage);
garbage変数がすでに宣言されているものとしてください。このとき、上のように書くことによってちゃんと変数garbageの中に入っている数字が表示されます。では、このプログラムについてひとつひとつ具体的に書いていきましょう。
最初の「printf」、「("」までは前回と変わりません。ただしその後に%dというわけのわからない何者かが発生しています。そしてその後を見ると第二回の講座におけるprintf関数の解説で「表示したい文字列を書き、それを("と")という括弧にダブルクオーテーションがついたもので囲みます」と書いてあったにもかかわらず、その後ろに「,」、そして中身を表示する変数の名前「garbage」が来た後にやっと「)」、そしてセミコロンと続いています。
さて、この不祥事はいったいどういうことなのか、と聞かれると「前回のアレは嘘です」みたいに答えるしかなくなってしまいます。というのも、前回はわかりやすくするために括弧とダブルクオーテーションをまとめてひとつの括弧みたいに扱いましたが、実際にはまあ当然ですがダブルクオーテーションと括弧にはそれぞれ意味があります。括弧で囲まれた部分はその中に関数へ渡すデータを入れるという意味、ダブルクオーテーションで囲まれた部分はそれが文字列であるという意味になります。printf関数では文字列を関数に渡して、それを関数側の処理で画面に描画してもらう、という流れになっています。
ではこれで前準備は終わりました。満を持してこのprintf関数がどのような動きで変数の中身が画面に表示されるのかを説明しましょう。といってもそれほど複雑なことではなく、ダブルクオーテーションで囲まれた中にある「%d」と書かれた部分が、後ろの「,」以下に書かれている変数の中身に置き換わります。このとき、「%d」が何を意味しているのかは考えないでください。プログラミングの初期の学習においてはいろいろな新しい記号や文法や用語が出てきます。このときに、その場で解説したほうがわかりやすいものと解説すると逆にわかりにくくなってしまうものがあります。……正直に言って「%d」が後者かというと断定はできませんが、それでも%やdがそれぞれ何が由来でその文字が使われているのかを知らなくともプログラムを組む上では支障はありません。これ以降このレポートで「意味を考えないでください」といった場合はそういうことなので、この講座で後で出てくるか、またはこの講座が終了するまでは意味について考えたり調べたりはしないでください。
さて、いろいろと話が前後いたしましたが、まだ数値が画面に表示されるところを直接見ていないと思います。ということで、以下のようなプログラムを組んで見ましょう。
main() { int garbage = 53; printf("%d",garbage); }
すると、
このようにコンパイルも通り、実行結果は
のようになります。変数に代入する数値を変えることによって、画面に表示される数字も変わります。
課題
今回の課題は以下の通りです。次回でも今回に引き続き変数についての解説が続きますので、今回の内容を必ず抑えておいてください。
1.変数を作成して数値を代入し、その内容をprintfで表示するプログラムを作成する
2.1のプログラムを作成し、変数にいろいろな値を入れて変化を観察する
3.変数を宣言するときに何も代入せず、その変数の中身をprintfで表示してみる
練習のために、課題のプログラムはそれぞれ全て一から作成してください。
課題2・3において今回の解説では理解できない結果が出る可能性があります。これに関しては次回までにコラムで解説いたします。
では、また来週の火曜日にお会いしましょう。
コラムその1 〜C言語と空白〜
「コラム」とは?
このコラムのコーナーでは、講座そのもので解説しようとすると全体の流れが見えにくくなって文章が長くなってしまうけれど、しかし重要なので解説しなければいけないという事項に関して、ちょっと長めの解説のような形をとって解説していこうと思います。解説
さて、ここで少し今まで意図的に無視していた事について解説しましょう。それは「空白」についてです。実はC言語では、どうでもいい位置に存在する空白は意味を成しません。
これはどういうことかというと、たとえば「一番簡単なプログラム」を例にとって解説しますと、「main(){}」は「main」「(」「)」「{」「}」の五つの要素に分解されるわけです。そして、この要素と要素の間、つまり「どうでもいい位置」にはいくら空白があってもかまいません。この場合の空白というのは通常の空白(半角スペース)のほかにもEnterキーによる改行も表します。ただしC言語は英語で書かなければいけないプログラミング言語なので日本語の空白である全角スペースは使用することはできません。
よって前回最後に示したプログラムは
main(){printf("世界中に狂気と混乱を")}
みたいに一行に並んでいて正直に言ってみにくかったと思いますが、わざわざこんなことをしなくても普通に
main() {
printf( "世界中に狂気と混乱を" ) ;
}
というように書けばいいのです。
極論を言えば最悪
main(
) {
printf( "世界中に狂気と混乱を");
}
みたいな書き方をしてもちゃんとコンパイルは通るわけです。
実際のプログラマは空白の入れ方に関してはそれぞれ独自のスタイルがあるとはいえ、必ずこのように空白を空白を入れることによって画面を見やすくしています。
ということで、第三回以降に出てくるソースコードではこのように空白を入れて解説していくことにいたします。
ミニ課題
一応コラムも解説なの課題を設けます。ただし本講並みの課題の量があるとつらいと思いますので基本的にひとつのコラムにつき課題はひとつとなります。第二回の文字列表示プログラムのいろいろな位置に空白を入れてみてどこで要素が区切れているのか確認する。そしてそれぞれの要素の間ごとに半角スペースを入れたプログラムを作成し、コンパイルして実行できることを確認する。
全体活動日誌 第35章
[活動日 2011年7月11日〜2011年7月16日(第15週)]
【各プロジェクトの進行状況】
ChaosWorld 〜future of hopes〜
製作者:G&Tジャンル:カオス弾幕シューティング
[今週の作成内容]
・システム調整
ミッションズ
製作者:ぅいーじジャンル:RPG
[今週の作成内容]
・難易度調整
アクションゲーム
製作者:スライム1/2・きたさいたまブラスター・ぅいーじジャンル:アクションゲーム
[今週の作成内容]
・ステージ作成
・難易度調整
データ構造 第02講「配列を使おう」
目次→美祢『データ構造』 目次 - 武蔵高中 CPU愛好会 活動日誌
前回は
前回は、「配列はどのように宣言し、どのように初期化するのか?」という事を学びました、では、今回は「配列はどのようにプログラム中で使っていくのか」という事を学びましょう。
要素の指定
配列の要素は、「 変数名[要素番号] 」という書き方で表します。
例えば、「int seisuu[6];」で宣言した配列の3番目は「seisuu[2]」です。(前回も言いましたが、最初の番号は[0]です)また、大括弧「[]」の中には変数を入れることもできますし、もちろん配列の要素を入れることもできます。
例02_1
int map[6][6]={ //マップのどの位置に何番のアイテムがあるか {0,1,2,3,4,5}, {0,0,0,0,0,5}, {0,1,2,3,4,3}, {0,4,2,3,0,5}, {0,1,0,0,0,1}, {0,4,2,3,0,1}}; int item[16]={0,1,-2,3,-4,5,-6,7,-8,9,-10,11,-12,13,-14,15}//何番のアイテムでいくつの効果を受けるか(例:回復量) int my_x=4,my_y=2; //自分の初期位置 int life=100; while(1){ //毎ターン行う他の処理 life+=item[ field[my_y][my_x] ]; //現在位置のアイテムを拾って回復orダメージ field[my_y][my_x]=0; //現在位置のアイテムを消費 //毎ターン行う他の処理 }
この例では、毎ターン、そのフィールドにある回復orダメージアイテムを必ず拾って(アイテムは消滅)、その効果分の回復orダメージ(マイナスの量の場合)を受ける様になっています。
「life+=item[ field[my_y][my_x] ];」→「life+=item[ field[2][4] ];」→「life+=item[4];」→「life+=(-4);」→「lifeを4減少させる。」というふうに内側から順々に当てはまっていきます。
ちなみに、「int map[6][6];」という書き方は前回やってませんが、「二次元配列」と呼ばれる技法で、配列の配列ができあがります。初期化は
int map[6][6]={ {map[0][0]の内容,map[0][1]の内容……,map[0][5]の内容}, {map[1][0]の内容,map[1][1]の内容……,map[1][5]の内容}, {map[2][0]の内容,map[2][1]の内容……,map[2][5]の内容}, {map[3][0]の内容,map[3][1]の内容……,map[3][5]の内容}, {map[4][0]の内容,map[4][1]の内容……,map[4][5]の内容}, {map[5][0]の内容,map[5][1]の内容……,map[5][5]の内容}};
という風に行います。(改行は見やすくする為に付けました)
※この講座では触れませんが、「毎ターン行う他の処理」には、おそらく「矢印キーに対応して自分を移動させる(キーが入力されるまでは待機する)」等の処理が含まれている事でしょう。
例02_2
int seisuu[15][15]; for(int j=0;j<15;j++){ for(int i=0;i<15;i++){ seisuu[j][i]=-1; } }
この例では、「配列内全ての要素を-1で初期化」しています。前回やった「0で初期化する」のなら簡単に0で初期化できますが、他の数ならこのように他の方法を取る必要があります。
配列の名前と要素の名前
「int seisuu[32];」と配列が宣言されていたとして、「seisuu[5]」とか「seisuu[i]」とかが(この場合int型の)要素である変数1つを指すことが分かりました。
では、この時「seisuu」という名前は何を指すのでしょうか?
答えは「先頭の要素へのポインタ」です。
つまり、配列の名前「seisuu」と、先頭要素のポインタ「&seisuu[0]」は同じものを指します。(二次元配列の場合の名前とポインタアドレスの関係は、参考URLをご覧ください)
例えば、標準ライブラリ「stdlib.h」内にある変数の初期化の為の関数「memset」という関数ですが、「(初期化したい変数へのポインタ,各ビットに入れたい0か1,何桁初期化するか)」引数を持っています。
int x; int y[16]; int z[16][16]; memset(&x,0,sizeof(int)); memset(y,0,sizeof(int)*16); memset(z,0,sizeof(int)*16*16);
見ていただければ分かるとおり、「&」が付くか付かないかという違いが発生します。配列の名前はそれ自体がポインタなので付きませんが、「x」は「y,z」とは違い1つの変数の名前なので、そのメモリを指すポインタは「&x」と表記する必要があるのです。
※ポインタについては『C言語入門講座』の方で後々解説されると思いますが、「ポインタを関数の引数にした場合はそのメモリの位置について扱い」「生の変数の形で引数にした場合は、関数の内部で使う変数にコピーした、中身の数値について扱う」という違いがあり、端的に言うと「ポインタなら渡す元の変数の数値を操作できて、生だと数値はその関数の中でだけ変化する」という事です。
参考
初心者のためのポイント学習C言語「10-2:配列とポインタ」http://www9.plala.or.jp/sgwr-t/c/sec10-2.html