관리 메뉴

Kim's Programming

C언어 - 파일 입출력(1/3) 본문

Programming/C

C언어 - 파일 입출력(1/3)

Programmer. 2015. 9. 12. 00:11

파일


정보의 저장


파일은 디스크에 정보가 저장되는 단위이며 고유의 이름을 가집니다. 파일에는 프로그램이 작성한 정보가 저장이 되는데 워드 프로세서는 문서 파일을 만들고 그래픽 프로그램은 그림을 만드며 컴파일러는 소스파일을 만듭니다. 프로그램이 실행 중에 파일을 엑세스하는 경우가 많은데 이번에는 디스크에 있는 파일을 읽거나 쓰고 관리하는 방법을 알아보겠습니다. 프로그램은 실행에 필요한 코드는 가지지만 모든 데이터를 가지지는 않습니다. 실행 파일의 크기에는 제약이 있기 떄문에 모든 정보를 다 가질 수 없으며 그래서 큰 정보는 외부의 파일에 두고 실행중에 읽어서 사용하는 방법을 씁니다.


또한 프로그램이 작업결과를 영구적으로 저장하기 위해서도 파일을 사용합니다. 편집하던 문서나 작성중인 프로그램 소스 등도 파일의 형태로 디스크에 저장되어야 합니다. 메모리는 아무리 빠르고 정확하더라도 전원이 없으면 기억된 내용을 잃어버리기 때문에 영구적인 정보 저장 목적으로 사용할 수 없으며 현실적으로는 파일 이외에는 정보를 저장할 대안이 없습니다. 파일은 CPU나 메모리에 존재하지 않으며 HDD나 CD등의 외부 미디어에 기록되어있습니다. 프로그램이나 이런 기계적인 장치를 직접 움직여서 파일을 엑세스 하는 것은 비효율적이며 현실적으로 불가능합니다. 그래서 운영체제나 컴파일러는 파일을 엑세스할 수 있는 함수를 제공하며 프로그램은 이 함수를 호출해서 원하는 파일을 읽고 씁니다. 파일을 엑세스하는 방법에는 여러가지 종류가 있습니다.


    1. 고수준 입출력 스트림 사용 : C라이브러리가 제공하는 파일 입출력 방법이며 성능은 조금 떨어지지만 사용하기는 쉽습니다. 표준에 의해 함수의 형태가 고정되어 있으므로 이식에 유리합니다.

    2. 저수준 파일 핸들 : C 라이브러리가 제공하는 입출력 방법이며 대규모의 데이터를 다룰 때 편리합니다.

    3. C++스트림 객체 : ifstream, ofstream 등의 입출력 객체와 그 멤버 함수를 사용하여 파일을 엑세스합니다.

    4. 운영체제가 제공하는 API함수 : 파일이 저장되는 디스크의 관리 주체는 운영체제이며 운영체제는 응용 프로그램을 위해 파일 관련 API함수를 제공합니다. 윈도우즈는 CreateFile, ReadFile, WriteFile등의 함수를 제공하며 이를 사용하면 파일을 엑세스 할 수 있습니다.

    5. 클래스 라이브러리가 제공하는 파일 엑세스 객체 사용 : 고수준 클래스 라이브러리들은 파일 엑세스 기능을 캡슐화한 클래스를 제공하며 이 클래스를 사용하면 쉽게 파일을 다룰 수 있습니다. MFC의 경우 CFile 클래스를 제공합니다.

방법이 너무 많습니다. 여기서는 C 컴파일러가 제공하는 두가지 방법만 알아보겠습니다.

정보의 저장


C언어가 제공하는 파일 입출력 방식은 고수준과 저수준 2가지가 있습니다. 저수준과 고수준으로 나누는 것은 사람에게 얼마나 가깝게 작동하는지를 나타내지 좋고 나쁨을 나타내지는 않습니다. 다음 표는 고수준과 저수준의 특징입니다.


 

 고수준

 저수준

 버퍼 사용

 사용

 메모리로 직접 읽어들임

 입출력 대상

 스트림

 파일 핸들

 속도

 느리다.

 빠르다.

 문자 단위 입출력

 가능

 가능하지만 비효율적

 난이도

 비교적 쉽다.

 조급 어렵다.

 세밀한 조작

 어렵다

 가능하다.


