[C] 표준 입출력 라이브러리

김태희·2023년 11월 25일
0
post-thumbnail

표준 입출력 라이브러리

표준 입출력 라이브러리란 ?

컴퓨터는 꺼져도 데이터를 유지할 수 있는 보조기억 장치(SSD, HDD 등)이 필요하다.

다양한 보조기억 장치의 특성을 각각 파악해서 프로그래밍하는 것은 불가능에 가깝기에 운영체제는 보조기억 장치에 상관없이 같은 함수로 데이터를 저장할 수 있도록 하는 파일 입출력 라이브러리 제공한다.

하지만 운영체제별로 제공하는 입출력 함수가 이름뿐만 아니라 사용법도 다르기에 C 언어에서 표준 입출력 라이브러리를 제공한다.

이를 사용하면 파일 단위로 데이터를 저장하거나 읽을 수 있고 여러 운영체제가 제공하기 때문에 호환성이 높아 같은 이름의 함수로 보조기억 장치를 사용할 수 있다.



데이터의 형식에 따라 다른 함수를 제공하는 표준 입출력 라이브러리

데이터의 형식 두 가지

1. 바이너리(이진)

2. 텍스트(문자열)

표준 입출력 라이브러리를 알맞게 사용하려면 사용할 데이터가 어떤 형식의 데이터인지 구별할 줄 알아야한다.


바이너리 속성과 문자열 속성

char temp[8] = {'a', 'b', 'c', 0, };

이와 같은 배열이 있을때 데이터의 형식에 따라 다르게 받아들여진다.

  • 바이너리 속성
    크기 : 8바이트
    데이터 8개
    내용 : (아스키 값에따라) 97, 98, 99, 0, 0, 0, 0, 0

  • 문자열 속성 - temp 변수 크기에 의미를 두지않고 NULL문자(0)이 나올때까지 찾는다
    크기 : 3바이트
    데이터 3개
    내용 : abc


두 속성의 차이점

변수에 저장된 데이터의 크기 구하기

바이너리 속성 : sizeof(temp)

문자열 속성 : strlen(temp)

변수 저장된 값 복사

바이너리 속성 : memcpy(복사할 값, 복사할 곳, sizeof(temp))

문자열 속성 : strcpy(복사할 값, 복사할 곳)

strcpy가 더 간단하고 메모리 복사하는 양이 더 작지만 아래와 같은 이유로 실제로는 효율이 떨어진다 !

  • memcpy는 특별한 체크나 데이터 가공없이 메모리를 복사
  • strcpy는 문자 하나하나 복사할때마다 문자열이 끝이났는지 체크

바이너리 파일과 텍스트 파일

바이너리 파일 : 바이너리 속성 개념이 적용된 파일
ex : 이미지 파일, 음악 파일, 동영상 파일, 실행 파일

텍스트 파일 : 문자열 속성이 적용된 파일
ex : 간단한 문서파일, 프로그램 소스 파일

대부분 프로그램은 저장 및 처리의 효율이 더 좋다는 이유로 바이너리 파일을 사용한다.

효율이 좋은 바이너리 파일이 있음에도 텍스트 파일이 공존하는 이유는 무엇일까 ?

바이너리 파일은 해당 파일을 사용할 수 있는 프로그램이 설치되어있어야 제대로 사용 가능하기 때문이다.


파일 입출력 함수

파일 열기와 닫기

파일 입출력 함수의 도우미 : FILE 구조체

FILE *p_file;

파일 열기 사용 형식

FILE *p_file = fopen("ex.dat", "r");

fopen 함수는 열기 실패 시 NULL을 반환한다.

파일 사용 형식

t : 텍스트 속성으로 열기

b : 바이너리 속성으로 열기


r : 파일 내용 읽기 모드 

rb : 읽기 모드로 바이너리 속성으로 열기  

rt : 읽기 모드로 텍스트 속성으로 열기


w : 파일 내용 쓰기 모드 

wb : 쓰기 모드로 바이너리 속성으로 열기  

wt : 쓰기 모드로 텍스트 속성으로 열기


a : 파일에 데이터 이어 쓰기 모드 

ab : 이어쓰기 모드로 바이너리 속성으로 열기

