10. Input and Ouput Streams

10.1. Input and output

범용적 프로그램을 만들기 위해서는 다양한 입력과 출력에 일관적이고 체계적으로 대응할 수 있어야 한다. 이 장에서는 C++에서 데이터의 입력/출력 스트림을 셋업하고 이로부터 입출력을 하는 방법을 다룬다. 프로그램이 받을 수 있는 입력은 데이 터 스트림으로 부터의 입력, 키보드로부터의 입력, GUI로부터의 입력으로 크게 3종류인데 C++ 표준 라이브러리에서는 앞의 2개만 커버하고 GUI에 대해서는 서드파티 라이브러리가 필요하다.

10.2. The I/O stream model

C++ 표준 라이브러리는 std::istream으로 입력을 다루고 std::ostream으로 출력을 다룬다. std::cin, std::cout은 그 예들 중 하나이다. 출력 스트림은 여러 값의 나열을 문자열로 바꾸고 이 문자들을 버퍼에 담았다가 어딘가로 전송한다. 입력 스트림은 어딘가에서 문자열을 받아와 이를 버퍼에 담았다가 여러 값들로 바꾼다.

10.3. Files

간단하게 보면 파일은 바이트 배열이라고 할 수 있다. 파일에 대해서 std::ostream은 메모리를 바이트 스트림으로 변환해 디스크에 쓴다. std::istream은 파일에서 바이트 스트림을 읽어 메모리로 가져온다. 이 과정에서 입출력 스트림의 역할은 파일명, 파일 열고 닫기, 바이트 읽고 쓰기에 대한 추상화를 제공하는 것이다.

10.4. Opening a file

파일 입출력 스트림은 std::ifstream, std::ofstream이다.

std::cout << "Please enter input file name: ";
std::string iname;
std::cin >> iname;
std::ifstream ist {iname};          // ist is an input stream for the file named name
if (!ist) throw std::runtime_error("can't open input file ", iname);

std::vector<Point> points;
for (Point p; ist >> p; )
    points.push_back(p);

std::cout << "Please enter name of output file: ";
std::string oname;
std::cin >> oname;
std::ofstream ost {oname};           // ost is an output stream for a file named oname
if (!ost) throw std::runtime_error("can't open output file ", oname);

for (int p : points)
    ost << '(' << p.x << ',' << p.y << ")\n";

파일 입출력 스트림은 그 스코프가 끝나면 자동으로 파일을 닫아주므로 명시적으로 close()하지 않는 것이 좋다.

std::ifstream ifs;
ifs >> foo;                                            // won’t succeed: no file opened for ifs
ifs.open(name, std::ios_base::in);         // open file named name for reading
ifs.close();                                           // close file
ifs >> bar;                                           // won’t succeed: ifs’ file was closed

10.5. Reading and writing a file

파일의 데이터가 형식화되어 있으면 체계적으로 형식에 맞춰 데이터 구조를 만들어 입출력을 수행하라.

10.6. I/O error handling

std::istream은 입력 오류를 다음의 4가지 경우로 나눠서 대처한다.

  • good() : 입력 성공.
  • eof() : 입력의 끝에 도달.
  • fail() : 예기치 않은 입력 (숫자를 입력받으려 헀는데 ‘x’을 입력받은 경우)
  • bad() : 나쁜 상황 (입력 오류로 인한 디스크 에러 등)

안타깝게도 fail()과 bad() 조건의 구별은 구현체에 따라 다르며 명확히 표준하에 정의되어 있지 않다. 하지만 bad()는 fail()에 포함된다. 이를 포함한 에러 처리 예는 다음과 같다.

