LoginSignup
0
0

More than 3 years have passed since last update.

C++ 講座 演習2

Last updated at Posted at 2019-05-26

注意: この記事は古い方針に基づいた内容であり、閲覧しないことを推奨します。学びたい人は こちら をどうぞ。

今回は複数の値の取り扱いと
それに必要な知識もやっていく

目標

  • 2 通り以上の条件分岐
  • 一時的な値の保存
  • ループ
  • 任意の長さの値の取り扱い

の習得

インデント (字下げ)

括弧の中身が複数行にまたがるとき、
その始まりと終わりを明確にするために行う

{
  {
{
     {
}
  }
}}

上のものより
下のほうが見やすい (と思ってくれれば嬉しい)

{
  {
    {
      ;
    }
  }
  {
    ;
  }
}

これから波括弧などが増えてくるので書いておく

else

if と一緒に使う構文
条件が false のときだけ実行される
分岐したお互いの文はどちらかしか実行されない

if ( expr ) {
  ~ ; // when expr is true
} else {
  ~ ; // when expr is false
}

else if

そして else の直後に if を付けられる
これで 2 パターン以上に対応できる

if ( expr ) {
  ~ ; // when expr is true
} else if ( expr2 ) {
  ~ ; // when expr is false & expr2 is true
} else if ( expr3 ) {
  ~ ; // when expr2 is false & expr3 is true
} else {
  ~ ; // when expr3 is false
}

上の if から優先的に分岐する

変数

値を保存する場所をつくる仕組み
関数が受け取る引数も変数のうちの一つ

関数内において、
以下のように変数を作ることを「変数の定義」という
文献によっては「変数の宣言」ともいわれる

// Variable definitions
int var = 12; // `var` stores 12
double rough_e = 2.7; // `rough_e` stores 2.7
bool completed = false; // `is_completed` stores false

変数にアクセス

変数の名前をそのまま値のように書くと、変数の値がコピーされる

int var = 12;
int val = var; // `val` stores 12 (copied from `var`)
return val; // return 12 (copied from `val`)

命名規則

変数には使えない名前がいくつかある

  • 数字で始まる
  • _ と大文字英字で始まる
  • __ が含まれている

ような名前は定義できない

// You can't define:
int 2_to_3 = -1;
// You must not define:
int _X = 0;
int f_G = 1;
int a__ = 2;
int ___v__ = 3;
int __Q = 4;

auto

auto はプレースホルダー型といわれるものの一つで、
実際の型は = の右側から推論される

auto count = 0; // typed int
auto rough_pi = 3.; // typed double
auto done = true; // typed bool

代入

変数の値を書き換えること

代入演算子 = を使う
※変数の定義に使う = は同じ記号なだけで代入演算子ではない

int ham = 1130;
ham = 2680; // Assignment is here

これを使うと、足し合わせることなどができる

auto summing = 0;
summing = summing + 5;
summing = summing + 8;
summing = summing + 12;
summing = summing + 100;

引数も上書きできる

int example(int what) {
  what = 2;
  return what;
}

複合代入

上の v = v なんとか 数 というパターンが多いのでそのための構文

このパターンを v なんとか= 数 と書ける
この += -= などを複合代入演算子という

auto summing = 0;
summing += 5;
summing += 8;
summing += 12;
summing += 100;

ただ対応する演算子が無いこともあるのでそのときは普通に書く

インクリメント / デクリメント

変数の値を 1 だけ増やす / 減らす演算子

auto count = 0;
++count; // increment
// count == 1
--count; // decrement
// count == 0

これらはそれぞれ += 1 / -= 1 かのように使われる

C++ 使いは、必ず絶対に変数の前 に書く (後に書く方もある)
技術的なアレコレがあるので理由は省く

while

ループするための構文

auto to_loop = true;
while (to_loop) { ~ }
// loop while `to_loop` is true

auto count = 3;
while (3 < count) {
  --count; // counting down
}

{ ~ } が繰り返され、これが終わる度に while (~) の式が評価される

実行順序

繰り返すかどうか → ループ内容 → 繰り返すかどうか → ループ内容 → ……

の順番で実行される
そのため条件式が最初から false だと中の処理は一度も実行されない

条件式が true のときにだけ繰り返されることに注意

ちなみに条件式に true と書くと (そのままでは) ループから出てこない

while (true) { ; }

ある制御文と組み合わせて
これを使うこともあるが
今はまだ説明しない

for

while の上位互換なので少し構文が複雑

for (int i = 0; i < 11; ++i) { ~ }
// To explain ...
for ( // For
int i = 0; // i starts from 0
i < 11; // to 11
++i // increment by the loop
) { 
  ~ ; // is repeated 11 times
}

for (
~ ; // before the loop
~ ; // loop while the expression is true
~ // at the end of the loop
) { ~ }

