SW 개발

[CPP 기본문법] 파일입출력

. . . 2010. 4. 4. 19:39
반응형

18장제 2 부 C++ 고유 특성

C++ 파일 입출력

C++ 파일 입출력

비록 C++의 입출력 방식이 통합적이라고 하더라도, 파일 입출력(특히, 디스크 파일 입출력)은 그 자체의 제약 사항들 때문에, 특별한 경우로 생각할 수 있다. 여기서 특히 디스크 파일을 고려하는 것은 가장 일반적인 파일이 디스크 파일이며, 대부분의 다른 장치들에는 없는 기능과 특성들을 가지고 있기 때문이다. 그러나, 디스크 파일 입출력은 단순히 일반 입출력 시스템의 특별한 경우이며, 이 책에서 논의한 대부분의 자료들은 또한 다른 종류의 장치에 연결된 스트림들에도 적용될 수 있다는 것을 명심하자.

fstream.h 와 파일 클래스

파일 입출력을 위해서는, 프로그램에 FSTREAM.H 헤더 파일을 포함해야 한다. 이것은 ifstream, ofstream, fstream 등을 포함하여 여러 가지 클래스들을 정의한다. 이 클래스들은 각각 istream과 ostream으로부터 파생된 것이다. (앞 장에서 논의된) istream과 ostream은 ios로부터 파생되었으므로, ifstream, ofstream, fstream도 ios에 의해서 정의된 모든 연산들에 접근할 수 있다.

파일 열기와 닫기

C++에서 파일은 특정 스트림에 연결함으로써 열린다. 파일을 열기 전에 먼저 특정 스트림을 얻어야 한다. 이 스트림에는 일반적으로 세 가지가 있다. 즉, 입력, 출력, 입출력이다. 입력 스트림을 만들기 위해서, ifstream 클래스로 입력 스트림을 선언해야 한다. 또한 출력 스트림을 만들기 위해서, 클래스 ofstream으로 출력 스트림을 선언해야 한다. 입력과 출력을 동시에 수행할 스트림들은 fstream으로 선언되어야 한다. 예를 들어, 다음 코드는 하나의 입력 스트림, 하나의 출력 스트림, 입력과 출력을 모두 할 수 있는 스트림을 생성한다.

  ifstream in;      // 입력
  ofstream out;     // 출력
  fstream io;       // 입출력

일단 특정 스트림을 생성하고 나면, 이것을 파일에 연결시키는 방법은 함수 open()을 이용하는 것이다. 이 함수는 3개의 스트림 클래스 각각의 멤버이다. 다음은 이것의 원형이다.

void open(const char *filename, int mode, int access=filebuf::openprot);

여기서, filename은 경로를 포함하는 파일 이름이다. mode 값은 파일을 어떻게 열지를 결정한다. 이것은 다음 값 중 하나(또는 그 이상)가 되어야 한다.

// MODE 값..
ios::app
ios::ate
ios::binary
ios::in
ios::nocreate
ios::noreplace
ios::out
ios::trunc

이 값들은 논리합(OR)으로 함께 사용할 수 있다. 이들 각각에 대한 의미를 알아보자.

  • ios::app : 모든 출력들을 그 파일의 끝에 덧붙인다. 이 값은 출력할 파일들에만 사용될 수 있다.
  • ios::ate : 파일을 열 때 그 파일의 끝을 찾아준다. ios::ate가 적용될 때도, 입출력 연산들은 여전히 파일 내의 어느 곳에서나 수행될 수 있다.
  • 내정적으로(default), 파일들은 텍스트 모드로 열린다.
  • ios::binary :파일을 2진 형식으로 열도록 한다. 파일이 텍스트 모드로 열릴 때, 캐리지 리턴(carriage- return)과 라인 피드(line-feed) 문자들이 뉴라인(newline) 문자로 변환되는 것처럼 다양한 문자들의 변환이 가능하다. 그러나, 파일이 2진 형식으로 열릴 때는 어떠한 문자 변환도 일어나지 않는다. 어떤 파일이 포맷된 텍스트이건 포맷되지 않은(raw) 2진 파일이건간에 텍스트 또는 2진 형식 모드 중 하나로 열릴 수 있다. 이때 단지 차이는 문자 변환 여부일 뿐이다.
  • ios::in : 파일이 입력용임을 의미한다.
  • ios::out : 그 파일이 출력용임을 의미한다. 그러나, ifstream을 이용하여 스트림을 만드는 것은 입력을, ofstream을 이용하여 스트림을 만드는 것은 출력을 의미한다. 그러므로, 이 스트림들이 사용될 경우에, 이 값들은 필요 없다.
  • ios::nocreate : 현재 파일이 존재하지 않으면 open() 함수를 실패로 처리하도록 한다.
  • ios::noreplace : 값은 파일이 이미 존재하면 open() 함수를 실패로 처리한다.
  • ios::trunc : 값은 동일한 이름으로 이미 존재하고 있는 파일의 내용을 모두 삭제하며, 파일 크기가 0이 되도록 한다.
  • 주의사항
    • 제안된 ANSI C++표준은 mode 매개변수의 형을 어떤 형태의 정수(일반적으로 int)인 openmode로 명시한다.현재, 대부분의 구현제품들은 모드 매개변수의 형을 int로 지정한다.

