ストラウストラップのプログラミング入門(26) 17章「ベクタとフリーストア」

ストラウストラップのプログラミング入門』を読む。第17章「ベクタとフリーストア」です。この章から、第3部「データとアルゴリズム」に入りました。

17〜19章では、ポインタを理解するために、vectorを再実装していきます。

読書メモ

ポインタ

次の表は、本文のコードを借りたまとめです。

(内容に間違いがあるかもしれません。信じないでください)

役割 記述例 演算子
ある型のポインタを保持するデータ型を宣言する int* pi; サフィックス
(※演算子ではない)
オブジェクト(X型)のアドレスを返す
X*型の変数で受け取ることができる
int ii = 17;
int* pi = ⅈ
アドレス演算子
ポインタで参照されているオブジェクトの値を返す *pi 間接参照演算子
(※オブジェクトが配列の場合は添え字演算子も利用できる)
作成したオブジェクト(X型)へのポインタ(X*型)を返す
配列の場合は、最初のオブジェクトへのポインタを返す
double* p = new double[4]; new演算子

「charポインタ型の値を1進める」とは?
ランタイムがそのプラットフォームにおけるcharのサイズ分だけ、メモリを見る位置を進めるための命令を発行する、の意。

配列で範囲外アクセスを許さない仕組みや、宣言した変数を初期化する仕組みは、Javaを使っていて当たり前だと思っていましたが、C++ではそういった親切機構(あるいは制約)がないことを学びました。

17.5「デストラクタ」

ひとまずこれを覚えておくことに。(17.7)

オブジェクトを削除するためのよい方法(中略)がある場合を除き、new演算子はコンストラクタの中だけで使用し、delete演算子はデストラクタの中だけで使用すること。

メンバオブジェクトに対するdeleteを備えたデストラクタが、スコープを外れるときに呼び出されるにもかかわらず、もしクラスの外でメンバオブジェクトを(それにアクセス可能だとして)明示的にdeleteしたら、deleteの二重呼出しが起きるのか? という疑問を持ったのですが、そもそもそういうスコープを破るような操作をするのは論外である、という示唆を某方面からいただきました。ハイ。つまり、上記でFAですね。

また、下記のページで、スコープを外れることによるデストラクタの自動呼出しと、明示的な呼出し(~Foo()の呼出し)とでは、実行される内容が異なることを知りました。

これは、非フリーストアから指定したメモリを確保するnewと、対応するdeleteの話ですが、フリーストアでのメモリの確保の場合でも同じなのでしょうか。デストラクタが「ただのメンバ関数として実行される」ということは、(クラス階層を利用した)オブジェクトの解体を行わない、を意味するという認識でよいのか? ・・・

デストラクタを要学習スタックに積みました。。。

17.8「型の操作: void*とキャスト」

static_cast、reinterpret_cast、const_castなどの刃物が紹介されました。

constを除去するキャストの使いどころが分からなかったので調べたら、こんな解説がありました。

「constメンバ関数と、非constメンバ関数オーバーロード」の項を参照。

const性の有無だけが異なる2つの関数があるとき、非constメンバ関数の中で、引数にconst性を付与した上でconstメンバ関数を呼び出し、最初の呼出し元に値を戻す際に、戻り値からconst性を除去しています。なるほど。

17.10「thisポインタ」

thisはメンバ関数が呼び出されたオブジェクトをポイントし、古いオブジェクトはポイントしない。コンパイラは、メンバ関数でthisの値が変更されないことを確認する。

「古いオブジェクト」って何でしょう。さっきまで見ていた(1つ前に呼び出された)オブジェクトをポイントするのではなく、直近に呼び出されたオブジェクトをポイントする、くらいの意味でしょうか。

また、17.10で、ちょっと疑問なコードがありました。疑問の焦点を先にいうと、「17.10のListクラスのfindメンバ関数は、戻り値にconstがついているけど、これって本当にいるんですか?」という話です。

17.10の冒頭で、Listクラスが次のように定義されます。

class Link {
public:
    string value;

    Link(const string v, Link* p = 0, Link* s = 0)
        : value(v), prev(p), succ(s) { }
(中略)
    const Link* find(const string& s) const;
    Link* advance(int n) const;
    Link* next() const { return succ; }
    Link* previous() const { return prev; }
private:
    Link* prev;
    Link* succ;
};

リストを操作する複数のメンバ関数があります。これらの多くが、前節(17.9)では、クラス外の関数として提供されていました。17.10では、それらをメンバ関数に変更した場合について説明されています。

findメンバ関数に注目します。findメンバ関数はconstのLink*を返します。

クラス外の関数としてのfindの定義は、次のようになっていました。(17.9.4)

Link* find(Link* p, const string& s)
{
    while(p) {
        if(p->value == s) return p;
        p = p->succ;
    }
    return 0;
}

戻り値にconstはついていません。

メンバ関数化したときのfindの定義は、17.10の本文で省略されています。なので、上のクラス宣言に合わせて、自分で書いてみます。こうでしょうか?

