70 파일에서 문자열을 읽고 쓰기

프로그래밍에서 중요한 축을 차지하는 부분이 파일 처리입니다. 이번 유닛부터는 파일에서 값을 쓰는 방법과 읽는 방법 알아보겠습니다.

70.1 서식을 지정하여 파일에 문자열 쓰기

지금까지 printf로 서식을 지정하여 문자열을 화면에 출력하고, sprintf로 서식을 지정하여 문자열을 생성했습니다. 그럼 문자열을 파일에 쓸 수는 없을까요? 이때는 printf의 파일 버전인 fprintf를 사용하면 됩니다.

파일에 문자열을 쓸 때는 먼저 fopen 함수로 파일을 열어서 파일 포인터를 얻은 뒤 fprintf 함수를 사용합니다(stdio.h 헤더 파일에 선언되어 있습니다).

  • FILE *포인터이름 = fopen(파일명, 파일모드);
    • FILE *fopen(char const *_FileName, char const *_Mode);
    • 성공하면 파일 포인터를 반환, 실패하면 NULL을 반환
  • fprintf(파일포인터, 서식, 값1, 값2, ...);
    • int fprintf(FILE * const _Stream, char const * const _Format, ...);
    • 성공하면 쓴 문자열의 길이를 반환, 실패하면 음수를 반환
  • fclose(파일포인터);
    • int fclose(FILE *_stream);
    • 성공하면 0을 반환, 실패하면 EOF(-1)를 반환

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

file_write_format.c

#define _CRT_SECURE_NO_WARNINGS    // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>     // fopen, fprintf, fclose 함수가 선언된 헤더 파일

int main()
{
    FILE *fp = fopen("hello.txt", "w");     // hello.txt 파일을 쓰기 모드(w)로 열기.
                                            // 파일 포인터를 반환

    fprintf(fp, "%s %d\n", "Hello", 100);   // 서식을 지정하여 문자열을 파일에 저장

    fclose(fp);    // 파일 포인터 닫기

    return 0;
}

Visual Studio에서 Ctrl+F5 키를 눌러서 프로그램을 실행하면 .c 파일이 있는 폴더에 hello.txt 파일이 생성됩니다. 메모장이나 기타 텍스트 편집기를 사용하여 파일을 열어보면 다음과 같이 "Hello 100" 문자열이 저장된 것을 볼 수 있습니다.

hello.txt

Hello 100

파일을 사용하기 위해서는 fopen 함수로 파일을 열어서 파일 포인터를 얻어야 합니다. 다음과 같이 읽을 파일 이름은 hello.txt로 지정하고, 파일을 생성하여 내용을 쓸 것이므로 파일 모드를 "w"로 지정해줍니다. 파일 열기에 성공하면 파일 포인터를 반환하고, 실패하면 NULL을 반환합니다.

//    ↓ 파일 포인터
FILE *fp = fopen("hello.txt", "w");           // hello.txt 파일을 쓰기 모드(w)로 열기. 
//                   ↑          ↖ 파일 모드   // 파일 포인터를 반환
//        파일 이름 또는 파일 경로

여기서 FILE 구조체는 stdio.h 헤더 파일에 정의되어 있으며 보통 FILE*를 합쳐서 파일 포인터라고 부릅니다. 포인터 변수의 이름도 file pointer의 약자로 fp를 사용하겠습니다.

다음은 파일 모드의 종류입니다.

표 70‑1 파일 모드
파일 모드 기능 설명
"r" 읽기 전용 파일을 읽기 전용으로 엽니다. 단, 파일이 반드시 있어야 합니다.
"w" 쓰기 전용 새 파일을 생성합니다. 만약 파일이 있으면 내용을 덮어씁니다.
"a" 추가 파일을 열어 파일 끝에 값을 이어 씁니다. 만약 파일이 없으면 파일을 생성합니다.
"r+" 읽기/쓰기 파일을 읽기/쓰기용으로 엽니다. 단, 파일이 반드시 있어야 하며 파일이 없으면 NULL을 반환합니다.
"w+"읽기/쓰기 파일을 읽기/쓰기용으로 엽니다. 파일이 없으면 파일을 생성하고, 파일이 있으면 내용을 덮어씁니다.
"a+" 추가(읽기/쓰기) 파일을 열어 파일 끝에 값을 이어 씁니다. 만약 파일이 없으면 파일을 생성합니다. 읽기는 파일의 모든 구간에서 가능하지만, 쓰기는 파일의 끝에서만 가능합니다.
t 텍스트 모드 파일을 읽거나 쓸 때 개행문자 \n\r\n을 서로 변환합니다.
^Z 파일의 끝으로 인식하므로 ^Z까지만 파일을 읽습니다(^Z는 Ctrl+Z 입력을 뜻합니다).
b 바이너리 모드 파일의 내용을 그대로 읽고, 값을 그대로 씁니다.