두 방식의 가장 큰 차이점은 버퍼를 쓰는가 그렇지 않은가 하는 점입니다. 나머지 차이점은 버퍼의 사용 유무에 따라 파생되는 특성들입니다. 버퍼는 파일로부터 입출력되는 데이터를 잠시 저장하는 메모리 영역입니다. 파일이 저장되는 하드 디스크는 모터에 의해 기계적으로 구동되므로 전자적으로 동작하는 메모리보다 상대적으로 느립니다. 그래서 가급적이면 하드 디스크를 엑세스 하는 횟수를 줄이기 위해 버퍼를 사용합니다. 파일을 문자 단위로 읽을 때 버퍼없이 하드 디스크를 액세스한다면 한 번 회전할때 한 바이트밖에 읽지 못하므로 백만 바이트를 읽기 위해 하드디스크가 백만번 회전할 때까지 기다려야 할 것이고 파일 엑세스 속도는 형편없을 적도로 느려집니다. 한번 읽을 때 미리 주변 데이터를 버퍼에 읽어 놓으면 다음 읽기 요청은 하드 디스크 엑세스없이 버퍼에서 바로 읽을 수 있습니다.


저수준과 고수준은 버퍼의 사용 유무에 따라 속도에 약간의 차이가 있기는 하지만 사실 현대의 컴퓨터의 환경에서는 무시할 정도입니다. 그래서 최근엔 저수준 파일 입출력이 큰 이점은 없습니다.


고수준 파일 입출력


스트림


스트림(Stream)이라는 용어는 흐름을 의미합니다. 물흘러 가듯이 바이트들이 순서대로 입출력되는 논리적인 장치를 스트림이라고 합니다. 파일에도 바이트들이 저장되어 있으며 읽을 때나 쓸 때 순서대로 바이트들이 입출력되므로 스트림이라고 할 수 있습니다. 키보드, 화면, 프린터 등의 물리적인 장비들도 바이트들이 순서대로 흘러 다니므로 일종의 스트림입니다.


대부분의 운영체제는 키보드와 화면(Console), 프린터 등을 스트림이라는 동질적인 장치로 다루며 파일과 같은 방법으로 입출력합니다. 파일과 키보드, 화면 등의 장치는 서로 제어하는 방법이 다르지만 스트림이라는 논리적으로 동등한 장치로 표현되기 때문에 동일한 방법으로 입출력할 수 있습니다. 가령 파일에 쓰듯이 콘솔에 쓰면 화면에 문자열이 출력되며 같은 방법으로 입출력할 수 있습니다. 가령 파일에 쓰듯이 콘솔에 쓰면 화면에 문자열이 출력되며 같은 방법으로 프린터로 출력하면 문서가 인쇄됩니다.


화면, 프린터, Serial Port등을 파일과 같이 동등한 장치로 취급하면 일관된 방법으로 스트림간의 데이터를 복사할 수 있으며 사용하는 한 명령을 여러 장치에 똑같이 적용할 수 있습니다. 도스에서 copy con a.txt명령은 콘솔로부터 입력된 문자열을 a.txt라는 파일로 복사하라는 명령으로 사용되며 copy a.txt prn은 a.txt를 프린터로 복사함으로써 문서를 인쇄합니다.


스트림을 내부에 입출력버퍼를 가지고 있으며 이 버퍼는 스트림에 의해 자동으로 관리되므로 프로그래머는 버퍼를 준비하거나 관리할 필요가 없습니다. 어떤 스트림으로부터 얼마만큼의 데이터를 읽거나 쓰고 싶다는 최소한의 의사 표현만 하면 필요한 동작은 내부 스트림이 알아서 수행합니다. 그래서 스트림을 통한 입출력방법은 사용하기 쉬우며 고수준이라고 합니다.


