본문 바로가기
Programming/C++

[Modern C++ 공부 - Day9] Files and Streams

by 롱일스 2023. 7. 9.
반응형

Modern C++ 공부 Day 9입니다.

1. string의 기본 operations

File: C++에서 file이란 a sequence of bytes

fstream object를 통해서 file 에 접근할 수 있고, 이 방식은 iostream objects를 이용한 console input/output 접근과 비슷하다. 


fstream objects는 항상 파일에 sequentially하게 순서대로 접근한다. fstream은 file format에 대해 알지 못한다.

fstream operations
1. open - program에 의해 file이 이용가능해지게 만드는 상태
2. read - 데이터가 file -> program's memory 로 복사됨
3. write - 데이터가 file <- program's memory 로 복사됨
4. close - fstream object를 file에서 연결 끊고 더이상 프로그램이 file에 접근 불가능.

* 반드시 사용이 종료된 file은 close해야 한다. 
* 너무 많은 file을 한번에 많이 사용하면, memory allocation 용량 초과로 더이상 열 수 없게 된다.
* program과 file 사이에 data를 전달할 때 large data 처리를 위해 일시적으로 memory buffer를 사용하기도 한다.


* iostream - class: ostream (cout), istream (cin)
* fstream - class: ofstream (file stream for writing), ifstream (file stream for reading)


1. file 읽기 위해 Open하기


file 이름을 ifstream 생성자에 전달

ifstream ifile{"test.txt"}; // 파일 열기, file로부터 데이터를 받을 수 있는 communication channel이 된다.

이 object를 사용하기 전에 stream의 상태를 체크해야 한다.

if (ifile) {  // true면 사용가능하다는 의미
cout << "File opened for reading" << endl;
}

c++11부터는 std::string을 전달해도 된다.
string test{"test.txt"};
fstream file(test);


2. file 읽기 - 한 단어씩

ifile을 cin과 같은 방식으로 사용

while (ifile >> text) {
cout << text << ", ";  // 파일에서 문자 읽기
}

한번에 한 '단어'씩 읽고, 빈칸은 input에서 제거한다.

예제 코드

   ifstream ifile{"test.txt"};

    if (ifile) {
        string text{""};
        while (ifile >> text) {
            cout << text << ", ";
        }
        cout << endl;
        ifile.close();
    }



3. file 읽기 - 한 줄씩


getline() 함수를 사용하면 한줄씩 읽을 수 있다. 
while (getline(ifile, text)) {
cout << text << endl; // 한줄씩 읽기
}

파일을 다 읽고 마지막 부분에 도달하면 getline()함수는 false를 반환한다.


4. file 쓰기 위해 Open

stream object를 만든다
ofstream ofile{"test_out.txt"};

ofile 객체는 파일로 데이터를 전송하기 위한 communication channel이된다.
마찬가지로 stream의 상태를 확인한다

if (ofile) {  // 파일에 쓸 준비가 됨.
cout << "Opened file for writing" << endl; // 파일 열기
}

cout과 비슷하게 ofile을 사용한다.
vector<string> ex = {"Good", "morning", "gentlemen", "and", "ladies"};
for (auto word: ex) {
ofile << word << ", "; // 데이터를 파일에 전송
}


예제 코드

ofstream ofile{"test_out.txt"};

if (ofile) {  // 파일에 쓸 준비가 됨.
    cout << "Opened file for writing" << endl; // 파일 열기


    vector<string> ex = {"Good", "morning", "gentlemen", "and", "ladies"};
    for (auto word: ex) {
        ofile << word << ", "; // 데이터를 파일에 전송
    }
    ofile.close();
}




5. fstream 소멸자

소멸자가 불리면, file은 자동적으로 close된다.
저장되지 않은 데이터도 file에 마저 써준다.
소멸자를 call한 후의 fstream 객체는 scope 밖에 존재하기 때문에 따로 close()를 해주지 않아도 된다.
하지만 명시적으로 close()해주는게 좋다.  