access 값

access 값은 그 파일에 어떻게 접근될 수 있는지를 결정한다. 이것의 내정값은 보통의 파일을 나타내는 filebuf::openprot이다. (filebuf는 streambuf에서 파생된 클래스이다.) 대부분의 access는 내정적으로(default) 허용한다. 그러나, 여러분의 운영체제에서 이 매개변수에 대해서 허용되는 다른 선택사항(option)들이 무엇인지를 알기 위해서는 여러분의 컴파일러 사용자 매뉴얼을 검토해 보아야 한다. 예를 들어, 파일 공유 선택사항은 전형적으로 통신망 환경에서 access 매개변수로 사용된다.

다음 코드 부분은 보통의 출력 파일을 연다.

ofstream out;
out.open("test", ios::out);

그러나, mode 매개변수는 내정값을 가지기 때문에, 이 경우에 open()에서 사용하지 않아도 된다. ifstream에 대해서, mode의 내정값은 ios::in이며, ofstream에 대해서는 내정값은 ios::out이다. 그러므로, 위의 문장은 대개 다음과 같이 본다.

out.open("test");  // 내정적으로 출력용인 보통 파일

입출력을 위한 스트림을 열기 위해서, 다음 예에서 보인 것처럼, ios::in과 ios::out mode 값 모두를 지정해야 한다.(이 경우에는 어떠한 mode 내정값도 적용되지 않는다.)

fstream mystream;
mystream.open("test", ios::in | ios:out);

만일 open()이 실패하면, mystream은 0이 된다. 그러므로, 파일을 사용하기 전에, 열기 연산이 성공적인지를 보장하기 위해서 다음과 같이 시험해야 한다.

if(!mystream) {
  cout << "Cannot open file.\n";
  // 오류 처리
}

비록 open() 함수를 사용하여 파일을 여는 것이 매우 적절하다고 하더라도, ifstream, ofstream, fstream 클래스들이 파일을 자동적으로 여는 생성자 함수를 가지고 있기 때문에, 대부분은 이렇게 하지 않아도 된다. 생성자 함수들은 open() 함수와 같이 매개변수와 내정값을 가진다. 그러므로, 일반적으로 다음과 같이 파일을 여는 것이 보통이다.

ifstream mystream("myfile"); // 입력용으로 파일 열기

이미 언급했듯이, 어떤 이유 때문에 파일을 열 수 없다면, 관련된 스트림 변수의 값은 0이 된다. 그러므로, 파일을 열기 위해서 생성자 함수를 사용하든, 또는 open()으로 호출하든, 스트림의 값을 검사하여 파일이 실제로 열렸는지 확인할 수 있다.

파일을 닫기 위해서는 멤버 함수 close()를 사용한다. 예를 들어, mystream이라는 스트림에 연결된 파일을 닫기 위해서 다음 문장을 사용한다.

mystream.close();

close() 함수는 매개변수도 없고 어떤 값도 리턴하지 않는다.

텍스트 파일 입출력

텍스트 파일을 읽고 쓰는 것은 매우 쉽다. cin과 cout을 사용하는 대신에 파일에 연결된 스트림을 사용하는 것을 제외하고는 콘솔 입출력을 수행하는 것과 같은 방법으로 간단히 « 과 » 연산자를 사용한다. 예를 들어, 다음 프로그램은 각 항목의 이름과 가격을 포함하는 간단한 재고 파일을 생성한다.

#include <iostream.h>
#include <fstream.h>
main()
{
  ofstream out("INVNTRY");  // 보통의 출력 파일  if(!out) {
    cout << "Cannot open INVENTORY file.\n";
    return 1;
  }  out << "Radios " << 39.95 << end1;
  out << "Toasters " << 19.95 < end1;
  out << "Mixers " << 24.80 << end1;  out.close();
  return 0;
}

다음 프로그램은 위의 프로그램에서 만들어진 재고 파일을 읽고 화면에 이 파일의 내용을 출력한다.

#include <iostream.h>
#include <fstream.h>
main()
{
  ifstream in("INVNTRY");  // 입력  if(!in) {
    cout << "Cannot open INVENTORY file.\n";
    return 1;
  }  char item[20];
  float cost;  in >> item >> cost;
  cout << item << " " << cost << "\n";
  in >> item >> cost;
  cout << item << " " << cost << "\n";
  in >> item >> cost;
  cout << item << " " << cost << "\n";  in.close();
  return 0;
}

>><< 연산자를 이용하여 파일을 읽고 쓰는 것은 C의 fprintf()와 fscanf()를 사용하는 것과 유사하다. 이것이 화면에 출력되는 것과 같은 포맷으로 모든 정보가 파일에 저장될 수 있다. 다음은 디스크 입출력의 또 다른 예이다. 이 프로그램은 키보드에서 입력된 문자열들을 읽고 이들을 디스크에 쓴다. 사용자가 빈 라인을 입력하면 프로그램을 정지한다. 프로그램을 실행하기 위해서, 명령어 라인에 출력 파일 이름을 지정해야 한다.

