ランダムアクセス 解答ページ | Programming Place Plus 新C++編

トップページ新C++編ランダムアクセス

このページの概要

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



解答・解説

問題1 (基本★)

std::istringstream に対してもシーク操作ができることを、プログラムを書いて確認してください。


std::istringstream は、「stringstream」のページで紹介して以来、文字列の分解をおこなう用途で活用してきましたが、名称のとおり、これもストリームを扱っていると考えられます。つまり、文字列を取り扱う文字列ストリームです。そのため、これにも現在対象になっている位置という考え方があります。ファイルポジションと呼んでもいいですが、ストリームポジションというほうが適切かもしれません。いずれにしても、その位置はシーク(移動)できます。その方法は、ファイルストリームの場合と同じで、seekgメンバ関数を使うというものです(本編解説)。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    char c[4] {};

    std::string s {"abcdefghijk"};
    std::istringstream iss {s};

    iss.seekg(3, std::ios_base::beg);  // d まで移動
    std::cout << iss.tellg() << "\n";
    iss.read(c, 3);  // def を読み取る
    std::cout << c << "\n";

    iss.seekg(-2, std::ios_base::cur);  // e まで戻る
    std::cout << iss.tellg() << "\n";
    iss.read(c, 3);  // efg を読み取る
    std::cout << c << "\n";
}

実行結果:

3
def
4
efg

引数が2つの seekgメンバ関数を使うと、第2引数で指定した基準位置から、第1引数で指定した値の分だけファイルポジションが移動します。

まず、iss.seekg(3, std::ios_base::beg); により、3バイト移動します。std::ios_base::beg は「先頭」を基準にすることを表します。tellgメンバ関数で現在のファイルポジションを取得できるので、これで確認してみると 3 が得られます。また、readメンバ関数を使って実際に文字列の内容を読み取ってみれば、“def” が取れることが分かります。そして、この読み取りによって、さらに 3バイト後ろにファイルポジションがずれます。

std::istringstream の strメンバ関数は、対象の文字列全体の参照を返すものなので、ファイルポジションに関係なく、文字列全体がみえます。また、読み込みを行っているわけではないので、ファイルポジションが移動することもありません。

その後、iss.seekg(-2, std::ios_base::cur); としています。std::ios_base::cur は「現在位置」を基準にすることを表します。現在位置は g のところなので、ここから 2バイト分、先頭に向かって移動します。したがって e の位置にまで戻ったことになります。tellgメンバ関数は 4 を返していますし、3バイト分読み取った結果は “efg” です。

問題2 (基本★★)

std::stringstream では、読み書きそれぞれのファイルポジションを制御できます。プログラムを書いて確認してください。


std::stringstream は、読み書き両用の文字列ストリームです(「stringstream」のページを参照)。

ファイルストリームの場合、std::fstream が読み書き両用ですが、読み取りのファイルポジションと、書き込みのファイルポジションはいつも同じ位置になります(。std::stringstream の場合は別個になっています(本編解説)。つまり、seekg/seekpメンバ関数と、tellg/tellpメンバ関数はそれぞれ使い分けができるということです。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    char c[4] {};

    std::string s {"abcdefghijk"};
    std::stringstream ss {s};

    ss.seekg(3, std::ios_base::beg);   // 読み取りポジションを移動
    std::cout << ss.tellg() << "," << ss.tellp() << "\n";
    ss.read(c, 3);  // 3バイト読み取る
    std::cout << c << "\n";

    ss.seekp(5, std::ios_base::cur);   // 書き込みポジションを移動
    std::cout << ss.tellg() << "," << ss.tellp() << "\n";
    ss.write(c, 3);  // 3バイト書き込む
    std::cout << ss.str() << "\n";
}

実行結果:

3,0
def
6,5
abcdedefijk

seekgメンバ関数を使って読み取りのファイルポジションを移動させても、tellpメンバ関数で得られる値に変化がないこと。また反対に、seekpメンバ関数を使って書き込みのファイルポジションを移動させても、tellgメンバ関数で得られる値に変化がないことが分かります。

問題3 (応用★★)

ファイルサイズを取得する方法として、本編で解説しなかった、「1バイトずつ読み込みながらカウントすることを、末尾に到達するまで繰り返す」プログラムを作成してください。


末尾に到達したかどうかは、eofメンバ関数で判定できます(「ファイル処理」のページを参照)。

#include <cassert>
#include <cstdint>
#include <fstream>
#include <iostream>

// ファイルサイズを取得する
bool get_file_size(const std::string& path, std::uintmax_t* file_size)
{
    assert(file_size);

    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (ifs.fail()) {
        return false;
    }

    for (std::uintmax_t i {0}; ifs; ++i) {

        // 1文字読み取る
        char c {};
        ifs.read(&c, 1);

        // 末尾に達したら終了
        if (ifs.eof()) {
            *file_size = i;
            return true;
        }
    }

    return false;
}

int main()
{
    constexpr auto path = "test.bin";

    std::uintmax_t file_size {};
    if (get_file_size(path, &file_size)) {
        std::cout << file_size << "\n";
    }
    else {
        std::cerr << "Get file size error: " << path << "\n";
    }
}

test.bin(バイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

実行結果:

16

この方法は、ファイルサイズの大きさによって処理時間が変わることになりますから、性能が安定しない問題があります。また、ファイルサイズが大きすぎると、途方もない時間が掛かることは重大な問題といえます。本編での解説のとおり、stat関数や、そのほか環境が提供している方法を選ぶのが、効率的で安全です(本編解説)。

【C++17】C++17 以降ならば、std::filesystem::file_size関数を使えばいいです。


参考リンク



更新履歴




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