본문 바로가기
P.L

c++ ofstream 파일 수정/쓰기 file(binary) modify/write

by ahsung 2020. 6. 13.

c++로 file 입출력을 하려고 시도하던 도중... 입력은 간단했는데

출력하는 ofstream 객체를 활용하여 append 쓰기 뿐만아닌 수정을 동시에 하고 싶었는데,

정식 문서에는 이 기능을 위한 별도의 member는 안보였고, 몇몇 블로그에도 file 내용을 수정하고 싶다면,

모든 file을 읽어 RAM에 올린후에 수정하고 싶은 부분만 수정후 모든 내용을 다시 쓰라는...포스팅만 있었다.

최근 c언어로 시스템콜을 활용하며 공부한 나로서, 운영체제들은 분명 file의 중간 부분을 수정할 수 있는 기능을 제공해주는데 저렇게 비효율적인 방법을 사용하고 싶지 않아 c++의 ofstream의 여러 기능을 찾아보고 테스트하여 방법을 찾았다.

정식 문서에 따르면,

출처: http://www.cplusplus.com/reference/fstream/ofstream/open/

보다 싶히 제공해주는 open 권한 flag가 이게 전부다...

언뜻보기에는 모두 있는 것 같지만 out flag기능이 out | trunc 와 같은 flag이다.

c언어에서는 write 권한만 주고 open이 가능하므로 기존에 존재하던 fileseek를 해서 특정 부분만 수정할 수 있었다.

그러나... ofstream은 out을 주게되면 애초에 open할때 항상 trunc시켜버린다 ( 0으로 만든다는 것)

그렇다면 ofstream은 기존 파일을 못바꾸나? 그건 아니다 app 권한을 주게되면 기존 파일을 회손시키지 않은 후

open하여 쓰기가 가능하고 항상 맨 file의 맨 끝에서 write를 시작한다.

 

# append 옵션 참고 지식

app Flag는 매번 seek를 통해 끝으로 이동시킨후 write하면 되는데 굳이 있는 이유는 편해서 있는 것이 아니다.
여러 쓰레드가 한 file을 동시에 write하고 있게되면, 내가 seek한 곳이 가장 끝이라는 보장이 없게된다.
내가 끝으로 seek하고 쓰려고하는 순간, 다른 쓰레드가 write하게되면 끝에 내용을 append하는 것이 아닌, 다른 쓰레드가 쓴 곳을 의도치 않게 수정해 버릴수 있다.
이를 방지하기 위해서 unix시스템에서는 app flag가 설정되있을 경우,
seek를 끝으로 보낸후 write하는 과정을 atomic(중간에 방해없이 한번에)하게 처리하여 위와 같은 문제를 방지하는 기능이다.

 

 

ofstream 객체가 file을 open할때 app를 추가적으로 주어도.. 매번 seekp 함수를 통해 stream pos를 조정해도 write할때마다 아토믹하게.. 맨뒤에 추가하게된다.

out만 쓰게되면... trunc해서 기존 파일을 지워버리고..

app를 쓰면 기존파일은 남기는데 뒤에 추가밖에 안되고..

ate는 그래서 왜 있는지 모르겠고...

이런 이유때문에 많은 블로그에서 기존 file을 모두 읽어온후 RAM에서 수정하고 싶은 부분만 수정후 trunc된 file에

전부 다시 적는 방법을 알려주는 것같다.

그러나 나보다 훨씬 똑똑하신 천재들이 ofstream을 이렇게 제한적으로만 만들지는 않았을 거라 생각하여 이런저런 방법을 찾아보았고 해결하였다.

일단 ofstream은 쓰기만 가능할 것 같지만 fstream을 상속한 클래스이며

fstream에서 default 옵션으로 write에 특화되게끔 상속한 클래스일뿐, fstream의 기능(read,write)을 flag 설정만 한다면 모두 사용할 수 있다.

즉 ofstream도 open시 ios:: in flag를 주게되면 읽기 기능을 사용할 수 있다.

물론 직접 read를 해보진 않았지만 이론적으론 가능하다.

그리고 중요한건 read를 하려는 것이 아니라, ios:: in flag 덕분에 trunc 기능이 사라지게 된다.

ofstream fout;
fout.open(fileName, ios::out | ios :: in);

이렇게 하게되면 trunc하지 않고 편집모드로 open이 가능하다. 와~우...

하지만 단점은 기존 out flag만 있을 경우 기존 file이 없으면 create해주었는데, in flag때문에 이 기능도 함께 사라졌다.

아마 open fail을 할 것같다.

is_open()을 써서 확인은 안해봤지만, 기존 file이 없는 상태에서 위 코드를 실행 후 read를 사용했을때 엉뚱한 file을 읽는 오류는 확인했다.

이 문제 해결은 간단하다.

그냥 file을 미리 만들어 놓는 방법도 있고, create를 위한 ofstream객체를 하나 만드는 방법도 있다.

out | app flag를 주어 open하면, 없으면 만들 것이고 있어도 app flag 덕분에 trunc는 안할 것이다.

그 다음 진짜 사용할 ofstream이 out | in을 통해 open하여 seekp를 사용하며 수정과, append를 하면 될 것이다.

fout.seekp(0,ios::end)