(ループ開始前; 条件式; ループ後処理) のように式が記述できる
独特かつ伝統的な記法なので、何回も書いて覚えよう
繰り返しの条件が一箇所にまとまる、何回目のループか分かるのが長所

実行順序

ループ開始前 → 条件式 → ループ内容 → ループ後処理
→ 条件式 → ループ内容 → ループ後処理 → ……

と実行される

よく使うパターン

  • 0 から始まる for (auto i = 0; i < 繰り返し回数; ++i) { ~ }
  • 1 から始まる for (auto i = 1; i <= 繰り返し回数; ++i) { ~ }

はよく使う
どうしてこうなるか考えてみよう

繰り返し回数を 2 とすると、

auto i = 0;

i < 2true

ループ内容 1 回目

++ii1
i < 2true

ループ内容 2 回目

++ii2
i < 2false

ループ終了

1 で始まる方も同様

例題 1

// void means return nothing
void fizz_buzz(int times) {
  // Write here
  print("Buzz");
}

ここで関数を実装せよ
times が入力される
この数まで Fizz Buzz (下記) を行え

数字を数えていき、以下を出力する

  • 3 と 5 の倍数なら "Fizz Buzz" を
  • 3 の倍数なら "Fizz" を
  • 5 の倍数なら "Buzz" を
  • それ以外は数字をそのまま

ここで出力する関数として print 関数を用意しておいた
print(10); とか print("Fizz"); で出力してほしい

解説

変数 count1 から 数え上げていき、
その値によって分岐する
else を使わないとおかしくなる

void fizz_buzz(int times) {
  for (auto count = 1; count <= times; ++count) {
    if (count % 15 == 0) {
      print("FizzBuzz");
    } else if (count % 3 == 0) {
      print("Fizz");
    } else if (count % 5 == 0) {
      print("Buzz");
    } else {
      print(count);
    }
  }
}

関数呼び出し

さっき使ったけどここでちょっと解説

自分や他人が作った関数は、
関数名(引数);
とすると呼び出せる

std::string

std::string はテキストを扱うためのクラス
先程のようにテキストの値を作るには " で囲う
これを「文字列」という

でもこの文字列 "abcdefg"std::string ではない
auto を使ってしまうと違う型になる

std::string empty;
std::string text = "abcdefg";

std::to_string

intdouble から数字の std::string にするには、std::to_string 関数を使う

auto text = std::to_string(2); // == "2"

find

std::string 型の値はオブジェクトという類のもので、
これらには関数が付いている
テキストにある文字が含まれているかどうかは、以下で判定できる

std::string text = "timeleeeess!";
auto l_pos = text.find("l"); // returns position if found the text
auto x_pos = text.find("x"); // returns std::string::npos if not found
// so
auto contains_exclamation =
  text.find("!") != std::string::npos; // means `whether contains "!"`

他にも機能があるけど次でこれを使うから説明した

#include

あ、これを使うより前に #include <string> と書いて
string ファイル (C++ に付属) を展開しておく必要がある

#include <string>

こういう外部のプログラムファイル群をライブラリといい、
特に <string> のような C++ 標準のものを標準ライブラリという

他の標準ライブラリ内でも #include <string> されていることが多いので、
この #include が要らないこともある

問 1

auto included_three(int n) {
  // Write here
  return false;
}

ここで関数を実装せよ
n が入力される
これが 3 のつく数かどうか判定せよ

std::vector

同じ型を大量に格納するためによく使われる
これは <vector> 標準ライブラリが必要

std::vector<int> v; // contains ints
auto copied = v; // another v

今回はこれを自分で作ったり触ったりする必要はない
あらかじめ用意されたものを渡すので、
これをどう加工するかをプログラムする

size

格納している個数を尋ねる

auto elements_count = v.size();

at

n 番目に格納しているものを取得する
この番号は 0 から始まり、0 以上 size 未満
また、格納しているものは「n 番目の要素」のように言う (他の格納するものでも同じ)
存在しない番号を渡すと terminating ~ std::out_of_range: vector
と出てプログラムが停止する

auto first_element = v.at(0);
auto third_element = v.at(2);
v.at(1) = 0;

イテレータ (反復子)

標準ライブラリには何かを格納するクラス (コンテナクラス) がいくつかあり、
それらはイテレーターを使って操作できる

ランダムアクセスイテレータ

std::vector<int> v;
auto head = v.begin();
auto tail = v.end();

によって得られるイテレータは、

  • アクセス演算子 * を使って格納された値を読み/書きできる
  • インクリメント演算子 ++ を使って次の要素に移動する
  • デクリメント演算子 -- を使って前の要素に移動する
  • 比較演算子 == / != を使って同じ要素を指すかどうか取得できる

といった特性を持っている

void example(std::vector<int> v) {
  auto head = v.begin();
  auto first_element = *head; // Read
  *head = 2; // Write
  auto next = ++head; // Advance, then copy
  head == --next; // is true
}

これを利用して、要素に対する操作ができる

例題 2