#include <iostream.h>
#include <fstream.h>
#include <stdio.h>   main(int argc, char *argv[])
{
  if(argc!=2) {
    cout << "Usage: output <filename>\n";
    return 1;
  }   ofstream out(argv[1]);  // 보통의 출력 파일   if(!out) {
    cout << "Cannot open output file.\n";
    return 1;
  }   char str[80];
  cout << "Write strings to disk, RETURN to stop.\n";   do {
    cout << ": ";
    gets(str);
    out << str << end1;
  } while(*str);   out.close();
  return 0;
}

>> 연산자를 이용하여 텍스트 파일을 읽을 때, 문자 변환이 발생한다는 것을 명심하자. 예를 들어, 공란(white-space) 문자들은 생략된다. 만일 어떠한 문자 변환을 방지하고자 한다면, C++의 2진 형식(binary) 입출력 함수들을 사용해야 한다. 이것은 다음 절에서 다룬다.

입력할 때, 파일의 끝 표시(end-of-file)를 만나면, 그 파일에 연결된 스트림이 0이 된다. (이것도 다음 절에서 다룬다.)

2진 입출력

2진 형식(binary) 자료를 파일에서 읽거나, 파일에 쓰는 데는 두 가지 방법이 있다. 여기서는 이 방법들을 다룬다.

기억할것 만일 특정 파일에서 2진 연산들을 수행한다면, ios::binary 모드 지정자를 사용하여 열어야 한다. 비록 2진 파일 함수들이 텍스트 모드로 열린 파일들에서 동작하더라도, 문자 변환이 발생할 수 있다. 이 문자변환들은 2진 파일 연산들의 목적을 무효화한다.

get()과 put()

2 진 자료를 읽고 쓰는 한 가지 방법은 멤버 함수 get()과 put()을 이용하는 것이다. 이 함수들은 바이트 단위를 기본으로 한다. 즉, get()은 자료의 한 바이트를 읽고 put은 자료의 한 바이트를 쓴다. get() 함수는 여러 가지 형식을 가질 수 있지만, put()과 함께 다음과 같은 형식을 가장 일반적으로 사용한다.

get() 함수는 관련된 스트림으로부터 하나의 문자를 읽어서 ch에 저장한다. 이것은 스트림에 대한 참조를 리턴한다. put() 함수는 ch를 특정 스트림에 쓰고 스트림에 대한 참조를 리턴한다. 다음 프로그램은 화면에 어떤 파일의 내용을 get() 함수를 사용하여 읽고, 이를 출력한다.

istream &get(char &ch);
ostream &put(char ch);
#include <iostream.h>
#include <fstream.h>   main(int argc, char *argv[])
{
  char ch;   if(argc!=2) {
    cout << "Usage: PR <filename>\n";
    return 1;
  }   ifstream in(argv[1], ios::in | ios::bianry);
  if(!in) {
    cout << "Cannot open file."};
    return 1;
  }   while(in) {  // 파일의 끝이 도달하면 0
    in.get(ch);
    cout << ch;
  }   return 0;
}

앞 절에서 논의했듯이, 파일의 끝에 도달하면, 파일과 연관된 스트림은 0이 된다. 그러므로, in이 파일의 끝에 도달하면, 이것은 0이 되며, 이로 인해서 while 루프가 정지된다.

파일을 읽어서 화면에 출력하는 루프를 실제로 다음과 같이 좀더 축약할 수도 있다.

while(in.get(ch))
  cout << ch;

이것은 get()이 스트림 in에 대한 참조를 리턴하기 때문에, 파일의 끝에 도달하면 in은 0이 된다.

다음 프로그램은 CHARS라는 파일에 0부터 255까지의 모든 문자들을 출력하는데 put()을 사용한다. 이미 알고 있겠지만, ASCII 문자들은 char에 의해서 저장될 수 있는 값들의 반에 대해서만 채워진다. 이외의 다른 값들은 일반적으로 확장된 문자 집합(extended character set)이라고 하며 외국어와 수학 기호와 같은 것들을 포함한다.(모든 시스템들이 확장된 문자 집합을 제공하는 것은 아니지만, 대부분은 제공한다.)

#include <iostream.h>
#include <fstream.h>   main()
{
  int i;
  ofstream out("CHARS", ios::out | ios::binary);   if(!out) {
    cout << "Cannot open output file.\n";
    return 1;
  }   // write all characters to disk
  for(i=0; i<256; i++) out.put((char) i);   out.close();   return 0;
}

여러분의 컴퓨터에서 확장된 문자 집합들이 무엇인지를 알기 위해서 CHARS 파일의 내용을 실행해 보는 것도 흥미있는 일일 것이다.

read()와 write()

2 진 자료의 블록들을 읽고 쓰는 두 번째 방법으로는 C++의 read()와 write() 함수를 이용하는 것이다. 이들의 원형은 다음과 같다.

istream &read(unsigned char *buf, int num);
ostream &write(const unsigned char *buf, int num);

read() 함수는 연관된 스트림으로부터 num만큼의 바이트를 읽어 buf가 지적하는 버퍼에 저장한다. write() 함수는 buf가 지적하고 있는 버퍼로부터 연관된 스트림으로 num만큼의 바이트를 쓴다.

