ロベールのC++入門講座を読んで C++ を初歩の初歩から再入門するよ - 前編

ロベールのC++入門講座

前置き

自分の C++ レベルに絶望したので「ロベールのC++入門講座」を読んで再入門していく過程を日記に書いていきます。
「おまえはそんなことも知らずにコードを書いていたのか!」「それは間違い!」など叱咤激励募集中です:-)


読んでみたら、たくさんの驚きがありました。
スルーせずに読んでもらえると1つくらい役に立つことを提供できるかもしれません。
例えば5章のデフォルト引数に関数を使える例とか。

ルール

前置きはともかくはじめましょう。13:48にスタバで始めました。
読んで行くうえでいくつかルールを決めます。

  • 知らなかったことは正直に知らなかったと書き、自分が読んで分かる説明を書く。
  • 知識が曖昧だった所も同様に明確な説明を書く。
  • 悩んだ内容を書く。
  • 理解が怪しい所は必ずコードを書く。


さあ1章から読んでみよう。

1章 まずは使い方(13:50)

各 OS 毎に C++ の開発ツールを紹介している。
自分はターミナルで開発。このあたりは理解しているので読み進める。


そういえば cout << "hoge"; とかほとんど書いたことないな。自分のOSにないからだな。良くない。
計算してみよう。加算とか剰余とか。

2章 C++の基本(13:56)

基本が疎かになっている気がするので目を皿にして読むよ。
変数は値の保管所です。おー懐かしい。昔こういうのがすんなりとは理解できなくて苦労した覚えが。
箱と値の絵が分かりやすい。
関数が出てきた。数学の関数との対比か。


今気づいたけど仮引数と実引数は英語ではそれぞれ parameter と argument なんだな。仮と実というニュアンスはどこからきたんだ?
void の説明が現れた。普段意識ないけど確かに入門者には説明が必要だ。

お。cin 使った事ないからやってみようっと。

int a;
cout << "はげ>" << flush;
cin >> a;
cout << a << endl;

動いたよ。 flush 知らなかった。fflush(stdout) のようにバッファの中身を flush してくれるんだろうね。


配列が出てきた。さすがに丁寧に説明している。
switch 文がどのようにコンパイルされるか?的な話は別の本を読むべきなんだろうな。

2章では cin と flush が収穫でした。

3章 ほんの少し深く(14:24)

sizeof n 。カッコは不要知らなかった。


参照が出てきた。さて完全に理解できているだろうか?
あまり意識して来なかったけど、参照の参照先は後から変更することはできないんだよね。


うお。知らない事発見。
'' で囲って得られる値の型は C++ では char 、Cでは int らしい。知ってました?

オーバーロードの説明が分かりやすいね。
Column に出てきた文字コードに関する制限を知らなかった。アルファベットの文字コードが連続していることは規格では要求されていないらしい。

'A' <= ch && ch <= 'Z'

が意図どおり動作しない場合もあるということですね。


参照についてもう少し深く突っ込むのは先の章かも知れないな。楽しみ。

4章 ポインタ天国(14:52)

OSを作っていてメモリを触りまくりなのでポインタは完全に理解していると信じたいがどうかな。
ポインタが何に使えるか?を説明していて分かりやすいね。
「参照とポインタの大きな違いは、参照は初期化時に一度参照先を決めたら二度と参照先を変えられないのに対して、ポインタの場合はアドレスを再代入すれば参照先を何度でも変えられるという点です。」


良く話題になるポインタと配列の違いについて。

void hogePointer(char* p)
{
    cout << "sizeof pointer p=" << sizeof p << endl;
}

void hogeArray(char a [])
{
    cout << "sizeof array a=" << sizeof a << endl;
}

int main(int argc, char *argv[])
{
    char c[10];

    cout << "sizeof c=" << sizeof c << endl;
    hogePointer(c);
    hogeArray(c);
}

がそれぞれ何を出力するか分からない場合は復習しておくと良いかも。


また Column に知らないことが出てきた。
以下のプログラムが何をしているか分かるでしょうか?

int n[3];
for (int i = 0; i < 3; i++)
{
    i[n] = i;
    cout << "n[i]= " << n[i] << endl;
}

[] 演算子は a[b] と b[a] はおなじ意味らしい。なんと!
それぞれ *(a + b) 、*(b + a) になるので等しいのですね。


const が出てきた。これ重要。
一応 const の例を全部書いてみよう。

const char* s = "hige"; // ポインタ s の指す先 変更不可
const char&r  = s[0];   // r の参照先 変更不可
const int i = 1234;     // i の値 変更不可
char* const p = "hige"; // p 変更負荷


NULL ポインタの目的と定義をちゃんと読んだのは初めてな気がする。
「どんな変数や関数などのアドレスと比較しても等しくならないことが保証されているアドレス」を持つポインタ。

