最近の休日のルーティン:
- MusescoreのソースコードをChatGPT(MEMちょ似 25歳女性ソフトウェアエンジニアの設定)と二人三脚で追う
- その日の学びを復習できるコーディング問題を彼女に出題してもらい、私が解いてフィードバックを受ける
一度解いても忘れるので自分用にまとめ。
注:下記の内容はChatGPTとの壁打ちをまとめているので、誤った情報が紛れている可能性がある。
第1問 インターフェース、仮想関数、コンストラクタ、デストラクタ (2/27)
問題:動物 (Animal) のクラス構造を作り、次の仕様を満たすプログラムを作成
条件
IAnimalインターフェースを作るmakeSound()(鳴き声を出す関数)を 純粋仮想関数 (= 0;) にする- 仮想デストラクタ (
virtual ~IAnimal()) を持たせる
Dog,Cat,Birdの3つのクラスをIAnimalを継承して作るmakeSound()をオーバーライドして、それぞれの鳴き声を出す ("Woof!","Meow!","Chirp!")
std::vector<std::shared_ptr<IAnimal>>を使って動物リストを作るDog,Cat,Birdをstd::make_shared<>で作成してリストに追加する
- リストの全ての動物について
makeSound()を呼び出し、それぞれの鳴き声を出す
解答の形
IAnimal のインターフェースを作り、Dog, Cat, Bird のクラスを作成し、それらをリストで管理する!
#include <iostream>
#include <vector>
#include <memory>
// ここに `IAnimal` の定義を書く
// ここに `Dog`, `Cat`, `Bird` の定義を書く
int main() {
std::vector<std::shared_ptr<IAnimal>> animals;
// ここに `Dog`, `Cat`, `Bird` をリストに追加するコードを書く
// 全ての動物について `makeSound()` を呼ぶ
for (const auto& animal : animals) {
animal->makeSound();
}
return 0;
}
この問題を解けば、今日の学びをフル活用できるよ!
最終解答
#include <iostream>
#include <vector>
#include <memory>
// ここに `IAnimal` の定義を書く
class IAnimal {
public:
virtual void makeSound() = 0;
// IAnimal() {std::cout << "IAnimalコンストラクタ" << std::endl; }
// virtual ~IAnimal() {std::cout << "IAnimalデストラクタ" << std::endl; };
};
// ここに `Dog`, `Cat`, `Bird` の定義を書く
class Dog : public IAnimal{
public:
// Dog() {std::cout << "Dogコンストラクタ" << std::endl; }
// ~Dog() {std::cout << "Dogデストラクタ" << std::endl; }
void makeSound() override { std::cout << "Woof!" << std::endl; }
};
class Cat : public IAnimal{
public:
// Cat() {std::cout << "Catコンストラクタ" << std::endl; }
// ~Cat() {std::cout << "Catデストラクタ" << std::endl; }
void makeSound() override { std::cout << "Meow!" << std::endl; }
};
class Bird : public IAnimal{
public:
// Bird() {std::cout << "Birdコンストラクタ" << std::endl; }
// ~Bird() {std::cout << "Birdデストラクタ" << std::endl; }
void makeSound() override { std::cout << "Chirp!" << std::endl; }
};
int main() {
std::vector<std::shared_ptr<IAnimal>> animals;
std::cout << &animals << std::endl;
// ここに `Dog`, `Cat`, `Bird` をリストに追加するコードを書く
animals.push_back(std::make_shared<Dog>());
animals.push_back(std::make_shared<Cat>());
animals.push_back(std::make_shared<Bird>());
std::cout << &animals << std::endl;
// 全ての動物について `makeSound()` を呼ぶ
for (const auto& animal : animals) {
std::cout << "animal at:" << animal << std::endl;
animal->makeSound();
}
return 0;
}最終解答(2周目)
#include <iostream>
#include <vector>
#include <memory>
// ここに `IAnimal` の定義を書く
class IAnimal{
public:
virtual void makeSound() = 0;
virtual ~IAnimal() = default;
};
// ここに `Dog`, `Cat`, `Bird` の定義を書く
class Dog : public IAnimal{
public:
void makeSound() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public IAnimal{
public:
void makeSound() override {
std::cout << "Meow!" << std::endl;
}
};
class Bird : public IAnimal{
public:
void makeSound() override {
std::cout << "Chirp!" << std::endl;
}
};
int main() {
std::vector<std::shared_ptr<IAnimal>> animals;
// ここに `Dog`, `Cat`, `Bird` をリストに追加するコードを書く
animals.push_back(std::make_shared<Dog>());
animals.push_back(std::make_shared<Cat>());
animals.push_back(std::make_shared<Bird>());
// 全ての動物について `makeSound()` を呼ぶ
for (const auto& animal : animals) {
animal->makeSound();
}
return 0;
}ふりかえり
override, constの明示をするとなおよい;のつけ忘れ、publicをPublicと書き間違えるpublicをかかないとデフォルトprivate→main()からインスタンスを作れない- デストラクタの
{ ...(処理)... }と=default;は同時に書けない- いま(この記事執筆時)見返すと「それはそう」なのだが、これがはじめて書いたC++プログラムだったので…。今後もしばらくそういう初歩的なミスが続く。
ふりかえり (2周目、4/6)
std::shared_ptr<Dog> dog = std::make_shared<Dog>();
animals.push_back(dog);
の二段構えにするとよいのは次のとき- あとで同じインスタンスを使い回したい
- デバッグでみたい、ブレークポイントを貼りたい
- 処理を挟みたい
std::make_shared<Cat>;はエラーmake_sharedというテンプレート関数をCat型用に用意した、の意味になる()までないと何も起きないしエラーになる
animals.push_back({std::make_shared<Dog>(), std::make_shared<Cat>(), std::make_shared<Bird>() });はダメなのか?animalsはstd::shared_ptr<IAnimal>型の値を待っているのに、3つ入りのリストを渡してしまっているからエラー- まとめて追加するなら、
- 宣言と一緒に、
vector<T> v = {a, b, c}; - あるいは、
v.insert(v.end(), {a, b, c};
- 宣言と一緒に、
第2問 インターフェース、ポリモーフィズム、シングルトン (2/28)
問題:図形(Shape)を扱うインターフェースとシングルトンのマネージャを実装
問題内容
IShapeインターフェースを作る- 純粋仮想関数
draw()を定義(図形を描画するメソッド。コンソールにメッセージを表示するだけでOK) - 純粋仮想関数
area()を定義(図形の面積を返す。double型)
- 純粋仮想関数
- 以下の図形クラスを
IShapeを継承して作るCircle- 半径 (
radius) を持つ draw()で"Drawing Circle (半径は...) "と出力area()はπ * 半径^2
- 半径 (
Rectangle- 幅 (
width), 高さ (height) を持つ draw()で"Drawing Rectangle (幅x高さ...)"と出力area()はwidth * height
- 幅 (
Triangle- 底辺 (
base), 高さ (height) を持つ draw()で"Drawing Triangle (底辺x高さ...)"と出力area()は(base * height) / 2
- 底辺 (
ShapeManagerクラス(シングルトン)を作る- 以下のメンバを持つ
std::vector<std::shared_ptr<IShape>> shapes;- ここで図形を管理する
static ShapeManager& getInstance()- シングルトン実装:ここで唯一の
ShapeManagerインスタンスを取得
- シングルトン実装:ここで唯一の
void addShape(const std::shared_ptr<IShape>& shape)- 受け取った図形を
shapesベクタに追加
- 受け取った図形を
void printAllShapes()shapesの各図形に対してdraw()を呼んで描画メッセージを出力area()を呼んで面積を出力
- 以下のメンバを持つ
main()関数で下記を実装せよCircle,Rectangle,Triangleをいくつか作成し、ShapeManager::getInstance().addShape(...)で登録ShapeManager::getInstance().printAllShapes()を呼んで、登録された図形をすべて出力
要点
- インターフェース(抽象クラス)
class IShape { public: virtual void draw() = 0; virtual double area() = 0; virtual ~IShape() = default; };- ポリモーフィズムを活用して、
draw()とarea()を統一的に扱う
- 継承先クラスの実装
Circle,Rectangle,Triangleがそれぞれコンストラクタで必要な値(半径・幅高・底辺高)を保持draw()とarea()をoverrideキーワード付きで実装- コンストラクタ・デストラクタでログを出すと挙動がわかりやすいかも(任意)
- シングルトンの
ShapeManagerstatic ShapeManager& getInstance() { static ShapeManager instance; return instance; }addShape()とprintAllShapes()を備えるprintAllShapes()内でループを回して各図形のdraw()とarea()を呼ぶ
main()でのテストstd::make_shared<Circle>(5)などで図形を生成ShapeManager::getInstance().addShape(...)で登録ShapeManager::getInstance().printAllShapes()で結果を出力
追加アイデア(発展的な要素)
ShapeManagerのデストラクタにログを入れてみる → 「シングルトンはいつ解放されるか?」がわかる- 面積計算時に
M_PI(<cmath>で定義)を使う usingやtypedefを活用してコードをシンプルに- エラーチェック(
Circleの半径が負の場合はどうするか?など) Triangle以外にもSquareやPolygonを追加してみる
ポイント
- 「インターフェース(純粋仮想クラス)で共通機能を定義し、ポリモーフィズムで扱う」
- 「シングルトン (
ShapeManager) でオブジェクト管理」 - 「
std::vector<std::shared_ptr<IShape>>を使ってメモリ管理を自動化」
これで前回の学び(インターフェース、ポリモーフィズム、シングルトン、メモリ管理など)を総合的に活かせると思うよ!
ぜひトライしてみてね。うまくいかなかったら「エラーがこう出た!」って質問してくれれば一緒に解決しよう!
最終解答
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class IShape{
public:
virtual void draw() = 0;
virtual double area() = 0; //constだめ?
virtual ~IShape() = default;
};
class Circle : public IShape{
public:
double radius;
Circle(double r) : radius(r) { std::cout << "Circleコンストラクタ" << std::endl; }
//~Circle() { std::cout << "Circleデストラクタ" << std::endl; }
void draw() override { std::cout << "Drawing Circle (半径は...)" << std::endl; }
double area() override {
return M_PI * radius * radius;
}
};
class Rectangle : public IShape{
public:
double width;
double height;
Rectangle(double w, double h) : width(w), height(h) { std::cout << "Rectangleコンストラクタ" << std::endl; }
//~Rectangle() { std::cout << "Rectangleデストラクタ" << std::endl; }
void draw() override { std::cout << "Drawing Rectangle (幅x高さ...)" << std::endl; }
double area() override {
return width * height;
}
};
class Triangle : public IShape{
public:
double base;
double height;
Triangle(double b, double h) : base(b), height(h) { std::cout << "Triangleコンストラクタ" << std::endl; }
//~Triangle() { std::cout << "Triangleデストラクタ" << std::endl; }
void draw() override { std::cout << "Drawing Triangle (底辺x高さ...)" << std::endl; }
double area() override{
return base * height / 2;
}
};
class ShapeManager{
public:
static ShapeManager& getInstance(){
static ShapeManager instance;
return instance;
}
void addShape(const std::shared_ptr<IShape>& shape){
shapes.push_back(shape);
}
void printAllShapes(){
for(const auto& shape : shapes){
shape->draw();
std::cout << shape->area() << std::endl;
std::cout << shape.get() << std::endl;
}
}
private:
ShapeManager() {}
std::vector<std::shared_ptr<IShape>> shapes;
};
int main()
{
std::shared_ptr<IShape> s1 = std::make_shared<Circle>(5);
std::shared_ptr<IShape> s2 = std::make_shared<Rectangle>(-3,9);
std::shared_ptr<IShape> s3 = std::make_shared<Triangle>(4,6);
ShapeManager::getInstance().addShape(s1);
ShapeManager::getInstance().addShape(s2);
ShapeManager::getInstance().addShape(s3);
ShapeManager::getInstance().printAllShapes();
std::cout << "&s1:" << &s1 << ",&s2:" << &s2 << ",&s3:" << &s3 << std::endl;
return 0;
}最終解答(2周目)
#include <iostream>
#include <cmath>
#include <vector>
#include <memory>
class IShape{
public:
virtual void draw() = 0;
virtual double area() = 0;
virtual ~IShape() = default;
};
class Circle : public IShape{
public:
Circle(double r) : radius(r) {}
double radius;
void draw() override{
std::cout << "Drawing Circle(r)..." << radius << std::endl;
}
double area() override{
return radius * radius * M_PI;
}
};
class Rectangle : public IShape{
public:
Rectangle(double w, double h) : width(w), height(h) {}
double width;
double height;
void draw() override{
std::cout << "Drawing Rectangle (w x h)..." << width << " x " << height << std::endl;
}
double area() override{
return width * height;
}
};
class Triangle : public IShape{
public:
Triangle(double b, double h) : base(b), height(h) {}
double base;
double height;
void draw() override{
std::cout << "Drawing Triangle (b x h)..." << base << " x " << height << std::endl;
}
double area() override{
return base * height / 2;
}
};
class ShapeManager{
public:
std::vector<std::shared_ptr<IShape>> shapes; //ptrのvectorじゃないとダメなの?
static ShapeManager& getInstance(){ //これはコンストラクタの一種ではない?
static ShapeManager instance;
return instance;
}
void addShape(const std::shared_ptr<IShape>& shape){
shapes.push_back(shape);
}
void printAllShapes(){
for(auto shape : shapes){
shape->draw();
std::cout << "area: " << shape->area() << std::endl;
}
}
};
int main()
{
std::shared_ptr<IShape> s1 = std::make_shared<Circle>(5);
std::shared_ptr<IShape> s2 = std::make_shared<Rectangle>(10,5);
std::shared_ptr<IShape> s3 = std::make_shared<Triangle>(3,8);
ShapeManager::getInstance().addShape(s1);
ShapeManager::getInstance().addShape(s2);
ShapeManager::getInstance().addShape(s3);
ShapeManager::getInstance().printAllShapes();
/* これではダメ? →ポリモーフィズムを使えない
Circle c1(10);
Rectangle r1(20,10);
Triangle t1(10,5);
*/
return 0;
}ふりかえり
関数 override { ...(処理)... }の形で書く関数 override = (処理);ではない
- シングルトンの特徴
- コンストラクタを
privateにする staticで一つだけのインスタンスを作る
- コンストラクタを
std::cout << (void型関数) << std::endl;はエラーになる。void型のなかで出力させる- 行64:
shapes->draw()はミスshapesはstd::vector<...>,shapeがstd::shared_ptr<IShape>- いま扱っている変数が一要素なのか、ベクターなのか
- ポインタの整理
shape.get()…それぞれのIshapeオブジェクトのアドレス&shape…std::shared_ptr<IShape>(スマートポインタ)自体のアドレス
- スマートポインタについて
std::make_shared<>でnewを使わず、shared_ptr<>を作れる- スマートポインタから値を取り出したいとき、
(*shape).area()の形もとれるが非推奨
- C++のコードを読み始め、学習を始めて3日目くらいでこの演習はキツい
ふりかえり (2周目、4/6)
- 抽象クラス
IShapeはインスタンスを作れないから、std::vector<IShape>も無理
class Circle : public IShape{…}のpublicの意味:継承のアクセス指定子public: 親クラスのpublicメンバを、子クラスでもpublicとして扱えるprivate: 親クラスのpublicメンバが、子クラスではprivateになる
- ポリモーフィズム(多態性)を使うには、ポインタor参照必須
- ポリモーフィズム:同じ型で異なる振る舞いを実行できる仕組み
- スライシング:子クラスの情報が、親クラスの変数にコピーされたときに消える現象
IShape s = Circle(5); // エラー、オブジェクトのスライシング
IShape* s = new Circle(5); // OK
型& 変数名 = 値の場合、参照する(エイリアスを作っている)&は参照型の意味- そのため、
int& x = 4;など右辺が定数の場合はエラーになる。 - ただし、
const int& x = 4;はOK(テンポラリ参照というらしい)
Circle c;
IShape& shape = c; //shape, c はどちらも同じオブジェクトを見ている
- カプセル化:オブジェクトの内部データや実装を隠して、安全に使わせる設計
- アクセスの窓口(getter/setter,操作メソッド)だけをpublicにする
- クラスの外から呼び出す操作(API)→public
- 内部のデータ構造やロジック→private
- 外部にも渡して良い読み取り専用の情報→public
- API: Application Programming Interface プログラム同士のやりとりを決める入口
- 自分でクラスを作ったら、そのpublicな部分がAPIになるイメージ
staticのイメージ:インスタンスに紐付かず、プログラム全体で一つだけ存在するもの- static変数(関数内)、staticメンバ関数、staticメンバ変数
- staticメンバ関数の使い道:
- シングルトン インスタンス1つだけほしい
- ユーティリティ関数 日付変換など状態がいらない処理を集約できる
- ファクトリメソッド 初期状態が決まったインスタンスを返す
- クラスはオブジェクトの表現だけでなくて、カテゴリ化して機能をグルーピングするためにも使える
- シングルトンと同じ実装を、他の書き方で実現できないのか?
- できなくはないがベストではない。
- シングルトンの目的:①プログラム内でインスタンスがただ1つ、②どこからもアクセス可能にしたい。
- 代案1 グローバル変数で持つ:どこからも書き換え可能、テストしにくい、初期化順の保証がないデメリット。C++では、グローバル変数の初期化順はファイルを跨ぐと未定義となる。初期化の順番によってはクラッシュの原因になる。
- 代案2 static変数を名前空間で囲む(他のファイルから見えなくする):クラスにカプセル化されない、自然な書き方にならない、保守性低下につながるデメリット。
第3問 ディスパッチャー (3/2)
問題:「ディスパッチャーにアクションを登録して実行する流れ」を小さく再現
じゃあ今日の復習を兼ねて、簡易版の「Dispatcher + ActionController」システムを作るコーディング問題 を出すね。今回学んだ「ディスパッチャーにアクション(=コールバック)を登録して実行する流れ」を小さく再現してみよう!
仕様
ActionCodeという enum を作り、いくつかのアクションを定義するDispatcherクラス を作り、アクションコード と ハンドラー(関数ポインタ or ラムダ) を保存しておくregisterAction()メソッドを持つActionControllerを作り、そこからDispatcherにアクションを登録するisEnabled(アクションが有効かどうかを返す関数)も登録し、アクション実行前にチェック してみて- メイン関数 から「アクション登録 → アクション実行」の流れを実装し、有効・無効で動きが変わる ことを確認する
要件詳細
enum class ActionCode- 例:
PrintHello,PrintWorld,Exitなど、何かしら3つくらい定義
- 例:
Dispatcherクラス- メンバ
std::unordered_map<ActionCode, std::function<void()>> handlers;(アクションと処理の紐づけ)std::unordered_map<ActionCode, std::function<bool()>> enablers;(アクションが有効かどうかを判定する関数)
- メソッド
void reg(ActionCode, std::function<void()>, std::function<bool()>)(アクションを登録)void dispatch(ActionCode)(登録されたアクションを呼ぶ。呼ぶ前に「enabler」がtrueかチェック)
- メンバ
ActionControllerクラス- メンバ
Dispatcher* dispatcher;(外部から渡してもいいし、内部で保持してもOK)
- メソッド
registerAction(ActionCode code, std::function<void()> handler, std::function<bool()> enabler)- → 中で
dispatcher->reg(code, handler, enabler);を呼ぶだけでもOK
- → 中で
- アクションを呼び出すメソッド(なくてもメインで
dispatcher->dispatch(...)してもいい)
- メンバ
- メイン関数
- 流れ(例)
ActionCodeのいくつかの値に対し、ハンドラー と 有効/無効判定関数 を登録- ユーザーにアクションコードを入力させる or 順番に
dispatcher->dispatch(...)を呼ぶ - アクション実行前に enabler が
falseの場合は「無効なアクションです」と表示 trueならハンドラーを実行 → 出力が確認できればOK
- 流れ(例)
ヒント
- ラムダ式 でハンドラーとエネーブラ(
isEnabled)を作るとスッキリ - たとえば
std::function<void()> handler = []() { std::cout << "Hello!\n"; };std::function<bool()> enabler = []() { return true; }; std::bindを使ってメンバ関数ポインタを紐づけるのもアリ- アクションコード → ハンドラー / エネーブラ を登録し、
dispatchで「どのアクションを呼びたいか?」を指定 → 実際に関数が動く流れを作ればOK
提出イメージ
- ヘッダファイルでも .cpp でもどっちでもOK
- 最低限ビルド可能な形にして、メインでアクションを登録 → dispatch → 出力テストを見せてくれたら嬉しい!
今回のポイントは 「Dispatcher を使って、アクションを登録し、アクションコードを指定して実行する流れを作る」 こと。
実際の notationactioncontroller.cpp に近い設計を めちゃ小さく再現 するイメージだよ。
もし時間があれば、「メンバ変数を切り替えるアクション」 を入れたり、「enabler が false になる条件」 を作ったりすると、より理解が深まるよ!
がんばれー!
最終解答
#include <iostream>
#include <unordered_map>
#include <functional>
enum class ActionCode {
PrintMem,
PrintRuby,
PrintKana
};
class Dispatcher {
private:
std::unordered_map<ActionCode, std::function<void()>> handlers;
std::unordered_map<ActionCode, std::function<bool()>> enablers;
public:
Dispatcher(){
// ハンドラー
handlers[ActionCode::PrintMem] = []() {
std::cout << "やあ!MEMちょだよ!" << std::endl;
};
handlers[ActionCode::PrintRuby] = []() {
std::cout << "みんなおまたせ! ルビーだよ" << std::endl;
};
handlers[ActionCode::PrintKana] = []() {
std::cout << "待たせたわね。かなよ" << std::endl;
};
// エネーブラー
enablers[ActionCode::PrintMem] = []() { return true; };
enablers[ActionCode::PrintRuby] = []() { return true; };
enablers[ActionCode::PrintKana] = []() { return false; };
}
// register
void reg(ActionCode code, std::function<void()> handler, std::function<bool()> enabler) {
handlers[code] = handler;
enablers[code] = enabler;
}
// Dispatch
void dispatch(ActionCode code){
if(enablers[code] && !enablers[code]()){
std::cout << "無効" << static_cast<int>(code) << std::endl;
return;
}
if(handlers.find(code) != handlers.end()){
handlers[code]();
} else {
std::cout << "未登録" << std::endl;
}
}
};
class ActionController{
Dispatcher* dispatcher;
void registerAction(ActionCode code, std::function<void()> handler, std::function<bool()> enabler){
dispatcher->reg(code, handler, enabler);
}
};
int main()
{
Dispatcher dispatcher;
dispatcher.dispatch(ActionCode::PrintMem);
dispatcher.dispatch(ActionCode::PrintRuby);
dispatcher.dispatch(ActionCode::PrintKana);
return 0;
}最終解答(2周目)
#include <iostream>
#include <unordered_map>
#include <functional>
enum class ActionCode{
midiPlay,
audioPlay,
putNote
};
class Dispatcher{
public:
std::unordered_map<ActionCode, std::function<void()>> handlers;
std::unordered_map<ActionCode, std::function<bool()>> enablers;
//複数の処理・実行可否をひとつのdispatcherにまとめる
void reg(ActionCode code, std::function<void()> handler, std::function<bool()> enabler){
handlers[code] = handler;
enablers[code] = enabler;
}
//ハンドラーの実行
void dispatch(ActionCode code){
if (handlers.find(code) == handlers.end()){
std::cout << "Action未登録: " << static_cast<int>(code) << std::endl;
return;
}
if (enablers.find(code) != enablers.end() && !enablers[code]()){
std::cout << "Actionは現在無効です: " << static_cast<int>(code) << std::endl;
return;
}
handlers[code]();
}
};
class ActionController{
public:
Dispatcher* dispatcher;
ActionController(Dispatcher* disp) : dispatcher(disp){}
void registerAction(ActionCode code, std::function<void()> handler, std::function<bool()> enabler){
dispatcher->reg(code, handler, enabler);
}
void registerAll(){
registerAction(ActionCode::midiPlay,
[](){ std::cout << "MIDI再生" << std::endl; },
[](){ return true; }
);
registerAction(ActionCode::audioPlay,
[](){ std::cout << "オーディオ再生" << std::endl; },
[](){ return false; }
);
registerAction(ActionCode::putNote,
[](){ std::cout << "音符を入力" << std::endl; },
[](){ return true; }
);
}
};
int main()
{
Dispatcher dispatcher;
ActionController controller(&dispatcher);
controller.registerAll();
dispatcher.dispatch(ActionCode::midiPlay);
dispatcher.dispatch(ActionCode::audioPlay);
dispatcher.dispatch(ActionCode::putNote);
return 0;
}ふりかえり
- エラーを出した原因
enum classは列挙型→メンバ関数を持たせることはできない- メンバ関数を持たせるなら、通常のクラスか構造体にする
- クラスのメンバ変数の初期化をクラス内部で直接行わない→コンストラクタ内で初期化
- 他にもたくさんミスを出しているが、根本的に概念の整理ができていないからだと思う
enablerでは、アクション実行の条件を決めておき、現在の状態に応じてtrue/falseを返す- 条件の例:「選択中の要素がある」「演奏中でない」「テキスト編集中でない」
- 行46:
enablers[code] && !enablers[code]()enablers[code]が存在する(nullptrでない) かつenablers[code]の戻り値がfalse
- いまの実装だと、
Actioncontrollerはいらないが、将来的な拡張を考えると必要らしい- よくわからん
- Actioncodeの実装は上の例だとふさわしくないかも。「演奏中」などにしたほうがよい?
find(), end()auto it = container.find(key):keyに対応する要素を探してそのイテレータを返す- キーが存在しなければ、
end()を返す
- STLコンテナ
- C++のSTL(Standard Template Library)に含まれるデータ構造
- 順序コンテナ:順番を保持する
vector,list,deque,array - 連想コンテナ:キーと値をペアで管理する
map,unordered_map,set,unordered_set - アダプタコンテナ:他のコンテナを内部で使い、特定の機能を提供
stack,queue,priority_queue
std::function- 関数ポインタをラップするクラス。関数を変数のように扱ったり、引数として渡せる
std::function<void()> func = [](){ ... };:ラムダ式を格納できる
std::bind- 関数の一部の引数を固定して、新しい関数オブジェクトを作る
std::bind(関数ポインタ, 引数1, 引数2, …, std::placeholders::_1, …)
ふりかえり (2周目、4/5)
- ディスパッチャー:複数の処理と実行可否をまとめて管理する
- メンバ関数の役割
reg:ActionCode、処理内容、判定可否の組み合わせを登録dispatch:入力されたActionCodeの実行
unordered_mapで、存在しないキーを参照すると新たに作られるhandlers[code]で、もしcodeがなければ新たに要素が作られる- →存在チェック (falseが返ってくることを期待)には使えない
handlers.find(code) == handlers.end(); // → 存在チェック
handlers[code]; // → 存在しなければ 新しい要素が作られる!
dispatchの実装if elseで実行ダメな場合と実行可能な場合を振り分け続ける
→ 関数全体をifで覆わないといけない、ネストが深くなる、条件漏れが怖い- はじめに実行ダメな場合を if で拾って早期 return、最後に本処理をネストなしで書くとわかりやすい
- コンストラクタの復習
- インスタンスが作られるときに実行されるメソッド。変数の初期化にも使える。
ActionController(Dispatcher* disp) : dispatcher(disp) {}
このクラスが持つメンバ変数dispatcherに、コンストラクタ引数のdispを代入するという初期化をしている。- この定義を外すと、インスタンス生成で引数として渡しても、クラス変数としてその引数を「持った」とはいえないので、ポインタなら未定義動作につながる。
Dispatcher* dispatcher;の必要性
この行がないと、ActionControllerクラスはメンバ変数としてdispatcherを持たなくなるため、後の処理で毎回dispatcherのポインタを引数として渡さないといけなくなる
第4問 enum(列挙型)、リスト、条件分岐 (3/6)
問題:音符・休符リストの選択範囲を拡張するプログラム
あなたは楽譜編集ソフトを作っています。ChordRest(音符・休符)のリストが与えられたとき、現在選択中の ChordRest を基準に 選択範囲を拡張 する処理を作成してください。
条件
- 音符をリスト(
std::vector<int>)として管理し、それぞれの音を数値で表す- 例:
{ 60, 62, 64, 65, 67 }→ C, D, E, F, G
- 例:
- 現在選択されている音符のインデックスと、拡張方向(
ExpandSelectionMode)を指定 - 拡張した結果、選択範囲の最小値と最大値をコンソール出力する
- モードによって選択範囲を以下のように拡張
BEGIN→ リストの最初の音を選択END→ リストの最後の音を選択NEXT→ 次の音に拡張(最後の音なら変化なし)PREV→ 前の音に拡張(最初の音なら変化なし)
関数のシグネチャ
enum class ExpandSelectionMode {
BEGIN,
END,
NEXT,
PREV
};
void expandSelection(const std::vector<int>& notes, int& start, int& end, ExpandSelectionMode mode);
入出力の例 (一部省略)
入力
std::vector<int> notes = { 60, 62, 64, 65, 67 }; // C, D, E, F, G
int start = 1, end = 2; // 最初に選択されている範囲 (D, E)
expandSelection(notes, start, end, ExpandSelectionMode::BEGIN);
出力
選択範囲: C - E
挑戦ポイント
- 配列の境界(最小・最大)を超えないように処理する
ExpandSelectionModeに応じてstartとendを適切に変更する- 変更後の
notes[start]からnotes[end]の範囲を出力する
今日学んだ「選択範囲の拡張」をコンソール出力で試してみよう!
コードが書けたらここに投稿してくれればレビューするよ!
最終解答
#include <iostream>
#include <vector>
#include <unordered_map>
enum class ExpandSelectionMode {
BEGIN,
END,
NEXT,
PREV
};
std::string getNoteName(int note){
const std::unordered_map<int, std::string> noteMap = {
{60, "C"}, {61, "C#"}, {62, "D"}, {63, "D#"}, {64, "E"},
{65, "F"}, {66, "F#"}, {67, "G"}, {68, "G#"}, {69, "A"},
{70, "A#"}, {71, "B"}
};
if(noteMap.find(note) != noteMap.end()){
return noteMap.at(note);
}
return "Unknown";
};
void expandSelection(const std::vector<int>& notes, int& start, int& end, ExpandSelectionMode mode) {
// modeに応じてstart-endの範囲を変更
if (mode == ExpandSelectionMode::BEGIN){
start = 0;
} else if (mode == ExpandSelectionMode::END){
end = notes.size() - 1;
} else if (mode == ExpandSelectionMode::NEXT){
if (end != notes.size() - 1){
end++;
}
} else { //PREV
if (start != 0){
start--;
}
}
//start,endの音名を表示
std::cout << "start:" << getNoteName(notes[start]) << std::endl;
std::cout << "end:" << getNoteName(notes[end]) << std::endl;
}
int main()
{
std::vector<int> notes={60, 62, 64, 65, 67, 71, 63};
int start = 3, end = 5;
expandSelection(notes, start, end, ExpandSelectionMode::END);
return 0;
}最終解答(2周目)
#include <iostream>
#include <vector>
#include <unordered_map>
enum class ExpandSelectionMode{
BEGIN,
END,
NEXT,
PREV
};
std::string pitchToNote(int pitch){
static const std::vector<std::string> names = {"C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"};
int octave = (pitch / 12) - 1;
return names[pitch % 12] + std::to_string(octave);
}
void ExpandSelection(const std::vector<int>& notes, int& start, int& end, ExpandSelectionMode mode){
switch(mode){
case ExpandSelectionMode::BEGIN:
start = 0;
break;
case ExpandSelectionMode::END:
end = notes.size() - 1;
break;
case ExpandSelectionMode::NEXT:
end += (end == notes.size() - 1) ? 0 : 1;
break;
case ExpandSelectionMode::PREV:
start -= (start == 0) ? 0 : 1;
break;
}
}
int main()
{
std::vector<int> notes = {60, 62, 66, 69, 73};
int start = 1;
int end = 3;
ExpandSelection(notes, start, end, ExpandSelectionMode::NEXT);
std::cout << "範囲変更:" << pitchToNote(notes[start]) << " - " <<
pitchToNote(notes[end]) << std::endl;
return 0;
}ふりかえり
- 関数シグネチャ:関数の名前、引数、戻り値の型を定義したもの
- 関数を宣言する順番を間違えると、スコープの問題が発生することがある
- プロトタイプ宣言でも対処可能
- C++ では関数のネスト(関数の中で関数定義)はサポートされない
- どうしても必要ならラムダ式
ふりかえり (2周目、4/5)
- 音名と数値の対応づけ:前回より効率的にできた。
std::string names[]よりは、std::vector<std::string>がいまふう- 文字列型にキャスト:
std::to_string(hoge) - 文字列のドッギングは
a+bでOK → “ab”と表示される namesはstatic constがいいかも- 今後の拡張ポイント:♯←→♭の切り替えロジック(調性、前後の文脈から判断させる)
第5問 ポインタ、参照 (3/7)
問題:動的配列を拡張する関数を実装
C++ で 「int**(ポインタのポインタ)と int&(参照)」 を使って、コンソール出力だけで完結するプログラムを作りましょう!
問題の内容
expandArray(int** arr, int& size)を作成せよ。- 引数:
int** arr: 動的に確保したint配列へのポインタ(ポインタのポインタ)。int& size: 現在の配列サイズ(参照渡し)。
- 処理:
- 新たに
size + 2の動的配列を確保 する。 - 古い配列の要素をコピー する。
- 追加された 2 つの要素に任意の値(例: 100, 200)を代入 する。
- 古い配列を解放し、新しい配列を
*arrに設定 する。 sizeをsize + 2に更新 する。- 拡張後の配列をコンソールに出力 する。
- 新たに
- 引数:
main()での流れint* dataを動的に確保し、3個の要素 を用意して好きな値を入れる(例: 10, 20, 30)。- 変数
int size = 3;として、配列サイズを管理。 expandArray(&data, size)を呼び出して配列を拡張!- 拡張後の配列を再度コンソール出力 して、拡張結果を確認。
ヒント
- ポインタのポインタ (
int** arr) を使うことで、関数内でarr自体を新しいメモリに貼り直せる! - 配列サイズは
int& sizeとして参照渡しすることで、関数内で更新した値を呼び出し元に反映できる! - 動的配列は
newとdelete[]を使って確保・解放する。
チャレンジのポイント
- ポインタのポインタ を使って、関数内で配列を再割り当て できるようにする
- 参照 (
int&) で配列サイズを更新し、呼び出し元にも反映 - ちゃんと 古いメモリを解放 (
delete[]) して、新しいメモリを割り当て する - コンソール出力は
expandArray()内とmain()両方 で確認
ポインタ、参照、動的メモリ確保を上手く使って、拡張後の配列を正しく表示できるプログラムを書いてみよう!
解けたらぜひソースコードを貼ってみてください。レビューしますよ!頑張ってね!
最終解答
#include <iostream>
void expandArray(int** arr, int& size){
int* expandarr = new int[size + 2];
std::copy(*arr, *arr+size, expandarr);
expandarr[size] = 100;
expandarr[size+1] = 200;
delete[] *arr;
*arr = expandarr;
size += 2;
std::cout << "new arr:";
for(int i=0; i < size; i++){
std::cout << (*arr)[i] << " ";
}
std::cout << std::endl;
std::cout << "expandarr(int*):" << expandarr << std::endl;
std::cout << "arr(int**):" << arr << std::endl;
std::cout << "*arr(int*):" << *arr << std::endl;
std::cout << "*arr+size(int*):" << *arr+size << std::endl;
}
int main()
{
int* data = new int[3]{10,20,30};
int size = 3;
std::cout << "data:";
for(int i=0; i < size; i++){
std::cout << data[i] << " ";
}
std::cout << std::endl;
expandArray(&data, size);
std::cout << "(new)data:";
for(int i=0; i < size; i++){
std::cout << data[i] << " ";
}
std::cout << std::endl;
return 0;
}最終解答(2周目)
#include <iostream>
void expandArray(int** arr, int& size){
int* newArr = new int[size+2]{};
newArr[0] = (*arr)[0];
newArr[1] = (*arr)[1];
newArr[2] = (*arr)[2];
newArr[3] = 100;
newArr[4] = 200;
delete[] *arr;
*arr = newArr;
size += 2;
std::cout << "updated *arr: ";
for(int i=0; i<size; ++i){
std::cout << (*arr)[i] << " ";
}
std::cout << std::endl;
//delete[] newArr; 二重解放エラーになる
}
int main()
{
int* data = new int[]{3,4,5};
int size = 3;
expandArray(&data, size);
std::cout << "updated data: ";
for(int i=0; i<size; ++i){
std::cout << data[i] << " ";
}
std::cout << std::endl;
delete[] data;
return 0;
}ふりかえり
- 行4:
int* expandarr = new int[size + 2];- この形で配列を宣言すると、expandarrはポインタとなる(newはポインタを返す)
delete[] *arr;すべき場所に注意- 正しくdeleteしないとメモリリークが発生するが、タイミングを間違えると新しいデータを削除してしまう(わかってない)
- C++では変数のメモリ確保の方法に2種類ある。
- スタック領域:関数のスコープ内で宣言された変数が確保される場所。自動的に開放
- ヒープ領域:プログラム実行中に自由にメモリ確保・開放される。new,deleteが必要
int**だけでなくint*&(ポインタ参照)も構文的には可能std::copyとbegin(), end()int*にbegin(),end()は使えない。std::vector<int>なら可能- copyの引数にはアドレスを渡す
- int*型の
dataに対して、(*data)[i]と参照するのは誤り*dataはint型と扱われるので、int[i]という無効な参照になる。data[i]でよい- または、
*(data+i)
arr[0]=10, arr[1]=20, ptr=&arrのとき、**ptr=arr[0]=10, **ptr+1=arr[0]+1=11, *(*ptr+1)=arr[1]=20
ふりかえり (2周目、4/3)
- 二重ポインタ
int** arr, *arr = data = new int[]{3,4,5}に対して、(*arr)[0]と*arr[0]は意味が違う。*arr[1] = *(arr+1)なので、arrという一つしかないint*型の値の次を要求している- →存在しないポインタを参照してしまう
- cf.
(*arr)[1] = data[1] = 4
- ポインタの二重解放エラー
- newして呼び出し元に渡したら解放しなくてよい
delete[] *arrをすると、*arr = dataが指す配列{3,4,5}の領域が開放される。dataのアドレスが消去されたり、配列の中身は書き換えられない。
第6問 ベクター、条件分岐、ループの処理 (3/10)
問題:音符リストのナビゲーションを実装
以下の要件を満たす C++ コンソールプログラムを作成してください。
- 音符 (Note) を表す構造体またはクラス
Noteを用意する。- メンバとして
int pitch(ピッチを表す、例: 60 = 中央ド付近)bool isRest(休符かどうか)
- 休符の場合、
pitchの値は無視してよい。
- メンバとして
- 音符のリスト (std::vector<Note>) を用意し、コンソール入力から値を受け取り初期化する。
- まず N (音符の数) を読み込む。
- その後、N 個の行について
pitchとisRest(0/1 や true/false などの形式は自由) を入力する。
- 移動処理 (ナビゲーション) を実装 する。
- コンソールから以下のコマンドを繰り返し受け付ける:
move next→ 現在位置から「次の休符でない音符」へ移動move prev→ 現在位置から「前の休符でない音符」へ移動quit→ 終了
- 移動先が存在しない場合は、「移動できません」 とメッセージを表示して、現在位置は変えない。
- コンソールから以下のコマンドを繰り返し受け付ける:
- 現在位置 (index) を管理し、
move next/move prevコマンドのたびに更新する。- プログラム開始時は 0番目 (先頭要素) を現在位置 とする。
move next:- 現在位置より後ろの要素で
isRest == falseな音符があれば、そこへ移動 - 見つからなければエラーメッセージを表示し、位置は変えない
- 現在位置より後ろの要素で
move prev:- 現在位置より前の要素で
isRest == falseな音符があれば、そこへ移動 - 見つからなければエラーメッセージを表示し、位置は変えない
- 現在位置より前の要素で
- 各コマンド実行後、現在位置をコンソールに出力 する。
- 例えば「現在位置: 3, pitch=67」といった形でOK (フォーマットは自由)。
- コマンド
quitが入力されたら、プログラムを終了 する。
入出力イメージ (略)
上記はあくまで一例です。入出力の形式は多少変えても構いませんが、
- 音符の情報を N 個読み込む
move next/move prev/quitを繰り返し入力できる- そのたびに結果をコンソールに表示する
という部分を満たしてください。
ポイント
- ベクター (
std::vector<Note>) の使い方 - 休符 (
isRest == true) をスキップ して移動するロジック - 現在位置を越えて移動しないように条件分岐
- 失敗時のメッセージ出力
この問題を解くことで、
- C++ のベクター操作
- ループを使った入力処理
- 条件分岐を使った探索 (次 or 前) の実装
- コンソール出力
などを練習できるはずです。頑張ってください!
最終解答
#include <iostream>
#include <vector>
#include <string>
class Note{
public:
int pitch;
bool isRest;
};
int main()
{
std::cout << "音符の数(N):...";
int n;
std::cin >> n ;
std::vector<Note> newNote;
int p;
bool r;
//音符リスト入力
for(int i=0; i < n; i++){
std::cout << "i=" << i << " , pitch, rest(0/1)...";
std::cin >> p >> r ;
newNote.push_back({p,r});
}
//移動処理
int idx = 0;
std::string str= "";
std::cout << "入力完了。移動:n/p/q...";
std::cin >> str;
while(true){
bool moved = false;
if(str=="n"){
for(int i = idx+1; i < n; i++){
if(!newNote[i].isRest){
idx = i;
moved = true;
std::cout << "移動:idx=" << idx << " ";
break;
}
}
}else if(str=="p"){
for(int i = idx-1; i > -1; i--){
if(!newNote[i].isRest){
idx = i;
moved = true;
std::cout << "移動:idx=" << idx << " ";
break;
}
}
}else if(str=="q"){
break;
}
if(!moved){
std::cout << "移動できません" << std::endl;
}
std::cout << "n/p/q...";
std::cin >> str;
}
std::cout << "終了" << std::endl;
std::cout << "現在位置:" << idx << " , pitch:" << newNote[idx].pitch << std::endl;
//for(const Note& note : newNote){
// std::cout << "Pitch:" << note.pitch << ",Rest:" << (note.isRest ? "Yes" : "No") << std::endl;
//}
return 0;
}最終解答(2周目)
#include <iostream>
#include <vector>
struct Note{
int pitch; //休符=0
bool isRest;
};
int main()
{
int N;
std::cout << "N: ";
std::cin >> N;
std::vector<Note> note;
int p;
bool r;
for(int i=0; i<N; ++i){
std::cout << "i=" << i << ": ";
std::cin >> p >> r;
note.push_back({p,r});
}
//ナビゲーション実装
std::string navi;
int idx = 0;
bool canmove = false;
while(true){
std::cout << "コマンド受付 n/p/q: ";
std::cin >> navi;
if(navi=="n"){
canmove = false;
if(idx < N-1){ //idx=後ろから2番目のときまで
for(int i=idx+1; i<N; ++i){
if(!note[i].isRest){
std::cout << "i=" << i << ", pitch=" << note[i].pitch << std::endl;
canmove = true;
idx = i;
break;
}
}
}
}else if(navi=="p"){
canmove = false;
if(0 < idx){
for(int i=idx-1; -1<i; --i){
if(!note[i].isRest){
std::cout << "i=" << i << ", pitch=" << note[i].pitch << std::endl;
canmove = true;
idx = i;
break;
}
}
}
}else if(navi=="q"){
std::cout << "終了";
break;
}else{
std::cout << "コマンドが不正です。" << std::endl;
}
if(!canmove){
std::cout << "移動できません。" << std::endl;
}
}
return 0;
}ふりかえり
- cinの使い方
cin >> p,rだとエラー。cin >> p >> r
- vectorへの代入:
push_back()- 複数要素をまとめるなら、
push_back({p,r})のようにする
- 複数要素をまとめるなら、
- 行18~28:vector<Note>の宣言で確保した後にさらにpush_backするのは誤り
std::vector<Note> newNote(n);// すでにn個確保newNote.push_back({p,r});//n個枠のあとにさらにデータを追加してしまう
- 行42など:
vector<Note>自体に、.pitchという形でメンバ変数にアクセスできないnewNoteがstd::vector<Note>ならば、newNote[0].pitchという表記でNoteオブジェクトのメンバ変数にアクセスできる
- 行37以降のwhile処理
- 行43,52で
idx++;にしていたがバグの原因になった - movedフラグを立てておくと、移動できない場合の処理を簡潔にかける
- 行43,52で
- 文字列型は、Cスタイルのchar, char配列もあるが、C++では
std::stringが一般的 - 出題の要求仕様はよくある内容で理解も容易いのに、正しく動くコードを書くのが意外と難しい
- どこで条件分岐させるか、while文をどのタイミングでbreakさせるか
- 変数の型の勘違いも多発。ベクターなのかNoteオブジェクトなのか
- 高度なテクニックより、こういった処理を地道にミスなくできるのも実力のひとつ。コードをざっくり読んでいるだけだと気づかないなあ
- 数学の問題で計算ミスをゼロにする努力と似ているかも
ふりかえり (2周目、3/31)
- フラグの使い方:フラグは1回だけ立てる
- 立てたり降ろしたりしていると、デバッグ作業中に混乱する。
- はじめに私が書いていたイケてないコード↓
- 処理を追うと、
canmoveフラグがtrueになったりfalseになったりしていてわかりづらい
- ネストを深くしない工夫をしたい
- 例:2重ループの抜け方
- 内側ループは
break、そこでフラグを立てる - 外側ループはフラグ検知したら
break。あるいは、関数化して抜けたい場所でreturn
//ナビゲーション実装
std::string navi;
int idx = 0;
bool canmove = true; //移動先が存在すると仮定しておく
while(true){
std::cout << "コマンド受付 n/p/q: ";
std::cin >> navi;
if(navi=="n"){
if(idx<N){
for(int i=idx+1; i<N; ++i){
if(note[i].isRest == false){
std::cout << "i=" << i << ", pitch=" << note[i].pitch << std::endl;
canmove = true; //移動先が存在した! true確定
idx = i;
break;
}
canmove = false; //次の移動先は休符→今回は移動可能じゃない! falseに変更
}
}else{
canmove = false; //現在地がそもそも idx = N のとき(←実は誤り、N-1のときの勘違い)後ろの音符がないから false に変更
}第7問 フィルタ、ソート (3/16)
問題:有効/無効を持つ譜面セグメントをフィルタ&ソートして出力
MuseScoreのコードリーディングで学んだ内容を活かしつつ、コンソールへ結果を出力するC++プログラムを作ってください。以下の条件を満たすように実装してください。
前提
- Segment構造体
- 以下のメンバを持つ:
int tick;: そのセグメントの時間(拍)位置を表すbool enabled;: セグメントが有効か無効かを示す
- 以下のメンバを持つ:
- filterAndSortSegments関数
std::vector<Segment>を受け取り、- 「enabled が true のセグメントだけ」 を取り出し、
tickの昇順でソート した結果を返す
- main関数
- 適当な
std::vector<Segment>を用意し、 filterAndSortSegmentsを使って整列・フィルタした結果を受け取り、- 「tick と enabled の値」をコンソールに出力 する
- 適当な
要求仕様
- 「enabled が false」のセグメントは無視(フィルタ)
- 「enabled が true」のセグメントのみ、
tickの小さい順にソート - 結果をコンソールに出力
- 出力形式は任意ですが、各セグメントの
tickとenabledを分かりやすく表示してください
- 出力形式は任意ですが、各セグメントの
- main関数で動作が分かるように、複数のセグメントを用意してテスト
最終解答
#include <iostream>
#include <vector>
struct Segment{
int tick;
bool enabled;
};
void filterAndSortSegments(std::vector<Segment>& seg){
std::vector<Segment> newSeg;
for(const auto& Segment : seg){
if(Segment.enabled == true){
newSeg.push_back(Segment);
}
}
std::sort(newSeg.begin(), newSeg.end(), [](auto const& a, auto const& b){
return b.tick > a.tick;
});
seg = newSeg;
};
int main(){
std::vector<Segment> exampleSeg1 = {
{10,true},
{6,false},
{5,true},
{2,true}
};
std::vector<Segment> exampleSeg2 = {
{3, true},
{9, false},
{4, true},
{20, true},
{14, true}
};
filterAndSortSegments(exampleSeg1);
filterAndSortSegments(exampleSeg2);
std::cout << "exampleSeg1 result" << std::endl;
for(const auto& Segment : exampleSeg1){
std::cout << "tick: " << Segment.tick << " , enabled: " << Segment.enabled << std::endl;
}
std::cout << "exampleSeg2 result" << std::endl;
for(const auto& Segment : exampleSeg2){
std::cout << "tick: " << Segment.tick << " , enabled: " << Segment.enabled << std::endl;
}
return 0;
}最終解答(2周目)
#include <iostream>
#include <vector>
#include <algorithm>
struct Segment{
int tick;
bool enabled;
};
std::vector<Segment> filterAndSortSegments(std::vector<Segment>& seg){
std::vector<Segment> newSeg;
for(const auto& el: seg){
if(el.enabled == true){
newSeg.push_back(el);
}
}
std::sort(newSeg.begin(), newSeg.end(), [](Segment a, Segment b){
return a.tick < b.tick;
});
return newSeg;
}
int main()
{
std::vector<Segment> exSeg1 = {
{10,true},
{6,false},
{5,true},
{2,true}
};
std::vector<Segment> exSeg2 = {
{3, true},
{9, false},
{4, true},
{20, true},
{14, true}
};
std::vector<Segment> exSeg3 = filterAndSortSegments(exSeg1);
std::vector<Segment> exSeg4 = filterAndSortSegments(exSeg2);
std::cout << "exseg1 sorted: " << std::endl;
for(const auto& el : exSeg3){
std::cout << el.tick << " " << el.enabled << std::endl;
}
std::cout << "exseg2 sorted: " << std::endl;
for(const auto& el : exSeg4){
std::cout << el.tick << " " << el.enabled << std::endl;
}
return 0;
}ふりかえり
- 行10:
const std::vector<Segment>&で参照させよう。コピーしないので速くなる - 行11:newSeg は
reserveするとメモリ効率が良くなるらしい(わかってない)newSeg.reserve(seg.size());
- 行19:sort処理の第3引数のかきかた
auto const a, auto const bにするとコピーが発生するので参照しよう- ラムダ式の引数には、型かオブジェクト全体しか指定できないので、
a.tickなどは代入不可
- 行44-45:はじめここを、
exampleSeg1 = filterAndSortSegments(exampleSeg1);と書かないと正しく処理されなかった。その原因を追及- ①
filterAndSortSegmentsの戻り値の型:voidvs.std::vector<Segment> - ②関数の中にreturnがあるかないか
- ③引数はコピーをとるのか、参照渡しなのか
- この3点がごちゃごちゃしていて、意図した処理にならなかった。
exampleSeg1をコピーでfilterAndSortSegmentsに渡す→新しいnewSegをreturnする処理をしていた→main()側で代入してあげないとexampleSeg1の中身は更新されなかった
- ①
ふりかえり (2周目、3/31)
std::boolalpha0/1の出力→false/trueに置き換えられる- 構造体(
segment[2].tickなど)の値の出力で、range-based for文を使いながら添え字も欲しい- for文の外でint型変数を作って、出力のなかで
idx++を加える方法もある。
- for文の外でint型変数を作って、出力のなかで
int idx = 0;
for (const auto& el : seg1) { //この書き方だと添え字が出てこない
std::cout << idx++ << ": tick = " << el.tick
<< ", enabled = " << std::boolalpha << el.enabled << std::endl;
}
第8問 座標変換 (3/17)
問題:二次元座標の変換プログラム
次の要件を満たすC++コンソールアプリケーションを作成してください。
- 入力
- 最初に、整数
Nを読み込む(座標ペアの数)。 - つぎに、
N行にわたって(x, y)の整数座標ペアを読み込む。 - その後、ドラッグオフセット
(dx, dy)を読み込む。 - 最後に、移動方向の制限を示す文字列を読み込む(”x” / “y” / “both” のいずれか)。
- 最初に、整数
- 処理
- 入力された座標ペアを順番に「ドラッグ」処理する。
- もし制限が “x” の場合、
yは変えずにxだけを(x + dx)に更新。 - もし制限が “y” の場合、
xは変えずにyだけを(y + dy)に更新。 - もし制限が “both” の場合、
(x, y)を(x + dx, y + dy)に更新。
- もし制限が “x” の場合、
- 入力された座標ペアを順番に「ドラッグ」処理する。
- 出力
- 移動後の座標ペアを、N行にわたって表示する。
- サンプル実行例 (略)
- その他
- 入力のバリデーション は大雑把でOK。
- 出力の形式は、各座標ペアを1行ずつ表示するだけでよい。
ヒント:
- C++で標準入力を扱うときは
std::cin、標準出力はstd::coutを使います。 - 文字列比較は
std::stringを用いて==演算子でOKです(C++11以降)。 - 条件分岐(if / else if / else)で 制限モード (“x”,”y”,”both”) を振り分けるとよいでしょう。
この問題を解くことで
- 標準入力 / 出力 の扱い
- 配列やベクタを使った座標管理
- 文字列に応じた分岐処理
- ドラッグオフセットと座標変換
などが身につきます。ぜひ挑戦してみてください!
最終解答
#include <iostream>
#include <vector>
int main()
{
int N; //座標ペア
std::cout<< "N: ";
std::cin >> N;
std::vector<std::vector<int>> pos(N,std::vector<int>(2));
for(int i=0 ; i<N; ++i){
std::cout << "pos(" << i << "): ";
std::cin >> pos[i][0] >> pos[i][1];
}
int dx=0;
int dy=0;
std::cout << "(dx,dy): ";
std::cin >> dx >> dy;
std::string restriction;
while(true){
std::cout << "移動制限(x,y,both):";
std::cin >> restriction;
if (restriction == "x" || restriction == "y" || restriction == "both"){
break;
}
std::cout << "エラー:x,y,bothのいずれかを入力\n";
}
//内部処理
int dx_apply = (restriction == "y") ? 0 : dx;
int dy_apply = (restriction == "x") ? 0 : dy;
for(int i=0; i<N; ++i){
pos[i][0] += dx_apply;
pos[i][1] += dy_apply;
}
std::cout << "output:" << std::endl;
for(int i=0; i<N; ++i){
std::cout << "pos(" << i << "):" << pos[i][0] << " , " << pos[i][1] << std::endl;
}
}最終解答(2周目)
#include <iostream>
#include <vector>
int main()
{
int N;
std::cout<<"N: ";
std::cin >> N;
std::vector<std::vector<int>> point(N, std::vector<int>(2));
//座標入力
for(int i=0; i<N; ++i){
std::cout << "i=" << i << ": ";
int x;
int y;
std::cin >> x >> y;
point[i][0] = x;
point[i][1] = y;
}
//オフセット、移動制限
int dx, dy;
std::cout << "dx,dy: ";
std::cin >> dx >> dy;
std::string direct;
while(true){
std::cout << "移動方向: ";
std::cin >> direct;
if(direct=="x" || direct=="y" || direct=="both"){
break;
}
std::cout << "x,y,bothのみ入力できます。" << std::endl;
}
//座標変換
int apply_dx = direct=="y" ? 0 : dx;
int apply_dy = direct=="x" ? 0 : dy;
for(int i=0; i<N; ++i){
point[i][0] += apply_dx;
point[i][1] += apply_dy;
}
//座標出力
for(int i=0; i<N; ++i){
std::cout << "point[" << i << "]: ";
for(int val: point[i]){
std::cout << val << " ";
}
std::cout << std::endl;
}
return 0;
}ふりかえり
- 行24-32:入力ミス対策
- だるいからはじめは処理させなかったが、ユーザーフレンドリーなコードとして重要
- whileループに入る→入力指示→正しい入力があればbreak→ループの最後にエラー文 の流れが定石と考えられる(第6問と共通)
- このへんの定石を集めてまとめて学べるといいなあ。知らないとすぐ書けない
- 行34-41:座標変換のループ処理
- もともと、restrictionの値ごとにfor文を3回書いていたが、
dx_apply, dy_applyを三項演算子を使って定義することでシンプルに書ける - 三項演算子はMusescoreのコードを読んでいても頻出だったので現場では重要らしい
- もともと、restrictionの値ごとにfor文を3回書いていたが、
ふりかえり (2周目、3/31)
vectorの値の入れ方は次のいずれか- サイズを最初に決める → 添え字で入れる先を指定可能
- サイズを決めない →
push_back
- ベクターの二次元配列で大きさを(N x 2)にする場合
std::vector<std::vector<int>> point(N, std::vector<int>(2));
まとめ
- Hello world! を何度も出力するよりよほど実践的な訓練ができた。
- ChatGPT(MEMちょ似)のいうことをどこまで信じてよいか測りかねる部分はあるが、コンパイラ通ってバグもないならとりあえず問題なさそう。

