1. 텍스트 파일과 바이너리 파일
2. C++ 파일 입출력 라이브러리
3. C++ 파일 입출력 모드 : 텍스트 I/O 와 바이너리 I/O, 파일 읽기 쓰기
4. 파일 모드 : 텍스트 I/O 와 바이너리 I/O
5. 멤버 함수를 이용한 텍스트 I/O : get(), put(), read(), write()
출처 : 명품 C++ 프로그래밍 (저자: 황기태)
1. 텍스트 파일과 바이너리 파일
파일은 기록되는 데이터 종류에 따라 텍스트 파일과 바이너리 파일로 나뉜다.
오직 글자로만 이루어진 문서 파일이 텍스트 파일이다. 각 글자에 2Byte 바이너리 코드를 부여하여 ASCII/UNICODE 를 만들었다. 공백은 ASCII로 0x20으로 저장된다. <Enter> 키는 '\r(0x0D)'+'\n(0x0A)' 두 제어 문자로 기록된다.
ex) txt, html, xmp, c++, c, java
바이너리 파일은 이미지, 컴파일 코드, 오디오 등 문자로 표현되지 않은 정보를 가지고 있는 파일이다.
ex) jpeg, bmp, mp3, obj, exe, hwp, doc
2. C++ 파일 입출력 라이브러리
ANSI/ISO C++ 표준 위원회에서는 파일 입출력 라이브러리에 대한 표준을 정하였다. 아래 그림은 C++ 표준 파일 입출력 라이브러리 핵심 클래스를 보여준다.
fstream은 파일에 읽기, 쓰기를 동시에 할 때 사용된다.
사실 이들은 basic_ifstream, basic_ofstream, basic_fstream 등의 템플릿 클래스에 char 타입으로 구체화하고 typedef로 선언된 클래스이다.
ifstream이나, ofstream은 파일을 프로그램과 연결하는 스트림으로서, 프로그램은 ifstream객체를 통해 파일을 읽고 ofstream 객체를 통해 파일 쓰기를 진행한다.
ios, istream, ostream, iostream 클래스 멤버 함수들은 파일 입출력에도 사용된다(왜냐하면, ifstream, ofstream, fstream 클래스가 이들의 자식 클래스이기 때문)
(예를 들어) istream의 get()은 스트림이 키보드에 연결되어 있다면 키보드에서 문자 하나를 읽고, 파일에 연결되어 있다면 파일에서 문자 하나를 읽는다. (키보드에 연결되어 있냐, 파일에 연결되어 있냐의 차이이다)
그러므로, ifstream 스트림 객체와 ofstream 스트림 객체가 파일을 연결하면, get(), getline(), put(), read(), write() 등의 멤버 함수들은 자연스럽게 스트림에 연결된 파일에서 읽고 쓴다.
ifstream, ofstream, fstream을 이용하기 위해서는 <fstream> 헤더 파일과 std 이름 공간이 필요하다
#include <fstream>
using namespace std;
3. C++ 파일 입출력 모드 : 텍스트 I/O 와 바이너리 I/O
C++파일 입출력 시스템은 파일에 읽고 쓰는 데이터를 문자로 국한한 텍스트 I/O와 문자로 해석하지 않고 바이너리 값 그대로 읽고 쓰는 바이너리 I/O로 구분한다.
텍스트 I/O는 문자들만 기록하고, 파일에 있는 바이트를 문자로만 해석하는 입출력 방식. 텍스트 파일을 읽고 쓸 때만 사용한다.
(텍스트 파일이라고 가정하여, ASCII 크기 단위 혹은 유니코드 크기 단위로 읽어 들이나 보다)
바이너리 I/O는 바이트 단위로 바이너리 데이터를 입출력 하는 방식이다. 모든 파일을 단순히 바이트의 스트림으로 다루기 때문에, 텍스트 파일이나 바이너리 파일 상관 없이 읽고 쓰기 가능하다.
파일 입출력 모드는 ifstream, ofstream, fstream 생성자에 지정하거나, 파일 Open시에 지정한다.
3-1. 텍스트 파일 입출력
- 텍스트 파일 입출력은 <<, >> 연산자를 사용하면 편리하다. <<, >> 연산자는 텍스트 파일에 대해서만 작동한다.
파일 쓰기 스트림 객체 생성, 열기, 검사, 닫기
- 아래의 예시에서 !fout으로 검사하는 것이 가능한 이유는 fstream은 ios 클래스를 상속 받았고, ios 클래스에는 멤버함수 operator!() 가 존재하기 때문이다.
ofstream fout; //파일 출력 스트림 객체 생성
fout.open("song.txt"); // open() 멤버함수 호출, 출력 스트림을 파일에 연결하는 행위
//혹은
ofstream fout("song.txt"); 파일 스트럼 생성과 동시에 파일을 열 수도 있다.
//검사
if(!fout){
}
//혹은 is_open() 메소드를 이용할 수 있다.
if(fout.is_open()){
}
//닫기
fout.close();
파일 읽기, 쓰기
- << 연산자는 문자만 저장하기 때문에 메모리에는 '2'(32), '1'(31), '\n'(0D, 0A)이 저장된다.
- 마찬가지로 >> 연산자는 문자만을 읽어 들이기 때문에 21을 쓰면 '2', '1'각각을 읽어 들이고 정수형 변수에 저장한다.
int age = 21;
char singer[] = "kim";
fout<<age<<'\n';
fout<<singer<<endl;
int sid;
fin>>sid;
※ 파일 열기/닫기 의 모든 과정은 OS에 의해 이루어 지며 열린 파일의 효율적인 입,출력을 위한 구조가 형성된다.
닫기 시에는 구성한 자료구조를 해제하여 읽기/쓰기를 끝내는 과정이며, 버퍼에 남아있는 데이터를 물리적 장치에 기록하는 작업을 포함한다. 프로그램이 종료되면, 열려진 파일은 모두 자동으로 닫힌다.
4. 파일 모드
- 파일 모드란 파일을 열 때 어떤 파일 입출력을 수행할 것인지 알리는 정보이다. iso 클래스에 상수로 선언되어 있다.
- 텍스트 I/O로 할 것인지, 바이너리 I/O로 할 것인지 등을 지정한다.
파일 모드 | 의미 |
iso::in | 읽기 위해 파일 연다(ifstream 디폴트) |
iso::out | 쓰기 위해 파일 연다(ofstream 디폴트) |
iso::ate | 쓰기 위해 파일 연다. 열기 후 파일 포인터 파일 끝에 둔다. |
iso::app | 파일 쓰기시에 적용. 항상 파일 끝에 쓰기가 된다. (파일 처음으로 갈 수 없다) |
iso::trunc | 파일이 존재하면, 파일 내용 모두 지운다. |
iso::binary | 바이너리 I/O로 파일 연다. 디폴트는 텍스트 I/O이다. |
- 파일 모드는 파일을 열 때 지정하며, open() 함수나 ifstream, ofstream, fstream 생성자를 통해 지정한다.
파일 끝에 데이터를 저장하는 경우
ofstream fout;
fout.open("student.txt", ios::out|ios::app);
fout<<"tel:01044447777"; //기존 파일 끝에 문자열 추가 저장
바이너리 I/O로 파일을 기록하는 경우
fstream fbinout;
fbin.open("data.bin", ios::out | ios::binary);
char buf[128];
fbinout.write(buf, 128);
스트림 객체의 생성자를 이용한 파일 모드 지정
ifstream fin("student.txt")
ofstream fout("student.txt", ios::out | ios::app);
ofstream fin("student.bin", ios::in | ios::binary);
5. 멤버 함수를 이용한 텍스트 I/O
- get(), put(), read(), write()
5-1. get(), put()
- 파일에서 한바이트를 읽고 쓴다. 텍스트 파일, 바이너리 파일 무관이다.
- 각각 istream, ostream 클래스의 멤버이다.
- 단 라인 끝 <Enter>'\r\n' 은 두 바이트 이지만, 한바이트('\n')로 처리한다(한바이트 읽은 것으로 간주한다)
- 파일 끝이면 get()이 EOF(-1, 0xFFFFFFFF)을 리턴한다. 따라서, while((c=fin.get()) != EOF) 와 같이 사용한다.
- 참고로, 파일 끝에서 get() 함수를 호출해야만, fin.eof()가 'True'가 된다(eofbit다 '1'로 Set된다). 그전엔 파일 끝이라도 -1이 안된다. 이러한 불편함으로 인해 .eof() 메서드가 아닌 fin.get()으로 파일 끝을 검사한다.
int get(); //파일 끝이라면 EOF(-1) 리턴
ostream& put(char ch) //파일에 문자(ch) 기록
while((c = fin.get()) != EOF)
{
// 파일에서 읽은 바이트씩 처리
}
※ get()은 int32 를 리턴한다. 파일 중간에 '-1'이 있어도 한 바이트만 읽어 들이므로 0x000000FF를 리턴한다. 파일 끝인 EOF(0xFFFFFFFF)와 혼동하지 않는다.
5-2. 파일 한줄 읽기
두가지 방법이 있다.
- istream의 getline(char* line, int n) 메소드 이용
- getline(ifstream fin, string line) 함수 이용, <string> 헤더 파일을 포함해야 한다.
<string> 헤더 파일을 포함하여, 문자열을 string 객체로 다루는 것이 훨씬 편하고 안전하다. string 객체에 포함된 여러 메소드를 이용할 수 있는 장점도 있다.
각 라인 문자열 길이를 의식하지 않아도 된다.
ifstream fin("{FILEPATH}");
if(!fin){
}
char buf[81];
while(true) {
fin.getline(buf, 81); //파일 한줄이 80개의 문자로 구성된다면 NULL 문자 포함해서 읽어 들인다.
if(fin.eof()) break; // 파일 끝이면 읽기 종료
}
string line;
while(true) {
getline(fin, line); //한 라인을 읽어 문자열에 저장한다.
if(fin.eof()) break; // 파일 끝이면 읽기 종료
}
파일 전체를 읽어 벡터에 저장하는 함수
void fileRead(vecotr<string> &v, ifstream &fin) {
string line;
while(true) {
getline(fin, line);
if(fin.eof()) break;
v.push_back(line);
}
}
5-3 바이너리 I/O
텍스트 파일은 텍스트 I/O나, 바이너리 I/O로 읽고 쓰는 것이 둘다 가능하지만, 바이너리 파일은 반드시 바이너리 I/O를 이용해야 한다.
get(), put()을 이용한 이미지 파일 복사
ifstream fsrc(srcFile, ios::in | ios::binary);
if(!fsrc) {
}
ofstream fdst(dstFile, ios::out | ios::binary);
if(!fdst) {
}
int c;
while((c=fsrc.get()) != EOF) {
fdst.put(c);
}
read(), write()로 블록 단위 파일 입출력
- read() 함수는 istream의 멤버로서 ifstream이 상속 받으며, write() 함수는 ostream의 멤버로서 ofstream이 상속 받아 사용한다.
- 블록 단위로 읽기 때문에 get(), put() 보다 프로그램 전체 실행 속도가 빠르다.
istream& read(char* s, int n); //파일에서 최대 n개의 바이트를 배열 s에 읽어 들임. 파일 끝을 만나면 읽기 중단
ostream* write(char* s, int n); //배열 s에 있는 처음 n개의 바이트를 파일에 저장
int gcount(); //파일에서 읽은 바이트수 리턴
read(), write() 함수를 이용한 텍스트 출력
int count = 0;
char s[32]; //블록 단위로 읽어 들일 버퍼
while(!fin.oef()) {
fin.read(buf, 32); //최대 32바이트를 읽어 배열 s에 저장
int n = fin.gcount(); //실제 읽은 바이트수를 알아냄
cout.write(s, n); //버퍼에 있는 n개의 바이트를 화면에 출력
count += n;
}
read(), write() 함수를 이용한 바이너리 파일 복사
char buf[1024]; //블록 단위로 읽어 들일 버퍼
while(!fin.oef()) {
fin.read(buf, 1024); //최대 32바이트를 읽어 배열 s에 저장
int n = fin.gcount(); //실제 읽은 바이트수를 알아냄
fdest.write(buf, n); //버퍼에 있는 n개의 바이트를 화면에 출력
}
※ 텍스트 I/O 로 처리하면 '\r\n'을 한바이트로 처리하지만, 바이너리 I/O로 처리하면 두바이트로 처리한다. 따라서, 파일 크기를 정확히 카운트 하려면 바이너리 I/O로 읽어야 한다.
멤버함수의 모드에서의 차이점
- 멤버함수는 모드에 따라 '\n' 문자를 만날 때만 다르게 동작한다.
텍스트 I/O 모드
- '\n'을 쓸때는 '\r\n' 두 바이트로 쓴다. '\r\n'을 읽을 때는 '\n' 한바이트를 리턴한다.
char buf[] = {'a', 'b', '\n'};
fout.write(buf, 3); // 파일에는 'a', 'b', '\r', '\n' 4바이트가 기록된다.
fout<<'\n'; //파일에 '\r', '\n' 2바이트가 기록된다.
fout.put('\n'); //파일에 '\r', '\n' 2바이트가 기록된다.
바이너리 I/O 모드
ofstream fout("c\\tmp.txt", ios::out | ios::binary);
char buf[] = {'a', 'b', '\n'};
fout.write(buf, 3); // 파일에는 'a', 'b', '\n' 3바이트가 기록된다.
6. 스트림 상태 검사
※ 텍스트 파일이든, 바이너리 파일이든 파일 끝을 인식하는 것은 OS 몫이다. 입출력 라이브러리는 OS 도움을 받아, API 함수 호출을 통해 파일 끝을 알린다. OS의 파일 시스템은 파일 정보를 가지고 있기 때문에 파일 포인터가 파일 끝에 도달하는 지 알 수 있다. 파일 포인터가 마지막 데이터 넘어 접근하게 되면, 파일 끝에 도달하였음을 기억해 두고, 입 출력 라이브러리가 요청할 때 이 사실을 알려준다.
7. 임의 접근
C++ 파일 입출력 시스템은 파일 내의 읽고 쓸 바이트 위치를 가리키는 파일 포인터 라는 특별한 마크(mark)를 두고 있다.
get pointer- 파일 내의 읽기 지점을 가리키는 파일 포인터. ifstream이나, 읽기모드(ios::in)로 열려진 fstream이 가진다.
put pointer- 파일 내의 쓰기 지점을 가리키는 파일 포인터. ofstream이나, 쓰기모드(ios::out)로 열려진 fstream이 가진다.
get(), read(), getline(), >> 등의 읽기 연산은 읽은 바이트 수 만큼 get pointer를 전진 시킨다.
put(), write(), << 등의 쓰기 연산은 put pointer를 전진 시킨다.
istream& seekg(streampos pos) | 정수 값으로 주어진 절대 위치 pos로 get pointer를 옮김 |
istream& seekg(streamoff offset, ios::seekdir seekbase) | seekbase를 기준으로 offset 만큼 떨어진 위치로 get pointer를 옮김 |
ostream& seekp(streampos pos) | 정수 값으로 주어진 절대 위치 pos로 put pointer를 옮김 |
ostream& seekp(streamoff offset, ios::seekdir seekbase) | seekbase를 기준으로 offset 만큼 떨어진 위치로 put pointer를 옮김 |
streampos tellg() | 입력 스트림의 현재 get pointer의 값 리턴 |
streampos tellp() | 입력 스트림의 현재 put pointer의 값 리턴 |
※ 입력은 get pointer, 출력은 put pointer.
seekg, tellg은 입력 스트림에서만 사용하고, seekp, tellp는 출력 스트임에서만 사용한다.
seekbase로 사용되는 ios::seekdir 타입의 상수
ios::beg, ios::cur, ios::end
활용 코드
fin.seekg(0, ios::end) //파일 끝. 읽을 데이터는 없다.
fin.seekg(-1, ios::end) // 맨 마지막 문자를 읽는다.
int c;
while((c = fin.get()) != EOF) {
fin.seekg(9, ios::cur); //10바이트 단위로 1바이트씩 읽는다.
}
fin.seekg(0, ios::end);
int fileSize = fin.tellg(); //get pointer로 파일 크기 읽어 들이기
for(int i=0; i<fileSize; i++){
fin.seekg(fileSize-1 -i, ios::beg); // 마지막 문자열 부터 읽어 들인다.
cout<<(char)c;
}
결론)
- 스트림 객체는 키보드에 연결하거나 파일에 연결한다. 동일 메소드를 사용하므로 사용자는 (키보드/ 파일) 연결 이후로는 동일하게 처리가 가능하다.
- 텍스트 파일이라도 속도를 높이고 싶다면, 바이너리 모드로 읽어 들이면 빠를 것이다. 그림/오디오가 포함되어 있다면 어짜피 바이너리 모드로 밖에 못읽을 것이다.
- 파일을 여는 3가지 방법이 존재한다. C++ 을 사용하고자 한다면 되도록 3번째 방법을 이용하는 것이 좋겠다.
1) FILE *fp = fopen(파일위치, "r"); // C 언어 방법
2) ifstream fIn; fIn.open() //멤버 함수를 이용하는 방법
3) ifstream fIn(파일 위치, ios::in | ios::app) //스트림 생성과 동시에 파일 열기
즉, fopen, fread, fwrite, fstream, ifstream, ofstream 존재하지만, 파일 스트림 생성과 동시에 열면 편하다.
(본인은 C 언어로 여는 것이 fseek, fwrite 등 편리한 함수를 사용할 수 있어서 좋다고 생각하였으나, C++에서는 write, seek 기능이 멤버함수 .write, .seek로 존재하여 C++ 로 모두 대체 가능하다. 또한 C에서 지원하는 파일 포인터도 C++에서 사용 가능하다)
(C는 문자열 출력이 fputs, fprintf를 이용해야 하지만, C++은 << 연산자를 이용할 수 있다. 형식화된 출력은 cout<<(char)c 과 같은 방식으로 시도 하지 말고, <cstdio>의 fprintf를 )
[1] https://www.cplusplus.com/doc/tutorial/files/
[2] https://www.cplusplus.com/reference/fstream/fstream/
※ RapidJSON 스트림
StringStream (Input)
- StringStream은 간단한 입력 스트림이다. Read-Only 이다.
#include "rapidjson/document.h" // will include "rapidjson/rapidjson.h"
using namespace rapidjson;
// ...
const char json[] = "[1, 2, 3, 4]";
StringStream s(json); //rapidJson에서 정의한 스트림이다. char버퍼를 스트림으로 변환한다.
Document d;
d.ParseStream(s);
혹은, 스트림을 안거치고 Parse메소드를 이용해 char 배열에서 바로 읽어 들일 수 있다.
const char json[] = "[1, 2, 3, 4]";
Document d;
d.Parse(json);
StringBuffer (Output)
- StringBuffer는 출력 스트림이다. 메모리 버퍼를 할당하고 전체 Json을 쓴다.
#include "rapidjson/stringbuffer.h"
#include <rapidjson/writer.h>
StringBuffer buffer;
Writer<StringBuffer> writer(buffer);
d.Accept(writer); //document 내용을 출력 버퍼에 쓰는 행위이다. json 크기에 따라 버퍼는 자동 증가된다.
const char* output = buffer.GetString();
https://rapidjson.org/md_doc_stream.html
'프로그래밍 언어 > C, C++' 카테고리의 다른 글
std::nothrow (0) | 2022.07.29 |
---|---|
fstream 처음 위치로 옮겨서 덮어쓰기 (0) | 2022.02.27 |
Unit testing 이란 (0) | 2022.02.13 |
void 포인터 용법 정리 (0) | 2021.08.26 |
Two pointer as argument (0) | 2021.08.02 |