본문 바로가기

c언어

파일 입출력 -파일 읽기 및 쓰기

이전 포스팅에서 파일을 입력하는 법을 배웠다. fopen_s함수가 그것이다.

 

그럼 이제 파일을 열었으니 그 파일을 읽어내거나 써야 할 것이다.  읽기나 쓰기 안할꺼면 뭐하러 열었누?

 

먼저 기본적 설정부터 말해주겠다. 이미 텍스트모드와 바이너리모드에 대해서는 이전 포스팅에서 적어두었다. 하지만 그것은 파일을 읽어내는 방식의 차이인지라 뭐가 바이너리 파일이고 텍스트 파일인지 모를 수 있다.

 

간단하다. 윈도우에서 지원하는 메모장 프로그램 즉 txt파일을 제외한 모든 파일은 바이너리형식 파일이라고 보면 된다. 

그리고 바이너리파일을 텍스트모드로 읽는것은 문제가 되지만, 텍스트파일을 바이너리모드로 읽는것은 문제가 되지 않는다.(약간의 바이트 손실이 있긴 하나 무시해도 될 정도이다.)

 

 

그리고 모든 파일의 끝은 EOF라는 특수한 문자로 이루어져있다. end of file의 줄임말이다.

사실 특별할거 없는  int형 '-1'이다. 그래서 이 바이트를 읽었다면 파일이 끝난것이다.

 

 

이제 파일을 읽는 방법을 설명하겠다.

파일을 여는 방법에는 고전적 방식과 비교적 최신의 방법이 있다. 당연히 나는 두종류 다 배웠고, 두개가 미세하게 차이가 있기 때문에 두가지 다 설명할 것이다.

 

고전적 방식인 fgetc()와 fputc()먼저 보겠다.

int 버퍼 =fgetc(읽어들일 파일 스트림);

fputc(버퍼, 작성할 파일 스트림)

내가 사용한 방식이다.

 

일단 fgetc는 파일에서 1바이트(char)를 받아오는 함수이다.

그래서 나는 int타입의 버퍼를 선언한 뒤에 fgetc로 한글자 읽어들인 값을 대입해주었다.

 

그러고 나서 fputc는 파일에 1바이트단위로 값을 넣어주는 함수이다.

그렇게 버퍼에 저장된 값을 작성할 파일에 넣어주었다.

 

하지만 여기서 의문이 들 수 있다. 왜 버퍼를 int로 받지?? 1바이트짜리면 char여도 되는거 아닌가?? 그리고 int는 4바이트인데 쓸때 1바이트로 들어가게 되면 문제되는거 아닌가??

 

충분히 의문이 들 만 하다. 나 또한 이것때문에 시간을 많이 잡아먹었다.

뒤의 질문부터 대답하겠다.

int는 4바이트이지만 fputc를 이용하면 1바이트 char로 형변환 되어 들어가게 된다. 즉 

fputc((char)버퍼,쓸 파일 스트림)

과 동일하다는 뜻이다. 하지만 어차피 함수에서 알아서 해주는데 굳이 캐스팅할 이유는 없다.

 

그리고 앞의 질문에 대해서는 이전에 설명한 EOF와 관련이 있다. EOF는 int -1과 같다고 했다. 

만일 내가 1바이트씩 읽어냈는데 이 버퍼를 int형이 아닌 char형으로 저장하였다고 하면 -1은 255와 동일한 숫자가 된다.

char형은 1바이트 즉 8비트인데, 8비트 최대값은 2의 8승인 256이다. 하지만 컴퓨터는 0부터 숫자를 세니 하나가 빠진 255가 char형의 최대값이고, 이는 char -1과 같은 값으로 읽어진다. 하지만 그렇다고 컴퓨터가 255라는 바이트를 쓰지 않는가 하면 또 아니다. 

 

우리가 1바이트씩 읽는것이지 컴퓨터가 1바이트씩 쓰지는 않는다. 그러니 최소 2바이트 이상의 버퍼를 사용해야 파일의 끝을 제대로 읽어낼 수 있다.

 