write을 하는 동안에 데이터가 일시적으로 메모리 버퍼에 저장되어 OS를 호출하는 횟수를 최소화한다. 버퍼가 다 차면, stream이 버퍼에서 데이터를 삭제하고 OS에 보낸다. 이걸 Flushing이라고 한다.


6. Input & Output 문자 하나씩 

stream에는 character 하나씩 읽거나/쓰는 멤버함수가 있다.
get() 은 input stream에서 다음 character를 읽어오고
put() 은 argument인 character를 output stream에 보낸다.

char c;
while (cin.get(c))     // input stream의 character를 읽는다. 더이상 input stream이 없을때까지
cout.put(c); // character를 display한다.


7. memory buffer를 이용해 file에서 읽은 데이터 콘솔에 display하기

 

const int filesize(10);  // 메모리 버퍼 사이즈
char filebuf[filesize];  // 메모리 버퍼
string filename{"test.txt"};

ifstream ifile(filename);

if (!ifile) {
cout << "Could not open " << filename << endl;
return -1;
}

ifile.read(filebuf, filesize); // file에서 메모리버퍼로 데이터 fetch
ifile.close();

cout << "Display file data: ";
cout.write(filebuf, filesize); // 데이터를 메모리버퍼에서부터 출력
cout << endl;



+ gcount()함수를 이용해서 input stream에서 데이터가 얼마나 전달됐는지 확인할 수 있고, 완전하게 모든 데이터가 전달되었는지 확인 차 사용될 수 있다. 

auto nread = ifile.gcount(); //실제로 전달된 byte(문자) 개수를 반환한다.

 

8. file 모드

file mode: app (append mode), trunc (truncated), out (writing), in (reading), 

fstream file;
file.open("test.txt", fstream::out | fstream::app); 
// open for writing, append mode (원래 있던 데이터 뒤에 추가로 writing됨. 덮어씌워지지 않고)



| 를 이용해서 file 모드 여러개 합쳐서 쓸 수 있다. 하나만 써도되고 안써도 된다.
default는 truncate 모드다. 

당연히 app과 trunc는 같이 쓸 수 없다. 상충되기 때문!

 

8. stream 멤버 함수들

 

1. is_open(): file이 열려있는지 체크
2. good(): input이 성공적으로 읽히면 true 반환
3. fail(): recoverable error가 발생하면 true반환, 잘못된 데이터 읽은경우
4. bad(): unrecoverable error 발생하면 true반환, 
5. clear(): stream의 상태를 valid하게 회복
6. eof(): end of file에 도달하면 true 반환

ifstream ifile;
ifile.open("test.txt");

int x{0};

while (!ifile.eof()) {
    ifile >> x;
    cout << x << ", ";
}
cout << endl;

7. ignore(): 버퍼에서 character를 삭제, input stream은 flush기능이 없기 때문에 이 멤버함수를 이용하여 flush기능을 구현할 수 있다.
2개의 argument가 필요하다: (1) 제거하고자 하는 문자 최대개수, (2) delimiter character, 제거를 멈추게하는 문자.

cin.ignore(10, '\n'); // 다음 newline 전까지 10개씩 문자를 삭제.




9. stream manipulators

1. boolalpha/nonboolalpha: print int bool to boolean,  0 --> false
2. setw() : 왼쪽정렬, 오른쪽정렬해서 프린트해줌, 테이블처럼. 

cout << left << setw(15) << "bears" << 5 << "\n";
cout << setw(15) << "apples" << 2 << "\n";

3. sticky/non-sticky: stream의 state를 계속 바꾸고 그 상태로 유지되게 한다. 
4. setfill(): <iomanip>에 있는 멤버함수로 padding character를 argument로 받아서 빈칸에 채운다.

cout << setfill('%');
cout << left << setw(15) << "bears" << 5 << "\n";
cout << setw(15) << "apples" << 2 << "\n";
cout << setfill(' '); 

