본문으로 건너뛰기

안동민 개발노트

본문 시작

텍스트 파일 쓰기

std::ofstream으로 문자열과 숫자를 기록하고 덮어쓰기·추가 모드, 버퍼 반영 시점과 쓰기 오류를 관리합니다.

반대로, 프로그램에서 생성된 데이터를 텍스트 파일에 저장하는 방법을 다룹니다.

텍스트 파일에 데이터를 쓰는 것은 사용자 설정 저장, 로그 기록, 결과 출력 등 많은 애플리케이션에서 필수적인 기능입니다.

이 장에서는 std::ofstream을 사용하여 텍스트 파일에 문자열과 숫자를 쓰는 방법, 파일 열기 모드, 출력 버퍼를 다룹니다.

std::ios::outstd::ios::app의 차이, 버퍼에서 파일로 반영되는 시점, fail()/bad() 점검 위치를 한 번에 정리한 다이어그램입니다.

C++ 호출 코드가 ofstream을 모드와 함께 열고, 서식 출력으로 스트림 버퍼에 문자를 보낸 뒤, 명시적으로 닫고 상태 비트를 확인하는 순서를 보여 주는 시퀀스

C++ · ofstream · sequence

쓰기는 close() 뒤 상태 확인까지 완료되어야 한다

모드가 파일의 수명을 정하고, 출력 연산은 먼저 스트림 버퍼를 거칩니다. 중요한 출력은 명시적으로 닫은 뒤 bad()fail()을 확인해야 마지막 동기화에서 드러난 오류까지 잡을 수 있습니다.

ofstream 열기부터 닫기 상태 확인까지의 출력 시퀀스 호출 코드가 경로와 모드로 ofstream을 열고 연결 여부를 확인한다. 삽입 연산은 값을 문자로 서식화해 스트림 버퍼로 보내며 app 모드이면 매 쓰기 전에 파일 끝을 찾는다. 중요한 출력은 close로 마지막 동기화를 요청한 뒤 bad와 fail 상태를 확인한다. OPT [mode == app] open(path, mode) associated / failed operator<<(value, '\n') seek end formatted characters close() final sync sync result close result bad() / fail() state bits 호출 코드 std::ofstream mode · state std::filebuf buffer · controlled sequence
  1. 파일 정책을 먼저 고른다

    out은 기본적으로 기존 내용을 교체하고, app은 매 쓰기 전에 파일 끝으로 이동합니다.

  2. 연결 여부를 확인한다

    is_open()은 파일 연결 여부를, 스트림의 불리언 상태는 현재 오류 비트를 확인합니다.

  3. 기본 줄바꿈은 '\n'

    operator<<가 값을 문자로 서식화해 버퍼에 보내며, 필요한 경계에서만 flushendl을 사용합니다.

  4. 중요한 출력은 명시적으로 닫는다

    close()가 마지막 버퍼 동기화를 수행하게 해 소멸자 뒤에는 확인할 수 없는 오류를 앞에서 드러냅니다.

  5. 닫은 뒤 상태를 판정한다

    bad()는 심각한 I/O 오류를, fail()failbit 또는 badbit를 보고합니다.

app ≠ ate

app은 매 쓰기, ate는 열 때 한 번

ate로 처음 끝에 놓인 뒤에는 위치를 바꿀 수 있지만, app은 각 쓰기 직전에 끝 위치를 강제합니다.

binary

텍스트 변환을 끌 뿐 서식 출력을 바꾸지 않는다

binary여도 operator<<는 문자 표현을 만듭니다. 원시 바이트 블록은 write()로 보냅니다.

sync

'\n'과 동기화 요구를 분리한다

보통은 '\n'으로 줄만 바꾸고, 외부 관찰이나 프로토콜 경계에서만 std::flush 또는 std::endl을 사용합니다.

state

원인 복구 없는 clear()는 재시도를 고치지 않는다

상태 비트만 지우기 전에 권한·공간·장치 문제를 해결하거나 출력 대상을 바꿔야 합니다. 닫기 뒤 실패 상태도 반드시 보고합니다.

