C언어로 파일을 읽고 쓰는 방법에 대해 정리해본다.
프로세스에서 다뤄지는 데이터는 메모리에 있기 때문에, 프로그램이 종료되면 소멸해버린다.
영구적으로 데이터를 저장하려면 서버에 저장하거나, 로컬에 저장해야 하는데 여기서는 로컬 파일로 저장하는 것을 다룬다.
파일(File)
파일이 뭐냐고 물어보면 파일이 파일이지.. 라고 대답하는 불상사를 막기 위해 간단하게 파일의 개념부터 알아보자.
파일은 컴퓨터에서 '데이터를 저장하는 논리적 단위'이다. 데이터가 디지털 형태로 기록되어 있고, 저장장치에 영구적으로 저장될 수 있다.
흔히 보는 텍스트,이미지,음악,비디오,코드 등 다양한 형태의 정보를 파일에 담을 수 있다.
(소켓 통신에서 소켓도 네트워크 통신을 위한 파일이다!)
파일은 다양한 형태와 구조를 가질 수 있는데, 텍스트 파일의 경우 문자 데이터를 순차적으로 저장하고
바이너리 파일은 이미지, 오디오, 컴파일된 프로그램 등을 저장할 수 있다.
파일은 스트림을 통해 순차적으로 접근하거나, 임의 접근이 가능하다.
또한 파일에는 파일의 이름,크기,생성 날짜 등의 메타 데이터가 포함되어 있는 경우가 많다.
이런 파일은 운영체제의 일부인 파일 시스템에 의해 관리된다. 파일 시스템을 통해 파일과 디렉토리(폴더)를 효율적으로 관리할 수 있다.
스트림(Stream)
스트림은 '데이터의 연속적인 흐름을 추상화한 개념'이다.
stream은 시냇물이란 뜻인데, 데이터가 물처럼 연속적으로 흐르는 모습을 떠올려보자.
스트림은 파일, 입출력 장치, 프로세스 간 통신 등 다양한 데이터 소스와 싱크를 일관적으로 관리하기 위해 사용한다.
간단하게는 데이터를 순차적으로 읽거나 쓰기 위한 함수들을 통해 사용한다.
스트림의 특징
- 순차접근
스트림을 통해서 데이터를 순차적으로 읽고 쓸 수 있다.
파일 스트림의 경우 파일의 시작부터 순차적으로 데이터를 처리한다.
- 추상화
스트림은 데이터의 출처나 목적지에 상관없이 일관된 인터페이스를 제공한다.
즉 파일, 키보드 입출력, 화면 출력 등 다양한 소스를 각자 다르게가 아닌, 동일한 방식으로 처리할 수 있다.
- 버퍼링
스트림은 대게 효율적인 입출력을 위해 버퍼링을 사용한다. 데이터를 바로바로 전송하는 대신
바구니로 생각할 수 있는 임시 저장 공간인 버퍼(Buffer)에 데이터를 모아뒀다가 일정량이 모이면 한 번에 전송하는 방식을 취한다.
표준 입/출력 스트림
표준 입출력 스트림은 표준 입출력 장치(키보드, 모니터)를 위한 스트림으로 운영체제 의해 자동으로 생성되고 관리된다.
이는 프로세스와 입출력 장치를 연결해 주는 논리적인 연결로 이해할 수 있다.
C언어에서 표준 입력 스트림은 stdin, 표준 출력 스트림은 stdout, 표준 에러스트림은 stderr이다.
파일 입/출력
파일 입출력은 대상이 디스크 저장장치이며, 디스크는 표준 입출력 장치가 아니기 때문에 운영체제에 의해 자동으로 관리되지 않는다.
따라서 개발자가 직접 파일 연결 과정을 세팅해줘야 한다.
즉 스트림이 필요할 때 생성하고, 필요 없어지면 닫아줘야 한다.
파일 스트림 열기
FILE* fopen(char *filename, char *mode);
파일의 이름과 파일에 대한 접근 모드를 받아서 스트림 연결이 성공하면 FILE 포인터를 반환하고, 실패하면 NULL 포인터를 반환한다.
파일 모드는 주로 r, w, a를 자주 쓰지만 다양한 모드들이 존재한다. (접은글 참고)
"r": 읽기 모드.
파일을 읽기를 위해 열고, 파일이 존재하지 않으면 NULL을 반환한다.
"w": 쓰기 모드.
파일을 쓰기를 위해 열고, 이미 파일이 존재하면 해당 파일의 내용을 삭제하고 새로운 내용을 작성한다.
파일이 존재하지 않으면 새로운 파일을 생성한다.
"a": 추가 모드.
파일을 쓰기를 위해 열고, 이미 파일이 존재하면 파일 끝에서부터 쓰기를 시작한다.
파일이 존재하지 않으면 새로운 파일을 생성합니다.
"r+": 읽기/쓰기 모드.
파일을 읽기와 쓰기를 위해 열고, 파일이 존재하지 않으면 NULL을 반환합니다.
"w+": 읽기/쓰기 모드.
파일을 읽기와 쓰기를 위해 열고, 이미 파일이 존재하면 해당 파일의 내용을 삭제하고 새로운 내용을 작성한다.
파일이 존재하지 않으면 새로운 파일을 생성한다.
"a+": 추가/읽기 모드.
파일을 읽기와 쓰기를 위해 열고, 이미 파일이 존재하면 파일 끝에서부터 쓰기를 시작한다.
파일이 존재하지 않으면 새로운 파일을 생성합니다.
여기서 이진 파일의 경우 rb, wb와 같이 모드 뒤에 'b'를 붙여준다.
파일 구조체
파일 구조체는 <stdio.h>에 들어있으며 스트림에 접근하기 위한 자료 구조로 fopen()함수가 실행될 때 FILE에 연결할 장치에 대한 정보(파일 크기, 현 위치, 파일 접근방법)가 저장되는 구조체이다.
주로 파일 포인터 형태로 사용한다.
#include <stdio.h>
int main() {
FILE *fp; // 파일 포인터 선언
// 파일 열기
fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("파일 스트림 연결 실패.\n");
//에러처리
}
//파일 처리
// 파일 닫기(누수 방지를 위해 꼭 닫아주자)
fclose(fp);
return 0;
}
스트림 입출력 함수 정리
역할 | 임의 스트림 | 표준 입출력 |
문자 입력 | fgetc, getc | getchar |
문자 출력 | fputc, putc | putchar |
한 행(문자열) 입력 | fgets | gets |
한 행(문자열) 출력 | fputs | puts |
형식화된 입력 | fscanf | scanf |
형식화된 출력 | fprintf | printf |
이진 데이터 입력 | fread | - |
이진 데이터 출력 | fwrite | - |
fgets()
스트림에서 문자들을 읽어 버퍼로 복사한다.
버퍼 사이즈 만큼 읽거나, 개행 문자를 만나거나, 파일의 끝을 만나면 읽은 내용을 버퍼에 저장하고 읽기를 중단한다.
언제나 버퍼에 저장되어 있는 문자열의 끝에는 NULL 바이트가 추가되어 문자열이 된다.
fgets는 버퍼가 가리키는 포인터를 반환한다. (파일 내용이 없으면 NULL 반환)
fputs()
버퍼가 가리키는 문자열을 출력한다. 에러가 발생하면 EOF를 반환, 잘 수행되면 음수가 아닌 값을 반환한다.
문자열 파일 입출력 예제 2
#include <stdio.h>
#include <stdlib.h>
int main() {
char* targets[10] = { "Swift\n", "Java\n", "Python\n", "SQL\n", "CLang\n",NULL };
FILE* fp;
char buffer[20];
int i = 0;
if ((fp = fopen("test2.txt", "wt")) == NULL) {
printf("스트림 연결 실패");
exit(1);
}
//파일 쓰기
while (targets[i]) {
fputs(targets[i], fp);
i++;
}
//읽기로 모드 변경
fp = freopen("test2.txt", "rt", fp);
while (fgets(buffer, 20, fp)) {
//버퍼의 내용을 출력합니다. (파일에서 한 줄 씩 읽어옴)
printf("%s", buffer);
}
fclose(fp);
return 0;
}
fgets와 fputs가 버퍼를 통해 문자를 읽고 출력했다면,
정해진 형식을 통해 데이터를 입출력 해보자.
fscanf()
int fscanf(FILE* stream, char const *format, args...);
입력된 문자들을 읽어서 format 문자열로 지정된 코드에 맞게 변환함.
형식 문자열의 끝에 도달하거나, 입력이 지정된 형식 문자열과 일치하지 않을 경우 입력이 종료됨.
변환된 입력 값의 개수를 반환함.
fprintf()
int fprintf(FILE* stream, char const *format, args...);
args에 있는 값들의 출력 형식을 format에 맞게 변경해 스트림으로 출력함.
출력된 값의 개수를 반환함.
fscanf와 fprintf 예시 코드
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
FILE* fp;
char name[20];
int salary, cn = 0;
if ((fp = fopen("test3.txt", "wt")) == NULL) {
printf("파일 열기 실패");
exit(1);
}
while (1) {
printf("이름: (입력 종료: EXIT)");
gets(name);
if (!strcmp(name, "EXIT")) {
break;
}
printf("월급: ");
scanf("%d%*c", &salary); // 정수형 형식 지정자, \n제거, salary주소 전달(저장)
fprintf(fp, "%10s%10d \n", name, salary); //파일에 쓰기
}
//읽기 모드로 변경
fp = freopen("test3.txt", "r", fp);
printf("파일을 읽어옵니다.\n");
while (1) {
cn = fscanf(fp, "%s %d", name, &salary); //파일 읽기
if (cn == 0 || cn == EOF) { //읽은게 없거나 파일의 끝이면(-1)
break;
}
printf("이름: %s, 연봉: %d", name, salary);
}
fclose(fp);
return 0;
}
이진 데이터 입출력
이진 데이터(binary data)
이진 데이터는 0과 1로만 구성된 데이터를 말한다. 컴퓨터는 결국 모든 정보를 이진 형태로 저장하고 처리한다.
이진 데이터는 텍스트 파일등과 달리 그냥 열어보면 알아볼 수가 없다.
하지만 텍스트 변환이 일어나지 않기 때문에 더 적은 공간을 사용하고 더 빠르게 처리할 수 있다.
주로 이미지, 비디오,오디오, 실행파일 등에서 이진 데이터 형식이 사용된다.
이진 입출력 함수
C에서는 fread()와 fwrite()가 제공되는데, 이를 통해서 어떤 형태의 데이터라도 읽고 쓸 수가 있다.
fread()
size_t fread(void *buffer, size_t size, size_t num, FILE *stream);
(params) -> 읽어 들일 메모리 주소, 읽을 크기, 읽을 개수, 스트림
size_t는 unsigned integer로 어떤 객체든 크기를 나타낼 때 사용함
실제로 읽은 개수를 반환한다.
fwrite()
size_t fwrite(void *buffer, size_t size, size_t num, FILE *stream);
파일 스트림으로 데이터를 읽어와서 버퍼에 쓴다.
구조체와 fread, fwrite를 활용한 예시 코드
#include <stdio.h>
#include <stdlib.h>
struct EMP {
char name[20];
char phone[20];
int age;
};
struct EMP emps[3] = {
{"김철수","010-1234-5678",20},
{"이수찬","010-1334-5678",24},
{"정수진","010-2334-5678",22}
};
int main() {
FILE* fp;
struct EMP tmp;
//바이너리 모드로 엽니다.
if ((fp = fopen("data1.dat", "wb")) == NULL) {
printf("스트림 오픈 실패");
exit(1);
}
//배열의 요소를 바이너리 데이터로 파일에 쓰기
fwrite(emps, sizeof(emps), 1, fp);
//fwrite(emps,sizeof(struct EMP),3,fp);
fp = freopen("data1.dat", "rb", fp);
printf("읽기 모드로 변경되었습니다.\n");
while (1) {
if (fread(&tmp, sizeof(tmp), 1, fp) != 1) { //파일 읽기
break;
}
printf("%s, %s, %d \n", tmp.name, tmp.phone, tmp.age);
}
fclose(fp);
return 0;
}
현재 위치(Current Location)
C언어에서 파일 스트림의 "현재 위치"는 파일 내의 특정 위치를 가리키며,
이 위치는 읽기 또는 쓰기 작업이 다음에 발생할 곳을 나타낸다.
파일 임의 접근
stream을 통해서 sequential 하게, 즉 순차적으로 데이터를 읽고 써봤다.
하지만 파일의 특정한 지점의 데이터에도 바로 접근하는 방법이 있는데, 이를 Random Access라고 한다.
(메모리를 RAM이라고 하는데 Random Access Memory 이름처럼 임의 접근이 가능한 저장장치이기 때문이다.)
위치 이동(fseek), 위치 확인(ftell), 처음으로 복귀(rewind)와 같은 함수가 제공된다.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Error opening file");
return -1;
}
// 현재 위치를 이동
fseek(file, 10, SEEK_SET); // 파일의 시작에서 10바이트 뒤로 이동
// 현재 위치 확인
long currentPos = ftell(file);
printf("Current position: %ld\n", currentPos);
// 다시 시작 부분으로 이동
rewind(file);
fclose(file);
return 0;
}
ftell()
int ftell(FILE* stream);
스트림에서 현재 위치를 반환한다. 즉 다음 읽기/쓰기를 할 위치를 의미하며
파일의 시작부터 현재 위치까지의 바이트 수를 반환한다.
fseek()
int fseek(FILE* stream, long offset, int from);
다음에 읽거나 쓸 수행 위치를 변경한다.
어떤 스트림에서, 얼만큼 이동할 것인지, 어디에서 이동할 것인지 이다.
여기서 from은 아래 상수 중에서 선택해야 한다.
from 매크로 상수 | |
SEEK_SET | 파일의 시작부분을 의미한다. (offset이 음수여서는 안된다 당연히..) |
SEEK_CUR | 현재 위치를 의미한다. (offset 양수는 앞으로, 음수는 뒤로 ) |
SEEK_END | 파일의 끝을 의미한다. (주로 음수를 통해 끝에서 거슬러 올라간다.) |
fseek이 성공하면 0을 반환하면, 실패하면 그 이외의 값을 반환한다.
이동 후 현재 위치를 확인하려면 ftell()을 사용하자.