void fill_vector(std::istream& ist, std::vector<int>& v, char terminator) {
    // read integers from ist into v until we reach eof() or terminator
    for (int I; ist >> I; ) v.push_back(i);
    if (ist.eof()) return;                            // fine: we found the end of file
    if (ist.bad()) throw std::runtime_error("ist is bad");       // stream corrupted; let’s get out of here!
    if (ist.fail()) {     // clean up the mess as best we can and report the problem
        ist.clear();         // clear stream state, so that we can look for terminator
        char c;
        ist >> c;              // read a character, hopefully terminator
        if (c != terminator) {                // unexpected character
            ist.unget();                               // put that character back
            ist.clear(std::ios_base::failbit);   // set the state to fail()
        }
    }
}

bad()가 발생할 경우 보통은 더 입력을 진행할 수 없으므로 예외를 던지게 할 수도 있다. 이를 통해 코드를 더 간소화시킬 수 있다.

// make ist throw if it goes bad
ist.exceptions(ist.exceptions() | std::ios_base::badbit);

void fill_vector(std::istream& ist, std::vector<int>& v, char terminator) {
    // read integers from ist into v until we reach eof() or terminator
    for (int I; ist >> I; ) v.push_back(i);
    if (ist.eof()) return;                            // fine: we found the end of file
    // not good() and not bad() and not eof(), ist must be fail()
    ist.clear();                       // clear stream state

    char c;
    ist >> c;                            // read a character, hopefully terminator

    if (c != terminator) {     // ouch: not the terminator, so we must fail
        ist.unget();                    // maybe my caller can use that character
        ist.clear(ios_base::failbit);        // set the state to fail()
    }
}

출력 스트림에 대해서도 똑같이 체크를 할 수 있지만 입력 스트림에 비해서는 그 의미가 적다.

10.7. Reading a Single Value

하나의 자리수를 입력받는 프로그램을 간단히 짜보면 다음과 같을 것이다.

std::cout << "Please enter an integer in the range 1 to 10 (inclusive):\n";
int n = 0;
while (std::cin >> n && !(1 <= n && n <= 10))      // read and check range
    std::cout << "Sorry " << n << " is not in the [1:10] range; please try again\n";
// ... use n here ...

그러나 이는 나쁜 입력을 받았을 때 제대로 대처하지 못한다. 범위 밖의 입력, 없는 값 (파일의 끝 문자), 정수가 아닌 입력 등에 대해. 이런 경우에 대해 문제를 해결하는 방법은 크게 3가지이다: 문제를 무시하거나, 예외를 던져서 다른 곳에 문제를 위임하거나, 문제를 읽는 시점에 해결하거나. 문제를 무시하는 것은 좋은 방법이 아니다. 나머지 2가지 방법 중 어느 것이 더 나은지는 상황에 따라 다르다.

10.7.1. Breaking the problem into managable parts

범위 밖 입력과 정수가 아닌 입력을 대처해보자. 논리적으로 다른 부분은 함수로 따로 빼 놓으면 더 좋을 것이다.

void skip_to_int() {
    if (std::cin.fail()) {              // we found something that wasn’t an integer
        std::cin.clear();       // we’d like to look at the characters
        for (char ch; std::cin >> ch; ) {      // throw away non-digits
            if (std::isdigit(ch) || ch == "-") {
                std::cin.unget();         // put the digit back so that we can read the number
                return;
            }
        }
    }
    throw std::runtime_error("no input");                         // eof or bad: give up
}

int get_int() {
    int n = 0;
    while (true) {
        if (std::cin >> n) return n;
        std::cout << "Sorry, that was not a number; please try again\n";
        skip_to_int();
    }
}

int get_int(int low, int high) {
    std::cout << "Please enter an integer in the range " << low << " to " << high << " 
 (inclusive):\n";
    while (true) {
        int n = get_int();
        if (low <= n && n <= high) return n;
        std::cout << "Sorry " << n << " is not in the [" << low << ':' << high
                  << "] range; please try again\n";
    }
}

이후에 어딘가에서 예외를 처리하는 것도 잊지 말라.

10.7.2. Separate dialog from function

메시지를 함수와 분리하는 것도 좋다.