주의 ) 제안된 ANSI C++ 표준은 정수형에 대한 typedef인 streamsize에 의해서 num 매개변수의 형을 지정한다. 현재, 대부분의 C++ 컴파일러는 간단히 위의 원형에서 보인 것처럼, num을 정수형으로 기술한다. 일반적으로 제안된 ANSI C++ 표준은 입력 또는 출력 연산으로 전송된 바이트 수를 기술하는 어떤 객체의 형으로 streamsize를 사용한다.

다음 프로그램은 디스크에 어떤 구조체를 쓰고, 이것을 다시 읽는다.

#include <iostream.h>
#include <fstream.h>
#include <string.h>   

struct status
{
    char name[80];
    float balance;
    unsigned long account_num;
};   

main()
{
    struct status acc;   strcpy(acc.name, "Ralph Trantor");
    acc.balance = 1123.23;
    acc.account_num = 34235678;   ofstream outbal("balance", ios::out | ios::binary);   if(!outbal)
    {
        cout << "Cannot open file.\n";
        return 1;
    }   outbal.write((unsigned char *) &acc, sizeof(struct status));
    outbal.close();   // 다시 읽기       ifstream inbal("balance", ios::in | ios::binary);
    if(!inbal)
    {
        cout << "Cannot open file.\n";
        return 1;
    }   inbal.read((unsigned char *) &acc, sizeof(struct status));   cout << acc.name << end1;
    cout<< "Account # " << acc.account_num;
    cout.precision(2);
    cout.setf(ios::fixed);
    cout << end1 << "Balance: $" << acc.balance;   inbal.close();
    return 0;
} 

여기서 알 수 있듯이, read()와 write()에 대한 한 번의 호출로 전체 구조체를 읽고 쓴다. 각각의 필드는 분리해서 읽고 쓸 필요가 없다. 이 예에서 알 수 있듯이, 버퍼는 어떤 형의 객체도 될 수 있다.

주의 ) read()와 write()에 대한 호출에서 형변환은 문자 배열로 정의되지 않은 버퍼에서 동작할 때 필요하다. C++의 강력한 형 검사 특성 때문에, 하나의 형에 대한 포인터는 다른 형에 대한 포인터로 자동적으로 변환되지 않는다.

num만큼의 문자들을 읽기 전에 파일의 끝에 도달하게 되면, read()는 정지하고, 버퍼는 사용할 수 있는 만큼의 문자들만을 포함한다. 또 다른 멤버 함수 gcount()를 이용하면 얼마나 많은 문자들을 읽었는지 알 수 있다. 다음은 이것에 대한 원형이다.

int gcount();

이것은 마지막으로 2진 형식의 입력 연산에 의해서 읽은 문자들의 수를 리턴한다. 다음 프로그램은 read()와 write()의 또 다른 예이며 gcount()를 사용하는 방법을 보여준다.

#include <iostream.h>
#include <fstream.h>
main(void)
{
    float fnum[4] = {99.75, -34.4, 1776.0, 200.1};
    int i;   ofstream out("numbers", ios::out | ios::binary);
    if(!out)
    {
        cout << "Cannot open file.";
        return 1;
    }   out.write((unsigned char *) &fnum, sizeof fnum);   out.close();   for(i=0; i<4; i++)  // 배열 초기화
        fnum[i] = 0.0;   ifstream in("numbers", ios::in | ios::binary);
    in.read((unsigned char *) &fnum, sizeof fnum);   // 얼마나 많은 바이트들을 읽었는지 보자   cout << in.gcount() << " bytes read\n";   for(i=0; i<4; i++)  // 파일에서 값을 읽어 출력
        cout << fnum[i] << " ";   in.close();   return 0;
}  

이 프로그램은 디스크에 부동 소수점 값들의 배열을 쓰고 이들을 다시 읽는다. read()를 호출한 후에, gcount()는 얼마나 많은 바이트들이 읽혀졌는지를 결정하기 위해서 사용된다.

또 다른 get() 함수

앞에서 보여진 형식과 함께, get() 함수는 여러 가지 방법으로 중복된다. 가장 일반적으로 사용된 중복 형식에 대한 2개의 원형은 다음과 같다.

istream &get(char *buf, int num, char delim='\n');
int get();

첫 번째 중복 형식은 num만큼의 문자들을 읽거나 또는 delim으로 명시된 문자에 도달할 때까지 buf에 의해서 지적된 배열로 문자들을 읽는다. buf에 의해서 지적된 배열은 get( )에 의해서 널(null) 문자로 끝난다. 만일 어떠한 delim도 명시되지 않았다면, 기본적으로 뉴라인 문자가 딜리미터(delimiter: 분할자)로 한다. 만일 딜리미터 문자가 입력 스트림에 있으면, 이것은 추출되지 않는다. 대신에, 다음 입력 연산 때까지 스트림에 남아 있는다. 두 번째 중복된 get() 형식은 특정 스트림에서 다음 문자를 리턴한다. 이것은 파일의 끝에 도달하면, EOF를 리턴한다. 이러한 형식의 get()은 C 언어의 getc() 함수와 유사한 것이다.

getline()

입력을 수행하는 또 다른 멤버 함수가 getline()이다. 다음은 이것의 원형이다.

istream &getline(char *buf, int num, char delim='\n');

