Entries

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

ポインタと、それに付随する4つの情報

タグ: C++

C言語やC++でポインタを扱う際には、それ自身が持つ「型」と「値(アドレス)」のほかに、以下の4つの情報を管理しなければならない。

  • 正当性
  • 終端
  • 複製処理
  • 解放処理

正当性

ポインタを逆参照しても良いか否か。
以下の3つの状態が存在する。

  1. 値がNULL(INVALID_HANDLE等の特殊な無効値含む)で、逆参照してはならない
    int* p = NULL;
  2. 値が非NULLで、逆参照しても良い
    p = new int(0);
  3. 値が非NULLで、逆参照してはならない
    delete p;

正しく管理しないと、セグメンテーション違反(access violation)の要因となる。

「ポインタはNULL初期化する」「deleteする時点でそのオブジェクトを参照しているポインタは1つだけになるようにする」「deleteしたらすぐにNULLを代入する」などといった行為を徹底し、できるだけ3番の状態を避けるようにすることで、「NULLなら逆参照してはならない」「非NULLなら逆参照しても良い」という分かりやすい状態にできる。
ただし「終端情報としてのポインタ(次項2番)」は例外で、こればかりは「逆参照してはならない非NULLポインタ」という状態を認めなければならない。

終端

ポインタが配列を指している場合に、その終端を示す情報。
以下の3種に大別される。

  1. 要素数
    int sum(const int* p, size_t size)
    {
        int result = 0;
        for (size_t i = 0; i < size; i++)
        {
            result += p[i];
        }
        return result;
    }
  2. ポインタ(反復子)
    int sum(const int* first, const int* last)
    {
        int result = 0;
        for (; first != last; ++first)
        {
            result += *first;
        }
        return result;
    }
  3. 値(0値終端)
    size_t count_lower_case(const char* string)
    {
        size_t num = 0;
        char ch;
        for (const char* p = string; ch = *p; ++p)
        {
            if (ch >= 'a' && ch <= 'z')
            {
                num++;
            }
        }
        return num;
    }

正しく管理しないと、バッファオーバーランの要因となる。

複製処理

ポインタをコピー(代入)するときにしなければならないこと。
「何もしない」「newしてディープコピー」「そもそもコピーしてはならない」「参照数を増やす」等。
正しく管理しないとメモリリークや二重解放、正当性情報の破壊(引いてはセグメンテーション違反)を引き起こす。

「何もしない」以外の、コーディングが必要な箇所が多ければ多いほど、見通しが悪くなってバグを含みやすくなる。
可能ならばスマートポインタの使用を徹底し、生ポインタの複製処理については「何もしない」で統一したいところ。

解放処理

ポインタを無効にする(NULLを代入するか、ポインタ変数の生存期間が終了する)前にしなければならないこと。
「何もしない」「free」「delete」「fclose」「参照数を減らす」等。
正しく管理しないとメモリリークや二重解放、正当性情報の破壊を引き起こす。

C++において基本的かつ重要と思う機能?』でも書いたが、これはスマートポインタ(のデストラクタ)に任せた方が良い。

ポインタのキャストについてつらつらと

タグ: C++ Boost
#include <iostream>
#include <boost/implicit_cast.hpp>
#include <boost/cstdint.hpp>

// メモリ上のデータ配置例(32bit版)
// 4byte Base1::vptr
// 4byte Base1::_a = 1
class Base1
{
public:
    Base1() : _a(1) {}
    virtual ~Base1() {}

    int _a;
};

// メモリ上のデータ配置例(32bit版)
// 4byte Base2::vptr
// 4byte Base2::_b = 2
class Base2
{
public:
    Base2() : _b(2) {}
    virtual ~Base2() {}

    int _b;
};

// メモリ上のデータ配置例(32bit版)
// 4byte Base1::vptr
// 4byte Base1::_a = 1
// 4byte Derived1::_c = 3
class Derived1 : public Base1
{
public:
    Derived1() : _c(3) {}

    int _c;
};

// メモリ上のデータ配置例(32bit版)
// (0xbffff75c) 4byte Base1::vptr
// (0xbffff760) 4byte Base1::_a = 1
// (0xbffff764) 4byte Base2::vptr
// (0xbffff768) 4byte Base2::_b = 2
// (0xbffff76c) 4byte Derived2::_d = 4
class Derived2 : public Base1, public Base2
{
public:
    Derived2() : _d(4) {}