flush(), std::endl, close()는 C++ 스트림 버퍼를 연결된 파일 장치와 동기화하는 경계입니다. 운영체제·장치 캐시를 넘어 물리 디스크에 영속되었다는 보장은 별도입니다.


std::ofstream로 텍스트 파일 열기

텍스트 파일에 데이터를 쓰기 위해서는 std::ofstream 클래스의 객체를 사용합니다.

ofstreamOutput File Stream의 약자입니다.

ofstream 객체를 생성할 때 파일 이름을 인자로 전달하여 바로 열거나, 객체 생성 후 open() 멤버 함수를 호출하여 열 수 있습니다.

텍스트 파일 열기 예제
#include <fstream>  // std::ofstream을 위해
#include <iostream> // std::cerr를 위해

int main() {
    // 방법 1: 생성과 동시에 파일 열기
    std::ofstream outFile1("output1.txt");
    if (!outFile1.is_open()) { // 파일 연결 여부 확인
        std::cerr << "output1.txt를 열 수 없습니다!\n";
    } else {
        // 파일 처리...
        outFile1.close();
    }

    // 방법 2: 객체 생성 후 open() 호출
    std::ofstream outFile2;
    outFile2.open("output2.txt");
    if (!outFile2) { // 스트림 객체 자체를 bool 컨텍스트에서 사용하여 성공 여부 확인 가능
        std::cerr << "output2.txt를 열 수 없습니다!\n";
    } else {
        // 파일 처리...
        outFile2.close();
    }

    return 0;
}

std::ofstream은 기본적으로 std::ios::out 모드로 파일을 엽니다.

이 모드는 다음과 같은 특징을 가집니다.

  • 지정된 이름의 파일이 존재하지 않으면 새로 생성합니다.
  • 지정된 이름의 파일이 이미 존재하면, 그 파일의 모든 내용을 지우고(truncate) 새로운 내용을 씁니다. (주의!)

파일 쓰기 (<< 연산자)

콘솔에 std::cout을 사용하여 데이터를 출력하듯이, 파일 스트림에서도 << (삽입) 연산자를 사용하여 데이터를 파일에 쓸 수 있습니다.

다양한 데이터 타입(문자열, 정수, 실수 등)을 파일에 쓸 수 있습니다. 일반적인 줄바꿈에는 \n을 사용하고, 개행과 버퍼 동기화가 함께 필요한 경계에서만 std::endl을 사용합니다.

다양한 데이터 쓰기
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ofstream outFile("my_data.txt"); // my_data.txt 파일을 쓰기 모드로 엶

    if (!outFile.is_open()) {
        std::cerr << "오류: my_data.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    // 문자열 쓰기
    outFile << "Hello, File World!\n"; // '\n'은 개행 문자
    outFile << "C++ 파일 입출력 예제입니다.\n";

    // 숫자 쓰기
    int age = 30;
    double temperature = 25.7;
    outFile << "나이: " << age << "세\n";
    outFile << "오늘의 온도: " << temperature << "도\n";

    // 여러 값 한 줄에 쓰기
    std::string product = "Laptop";
    int quantity = 5;
    double price = 1200.50;
    outFile << product << ", " << quantity << ", " << price << '\n';

    // 중요한 출력은 close()가 보고하는 동기화 오류까지 확인
    outFile.close();
    if (!outFile) {
        std::cerr << "오류: my_data.txt 기록을 완료하지 못했습니다.\n";
        return 1;
    }

    std::cout << "데이터가 'my_data.txt' 파일에 기록되었습니다.\n";
    return 0;
}

위 코드를 실행하면, my_data.txt 파일이 생성되거나 기존 내용이 지워지고 다음과 같은 내용이 기록됩니다.

my_data.txt
Hello, File World!
C++ 파일 입출력 예제입니다.
나이: 30세
오늘의 온도: 25.7도
Laptop, 5, 1200.5

파일 열기 모드 (Open Modes)의 중요성

텍스트 파일 쓰기는 열기 모드, 상태 플래그, 읽기·쓰기 단위, 닫기 책임으로 확인합니다.

ofstream 쓰기 결과의 증상별로 열기 모드, 파일 연결, 서식 출력, 스트림 버퍼, 상태 비트를 확인하고 안전한 조치와 완료 조건을 선택하는 진단 표

