メモリとオブジェクト 解答ページ | Programming Place Plus 新C++編

トップページ新C++編メモリとオブジェクト

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

ローカル変数について、そのストレージ期間が自動ストレージ期間である場合と、静的ストレージ期間である場合とで、どのような違いがあるか説明してください。


まず、ローカル変数とは、関数の内側で定義された変数のことです(本編解説)。この用語は、その変数がどこで定義されたかということしか表しておらず、ストレージ期間とは無関係です。

ローカル変数のストレージ期間は、特別なキーワードを何も付加せずに定義したのなら自動ストレージ期間になり、staticキーワードを付加して定義したのなら静的ストレージ期間になります(本編解説)。前者を自動変数、後者を静的ローカル変数と呼ぶことがあります。

自動ストレージ期間を持つ場合は、寿命がその関数内に限られるため、関数を抜けた時点でそのオブジェクトがメモリ上にある保証がなくなります。それは値が保持されないことを意味しています。

#include <iostream>

void f1()
{
    int v {0};  // 自動ストレージ期間を持つため、関数を抜けるたびに消える
    ++v;
    std::cout << v << "\n";
}

int main()
{
    for (int i = 0; i < 5; ++i) {
        f1();
    }
}

実行結果:

1
1
1
1
1

また、関数を抜けたあとにオブジェクトにアクセスすることは未定義の動作であり、危険です。そもそもローカル変数なので、関数の呼び出し元から直接的にアクセスすることはできませんが、参照を返すことはできてしまいます。

#include <iostream>
#include <string>

std::string& f2(const std::string& s)
{
    std::string str {};  // 自動ストレージ期間を持つため、関数を抜けるたびに消える
    str += s;
    return str;  // 危険(戻り値が参照型であるため)
}

int main()
{
    for (int i = 0; i < 5; ++i) {
        std::string& str {f2("Hello.")};
        std::cout << str << "\n";  // str の参照先にオブジェクトはない(未定義の動作)
    }
}

静的ストレージ期間を持つ場合は、寿命がプログラムの実行中ずっと続くため、関数を抜けても値を失いません。ただ、ローカル変数ではあるので、関数を抜けた先から普通にアクセスできるわけではありません(参照型で返してやれば可能です)。

#include <iostream>
#include <string>

void f1()
{
    static int v {0};  // 静的ストレージ期間を持つため、関数を抜けても消えず、値が保持される
    ++v;
    std::cout << v << "\n";
}

std::string& f2(const std::string& s)
{
    static std::string str {};  // 静的ストレージ期間を持つため、関数を抜けても消えず、値が保持される
    str += s;
    return str;  // 可能
}

int main()
{
    for (int i = 0; i < 5; ++i) {
        f1();
    }

    for (int i = 0; i < 5; ++i) {
        std::string& str {f2("Hello.")};
        std::cout << str << "\n";  // str の参照先のオブジェクトは生存している
    }
}

実行結果:

1
2
3
4
5
Hello.
Hello.Hello.
Hello.Hello.Hello.
Hello.Hello.Hello.Hello.
Hello.Hello.Hello.Hello.Hello.

静的ローカル変数は便利に使えることもありますが、うまく制御することが難しいケースもあり、基本的には避けるのが無難です。

問題2 (基本★)

std::string の各文字がメモリ上で隙間なく連続的に並ぶことを確認してください。


配列の各要素は、メモリ上で隙間なく連続的に並ぶ保証があります(本編解説)。std::string は文字列、つまり文字の配列を表現したものなので、同じ性質を持ちます。

たとえば次のようにプログラムを書いて確かめられます。

#include <iostream>
#include <string>

int main()
{
    std::string s {"Hello, World."};

    for (std::string::size_type i = 0; i < s.length(); ++i) {
        std::cout << static_cast<void*>(&s[i]) << "\n";
    }
}

実行結果:

012FFAC8
012FFAC9
012FFACA
012FFACB
012FFACC
012FFACD
012FFACE
012FFACF
012FFAD0
012FFAD1
012FFAD2
012FFAD3
012FFAD4