    int _d;
};

void output(const char* n, void* p)
{
    std::cout << n << " void*:" << p << std::endl;
}

void output(const char* n, Base1* p)
{
    std::cout << n << " Base1*:" << p;
    if (p)
    {
        std::cout << " _a:" << p->_a;
    }
    std::cout << std::endl;
}

void output(const char* n, Base2* p)
{
    std::cout << n << " Base2*:" << p;
    if (p)
    {
        std::cout << " _b:" << p->_b;
    }
    std::cout << std::endl;
}

void output(const char* n, Derived1* p)
{
    std::cout << n << " Derived1*:" << p;
    if (p)
    {
        std::cout << " _a:" << p->_a << " _c:" << p->_c;
    }
    std::cout << std::endl;
}

void output(const char* n, Derived2* p)
{
    std::cout << n << " Derived2*:" << p;
    if (p)
    {
        std::cout << " _a:" << p->_a << " _b:" << p->_b << " _d:" << p->_d;
    }
    std::cout << std::endl;
}

暗黙の型変換

出来ること:

  1. 任意の型のポインタ→void*
  2. 派生クラスのポインタ→基底クラスのポインタ(アップキャスト)
  3. 整数値0→任意の型のポインタ(NULLポインタ)

もっとも安全なキャスト。
元のポインタが正常なデータを指していれば、キャスト後のポインタも正常なデータを指す。

Derived2 obj;

// 1. 任意の型のポインタ→void*
void* p = &obj;
output("1.ok", p);

// 2. 派生クラスのポインタ→基底クラスのポインタ
Base1* base1 = &obj;
output("2.ok", base1);
Base2* base2 = &obj;
output("2.ok", base2); // OK: 継承関係を考慮して、&obj(=0xbffff75c)より8バイト後ろ(=0xbffff764)が指される
1.ok void*:0xbffff75c
2.ok Base1*:0xbffff75c _a:1
2.ok Base2*:0xbffff764 _b:2

boost::implicit_cast

出来ること:

  1. 暗黙の型変換と同じ

暗黙の型変換を明示的に行うテンプレート関数。
暗黙の型変換用のポインタ変数を用意するのが面倒な時に使う。
暗黙の型変換同様、キャスト前のポインタが正常ならキャスト後のポインタも正常。

Derived2 obj;

// 1. 暗黙の型変換と同じキャスト
output("1.ok", boost::implicit_cast<void*>(&obj));
output("1.ok", boost::implicit_cast<Base1*>(&obj));
output("1.ok", boost::implicit_cast<Base2*>(&obj));
1.ok void*:0xbffff75c
2.ok Base1*:0xbffff75c _a:1
2.ok Base2*:0xbffff764 _b:2

dynamic_cast

出来ること:

  1. 実行時型情報を持つクラスのポインタのアップ/ダウン/クロスキャスト

実行時にオブジェクトの型情報を評価し、正常にキャストできないようならNULLを返す。
キャスト前のポインタが正常ならキャスト後のポインタが評価不能な異常なデータを指すことはない。NULLチェックさえ怠らなければ安全なキャスト。
実行時に一切コストが発生しない他のキャストと違い、dynamic_castによるダウンキャストやクロスキャストは実行時に型情報を評価するため少しだけ遅くなる。

Derived2 obj;

// 1. 実行時型情報を持つクラスのポインタのアップ/ダウンキャスト
Base1* base1 = dynamic_cast<Base1*>(&obj);
Base2* base2 = dynamic_cast<Base2*>(&obj);
output("1.ok", base1);
output("1.ok", base2);
output("1.ok", dynamic_cast<Derived2*>(base2));// OK: 継承関係を考慮して、base2(=0xbffff75c)より8バイト後ろ(=0xbffff764)が指される
output("1.ok", dynamic_cast<Derived1*>(base2)); // OK: 継承関係がないことを考慮して「正常に」NULLを返す
1.ok Base1*:0xbffff75c _a:1
1.ok Base2*:0xbffff764 _b:2
1.ok Derived2*:0xbffff75c _a:1 _b:2 _d:4
1.ok Derived1*:0

static_cast

出来ること:

  1. 暗黙の型変換と同じキャスト
  2. void*→任意の型のポインタ
  3. 基底クラスのポインタ→派生クラスのポインタ(ダウンキャスト)