C++ · output stream · diagnostic table

증상에서 모드·버퍼·상태 순으로 원인을 좁힌다

같은 operator<<라도 모드와 동기화 경계에 따라 결과가 달라집니다. 먼저 파일 수명 정책을 확인하고, 서식과 버퍼를 본 뒤, 쓰기와 close() 이후 상태로 완료 여부를 판정합니다.

증상·요구 × 먼저 확인할 위치 × 의미 × 조치와 완료 조건
증상·요구 먼저 확인 판정 조치·완료 확인
기존 내용이 사라짐 out · trunc 기본 ofstream(path) 출력은 기존 파일을 새 결과로 교체합니다. 로그처럼 보존해야 한다면 app을 명시하고, 중복 기록 정책도 함께 정합니다.
위치를 옮겨도 끝에 추가됨 appate app은 매 쓰기 직전에 끝을 찾고, ate는 열 직후 한 번만 끝에 놓습니다. 항상 추가할 때만 app을 쓰고, 이후 위치 변경이 필요하면 의도에 맞는 다른 모드 조합을 고릅니다.
binary인데 숫자가 문자로 기록됨 binary · operator<< binary는 텍스트 변환을 끌 뿐, 삽입 연산의 숫자 서식화를 원시 바이트로 바꾸지 않습니다. 문자 표현은 operator<<, 메모리의 바이트 블록은 크기와 형식을 정한 뒤 write()로 기록합니다.
파일에 바로 보이지 않음 '\n' · flush · endl '\n'은 줄만 바꾸며, flushendl은 C++ 스트림 버퍼 동기화를 요청합니다. 기본 줄바꿈은 '\n'으로 두고, 외부 관찰 경계에서만 동기화합니다. 물리 디스크 영속성은 별도 수단이 필요합니다.
열기 직후 출력할 수 없음 is_open() · 스트림 상태 is_open()은 파일 연결 여부를, fail()failbit 또는 badbit 상태를 말합니다. 경로와 권한을 고친 뒤 다시 열고, 연결되지 않은 스트림에는 쓰기를 시작하지 않습니다.
쓰기 또는 닫기 실패 bad()fail()close() 공간 부족·파일 시스템·장치 오류는 쓰기 도중이나 마지막 닫기에서 드러날 수 있습니다. fail()badbit도 포함합니다. 중요한 출력은 명시적으로 닫고 다시 상태를 확인합니다. 원인을 복구하지 않은 clear()로 재시도하지 않습니다.
줄·구분자·숫자 형식이 깨짐 newline · separator · precision 쓰기 코드와 읽기 코드가 서로 다른 형식 계약을 가정했을 수 있습니다. 줄바꿈·구분자·정밀도를 한 형식으로 고정하고, 성공한 close() 뒤 결과를 소비자가 다시 읽어 검증합니다.
내용 삭제

기존 내용이 사라졌다면 out / trunc

기본 출력은 기존 파일을 새 결과로 교체합니다. 로그라면 app을 명시하고 중복 정책도 정합니다.

append

app은 매 쓰기, ate는 열 때 한 번

위치를 바꿔도 끝에 붙는다면 app의 매 쓰기 전 끝 이동이 원인입니다.

binary

binaryoperator<<를 raw bytes로 바꾸지 않는다

문자 서식은 삽입 연산, 크기와 형식을 정한 바이트 블록은 write()로 기록합니다.

buffer

기본 줄바꿈은 '\n', 동기화는 의도한 경계만

flushendl은 스트림 버퍼를 동기화하지만 물리 디스크 영속성까지 보장하지 않습니다.

open

is_open()은 연결, fail()은 상태

경로와 권한을 고친 뒤 다시 열고, 연결되지 않은 스트림에는 출력을 시작하지 않습니다.

write

쓰기와 close() 뒤 상태를 모두 확인

bad()는 심각한 I/O 오류이며 fail()badbit도 포함합니다. 원인 복구 없는 clear()는 제외합니다.

format

newline · separator · precision

쓰기와 읽기가 같은 형식 계약을 쓰도록 고정하고, 성공한 close() 뒤 소비자가 다시 읽어 검증합니다.

완료 조건