사실 이것을 적으면서 계속 찾아보았으나 이해가 되지 않은것도 사실이다. 이 -1이 int 타입인지 아니면 다른 타입인지,

1바이트를 읽는 fgetc로 읽어낼 수 있지만 char형 -1인 255와는 또 다른것이고, 비트로 따지면 1이 32개 있는 255와는 또 어떻게 다른건지... 어떤 형태로 읽어내도 -1이라는 값이 나온다는거는 이게 컴퓨터만의 특수문자인가 싶기도 하고 여전히 이해가 되지 않는다. 추후에 이해가 된다면 이 뒤에 추가로 덧붙이겠다.

 

어찌됐든 파일의 끝에는 어떤 파일이건 EOF를 받을 수 있게 되어있다. 그래서 우리는 그것을 감지해낼 때까지 파일을 읽어내면 되는것이다.

 

다음은 실험용 Test.txt이다. 물론 이파일은 현재 작성중인 cpp파일의 위치보다 한칸 위,Data폴더에 저장될것이다.

내용은 직접 다른값으로 채워도 상관없다. 다만 숫자와 영문, 한글을 모두 적어서 상태를보자.

Test.txt

=========================
10
20
30
40
50
100
abcde
가나다라마

 

다음과 같은 방식을 통해 파일을 다음과같이 읽어 낼 수 있다

FILE* fileRead = nullptr;

errno_t errRead = fopen_s(&fileRead, "../Data/Test.txt", "rt");

if (!errRead && !errWrite) {
    while (1) {
        int iTemp = fgetc(fileRead);
        cout << (char)iTemp <<endl;
        
        if (iTemp==(unsigned char)EOF) {

            break;
        }
    }
    fclose(fileRead);
}
else {
    cout << "파일이 잘못 열렸습니다.";
}

fileRead의 이름으로 Test.txt파일을 열고

int iTemp에 fgetc를 이용해 1바이트 값을 받아내었다. 그리고 받아낸 iTemp의 값을 char로 변환 후(int는 숫자이기에 변환하지 않으면 그것의 아스키코드값이 나올것이다.) 그 값을 출력하였다.

그리고 읽어낸 iTemp의 값이 EOF일 경우(파일의 끝이 인지되었을 경우) 반복을 끝내고 파일을 닫는다.

 

내가 저 코드를 실행한 결과는 다음과 같다.

10
20
30
40
50
100
abcde
媛€?

숫자와 영어는 잘 읽혔지만, 한글은 이상한값으로 읽혀졌다.

이유는 1바이트 아스키코드는 영어와 숫자, 특수문자만을 읽어낼 수 있지만, 한글은 유니코드로 아스키코드 두개가 결합되어 한 글자를 표현하기때문에 아스키코드의 범주를 벗어난 문자는 읽을 수 없다.

 

만일 위의 코드에서 char형으로 iTemp를 받아내더라도 특별히 잘못된점은 못느낄 것이다. 이는 다음 바이너리 파일을 읽을때 다시 언급하겠다.

 

다음은 텍스트파일을 쓰는법이다.

이미 fgetc의 형태는 설명했으니 바로 실전으로 가겠다.

 

FILE* fileWrite = nullptr;

errno_t errWrite = fopen_s(&fileWrite, "../Data/Test2.txt", "wt");

if (!errWrite) {
    int iTemp = 50;
    fputc(iTemp, fileWrite);

    fclose(fileWrite);
}
else {
    cout << "파일이 잘못 열렸습니다.";
}

다음과 같은 코드를 만들었다. 달라진부분만 설명하겠다

파일을 여는 fopen_s함수에서 경로를 바꿔 새로운 파일을 만들었다.(그렇지 않으면 덮어쓰기되니 주의하여야 한다.)

또한 파일을 여는 방식을 텍스트 쓰기모드로 바꾸었다.

 