스트림의 현재 상태는 FILE구조체에 기억이됩니다. 이 구조체는 stdio.h에 다음과 같이 정의되어 있습니다. 물론 운영체제에 따라 달라지기도 합니다.

1
2
3
4
5
6
7
8
9
10
11
struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
        };
typedef struct _iobuf FILE;
cs

이 구조체에는 스트림이 내부적으로 사용하는 버퍼, 버퍼의 크기와 현재 위치, 엑세스하고 있는 파일의 이름등이 저장됩니다. 파일 입출력 함수들은 모두 이 구조체의 내용을 참조하여 동작하도록 되어 있으며 사용자는 이 구조체의 멤버를 직접 다룰 필요가 없습니다. 파일 입출력을 하기 전에 이 구조체의 포인터를 하나 선언하고 입출력 함수에게 포인터만 넘겨주면 됩니다.


파일 열기 


파일을 엑세스하려면 먼저 대상 파일을 열어야(Open)합니다. 파일을 오픈한다는 것은 파일 입출력을 하기위한 준비를 한다는 뜻입니다. 스트림 입출력을 위해서는 파일의 데이터를 잠시 저장하는 내부 버퍼가 필요하며 파일의 현재 위치(FP)를 초기화해야 하는데 이런 준비 하는 과정이 오픈입니다. 파일을 오픈할 떄는 다음 함수를 이용합니다.

1
FILE *fopen(const char *filename, const char *mode);
cs

이 함수는 지정한 파일을 엑세스하기 위한 준비를 하며 이 정보들을 가지는 FILE형 구조체를 생성하고 그 포인터를 잘 받아 두었다가 이후 입출력 함수로 전달해 주면 됩니다. fopen 함수의 인수에 대해 알아보겠습니다.


::파일 이름 


엑세스할 대상 파일의 이름입니다. 필요할 경우 드라이브와 디렉토리 경로를 전달할 수 있으며 현재 위치를 기준으로 한 상대 경로를 지정할 수도 있습니다. 경로가 생략되고 파일 이름만 주어질 경우 현재 디렉토리에서 파일을 찾습니다. 디렉토리 구분자로 사용되는 역슬레시 문자는 문자열 내에서 확장열 표시에 사용되므로 반드시 \\(\\)로 써야합니다.


::모드


모드는 파일을 어떻게 열 것인지를 지정하며 파일을 열어서 어떤 작업을 할 것인가에 따라 달라집니다. mode인수에 다음 문자들의 조합을 지정합니다.


모드 

 설명

 r

 읽기 전용으로 파일을 엽니다. 이 모드로 연 파일은 읽을 수만 있으며 데이터를 기록하지는 못합니다. 파일이 없으면 에러가 리턴됩니다.

 w

 쓰기 위해 파일을 엽니다. 이 모드로 연 파일은 쓰기만 가능하며 읽지는 못합니다. 도스나 윈도우즈의 파일은 쓰기 전용 속성이 없지만 스트림이 쓰기 전용 상태로 열 수 있습니다. 파일이 없으면 새로 만들고 이미 존재하면 기존것은 지워집니다.

 a

 추가를 위해 파일을 엽니다. 추가란 파일의 끝에 다른 정보를 더 써넣는다는 뜻입니다. 이 모드로 연 파일은 오픈 직후에 FP가 파일의 끝으로 이동합니다. 파일이 없으면 새로 만듭니다.

 r+

 읽고 쓰기가 가능하도록 파일을 엽니다. 파일이 없을 경우 에러가 리턴됩니다.

 w+

 읽고 쓰기가 가능하도록 파일을 엽니다. 파일이 없을 경우 새로 만듭니다.

 a+

 읽기와 추가가 가능하도록 파일을 엽니다. 파일이 없으면 새로 만듭니다.


::모드