여기서 알 수 있듯이, 이 함수는 가상적으로 get() 함수의 get(buf, num, delim) 형식과 동일한 것이다. 이것은 num만큼의 문자들을 읽거나, 또는 delim으로 표기된 문자에 도달할 때까지, 문자들을 읽고 이들을 buf에 의해서 지적된 배열에 저장한다. 특별히 지정되어 있지 않으면, delim은 내정적으로 뉴라인(‘\n') 문자이다. buf에 의해서 지적된 배열은 널(null) 문자로 끝난다. get(buf, num, delim)과 getline()의 차이점은 getline()이 입력 스트림으로부터 딜리미터(delimiter)까지를 읽고 제거한다는 것이다.

다음은 getline() 함수를 보여 주기 위한 프로그램이다. 이것은 텍스트 파일에서 한 번에 한 라인씩 내용을 읽고 이것을 화면에 출력한다.

// 텍스트 파일에서 한 라인씩 읽고 출력한다.  #include 
#include   main(int argc, char *argv[])
{
  if(argc!=2) {
    cout << "Usage: Display \n";
    return 1;
  }  ifstream in(argv[1]);  // 입력  if(!in) {
    cout << "Cannot open input file.\n";
    return 1;
  }  char str[255];  while(in) {
    in.getline(str, 255);  // delim은 내정적으로 '\n'이다.
    cout << str << end1;
  }  in.close();  return 0;
}

EOF 찾기

eof() 멤버 함수를 이용하면 파일의 끝을 찾을 수 있다. 다음은 이것의 원형이다.

int eof();

이것은 파일의 끝에 도달하면 0 아닌 값을, 그렇지 않으면 0을 리턴한다.

주의 ) 제안된 ANSI C++ 표준은 eof()의 리턴 형을 bool형으로 한다. 그러나, 현재 대부분의 C++ 컴파일러들은 bool형을 제공하지 못하고 있다. bool형은 자동적으로 int형으로 평가되기 때문에, 실제로는 eof()의 리턴 형이 bool형이거나 또는 int형이거나 관계는 없다.

다음 프로그램은 16진수와 ASCII 형식으로 파일의 내용을 출력하는데 eof()를 사용한다.

* ASCII¿I 16Aø¼o·I Æ?A¤ ÆAAIAC ³≫¿eA≫ Aa·ACN´U. */   #include <iostream.h>
#include <fstream.h>
#include <ctype.h>
#include <iomanip.h>
#include <stdio.h>   main(int argc, char *argv[])
{
    if(argc!=2)
    {
        cout << "Usage: Display <filename>\n";
        return 1;
    }   ifstream in(argv[1], ios::in | ios::binary);   if(!in)
    {
        cout << "Cannot open input file.\n";
        return 1;
    }   register int i, j;
    int count = 0;
    char c[16];   cout.setf(ios::uppercase);
    while(!in.eof())
    {
        for(i=0; i<16 && !in.eof(); i++)
        {     in.get(c[i]);
        }
        if(i<16) i--;  // ÆAAIAC ³¡AI i¹øA°¸| AeCI±a   for(j=0; j<i; j++)
            cout << setw(3) << hex << (int) c[j];
        for(; j<16; j++) cout << "  ";   cout << "\t";
        for(j=0; j<i; j++)
            if(isprint(c[j])) cout << c[j];
            else cout << ".";   cout << end1;   count++;
        if(count==16)
        {
            count = 0;
            cout << "Press ENTER to continue: ";
            cin.get();
            cout << end1;
        }
    }   in.close();   return 0;
}

이 프로그램을 실행하여 출력하면, 다음과 같은 첫 번째 출력 화면이 생성된다. 단, 이것은 위 프로그램 자체를 저장하고 있는 파일이다.