열기 성공은 쓰기 성공이 아니다

쓰기 중 상태와 명시적 close() 뒤 상태가 모두 정상이어야 중요한 출력의 C++ 스트림 처리가 완료됩니다.

복구 조건

clear()는 원인을 제거하지 않는다

상태 비트만 지우기 전에 권한·공간·장치 문제를 해결하거나 다른 출력 대상으로 전환하는 명시적 복구가 필요합니다.

진단 순서는 “파일 수명 정책 → 연결 여부 → 서식과 버퍼 → 쓰기·닫기 상태”입니다. 이 순서를 지키면 내용 삭제, 지연된 관찰, 형식 불일치, 뒤늦은 I/O 실패를 서로 다른 원인으로 분리할 수 있습니다.

std::ios::out 모드는 기본적으로 파일의 내용을 지우고 새롭게 쓰기 시작합니다.

만약 기존 파일의 내용을 유지하면서 새로운 데이터를 추가하고 싶다면, std::ios::app 모드를 사용해야 합니다.

  • std::ios::out (기본값): 파일을 쓰기 모드로 열고, 기존 내용이 있으면 모두 지웁니다.
  • std::ios::app: 매 쓰기 연산 전에 파일 끝으로 이동하여 추가합니다. 기존 내용은 보존됩니다.
  • std::ios::ate: 파일을 연 직후 한 번만 끝으로 이동합니다. 이후 위치를 바꿀 수 있으므로, 매 쓰기를 끝에 강제하는 app과 다릅니다.
  • std::ios::binary: 구현의 텍스트 변환을 끄는 모드입니다. 서식 출력인 operator&lt;&lt;를 원시 바이트 출력으로 바꾸지는 않으며, 바이트 블록은 write()로 기록합니다.
파일 끝에 데이터 추가
#include <iostream>
#include <fstream>
#include <string>
#include <chrono> // 현재 시간을 위해
#include <ctime> // std::localtime을 위해
#include <iomanip> // std::put_time을 위해