実行結果は環境によって変わりますし、実行するたびにも変わるかもしれません。それでも必ず +1 ずつ増えていくような結果が得られるはずです。

オブジェクトのメモリアドレスは、&演算子を使って取得できます(本編解説)。std::string の要素の場合は、&s[i] のように記述すればいいです。at関数を使う場合は、&s.at(i) と書けます。

&演算子で得られる値の型は、オペランドの型に * を付けて表現されるポインタ型です。std::string の要素は char型なので、char*型ということになります。

std::cout で出力させるとき、char*型は特別あつかいされてしまって、思うような結果が得られません。ここでは、void*型にキャストして使っています(本編解説)。

問題3 (基本★★)

パディングが入った構造体型を std::vector の要素にしたとき、各要素のデータメンバがメモリ上にどのように配置されるか確認してください。


まず、パディングが入る構造体型を定義します。やり方は色々ですし、処理系による調整も必要ですが、short型が 2バイト、int型が 4バイトの環境では次のように書けます(ここは理解のために、色々自力で試すべきところです)。

struct S {
    short a;
    int b;
    short c;
};

この場合、S のアラインメントは、一番大きいデータメンバである b(int型)と一致するため 4 になると思われます(本編解説)。a は 2バイトしか使いませんが、b のアラインメントは 4バイトなので、調整のため a と b のあいだに 2バイトのパディングが入るはずです。また、c も 2バイトしか使わないため、末尾にも 2バイトのパディングが入るでしょう。

想定どおりなら、パディングは 4バイト加わっているので、sizeof(S) の結果は、sizeof(S::a) + sizeof(S::b) + sizeof(S::c) + 4 つまり 12 と一致するはずです。また、offsetofマクロを使って、各データメンバが先頭からどれだけ離れた位置にあるかを調べて確認するのも良いでしょう。

#include <iostream>

struct S {
    short a;
    int b;
    short c;
};

int main()
{
    S s {};
    std::cout << "sizeof(S): " << sizeof(S) << "\n"
              << "sizeof(S::a): " << sizeof(S::a) << "\n"
              << "sizeof(S::b): " << sizeof(S::b) << "\n"
              << "sizeof(S::c): " << sizeof(S::c) << "\n"
              << "offsetof(S, a): " << offsetof(S, a) << "\n"
              << "offsetof(S, b): " << offsetof(S, b) << "\n"
              << "offsetof(S, c): " << offsetof(S, c) << "\n";
}

実行結果:

sizeof(S): 12
sizeof(S::a): 2
sizeof(S::b): 4
sizeof(S::c): 2
offsetof(S, a): 0
offsetof(S, b): 4
offsetof(S, c): 8

想定どおりの結果になっています。

では、今度は std::vector<S> を定義して、各要素の配置を確認してみましょう。

#include <iostream>
#include <vector>

struct S {
    short a;
    int b;
    short c;
};

int main()
{
    std::vector<S> vec(5);
    for (std::vector<S>::size_type i = 0; i < vec.size(); ++i) {
        auto& v = vec.at(i);
        std::cout << "vec[" << i << "]\n"
                  << "  a: " << &v.a << "\n"
                  << "  b: " << &v.b << "\n"
                  << "  c: " << &v.c << "\n";
    }
}

実行結果:

vec[0]
  a: 00D9FC68
  b: 00D9FC6C
  c: 00D9FC70
vec[1]
  a: 00D9FC74
  b: 00D9FC78
  c: 00D9FC7C
vec[2]
  a: 00D9FC80
  b: 00D9FC84
  c: 00D9FC88
vec[3]
  a: 00D9FC8C
  b: 00D9FC90
  c: 00D9FC94
vec[4]
  a: 00D9FC98
  b: 00D9FC9C
  c: 00D9FCA0

それぞれの要素のデータメンバ a、b、c は 4バイトずつ離れて配置されていることが分かります。さきほど、offsetofマクロで確認したとおりの配置だということです。

そして、各要素は 12バイトずつ離れていることが分かります。これは sizeof(S) の結果と一致しています。std::vector は配列を表現したものなので、各要素は隙間なく並びます。構造体の末尾にパディングが入るのはこれが理由でした(本編解説)。


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る