파일 모드는 보통 "rb", "rt", "w+b", "w+t"와 같이 읽기/쓰기 모드와 텍스트/바이너리 모드를 조합해서 사용합니다. tb는 단독으로 사용할 수 없습니다.

이제 파일 포인터를 얻었으니 fprintf 함수로 문자열을 파일에 씁니다. 먼저 앞에서 얻은 파일 포인터를 지정하고 서식과 값을 순서대로 지정하면 됩니다.

fprintf(fp, "%s %d\n", "Hello", 100);   // 서식을 지정하여 문자열을 파일에 저장

이렇게 하면 "Hello"와 정수 100을 합쳐서 문자열 "Hello 100"hello.txt 파일에 저장됩니다.

파일 쓰기가 끝났으면 반드시 fclose 함수로 파일 포인터를 닫아줍니다. 파일 포인터 fp도 구조체 FILE 크기만큼 동적 메모리를 할당한 것이기 때문에 fclose 함수로 닫아주지 않으면 메모리 누수가 발생합니다.

fclose(fp);    // 파일 포인터 닫기

서식을 지정하여 파일에 문자열을 쓰는 과정을 그림으로 나타내면 다음과 같습니다.

그림 70‑1 서식을 지정하여 파일에 문자열 쓰기

한 가지 특이한 점은 fprintf 함수로도 화면에 문자열을 출력할 수 있다는 점인데 stdout이라는 매크로를 활용하면 됩니다.

fprintf(stdout, "%s %d\n", "Hello", 100);   // Hello 100: 서식을 지정하여 화면(stdout)에 문자열 출력

즉, fprintf(stdout, ...);printf 함수와 동작이 같습니다.

참고 | 파일명과 파일 경로

fopen 함수에 파일명만 지정하면 현재 작업 디렉터리(working directory)에서 파일을 엽니다. 만약 fopen("hello.txt", "w")이라면 현재 작업 디렉터리 아래의 hello.txt 파일을 열게 됩니다. 여기서 중요한 점은 프로그램을 실행하는 방식과 위치에 따라 현재 작업 디렉터리가 바뀔 수 있다는 점입니다.

  • Ctrl+F5, F5로 실행했을 때: Visual Studio에 설정된 작업 디렉터리 설정을 따릅니다. 보통 .c 파일과 .vcxproj 파일이 있는 디렉터리(예: c:\project\hello\hello)입니다. 이 설정은 솔루션 탐색기에서 프로젝트 선택 > 메인 메뉴의 프로젝트(P) > 속성(P) > 디버깅 > 작업 디렉터리에서 바꿀 수 있습니다.
  • 명령 프롬프트, 탐색기에서 실행했을 때: 소스 코드를 컴파일하면 Debug 모드일 경우 프로젝트 디렉터리 아래의 Debug 아래에 실행 파일이 생성됩니다(예: c:\project\hello\Debug). 이 상태에서 실행 파일을 직접 실행하면 실행 파일이 있는 디렉터리가 작업 디렉터리가 됩니다.
  • 명령 프롬프트에서 실행했을 때: 실행 파일이 있는 디렉터리로 이동해서 실행 파일을 실행하는 것이 아닌 상대 경로로 실행했을 때는 현재 디렉터리가 작업 디렉터리입니다(예: Debug\hello.exe처럼 실행)

fopen에는 파일이 있는 경로까지 함께 지정할 수 있습니다(경로에 \를 입력할 때는 \\와 같이 입력해야 합니다).

  • 절대 경로: fopen("c:\\project\\hello.txt", "w");
  • 상대 경로: fopen("..\\hello.txt", "w"); 상대 경로는 작업 디렉터리를 기준으로 파일을 찾아서 열게 됩니다.

리눅스 및 OS X는 경로를 표현할 때 \ 대신 /를 사용합니다

  • fopen("/home/project/hello.txt", "w");