at : 이어쓰기 모드로 텍스트 속성으로 열기

읽기와 쓰기를 같이 사용하기
r+ : 읽기 강조  / rb+ r+b / rt+ r+t 

w+ : 쓰기 강조 / wb+ w+b / wt+ w+t

a+ : 읽기와 이어쓰기를 같이 사용 / ab+ a+b / at+ a+t

파일 닫기 사용 형식

fclose(p_file);

텍스트 파일에 데이터 읽고 쓰기

텍스트 파일에서 데이터 쓰기(fprintf())

문자열 저장하기

fprintf(p_file, "Hello\n"); //파일에 Hello 문자열을 쓰고 줄 바꾸기

바이너리 형태를 문자열 형태로 저장하기

int형 변수에 들어있는 값은 바이너리 데이터이기 때문에 텍스트 파일에 저장하려면 문자열 형식으로 변환해야한다.

이때 printf와 마찬가지로 %d, %f과 같은 형식 지정 키워드를 사용해 변수값을 문자열로 저장할 수 있다.

short int data = 0x0412;
fprintf(p_file, "%x\n", data); //파일에 "412"라고 저장 후 줄 바꾸기

fprintf 함수는 호출될 때마다 파일에 저장한 문자열의 개수만큼 파일 포인터를 이동한다.
즉 문자열의 개수만큼 파일 내부 데이터를 읽거나 쓰기 시작하는 위치가 이동한다.
그러하여 fprintf 함수 연속적으로 호출할 시에 문자열이 차례대로 각 파일에 저장된다.


텍스트 파일에서 문자열 읽기(fscan())

ex.txt -> 123 456 789



#include <stdio.h>

void main() {
  int num1, num2, num3;
  FILE *p_file = fopen("ex.txt", "rt");
  fscanf(p_file, "%d %d %d", &num1, &num2, &num3);
  printf("%d %d %d\n", num1, num2, num3);
  fclose(p_file);
}



결과 : 123 456 789

문자열 형식의 정수 값 모두 읽어오기

ex.txt -> 123 456 789
#include <stdio.h>



void main() {
  int num;
  FILE *p_file = fopen("ex.txt", "rt");
  if(p_file != NULL){
    while(EOF != fscanf(p_file, "%d", &num)){
      printf("%d", num);
    }
  }  
  fclose(p_file);
}



결과 : 123 456 789

텍스트 파일의 끝은 EOF(End Of File) 문자로 구별한다.
fscanf 함수가 EOF 문자를 만나면 EOF 값을 반환하기에 이러한 반복문을 사용할 수 있다.

이는 공백(Space) 단위로 실행된다 그래서 한줄 단위로 문자열을 읽기 위해서는 fgets를 사용한다.

텍스트 파일에서 한줄 단위로 문자열을 읽기(fgets())

fgets(temp, sizeof(temp), p_file);

ex.txt -> Hello World !

fscanf를 사용해서 출력시 

#include <stdio.h>

void main() {
  char temp[64];
  FILE *p_file = fopen("ex.txt", "rt");
  if(p_file != NULL){
    while(EOF != fscanf(p_file, "%s", &temp)){
      printf("%s", temp);
    }
  }  
  fclose(p_file);
}

결과 : HelloWorld!
fgets를 사용해서 출력 시

#include <stdio.h>

void main() {
  char temp[64];
  FILE *p_file = fopen("ex.txt", "rt");
  if(p_file != NULL){
    while(NULL != fgets(temp, sizeof(temp), p_file)){
      printf("%s", temp);
    }
  }  
  fclose(p_file);
}

결과 : Hello World !

fscanf()와 fgets()의 차이점

공백 단위와 한 줄 단위로 출력한다는 점 말고도 차이점이 있다.

fscanf 함수는 EOF 문자를 만나면 EOF를 반환하지만 fgets 함수는 EOF 문자를 만나면 NULL을 반환한다.

또한 fscanf 함수는 읽은 문자열에서 \n을 제외하는데 fgets 함수는 \n을 포함한다.

바이너리 파일에 데이터 읽고 쓰기