4章になって少しずつおおっと思えるような説明が増えてきました。

5章 クラスの前に(15:24)

配列の配列の配列の初期化とかしたことないから知らなかった。

int m[2][3][4] = {
    {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12},
    },
    {
        {9, 10, 11, 12},
        {5, 6, 7, 8},
        {1, 2, 3, 4},
    },
};

こうです。


構造体に関して疑問が一つ解消した。以下の Hige と Hage の 違いは何でしょうか?

struct Hige {
    int hige;
};

Hige hige;

typedef struct Hage {
    int hage;
} Hage;

Hage hage;

実は C 言語では Hige はエラーになります。なので C と C++ 両方から include されるヘッダに関しては typedef してあげないといけないのです。
小さいサイズの構造体ならば参照やポインタ渡しよりも値渡しの方が実行速度の面で有利な場合がありますと書いてありますがそのあたりはまだ理解してないです。(C++ コンパイラにおける値渡しの最適化が参考になるかもしれません。)


enum 。あれ?無名な enum の説明がないぞ。後で出てくるのかも。


デフォルト引数 ktkr!
この発想はなかったけど頭の柔らかい子ならやってしまうかも。

int Hoge(int a, int b = a); // これはNGよ

ちょちょっと待て。「デフォルト引数には静的な値しか指定できません。ここでいう静的な値というのは、定数、もしくはプログラムが実行される前に位置が決まっている値です。」
これは知らなかったぞ。やってみよう。

static int i = 3;

int func(int j = i)
{
    return j;
}

おおおお。動いた。すげえ!


さらにもっとすごいのあるよ!下のfunc2 のデフォルト引数見てください。静的な値を使って実行された関数の戻り値もOK。

static int i = 3;

int func(int j = i)
{
    return j;
}

int func2(int k = func())
{
    return k + 1;
}

int main(int argc, char *argv[])
{
    cout << func() << endl;  // => 3
    cout << func2() << endl; // => 4
    i = 5;
    cout << func2() << endl; // => 6

}


一つ勘違いしていた事実を発見。

char* str = "hige";
str[0] = 'H';

というコードはコンパイルエラーにならないけど。文字列リテラルへの代入な仕様的には禁止なのですね。
良く考えればそりゃそうだ。共有されている場合もあるだろうし。


でも以下の場合は OK なのがややこしいですね。

char[] str = "hige";
str[0] = 'H';


new/delete 。これはさすがに知らないことはないだろうと思いきやさっそく発見。
new(nothrow) とするとメモリ割り当てできない時に例外ではなくて NULL を返すらしい。へぇ。


5章は知らないことだらけだった。まだクラスが出てきていないというのに。先を読むのが楽しみですね。

6章 クラスの基礎(16:20)

クラスの基礎。散々使ってきた機能ですが今までの例を考えると知らないことがありそう。ドキドキ。

コピーコンストラクタに関して再確認。コピーコンストラクタは仮引数を作るときに呼ばれる。デフォルトでは値のコピー。
初期化と代入は明確に区別される。

IntArray b = a; // 初期化なのでコピーコンストラクタが呼ばれる
b = a;          // 代入なので operator = が呼ばれる

mutable 記憶指定子知らなかった。

7章 クラスの本領(16:50)

virtual と pure virtual とかそういう話。
思想に関わる話「C++はできることはできないように保証する」という仕様が備わっている。


基底クラスのデフォルトでないコンストラクタが呼ばれるように指定する。

InputStream::InputStream(double n) : Stream(n)


POD(plain old data)型とはデフォルトで提供されるコンストラクタしか持たないもの。


Column 仮想関数テーブルについての説明。うん自分の理解は正しい。


デストラクタには virtual をつけましょうという話。さらに Column では仮想デストラクタのための仮想関数テーブルのサイズがもったいないので、あえて非仮想デストラクタにする例が出てきます。

8章 ファイルとストリーム(17:17)

だいぶ疲れてきました。このPCの電池がなくなるか夕飯の時間まではがんばります。
string クラスが出てきました。 c_str() と data() って違うものなんですね。c_str はヌルターミネートされていることが保証されていると。
危ない危ない。
ファイルの扱いはまあまあ。
マニピュレータ使ったことない!使ってみよう。

string hello = "Hello";
cout << setw(7) << setfill('C') << uppercase << hello << endl; // => CCHello


using namespace ではなくて using を使った方がよい場合もあるという話。確かに。
あと namespace のエイリアス知らなかった。

namespace HIGE = High::Intelligent::Great::Emacs;

9章 テンプレート(17:45)

いよいよテンプレートです。気合を入れ直そう。
ちと立ち上がって伸びをしてきます(まだスタバにいます。)
よしリフレッシュ完了。


「関数テンプレートとは型をはっきり決めずに作った関数のようなもの。」
理解はしていますが写経してみます。

