C++講座++
この記事について
この記事はCCS Advent Calendar 2019 - Adventarの6日目の記事です。
はじめに
弊サークルのC++講座を履修してはや2年、講座ではやらなかったことに直面し、ほえ〜〜〜〜〜〜ってなってきました。
それを解説していこうと思います。
といってもどれも深くまではやってないので目次みて知ってる〜〜〜ってなったやつはみる必要ないと思います。
なんなら全部知らなくてもゲームは余裕に作れちゃうからそれよりもゲー制頑張ってくださいってお気持ち。( ˘ω˘)
しかも非推奨なものまである・・・。
あとC++講座受講勢は最初の4項以外は講座全部終わった後にみたほうがいいかも。
知らね〜〜〜〜〜〜
右辺値参照
下のようなかんじで使う左辺値参照はC++講座で出てきたと思います。
auto& x = zoo.bird.pengin;
左辺値(その名の通り左辺に置くことができるやつ)(クソ雑に言うと変数(const修飾された定数も含む))に対して右辺値への参照というものがあります。
右辺値とは関数の返り値のようなすぐに消えてしまうオブジェクトのことをいいます。
左辺値参照では型の後に&をつけていましたが、右辺値参照では&&をつけます。
つまり以下の通り。(add関数は2つの引数の和を返す関数)
int&& x = add(2, 10);
正直この程度のことで右辺値参照を使う必要はありませんが、返り値が巨大なクラスだったりすると戻り値から代入先変数へわざわざコピーする必要がないため非常に有効です。
また、add関数の返り値(のオブジェクト)は本来はすぐに消えるはずでしたが、これにより x が消えるまでは永らえることができました。
そのため、右辺値参照は一時オブジェクトの延命をさせるとも言われます。
std::array
C++講座ではvector以外のSTLは利用されなかったけどSTLはももはらさんが解説してくれると思うので、他のSTLと比べて特殊感が薄いし競プロでは活躍しなそうなこいつだけ。
std::arrayは固定長の配列でstd::array<[型], [要素数]>ってかんじで使う。
つまり[型] array[要素数]。
じゃあ別に普通の配列でよくね?ってかんじがするかもしれないけどこいつはちゃんと要素数が決まっていて、しかも配列として使っていることを保証できる。
そのため関数の引数にとる固定長配列の要素数をコンパイル段階で制限することができるし、2重配列も容易に渡すことができる、最強。
しかも普通の配列はポインタと区別がつかないのに対してそこも明らかになってる。
基本的にC++では固定長配列はstd::array、可変長配列はstd::vectorを利用するのが吉。
ただし、ライブラリによってはそれよりも推奨されているやつがあるときがあるのでその場合はそちらを。(例えばSiv3dでは固定長配列ではstd::arrayを、可変長配列ではArrayを推奨している。)
拡張for
従来のfor文では配列を回すときにこうしてたはず。
for(int i = 0; i < ary.size(); i++) { // なんか処理 }
これを拡張forで書くとこうなる。
for(auto i : ary) { // なんか処理 }
最強に便利なのがmapやlistなどもこれで回せるということ、最強に便利。
でも実はこれ、要素の書き換えができない。
コピーして回してるのかと思ってたけどどうも違うらしい。(よくわかってない顔。)
auto&やauto&&にすることで書き換え可能になる。(よくわかってない顔。)
const修飾とポインタ修飾
これは私がC++講座を受けたときはポインタ完全理解幼女先輩によって解説されましたが、18以降はどうかしらないので一応。
ポインタ修飾がないとき、const は型名の前につけても後につけてもどっちでもいいです。
つまり以下のやつは両方ともおけおけおっけー
const int A = 10; int const B = 53;
*がついてても同じよ〜って行きたいところですが、書き換えをさせないというのはポインタの場合2つの対象があります。
- ポインタの指す先を参照したオブジェクト
- ポインタの指す先
ポインタの指す先を参照したオブジェクトをconstにする場合はconst [型]*となります。
[型] const*でも大丈夫。
こんなかんじ。
const int a = 3; const int b = 44; const int* p1 = &a; //*p1 = 0; エラー、参照先を書き換えてはならない p1 = &b; // 指す先は変更可能なのでおけおけおっけー //int* p2 = &a; エラー、参照先が書き換えられてしまう可能性があるのでconst intのポインタを渡すことはできない
対してポインタの指す先をconstにする場合は [型]* const となります。
int a = 3; int b = 44; int* const p1 = &a; p1 = &b; // 指す先は書き換え可能なのでおけおけおっけー const int c = 0; // int* const p2 = &c; エラー、参照先は書き換え不可能なのでダメ、しかもc はconst int なので二重にダメ
指す先も参照先も書き換え内容にする場合は上記2つを合わせて const [型]* const 、または [型] const* const となります。
const の位置に注目すると結構むずいですがポインタ修飾記号に注目すると幸せに慣れます。
*を「〜へのポインタ」と考えればconst [型]* や [型] const*はconst [型]へのポインタ、[型] constへのポインタとなり、[型]* const は[型]へのポインタでconstとなります。
この考えで行けばポインタへのポインタで一部が const のときも容易👼に考えることができます。(なお見た目)
const修飾以外のvolatile修飾などが絡んできても同様。
メンバ関数へのポインタ
メンバ関数ポインタ
何かと便利な関数ポインタ、staticなメンバ関数だと普通に入れることができますが、非staticなものだとエラーを起こします。
これは非staticなメンバ関数はオブジェクトの持つメンバ変数を利用する可能性があるからです。
なのでメンバ関数へのポインタを扱いたい場合は、メンバ関数ポインタと呼ばれる特殊な関数ポインタを用いる必要があります。
#include <iostream> using namespace std; class Number { private: int mA; public: Number(const int a) : mA(a) {} int getA() const { return mA; } }; int main() { Number a(01); int (Number::*f)(void) const = &Number::getA; cout << (a.*f)() << endl; return 0; }
こうなるんですが、つらつらのつら。
もっとどうにかならんのか。
functionクラス
メンバ関数ポインタを気軽に使えるようにしてくれるのがfunctionクラス。
#include <iostream> #include <functional> using namespace std; class Number { private: int mA; public: Number(const int a) : mA(a) {} int getA() const { return mA; } }; int main() { Number a(01); function<int()> f = bind(&Number::getA, &a); cout << f() << endl; return 0; }
普通の関数ポインタにみたいに使えるやったぜ。
ちなみに本物の普通の関数ポインタに対してもfunctionは使える。
ラムダ式でキャプチャ
ラムダ式のキャプチャ指定を&にすることで他の変数を参照してラムダ式内で利用することできる。
これを利用すればさらに楽になる。
#include <iostream> using namespace std; class Number { private: int mA; public: Number(const int a) : mA(a) {} int getA() const { return mA; } }; int main() { Number a(01); auto f = [&]() { return a.getA(); }; cout << f() << endl; return 0; }
神、ゴッドマキシマムゲーマーレベルビリオンまである。
テンプレート
vectorは中身の型を指定できますが、あれのことです。
クラスや関数をテンプレートにすることで型だけ違うような同一処理を行うクラス簡単に錬成できます。
テンプレートに関することだけで本が作れるレベルの沼分野なので超入門レベルだけ。(そもそも深淵部分はまだ私自身が勉強してない。)
おじょうさんのテンプレート講座を待ちましょう。(結構深いところまでやっていく予定らしいです。)
関数テンプレート
関数をテンプレート化することで引数の違うほぼ同じことをしている関数を作らなくてすむ。
たとえばこんなかんじのint型のadd関数があったとして
int add(const int a, const int b) { return a + b; }
これをテンプレート化するとこうなる。
template<class T> T add(const T a, const T b) { return a + b; }
使うときはadd<[型]>([値], [値])。(引数で判別できる場合は<[型]>は省略可能。)
メンバ関数をテンプレート化したやつはメンバ関数テンプレートと呼ばれるけどまあ一緒。
クラステンプレート
vectorをはじめとするSTLはこれ。
こんなかんじのint型の2次元ベクトル構造体があったとして
struct Vec2 { int x, y; Vec2(const int x_, const int y_) : x(x_), y(y_) {} Vec2() = default; };
これをテンプレート化するとこう。
template<class T> struct Vec2 { T x, y; Vec2(const T x_, const T y_) : x(x_), y(y_) {} Vec2() = default; };
使うときはVec2<[型]>。
例では構造体だったけどクラスでも全く同じ。
エイリアステンプレート
実はもう一つある。
エイリアステンプレートはテンプレートに対して別名をつけることができるやつで、テンプレート引数の一部を指定させた状態にすることもできる。
こんなかんじ
template<class T> using vec = std::vector<T>; template<class T, class U> using rmap = std::multimap<T, U, std::greater<T>>;
これでVec<[型]>でvectorが使えるようになって、rmap<[型1], [型2]>で降順ソートのmapが使えるようになります。
同じく型の別名付けに用いられるtypedefでは同じようなことはできません。
ところでmultimap使うの久々すぎて覚えてなかったんですが、multimapって添字演算子使えないんですね・・・。
注意点
今回はtemplate
テンプレートはファイル分けをするときに実装をヘッダーファイルに記述にしなくてはなりません。
というのも、テンプレートクラスのソースファイルのコンパイル時には実際にどの型として使われるのかわからないからです。
そのため実際に使うそのテンプレートを使用するファイルをコンパイルするときに指定型のそのクラスを作ってもらう必要があり、そこで実装部分が必要となります。
どの型は確実に使うのかを指定することにより指定済みの型バージョンについては実装を分離させたり、他の型のときとは違う処理をさせることもできますがそこらへんはおじょうさんの講座を待ちましょう。
演算子オーバーロード
2次元ベクトルを扱う構造体やクラスを考えるとき、必要そうなのは演算です。
add関数などを作って引数にとった別オブジェクト(とは限らないけど)の成分を加えるようにすればできますが、もっと足し算っぽく書きたい。
こういうのができたら最高最善最大王。
Vec2 a(8, 1), b(11, 2); Vec2 c = a + b; std::cout << c.x << " " << c.y << std::endl;
出力
19 3
そういうときに演算子オーバーロード。演算子に対する振る舞いを定義できます。
二項演算子
A + B、A / B、A & B、のような形の演算子をオーバーロードするときはこうなる。(3分クッキング並)
#include <iostream> using namespace std; struct Vec2 { int x, y; Vec2(int x_, int y_) : x(x_), y(y_) {} Vec2() = default; Vec2 operator +(const Vec2& obj) const { Vec2 ret; ret.x = this->x + obj.x; ret.y = this->y + obj.y; return ret; } }; int main() { Vec2 a(8, 1), b(11, 2); Vec2 c = a + b; cout << c.x << " " << c.y << endl; return 0; }
だいたい普通の関数と一緒で、[戻り値の型] operator [演算子] ([引数(演算子の右側にくるオブジェクト)]) となる。
演算子前後には空白あってもなくてもおっけー。
引数や戻り値の方は被演算オブジェクトと同じじゃなくても大丈夫なので、今回の2次元ベクトルの例だとスカラー倍させる *演算子や内積を返す *演算子などを定義することもできる。
ただし、オーバーロードなので引数の型は同じで返り値の型は違うようにはできない。(つまり今回の例だと必要になりそうな、ベクトルの内積と外積のどちらか片方は諦めて普通の関数を使わなくてはならない。)
添字演算子
xとyに対して配列的なアクセスをしたい!というときは添字演算子のオーバーロード。
添字演算子というのは配列のときの []。
以下3分クッキング。
#include <iostream> #include <stdexcept> using namespace std; struct Vec2 { int x, y; Vec2(const int x_, const int y_) : x(x_), y(y_) {} Vec2() = default; int& operator [](const int n) { if(n == 0) { return x; } else if(n == 1) { return y; } else { throw out_of_range("over index"); } } }; int main() { Vec2 a(7, 53); a[0] = 10; cout << a[0] << " " << a[1] << endl; return 0; }
出力
10 53
添字は入力に注意。
単項演算子
ここまでは引数といえるものがあるやつでしたが、明らかにそういうのがない演算子があります。
++と--です。しかもこいつ、変数の前後で処理が微妙に違う。
じゃあどうするのかというと・・・ちょっと頭悪いかんじでした。
#include <iostream> using namespace std; struct Vec2 { int x, y; Vec2(const int x_, const int y_) : x(x_), y(y_) {} Vec2() = default; // 後置インクリメント Vec2 operator ++(int n) { Vec2 ret = *this; this->x++; this->y++; return ret; } // 前置インクリメント Vec2 operator ++() { ++this->x; ++this->y; return *this; } }; int main() { Vec2 a(0, 0), b(0, 0); Vec2 c = a++; Vec2 d = ++b; cout << "a: " << a.x << " " << a.y << endl; cout << "b: " << b.x << " " << b.y << endl; cout << "c: " << c.x << " " << c.y << endl; cout << "d: " << d.x << " " << d.y << endl; return 0; }
出力
a: 1 1 b: 1 1 c: 0 0 d: 1 1
ゴリ押しだった・・・。
使うことはないですがこのnには0が入るらしいです。
仮想継承
C++は複数のクラスを基底クラスとする多重継承が可能、やったぜ。
でもこれはとんでもない災厄を招くことが・・・。
上のような継承関係になってるときがまずい。(ダイヤモンド継承とかひし形継承とか呼ばれる。)
BによってAの一部の関数がオーバーライドされて、CによってAの他の関数がオーバーライドされて、BとCをDに継承させることで両方ともオーバーライドされた状態が完成!。
ってしたいのはわかるが、この場合AはBとCで共通ではなく、Bによって継承されたAと、Cによって継承されたAが混在した状態になる。
上のような図になっているということ。
Aが重複してて厄介だしメモリも無駄、これを解消するのが仮想継承。
BとCによるAの継承を通常継承ではなく、仮想継承にすることでAの実態生成を保留にできる。
その後、BとCをDに通常継承させることで唯一のAが爆誕。
コードにするとこう。
#include <iostream> using namespace std; class A { private: int mA; public: A(const int a) : mA(a) {} virtual void hoge() const = 0; virtual void huga() const = 0; int getA() const { return mA; } }; class B : virtual public A { public: // 仮想継承する場合は基底クラスのコンストラクタは呼び出さない B() {} void hoge() const override { cout << "ほげええええええ" << endl; } }; class C : virtual public A { public: // 仮想継承する場合は基底クラスのコンストラクタは呼び出さない C() {} void huga() const override { cout << "ふがああああああ" << endl; } }; class D : public B, public C { public: // 仮想継承したやつを通常継承するときにAのコンストラクタを呼び出す D(): A(555) {} void hoge() const override { B::hoge(); } void huga() const override { C::huga(); } }; int main() { D d; cout << d.getA() << endl; d.hoge(); d.huga(); return 0; }
出力
555 ほげええええええ ふがああああああ
virtual [アクセス修飾子] で仮想継承にできる。
出力結果じゃわかりにくいけどブレイクポイントとか使うとちゃんとAが一つしかないことがわかる。
これで厄介な問題は解決した・・・したけどこれはあくまで苦肉の策でダイヤモンド継承自体がそもそもカス。
殆どの場合は分割継承(?)される関数を別クラス化して解決できるはずなのでそのほうが圧倒的にいい。
おまけ
終わりに
猫になりたい。
没作品に黙とうを
この記事について
この記事はCCS †裏† Advent Calendar 2019 - Adventarの2日目の記事です。
前日記事:Hello, Hello, Best Friends!
後日記事:ccs裏アドカレ3日目 - Neko_NotNamedの日記
はじめに
創作物というのはこういうの作りたいな~と考えているうちは究極に楽しいですが実際に作るのは修羅の道。
考えただけで実際には作るに至らなかった作品や技術が足りなくて途中で挫折した、単にモチベが消え去って飽きたことによって没になった作品は残念ながら多々存在すると思います。
そこで私自身の没作品の一部をまとめてみました。
神頼みも捨てたもんじゃない
概要
だいたい中学2年から高校2年くらいまで考えてたやつ。
何度も考え直して、実は大学入学当初は在学中に作り上げたいと思ってたけど難点がありすぎてやめたターン制RPG。
今思うとほんまカス。
文章的なタイトルは中学の時にはまってた銀魂の影響。
他にもBLEACHやハンターハンターの影響を多大に受けている。
大雑把なストーリー
人々は葦原と呼ばれる世界の中でモンスターに悩まされながら暮らしていましたが、ある日葦原の外側にも世界が続いていることが判明しました。
その新たに発見された外世界の探索に国連は乗り出し、なんやかんやで旅をして(ここらへんは考えるの後回しにしたまま考えずじまいだった)国連のお偉いさんと仲良くなった主人公も探索メンバー入りが決まりました。
ですが外世界の探索中に魔王軍を名乗るやつらがせめてきて・・・。
世界観
前述のとおり主人公の世界(通称:葦原)の外側に世界が広がっているかんじなんですが、実は外世界とは別に裏世界も存在していて、魔王軍はそこにいるという設定でした。
図にするとこんなかんじ。(葦原などが四角になっているが、実際に巨大な台地になっている)
モンスターはもともとは裏世界の生物という設定でした。
たまに時空のゆがみによって表裏移動されることがあるけど強いやつほど移動確率が低いという設定で、そのため裏世界の技術がまだそこまででない序盤はあまり強い敵は出てこず、裏世界のモンスターは表側より圧倒的に強い。
これだけでも裏世界はともかく、外世界いらなくね?ってかんじですがまだまだ無駄に複雑な設定がありました。
表世界は47の国が存在していて割と少し前まで2つの陣営に分かれて対立していました。
その2つが東京中心の国際連合(通称:東側陣営)と京都中心の国際連盟(通称:西側陣営)です。
とりあえず両陣営が和解したことで2つの組織が合併して「国連」となりましたが、組織内部で旧東と旧西がいまだににらみ合っている・・・という設定でした。
アホ
そもそも国が47というのが多すぎる。(47秘宝と呼ばれる国ごとに存在する国宝を集めるサブイベントなんてのも考えてました、アホ)
魔王軍の組織図もすごいことになっててこんなかんじ。
実は第一構想時は各師団にも下位組織が存在してたけどさすがに省略。
さすがにすべての師団やそのボスを主人公に倒させるつもりはなかったけどアホ。
以上が特に無駄に複雑だった設定。
ここからはだいぶマシになる、はず・・・。
この世界には魔法は大きく分けて2種類存在していて、それが新式魔法と旧式魔法。
新式魔法は主人公たちも使う魔法、というよりこの世界で使われる魔法のほとんどがこれ。
新式っていっても大昔から新式って呼ばれてる。(新約聖書みたいなかんじ)
人間の力しか使ってないので比較的安全。
対して旧式魔法は精霊から力を借りて使う魔法。
術者への負担が大きく危険。
そのため新式魔法が開発されてからはあまり使われなくなり、主人公たちの時代では使える人間はほとんどいない。
また旧式魔法の中に細かい分類として禁忌魔法というのがあり、精霊ではなく神から力を借りる魔法。
術者への負担がすさまじく旧式魔法の中でも初期に衰退し、主人公の時代では実在すら疑われている。
これを使って退場するキャラを出したかった。(遠い目)
システム
敵を倒したときに敵の残りMPに応じて貯蓄魔力が高まり、使用時に貯蓄魔力に応じた効果値を発揮する魔法瓶というのを考えてた。
ただ言葉遊びしたかっただけ。
アイテムをレンタルさせてくれるお店、帝釈店なんてのもかんがえたけどこれは流石に存在意義が怪しすぎて秒で没になった。
これも言葉遊びがしたかっただけ。
没原因
当時の私自身の技術というのもあるが、無駄に壮大で無駄に複雑すぎる設定が9999999.99999999%悪い。
うんち、ごみ、かす。
One week adventure(仮称)
概要
大学1年の夏コミで出そうとしたやつ。
タイトルは正直忘れたけどこんな感じだったと思う。
余談だけど台本の公正を抹茶にしてもらったらキャラぶれすぎと酷評だった。
この記事に関しても口調ブレまくりだから怒られる。(;´д`ノ)
大雑把なストーリー
勇者は魔王を倒すことに成功した。
しかし魔王は最後の力を振り絞り世界と勇者を道ずれにしようとした。
勇者は倒れ世界の終焉まであと7日。
一介の兵士長にすぎない主人公が立ち上がる。
世界観
大雑把なストーリーの項でほぼ言い尽くした。
勇者も魔王も倒れて両陣営とも2軍、3軍しか残っていない状態。
システム
とてもシンプルなターン制RPGなので特になし。
没原因
単純に間に合わなかった。
敵やアイテム、果てはマップデータを手打ちはアホ。
こうならないようにまずエディタを作りましょう。
Action Rogue(大祭版:ハン活!)
概要
今年の大祭で「ハンター活動!」として出したアクションとローグライクを合わせようとしたゲーム。
没になってないじゃんって思うかもしれないけど、本当は大祭後もアップデートしていくつもりだったけど凍結した。
ストーリー
大祭版であるハンター活動!にはストーリーはなかったし、その後のアップデートでどうするかも特に考えてなかった。
システム
無駄に当たり判定がシビア。
そして最大の特徴が、毎フレーム判定が行われているということ。
これのせいでバランス調整がハイパールナティックだった。
没原因
起動時にたまにフリーズする(赤城では100%らしい)謎の超致命的バグの解消法がわからなくて挫折。
クラス構成がどんどん気に入らなくなってきた、仕様的にバランス調整が激むず、OpenSiv3d・Visual Studio2019に乗り換えたいというのもあって凍結。
ターン制だけど新たにローグライクを作り始めているので繰り返さないようにしたいところ。
その他
てきとうクエスト
てきとう農場
てきとう道中
金と権力との戦い
概要
全部中学1、2年に考えてたり作ってたやつ
没原因
モチベ維持ができなかったことと圧倒的な技術不足。
てきとう道中とか作り始めることにすら至らず考えてたことすら忘れてた。(高校の時にノートが見つかって思い出した。)
最後に
忘れてる奴もめっちゃありそう。特に中学時代。
完成すらできなかった没作品に今一度黙とうを。
vectorを頑張って作ろうとする
当記事はCCS Advent Calendar 2018の22日目の記事です
昨日(21日目)の記事↓
[まだ公開されてないようです]
C++には便利な関数やクラスが備わっています。
その一つにvectorがあります
今回はそいつを実装していくぞい!
※間違ったことを言ってる可能性があります
※実装したコードの解説はかなり少なめで、使った技術の解説()が主となっています
※当サークルのC言語講座、C++講座レベルの知識があることが前提の記事となっています
※実は当初はvector、tuple、functionの3本立ての予定でした
vector
そもそもvectorってなんじゃい!
って方のために軽く紹介とテストをしていこうかと。
vectorとは
newで確保してdeleteで開放する普通の動的配列はdeleteし忘れるとメモリが大変なことになってしまうし、途中で要素を追加するときとか結構面倒。
億劫すぎてお空ちゃんになるまである。
そこでvector!
vectorは脳死で要素の追加や削除をさせてくれる上に、自分が消滅するときに開放も行ってくれます。
まさに悪魔の力。
代表的なメンバ関数
- push_back(T obj)
配列末尾に引数にしたやつを要素として追加する。
- size(void)
配列のサイズを返す。
指定要素の削除。(開放されるのかは・・・しらないです・・・)
- clear(void)
要素全削除(開放はされない)。
指定したところに新要素追加
他にも色々便利なのがある上に、引数の違う同名関数も存在しますが今回実装していくのはここらへんに絞ろうと思います。
使ってみよう!
実際に使ってみましょう。
用意したサンプルコードがこちら
#include <iostream> #include <vector> using namespace std; class Vec2{ private: int x, y; public: Vec2(int _x, int _y): x(_x), y(_y){} void show(){ cout << "(x, y) = " << "(" << x << ", " << y << ")" << endl; } }; int main(){ vector<int> num; for(int i = 0; i < 5; i++){ num.push_back(i); } cout << "Add value to num" << endl; for(int i : num){ cout << i << " "; } cout << endl << endl; cout << "Add value to chara" << endl; vector<char> chara = {'C', 'h', 'r', 'n', 'o'}; for(char c : chara){ cout << c; } cout << endl << endl; cout << "Clear num" << endl; chara.clear(); for(int i = 0; i < (int)chara.size(); i++){ cout << chara[i]; } cout << endl << endl; cout << "Add value to Vec2" << endl; vector<Vec2> vec2 = {{3, 3}, {0, 4}}; for(int i = 0; i < (int)vec2.size(); i++){ vec2[i].show(); } cout << endl; return 0; } }
出力はこうなりました。
Add value to num 0 1 2 3 4 Add value to chara Chrno Clear num Add value to Vec2 (x, y) = (3, 3) (x, y) = (0, 4)
push_backでの要素の追加、sizeによる要素数取得、clearによる全要素削除なされてますね
また例のように、通常の配列のように、(obj) = {a, b, c, d, ...}の形式で初期化することもできます
例ではクラスだけですが、構造体はもちろん、共用体(Tupleの章で紹介)、列挙体(当記事ではあつかってないです.自分で調べてください(投げやり).当アドベントカレンダーでも扱っている記事があります.)でも大丈夫ですqiita.com
Cによる実装
方針
Cだと構造体に関数を入れることができないので、面倒だけどvectorのポインタを引数に取ってどうこうするしかなさそう
また、デストラクタも作成できないため自動開放機能をもたせることは困難・・・
free関数を作成してそれを呼び出すように心がけなくてはいけません
(実は次の項目で載せたプログラムはC言語では関数オーバーロードができないことを知らずに書いてました.衝突を避けるためにget_Intなどとしたほうが良さそうです)
そしてこうなった
コード
// Vector.h #pragma once typedef struct{ int *array; int size; }Vector_Int; int get(Vector_Int *vector, int index); void push_back(Vector_Int *vector, int value); void erase(Vector_Int *vector, int erase); void clear(Vector_Int *vector); int size(Vector_Int *vector); void vec_free(Vector_Int *vector);
// Vector.cpp #include "Vector.h" #include <stdlib.h> #include<stdio.h> int get(Vector_Int *vector, int index){ if(index < vector->size){ return vector->array[index]; } return -1; } void push_back(Vector_Int *vector, int value){ int *new_array = (int*)malloc(sizeof(int) * (vector->size + 1)); for(int i = 0; i < vector->size; i++){ new_array[i] = vector->array[i]; } new_array[vector->size] = value; if(vector->size != 0){ free(vector->array); } vector->array = new_array; vector->size++; } void erase(Vector_Int *vector, int index){ for(int i = index; i < vector->size - 1; i++){ vector->array[i] = vector->array[i + 1]; } vector->size--; if(vector->size == 0){ free(vector->array); } } void clear(Vector_Int *vector){ free(vector->array); vector->size = 0; } int size(Vector_Int *vector){ return vector->size; } void vec_free(Vector_Int *vector){ free(vector->array); vector->size = 0; }
// main.c #include "Vector.h" #include <stdio.h> int main(){ Vector_Int vec; int num[5] = {1, 0, 6, 5, 3}; // 末尾にどんどん追加していく for(int i = 0; i < 5; i++){ push_back(&vec, num[i]); } // 追加したことを確認するために出力 for(int i = 0; i < size(&vec); i++){ printf("vec[%d] = %d\n", i, get(&vec, i)); } printf("size = %d\n\n", size(&vec)); // 2番目のやつ、つまり6を削除 erase(&vec, 2); // 削除できたか確認 for(int i = 0; i < size(&vec); i++){ printf("vec[%d] = %d\n", i, get(&vec, i)); } printf("size = %d\n\n", size(&vec)); // 全部削除 clear(&vec); // 削除できたか確認 for(int i = 0; i < size(&vec); i++){ printf("vec[%d] = %d\n", i, get(&vec, i)); } printf("size = %d\n\n", size(&vec)); vec_free(&vec); return 0; }
出力
vec[0] = 1 vec[1] = 0 vec[2] = 6 vec[3] = 5 vec[4] = 3 size = 5 vec[0] = 1 vec[1] = 0 vec[2] = 5 vec[3] = 3 size = 4 size = 0
とりあえずそれっぽく動いてはいる・・・が・・・
問題点
- 今回は律儀にget関数やsize関数を使って中身の配列にアクセスしていたが、vec.array[0]、vec.sizeのように直接アクセスすることができてしまう
- 引数に毎度アドレスを入れるのが面倒
- 自動開放してくれない
- 配列として使ってるんだからvec[0]で配列0番目にアクセスできるようにしたい
- 他の型のやつを別に実装しなくてはならない
C++のクラスを使えばいくつかは解消できます
クラスを使った実装
方針
C版の関数をvectorクラスのメンバ関数にしていき、vec_freeはデストラクタにすることで自動開放を実現
クラスにしていくだけなので解説はほとんどないです・・・(Cも全然解説してなかったけど)
こうなった
コード
// Vector.h #pragma once class Vector{ protected: int arysize; int maxsize; public: Vector(void): arysize(0), maxsize(0) {} virtual void erase(int index) = 0; void clear(void); int size(void); };
// Vector.cpp #include "Vector.h" void Vector::clear(){ arysize = 0; } int Vector::size(){ return arysize; }
// Vector_Int.h #pragma once #include "Vector.h" #include <stdlib.h> class Vector_Int : public Vector{ protected: int *array; public: Vector_Int(void): Vector(), array(NULL) {} ~Vector_Int(void){ if(array != NULL){ delete array; } } int get(int index); void push_back(int value); void erase(int index) override; void insert(int index, int value); };
// Vector_Int.cpp #include "Vector_Int.h" int Vector_Int::get(int index){ return array[index]; } void Vector_Int::push_back(int value){ if(arysize == maxsize){ int *temp = new int[maxsize + 5]; for(int i = 0; i < arysize; i++){ temp[i] = array[i]; } delete array; array = temp; maxsize += 5; } array[arysize] = value; arysize++; } void Vector_Int::erase(int index){ for(int i = index; i < arysize - 1; i++){ array[i] = array[i + 1]; } arysize--; } void Vector_Int::insert(int index, int value){ if(arysize == maxsize){ int *temp = new int[maxsize + 5]; for(int i = 0; i < index - 1; i++){ temp[i] = array[i]; } delete array; array = temp; maxsize += 5; } for(int i = arysize; i > index; i--){ array[i] = array[i - 1]; } array[index] = value; arysize++; }
// main.cpp #include "Vector_Int.h" #include <iostream> void show(Vector_Int& vec){ for(int i = 0; i < vec.size(); i++){ std::cout << "vec[" << i << "] = " << vec.get(i) << std::endl; } std::cout << "size = " << vec.size() << std::endl << std::endl; } int main(){ Vector_Int vec; // 7, 8, 3をとりあえず入れてみて確認 int num[3] = {7, 8, 4}; for(int i : num){ vec.push_back(i); } show(vec); // 1番目、つまり8を削除して確認 vec.erase(1); show(vec); // 1番目に9を挿入、つまり7, 9, 3になるはずなので確認 vec.insert(1, 9); show(vec); // 全要素削除して確認 vec.clear(); show(vec); return 0; }
出力
vec[0] = 7 vec[1] = 8 vec[2] = 4 size = 3 vec[0] = 7 vec[1] = 4 size = 2 vec[0] = 7 vec[1] = 9 vec[2] = 4 size = 3 size = 0
いいかんじ
なんか実際のvectorはメモリ確保するときに多めに確保するらしく、またclearではメモリ解放がなされないようなのでそれに従いました(実際こんなんなのかは知りませんが・・・)
C版と比べたらだいぶ使いやすくなった・・・けどまだまだ・・・
問題点
- 配列として使ってるんだからvec[0]で配列0番目にアクセスできるようにしたい
- 他の型のやつを別に実装しなくてはならない
とりあえず[]によるアクセスをできるようにしていきましょう
演算子オーバーロードを使った実装
演算子オーバーロードとは
C++では引数型違いの同名関数を作成、使用可能ですが演算子もオーバーロードすることができ、これによって今までできなかった自作クラスに対して*や+を使用することができるようになります
- 四則演算のオーバーロード
四則演算は次のように記述することでオーバーロードできます
/返り値の型/ operator/演算子/ (/演算対象(足し算で言うと足す数みたいな)の型と仮引数名/){ //処理を記述 return /返り値/ }
例えば、xとyの二次元座標を扱うVec2型の構造体に対して、+演算子により単純にxとyをそれぞれ足し合わせたものを返すようにする場合はこのようになります
struct Vec2{ int x, y; Vec2(int _x, int _y): x(_x), y(_y){} Vec2 operator+ (Vec2& obj){ Vec2 ret_obj(this->x + obj.x, this->y + obj.y); return ret_obj; } void show(){ std::cout << "(" << x << ", " << y << ")" << std::endl; } };
テストするために下のようなmain関数を書きました
int main(){ Vec2 a(2, 0), b(3, 3); Vec2 c = a + b; c.show(); }
出力結果
(5, 3)
ちゃんと足せてますね
楽勝すぎる、完全に理解した
😇「と思っていたのかぁ?」
こんな構造体を用意しました
struct test{ int a; test(int x):a(x){} int operator+ (test& x){ return 0; } };
これに対してmainをこのようにします
int main(){ test a(0); const test b(0); std::cout << a + a << std::endl; return 0; };
すると出力は
0
普通やんってかんじですが、a + aをa + bやb + bにするとコンパイルエラーになります
それもそのはず、引数がtest&では途中で書き換えられてしまう可能性があります
そのような処理をconst修飾子のついた変数を渡すわけにはいかないのです
ということで、このようにすればa + bを計算することができるようになります
#include <iostream> struct test{ int a; test(int x):a(x){} int operator+ (test& x){ return 0; } int operator+ (const test& x){ return 1; } }; int main(){ test a(0); const test b(0); std::cout << a + b << " " << a + a << std::endl; return 0; };
出力結果は
1 0
ちゃんとできるようになりました
ちなみにこれ、test&の方を消しても大丈夫です(出力は1 1)
constが非const&引数として渡されると改ざんの危険がありますが、非constがconst&引数として渡されてもなんの危険もありませんからね(ただし、非const&のほうがより適してるので両方存在する場合はそっち)
では足される側がconstの場合はどうしたらいいのかというと・・・
関数をconst化します
なんじゃそりゃってかんじですが
void func() const{ }
のようにすると、その関数内でメンバ変数の変更、非constメンバ関数の仕様が禁止されます
これを利用することでconst + constやconst +非constに対応することができるようになります
例えばこんなかんじにすると
#include <iostream> struct test{ int a; test(int x):a(x){} int operator+ (test& x){ return 0; } int operator+ (const test& x){ return 1; } int operator+ (test& x) const{ return 2; } int operator+ (const test& x) const{ return 3; } }; int main(){ test a(0); const test b(0); std::cout << a + b << " " << a + a << " " << b + a << " " << b + b << std::endl; return 0; };
出力は
1 0 2 3
いいかんじですね
先程同様、(test&)を削除しても大丈夫です
・・・とはなりませんでした
a + aは両方非constなため(const test&)、(test&)constとの一致度が同じになってしまい、衝突してしまいます
どっちかを消せば一応解決です
どの組み合わせでも処理を変える気ないなくて面倒なら(const&)constのみ、念のために・・・という場合は全部定義しておくのが良さそうです
本当はもっと定義しておいたほうが良さそうなのあるかもしれませんが・・・同型同士に関しては今回はここまでで・・・
あ、今回は実験のために返り値を定数にしましたが、+にかかわらず処理内容はその演算子っぽいものにしましょう
同型同士といいましたが、他の型に対しても定義することができます
例
#include <iostream> struct Vec2{ int x, y; Vec2(int _x, int _y): x(_x), y(_y){} Vec2 operator+(int& num){ Vec2 ret_obj(this->x + num, this->y + num); return ret_obj; } void show(){ std::cout << "(" << x << ", " << y << ")" << std::endl; } }; int main(){ Vec2 a(2, 0); int b = 3; Vec2 c = a + b; c.show(); }
出力
(5, 3)
できました
こちらでもconstの問題はありますが同型同士と同じようなかんじなので割愛
- 以外の四則演算子や%、=もほぼ同じです(+が*や%になる)
また、今回は二次元座標を表す構造体を作りました
二次元座標に対する同型同士の掛け算の処理は内積と外積の2種類がありますが、関数オーバーロード同様に返り値の型違いでオーバーロードできないので注意してください
諦めてどちらかをメンバ関数にしましょう・・・
- 添字演算子
まず添字演算子とはなんぞい、というと配列の[]これです
これをオーバーロードすることで配列感マシマシ
とりあえず、添字演算子はこんなかんじで記述する
(返り値の型) operator[] (int n){ // 処理を記述 return (返り値) }
引数のnはarray[5]でいう5です
まぁ、使用例を見ていきましょう
#include <iostream> struct Vec2{ int x, y; Vec2(int _x = 0, int _y = 0): x(_x), y(_y){} void show(){ std::cout << "(" << x << ", " << y << ")" << std::endl; } }; class Array_Vec2{ private: Vec2 *array; public: Array_Vec2(int num_elm): array(new Vec2[num_elm]()){} Vec2& operator[] (int n){ return array[n]; } }; int main(){ const int ARRAY_SIZE = 3; Vec2 a(2, 0); Array_Vec2 vecary(ARRAY_SIZE); vecary[1] = a; for(int i = 0; i < ARRAY_SIZE; i++){ vecary[i].show(); } return 0; }
出力
(0, 0) (2, 0) (0, 0)
ちゃんと配列のようになってますね
ただ注意が必要なのは、operator[]の返り値の方をVec2&からVec2にすると出力は
(0, 0) (0, 0) (0, 0)
となってしまいます
&がなくなったことにより参照渡し(array[n]そのものを渡す)から値渡し(array[n]が持っていた値を渡す)(←ここらへんの説明は強い人が見たらいやいや・・・と言われてしまう可能性ありそうですが、まあそんな認識でとりあえず大丈夫なはず)になってしまうためです
array[1]とはもはやなんのつながりもないものに対して代入をしているので当然array[1]にはなんの影響もありません
これでどうすればvectorを配列っぽく使えるか見えてきましたね
ちなみに、Java版vectorのArrayListは[]が定義されておらずget関数でわざわざ要素を呼び出すという面倒な仕様になったますが、あれはJavaは演算子オーバーロードをサポートしてないからです(Stringはなんかできますが・・・)
結局vectorはどうなったの
vector.hだけの記載にしますが、こうなりました
// Vector_Int.h #pragma once #include "Vector.h" #include <stdlib.h> class Vector_Int : public Vector{ protected: int *array; public: Vector_Int(void): Vector(), array(NULL) {} ~Vector_Int(void){ if(array != NULL){ delete array; } } void push_back(int value); void erase(int index) override; void insert(int index, int value); int& operator[](int n){ return array[n]; } };
例のとほぼ変わらないです
特に解説することも・・・なさそう・・・
問題点
- 他の型のやつを別に実装しなくてはならない
この問題を解決するためにクラステンプレートとかいう闇技術を使用します
クラステンプレートを用いた実装
方針
クラステンプレート化して他のchar型など組み込み式の他の方やユーザー定義の型にも対応させる
クラステンプレートとは
まず、クラステンプレートはテンプレートの一種です
ほとんど同じだけど型が違う処理を、仮の型名を定めて一つ書いただけでいろいろな型に対応できるようにしてくれるのがテンプレートです
最も単純なものとしてテンプレート関数があります
まあ下手すぎる説明より例を見たほうがいいでしょう
どんな型でも2乗してその型のまま返してくれるテンプレート版square関数を作ってみました
コード
#include <iostream> using namespace std; template <typename T> T square(T x){ return x * x; } int main(){ int a = 9; double b = 1.1; char c = 5; a = square(a); b = square(b); c = square(c); cout << a << " " << b << " " << (int)c << endl; }
出力
81 1.21 25
char型はcoutで出力すると対応する文字が出力されてしまうのでintでキャストしましたが、ちゃんとどれも2乗されています
templateで次の関数がテンプレート関数であることを宣言、
Tはその関数内なら返り値の型、引数の型、途中で宣言する変数の型など、なんにでも使えます
また、先程の例では省略しましたがテンプレート関数を使うときに<"型">と記述することでそのテンプレート関数をどの型のものとして使うかを明示できます
つまり、先程のmain関数をこのようにすると・・・
#include <iostream> using namespace std; template <typename T> T square(T x){ return x * x; } int main(){ int a = 9; double b = 1.1; char c = 5; a = square<int>(a); b = square<int>(b); c = square<char>(c); cout << a << " " << b << " " << (int)c << endl; }
こうなります
81 1 25 <|| double型であるbをint版squareに渡すことで、引数bが(int)1.1、つまり1として扱われ、1 * 1で1がbに代入されたというわけです 何個もオーバーロードする必要もなく、どの型を使うかの明示もしやすくてとても便利 しかしこれは実際はあらゆる型に対応した関数ではなく、対応した型バージョンの関数を作ってくれるものなので、使った型の分だけコンパイル時間や実行ファイルサイズが増大してしまいます これは他の種類のテンプレートでも同じです また、正直これは私はあんま理解できてないんですが、テンプレートは一つのコンパイル単位に実装をまとめる必要があります つまりヘッダーファイルに実装を書くというなんとも奇妙なことをしなくてはならんわけです 今回使うクラステンプレートもだいたい同じようなかんじですが、例を見たほうがわかりやすいでしょう >|cpp| #include <iostream> using namespace std; template <typename T> class test{ private: int a; T b; public: test(int _a, T _b): a(_a), b(_b){} int get_a(){ return a; } T get_b(){ return b; } }; int main(){ test<char> tes(1053, '!'); cout << tes.get_a() << " " << tes.get_b() << endl; return 0; }
出力
1053 !
aとbの2つのメンバ変数を持つテンプレートクラスtestを作成しました
aはint型ですが、bの方は自由に指定可能です
テンプレート関数は使用時に「(関数名)<(どの型にするか)>」という書き方をしましたが、クラステンプレートは「(クラス名)<(どの型にするか)>」となります
ただし、テンプレート関数とは違い、<型>の省略は通常ではできません
が、テンプレートクラスは、関数でデフォルト引数を設定できるように、デフォルトクラスを設定でき、設定した場合、型を省略して<>にするとTはデフォルトクラスの型として扱われます(<>は省略できないです)
例えば先程のtestテンプレートクラスのtemplate
このように
#include <iostream> using namespace std; template <typename T = double> class test{ private: int a; T b; public: test(int _a, T _b): a(_a), b(_b){} int get_a(){ return a; } T get_b(){ return b; } }; int main(){ test<> tes(1053, 0.00094967); cout << tes.get_a() << " " << tes.get_b() << endl; return 0; }
出力
1053 0.00094967
ちなみに、今回は使いませんが、複数のテンプレート引数を指定することもできます
例えばこんなかんじ
template <typename T, typename U> class test{ private: int a; T b; U c; public: test(int _a, T _b, U _c): a(_a), b(_b), c(_c){} int get_a(){ return a; } T get_b(){ return b; } U get_c(){ return c; } };
Vectorをテンプレート化できそうですね
そして出来上がったのがこれ
// Vector.h #pragma once #include <iostream> template<typename T> class Vector{ protected: T *array; int arysize; int maxsize; public: Vector(void): array(NULL), arysize(0), maxsize(0) {} ~Vector(void){ if(array != NULL){ delete array; } } void push_back(T obj){ if(arysize == maxsize){ T *temp = new T[maxsize + 5]; for(int i = 0; i < arysize; i++){ temp[i] = array[i]; } maxsize += 5; delete array; array = temp; } array[arysize] = obj; arysize++; } void insert(int index, T obj){ if(arysize == maxsize){ T *temp = new T[maxsize + 5]; for(int i = 0; i < arysize; i++){ temp[i] = array[i]; } maxsize += 5; delete array; array = temp; } for(int i = arysize; i > index; i--){ array[i] = array[i - 1]; } array[index] = obj; arysize++; } void erase(int index){ for(int i = index; i < arysize - 1; i++){ array[i] = array[i + 1]; } arysize--; } void clear(void){ arysize = 0; } int size(void){ return arysize; } T& operator[](int n){ return array[n]; } };
// main.cpp #include "Vector.h" int main(){ int num[] = {3, 3, 4}; Vector<int> vec_int; for(int i = 0; i < 3; i++){ vec_int.push_back(num[i]); } for(int i = 0; i < vec_int.size(); i++){ std::cout << vec_int[i] << " "; } std::cout << std::endl << std::endl; const char *str[] = { "KamenRide ", "ZI-O"}; Vector<const char*> vec_charp; for(int i = 0; i < 2; i++){ vec_charp.push_back(str[i]); } for(int i = 0; i < vec_charp.size(); i++){ std::cout << vec_charp[i]; } std::cout << std::endl << std::endl; vec_charp[1] = "DECADE"; for(int i = 0; i < vec_charp.size(); i++){ std::cout << vec_charp[i]; } std::cout << std::endl << std::endl; vec_charp.insert(0, "Final"); for(int i = 0; i < vec_charp.size(); i++){ std::cout << vec_charp[i]; } std::cout << std::endl << std::endl; return 0; }
出力
3 3 4 KamenRide ZI-O KamenRide DECADE FinalKamenRide DECADE
うまくいきましたね
ジオウにカメンライドした後にディケイドにカメンライドするのセンスないけど許して・・・
平ジェネ観にいきてぇ・・・
おしまい
これでこの記事はおしまい
本当はもう少しクラステンプレート掘り下げたかったのと、Cでもできるけど本サークル2年生以下はあまり知らなそうで、クラステンプレートよかゲーム制作に役立ちそうそうな可変長引数の関数とか紹介したかったんですが、時間やばいのともともと低い説明力がどんどん低下していってるのを感じてきてるのでやめます・・・逆にわからなくなりそう・・・
クッキー生産マニュアル
アドベントカレンダー5日目です!
この記事では何年か前に流行った(らしい)狂気のクッキー生産ゲームCookie Clickerを紹介していきます。
Cookie Clicker #とは
フランス人のOrteil氏によって開発されたフリーのブラウザゲーム。
2013年8月8日に、氏の友人のサイト「DashNet」にて公開、同24日にデザインとシステムを一新したのちブームとなった。
「ババアがクッキーを焼くゲーム」とも呼ばれている。 遊び方は、画面左に表示されているクッキーをクリックし、ひたすらクッキーを生産していく。ただそれだけ。
だが、プレイしてみると意外に奥が深く中毒性があり、無意味だと分かっていてもクッキーを大量生産するのが快感になってしまいなかなかやめられなくなってしまう、ある意味恐ろしいゲームである。 (ピクシブ百科事典より)
遊び方
ブラウザ上で動くゲームなのでDLする必要はなく、公式サイトを開くだけでプレイできます。
Cookie Clickerと検索してみよう!多分一番上に出てくるのがそれ。
億劫だという怠け者さんはこれをクリック。
ん?おっくう?お空ちゃんハァハァ
ではサイトに入ったら何をすればいいのか?
画面左側のデカいクッキーをクリックしまくる!
これによってクッキーが生産される。
ね、簡単でしょう?
クッキーを一定数貯めると施設が買えるようになる。
施設を購入すると毎秒自動的にクッキーが生産されるようになる。
複数買うとその分、毎秒生産されるクッキーの数、cookies per secon(CPS)が増加するがまたその施設を買うのに必要なクッキーの数も増加してしまう。
クリックと施設でクッキーを増やして上位の施設を購入していこう!
また貯めたクッキーは施設のほかにアップグレードに使うこともできる。
アップグレードで1クリック当たりのクッキー生産数、cookies per click(CPS)を増加させたり、施設の生産性強化など様々な特殊効果を得られるようになる!
ところで、プレイ中画面上に金色のクッキーが出現することがある。
これをクリックすると一定時間CPS増加、一定数クッキー入手などの様々な恩恵が得られる。制限時間は短いがCPCが777倍という非常に強力な効果を得らることもある。
見つけたらすぐさまクリックしよう!
中級編
クッキーを生産していくと実績が解放されることがあります。
最初は実績を集めても意味がなくただの自己満なのですが、アップグレードで猫を手に入れることでCPSを劇的に増加させることができます。
運よく新しい猫を手に入れられるだけのクッキーが集まったので購入してみました。
なんとCPSが10Gから18G程まで上がりました。
猫はほんま有能・・・。
あと中級者向け要素としてババアポカリプスがあります。
・・・が、進度の関係でスクショが用意できなかったのでこちらを参考にしてください。(は?)
上級編
Storeの横のLegacyで強くてニューゲームをすることができます。
クッキーを焼いた枚数によってprestage level(名声レベル)が上がり、名声レベルに比例して次回のクッキー生産時のCPS倍率が増加します。
また、上昇した名声レベルと同じ枚数のヘブンリークッキーを手に入れることができ、ヘブンリークッキーを使って特殊なアップグレードを施すことができます。
(通常のアップグレードはLegacy時にリセットされてしまいますが、ここで手に入れたアップグレードはずっと残ります。)
使わなかったヘブンリークッキーは次回に持ち越されます。
1回以上Legacyした状態で始めると砂糖という概念が追加されます。
砂糖は時間経過によって勝手にたまっていきます。
一応ゴールデンクッキークリック時に超低確率で入手できることがありますが・・・デレステの限定SSRよりもひどい確率なので・・・。
この砂糖を消費することでクリックや施設の効果倍率を向上させることができます。
終わり!
ほかにもドラゴンや季節イベントなどの要素がありますが・・・そこは自分で調べて・・・。(クソオブクソ)
それではクッキー生産に勤しんでください。