이렇게 seekp하고 write를 하게되면 가장 끝에서부터 수정없이 추가하게 된다.

물론 포스팅글 위에서 말한 app를 사용하는 이유를 보완하지는 못한다.

위 코드는 stream pos를 ios :: end 부터 0만큼 이동한다는 뜻이다. 2번쨰 인자를 사용하지 않으면 절대값 stream pos(byte 단위)로 이동하게된다. 참고로 ios :: end는 실제 사용한 file의 맨마지막이 아닌, 실제 마지막의 바로 다음을 가르킨다.

진짜 사용한 마지막 byte를 가르키려면 fout.seekp(-1, ios::end)를 사용해야한다.

#ifndef DATABASE
#define DATABASE

#include<string>
#include<fstream>
#include<iostream>
using namespace std;


template<class T>
class Database {
    string filename;
    int data_size;
    ofstream mout;
    ifstream in;
    ofstream copen; //create용 객체, file이 없다면 생성해준다.
public:
    Database() {}
    ~Database(){
        mout.close();
        copen.close();
        in.close();
    }
    Database(string filename) {
        copen.open(filename,ios :: out | ios::app | ios::binary);
        mout.open(filename, ios :: out | ios :: in | ios::binary);
        in.open(filename, ios::in | ios::binary);
        in.seekg(0, ios::end); data_size = in.tellg()/sizeof(T); in.seekg(0, ios::beg);
    }

    inline int cur_idx() {
        return in.tellg() / sizeof(T);
    }
    inline int dataSize() {
        return this->data_size;
    }

    T read(int idx = -1);
    void write(T &data,int idx = -1);
};


template<class T>
void Database<T>::write(T &data,int idx) {
    if(idx < 0) idx = data_size;
    int tellp = idx * sizeof(T);

    if (idx >= data_size) {

        mout.seekp(0,ios::end);
        mout.write((char *)&data, sizeof(data));
        data_size++;
    }
    else {
        mout.seekp(tellp);
        mout.write((char *)&data, sizeof(data));
    }
}

template<class T>
T Database<T>::read(int idx) {
    T data;
    if (idx >= 0) {
        int tellg = idx * sizeof(T);
        if (tellg >= data_size) {
            cerr << "fileSize over" << endl;
            return data;
        }
        in.seekg(tellg);
    }
    in.read((char *)&data, sizeof(data));

    return data;
}

#endif 

객체 단위로 file에 binary 쓰기와 읽기를 하는 Class

주의 사항

1.

iostream과 관계있는 객체들은 모두 기본적으로 buffering을 하기 때문에 싱크가 항상 맞지 않습니다.

예로 ofstream은 보통 파일 쓰기를 close까지 미루는 경향이 있으므로 다른 객체가 file을 읽을때 내용이 없을지도 모르겠네요. 만약 읽는걸 감지하고 그때 write back하게 끔 구현했다면 괜찮겠지만 그 부분까지는 체크 안해봤습니다.

아마 flush하는 함수가 따로 있지 않을까 생각합니다. i/o 객체들에서 flush함수들은 buffering한 정보를 바로 출력시키게 합니다.

참고로 exit(0)를 통해 종료하게되면 write를 미루다가 하지 못하고 종료되더군요..

같은 이유로 error체크를 위해 어디까지 실행됬는지 보려고 cout을 사용하게되면 buffering 때문에

확실한 타이밍을 알 수 없습니다. 이럴떄는 cerr를 사용해주세요

2.

위 코드 블록의 read함수처럼 T data를 지역변수로 선언하고 return data를 하게되면,

깊은 복사생성자가 잘 구현된 class라면 별문제가 없지만, 그렇지 않다면

pointer 객체들은 주소값을 복사하여 전달해 줄텐데, read함수가 끝나면서 data도 반환되어

pointer가 가르키는 주소는 inValid한 상태가 되어버린다.

binary write/read를 할때, pointer 객체를 사용하는 것도 같은 문제로,

pointer 객체를 write하게되면 주소값을 file안에 적게 되는데,

나중에 process가 file을 읽었을때 그 주소가 현 process에는 당연히 다른 값을 가지고 있거나 inValid 할 것이다.

그래서 몇 STL을 그대로 binary write하게되면 제대로 적용이 안될 것이다.

Class의 binary쓰기를 할때는 고정된 크기의 배열을 사용하던지,

포인터가 가르키는 주소와 거기서부터 시작되는 배열의 크기만큼을 size로 넘겨 write해야한다.

int *a; 일때, write((char *)&a,size); 하게되면, 포인터가 가르키는 주소의 메모리값을 쓴는게 아니라,

a의 메모리 주소를 넘겨줬기 때문에, a의 메모리안의 값( a가 가르키고 있는 주소 값)이 write된다.

write((char *)a,size); 이렇게 사용하여, 포인터 객체가 가르키는 메모리 영역을 binary write를 해야한다.

이때 size는 a가 가르키는 주소의 배열 크기(byte단위)를 넘겨주면 되겠다.

받을때도 pointer배열을 미리 크기 할당을 하고,같은 방식으로 size만큼 받으면 되겠다.

그러나 보통은 얼만큼의 size를 할당해야할지 모르기때문에, file에 적어놓던지, 마음편하게 고정크기 배열을 사용하면 되겠다.

댓글