2F 2A 20 44 69 73 70 6C 61 79 20 63 6F 6E 74 65 /* Display conte
6E 74 73 20 6F 66 20 73 70 65 63 69 66 69 65 64 nts of specified
20 66 69 6C 65  D  A 20 20 20 69 6E 20 62 6F 74  file..   in bot
68 20 41 53 43 49 49 20 61 6E 64 20 69 6E 20 68 h ASCII and in h
65 78 2E  D  A 2A 2F  D  A 23 69 6E 63 6C 75 64 ex...*/..#includ
65 20 3C 69 6F 73 74 72 65 61 6D 2E 68 3E  D  A e ..
23 69 6E 63 6C 75 64 65 20 3C 66 73 74 72 65 61 #include ..#include <
63 74 79 70 65 2E 68 3E  D  A 23 69 6E 63 6C 75 ctype.h>..#inclu
64 65 20 3C 69 6F 6D 61 6E 69 70 2E 68 3E  D  A de ..
23 69 6E 63 6C 75 64 65 20 3C 73 74 64 69 6F 2E #include ....main(int a
72 67 63 2C 20 63 68 61 72 20 2A 61 72 67 76 5B rgc, char *argv[
5D 29  D  A 7B  D  A 20 20 69 66 28 61 72 67 63 ])..{..  if(argc
21 3D 32 29 20 7B  D  A 20 20 20 20 63 6F 75 74 !=2) {..   cout
20 3C 3C 20 22 55 73 61 67 65 3A 20 44 69 73 70  << "Usage: Disp
Press ENTER to continue:

ignore() 함수

입력 스트림으로부터 문자들을 읽고 무시하기 위해서 ignore() 함수를 사용할 수 있다. 다음은 이것의 원형이다.

istream &ignore(int num=1, int delim=EOF);

이것은 num만큼의 문자들을 무시하거나(내정값은 하나의 문자), 또는 delim에 의해서 지정된 문자(내정은 EOF)를 만날 때까지 문자들을 읽고 무시한다. 만일 딜리미터 문자를 만나면, 입력 스트림으로부터 제거되지 않는다. 다음 프로그램은 TEST라는 파일을 읽는다. 여기서는 공란을 만나거나, 또는 10개의 문자들을 읽어 무시하고 난 다음에, 파일의 나머지를 출력한다.

#include <iostream.h>
#include <fstream.h>   main()
{
    ifstream in("test");   if(!in)
    {
        cout << "Cannot open file.\n";
        return 1;
    }   /* 10개 문자 또는 첫 번째 공란을 만날 때까지 무시 */
    in.ignore(10, ' ');   char c;
    while(in)
    {
        in.get(c);     cout << c;
    }   in.close();
    return 0;
}

peek()와 putback() 함수

입력 스트림에서 문자를 제거하지 않고 다음 문자를 얻고자 할 때 peek()를 이용한다. 다음은 이것의 원형이다.

int peek();

이것은 스트림에서 다음 문자를 리턴하거나 또는 파일의 끝을 만나면, EOF를 리턴한다.

스트림으로부터 읽은 마지막 문자를 다시 그 스트림에게 리턴할 때 putback()을 이용한다. 이것의 원형은 다음과 같다.

istream &putback(char c);

여기서 c는 읽은 마지막 문자이다.

flush() 함수

자료를 출력할 때, 그 자료는 특정 스트림에 연결된 물리 장치에 즉시 출력되는 것이 아니라 내부 버퍼에 저장되고 이 버퍼가 다 차고 나면, 이 버퍼의 내용들만이 디스크에 저장된다. 그러나, 버퍼가 차기 전에 정보를 강제로 물리적인 디스크로 출력할 수 있도록 하는 것이 flush()이다. 다음은 이것의 원형이다.

ostream &flush();

flush()는 특정 프로그램이 불안한 환경에서 사용될 경우 버퍼 저장을 보장하기 위해서 이용된다. (예를 들면, 자주 전기가 꺼지는 상황에서)

주의) 파일을 닫거나 정상적인 프로그램 종료시에도 모든 버퍼들을 삭제한다.

임의 접근

C++의 입출력 시스템에서는 seekg()와 seekp() 함수를 이용하여 임의 접근(random access)이 가능하다. 이들의 가장 일반적인 형식은 다음과 같다.

istream &seekg(streamoff offset, seek_dir origin);
ostream &seekp(streamoff offset, seek_dir origin);

여기서, streamoff는 offset이 가질 수 있는 최대 유효값을 포함할 수 있으며, IOSTREAM.H에 정의된 형이다. 또한 seek_dir은 다음의 값들을 가지는 나열형이다.

  ios::beg
  ios::cur
  ios::end

C++ 입출력 시스템은 파일과 관련하여 2개의 포인터들을 관리한다. 하나는 입력 포인터(get pointer)로서, 이것은 파일에 다음 입력 연산이 발생할 장소를 지정한다. 또 다른 하나는 출력 포인터(put pointer)로서, 파일에서 다음 출력 연산이 일어날 장소를 지정한다. 입력 또는 출력 연산이 일어날 때마다, 적절한 포인터가 순차에 따라 자동적으로 전진한다. 그러나, seekg()와 seekp() 함수를 이용하면 파일을 비순차적으로 접근할 수 있다.

seekg() 함수는 관련된 파일의 현재 입력 포인터로 지정된 origin으로부터 offset 바이트 수만큼 떨어진 위치로 이동한다. 이때 origin은 다음 3개의 값 중 하나가 된다.

ios::beg  파일의 시작 위치
ios::cur  현재 위치
ios::end  파일의 끝

seekp() 함수는 관련된 파일의 현재 출력 포인터를 위의 값들 중 하나로 지정된 origin으로부터 offset 바이트수 만큼 떨어진 곳으로 이동시킨다.

다음 프로그램은 seekp() 함수를 보여준다. 이것은 사용자에게 파일에서 특정한 문자를 변경할 수 있도록 한다. 사용자는 특정 파일 이름, 변경하기를 원하는 바이트 위치, 새로운 문자 등을 명령어 라인에 명시한다. 여기서 파일은 읽기/쓰기 연산으로 열린다.

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>   
main(int argc, char *argv[])
{
    if(argc!=4)
    {
        cout << "Usage: CHANGE <filename> <byte> <char>\n";
        return 1;
    }   fstream out(argv[1], ios::in | ios::out | ios::binary);
    if(!out)
    {
        cout << "Cannot open file.";
        return 1;
    }   out.seekp(atoi(argv[2], ios::beg);
    out.put(*argv[3]);
    out.close();   return 0;
}  

예를 들어, TEST라는 파일에서 12번째 바이트를 Z로 변경하고자 할 때 이 프로그램을 사용하며, 이것은 다음의 명령어 라인을 사용한다.

change test 12 Z

다음 프로그램은 seekg()를 사용한다. 이것은 명령어 라인에서 사용자가 지정한 위치에서 시작해서 파일의 내용을 출력한다.

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>   
main(int argc, char *argv[])
{
    char ch;   if(argc!=3)
    {
        cout << "Usage: SHOW <filename> <starting location>\n";
        return 1;
    }   ifstream in(argv[1], ios::in | ios::binary);
    if(!in)
    {
        cout << "Cannot open file.\n";
        return 1;
    }   in.seekg(atoi(argv[2]), ios::beg);   while(in.get(ch))
        cout << ch;   return 0;
}

다음 프로그램은 파일에서 첫 번째 만큼의 문자들을 역순으로 하기 위해서 seekp()와 seekg() 둘 다를 이용한 것이다.

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>   main(int argc, char *argv[])
{
    if(argc!=3)
    {
        cout << "Usage: Reverse <filename> <num>\n";     return 1;
    }   fstream inout(argv[1], ios::in | ios::out | ios::binary);   if(!inout)
    {
        cout << "Cannot open input file.\n";
        return 1;
    }   long e, i, j;
    char c1, c2;
    e = atol(argv[2]);   for(i=0, j=e; i<j; i++, j--)
    {
        inout.seekg(i, ios::beg);
        inout.get(c1);
        inout.seekg(j, ios::beg);
        inout.get(c2);   inout.seekp(i, ios::beg);
        inout.put(c2);
        inout.seekp(j, ios::beg);
        inout.put(c1);
    }   inout.close();
    return 0;
}

이 프로그램을 이용하기 위해서, 역순으로 하기를 원하는 파일의 이름, 바꿀 문자들의 수 등을 지정한다. 예를 들어, TEST 파일의 첫 번째 10개 문자들을 역순으로 하기 위해서는 다음과 같이 사용해야 한다.

reverse test 10

만일 파일 내용이 “This is a test”라고 하면, 프로그램을 실행한 후에는 파일이 다음과 같이 바뀔 것이다.

a si sihTtest

현재 파일 위치 얻기

각 각의 파일 포인터의 현재 위치는 다음의 함수들을 이용하여 결정된다.

streampos tellg();
streampos tellp();

여기서, streampos는 2개의 각 함수가 리턴하는 최대값을 저장할 수 있는 IOSTREAM.H에 정의된 형이다. tellg()와 tellp()에 의해서 리턴된 값들을 각각 seekg()와 seekp()의 다음 형식에서 인수로 사용할 수 있다.

iostream &seekg(streampos pos);
iostream &seekp(streampos pos);

이 함수들은 현재 파일 위치를 저장하며, 다른 파일 연산들을 수행하게 한 후, 파일 위치를 이전에 저장된 위치로 재설정한다.

입력/출력 상태

C++ 입출력 시스템은 각 입출력 연산의 결과에 대한 상태 정보를 유지한다. 입출력 시스템의 현재 상태는 정수형으로 저장되며, 다음과 같은 플래그(flags)들로 코드화된다.

이름            의미
eofbit   ==>  파일의 끝을 만날 때는 1  아니면, 0
failbit   ==>  심각하지 않은 입출력 오류가 발생했을 때는 1  아니면, 0
badbit  ==>  심각한 입출력 오류가 발생할 때는 1  아니면, 0

이 플래그들은 ios 내에 열거형으로 되어 있다. ios에는 또한 0 값을 가진 goodbit도 정의되어 있다.

입출력 상태 정보를 얻을 수 있는 방법은 두 가지가 있다. 첫째로, rdstate() 멤버 함수를 호출하는 것이다. 다음은 이것의 원형이다.

int rdstate();

이것은 정수로 코드화된 오류 플래그의 현재 상태를 리턴한다. 앞에서 논의한 플래그들의 목록을 보고 짐작은 하겠지만, rdstate()는 오류가 발생하지 않았을 때 0을 리턴한다. 그렇지 않으면, 오류 비트가 (1로) 설정된다.

주의) 제안된 ANSI C++ 표준은 rdstate()의 리턴형을 어떤 형태의 정수에 대한 typedef인 iostate로 명시한다. 현재, 대부분의 C++ 컴파일러들은 rdstate() 함수의 리턴 형을 정수형(int)으로 명시하고 있다.

다음 프로그램은 rdstate()를 설명하고 있다. 이것은 특정 텍스트 파일의 내용을 출력한다. 만일 오류가 발생하면, checkstate()를 사용하여 이것을 보고한다.

#include <iostream.h>
#include <fstream.h>   

void checkstatus(ifstream &in);   main(int argc, char *argv[])
{
    if(argc!=2)
    {
        cout << "Usage: Display <filename>\n";
        return 1;
    }   ifstream in(argv[1]);   if(!in)
    {
        cout << "Cannot open input file.\n";
        return 1;
    }   char c;     while(in.get(c))
    {
        cout << c;
        checkstatus(in);
    }   checkstatus(in);  // 마지막 상태 검토
    in.close();
    return 0;
}   void checkstatus(ifstream &in)
{
    int i;   i = in.rdstate();   if(i & ios::eofbit)
        cout << "EOF encountered\n";
    else if(i & ios::failbit)
        cout << "Non-Fatal I/O error\n";
    else if(i & ios::badbit)
        cout << "Fatal I/O error\n";
}

이 프로그램은 항상 하나의 “오류”를 보고한다. while 반복문이 끝난 후에, 기대한 것처럼, checkstate() 함수에 대한 마지막 호출은 EOF를 만났다고 보고한다. 이러한 checkstate() 함수는 프로그램 작성시에 유용하게 이용될 수 있다.

오류가 발생했는지를 알 수 있는 또 다른 방법은 다음 중의 하나 또는 그 이상의 함수를 사용하는 것이다.

int bad();
int eof();
int fail();
int good();

eof() 함수는 앞에서 설명했다. bad() 함수는 badbit가 설정되면 참 값을 리턴한다. fail() 함수는 failbit가 설정되면 참 값을 리턴한다. good() 함수는 오류가 발생하지 않으면 참 값을 리턴한다. 그렇지 않으면, 이들 모두 거짓 값을 리턴한다.

주의) 제안된 ANSI C++ 표준은 bad(), eof(), fail(), good()의 리턴형을 bool형으로 규정하고 있다. 그러나, 현재 대부분의 C++ 컴파일러들은 이들의 리턴 형을 정수형(int)으로 규정한다. bool형은 어떤 연산문에서 자동적으로 int 형으로 평가하기 때문에, 실제로는 이러한 차이점은 무관한 것이다.