앞에서 다뤘듯이 문자열 속성은 NULL문자인 0을 찾아서 데이터 크기를 체크하기 때문에 문자열 길이를 추가로 적을 필요가 없지만 바이너리 속성은 데이터를 숫자로만 판단하기에 표준 입출력 함수가 길이나 크기를 알아낼 수 없다.

따라서 바이너리 속성으로 데이터를 읽거나 쓰려면 프로그래머가 반드시 크기를 적어줘야한다.

바이너리 파일에 데이터 저장하기(fwrite())

int data = 0x00000412;
fwrite(&data, sizeof(int), 1, p_file); 
//data 변수가 할당된 메모리를 4바이트 크기만큼 1회만 p_file 포인터가 가르키는 파일에 저장함

반복 횟수의 의미 및 다양한 사용방법

int data[5] = {0, 1, 2, 3, 4};
fwrite(data, sizeof(int), 5, p_file);
fwrite(data, sizeof(int)*5, 1, p_file);
fwrite(data, sizeof(data), 1, p_file);

// 모두 배열을 저장한다

단위 크기 * 반복횟수로 연산하여 사용된다.


fwrite 함수가 반드시 성공하지는 않는다

fwrite 함수는 디스크 용량이나 쓰기 제한 속성때문에 실패할 수도 있다.

fwrite 함수가 작업에 성공하면 실제로 반복한 횟수를 반환하기 때문에 반환값을 체크해서 확인해 오류를 처리하는것이 좋다.

if( 5 == fwrite(&data, sizeof(int), 5, p_file)){
//쓰기에 성공한 경우 수행할 명령문
}

바이너리 파일에서 데이터 읽기(fread())

int data;
fread(&data, sizeof(int), 1, p_file);
//p_file이 가리키는 파일에서 4바이트 크기만큼 1회만 데이터를 읽어 와서 data 변수에 저장

반복 횟수의 의미 및 다양한 사용방법

int data[5];
fread(data, sizeof(int), 5, p_file);
fread(data, sizeof(int)*5, 1, p_file); //전체 데이터 크기가 20바이트 메모리라는 것을 더 강조하는 형태
fread(data, sizeof(data), 1, p_file);

fread 함수도 반드시 성공하지는 않는다

fread 함수의 작업도 실패할 수 있다.

디스크 섹터의 문제가 발생하거나 실제 파일에 있는 데이터 보다 더 많이 읽으려고 하면 실패할 수 있다.

fread 함수도 작업에 성공하면 실제로 반복한 횟수만큼 반환하기에 이 값을 확인하여 오류를 처리하면 좋다.

if( 5 == fread(&data, sizeof(int), 5, p_file)){
//읽기에 성공한 경우 수행할 명령문
}

파일 내부의 작업 위치를 탐색하고 확인하기(fseek(), ftell())

fseek(파일 포인터, 이동 거리, 기준위치) 함수에서는 파일의 데이터를 읽을 기준 위치를 사용할 수 있다.

  • SEEK_SET : 파일의 시작
  • SEEK_END : 파일의 끝
  • SEEK_CUR : 현재 위치

저장한 기준 위치로부터 사용자가 지정한 이동거리만큼 이동한다.

이동거리는 양수 또는 음수로 저장할 수 있고 양수는 뒤로 음수는 앞으로 이동한다.

fseek(p_file, 0, SEEK_SET); //파일의 시작 위치로 이동
fseek(p_file, 32, SEEK_CUR); //현재 위치에서 32바이트만큼 뒤로 이동

이동한 위치를 값으로 확인하기(ftell(파일 포인터))

fseek 함수와 ftell 함수로 바이너리 파일 크기 알아내기

#include <stdio.h>

void main(){
  int file_size = 0;
  FILE *p_file = fopen("ex.dat", "rb");
  if(NULL != p_file){
    fseek(p_file, 0, SEEK_END);
    file_size = ftell(p_file);
    printf(" 파일 크기 : %d\n", file_size);
    fclose(p_file);
  }
}


빈 ex.dat에 실행한 결과 : 파일 크기 : 0

파일 입출력에 대해 가볍게 다뤄보았다. 예제를 직접 쳐보고 이렇게 정리해보았으니 나중에 쓸일이 생긴다면 더욱 쉽게 활용할 수 있을 것 같다.

0개의 댓글