출력:
bears%%%%%%%%%%5
apples%%%%%%%%%2


5. scientific : 이걸 cout << 다음에 추가하면 3.141593e+000 이런 format으로 출력된다.
6. fixed: scientific과 반대로 input에 3.14e+2를 입력해도 출력은 314.000로 출력한다.
7. setprecision: 이걸 추가하면 number of digits를 정할 수 있다.
cout << setprecision(3) << 3.14123123 << endl; // 3.14 출력


10. Stream Types

1. iostream: ostream (cout), istream (cin)
2. fstream: ofstream (file stream for writing), ifstream (for reading)
3. stringstream: ostringstream (string stream for writing), istringstream (for reading)

4. ostringstream 이용해서 to_string과 같은 기능하는 함수 구현

string To_String_Ft(int i) {
ostringstream os;
os<<i;
return os.str(); // 만약 input이 13이면 string '13'을 반환한다.
}

template 이용해서 여러 type에도 적용할 수 있다.

template <typename T>
string To_String_Ft(const T& t){
ostringstream os;
os<<t;
return os.str();
}




(예제코드1) - to_string 기능을 하는 함수를 ostringstream을 이용해 만들기

#include <iostream>
#include <sstream>

template <typename T>
string To_String_Ft(const T& t){
ostringstream os;
os<<t;
return os.str();
}

int main() {
string test{"hello, "};
string pi{To_String_Ft(3.14159)}; // input type = double
test += pi;
cout << hello << endl;
}


(예제코드2)

// console에 입력한 값들을 모두 ostringstream 객체에 쌓이게 하고 전체 입력값들을 포맷에 맞게 마지막에 display하는 코드
#include <iomanip>
#include <sstream>
#include <iostream>

int main() {
    ostringstream ostr;
    string text;

    cout << "Please enter a word\n";
    cin >> text;    // 데이터 읽기
    ostr << setw(20) << text; // output 만들기
    cout << "Please enter another word\n"; // 데이터 더 읽고 ouput에 추가
    cin >> text;
    ostr << setw(20) << text;

    cout << ostr.str() << endl; // output string 출력
}

 

5. istringstream: 입력값을 std::string 객체로 읽고, 입력값을 검증해서 불완전하진 않은지, missing되진 않았는지 format은 제대로되어있는지 등을 확인한다. 

string text;

while (getline(cin, text)) {   // input을 한줄씩 std::string으로 읽고
    istringstream istr(text); 
    int num; // input line을 처리할 istringstream 객체를 만든다
    while (istr >> num) {  // 한번에 하나씩 숫자를 추출.
    	cout << num << endl;
    }
}

// Console 입력
13 15 17 19 21
// Console 출력
13
15
17
19
21


6. stringstreams 활용
- ostringstreams는 string을 특정 포맷에 맞춰서 interfacing할 때 유용하다, GUI, OS, ...
- istringstreams는 getline()을 활용해서 >> 이걱보다 더 쉽게 input을 처리할 수 있다.




11. streams 랜덤 액세스


보통 파일을 읽을 때 처음부터 sequential하게 읽는데, streams 클래스의 멤버함수를 이용해서 파일의 원하는 위치부터 읽을 수도 있다.

1. streams 클래스에 position marker라는 멤버함수는 last operation이 stopped됐던 위치를 기록한다.

write operation에서는 stream의 마지막에 marker가 위치하고, read operation에서는 마지막으로 읽은 부분 바로 뒤에 위치한다.

fstream을 "app"모드로 열지 않은 경우나, stringstream의 경우에는 개발자가 직접 이 marker's position을 바꿔서 read write할 위치를 정할 수 있다.


2. seek 멤버함수를 이용해서 marker's position을 변경할 수 있다. 
seekg: input stream에서 현재position을 정한다.
seekp: output stream에서 현재 position을 정한다.