일단 오류가 발생하게 되면, 프로그램을 계속하기 전에 이 오류들을 제거할 필요가 있다. 이것을 수행하기 위해서, clear() 함수를 사용한다. 다음은 이것에 대한 원형이다.

void clear(int flags=0);

flags가 0(이것이 내정값이다)이면, 모든 오류 플래그들은 해제된다(0으로 재설정). 그렇지 않으면, flags를 해당 플래그들이나 또는 해제하고자 하는 값들로 설정한다.

사용자 정의 입출력과 파일

17장에서 사용자가 정의한 클래스와 관련된 삽입과 추출 연산자를 중복시키는 방법을 공부했다. 그 장에서는 단지 콘솔 입출력만을 기술했다. 그러나, 모든 C++ 스트림들은 동일하기 때문에, 같은 중복된 삽입자 함수를 사용하여 어떠한 변경도 없이 그대로 화면이나 또는 파일에 출력할 수 있다. 예를 들어, 다음 프로그램은 17장의 전화번호책의 예를 가지고 다시 작업한 것이며, 이것은 목록을 디스크에 저장하고 있다. 이 프로그램은 매우 간단하다. 즉, 이것은 목록에 이름들을 추가하거나 또는 화면에 목록을 출력할 수 있도록 한다. 그러나, 이것은 특정 번호를 찾고 원하지 않는 번호를 삭제하는 등의 프로그램으로 향상시킬 수도 있을 것이다.

