본문 바로가기
Topos Uranos

요약

이 수업은 텍스트와 파일을 다루는 강좌의 일곱째 단원을 마무리한다. 아무 경고 없이 데이터를 잃고 ASCII 밖의 문자가 들어간 이름을 잘못 재던 도입 수업의 프로그램을 배운 것으로 다시 쓴다. 문자열에서 파일 형식으로 이어지는 개념의 사슬을 다시 세운다. 그 가운데 어느 언어에서나 통하는 것과 C++에 고유한 것을 나눈다. 단원의 정의되지 않은 동작을, 새니타이저가 멈추는 것과 아무도 보지 못하는 것과 함께 모은다. 마무리 평가에서 입문 평가의 능력을 되풀이한다. 그리고 단원 전체를 엮는 여섯 도전 과제로 끝난다.

학습 목표

  1. 실제 텍스트를 그 인코딩과 함께 처리한다. 문자열의 바이트 수를 예측하고, 그 문자를 세고, UTF-8을 깨뜨리는 연산을 피한다.
  2. 입력과 파일을 견고하게 읽으며, 잘못된 데이터와 데이터의 끝을 구별하고 각 데이터를 완전한 텍스트로서 검증한다.
  3. 지정된 형식으로 결과 파일을 만들고, 잘못된 줄을 하나하나 그 번호와 함께 보고한다.
  4. 단원의 정의되지 않은 동작(댕글링 뷰, 문자열 밖의 인덱스)을 알아보고, 마찬가지로 조용히 지나가는 정의된 실패와 구별한다.
  5. 단원에서 배운 것 가운데 어느 언어에서나 통하는 것과 C++에 고유한 것을 나눈다.

단원에서 이룬 것

이 단원은 텍스트를 데이터로 다루는 법을 가르치고, 형식이 잘못된 입력 앞에서도 무너지지 않고 파일을 읽고 쓰는 프로그램을 만드는 것을 목표로 삼았다. 그 목표는 도입 수업이 이미 안다고 선언한 것에만 기대어 이루어졌다. 2단원의 ‘코드로서의 문자’, 3단원의 비트, 6단원의 벡터와 레코드, 그리고 5단원의 계약을 가진 함수이다. >> 연산자로 읽는 바람에 남궁 지은과 서준을 잃고 ‘민수’가 ‘Mina’보다 길다고 판정하던 성적 목록 프로그램은 이제 이렇게 쓴다. 파일의 줄을 읽고, 마지막 공백에서 점수를 떼어 내고, 그 점수를 parseInt 함수(입력을 제대로 읽기 수업의 함수)로 검증하고, 잘못된 줄을 오류 스트림으로 보고하고, 이름을 문자 수로 재고, 목록을 다른 파일에 남긴다. 예제가 그 자체로 완결되도록 프로그램은 먼저 데이터 파일을 쓴다. 실제로는 그 파일이 이미 있을 것이다.

C++ 프로그램
#include <charconv>#include <cstddef>#include <fstream>#include <iostream>#include <print>#include <string>#include <string_view>​// 텍스트가 정확히 하나의 정수인지 판정한다.// 사전 조건: 없음.// 사후 조건: ‘입력을 제대로 읽기’ 수업의 것과 같다.bool parseInt(std::string_view text, int& value){    int result{};    const auto last{text.data() + text.size()};    const auto [stop, error]{std::from_chars(text.data(), last, result)};    if (error != std::errc{} || stop != last) {        return false;    }    value = result;    return true;}​// UTF-8 텍스트의 문자 수.// 사전 조건: text는 올바른 UTF-8이다.// 사후 조건: text의 문자 수(바이트 수가 아니다)를 돌려준다.std::size_t letterCount(std::string_view text){    std::size_t count{0};    for (const char c : text) {        if ((c & 0xC0) != 0x80) {            ++count;        }    }    return count;}​int main(){    {        std::ofstream out{"grades.txt"};        std::println(out, "Mina 65\n민수 70\n남궁 지은 58\n서준 여섯\n하은 61");    }    std::ifstream in{"grades.txt"};    std::ofstream report{"report.txt"};    if (!in || !report) {        std::println(std::cerr, "파일들을 열 수 없습니다");        return 1;    }    std::string line{};    std::string longest{};    int lineNumber{0};    while (std::getline(in, line)) {        ++lineNumber;        const std::size_t space{line.rfind(' ')};        int grade{};        if (space == std::string::npos || !parseInt(std::string_view{line}.substr(space + 1), grade)) {            std::println(std::cerr, "{}번째 줄이 잘못됨: ‘{}’", lineNumber, line);            continue;        }        const std::string name{line.substr(0, space)};        std::println(report, "{:<10}{:>3}", name, grade);        if (letterCount(name) > letterCount(longest)) {            longest = name;        }    }    std::println("가장 긴 이름: {} ({}자)", longest, letterCount(longest));}
출력
4번째 줄이 잘못됨: ‘서준 여섯’가장 긴 이름: 남궁 지은 (5자)