int main() {
    std::ofstream logFile;
    // std::ios::app 모드로 파일 열기. 파일이 없으면 생성, 있으면 끝에 추가
    logFile.open("application_log.txt", std::ios::app);

    if (!logFile.is_open()) {
        std::cerr << "오류: application_log.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    // 현재 시간을 가져와서 로그에 포함
    auto now = std::chrono::system_clock::now();
    auto in_time_t = std::chrono::system_clock::to_time_t(now);
    const std::tm* localTime = std::localtime(&in_time_t);
    if (localTime == nullptr) {
        std::cerr << "오류: 로컬 시간을 변환할 수 없습니다.\n";
        return 1;
    }

    // 시간 형식을 지정하여 출력
    // std::put_time은 C++11부터 사용 가능하며, <iomanip> 헤더 필요
    logFile << std::put_time(localTime, "%Y-%m-%d %H:%M:%S") << ": ";

    logFile << "애플리케이션이 실행되었습니다.\n";
    logFile << std::put_time(localTime, "%Y-%m-%d %H:%M:%S") << ": ";
    logFile << "사용자 'Admin'이 로그인했습니다.\n";

    logFile.close();
    if (!logFile) {
        std::cerr << "오류: 로그 쓰기 또는 닫기에 실패했습니다.\n";
        return 1;
    }

    std::cout << "로그가 'application_log.txt'에 추가되었습니다.\n";
    return 0;
}

위 코드를 여러 번 실행해 보면, application_log.txt 파일의 내용이 계속해서 늘어나는 것을 확인할 수 있습니다.


출력 버퍼와 flush()

파일 스트림은 효율적인 데이터 전송을 위해 버퍼링(Buffering)을 사용합니다.

데이터를 파일에 바로 쓰지 않고, 일단 메모리의 임시 저장 공간(버퍼)에 모아둡니다.

버퍼가 가득 차거나, std::endl을 사용하거나, flush() 멤버 함수를 호출하거나, 파일을 닫을 때(스트림 객체 소멸 시) C++ 스트림 버퍼가 연결된 파일 장치와 동기화를 시도합니다.

  • std::endl: 개행 문자를 삽입하고 버퍼를 flush()합니다.
  • \n: 단순히 개행 문자만 삽입하고 버퍼를 flush()하지 않습니다.
  • std::flush: 스트림 버퍼를 연결된 파일 장치와 즉시 동기화하도록 요청합니다.

flush(), std::endl, close()는 C++ 스트림 버퍼의 동기화 경계입니다. 운영체제나 저장 장치의 캐시를 넘어 물리 디스크에 영속되었다는 보장까지 제공하지는 않습니다.

flush() 사용 예제
#include <iostream>
#include <fstream>
#include <string>
#include <thread> // std::this_thread::sleep_for
#include <chrono> // std::chrono::seconds

int main() {
    std::ofstream outFile("buffered_output.txt");

    if (!outFile.is_open()) {
        std::cerr << "오류: buffered_output.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    outFile << "이것은 첫 번째 줄입니다.\n"; // '\n'만 사용, flush 안됨
    std::cout << "버퍼에만 쓰여졌을 수 있습니다. 잠시 대기...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기

    // 버퍼의 내용을 강제로 파일에 기록
    outFile.flush();
    std::cout << "스트림 버퍼 동기화를 요청했습니다. 잠시 대기...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기

    outFile << "이것은 두 번째 줄입니다." << std::endl; // 의도적인 개행 + flush
    std::cout << "std::endl로 스트림 버퍼 동기화를 요청했습니다.\n";

    outFile.close(); // 닫기 과정의 동기화 실패는 상태 비트에 반영될 수 있음
    if (!outFile) {
        std::cerr << "오류: 쓰기 또는 닫기에 실패했습니다.\n";
        return 1;
    }
    std::cout << "파일을 닫고 스트림 상태를 확인했습니다.\n";

    return 0;
}

일반적인 줄바꿈에는 \n을 사용하고, 다른 프로세스가 즉시 읽어야 하는 경계처럼 의도적인 동기화가 필요할 때만 std::endl이나 flush()를 사용합니다.

중요한 출력은 명시적으로 close()한 뒤 스트림 상태를 확인해야 닫기 과정에서 드러난 오류까지 잡을 수 있습니다. 정전 뒤에도 남아야 하는 수준의 영속성이 필요하다면 C++ 스트림 밖의 운영체제별 동기화 수단과 저장 장치 특성도 별도로 확인해야 합니다.


파일 쓰기 오류 처리

파일 쓰기 중에도 여러 가지 오류가 발생할 수 있습니다.

예를 들어 디스크 공간 부족, 파일 시스템 오류, 접근 권한 없음 등입니다.

읽기에서와 마찬가지로 fail()이나 bad() 멤버 함수를 사용하여 오류를 확인해야 합니다.

파일 쓰기 오류 처리 예제
#include <iostream>
#include <fstream>

int main() {
    std::ofstream outFile("protected_file.txt"); // 쓰기 권한이 없는 파일 또는 디스크 가득 참 가정

    if (!outFile.is_open()) {
        std::cerr << "파일을 열 수 없습니다. 쓰기 권한을 확인하세요.\n";
        return 1;
    }

    outFile << "데이터를 씁니다.\n";

    if (outFile.bad()) {
        std::cerr << "파일 쓰기 중 심각한 오류가 발생했습니다. (bad())\n";
    } else if (outFile.fail()) { // fail()은 failbit뿐 아니라 badbit도 포함
        std::cerr << "파일 쓰기 중 오류가 발생했습니다. (fail())\n";
    }

    outFile.close();
    if (!outFile) {
        std::cerr << "쓰기 또는 닫기를 완료하지 못했습니다.\n";
        return 1;
    }
    return 0;
}

fail()failbit 또는 badbit가 설정되면 참입니다. 원인을 해결하거나 출력 대상을 바꾸지 않은 채 clear()만 호출하면 상태 비트만 지울 뿐 실패 원인은 남으므로 이 예제에서는 사용하지 않습니다.

텍스트 파일 쓰기는 열기 성공만으로 끝나지 않습니다. 쓰기와 닫기 뒤의 상태를 확인하고, 실패했다면 권한·공간·장치 오류 같은 원인을 보고한 뒤 안전하게 종료하거나 복구 전략을 적용해야 합니다.