void*からのキャストもダウンキャストも、オブジェクト本来の型と異なる型のポインタへとキャストしてしまうと異常なデータを指すことになる。

reinterpret_castと比べると

  1. 継承関係のない型のポインタへキャストできない
  2. 「ポインタのポインタ」から「ポインタ」へといったキャストはできない

ので多少はうっかりを防止できるが、それでも異常なポインタを容易に作れる危険なキャストであることは意識しなければならない。

Derived2 obj;

// 1. 暗黙の型変換と同じキャスト
output("1.ok", static_cast<void*>(&obj));
output("1.ok", static_cast<Base1*>(&obj));
output("1.ok", static_cast<Base2*>(&obj));

void* p = &obj;

// 2. void*→任意の型のポインタ
output("2.ok", static_cast<Derived2*>(p));
output("2.ok", static_cast<Base1*>(p));
output("2.ng", static_cast<Base2*>(p)); // NG: _a のデータ領域を _b のデータ領域と解釈する

Base1* base1 = &obj;
Base2* base2 = &obj;

// 3. 基底クラスのポインタ→派生クラスのポインタ
output("3.ok", static_cast<Derived2*>(base1));
output("3.ok", static_cast<Derived2*>(base2)); // OK: 継承関係を考慮して、base2(=0xbffff764)より8バイト手前(=0xbffff75c)が指される
output("3.ng", static_cast<Derived1*>(base1)); // NG: Base2::vptr のデータ領域を Derived::1_c のデータ領域と解釈する
1.ok void*:0xbffff75c
1.ok Base1*:0xbffff75c _a:1
1.ok Base2*:0xbffff764 _b:2
2.ok Derived2*:0xbffff75c _a:1 _b:2 _d:4
2.ok Base1*:0xbffff75c _a:1
2.ng Base2*:0xbffff75c _b:1
3.ok Derived2*:0xbffff75c _a:1 _b:2 _d:4
3.ok Derived2*:0xbffff75c _a:1 _b:2 _d:4
3.ng Derived1*:0xbffff75c _a:1 _c:12504

reinterpret_cast

出来ること:

  1. 整数値⇔ポインタ
  2. 任意の型のポインタから、他の任意の型のポインタへの強制キャスト

Cスタイルキャストを含むC++の全てのキャストの中で唯一、あらゆるポインタを「オブジェクトを参照するもの」ではなくただの「整数値」として扱う。
継承関係を完全に無視するため、アップキャストやダウンキャストには使えない。C言語的には一番分かりやすいキャストとも言える。
「void*を介したstatic_cast」に近いが、「ポインタのポインタ」を「ポインタ」へキャストできるという点でより危険。
が、これはこれでreinterpret_castが必要なほど危険なキャストをしていることが一目で分かるため、意外とstatic_castより間違いを見つけやすかったりする、こともある。

Derived2 obj;

// 1. 整数値⇔ポインタ
boost::int64_t n = reinterpret_cast<boost::int64_t>(&obj);
output("1.ok", reinterpret_cast<Derived2*>(n));

Base1* base1 = &obj;
Base2* base2 = &obj;

// 2. 任意の型のポインタから、他の任意の型のポインタへの強制キャスト
output("2.ok", reinterpret_cast<Base1*>(&obj));
output("2.ng", reinterpret_cast<Base2*>(&obj)); // NG: _a のデータ領域を _b のデータ領域と解釈する
output("2.ok", reinterpret_cast<Derived2*>(base1));
output("2.ng", reinterpret_cast<Derived2*>(base2)); // NG: 継承関係が考慮されずに型だけ変わる
output("2.ng", reinterpret_cast<Derived1*>(&obj)); // NG: Base2::vptr のデータ領域を Derived::1_c のデータ領域と解釈する
1.ok Derived2*:0xbffff75c _a:1 _b:2 _d:4
2.ok Base1*:0xbffff75c _a:1
2.ng Base2*:0xbffff75c _b:1
2.ok Derived2*:0xbffff75c _a:1 _b:2 _d:4
2.ng Derived2*:0xbffff764 _a:2 _b:-1606460800 _d:-1606460800
2.ng Derived1*:0xbffff75c _a:1 _c:12504

Cスタイルキャスト

static_castできるならstatic_cast。そうでないならreinterpret_cast。みたいな感じの挙動。だったと思う。たしか。

Appendix

タグ

Blog内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。