화면에는 오류 메시지와 결론만 나타난다. 목록은 파일에 있으며, 프로그램을 실행한 뒤 cat 명령(PowerShell에서는 Get-Content 명령)으로 볼 수 있다.

터미널
$ cat report.txtMina       65민수       70남궁 지은  58하은       61

각 부분은 도입 수업의 결함 하나에 답한다. 줄 단위 읽기와 rfind 함수 덕분에 공백이 든 이름을 다룰 수 있다. npos 값과 parseInt 함수는 서준의 줄을 거부하고, 그 줄을 번호와 함께 보고하고, 하은의 줄로 넘어가게 한다. letterCount 함수는 문자를 세므로, 다섯 문자에 열세 바이트인 ‘남궁 지은’이 정당하게 이긴다. 그리고 report.txt 파일은 main 함수가 끝나 그 스트림이 닫힐 때 완전해진다.

문자열

문자열 수업은 std::string 타입을 텍스트 연산을 가진 char 값의 시퀀스로 소개했다. find 함수로 검색하는 법(위치나 npos 값을 돌려준다), 그 값을 언제나 명시적으로 비교하는 법, substr 함수로 추출하는 법(유효한 위치를 요구한다)을 가르쳤고, 매개변수로는 안전하지만 관찰하는 텍스트보다 오래 살아남으면 위험한 std::string_view 타입과, 대비로서 C 문자열을 소개했다.

텍스트는 ASCII가 아니다

텍스트는 ASCII가 아니다 수업은 문자와 그 코드 포인트와 그 바이트를 구별하고, UTF-8이 비트를 한 바이트에서 네 바이트까지에 어떻게 나누어 담는지 설명했다. 그로부터 size 함수가 바이트를 센다는 것, 문자는 (c & 0xC0) != 0x80 조건으로 센다는 것, 바이트 단위로 뒤집거나 대문자로 바꾸거나 자르면 텍스트가 깨지고 검색과 연결은 깨뜨리지 않는다는 것, 그리고 표준 라이브러리가 하지 않는 일이 무엇인지를 끌어냈다.

입력을 제대로 읽기

입력을 제대로 읽기 수업은 입력을 신뢰할 수 없는 경계로 다루었다. std::getline 함수로 줄을 읽는 법, fail() 함수와 eof() 함수로 잘못된 데이터와 담기지 않는 수와 데이터의 끝을 구별하는 법, clear 함수와 ignore 함수로 복구하는 법, 그리고 std::from_chars 함수로 수를 텍스트로서 검증하되 텍스트 전체가 소비되기를 요구하는 법을 가르쳤고, 계약을 가진 경계 함수인 parseInt 함수와 readInt 함수로 마무리했다.

텍스트 파일

텍스트 파일 수업은 파일이 스트림이며 std::cin 객체와 같은 상태를 가진다는 것을 보였다. 열림을 확인하는 일을 규칙으로 삼았고, while (!in.eof()) 반복문이 왜 데이터를 하나 더 처리하는지 증명했고, std::println(out, …) 호출로 쓰는 법(대체하거나 추가하거나)과 std::cerr 스트림으로 메시지와 결과를 나누는 법을 가르쳤으며, 모든 것을 지정되고 검증된 줄 형식으로 이끌었다.