3.  tell 멤버함수를 이용해서 marker's current position을 반환할 수 있다.
tellg: input stream에서 현재 marker's position을 반환한다.
tellp:output stream에서 현재 marker's position을 반환한다.
pos_type 객체를 반환하고, int로 변환가능하다. stream이 invalid한 state에 있으면 fail할 수 있고 이 경우에는 -1을 반환한다. 
auto pos = file.tellg();
if (pos != -1) {
...	// 포지션 마커가 유효한 경우
}


4. 추가사항
  1) seek과 tell은 iostream에서도 정의는 되지만, 사용하면 runtime error가 발생할 수 있으니, iostream에서는 사용하지 않는게 좋다
  2) append모드 "app"로 열린 fstream에서 seekp는 아무 영향을 줄 수 없다. output이 항상 파일 가자 마지막부터 쓰여지기 때문이다. 

5. stringstream 예제 코드

ostringstream output; // output stringstream을 열기
string data{"This is a tiger"};
output << data; // stream에 data 쓰기
auto marker = ofile.tellp(); // 현재 stream position marker 저장(a라고 하자)
cout << data.size() << " sdfsdfsfdsfsd stream\n"; // 뭔갈 쓴다
cout << "Current position marker is " << marker << "bytes into the stream\n";
output << "byebye";
cout << "Current position marker is now " << output.tellp() << " bytes into the stream.\n";
cout << output.str() << endl; 
if (marker != -1) // 
output.seekp(marker); // marker 위치를 원래 위치 (a)로 바꾼다.
output << "goodnight"; // overwrite
cout << output.str() << endl;


6. 하지만 seek 이랑 tell operations를 이용해서 파일 수정하는 건 위험할 수 있다. 이 operations 중에 에러가 나면 더이상 원본 파일에 접근이 불가능해지기 때문이다. 

그래서 파일 수정하는 가장 좋은 방법은 istringstream을 이용해서 read하고 bound string을 얻고 데이터를 바꾼다. 그 다음에 원본파일에 overwrite하는게 좋다.

 

12. Binary files


1. binary file을 다룰 때는 binary mode를 사용해야 한다.
ofile.open("image.bmp", fstream::binary);

다루고자하는 파일 포맷을 data member로 가지고 있는 struct를 만들어서 binary file을 작업하는 게 가장 좋다.
struct point {
char c;
int32_t x;
int32_t y;
};

fixed-size integer를 사용해서 모든 시스템에서 같은 결과가 나오도록 한다.

read(), write()을 이용해서 작업할건데,
1번째 argument는 char를 가리키는 pointer여야 한다.
reinterpret_cast<char *>(&p)

2번째 argument는 object의 바이트 수다 sizeof(point)

그래서 read, write call은 다음과 같은 형식으로 사용한다.
ofile.write(reinterpret_cast<char *>(&p), sizeof(point));
ifile.read(reinterpret_cast<char *>(&p2), sizeof(point));

예제코드

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

struct point {
	char c;
	int32_t x;   // fixed 사이즈 int
	int32_t y;
};

int main() {
	point p{'a', 1, 2};
	ofstream ofile("file.bin", fstream::binary);
	
	if (ofile.is_open()) {

		ofile.write(reinterpret_cast<char *>(&p), sizeof(point));
		ofile.close();
	}

	ifstream ifile("file.bin", fstream::binary);
	point p2;

	if (ifile.is_open()){
		ifile.read(reinterpret_cast<char *>(&p2), sizeof(point));
		ifile.close();
		cout << "Read " << ifile.gcount() << " bytes\n";
		cout << "read x = " << p2.x << ", y = " << p2.y << endl;
	}
}


출력:
Read 12 bytes
read x = 1, y = 2

 

2) image data의 경우 pixel로 이루어져 있다.
bitmap format의 경우 한 pixel은 3개의 byte로 이루어져 있다.
한 byte당 색깔이고, 값은 0-255 사이다.

struct pixel {
uint8_t blue;
uint8_t green;
uint8_t red;
};
728x90
반응형