0️⃣

[C++ 基本] CPP Module00振り返り

に公開

はじめに

今回は42Tokyoの課題の1つであるcppmoduleを振り返っていきたいと思います。

課題概要

C++について幅広く学んでいく課題です。
僕は競技プログラミングでC++を触ることはあっても、クラスの操作やC++の特性や細かい部分について学べた課題でした。

学んだこと

std::cinのCtrl+Dに対する挙動

以下は標準入力を受け取り、そのまま出力するという手順を繰り返すコードです。

#include <iostream>
#include <string>

int main(void)
{
    std::string str;

    while (true)
    {
        std::cin >> str;
        std::cout << "I got: " << str << std::endl;
    }

    return 0;
}

このコードを実行して、Ctrl+Dを押すとどうなるでしょうか?
結果は以下のようになります。

$ g++ test1.cpp
$ ./a.out
I got: 
I got: 
I got: 
I got:

...

I got:
I got:
I got:
^C

Ctrl+Cで中断するまで無限ループしました。
以前別の記事でCtrl+DはEOFを送信するというように解説しました。
直感的には1回分のループにのみEOFが送信されてまた次の入力を受け付けるかと思いましたが、無限ループしてしまいます。
これはEOFによって入力ストリームにeofbitという状態フラグが設定され、2回目以降もこれを検出して入力を受け付けないので無限ループしています。
では、以下のような処理を追加したら引き続き入力を受け付けることはできるでしょうか?

#include <iostream>
#include <string>

int main(void)
{
    std::string str;

    while (true)
    {
        std::cin >> str;
        if (std::cin.eof())
        {
            std::cout << "EOF detected" << std::endl;
            std::cin.clear(); // 入力ストリームのフラグリセット
            std::cin.ignore(); // 現在のバッファの文字を無視する
            continue;
        }

        std::cout << "I got: " << str << std::endl;
    }

    return 0;
}
$ g++ test1.cpp
$ ./a.out 
^D
EOF detected
EOF detected
EOF detected
EOF detected
EOF detected

...

EOF detected
EOF detected
^C

無限ループしてしまいます。なぜでしょうか?
これは、clear()によってeofbitのフラグ自体をリセットしても、ignore()によって無視されるのはあくまで文字でありEOF(終端マーク)は無視されないので、改めて終端を検出してしまいます。
原則EOFはプログラム側から取り消すことはできないとされています。
なので、基本的にはEOFを検出したらループをbreakするべきです。

クラス(基本)

クラスとはユーザーが定義できる、変数や関数等をまとめて定義できるデータ型です。
例えば以下のように定義されます。

(dog.hppファイル)
#ifndef DOG_HPP
#define DOG_HPP
#include <string>

class Dog {
public:
    void        bark();
private:
    int         age;
    std::string name;
};

#endif
(dog.cppファイル)
#include <iostream>
#include "dog.hpp"

int main(void)
{
    Dog dog;
    dog.bark();

    return 0;
}

慣習として上記のように~.hppファイルに定義してその具体的な関数の実装などを~.cppファイルに定義します。
このようなクラスの実装には可読性や同名関数の定義など、さまざまな利点があります。
さらにクラスはC++の大きな特徴の一つと言える"オブジェクト指向"の実装の基礎になります。
オブジェクト指向については別の記事でまた詳しく触れようと思います。

ifndef ~ define ~ endif

C++のみでなくCでもヘッダファイルにはお決まりの呪文のようにifndef ~ define ~ endifを記述します。これは何を意味しているのでしょうか?
これは「インクルードガード」と呼ばれ、ヘッダファイルの多重読み込みを防ぎます。
例えば先ほどのDogクラスを複数の別のファイルから使用したい場合を考えます。
このときもちろんそれぞれのファイルでdog.hppファイルをincludeする必要があります。
ですがこれによってDog.hppが2回読み込まれ、再定義されてコンパイルエラーになってしまいます。
これを避けるためにインクルードガードをします。

#ifndef DOG_HPP // もしDOG_HPPが定義されていなければ(これで2回目以降の定義を避ける)
#define DOG_HPP // DOG_HPPを定義する

...

#endif // ifndefの終了

よくdefine ~ endifで定義するブロックを作成してそれがDOG_HPPと勘違いされがちですが、あくまでも「dog.hppファイルは読み込みましたよ」と明示するためにカカシとしてDOG_HPPを定義しているだけで、DOG_HPPがDogクラスと関連しているということはないです。
むしろ定義するブロックはifndef~endifの間といった方が正しいかもしれません。

publicとprivate

クラス変数、関数にはpublicとprivateという属性をつけることができます。(他にも種類がある)
これらは簡単にいうとpublicはクラス外からアクセスできるもの、privateはクラス外からアクセスできないものです。
これらをうまく活用することでコードの保守性があがります。private属性の変数にアクセスする関数をゲッター、セッターと一般的に言います。
僕はpublicとprivateはクラス内でpublicを上に記述するようにしています。
なぜなら実際に使用するのはpublic変数、関数の方だからです。

cppファイル

前述したようにDog::bark()のように関数を実装することでクラス関数の具体的な実装を記述することができます。
注意するべきなのは#include <iostream>というcppのみで使用しているヘッダをhppとcppのどちらにインクルードすべきかです。
僕は基本的にはcppファイルにインクルードすべきだと考えます。
なぜならhppファイルに記述するとそのhppをインクルードする全てのファイルでiostreamが読み込まれてしまうからです。コンパイル時間が増えます。

終わりに

今回はかなり基本的な内容でしたが、cinに対するCtrl+Dのくだりなど大切な部分はあったと思います。

Discussion