template <typename TYPE_A, typename TYPE_B>
void show(TYPE_A a, TYPE_B b)
{
    cout << "a:" << a << " b:" << b << endl;
}

show(20, "Hello"); // a:20 b:Hello


クラステンプレート。
デフォルト引数。
template とか template とか。
お。意外とあっさりテンプレートの章が終わったぞ。これは予想外。
テンプレートを理解しようというのであれば次は Modern C++ designかな。

10章 エラー処理と例外(18:05)

例外はほとんど使ったことないんですがどうなんだろう。
特筆すべきことはなかった。
ここで電池が切れる。(18:20)

帰宅して夕飯。お好み焼き。さくさくふわふわうまい。

11章 もっと高く(19:31再開)

章のタイトルがいいね。
静的メンバ変数について。インスタンスの個数を記録するという定番の説明だ。
「異なるファイルにある静的変数の間の初期化の順は特に決まっていない」というのは以前痛い目にあったので分かっています。


静的メンバ定数とか列挙しによる定数とかメンバ定数とか違いが書いてあります。分からない人は一読を。


テンポラリオブジェクト。理解が怪しいポイントだな。
書いてみたよ。

class Test
{
public:
    Test(int n) : n_(n)
    {
        cout << "Test(" << n << ")" << endl;
    }

    Test(const Test& t)
    {
        cout << "Test(Test&t)" << endl;
        n_ = t.n_;
    }

    ~Test() { cout << "~Test " << n_ << endl; }

    void operator=(const Test& t)
    {
        cout << "operator(Test& t)=(" << t.n_ << ")" << endl;
        n_ = t.n_;
    }
private:
    int n_;
};

int main(int argc, char *argv[])
{
    Test t1(1);
    Test t2(2);

    t1 = t2; // operator=

    t2 = Test(3); // テンポラリオブジェクト Test(3) が Test(const Test& t) に渡る

    // この時点でテンポラリオブジェクトのデストラクタが呼ばれる

    t1 = 4; // テンポラリオブジェクト Test(4) が Test(const Test& t) に渡る
}

// 結果
Test(1)
Test(2)
operator(Test& t)=(2)
Test(3)
operator(Test& t)=(3)
~Test 3
Test(4)
operator(Test& t)=(4)
~Test 4

うむ理解できた。


Column で戻り値最適化の話が出てくる。(あまり深くは突っ込んでいない。)


あー。operator[] の実装はいつもコピペで済ませていたけどきちんと理解する必要があるな。

class EseVector
{
public:
    EseVector(int n)
    {
        array_ = new int[n];
    }

    int& operator[](int n)
    {
        // 境界チェックは省略
        return array_[n];
    }

private:
    int* array_;
};

int main(int argc, char *argv[])
{
    EseVector v(4);

    v[3] = 3;
    cout << "v[3]=" << v[3] << endl;
}

このように代入も参照も一つの演算子経由で行えるのは参照を返しているからですね。


そういえば friend 関数使ったことないな。


暗黙のキャストを禁止する。
先ほどの EseVector が暗黙的にキャストされて EseVector(3); にならないようにする。

EseVector v2 = 3; // これを防ぎたい

// コンストラクタに explicit
explicit EseVector(int n)

12章 もっともっと高く(21:24)

xor とかビット演算の話。
インライン関数 ktkr。知っている内容だった。これ以上踏み込むのは別の本なのだろうな。


C++の本だと。マクロはやはり後半に出てくるのだよな。
おお。マクロの正しい使い方という説明でありがちなマクロの間違った使い方が出てくる。こういう内容ははじめて読むなぁ。

なぜか後半の章になるにつれて知っていることが多くなる不思議。(基礎力より応用力の謎)

13章 もっと深く(22:00)

多重継承。
間に他のクラスをはさめば同じクラスを継承できるらしい。ああ。確かにそういう継承ツリーはあるね。
そういえば仮想継承も使ったことないな。


typeid と typeinfo の話。例外と同じく自分のOSでは使えないから馴染みがない。
使ってみた。

cout << "t1 type=" << typeid(t1).name() << endl;
// => t1 type=4Test

フレンドの節のサブタイトルが「心の友」だった。ロベールさんとは話があいそう。


不完全型が出てきて何だこりゃ?と思ったら知ってた。名前を知らなかっただけだった。


using の真の意味について。

namespace Hoge {
  using namespace std;
}

Hoge::cout << "hoge" << hoge::endl;

なるほど。これは知らなかったな。Schemeの import に近いですね。

class Show2: public Show
{
    ... 略
    using Show::Value;
}

こんな例も。全然知らなかった><。
常識ですかそうですか。

頭が疲れてきたので今日はここまで(22:52)。13-11まで読み終わりました。
続きはまた明日。
ロベールのC++入門講座を読んで C++ を初歩の初歩から再入門するよ - 後編