그리고 int iTemp에 50이라는 숫자를 입력 후 fputc로 iTemp의 값을 fileWrite파일에 넣어주었다.

 

파일탐색기로 돌아가 Test2.txt파일을 열면 그 안의 값은 2 한글자만이 들어가있는걸 볼 수 있다.

 

왜 50을 넣었는데 2가 적혀있는가 하면 아스키코드상 50이 숫자 2이다.

 

그럼 여러가지 문자들을 입력하기 위해서는 어떻게 해야하는가?

 FILE* fileWrite = nullptr;

errno_t errWrite = fopen_s(&fileWrite, "../Data/Test2.txt", "wt");

if (!errWrite) {
    char cTemp[32] = "가나다라마";
    
    for (int i = 0; i < sizeof(cTemp); ++i) {
        
        fputc((int)cTemp[i], fileWrite);
    }

    fclose(fileWrite);
}
else {
    cout << "파일이 잘못 열렸습니다.";
}

이처럼 char 배열 cTemp를 선언하고 그 안에 값을 넣었다. 그리고 cTemp의 크기만큼 반복하여 cTemp를 int치환한 값으로 넣어주었다.

 

이 경우 보다시피 한글또한 동일하게 입력이 가능해진다.

 

 

이번엔 파일을 온전히 복제해 보겠다. Test.txt를 동일하게 만든 Testcopy.txt를 만들겠다는 뜻이다.

FILE* fileRead = nullptr;
FILE* fileWrite = nullptr;

errno_t errRead = fopen_s(&fileRead, "../Data/Test.txt", "rt");
errno_t errWrite = fopen_s(&fileWrite, "../Data/Testcopy.txt", "wt");

if (!errRead && !errWrite) {
    while(1){
        int iTemp = fgetc(fileRead);
        if (iTemp == EOF) {
            break;
        }
        fputc(iTemp, fileWrite);
    }


    fclose(fileRead);
    fclose(fileWrite);
}
else {
    cout << "파일이 잘못 열렸습니다.";
}

단순히 파일을 두개 열어 원본파일은 텍스트 읽기모드, 복사본파일은 텍스트 쓰기모드로 열었고,

fgetc로 1바이트 읽어낸 값을 iTemp에 받아내고 iTemp가 EOF가 아니라면 fputc를 이용하여 복사본 파일에 넣어주었다.

 

이것으로 파일 복제가 성공하였다.

 

추가로 생각해야할 것은 txt파일이 아닌 파일은 모두 바이너리 모드로 읽어야하며, 텍스트와 달리 바이너리는 무조건적으로 버퍼가 2바이트 이상이 되어야한다.

 

 

아 이를 이용하다가 fgetc를 반복하면 제일 첫 값을 반복해서 받아들이는것 아니냐?? 라는 의문이 들 수 있다.

하지만 첫번째로 fopen은 반복된 적이 없다. 즉 파일스트림은 한번 열렸다는 것이다. 그리고 파일 스트림은 "커서"라는 개념이 있다. 즉 fgetc로 읽어냈다면 읽어낸 다음 공간에 커서가 이동되며, 다시 fgetc 하게되면 이전값의 바로 다음값을 읽어내는 것이다.

이후 신버전에 커서와 관련된 함수들이 나오니 이후 포스팅을 보자.

 

 

 

이것으로 파일 읽기 및 쓰기 구버전을 마친다.

 

 

바로 신버전으로 넘어가겠다.

 

사실 이전 구버전은 말 그대로 구버전으로 현재는 별로 쓰이지 않는다. 지금부터 설명할 신버전은 코드가 더욱 깔끔하고, 자동화된 부분이 많기때문에 따로 적어둔다.

 

파일을 여는방법은 동일하게 fopen_s로 열어준다. 그 뒤 우리는 읽거나 쓰기 위하여 다음의 함수들을 사용할 수 있다.

 

fread_s(버퍼,받을 단위,단위의 갯수,파일스트림);