#include <iostream.h>
#include <fstream.h>
#include <string.h>   class phonebook
{
    char name[80];
    char areacode[4];
    char prefix[4];     char num[5];
public:
    phonebook() { };
    phonebook(char *n, char *a, char *p, char *nm)
    {
        strcpy(name, n);
        strcpy(areacode, a);
        strcpy(prefix, p);
        strcpy(num, nm);
    }
    friend ostream &operator<<(ostream &stream, phonebook o);
    friend istream &operator>>(istream &stream, phonebook &o);
};   // 이름과 전화번호 출력
ostream&operator<<(ostream &stream, phonebook o)
{
    stream << o.name << " ";
    stream << "(" << o.areacode << ") ";
    stream << o.prefix << "-";
    stream << o.num << "\n";
    return stream;  // must return stream
}   // 이름과 전화번호 입력
istream &operator>>(istream &stream, phonebook &o)
{
    cout << "Enter name: ";
    stream >> o.name;
    cout << "Enter area code: ";
    stream >> o.areacode;
    cout << "Enter prefix: ";
    stream >> o.prefix;
    cout << "Enter number: ";
    stream >> o.num;
    cout << "\n";
    return stream;
}   main()
{
    phonebook a;
    char c;   fstream pb("phone", ios::in | ios::out | ios::app);   if(!pb)
    {
        cout << "Cannot open phone book file.\n";     return 1;
    }   for(;;)
    {
        do
        {
            cout << "1. Enter numbers\n";
            cout << "2. Display numbers\n";
            cout << "3. Quit\n";
            cout << "\nEnter a choice: ";
            cin >> c;
        }
        while(c<'1' || c>'3');   switch(c)
        {
        case '1':
            cin >> a;
            cout << "Entry is: ";
            cout << a;  // show on screen
            pb << a;  // write to disk
            break;
        case '2':
            char ch;
            pb.seekg(0, ios::beg);
            while(!pb.eof())
            {
                pb.get(ch);
                cout << ch;
            }
            pb.clear();  // reset eof
            cout << end1;
            break;
        case '3':
            pb.close();
            return 0;
        }
    }
}

여기서 어떠한 변경도 없이 디스크 파일이나 또는 화면에 출력하는데 중복된 « 연산자를 사용할 수 있다는 것을 주목하자. 이것은 C++의 입출력에 대한 접근 방식에서 가장 중요하고 가장 유용한 특성들 중의 하나이다.

반응형