C++ - fstream
C++
fstream
fstream은 파일에서 입출력을 지원하는 라이브러리로, ifstream과 ofstream을 모두 포함하는 라이브러리다.
1
2
ifstream readFile(fileName); // InputFileStream 생성
ofstream writeFile(fileName, static_cast<ios_base::openmode>(mode)); // OutputFileStream 생성성
생성자 탐색
_EXPORT_STD
_EXPORT_STD는 일반적으로 템플릿 클래스의 별칭을 표준 라이브러리 이름 공간 내에서 사용할 수 있도록 내보내는 데 사용된다.
좀 더 풀어서 설명하자면, _EXPORT_STD는 매크로로, 정의된 텍스트 치환을 실행한다. define과 다른 점은, 특정 조건에 맞춰 설정되는 매크로라는 점인데, 특정 컴파일 환경에서 심볼을 내보내기 위해 사용된다.
1
2
3
4
5
6
7
8
#ifdef _BUILD_STD_MODULE
#if !_HAS_CXX20
#error The Standard Library Modules are available only with C++20 or later.
#endif // ^^^ !_HAS_CXX20 ^^^
#define _EXPORT_STD export
#else // ^^^ defined(_BUILD_STD_MODULE) / !defined(_BUILD_STD_MODULE) vvv
#define _EXPORT_STD
#endif // ^^^ !defined(_BUILD_STD_MODULE) ^^^
코드 분석 결과
- _BUILD_STD_MODULE이라는 매크로가 정의되어 있는지 확인한다.
- #ifdef는 매크로가 정의되어 있으면 이라는 의미이다.
- _HAS_CXX20은 C++20을 지원하는지 나타내는 매크로.
- 이 라이브러리는 export 키워드를 사용하는데, export는 20 이상부터 사용이 가능하기에 검사해야만 한다.
- #define _EXPORT_STD export는 C++ 20 이상인 경우 export 키워드를 사용할 수 있도록 설정하는 부분이다.
export 키워드는 모듈에서 정의된 기호를 다른 코드에서 사용할 수 있도록 공개하는 역할을 한다. 모듈은 내부 구현을 외부에서 접근할 수 없도록 숨기며, export 되지 않은 클래스, 함수, 변수 등을 사용하지 못하게 만든다.
결론
- _EXPORT_STD using ifstream = basic_ifstream<char, char_traits
>;는 ifstream을 치환하고, C++ 20 이상이면 export해주는 코드다. - fstream 자체는 모듈이 아니지만, 20 이상의 표준 라이브러리 중 fstream을 사용하는 모듈들을 외부에서 임포트할 때 ifstream을 사용할 수 있도록 추가해준 것 같다.
using
using은 #define처럼 키워드를 치환하는 기능을 가지지만 다른 점이 몇 가지 있다.
- define은 전처리 단계에서 키워드를 치환하여 타입 안정성을 확인하지 못하고, 컴파일 단계에서 디버깅이 어렵다.
- using의 경우 컴파일 단계에서 치환이 이루어져, 컴파일러가 타입에 대한 제약을 검증해 타입 안정성이 제공된다.
using의 경우 컴파일 단계이므로 디버깅이 수월하며, 템플릿에서도 사용이 가능하다는 장점이 있다.
파일 입출력기
간단한 파일 입출력기를 만들었다. 기능으로는 텍스트, 바이너리 파일을 읽거나 작성할 수 있다. 현재 코드는 분기가 많고, 하드 코딩된 부분이 있어서 개선할 여지가 많다.
클래스 설명
클래스는 크게 3가지가 있다.
- FileManager
- 입출력기에서 사용 가능한 파일을 찾고 이름을 반환해주는 클래스다. 확장자를 받으면, 확장자가 일치하는 파일들을 찾고 이름을 반환한다. FileReader
- FileManager에게 받은 파일을 열어 텍스트 파일 읽거나, 이진 파일처럼 읽어준다. FileWriter
- FileManager에게 받은 파일 혹은 새 파일을 만들고, 텍스트를 작성하는 역할을 한다.
- FileManager에게 받은 파일을 열어 텍스트 파일 읽거나, 이진 파일처럼 읽어준다. FileWriter
FileManager
파일 매니저는 3가지 방식으로 파일 이름을 가져올 수 있다.
FileSystem
1
2
3
4
5
6
7
8
9
10
11
12
void FileManager::GetFileNamesWithFileSystem(const string& extension, vector<string>& fileNames) // C++ 17 이상
{
string dir = "./"; //filesystem은 확장자명으로 필터링하지 못함
for (const auto iter : fs::directory_iterator(dir))
{
if (iter.path().extension() == extension) //.확장자 로 비교해야함
{
fileNames.push_back(iter.path().filename().string());
}
}
}
C++ 17 이상부터 사용 가능한 FileSystem을 이용한 방법. 엄청 간단하게 구현이 가능하다.
디렉토리 이터레이터로 현재 디렉토리의 경로를 가져오고, 거기서 확장자가 같은 파일들만 찾아오는 함수다.
_findfirst
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void FileManager::GetFileNamesWith_findfirst(const string& path, vector<string>& fileNames) // Windows 전용, 표준 라이브러리 아님
{
struct _finddata_t fd;
intptr_t handle;
if ((handle = _findfirst(path.c_str(), &fd)) != -1L)
{
do
{
fileNames.push_back(fd.name);
} while (_findnext(handle, &fd) == 0);
_findclose(handle);
}
}
_findfirst는 인터넷에 검색했을 때 가장 많이 나온 방법이다.
찾아보니 표준 C++ 라이브러리에 포함된 건 아니고, Microsoft의 Visual C++ 컴파일러에서 제공하는 함수다.
_finddata_t 구조체를 살펴보면, 이렇게 생겼다.
나는 이름만 사용했지만, 생성 시간, 최근 접근일, 마지막 수정 시간, 파일 크기 등등 꽤 많은 정보를 알 수 있다.
_findfirst 함수는 파일을 찾고 구조체에 채워넣어준다. 파일이 없으면 -1L을 반환한다.
이것도 간단하게 파일을 찾을 수 있다. 물론 _findfirst는 표준 C++ 라이브러리가 아니기에 호환성이 나쁘고, 차라리 다른 두 버전의 방법을 이용하는 게 좋을 것 같다.
WinAPI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void FileManager::GetFileNamesWithWinAPI(const string& path, vector<string>& fileNames) // Windows 전용, 빠르고 고급 제어가 가능
{
WIN32_FIND_DATAA findFileData;
HANDLE hFind = FindFirstFileA(path.c_str(), &findFileData);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
fileNames.push_back(findFileData.cFileName);
} while (FindNextFileA(hFind, &findFileData) != 0);
FindClose(hFind);
}
}
마지막은 WinAPI로 구하는 버전이다. 얘도 _findfirst처럼 구조체를 만들고 FindFirstFileA로 찾는다. 동작은 _findfirst와 거의 똑같다.
대신 이 코드는 WinAPI에서 제공하는 함수라 매우 빠르고 효율적이다. 또한 구조체 내용물도 굉장히 많아서 좀 더 고급 제어가 가능하다는 장점이 있다.
정리한 표
특징 | GetFileNamesWithFileSystem | GetFileNamesWith_findfirst | GetFileNamesWithWinAPI |
---|---|---|---|
라이브러리 | C++17 이상 filesystem 라이브러리 | Windows 전용, 표준 라이브러리 아님 | Windows 전용, Windows API |
플랫폼 | 크로스 플랫폼 (Windows, Linux, macOS 등) | Windows 전용 | Windows 전용 |
확장자 필터링 | 필터링 불가: 확장자 필터링은 코드 내에서 직접 처리 | 필터링 불가: 확장자 필터링은 코드 내에서 직접 처리 | 필터링 가능: 경로와 확장자에 대한 필터링 가능 |
성능 | 상대적으로 느림 (파일 시스템 관련 기능 제공) | 빠르지만 최신 C++ 표준 라이브러리가 아님 | 매우 빠르며, 고급 제어 가능 (Windows API 사용) |
사용 편의성 | C++17 이상의 표준에 맞춰 간결하고 직관적 | 코드가 길어지고 Windows 전용 | 코드가 직관적이지만 Windows 전용 |
파일 이름 | iter.path().filename().string() 으로 얻음 | fd.name 으로 얻음 | findFileData.cFileName 으로 얻음 |
디렉터리 탐색 방식 | directory_iterator 를 사용하여 디렉터리를 탐색 | _findfirst 와 _findnext 를 사용 | FindFirstFileA 와 FindNextFileA 를 사용 |
파일 필터링 방식 | 코드 내에서 확장자를 비교하여 필터링 | fd.name 을 통해 확장자를 비교하여 필터링 | FindFirstFileA 로 확장자나 파일명 필터링 가능 |
FileSystem은 크로스 플랫폼을 지원하므로, Windows만 사용할 거라면 WinAPI 방식이 더 적합하고, 크로스 플랫폼 환경에서는 FileSystem 방식이 좋을 것이다.
FileWriter
FileWriter는 ofstream을 사용했는데, ofstream은 없는 파일명을 매개변수로 주면 파일을 생성한다. 그리고 두 번째 매개변수로 비트 플래그를 받는다.
내가 사용한 비트 플래그들
- ios::binary - 파일을 이진 파일 형식으로 연다.
- ios::trunc - 파일의 내용을 덮어쓴다.
- ios::app - 파일 내용 뒤에 붙여쓴다.
텍스트 모드와 이진 모드의 차이
- 문자 인코딩
- 텍스트 모드에서는 파일 내용이 텍스트로 저장된다. 텍스트 파일에서 사용하는 문자 인코딩에 따라 저장된다. 이진 모드에서는 데이터를 바이트 단위로 그대로 파일에 기록한다. 라인 종료 처리
- 텍스트 모드에서는 줄바꿈 문자를 기준으로 라인 종료 문자가 사용된다. 이진 모드에서는 라인 종료 문자를 구분하지 않고 모든 문자를 그대로 쓴다.
보통 텍스트가 아닌 데이터를 저장할 때 이진 데이터를 사용한다고 한다. 데이터의 정확한 바이트 순서를 유지하고, 문자 변환 등을 방지하기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
void FileWriter::WriteTextFile(const string& fileName, FileOpenMode mode)
{
ofstream writeFile(fileName, static_cast<ios_base::openmode>(mode));
if (!writeFile.is_open())
{
cout << ERROR_INVALID_FILENAME<< endl;
return;
}
string line;
while (true)
{
cout << FILE_WRITER_ENTER_LINE_PROMPT;
getline(cin, line);
if (line.empty())
{
continue;
}
writeFile << line << "\n";
if (cin.eof())
{
cin.clear();
cout << FILE_WRITER_EOC_INPUT_CONFIRM << endl;
break;
}
else if (cin.fail())
{
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << ERROR_INVALID_INPUT << endl;
}
}
writeFile.close();
cout << "\n";
}
void FileWriter::WriteBinFile(const string& fileName, FileOpenMode mode)
{
ofstream writeFile(fileName, ios::binary | static_cast<ios_base::openmode>(mode));
if (!writeFile.is_open())
{
cout << "\n" << ERROR_INVALID_FILENAME << endl;
return;
}
string line;
while (true)
{
cout << FILE_WRITER_ENTER_LINE_PROMPT;
getline(cin, line);
if (line.empty())
{
continue;
}
uint8_t encoding = 1;
uint32_t len = static_cast<uint32_t>(line.size());
writeFile.write(reinterpret_cast<char*>(&encoding), sizeof(encoding));
writeFile.write(reinterpret_cast<char*>(&len), sizeof(len));
writeFile.write(line.c_str(), line.size());
if (cin.eof())
{
cin.clear();
cout << FILE_WRITER_EOC_INPUT_CONFIRM << endl;
break;
}
else if (cin.fail())
{
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << ERROR_INVALID_INPUT << endl;
}
}
writeFile.close();
cout << "\n";
}
처음 이진 모드를 테스트할 때, 메모장을 사용해 열어봤는데 내용이 텍스트 그대로 나와 있어 좀 헷갈렸다.
찾아보니 텍스트 에디터에는 자동으로 이진 데이터를 변환해주는 기능을 가진 것들도 있는 것 같다.
FileReader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void FileReader::ReadBinFile(string fileName)
{
ifstream readFile(fileName, ios::binary);
if (!readFile.is_open())
{
cout << ERROR_INVALID_FILENAME << endl;
return;
}
uint8_t encoding;
uint32_t len;
while (readFile.read((char*)(&encoding), sizeof(encoding)))//read 함수는 binary 일 때 사용하며, 구분자 개념없이 모든 데이터를 그대로 읽음
{
readFile.read((char*)(&len), sizeof(len));
char* buffer = new char[len + 1]; //null-terminator 포함
readFile.read(buffer, len);
buffer[len] = '\0'; //null-terminator 설정
cout << buffer << "\n";
delete[] buffer;
if (readFile.eof())
{
break;
}
}
cout << "\n\n";
readFile.close();
}
void FileReader::ReadTextFile(string fileName)
{
ifstream readFile(fileName);
if (!readFile.is_open())
{
cout << ERROR_INVALID_FILENAME << endl;
return;
}
cout << FILE_READER_START_READ;
string line;
while (getline(readFile, line)) //줄 바꿈 문자를 구분자로 사용하며, 줄바꿈 문자를 버리고 나머지를 가져옴
{
cout << line << "\n";
}
cout << "\n";
readFile.close();
}
FileReader는 텍스트 파일의 경우 간단히 getline으로 줄 바꿈 문자를 구분자로 사용하여 한 줄씩 읽어들인다.
이진 파일의 경우, 위에서 설명했듯이 인코딩 정보와 데이터 사이즈를 먼저 읽고, 사이즈만큼 읽어온다.
선택지 출력력 텍스트는 이렇게 정리해두었고, 가독성이 떨어지니 개선 방법을 찾고 있다. 지금은 XML이나 JSON을 이용해 따로 관리하고, 로컬라이징까지 적용해볼까 생각 중이다.
이외에도 Base64 인코딩 기법을 이용해 파일을 암호화할 수 있다는데, stream에 시간을 너무 많이 써서 나중에 복습할 때 추가할 예정이다.
아직 부족한 부분이 많지만, 스트림 관련 내용은 여기서 잠깐 멈추고, 다음에는 전처리, 컴파일, 런타임에 대해 좀 더 깊이 공부할 계획이다.