fwrite(버퍼,쓸 단위, 단위의 갯수, 파일스트림);

이전 fgetc, fputc와 크게 다르지 않은 구성이지만, 차이점으로 입력받을 바이트 단위를 정할 수 있다.

한꺼번에 많은 데이터를 버퍼에 넣어둘 수 있는것이다. 물론 버퍼의 크기 이상은 불가능하다.

 

하지만 결국 한번에 읽어들이는 버퍼만 커지고 그것을 반복해야한다는것은 구버전과 다를것이 없어 보인다.

딱하나 중요한것이 달라졌다. fread로 읽은 값은 EOF를 탐색할 수  없다. 즉 이전처럼 

if(iTemp==EOF)를 못한다는것이다.

그럼 어떻게 할 수 있는가??

 

 

신버전에는 파일을 읽고 쓰는데 몇가지 함수를 이용할 수 있다.

fseek(파일 스트림, 이동할 바이트 수, 시작지점) 파일스트림 내 커서를 원하는 위치로 이동

ftell(파일 스트림) 파일스트림 내 커서의 위치(시작위치부터 몇바이트인가)를 반환

feof(파일 스트림) 파일스트림의 현재 커서위치 다음값이 EOF인가를 확인

 

일단 여기서 나온 feof를 이용하여 파일이 끝에 도달했는가를 알 수 있을것이다.

 

fseek는 설명할것이 많다.

 

먼저 파일스트림은 이전과 동일하고, 이동할 바이트수만큼 커서를 이동한다는것도 알겠는데 시작지점이 문제이다. 시작지점 부위에 들어갈 수 있는 명령어들은 다음 세가지가 있다.

SEEK_SET 시작지점부터

SEEK_CUR 현재 커서 위치로부터

SEEK_END 마지막 위치(EOF)부터

이를 이용해 커서를 이동시킬 수 있다.

 

또한 ftell함수를 이용해 커서의 현재위치를 알 수 있다.

이 세가지와 동적배열을 이용하면 다음과같은 행동을 할 수 있다.

 

FILE* fileRead = nullptr;
FILE* fileWrite = nullptr;

errno_t errRead = fopen_s(&fileRead, "../Data/Test.txt", "rt");
errno_t errWrite = fopen_s(&fileWrite, "../Data/Testcopy.txt", "wt");

if (!errRead && !errWrite) {
    fseek(fileRead, 0, SEEK_END);
    int iSize = ftell(fileRead);

    char* buffer = (char*)malloc(iSize);

    fseek(fileRead, 0, SEEK_SET);
    fread(buffer, sizeof(char), iSize, fileRead);
    cout << buffer << endl;
    fwrite(buffer, sizeof(char), iSize, fileWrite);

    free(buffer);
    fclose(fileRead);
    fclose(fileWrite);
}
else {
    cout << "파일이 잘못 열렸습니다.";
}

파일이 정상적으로 열렸다면 바로 원본파일의 끝으로 커서를 이동시킨다.

그리고 그 위치를 iSize에 입력한다.

char* buffer에 동적할당으로 iSize만큼의 공간을 할당한다.

그리고 그 버퍼에 fread로 iSize만큼의 데이터를 읽는다.

똑같이 버퍼의 데이터를 fwrite로 iSize만큼의 데이터를 쓴다.

 

복제 끝이다.

 

하지만 이 방법은 왠지 모르게 한글이 입력이 안된다. 따라서 텍스트가 아닌 바이너리 파일만을 하길 바란다. 왠지 나도 모른다 심지어 나 방금알았다.

 

이상으로 파일 입출력을 모두 마친다.

'c언어' 카테고리의 다른 글

2개월차의 시작, 객체 지향 프로그래밍?  (0) 2023.02.10
매크로와 레퍼런스  (0) 2023.02.09
파일입출력  (0) 2023.02.07
동적 할당(신버전) + 메모리 관련 함수들  (0) 2023.02.07
동적 할당(구버전)  (0) 2023.02.06