// 不完全ver.
const Link* Link::find(const string& s) const   // リストでsを検索する
        // 見つからない場合は0を返す
{
    Link* p = this;
    while(p){
        if(p->value == s) return p;
        p = p->succ;
    }
    return 0;
}

ダメですよね。戻り値のpはconstにしなければなりません。が、そうすると代入ができません。

そうだ、キャストしよう。ということで、覚えたてのキャストを使ってみます。

const Link* Link::find(const string& s) const   // リストでsを検索する
        // 見つからない場合は0を返す
{
    const Link* pp = this;
    Link* p = const_cast<Link*>(pp);
    while(p){
        if(p->value == s) return static_cast<const Link*>(p);
        p = p->succ;
    }
    return 0;
}

これは…ちょっと…。こんな何でもないところでconst_castを使ってよいものでしょうか。

さらに、17.10ではfind関数を使うコードが出てきますが、クラス宣言と矛盾した使われ方をしていて、コンパイルが通りません。

    Link* greek_gods = new Link("Hera");
    greek_gods = greek_gods->insert(new Link("Athena"));
    greek_gods = greek_gods->insert(new Link("Mars"));
    greek_gods = greek_gods->insert(new Link("Poseidon"));
    
    Link* p = greek_gods->find("Mars");    // エラー
    if(p) p->value = "Ares";

error C2440: '初期化中' : 'const Link *const ' から 'Link *' に変換できません。変換で修飾子が失われます。

どうやら、戻り値がconstであることを想定していないようです。

というわけで、findメンバ関数の戻り値のconstは余計では、と思いました。あるいは、constが要るなら、どう実装すればよいのか? 要素を探す処理は、たしかに(それ単体で考えると)対象を変更しないのでconstな戻り値でよいのでしょうが、使いづらいです。

ドリル

やりました。が、後半のドリル13が分かりません。

10. int型の10個の要素からなる配列を割り当て、この配列を1、2、4、8などの値で初期化する。そのアドレスを変数p1に代入する。

11. int型の10個の要素からなる配列を割り当て、そのアドレスを変数p2に代入する。

12. p1がポイントする配列の各要素をp2がポイントする配列にコピーする。

13. 配列の代わりにvectorを使用して、練習問題10〜12を繰り返す。

配列をvectorに読み替えるなら、これはvectorのポインタを使えということ? でしょうか?

#include "../../std_lib_facilities.h"

void print_vector(ostream& os, vector<int> v)
{
    for(int i = 0; i < v.size(); ++i){
        os << "v[" << i << "] == " << v[i] << '\n';
    }
}

int main(){
    vector<int> vv; // ドリル(13_10)
    for(int i = 0; i < 10; ++i){
        if(i == 0){
            vv.push_back(1);
        }else{
            vv.push_back(vv[i - 1] * 2);
        } 
    }
    cout << "vv: \n";
    print_vector(cout, vv);

    vector<int>* vv2 = new vector<int>(10); // ドリル(13_11)

    cout << (*vv2).size();  // 出力は10だから10個確保されてると思うんだけど…ダメ?

    for(int i = 0; i < (*vv2).size(); ++i){    // ドリル(13_12) ダメ!
        vv2[i].push_back(vv[i]);
    }

    keep_window_open();
}

範囲外アクセスで死んでしまいました。。。ぬー。

追記 2011/11/03)

push_back関数
vectorに要素を追加する。もし、要素数(size関数で取得できる値)と割り当て済みのスペース(capacity関数で取得できる値)が等しければ、メモリを後ろに拡張する。

…なので、ここでpush_backを使用するのは適切ではなかったですね。あと、vectorに添え字でアクセスしてpush_backするのも、ヘンでした。(finalfusionさん、ありがとうございます)

newでvectorを確保した時点で、10個の要素が存在することになります。その後、push_back関数を使うと、さらに後ろに要素が付け足されます。vv2が拡張していくので、添字がvvの範囲を超えて、エラーになったんですね。

main関数を次のように変えたら、コピーできました。

int main(){

    vector<int> vv; // drill(13_10)
    for(int i = 0; i < 10; ++i){
        if(i == 0){
            vv.push_back(1);
        }else{
            vv.push_back(vv[i - 1] * 2);
        } 
    }
    cout << "vv: \n";
    print_vector(cout, vv);

    vector<int>* vv2 = new vector<int>(10); // drill(13_11)

    cout << "使用中の数: " << (*vv2).size() << '\n';    // 10
    cout << "確保済みの数: " << (*vv2).capacity() << '\n';    // 10

    vv2 = &vv;    // drill(13_12)★
    cout << "vv2: \n";
    print_vector(cout, *vv2);

    keep_window_open();
}

★の行で、vectorの先頭要素のアドレスをコピーします。

…しかし、vv2とvvの要素数が違っていても構わないので、newで要素数を指定してメモリを確保しているのが無意味な気がします。要素数を指定するためというより、フリーストアにメモリを確保するために、newを使ったと考えればよいのか。

あと、値を1個1個コピーするなら、★行は下記でもよいような。

    for(int i = 0; i < (*vv2).size(); ++i){    // drill(13_12)★'
        (*vv2)[i] = vv[i];
    }