fopen의 두 번쨰 인수 mode에는 오픈 모드 외에도 파일의 형태를 지정하는 플래그를 추가로 지정할 수 있습니다. 열고자 하는 파일이 텍스트 파일이면 t를 붙이고 이진 파일이면 b를 붙입니다. 파일 형태에 아무런 지정이 없으면 전역변수 _fnmod의 값이 사용됩니다. 이진 파일은 아무런 변환없이 읽혀지지만 텍스트 파일모드로 파일을 열면 다음 두 가지 변환을 합니다.


  1. 개행 코드를 의미하는 CR/LF조합은 LF로 변환되어 읽혀지며 LF를 기록하면 CR/LF가 출력됩니다. 이런 변환을 해주는 이유는 C 문자열 함수들은 개행을 위해 확장열 LF(\n)를 사용하기 떄문입니다.
  2. 파일의 끝을 나타내는 Ctrl+z(0x1A)는 EOF(-1)로 변환되어 읽혀집니다. 단 "a+"모드로 열었을 때는 끝부분에 데이터를 추가할 수 있도록 Ctrl+Z를 제거합니다.
오픈 모드와 파일 형태가 mode 인수에 같이 기록되는데 오픈 모드가 먼저 오고 파일 형태가 뒤에 오는 형식으로 써야합니다. 단, +문자는 파일 형태 다음에 와도 상관없습니다. 다음이 mode 인수의 예인데 문자열 이므로 반드시 겹따옴표로 싸줘야합니다.

  • "rt" : 텍스트 파일을 읽기 전용으로 엽니다.

  • "wb" : 이진 파일을 쓰기 전용으로 엽니다.

  • "r+b" : 이진 파일을 읽기, 쓰기 가능하도록 엽니다. "rb+"로 쓸 수도 있습니다.

이외에도 mode 인수에는 캐시를 관리하는 방법과 임시 파일 생성에 대한 몇 가지 플래그를 더 지정할 수 있습니다.

::리턴값

fopen은 지정한 파일을 지정한 모드로 열고 입출력에 필요한 FILE구조체를 생성한 후 그 포인터를 리턴합니다. 만약 에러가 발생하면 NULL을 리턴합니다. 파일 입출력시에는 여러 가지 이유로 에러가 발생할 수 있으므로 이 함수의 리턴값은 반드시 점검해 보아야 합니다. 파일이 없거나 디스크 용량이 부족하거나 착탈식 미디어의 문이 열려 있거나 하는 경우등등 여러가지로 에러가 발생할 수 있습니다. 그래서 fopen함수는 통상 다음과 같은 방법으로 사용합니다.
1
2
3
4
5
6
File f;
= fopen("C:\\DATA\\제목.txt","rb");
if(f == NULL)
{
    printf("지정한 파일이 없습니다.\n");
}
cs
fopen으로 파일 열기에 성공했으면 각종 입출력 함수를 활용하여 파일의 데이터를 엑세스할 수 있습니다. 파일을 다 사용한 후에는 파일을 닫아줘야 하는데 이떄는 이 함수를 사용합니다.
1
int fclose(FILE *stream)
cs
이 함수는 버퍼에 남아있는 데이터를 파일로 완전히 출력(flush)하고 파일 입출력을 위해 내부적으로 생성했던 FILE 구조체를 해제합니다. 다 사용한 파일은 이 함수로 반드시 닫아줘야합니다.

파일 엑세스