int get_int(int low, int high, const std::string& greeting, const std::string& sorry) {
    std::cout << greeting << ": [" << low << ':' << high << "]\n";
    while (true) {
        int n = get_int();
        if (low <= n && n <= high) return n;
        std::cout << sorry << ": [" << low << ':' << high << "]\n";
    }
}

10.8. User-defined output operators

출력 연산자 <<를 사용자 정의 클래스에 지정할 수 있다. 이는 다음과 같이 쓸 수 있다.

std::ostream& operator<< (std::ostream& os, const Date& d) {
    return os << '(' << d.year() << ',' << d.month() << ',' << d.day() << ')';
}

Date d1, d2;
std::cout << d1 << d2;

10.9. User-defined input operators

입력 연산자 >>를 지정하는 것은 조금 더 복잡하다. 잘못된 입력에 대처해야 하기 때문이다.

std::istream& operator>> (std::istream& is, Date& dd) {
    int y = 0, m = 0, d = 0;
    char ch1, ch2, ch3, ch4;
    is >> ch1 >> y >> ch2 >> m >> ch3 >> d >> ch4;
    if (!is) return is;
    if (ch1 != '(' || ch2 != ',' || ch3 != ',' || ch4 != ')') {        // oops: format error
        is.clear(std::ios_base::failbit);
        return is;
    }
    dd = Date {y, Date::Month(m), d};                       // update dd
    return is;
}

잘못된 형식에 대한 입력은 std::ios_base::failbit을 세팅해 대처하고, 잘못된 날짜의 입력은 Date의 생성자에서 대처하고 있다.

10.10. A standard input loop

종료 문자를 둬서 종료를 시킬 때 해당 입력을 예외로 처리하게 할 수 있다.

// somewhere: make ist throw if it goes bad:
ist.exceptions(ist.exceptions()|std::ios_base::badbit);

void end_of_loop(std::istream& ist, char term, const std::string& message) {
    if (ist.fail()) {                // use term as terminator and/or separator
        ist.clear();
        char ch;
        if (ist >> ch && ch==term) return;             // all is fine
        throw std::runtime_error(message);
    }
}

for (My_type var; ist >> var; ) {                                     // read until end of file
    // maybe check that var is valid
    // . . . do something with var . . .
}
end_of_loop(ist,'|',"bad termination of file");        // test if we can continue
// carry on: we found end of file or a terminator

10.11. Reading a structured file

년, 월, 일, 시각, 온도를 담은 형식화된 온도 데이터를 입력받고 싶다고 하자. 다음과 같은 포맷이라 하자.

{ year 1990 }
{year 1991 { month jun }}
{ year 1992 { month jan ( 1 0 61.5) }  {month feb (1 1 64) (2 2 65.2) } }
{year 2000
          { month feb (1 1 68 ) (2 3 66.66 ) ( 1 0 67.2)}
          {month dec (15 15 –9.2 ) (15 14 –8.8) (14 0 –2) }
}

이를 입력받기 위해서는 적절한 구조체들을 만들어야 할 것이다.

10.11.1. In-memory representation

년도, 월, 날짜를 담을 핵심 클래스가 필요하다.

constexpr int not_a_reading = -7777; // less than absolute zero
constexpr int not_a_month = –1;

struct Day {
    std::vector<double> hour { std::vector<double>(24, not_a_reading)};
};

struct Month {                                         // a month of temperature readings
    int month {not_a_month};         // [0:11] January is 0
    std::vector<Day> day {32};                // [1:31] one vector of readings per day
};

struct Year {               // a year of temperature readings, organized by month
    int year;                                         // positive == A.D.
    std::vector<Month> month {12};    // [0:11] January is 0
};

32, 12 등의 매직 넘버를 몇 개 사용하였으나 실제로 상수는 심볼화해서 사용하는 것이 좋다.

10.11.2. Reading structured values

읽기 클래스를 만들고 읽기 연산자를 만들어 보자.

struct Reading {
    int day;
    int hour;
    double temperature;
};