int sum(It head, It tail) {
  int current = 0;
  // Write here
  return current;
}

ここで関数を実装せよ
イテレーター head tail が入力される
この範囲 (head ~ tail) の総和を計算せよ

解説

int sum(It head, It tail) {
  int current = 0;
  for (auto it = head; it != tail; +it) {
    current += *it;
  }
  return current;
}

こうして実装できる

実は <numeric> 標準ライブラリに
std::accumulate という上位互換の関数がある

問 2

void seq_interpolation(It begin, It end) {
  int back = 0;
  // Write here
}

ここで関数を実装せよ
イテレータ head tail が入力される
途中に -1 のデータがある
そのデータをそれより前の -1 でないデータで置き換えよ
最初に -1 がある場合は 0 にセットせよ

問 3

void normalize(It head, It tail, double min, double max) {
  // Write here
}

ここで関数を実装せよ
イテレータ head tailmin max が入力される
head ~ tail の要素を min ~ max の範囲で以下の値にせよ

  • min 以下の値は 0
  • max 以上の値は 1
  • それ以外の値は (要素の値 - min) / (max - min)

これは、min ~ max の範囲で 0 ~ 1 の値に変換 (正規化) する処理になっている

多次元コンテナ

std::vector は任意の (実際は少し条件がある) 型を格納できる
つまり、

std::vector<
  std::vector<
    ~
  >
>

としてもいい

std::vector<std::vector<int>> square;
std::vector<std::vector<std::vector<int>>> cubic;

こういったコンテナのコンテナは「二次元」のコンテナと呼ばれ、
こんな感じで格納される

[
  [0, 0, 0, ...],
  [1, 2, 1, ...],
  [3, 6, 1, ...],
  :
  :
]

行 (上図での横) 方向に格納されたコンテナが
列 (上図での縦) 方向に格納されるため、
これらへのインデックスによるアクセスは逆順 (列→行) になる

auto row = 0;
auto column = 0;
square.at(row).at(column);

auto depth = 0;
cubic.at(depth).at(row).at(column);

インデックスによる要素ループ

インデックスを指定すれば要素にアクセスできるということは、
for 文で 0 からコンテナのサイズの範囲を数えているカウントを
インデックスに指定すると順々にアクセスできる
ちなみに、インデックスに使う値の型は size_t (これも整数) が正しい

std::vector<int> v;
for (size_t i = 0; i < v.size(); ++i) {
  v.at(i) = 1;
}

二次元だとこう

std::vector<std::vector<int>> m;
for (size_t y = 0; y < m.size(); ++y) {
  for (size_t x = 0; x < m[y].size(); ++x) {
    m.at(y).at(x);
  }
}

次回あたりからの数学的な問題を解くために
インデックスを用いた計算を実装する必要が多い

例題 3

std::vector<std::vector<bool>> shift_field(
  std::vector<std::vector<bool>> field,
  int offset_x,
  int offset_y
) {
  auto processed = field;
  // Write here
  return processed;
}

ここで関数を実装せよ
コンテナ field と整数 offset_x offset_y が入力される
field を行方向に offset_x、列方向に offset_y ずらしたものを返せ
範囲外からずらされてきた要素は false にせよ

解説

コピーして新しい配列を作り、
要素 (x, y) における元の配列との関係を
漸化式のように
processed.at(y + offset_y).at(x + offset_x) = field.at(y).at(x);
と記述する

また、アクセスしようとしている x のインデックスを
src_x として (名前は何でもいい)、
0 <= src_x && src_x < field.at(y).size()
というように範囲外でないかを
y においても同様に確認している

std::vector<std::vector<bool>> shift_field(
  std::vector<std::vector<bool>> field,
  int offset_x,
  int offset_y
) {
  auto processed = field; // copy
  for (size_t y = 0; y < field.size(); ++y) {
    for (size_t x = 0; x < field.at(y).size(); ++x) {
      // index validation
      auto src_x = x - offset_x;
      auto src_y = y - offset_y;
      if (0 <= src_x && src_x < field.at(y).size()
        && 0 <= src_y && src_y < field.size()) {
        processed.at(y).at(x) = field.at(src_y).at(src_x);
      } else {
        processed.at(y).at(x) = false;
      }
    }
  }
  return processed;
}

問 4

bool collides_field(
  std::vector<std::vector<bool>> field,
  std::vector<std::vector<bool>> to_join
) {
  // Write here
  return false;
}

ここで関数を実装せよ
同じサイズの二次元コンテナ field to_join が入力される
fieldto_join において、
同じインデックスの2つの要素が true であれば、true を返せ
それ以外は false を返せ

まとめ

お疲れ様!

次回はより複雑なデータ構造を扱うことになって、

「一つくらいの入力」→「データ構造を作成するシステム」→「データ構造を出力」

「データ構造」→「それを探索するシステム」→「一つくらいの出力」

を実装するよ

次回もよしなに

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0