파일을 열었으면 파일안의 내용을 읽고 씁니다. 먼저 간단한 포맷인 문자열부터 입출력해보겠습니다. 이때는 두 함수를 사용합니다. 모든 입출력 함수는 대상 스트림을 전달하기 위해 FILE형 구조체 포인터를 인수로 취한다는 점에서 공통적입니다.
1
2
char *fgets(char *string, int n, FILE *stream);
int fputs(const char *string, FILE *stream);
cs
fgets가 파일에서 문자열을 읽어들이는 함수입니다. 읽어들인 문자열을 저장할 버퍼를 첫 번쨰 인수로 주고 두 번쨰 인수로 이 버퍼의 크기를 알려줍니다. fgets는 최초의 개행 문자를 만날 때까지 또는 버퍼의 길이만큼 문자열을 읽어들이므로 이 함수를 반복적으로 호출하면 텍스트 파일을 줄 단위로 읽을 수 있습니다. 텍스트 파일의 한 줄은 통상 80문자 정도 되므로 버퍼는 256 정도면 충분합니다. 만약 읽는 도중에 에러가 발생했거나 파일 끝에 도달했으면 NULL을 리턴합니다. fputs는 첫 번쨰 인수로 전달된 문자열을 파일로 출력하는데 중간에 개행 문자가 있더라도 한꺼번에 출력합니다. 만약 중간에 널 문자를 만나면 널 종료 문자 앞까지만 출력합니다. 다음 소스는 새로운 파일을 만들고 이 파일에 두 줄로 된 텍스트를 출력합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void main()
{
    FILE *f;
    char *str = "이 파일은 C Standard Function으로 작성된 텍스트 파일입니다.\n"
        "D 드라이브의 루트 디렉토리에 Test.txt 파일로 생성됩니다.\n";
    f = fopen("D:\\Test.txt""wt");
    if (f != NULL)
    {
        fputs(str, f);
        fclose(f);
    }
}
cs