std::istream& operator>> (std::istream& is, Reading& r) {
// read a temperature reading from is into r
// format: ( 3 4 9.7 )
// check format, but don’t bother with data validity
    char ch1;
    if (is >> ch1 && ch1 != '(') {                                // could it be a Reading?
        is.unget();
        is.clear(std::ios_base::failbit);
        return is;
    }

    char ch2;
    int d = 0;
    int h = 0;
    double t = 0.0;
    is >> d >> h >> t >> ch2;
    if (!is || ch2 != ')') throw std::runtime_error("bad reading");      // messed-up reading
    r.day = d;
    r.hour = h;
    r.temperature = t;
    return is;
}

std::istream& operator>> (std::istream& is, Month& m) {
// read a month from is into m
// format: { month feb . . . }
    char ch = 0;
    if (is >> ch && ch != '{') {
        is.unget();
        is.clear(std::ios_base::failbit);                  // we failed to read a Month
        return is;
    }

    std::string month_marker;
    std::string mm;
    is >> month_marker >> mm;
    if (!is || month_marker != "month") throw std::runtime_error("bad start of month");
    m.month = month_to_int(mm);
    int duplicates = 0;
    int invalids = 0;
    for (Reading r; is >> r; ) {
        if (is_valid(r)) {
            if (m.day[r.day].hour[r.hour] != not_a_reading) ++duplicates;
            m.day[r.day].hour[r.hour] = r.temperature;
        } else {
            ++invalids;
        }
    if (invalids) throw std::runtime_error("invalid readings in month", invalids);
    if (duplicates) throw std::runtime_error("duplicate readings in month", duplicates);
    end_of_loop(is,'}',"bad end of month");
    return is;
}

constexpr int implausible_min = –200;
constexpr int implausible_max = 200;

bool is_valid(const Reading& r) {
// a rough test
    if (r.day < 1 || 31 < r.day) return false;
    if (r.hour < 0 || 23 < r.hour) return false;
    if (r.temperature < implausible_min || implausible_max < r.temperature) return false;
    return true;
}

std::istream& operator>> (std::istream& is, Year& y) {
// read a year from is into y
// format: { year 1972 . . . }
    char ch;
    is >> ch;
    if (ch!='{') {
        is.unget();
        is.clear(std::ios::failbit);
        return is;
    }

    std::string year_marker;
    int yy = 0;
    is >> year_marker >> yy;
    if (!is || year_marker != "year") throw std::runtime_error("bad start of year");
    y.year = yy;

    while(true) {
        Month m;            // get a clean m each time around
        if(!(is >> m)) break;
        y.month[m.month] = m;
    }
    end_of_loop(is,'}',"bad end of year");
    return is;
}

이는 다음과 같이 활용한다.

// open an input file:
std::cout << "Please enter input file name\n";
std::string iname;
std::cin >> iname;
std::ifstream ist {iname};
if (!ifs) throw std::runtime_error("can't open input file", iname);

ifs.exceptions(ifs.exceptions() | std::ios_base::badbit);       // throw for bad()

// open an output file:
std::cout << "Please enter output file name\n";
std::string oname;
std::cin >> oname;
std::ofstream ost {oname};
if (!ofs) throw std::runtime_error("can't open output file", oname);

// read an arbitrary number of years:
std::vector<Year> ys;
while(true) {
    Year y;                  // get a freshly initialized Year each time around
    if (!(ifs >> y)) break;
    ys.push_back(y);
}
std::cout << "read " << ys.size() << " years of readings\n";

for (const Year& y : ys) print_year(ofs, y);

10.11.3. Changing representations

달을 Jan, Feb 등으로 표현하려면 1, 2, … 등 숫자에 대한 표현 대응을 만들어주어야 한다.

std::vector<std::string> month_print_tbl = {
          "January", "February", "March", "April", "May", "June", "July",
          "August", "September", "October", "November", "December"
};

std::string int_to_month(int i) {
// months [0:11]
    if (i < 0 || 12 <= i) throw std::runtime_error("bad month index");
    return month_print_tbl[i];
}

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중