예외 처리와 파일 입출력
예외 처리와 파일 입출력¶
1. 예외 처리란?¶
예외(Exception)는 프로그램 실행 중 발생하는 비정상적인 상황입니다. C++은 try-catch 구문으로 예외를 처리합니다.
┌─────────────────────────────────────────────┐
│ 예외 처리 흐름 │
└─────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ try │ ───예외 발생───▶ │ throw │
│ 블록 │ │ │
└─────────┘ └─────────┘
│ │
│ 예외 없음 │ 예외 전파
▼ ▼
┌─────────┐ ┌─────────┐
│ 정상 │ │ catch │
│ 종료 │ │ 블록 │
└─────────┘ └─────────┘
2. try, throw, catch¶
기본 문법¶
#include <iostream>
#include <string>
double divide(double a, double b) {
if (b == 0) {
throw std::string("0으로 나눌 수 없습니다"); // 예외 발생
}
return a / b;
}
int main() {
try {
std::cout << divide(10, 2) << std::endl; // 5
std::cout << divide(10, 0) << std::endl; // 예외 발생!
std::cout << "이 줄은 실행되지 않음" << std::endl;
}
catch (const std::string& e) {
std::cout << "에러: " << e << std::endl;
}
std::cout << "프로그램 계속 실행" << std::endl;
return 0;
}
출력:
5
에러: 0으로 나눌 수 없습니다
프로그램 계속 실행
여러 catch 블록¶
#include <iostream>
#include <stdexcept>
void process(int value) {
if (value < 0) {
throw std::invalid_argument("음수는 허용되지 않습니다");
}
if (value > 100) {
throw std::out_of_range("100을 초과할 수 없습니다");
}
if (value == 0) {
throw 0; // int 타입 예외
}
std::cout << "값: " << value << std::endl;
}
int main() {
int tests[] = {50, -10, 150, 0};
for (int val : tests) {
try {
process(val);
}
catch (const std::invalid_argument& e) {
std::cout << "잘못된 인자: " << e.what() << std::endl;
}
catch (const std::out_of_range& e) {
std::cout << "범위 초과: " << e.what() << std::endl;
}
catch (int e) {
std::cout << "정수 예외: " << e << std::endl;
}
catch (...) { // 모든 예외 포착
std::cout << "알 수 없는 예외" << std::endl;
}
}
return 0;
}
출력:
값: 50
잘못된 인자: 음수는 허용되지 않습니다
범위 초과: 100을 초과할 수 없습니다
정수 예외: 0
3. 표준 예외 클래스¶
std::exception
│
┌──────────────┼──────────────┐
▼ ▼ ▼
logic_error runtime_error bad_alloc
│ │
┌───┴───┐ ┌───┴───┐
▼ ▼ ▼ ▼
invalid_ out_of_ overflow_ underflow_
argument range error error
주요 예외 클래스¶
#include <iostream>
#include <stdexcept>
#include <vector>
#include <new>
int main() {
// logic_error 계열 (프로그래머 실수)
try {
throw std::invalid_argument("잘못된 인자");
} catch (const std::exception& e) {
std::cout << "invalid_argument: " << e.what() << std::endl;
}
try {
throw std::out_of_range("범위 초과");
} catch (const std::exception& e) {
std::cout << "out_of_range: " << e.what() << std::endl;
}
try {
throw std::length_error("길이 오류");
} catch (const std::exception& e) {
std::cout << "length_error: " << e.what() << std::endl;
}
// runtime_error 계열 (실행 시 오류)
try {
throw std::runtime_error("런타임 오류");
} catch (const std::exception& e) {
std::cout << "runtime_error: " << e.what() << std::endl;
}
try {
throw std::overflow_error("오버플로우");
} catch (const std::exception& e) {
std::cout << "overflow_error: " << e.what() << std::endl;
}
// bad_alloc (메모리 할당 실패)
try {
throw std::bad_alloc();
} catch (const std::exception& e) {
std::cout << "bad_alloc: " << e.what() << std::endl;
}
return 0;
}
exception 클래스 상속¶
#include <iostream>
#include <exception>
#include <string>
// 커스텀 예외 클래스
class FileNotFoundError : public std::exception {
private:
std::string message;
public:
FileNotFoundError(const std::string& filename)
: message("파일을 찾을 수 없음: " + filename) {}
const char* what() const noexcept override {
return message.c_str();
}
};
class InvalidFormatError : public std::exception {
private:
std::string message;
public:
InvalidFormatError(const std::string& detail)
: message("잘못된 형식: " + detail) {}
const char* what() const noexcept override {
return message.c_str();
}
};
void readConfig(const std::string& filename) {
if (filename.empty()) {
throw FileNotFoundError("(빈 파일명)");
}
if (filename.find(".cfg") == std::string::npos) {
throw InvalidFormatError("확장자는 .cfg여야 합니다");
}
std::cout << filename << " 읽기 성공" << std::endl;
}
int main() {
std::string files[] = {"", "data.txt", "config.cfg"};
for (const auto& f : files) {
try {
readConfig(f);
}
catch (const FileNotFoundError& e) {
std::cout << "[파일 오류] " << e.what() << std::endl;
}
catch (const InvalidFormatError& e) {
std::cout << "[형식 오류] " << e.what() << std::endl;
}
}
return 0;
}
4. 예외 재발생과 noexcept¶
예외 재발생¶
#include <iostream>
#include <stdexcept>
void lowLevel() {
throw std::runtime_error("저수준 오류");
}
void midLevel() {
try {
lowLevel();
}
catch (const std::exception& e) {
std::cout << "[중간층] 예외 감지: " << e.what() << std::endl;
throw; // 예외 재발생 (상위로 전달)
}
}
void highLevel() {
try {
midLevel();
}
catch (const std::exception& e) {
std::cout << "[상위층] 최종 처리: " << e.what() << std::endl;
}
}
int main() {
highLevel();
return 0;
}
출력:
[중간층] 예외 감지: 저수준 오류
[상위층] 최종 처리: 저수준 오류
noexcept 지정자¶
#include <iostream>
// 예외를 던지지 않음을 보장
void safeFunction() noexcept {
// 예외를 던지면 std::terminate() 호출
std::cout << "안전한 함수" << std::endl;
}
// 조건부 noexcept
template<typename T>
void process(T& obj) noexcept(noexcept(obj.doSomething())) {
obj.doSomething();
}
class Safe {
public:
void doSomething() noexcept {
std::cout << "Safe::doSomething" << std::endl;
}
};
class Unsafe {
public:
void doSomething() {
throw std::runtime_error("오류");
}
};
int main() {
std::cout << std::boolalpha;
// noexcept 여부 확인
std::cout << "safeFunction noexcept: "
<< noexcept(safeFunction()) << std::endl; // true
Safe s;
Unsafe u;
std::cout << "Safe noexcept: "
<< noexcept(process(s)) << std::endl; // true
std::cout << "Unsafe noexcept: "
<< noexcept(process(u)) << std::endl; // false
safeFunction();
return 0;
}
5. 예외 안전성¶
예외 안전성 수준¶
| 수준 | 설명 |
|---|---|
| No-throw | 예외를 절대 던지지 않음 |
| Strong | 예외 발생 시 원래 상태로 복원 |
| Basic | 예외 발생 후에도 유효한 상태 유지 |
| No guarantee | 예외 발생 시 상태 불명 |
RAII로 예외 안전성 확보¶
#include <iostream>
#include <memory>
#include <stdexcept>
// RAII 클래스
class FileHandler {
private:
FILE* file;
public:
FileHandler(const char* filename, const char* mode) {
file = fopen(filename, mode);
if (!file) {
throw std::runtime_error("파일 열기 실패");
}
std::cout << "파일 열림" << std::endl;
}
~FileHandler() {
if (file) {
fclose(file);
std::cout << "파일 닫힘" << std::endl;
}
}
void write(const char* data) {
if (fputs(data, file) == EOF) {
throw std::runtime_error("쓰기 실패");
}
}
// 복사 금지
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;
};
void processFile() {
FileHandler fh("test.txt", "w"); // RAII: 생성자에서 열기
fh.write("Hello, World!\n");
throw std::runtime_error("중간에 예외 발생!");
fh.write("이 줄은 실행되지 않음");
} // RAII: 소멸자에서 자동으로 닫힘
int main() {
try {
processFile();
}
catch (const std::exception& e) {
std::cout << "예외: " << e.what() << std::endl;
}
return 0;
}
출력:
파일 열림
파일 닫힘
예외: 중간에 예외 발생!
6. 파일 입출력 기초¶
파일 스트림 클래스¶
| 클래스 | 용도 |
|---|---|
ifstream |
파일 읽기 |
ofstream |
파일 쓰기 |
fstream |
읽기/쓰기 |
#include <iostream>
#include <fstream>
#include <string>
int main() {
// 파일 쓰기
std::ofstream outFile("example.txt");
if (outFile.is_open()) {
outFile << "Hello, File!" << std::endl;
outFile << "Line 2" << std::endl;
outFile << 42 << " " << 3.14 << std::endl;
outFile.close();
std::cout << "파일 쓰기 완료" << std::endl;
}
// 파일 읽기
std::ifstream inFile("example.txt");
if (inFile.is_open()) {
std::string line;
while (std::getline(inFile, line)) {
std::cout << "읽음: " << line << std::endl;
}
inFile.close();
}
return 0;
}
파일 열기 모드¶
#include <iostream>
#include <fstream>
int main() {
// 쓰기 모드 (기본: 덮어쓰기)
std::ofstream f1("test.txt");
f1 << "새 내용" << std::endl;
f1.close();
// 추가 모드
std::ofstream f2("test.txt", std::ios::app);
f2 << "추가된 내용" << std::endl;
f2.close();
// 바이너리 모드
std::ofstream f3("data.bin", std::ios::binary);
int num = 12345;
f3.write(reinterpret_cast<char*>(&num), sizeof(num));
f3.close();
// 읽기+쓰기 모드
std::fstream f4("test.txt", std::ios::in | std::ios::out);
// 파일 끝에서 시작 (추가)
std::ofstream f5("test.txt", std::ios::ate);
// 기존 내용 삭제 후 쓰기
std::ofstream f6("test.txt", std::ios::trunc);
return 0;
}
| 모드 | 설명 |
|---|---|
ios::in |
읽기 |
ios::out |
쓰기 |
ios::app |
끝에 추가 |
ios::ate |
파일 끝에서 시작 |
ios::trunc |
기존 내용 삭제 |
ios::binary |
바이너리 모드 |
7. 파일 읽기 방법¶
다양한 읽기 방법¶
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
int main() {
// 테스트 파일 생성
std::ofstream out("data.txt");
out << "Alice 25 90.5\n";
out << "Bob 30 85.0\n";
out << "Charlie 28 92.3\n";
out.close();
// 방법 1: >> 연산자 (공백 기준)
std::ifstream f1("data.txt");
std::string name;
int age;
double score;
std::cout << "=== >> 연산자 ===" << std::endl;
while (f1 >> name >> age >> score) {
std::cout << name << ", " << age << ", " << score << std::endl;
}
f1.close();
// 방법 2: getline (한 줄씩)
std::ifstream f2("data.txt");
std::string line;
std::cout << "\n=== getline ===" << std::endl;
while (std::getline(f2, line)) {
std::cout << "줄: " << line << std::endl;
}
f2.close();
// 방법 3: getline + stringstream
std::ifstream f3("data.txt");
std::cout << "\n=== stringstream ===" << std::endl;
while (std::getline(f3, line)) {
std::istringstream iss(line);
iss >> name >> age >> score;
std::cout << "이름=" << name << ", 나이=" << age
<< ", 점수=" << score << std::endl;
}
f3.close();
// 방법 4: 전체 파일 읽기
std::ifstream f4("data.txt");
std::stringstream buffer;
buffer << f4.rdbuf();
std::string content = buffer.str();
std::cout << "\n=== 전체 내용 ===" << std::endl;
std::cout << content;
f4.close();
return 0;
}
문자 단위 읽기¶
#include <iostream>
#include <fstream>
int main() {
std::ofstream out("chars.txt");
out << "ABC\nDEF";
out.close();
std::ifstream in("chars.txt");
char c;
// get()으로 한 문자씩
std::cout << "문자별: ";
while (in.get(c)) {
if (c == '\n') {
std::cout << "[LF]";
} else {
std::cout << c;
}
}
std::cout << std::endl;
// peek()으로 미리보기
in.clear();
in.seekg(0);
std::cout << "Peek: ";
while (in.peek() != EOF) {
char peeked = in.peek();
char got;
in.get(got);
std::cout << "(" << (int)peeked << ")";
}
std::cout << std::endl;
in.close();
return 0;
}
8. 바이너리 파일¶
바이너리 읽기/쓰기¶
#include <iostream>
#include <fstream>
#include <vector>
struct Record {
int id;
char name[50];
double score;
};
int main() {
// 바이너리 쓰기
std::ofstream out("records.bin", std::ios::binary);
Record r1 = {1, "Alice", 95.5};
Record r2 = {2, "Bob", 87.0};
Record r3 = {3, "Charlie", 91.2};
out.write(reinterpret_cast<char*>(&r1), sizeof(Record));
out.write(reinterpret_cast<char*>(&r2), sizeof(Record));
out.write(reinterpret_cast<char*>(&r3), sizeof(Record));
out.close();
std::cout << "Record 크기: " << sizeof(Record) << " bytes" << std::endl;
// 바이너리 읽기
std::ifstream in("records.bin", std::ios::binary);
Record record;
std::cout << "\n=== 레코드 읽기 ===" << std::endl;
while (in.read(reinterpret_cast<char*>(&record), sizeof(Record))) {
std::cout << "ID: " << record.id
<< ", Name: " << record.name
<< ", Score: " << record.score << std::endl;
}
in.close();
// 특정 레코드 접근 (랜덤 액세스)
std::ifstream in2("records.bin", std::ios::binary);
// 두 번째 레코드로 이동 (0부터 시작)
in2.seekg(1 * sizeof(Record));
in2.read(reinterpret_cast<char*>(&record), sizeof(Record));
std::cout << "\n두 번째 레코드: " << record.name << std::endl;
in2.close();
return 0;
}
벡터 저장/로드¶
#include <iostream>
#include <fstream>
#include <vector>
void saveVector(const std::string& filename, const std::vector<int>& vec) {
std::ofstream out(filename, std::ios::binary);
// 크기 먼저 저장
size_t size = vec.size();
out.write(reinterpret_cast<char*>(&size), sizeof(size));
// 데이터 저장
out.write(reinterpret_cast<const char*>(vec.data()),
size * sizeof(int));
out.close();
}
std::vector<int> loadVector(const std::string& filename) {
std::ifstream in(filename, std::ios::binary);
// 크기 읽기
size_t size;
in.read(reinterpret_cast<char*>(&size), sizeof(size));
// 데이터 읽기
std::vector<int> vec(size);
in.read(reinterpret_cast<char*>(vec.data()),
size * sizeof(int));
in.close();
return vec;
}
int main() {
std::vector<int> original = {10, 20, 30, 40, 50};
saveVector("vector.bin", original);
std::cout << "저장 완료" << std::endl;
std::vector<int> loaded = loadVector("vector.bin");
std::cout << "로드된 데이터: ";
for (int n : loaded) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
9. 파일 위치 제어¶
seekg, seekp, tellg, tellp¶
#include <iostream>
#include <fstream>
int main() {
// 파일 생성
std::ofstream out("position.txt");
out << "0123456789ABCDEF";
out.close();
// 읽기 위치 제어
std::ifstream in("position.txt");
// 현재 위치 확인
std::cout << "시작 위치: " << in.tellg() << std::endl;
// 5번째 위치로 이동 (처음부터)
in.seekg(5, std::ios::beg);
char c;
in.get(c);
std::cout << "위치 5의 문자: " << c << std::endl;
// 현재 위치에서 3칸 뒤로
in.seekg(3, std::ios::cur);
in.get(c);
std::cout << "3칸 뒤: " << c << std::endl;
// 끝에서 2칸 앞
in.seekg(-2, std::ios::end);
in.get(c);
std::cout << "끝에서 2칸 앞: " << c << std::endl;
in.close();
// 쓰기 위치 제어
std::fstream file("position.txt", std::ios::in | std::ios::out);
file.seekp(10); // 10번째 위치로 이동
file << "XYZ"; // ABC를 XYZ로 덮어씀
file.seekg(0); // 처음으로
std::string content;
std::getline(file, content);
std::cout << "수정 후: " << content << std::endl;
file.close();
return 0;
}
파일 크기 확인¶
#include <iostream>
#include <fstream>
long getFileSize(const std::string& filename) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return -1;
}
return file.tellg();
}
int main() {
// 테스트 파일 생성
std::ofstream out("size_test.txt");
out << "Hello, World!";
out.close();
long size = getFileSize("size_test.txt");
std::cout << "파일 크기: " << size << " bytes" << std::endl;
return 0;
}
10. 스트림 상태 확인¶
상태 플래그¶
#include <iostream>
#include <fstream>
#include <sstream>
void checkStreamState(std::ios& stream) {
std::cout << "good(): " << stream.good() << std::endl;
std::cout << "eof(): " << stream.eof() << std::endl;
std::cout << "fail(): " << stream.fail() << std::endl;
std::cout << "bad(): " << stream.bad() << std::endl;
}
int main() {
std::cout << std::boolalpha;
// 정상 상태
std::istringstream ss1("100");
int num;
ss1 >> num;
std::cout << "=== 정상 읽기 후 ===" << std::endl;
checkStreamState(ss1);
// EOF 상태
ss1 >> num;
std::cout << "\n=== EOF 후 ===" << std::endl;
checkStreamState(ss1);
// 실패 상태
std::istringstream ss2("abc");
ss2 >> num;
std::cout << "\n=== 잘못된 형식 ===" << std::endl;
checkStreamState(ss2);
// 상태 초기화
ss2.clear();
std::cout << "\n=== clear() 후 ===" << std::endl;
checkStreamState(ss2);
// 파일 열기 실패
std::ifstream file("nonexistent.txt");
std::cout << "\n=== 존재하지 않는 파일 ===" << std::endl;
checkStreamState(file);
return 0;
}
예외 활성화¶
#include <iostream>
#include <fstream>
int main() {
std::ifstream file;
// 스트림 예외 활성화
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
file.open("nonexistent_file.txt");
// 파일이 없으면 예외 발생
}
catch (const std::ios_base::failure& e) {
std::cout << "파일 열기 실패: " << e.what() << std::endl;
}
return 0;
}
11. 문자열 스트림¶
stringstream 활용¶
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
// 문자열 -> 숫자 변환
std::string numStr = "42 3.14 100";
std::istringstream iss(numStr);
int i;
double d;
int j;
iss >> i >> d >> j;
std::cout << "파싱: " << i << ", " << d << ", " << j << std::endl;
// 숫자 -> 문자열 변환
std::ostringstream oss;
oss << "결과: " << 123 << " + " << 456 << " = " << (123 + 456);
std::string result = oss.str();
std::cout << result << std::endl;
// CSV 파싱
std::string csv = "Alice,25,90.5";
std::istringstream csvStream(csv);
std::string token;
std::vector<std::string> tokens;
while (std::getline(csvStream, token, ',')) {
tokens.push_back(token);
}
std::cout << "CSV 파싱: ";
for (const auto& t : tokens) {
std::cout << "[" << t << "] ";
}
std::cout << std::endl;
// stringstream 재사용
std::stringstream ss;
ss << "Hello";
std::cout << "1: " << ss.str() << std::endl;
ss.str(""); // 내용 비우기
ss.clear(); // 상태 초기화
ss << "World";
std::cout << "2: " << ss.str() << std::endl;
return 0;
}
12. 실용적인 예제¶
설정 파일 파서¶
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
class ConfigParser {
private:
std::map<std::string, std::string> config;
public:
bool load(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
return false;
}
std::string line;
while (std::getline(file, line)) {
// 빈 줄, 주석 건너뛰기
if (line.empty() || line[0] == '#') continue;
// '=' 기준으로 분리
size_t pos = line.find('=');
if (pos != std::string::npos) {
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
// 공백 제거 (간단한 버전)
config[key] = value;
}
}
return true;
}
std::string get(const std::string& key,
const std::string& defaultValue = "") const {
auto it = config.find(key);
if (it != config.end()) {
return it->second;
}
return defaultValue;
}
int getInt(const std::string& key, int defaultValue = 0) const {
auto it = config.find(key);
if (it != config.end()) {
return std::stoi(it->second);
}
return defaultValue;
}
void display() const {
for (const auto& [key, value] : config) {
std::cout << key << " = " << value << std::endl;
}
}
};
int main() {
// 설정 파일 생성
std::ofstream out("config.ini");
out << "# 서버 설정\n";
out << "host=localhost\n";
out << "port=8080\n";
out << "max_connections=100\n";
out << "debug=true\n";
out.close();
// 설정 파일 읽기
ConfigParser config;
if (config.load("config.ini")) {
std::cout << "=== 설정 파일 ===" << std::endl;
config.display();
std::cout << "\n=== 개별 접근 ===" << std::endl;
std::cout << "Host: " << config.get("host") << std::endl;
std::cout << "Port: " << config.getInt("port") << std::endl;
std::cout << "Timeout: " << config.getInt("timeout", 30) << std::endl;
}
return 0;
}
CSV 파일 처리¶
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
struct Student {
std::string name;
int age;
double score;
};
class CSVHandler {
public:
static void write(const std::string& filename,
const std::vector<Student>& students) {
std::ofstream file(filename);
// 헤더
file << "name,age,score\n";
// 데이터
for (const auto& s : students) {
file << s.name << "," << s.age << "," << s.score << "\n";
}
}
static std::vector<Student> read(const std::string& filename) {
std::vector<Student> students;
std::ifstream file(filename);
std::string line;
std::getline(file, line); // 헤더 건너뛰기
while (std::getline(file, line)) {
std::istringstream iss(line);
Student s;
std::string field;
std::getline(iss, s.name, ',');
std::getline(iss, field, ',');
s.age = std::stoi(field);
std::getline(iss, field, ',');
s.score = std::stod(field);
students.push_back(s);
}
return students;
}
};
int main() {
// CSV 쓰기
std::vector<Student> students = {
{"Alice", 20, 95.5},
{"Bob", 22, 87.0},
{"Charlie", 21, 91.2}
};
CSVHandler::write("students.csv", students);
std::cout << "CSV 저장 완료" << std::endl;
// CSV 읽기
auto loaded = CSVHandler::read("students.csv");
std::cout << "\n=== 학생 목록 ===" << std::endl;
for (const auto& s : loaded) {
std::cout << s.name << " (" << s.age << "세): "
<< s.score << "점" << std::endl;
}
return 0;
}
13. 요약¶
| 개념 | 설명 |
|---|---|
try-catch |
예외 처리 블록 |
throw |
예외 발생 |
noexcept |
예외를 던지지 않음 보장 |
std::exception |
표준 예외 기본 클래스 |
ifstream |
파일 읽기 스트림 |
ofstream |
파일 쓰기 스트림 |
fstream |
읽기/쓰기 스트림 |
stringstream |
문자열 스트림 |
seekg/seekp |
파일 위치 이동 |
tellg/tellp |
현재 위치 확인 |
14. 연습 문제¶
연습 1: 로그 파일 클래스¶
날짜/시간과 함께 메시지를 기록하는 Logger 클래스를 작성하세요.
연습 2: 예외 계층 구조¶
데이터베이스 관련 예외 클래스 계층구조를 설계하세요. (ConnectionError, QueryError, AuthenticationError 등)
연습 3: JSON 파서 (간단 버전)¶
간단한 key-value JSON을 파싱하는 클래스를 작성하세요.
(예: {"name": "Alice", "age": 25})
다음 단계¶
14_Smart_Pointers_Memory.md에서 스마트 포인터를 배워봅시다!