파일 입출력을 위해 FILE 구조체의 포인터 f를 선언하고 fopen으로 파일을 열되 w(쓰기) 모드로 열고 파일 형태는 t(텍스트)로 주어 텍스트 파일로 만들도록 했습니다. 파일이 생성 되었으면 fputs함수로 str 문자열을 파일로 출력하고 fclose 함수로 파일을 닫았습니다. str 문자열 내에 개행코드는 \n으로 기록되어 있지만 텍스트 모드로 파일을 생성했기떄문에 fputs가 실제로 파일에 출력하는 코드는 \r\n코드입니다. 확인해 보면 D드라이브에 문자열이 들어간 텍스트 파일이 생성되어 있을 겁니다(관리자 권한이 필요한 C루트 대신 관리자 권한없이 수정이 가능한 D 루트에 생성을 하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
 
void main()
{
    FILE *f;
    char buffer[128];
 
    f = fopen("D:\\test.txt""rt");
    if (f != NULL)
    {
        for (;;)
        {
            if (fgets(buffer, 256, f) == NULL)
                break;
        }
        printf("%s", buffer);
    }
    fclose(f);
    
}
cs

fopen으로 읽을 파일을 열되 이번에는 r(읽기) 모드로 열었으며 fgets를 이용 한줄 씩 읽어 화면에 출력하기를 파일 끝까지 반복합니다. 읽기에 사용하는 buffer는 256의 크기만큼 선언했고 fgets 함수의 2번쨰 인수로 256까지 읽어 들이도록 설정하였는데 물론 이값은 2이상의 어떤 값으로도 설정이 가능하지만 너무 작으면 조금씩 여러번 읽어 들이게 되므로 전체적인 읽기 성능이 떨어지게 됩니다. 그렇기 떄문에 충분한 크기를 주는 것이 좋습니다. feof함수는 인수로 주어진 스트림의 끝(EOF = End Of File) 까지 읽었는지를 조사합니다.

1
int feof(FILE *stream);
cs

이 함수가 TRUE를 리턴할 떄까지 반복적으로 fgets 함수를 호출하면 파일의 끝까지 모든 내용을 읽을 수 있습니다. 다음 두 함수는 스트림으로부터 문자를 하나씩 입충력합니다.

1
2
int fgets(FILE *stream);
int fputs(int c, FILE *stream);
cs

입출력 위치는 물론 FP이며 현재 위치에서 한 문자를 읽거나 문자 하나를 출력합니다. 다음 두 함수는 블록 단위로 입출력합니다.

1
2
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
size_t fread(const void *buffer, size_t size, size_t count, FILE *stream);
cs

두 함수의 인수를 동일한데 buffer에 저장된 size크기의 메모리 블록 count개를 스트림으로 입출력하며 실제로 입출력한 길이를 리턴합니다. 대개의 경우 지정한 크기만큼 입출력하지만 파일의 끝 부분을 읽거나 디스크가 가득 찼을 때는 더 작은 크기만 입출력할 수도 있습니다. 이 두 함수를 사용하면 구조체의 배열이나 임의 타입의 집합을 한꺼번에 스트림으로 입출력 할 수 있습니다. 다음 소스는 파일을 읽어서 다른 파일을 생성합니다.

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
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
 
void main()
{
    FILE *src, *dest;
    char buffer[256];
    size_t nRead;
 
    src = fopen("D:\\Test.txt""rb");
    if (src != NULL)
    {
        dest = fopen("D:\\test2.txt""wb");
        if (dest != NULL)
        {
            while (!feof(src))
            {
                nRead = fread(buffer, 1256, src);
                fwrite(buffer, 1, nRead, dest);
            }
            fclose(dest);
        }
        fclose(src);
    }
}
cs

src로 복사할 원본을 r(읽기)모드로 열고 dest는 w(쓰기)모드로 열고 src의 처음부터 256바이트씩 읽어 dest로 출력하기를 파일의 끝에 이를 떄까지 반복하면 됩니다. 원본의 파일끝일 때는 256바이트를 다 읽지 못할 수도 있으므로 fread가 리턴하는 실제 읽은 길이만큼만 출력해야됩니다. test.txt 라는 파일은 짧은 파일이지만(위에서 생성된 파일기준) 이런식으로 fread로 읽어 fwrite로 출력하면 큰 파일도 얼마든지 복사할 수 있습니다. 다음은 서식화된 스트림 입출력 함수입니다.

1
2
int fscanf(FILE *stream, const char *format [,argument]...);
int fprintf(FILE *stream, const char *format [,argument]...);
cs

사용하는 방법은 scanf, printf와 동일하되 대상이 화면이나 키보드가 아니라 파일이라는 점만 다릅니다. 이 두함수를 사용하면 정수나 실수 변수를 스트림으로 입출력할 수 있습니다. 다음 소스는 파일로 서식화된 출력을 보낸 후 다시 읽어 들입니다.

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
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
 
void main()
{
    char str[128= "String";
    int i = 1234;
    double d = 3.1416;
    FILE *f;
    f = fopen("D:\\test.dat""wb");
    if (f != NULL)
    {
        fprintf(f, "%d %f %s", i, d, str);
        fclose(f);
    }
    i = 0;
    d = 0.0;
    f = fopen("D:\\test.dat""rb");
    if (f != NULL)
    {
        fscanf(f, "%d %lf %s"&i, &d, &str);
        printf("파일에서 읽은 정수값 = %d, 실수값 = %f, 문자열 = %s \n", i, d, str);
        fclose(f);
    }
}
cs

정수, 실수, 문자열을 test.dat 파일로 출력한 후 다시 읽어 들여 출력해 보았습니다. 실행 결과는 다음과 같이 파일에 있는 내용을 출력시켜줍니다.


D:\test.dat 파일을 열어보면 세 변수의 값이 공백으로  나란히 저장되어 있습니다. fscanf는 이 값들을 공백으로구분하여 세 변수로 다시 읽어 들입니다. 이 두 함수를 사용하면 화면으로 출력할 수 있는 모든 값들을 파일로도 출력되 되며 키보드로 입력받을 수 있는 모든 값을 파일에서 입력받을 수 있습니다. 화면 이나 키보드도 파일과 같은 스트림이므로 이것이 가능합니다.








'Programming > C' 카테고리의 다른 글

C언어 - 포인터 고급(4/4)  (0) 2015.09.07
C언어 - 포인터 고급(3/4)  (0) 2015.09.06
C언어 - 포인터 고급(2/4)  (0) 2015.09.05
C언어 - 포인터 고급(1/4)  (0) 2015.08.31
C언어 - 구조체(Structure)(2/2)  (23) 2015.08.21