텍스트 파일 쓰기
반대로, 프로그램에서 생성된 데이터를 텍스트 파일에 저장하는 방법에 대해 알아보겠습니다.
텍스트 파일에 데이터를 쓰는 것은 사용자 설정 저장, 로그 기록, 결과 출력 등 많은 애플리케이션에서 필수적인 기능입니다.
이 장에서는 std::ofstream
을 사용하여 텍스트 파일에 문자열과 숫자를 쓰는 방법, 파일 열기 모드의 중요성, 그리고 출력 버퍼에 대해 살펴보겠습니다.
std::ofstream
로 텍스트 파일 열기
텍스트 파일에 데이터를 쓰기 위해서는 std::ofstream
클래스의 객체를 사용합니다.
ofstream
은 "Output File Stream"의 약자입니다.
ofstream
객체를 생성할 때 파일 이름을 인자로 전달하여 바로 열거나, 객체 생성 후 open()
멤버 함수를 호출하여 열 수 있습니다.
#include <fstream> // std::ofstream을 위해
#include <iostream> // std::cout, std::endl을 위해
int main() {
// 방법 1: 생성과 동시에 파일 열기
std::ofstream outFile1("output1.txt");
if (!outFile1.is_open()) { // 또는 if (outFile1.fail())
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
을 사용하여 데이터를 출력하듯이, 파일 스트림에서도 <<
(삽입) 연산자를 사용하여 데이터를 파일에 쓸 수 있습니다.
다양한 데이터 타입(문자열, 정수, 실수 등)을 파일에 쓸 수 있으며, 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++ 파일 입출력 예제입니다." << std::endl; // std::endl은 '\n' + flush
// 숫자 쓰기
int age = 30;
double temperature = 25.7;
outFile << "나이: " << age << "세" << std::endl;
outFile << "오늘의 온도: " << temperature << "도" << std::endl;
// 여러 값 한 줄에 쓰기
std::string product = "Laptop";
int quantity = 5;
double price = 1200.50;
outFile << product << ", " << quantity << ", " << price << std::endl;
std::cout << "데이터가 'my_data.txt' 파일에 성공적으로 기록되었습니다." << std::endl;
outFile.close(); // 파일 닫기
return 0;
}
위 코드를 실행하면, my_data.txt
파일이 생성되거나 기존 내용이 지워지고 다음과 같은 내용이 기록됩니다.
Hello, File World!
C++ 파일 입출력 예제입니다.
나이: 30세
오늘의 온도: 25.7도
Laptop, 5, 1200.5
파일 열기 모드 (Open Modes)의 중요성
std::ios::out
모드는 기본적으로 파일의 내용을 지우고 새롭게 쓰기 시작합니다.
만약 기존 파일의 내용을 유지하면서 새로운 데이터를 추가하고 싶다면, std::ios::app
모드를 사용해야 합니다.
std::ios::out
(기본값): 파일을 쓰기 모드로 열고, 기존 내용이 있으면 모두 지웁니다.std::ios::app
: 파일을 쓰기 모드로 열고, 파일의 맨 끝에 새로운 내용을 추가(append)합니다. 기존 내용은 보존됩니다.
#include <iostream>
#include <fstream>
#include <string>
#include <chrono> // 현재 시간을 위해
#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);
// 시간 형식을 지정하여 출력
// std::put_time은 C++11부터 사용 가능하며, <iomanip> 헤더 필요
logFile << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %H:%M:%S") << ": ";
logFile << "애플리케이션이 실행되었습니다." << std::endl;
logFile << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %H:%M:%S") << ": ";
logFile << "사용자 'Admin'이 로그인했습니다." << std::endl;
std::cout << "로그가 'application_log.txt'에 추가되었습니다." << std::endl;
logFile.close();
return 0;
}
위 코드를 여러 번 실행해 보면, application_log.txt
파일의 내용이 계속해서 늘어나는 것을 확인할 수 있습니다.
출력 버퍼와 flush()
파일 스트림은 효율적인 데이터 전송을 위해 버퍼링(Buffering) 을 사용합니다.
데이터를 파일에 바로 쓰지 않고, 일단 메모리의 임시 저장 공간(버퍼)에 모아둡니다.
버퍼가 가득 차거나, std::endl
을 사용하거나, flush()
멤버 함수를 호출하거나, 파일을 닫을 때(스트림 객체 소멸 시) 버퍼의 내용이 실제 파일에 기록됩니다.
std::endl
: 개행 문자를 삽입하고 버퍼를flush()
합니다.\n
: 단순히 개행 문자만 삽입하고 버퍼를flush()
하지 않습니다.std::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 << "버퍼에만 쓰여졌을 수 있습니다. 잠시 대기..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
// 버퍼의 내용을 강제로 파일에 기록
outFile.flush();
std::cout << "버퍼가 플러시되었습니다. 이제 파일에 기록되었을 것입니다." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
outFile << "이것은 두 번째 줄입니다." << std::endl; // std::endl 사용, flush 됨
std::cout << "std::endl로 인해 버퍼가 플러시되었습니다." << std::endl;
outFile.close(); // 파일 닫기 시 자동으로 flush
std::cout << "파일이 닫혔습니다." << std::endl;
return 0;
}
일반적으로 std::endl
을 사용하여 개행과 플러시를 함께 처리하는 것이 편리합니다.
그러나 실시간으로 데이터가 파일에 기록되어야 하는 매우 중요한 상황(예: 오류 로그, 중요한 데이터 저장)에서는 명시적으로 flush()
를 호출하여 데이터 유실을 방지하는 것이 좋습니다.
파일 쓰기 오류 처리
파일 쓰기 중에도 여러 가지 오류가 발생할 수 있습니다.
예를 들어 디스크 공간 부족, 파일 시스템 오류, 접근 권한 없음 등입니다.
읽기에서와 마찬가지로 fail()
이나 bad()
멤버 함수를 사용하여 오류를 확인해야 합니다.
#include <iostream>
#include <fstream>
int main() {
std::ofstream outFile("protected_file.txt"); // 쓰기 권한이 없는 파일 또는 디스크 가득 참 가정
if (!outFile.is_open()) {
std::cerr << "파일을 열 수 없습니다. 쓰기 권한을 확인하세요.\n";
return 1;
}
outFile << "데이터를 씁니다.";
if (outFile.fail()) {
std::cerr << "파일 쓰기 중 오류가 발생했습니다. (fail())\n";
outFile.clear(); // 오류 플래그 초기화
}
if (outFile.bad()) {
std::cerr << "파일 쓰기 중 심각한 오류가 발생했습니다. (bad())\n";
outFile.clear(); // 오류 플래그 초기화
}
outFile.close();
return 0;
}