C언어 파일 입출력

Minimal_user·2024년 5월 15일

c언어

목록 보기
16/17

1. 파일

1.1 파일 액세스 방법

  • 파일을 액세스하는 방법에는 여러 가지 종류가 있다.
    • 고수준 입출력 스트림 사용
      • C라이브러리가 제공하는 파일 입출력 방법이며 성능은 조금 떨어지지만 사용하기는 쉽다.
        표준에 의해 함수의 형태가 고정되어 있으므로 이식에 유리하다.
    • 저수준 파일 핸들
      • C 라이브러리가 제공하는 파일 입출력 방법이며 대규모의 데이터를 다룰 때 편리하다.
    • C++의 스트림 객체
      • ifstream, ofstream 등의 입출려 객체와 그 멤버 함수를 사용하여 파일을 액세스 한다.
    • 운영체제가 제공하는 API 함수
      • 파일이 저장되는 디스크의 관리 주체는 운영체제이며 운영체제는 응용프로그램으 위해 파일 관련 API 함수를 제공한다.
        윈도우즈는 CreateFile, ReadFile, WriteFile 등의 함수를 제공하므로 이 함수를 사용하면 파일을 액세스할 수 있다.
    • 클래스 라이브러리가 제공하는 파일 액세스 객체 사용
      • 고수준 클래스 라이브러리들은 파일 액세스 기능을 캡슐화한 클래스를 제공하며 이 클래스를 사용하면 쉽게 파일을 다룰 수 있다.
        MFC의 경우 CFile 클래스를 제공한다.
  • 이 글에서는 고수준 입출력 스트림 사용, 저수준 파일 핸들에 대해서 다룬다.

1.2. C언어의 파일 지원

  • 저수준과 고수준 액세스 방법에 대한 장단점
고수준저수준
버퍼 사용사용메모리로 직접 읽어들임
입출력 대상스트림파일 핸들
속도느리다빠르다
문자 단위 입출력가능가능하지만 비효율적이다
난이도비교적 쉽다조금 어렵다
세밀한 조작어렵다가능하다
  • 두 방식의 큰 차이점은 버퍼를 쓰는가 그렇지 않은가 하는 점이다.
    • 나머지 차이점은 버퍼의 사용 유무에 따라 파생되는 특정들이다.
    • 버퍼는 파일로부터 입출력되는 데이터를 잠시 저장하는 메모리 영역이다.
      • 파일이 저장되는 하드 디스크는 모터에 의해 기계적으로 구동되므로 전자적으로 동작하는 메모리보다 상태적으로 느리다.
      • 그래서 가급적이면 하드 디스크를 액세스하는 회수를 줄이기 위해 버퍼를 사용한다.
  • 저수준과 고수준은 버퍼의 사용 유무에 따라 속도에 약간의 차이가 있기는 하지만 사실 현대의 컴퓨터 환경에서 이정도 속도차는 무시해도 될 정도다.
  • 아주 정밀하게 측정해 본다면 저수준이 조금 더 빠르기는 하겠지만 CPU와 메모리, 하드 디스크가 워낙 빠르기 때문에 체감한다는 것은 불가능하다.
  • 그래서 요즘은 저수준 파일 입출력이 큰 이점이 없는 셈이다.

2. 고수준 파일 입출력

2.1. 스트림

  • 스트림이란 바이트들이 순서대로 입출력되는 논리적인 장치를 의미한다.
    • 파일에도 바이트들이 저장되어 있으며 읽을 때나 쓸 때 순서대로 바이트들이 입출력되므로 스트림이라고 할 수 있다.
    • 키보드, 화면, 프린터 등의 물리적인 장비들도 바이트들이 순서대로 흘러 다니므로 일종의 스트림이다.
  • 대부분의 운영체제는 키보드와 화면(console), 프린터 등을 스트림이라는 동질적인 장치로 다루며 파일과 같은 방법으로 입출력 한다.
    • 파일과 키보드, 화면 등의 장치는 서로 제어하는 방법이 다르지만 스트림이라는 논리적으로 동등한 장치로 표현되기 때문에 동일한 방법으로 입출력할 수 있다.
    • 화면, 프린터, 직렬 포트 등을 파일과 동등한 장치로 취급하면 일관된 방법으로 스트립간의 데이터를 복사할 수 있으며, 사용자는 한 명령을 여러 장치에 똑같이 적용할 수 있다.
  • 스트림은 내부에 입출력 버퍼를 가지고 있으며, 이 버퍼는 스트림에 의해 자동으로 관리되므로 프로그래머는 버퍼를 준비하거나 관리할 필요가 없다.
    • 어떤 스트림으로부터 얼마만큼의 데이터를 읽거나 쓰고 싶다는 최소한의 의사 표현만 하면 필요한 동작은 스트림이 내부적으로 알아서 수행한다.
    • 그래서 스트림을 통한 입출력 방법은 사용하기 쉬우며 그래서 고수준이라고 하는 것이다.
  • 스트림의 현재 상태는 FILE 구조체에 기억된다.
    • 이 구조체는 stdio.h에 다음과 같이 정의되어 있으며 구조체의 멤버들은 운영체제에 따라 조금씩 달라지기도 한다.
    • 이 구조체에는 스트림이 내부적으로 사용하는 버퍼, 버퍼의 크기와 현재 위치, 액세스하고 있는 파일의 이름 등이 저장된다.
    • 파일 입출력 함수들은 모두 이 구조체의 내용을 참조하여 동작하도록 되어 있으며, 사용자는 이 구조체의 멤버를 직접 다룰 필요가 없다.
    • 파일 입출력을 하기 전에 이 구조체의 포인터를 하나 선언하고 입출력 함수에게 포인터만 넘겨주면 된다.

2.2 파일 열기

  • 파일을 액세스하려면 먼저 대상 파일을 열어야(Open) 한다.
    • 파일을 오픈한다는 것은 파일 입출력을 하기 위한 준비를 한다는 듯이다.
    • 스트림 입출력을 하기 위해서는 파일의 데이터를 잠시 저장하는 내부 버퍼가 필요하며, 파일의 현재 위치(FP)를 초기화해야 하는데 이런 준비를 하는 과정이 오픈이다.
  • 파일을 오픈할 때는 다음 함수를 사용한다.
    • FILE *fopen (const char *filename, const char *modes)
    • 이 함수는 지정한 파일을 액세스하기 위한 준비를 하며 이 정보들을 가지는 FILE형 구조체를 생성하고 그 포인터를 리턴한다.
    • 이 포인터는 잘 받아 두었다가 이후 입출력 함수로 전달해 주면 된다.
    • 파일 이름 인수
      • 액세스할 대상 파일의 이름이다.
      • 필요한 경우 드라이브와 디렉토리 경로를 전달할 수 있으며, 현재 위치를 기준으로 한 상대 경로를 지정할 수 있다.
    • 모드 인수
    • 모드는 파일을 어떻게 열 것인지를 지정하며 파일을 열어서 어떤 작업을 할 것인가에 따라 달라진다.
모드설명
r읽기 전용으로 파일을 연다. 이 모드로 연 파일은 읽을 수만 있으며 데이터를 기록하지는 못한다. 만약 파일이 없을 경우 에러가 리턴된다.
w\,\,\,\,\,\,\,\,\,\,\,\,쓰기 위해 파일을 연다. 이 모드로 연 파일을 쓰기만 가능하며 읽지는 못한다. 도스나 윈도우즈의 파일은 쓰기 전용 속성이 없지만 스트림은 스기 전용 상태로 열 수 있다. 파일이 없으면 새로 만들고 이미 존재한다면 기존의 파일은 지워진다.
a추가를 위해 파일을 연다. 추가란 파일의 끝에 다른 정보를 더 써 넣는다는 뜻이다. 이 모드로 연 파일은 오픈 직후에 FP가 파일의 끝으로 이동한다. 파일이 없으면 새로 만든다.
r+읽고 쓰기가 가능하도록 파일을 연다. 파일이 없을 경우 에러가 리턴된다.
w+읽고 쓰기가 가능하도록 파일을 연다. 파일이 없을 경우 새로 만든다.
a+  읽기와 추가가 가능하도록 파일을 연다. 파일이 없으면 새로 만든다.
  • 파일 형태
    • fopen의 두 번째 인수 modes에는 오픈 모드 외에도 파일의 형태를 지정하는 플래그를 추가로 지정할 수 있다.
      • 열고자 하는 파일이 텍스트 파일이면 t를 붙이고 이진 파일이면 b를 붙인다.
      • 파일 형태에 아무런 지정이 없으면 전역변수 _fnmode의 값이 사용된다.
      • 이진 파일은 아무런 변환없이 읽혀지지만 텍스트 파일 모드로 파일을 열면 다음 두 가지 변환을 한다.
        • 개행 코드를 의미하는 CR/LF 조합은 LF로 변환되어 읽혀지며 LF를 기록하면 CR/LF가 출력된다. 이런 변환을 해주는 이유는 C 문자열 출력 함수들은 개행을 위해 확장열(\n)를 사용하기 때문이다.
        • 파일의 끝을 나타내는 Ctrl+Z(0x1A)는 EOF9-1)로 변환되어 읽혀진다. 단 "a+" 모드로 열었을 때는 끝부분에 데이터를 추가할 수 있도록 Ctrl+Z를 제거한다.
  • 오픈 모드와 파일 형태가 modes 인수에 같이 기록되는데 오픈 모드가 먼저 오고 파일 형태가 뒤에 오는 형식으로 써야 한다.
    • 단, +문자는 파일 형태 다음에 와도 상관없다.
    • rt : 텍스트 파일을 읽기 전용으로 연다.
    • wb : 이진 파일을 쓰기 전용으로 연다.
    • r+b : 이진 파일을 읽기, 쓰기 가능호도록 연다. rb+로 쓸 수도 있다.
    • 이 외에 modes 인수에는 캐시를 관리하는 방법과 임시 파일 생성에 대한 몇 가지 플래그를 더 지정할 수 있다.
  • 리턴값
    • fopen은 지정한 파일을 지정한 모드로 열고 입출력에 필요한 FILE 구조체를 생성한 후 그 포인터를 리턴한다.
    • 만약 에러가 발생하면 NULL을 리턴한다.
    • 파일 입출력시에는 여러 가지 이유로 에러가 발생할 수 있으므로 이 함수의 리턴값은 반드시 점검해 보야야 한다.
      • `FILE *f; f=fopen(...); if (f==NULL){ ... }
  • fopen으로 파일 열기에 성공했으면 각 종 입출력 함수를 활용하여 파일의 데이터를 액세스할 수 있다.
  • 파일을 다 사용한 후에는 파일을 닫아야 하는데 이때는 다음 함수를 사용한다.
    • int fclose (FILE *stream);
    • 이 함수는 버퍼에 남아있는 데이터를 파일로 완전히 출력(flush)하고 파일 입출력을 위해 내부적으로 생성했던 FILE 구조체를 해제한다.
    • 다 사용한 파일은 이 함수로 반드시 닫아야 한다.

2.3. 파일 액세스

  • 파일을 열었으면 파일안의 내용을 읽고 쓴다.
  • 다음 두 함수를 이용하여 문자열을 입출력 할 수 있다.
    (모든 파일 입출력 함수는 대상 스트림을 전달하기 위해 FILE형의 구조체 포인터를 인수로 취한다는 점에서 공통적이다)
    • char *fgets(char *string, int n, FILE *stream);
    • int fputs(const char *string, FILE *stream);
  • fget가 파일에서 문자열을 읽어들이는 함수다.
    • 읽어들인 문자열을 저장할 버퍼를 첫 번째 인수로 주고, 두 번째 인수로 이 버퍼의 크기를 알려준다.
    • fgets는 최초의 개행 문자를 만날 때까지 또는 버퍼의 길이만큼 문자열을 읽어들이므로 이 함수를 반복적으로 호출하면 이 텍스트 파일을 줄 단위로 읽을 수 있다.
    • 읽기에 사용하는 buf는 2 이상이면 어떤 값을 주어도 상관없다.
      • 단 이 값이 너무 작으면 조금씩 여러 번 읽어야 하므로 전체적인 읽기 성능이 떨어질 것이다.
    • 만약 읽는 도중에 에러가 발생했거나 파일 끝에 도달했으면 NULL을 리턴한다.
  • fputs는 첫 번째 인수로 전달된 문자열을 파일로 출력하는데 중간에 개행 문자가 있더라도 한꺼번에 출력한다.
    • 만약 중간에 널 종료 문자를 만나면 널 종료 문자 앞까지만 출력한다.
// fputs 예제
// ...
void main(){
	FILE *f;
	char *str="이 파일은 C 표준 함수로 생성된 텍스트 파일입니다.\n"
		"현재 디렉토리에 Test.txt라는 이름으로 생성됩니다.\n";

	f=fopen("./Test.txt","wt");
	if (f != NULL) {
		fputs(str,f);
		fclose(f);
	}
}

// fgets 예제
// ...
void main(){
	FILE *f;
	char buf[256];

	f=fopen("./Test.txt","rt");
	if (f!=NULL) {
		for (;;) {
			if (fgets(buf,256,f)==NULL) {
				break;
			}
			printf("%s",buf);
		}
		fclose(f);
	}
}
  • int feof(FILE *stream);
    • feof함수는 인수로 주어진 스트림의 끝(EOF, End of File)까지 읽었는지를 조사한다.
    • 이 함수가 TRUE를 리턴할 때까지 fgets 함수를 호출하면 파일의 끝까지모든 내용을 읽을 수 있다.
  • int fgetc(FILE *stream);, int fputc(int c, FILE *stream);
    • 이 두 함수는 스트림으부터 문자 하나씩을 입출력한다.
    • 입출력 취치는 물론 FP이며 현재 위치에서 한 문자를 읽거나 문자 하나를 출력한다.
  • size_t fread(void *buffer, size_t size, size_t cuont, FILE *stream);
    size_t fwrite(const void *buffer, size_t size, size_t cuont, FILE *stream);
    • 이 두 함수는 블록 단위로 입출력한다.
      • 두 함수의 인수는 동일한데 buffer에 저장된 size크기의 메모리 블록 count개를 스트림으로 입출력하며 실제로 입출력한 길이를 리턴한다.
      • 대개의 경우 지정한 크기만큼 입출력하지만 파일의 끝 부분을 읽거나 디스크가 가득찼을 때는 더 작은 크기만 입출력할 수 있다.
      • fread가 지정된 크기(size X count)만큼 다 읽지 못할 수도 있으므로 fread가 리턴하는 실제 읽은 길이만큼 fwrite로 출력해야 한다.
      • 적어도 구조체 배열정도는 되어야 파일로 저장할만한 가치가 있다.
        • 저장할 대상(멤버)이 가변 길이일 경우 길이를 반드시 먼저 저장해야 한다.
void main(){
	FILE *src, *dest;
	char buf[256];
	size_t nRead;

	src=fopen("./Test.txt","rb");
	if (src != NULL) {
		dest=fopen("./Test2.txt","wb");
		if (dest != NULL) {
			while(!feof(src)) {
				nRead=fread(buf,1,256,src);
				printf("%ld\n",nRead);
				fwrite(buf,1,nRead, dest);
			}
			fclose(dest);
		}
		fclose(src);
	}
}
  • int fscanf(FILE *stream, const char * format [,argument ]...);
    int fprintf(FILE *stream, const char * format [,argument ]...);
    • 위 두 함수는 서식화된 스트림 입출력 함수이다.
    • 사용하는 방법은 scanf, printf와 동일하되 대상이 화면이나 키보드가 아니라 파일이라는 점만 다르다.
    • 이 두 함수를 사용하면 정수나 실수 변수를 스트림으로 입출력할 수 있다.
void main(){
	char str[128]="String";
	int i=1234;
	double d=3.1416;
	FILE *f;

	f=fopen("./Test.dat","wb");
	if (f != NULL) {
		fprintf(f,"%d %f %s",i,d,str);
		fclose(f);
	}

	i=0;
	d=0.0;
	f=fopen("./Test.dat","rb");
	if (f!=NULL) {
		fscanf(f,"%d %lf %s",&i,&d,&str);
		printf("파일에서 읽은 정수값=%d, 실수값=%f, 문자열=%s\n",i,d,str);
		fclose(f);
	}
}

2.4. 임의의 접근

  • 스트림은 다음 입출력할 파일의 위치를 항상 기억하고 있는데 이 위치를 FP(File Position)라고 한다.
  • 최초 파일을 열 대 FP는 선두를 가리키고 있으며, 스트림에서 내용을 읽거나 쓸 때 FP는 액세스한 만큼 자동으로 뒤로 이동한다.
  • fgets를 반복적으로 호출하기만 하면 FP가 읽은 만큼 이동하므로 파일의 모든 내용을 줄 단위로 순서대로 읽을 수 있는 것이다.
  • 이런식으로 파일의 처음부터 뒤쪽으로 순서대로 내용을 액세스하는 것을 순차 접근(Sequential Access)이라고 한다.
  • 이에 비해 파일의 임의의 위치로 이동하면서 원하는 내용을 읽는 방법을 임의 접근(Random Acccess)라고 한다.
  • 다음 액세스할 위치를 옮기고 싶을 때는 FP를 원하는 위치로 옮긴 후 액세스 함수를 호출하면 된다.
  • 다음 함수는 FP를 원하는 곳으로 옮겨 준다.
  • int fseek(FILE *stream, long offset, int origin);
    • 첫 번째 인수는 대상 스트림이며 두 번째 인수 offset은 FP를 어디로 옮길 것인가를 지정하며 세 번째 인수 origin은 어디를 기준으로 FP를 옮길 것인가를 지정한다.
    • origin은 다음 세 가지 종류가 있다.
      • SEEK_SET은 스트림의 선두를 기준으로 FP를 이동시킨다.
      • SEEK_CUR는 현재위치를 기준으로
      • SEEK_END는 스트림의 끝을 기준으로 한다.
        • SEEK_END의 경우 offset은 음수여야 한다.
  • 텍스트 파일의 특정 문자열을 읽기 위해 임의 접근을 사용하는 것은 사실 별로 실용성이 없다.
    • 일정한 크기를 가지는 구조체 배열이 저장된 파일에서 n번째 구조체를 액세스할 때 임의 접근이 사용되며 현실적인 실용성이 있다.
void main(){
	FILE *f;
	char buf[256];

	f=fopen("./Test.txt","rt");
	if (f!=NULL) {
		fseek(f,23,SEEK_SET);
		fgets(buf,256,f);
		printf("%s",buf); //  결과 : 함수로 생성된 텍스트 파일입니다.
		fclose(f);
	}
}
  • long ftell(FILE *stream);
    void rewind(FILE *stream);
    • 이 두 함수는 현재 FP를 조하거나 리셋한다.
    • ftell은 스트림의 현재 FP를 조사한다.
    • rewind는 FP를 파일 선두로 보내는데 fseek(f, 0, SEEK_SET);과 동일한 명령이다.
      • 파일의 처음부터 다시 액세스하고 싶을 때 이 함수를 사용한다.
    • fseek, ftell과 똑같은 동작을 하는 fgetpos, fsetpos라는 함수들도 있다.

2.5. 기정의 스트림

  • 고수준 입출력 함수들은 스트림은 대상으로 입출력을 수행한다.
  • 스트림이란 파일뿐만 아니라 키보드나 모니터처럼 바이트를 연속적으로 입출력하는 물리적인 장치가지 포과하는 개념이므로 고수준 입출력 함수로 이런 장치들을 다룰 수 있다.
  • 키보드나 화면을 스트림으로 관리하고자 할 때는 미리 정의되어 있는 표준 스트림을 사용한다.
이름설명버퍼
stdin표준 입력사용
stdout표준 출력미사용
stderr표준 에러미사용
  • FILE *를 인수로 요구하는 함수에 표준 스트림 대신 사용할 수 있으며 이 스트림들은 미리 정의되어 있기 때문에 별도로 오픈할 필요 없이 바로 사용할 수 있다.
  • 항상 열려 있으므로 다 사용한 후에 닫을 필요도 없다.
void main(){
	char buf[256];

	fputs("문자열을 입력해 보시오 : ",stdout);
	fgets(buf,256,stdin);
	fputs(buf,stderr);
}
  • stderr는 표준 출력 스트림인 stdout과 마찬가지로 화면을 가리키며 주로 에러 메시지를 출력하기 위해 사용한다.
  • 디폴트는 화면으로 되어 있지만 프린터나 파일로 재지향(Redirection)할 경우 에러 메시지를 별도의 장치로 보낼 수 있다.
    • 통합 개발 환경이 일반화되기 전의 싱글 태스킹 환경에서 디버깅 목적으로 이 스트림을 사용했었으나 요즘은 그럴 필요가 없어졌다.

  • 아래는 fflush(stdin)에 대한 설명인데 stdin에 대한 이 함수의 작동은 vc2015까지만 가능하다.
    • 즉, Windows이외의 운영체제에서는 아래의 내용과 같이 작동하지 않음.
    • C 언어 표준에 따르면, fflush()는 output stream에 대해서만 동작한다.
      • 혹시 stdout을 이용할 수 있지만 참고 stdout은 버퍼가 없다.
  • 표준 스트림 중에 stdin은 버퍼를 사용하기 때문에 stdin으로부터 입력을 받는 함수들은 키보드로부터 직접 입력을 받지 않고 버퍼에 입력되어 있는 값을 꺼내온다.
  • 그러다 보니 미리 입력해 높은 텍스트가 다음 번 입력 함수가 호출될 때 읽려지는 경우가 종종 있다.
  • scanf 함수는 문자열을 입력받을 때 공백으로 구분된 단어까지만 입력받고 나머지는 버퍼에 그대로 남겨두며 다음번에 버퍼에 남아 있는 값을 읽어온다.
    • 실시간으로 입력을 받아야 하는 프로그램의 경우 버퍼에 아직 읽지 않은 문자가 남아 있다면 입력 대기를 하지 않을 뿐만 아니라 버퍼의 문자를 바로 읽어 버리는 문제가 있다.
  • 이때는 다음 함수로 입력 버퍼에 남아 있는 불필요한 텍스트를 지운 후 다시 입력을 받아야 한다.
  • int fflush(FILE *stream);
  • 버퍼는 원래 운영체제가 관리하는 것이며 적당한 시간이 되면, 예를 들어 버퍼가 가득 차거나 스트림을 닫을 때 또는 한가할 때 백그라운드에서 비동기적으로 비워진다. 그래서 응용 프로그램은 버퍼 관리에 대해서는 신경쓰지 않아도 된다.
  • 그러나 당장 사용해야할 데이터라면 버퍼가 비워질 때까지 기다릴 수 없으며 즉시 버퍼를 비워야 하는데 이때 fflush 함수를 사용한다.
  • 플러쉬란 버퍼에 남아 있는 데이터를 비운다는 뜻인데, 출력용일 경우 출력 스트림으로 완전히 보내고, 입력용일 경우는 버퍼에 남아 있는 데이터를 삭제한다.
    • 출력 대기 중인 데이터를 즉시 버퍼로 보내거나 입력 버퍼에 남아 있는 불필요한 텍스트를 지울 때는 fflush 함수로 버퍼를 비우면 된다.
    • fflush(stdin);명령으로 입력 버퍼를 비운 후 scanf를 호출하면 scanf는 항상 새로 입력을 받을 것이다.

2.6. 예시

struct tag_Friend {
	char *Name;
	int Age;
	double Height;
};

tag_Friend Friends[256]={
	{NULL, 30, 178.2 },
	{NULL, 19, 169.8 },
	{NULL, 26, 176.5 },
	{NULL, 58, 172.3 },
	{NULL, 0, 0 },
};

struct tag_Header {
	char desc[32];
	int ver;
	int num;
};

int Num=4;

void DeleteAll(){ // 사용중인 모든 메모리를 해제한다.
	int i;

	for (i=0;i<Num;i++) {
		if (Friends[i].Name == NULL) {
			break;
		} 
		free(Friends[i].Name);
	}
	memset(Friends,0,sizeof(Friends));
	Num=0;
}

void WriteFriend(){ // 구조체 배열을 파일로 저장하기
	FILE *f;
	tag_Header H;
	int i,len;

	f=fopen("./Friend.dat","wb");
	if (f == NULL) {
		printf("파일을 생성할 수 없습니다.\n");
	} else {
		strcpy(H.desc,"친구목록");
		H.ver=110;
		H.num=Num; // 주소록 목록 갯수
		fwrite(&H,sizeof(tag_Header),1,f);
		// Name의 길이, Name, Age, Height
		for (i=0;i<Num;i++) {
			len=strlen(Friends[i].Name);
			fwrite(&len,sizeof(int),1,f);
			fwrite(Friends[i].Name,len,1,f);
			fwrite(&Friends[i].Age,sizeof(int),1,f);
			fwrite(&Friends[i].Height,sizeof(double),1,f);
		}
		fclose(f);
		printf("파일을 저장했습니다.\n");
	}
}

void ReadFriend(){ // 파일로부터 구조체 배열 읽어오기
	FILE *f=NULL;
	tag_Header H;
	int i,len;

	f=fopen("./Friend.dat","rb");
	if (f == NULL) {
		printf("파일을 열 수 없습니다.\n");
	} else {
		DeleteAll(); // 일단 사용중인 데이터를 먼저 지운다.
		fread(&H,sizeof(tag_Header),1,f);
		if (strcmp(H.desc,"친구목록") != 0) {
			puts("주소록 파일이 아닙니다.");
			goto end;
		}
		if (H.ver != 110) {
			printf("버전이 틀립니다.\n");
			goto end;
		}
		Num=H.num;
		for (i=0;i<Num;i++) {
			fread(&len,sizeof(int),1,f);
			Friends[i].Name=(char *)calloc(len+1,1);
			fread(Friends[i].Name,len,1,f);
			fread(&Friends[i].Age,sizeof(int),1,f);
			fread(&Friends[i].Height,sizeof(double),1,f);
		}
		printf("파일을 읽었습니다.\n");
	}

end:
	if (f)
		fclose(f);
}

// main 함수는 생략
//    main는 Friends배열의 Friend 구조체의 name 멤버에 동적할당하고
//    이름을 채워 넣음으로써 사용자가 입력하는 행위를 흉내내고 있다.

3. 저수준 파일 입출력 (gcc에서 아래 내용이 정확히 작동할지 보장 못함)

3.1. 파일 핸들

  • 저수준 파일 입출력 방법은 운영체제(DOS, 윈도우즈)가 파일을 관리하는 방법과 동일하다.
  • 고수준은 파일을 스트림이라는 논리적인 대상으로 취급할 수 있도록 하는데 비해 저수준은 파일을 핸들로 관리한다는 것이 특징이다.
  • 스트림에 비해 핸들을 사용하는 방법이 조금더 어렵지만 성능은 더 좋다.
  • 저수준 파일 입출력을 할 때는 먼저 대상 파일을 열어 핸들을 얻어야 한다.
    • 핸들은 열려진 파일을 대표하는 값이며 핸들에는 파일을 액세스하기 위한 모든 정보가 저장되어 있다.
  • gcc에서는 fcntl.h과, unistd.h, sys/stat.h 헤더파일을 include 해야한다.
  • 파일을 열고 닫을 때는 다음 두 함수를 이용한다.
    • open (const char *filename, int oflag, [, int pmode]);
    • int close (int fd);
  • filename 인수로 열고자 하는 파일의 경로를 주는데 드라이브와 디렉토리 정보를 포함한 완전 경로를 줄 수도 있고 현재 디렉토리를 기준으로 한 상대 경로로 줄 수 있다.
  • oflag 인수에는 파일을 어떤 모드로 열 것인지를 지정하는 다음과 같은 플래들을 준다.
    • OR 연산자로 여러 개의 플래그를 한꺼번에 지정할 수도 있다.
플래그설명
O_CREAT새로 파일을 만들며 파일이 이미 존재하면 아무 것도 하지 않는다.
O_RDONLY읽기 전용으로 연다.
O_RDWR일기 쓰기가 가능하도록 연다
O_WRONLY쓰기 전용으로 연다.
O_TRUNC파일을 열고 크기를 0으로 만든다. O_CREAT와 함께 사용될 경우 새로우 파일을 만든다.
O_APPENDFP를 파일 끝으로 보낸다.
O_EXCLO_CREAT 플래그와 함께 사용되며 파일이 이미 존재할 경우 에러를 리턴한다.
  • 마지막 인수 pmode는 파일의 보안 속성을 지정하는데, O_CREAT 플래그로 파일을 새로 만들 때만 적용된다.
    • 나머지 플래그로 파일을 열 때 이 인수는 생략한다.
  • 생성한 파일을 닫을 때 보안 속성이 적용되는데 이미 파일이 존재한다면 pmode 인수는 무시된다.
    • 파일을 읽기 가능하도록 하려면 __S_IREAD 플래그를 주고, 쓰기 가능하도록 하려면 __S_IWRITE를 주며 두 플래그를 모두 다 줄 수도 있다.
  • __S_IWRITE플래그를 주면 이 파일은 기록 가능한 파일이 되며 __S_IREAD만 주면 읽기 전용으로 파일이 된다.
  • open은 파일을 무사히 열었을 경우 파일의 핸들을 리턴하는데 이 값은 정수형 변수에 잘 저장해두었다가 이후 저수준 파일 액세스 함수로 전달한다.
    • 에러 발생시 -1을 리턴하는데 안전을 위해 이 리턴값을 반드시 점검해 볼 필요가 있다.
  • 파일 입출력이 끝난 후 close 함수로 파일을 닫는다.

3.2. 저수준 파일 액세스

  • 저수준으로 파일을 읽고 쓸 때는 다음 함수를 사용한다.
    • 두 함수 모두 첫 번재 인수로 파일 핸들을 요구하는데 open으로 구한 핸들을 전달하면 된다.
    • read (int fd, void *buffer, size_t nbytes);
      write (int fd, const void *buffer, size_t n);
  • read 함수는 파일로 부터 nbytes바이트를 읽어 buffer에 채운다.
    • buffer는 nbytes보다 크거나 최소한 같아야 하는데 통상 malloc으로 필요한 만큼 메모리를 할당한 후 사용한다.
    • read는 파일에서 읽은 실제 바이트 수를 리턴하는데 이 값은 통상 nbytes와 같지만 파일 끝에서는 nbytes보다 더 작을 수도 있다.
    • 파일에서는 바이트를 읽은 후 FP는 읽은 만큼 뒤로 자동으로 이동한다.
      • 만약 파일 끝이거나 파일 핸들이 무효하다면 -1이 리턴된다.
  • write는 buffer에 저장된 n바이트 만큼 파일로 출력한다.
    • 리턴값은 실제로 파일로 출력된 바이트수인데 이 값은 통상 count와 같지만 디스크 공간이 부족한 경우에는 count보다 작은 값일 수도 있다.
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

void main()
{
	int file;
	char buf[256]={0,};

	file=open("./Test.txt",O_RDONLY);
	if (file != -1) {
		read(file,buf,256);
		printf("%s",buf);
		close(file);
	}
}
  • 다음 예제는 저수준 파일 입출력 함수를 사용하여 파일을 복사한다.
void main(int argc, char *argv[]){
	int src, dest;
	int readnum;
	void *buf;

	if (argc < 3) {
		printf("복사 원본과 목적 파일 이름을 지정해야 합니다.\n");
		exit(1);
	}

	src=open(argv[1],O_RDONLY);
	if (src == -1) {
		printf("원본 파일을 열 수 없습니다.\n");
		exit(1);
	}

	dest=open(argv[2],O_CREAT | O_WRONLY | O_TRUNC,
		__S_IWRITE);
	if (dest == -1) {
		printf("목적 파일을 생성할 수 없습니다.\n");
		close(src);
		exit(1);
	}

	buf=malloc(60000); //buffer 크기를 더 크게 늘려주면 복사 속도가 조금 더 빨라진다.

	for (;;) {
		readnum=read(src,buf,60000);
		if (readnum == 0) {
			break;
		}
		write(dest,buf,readnum);
	}

	close(src);
	close(dest);
	free(buf);
	printf("%s 파일을 %s로 복사했습니다.\n",argv[1],argv[2]);
}

  • 저수준 입출력 함수도 고수준과 마찬가지로 다음 액세스할 위츠를 FP로 가리키는데 FP를 옮기면 임의의 위치를 액세스할 수 있다.
  • 다음 함수는 저수준 파일 입출력의 임의 접근 함수다.
    __off_t lseek (int fd, __off_t offset, int origin);
    • __offset_tlong 타입이다.
    • 첫 번째 인수가 스트림 대신 파일 핸들이라는 점만 다를 뿐 두 함수 모두 사용하는 방법과 기준 위치는 고수준의 경우와 동일하다.

4. 파일 관리

4.1. 기본적인 파일 관리

  • 파일 관리 함수들은 저장된 데이터를 대상으로 하는 것이 아니라 파일 그 자체를 대상으로 한다는 점이 다르다.
  • int access (const char *path, int mode);
    • access 함수는 파일 또는 디렉토리의 보안 허가 상태(Permssion) 즉 쓰기가 가능한지, 읽기 전용인지 등을 조사하는데 주로 파일이 존재하는지를 조사하는 목적으로 많이 사용한다.
    • 파일을 액세스하기 전에 해당 파일이 실제로 존재하는지를 먼저 알아야 한다면 이 함수로 조사할 수 있다.
    • path에 조사할 파일의 경로를 주고, mode에 조사할 상태를 지적하는데 0은 존재, 2는 쓰기, 4는 읽기를 나타낸다.
    • 요청한 허가 상태를 가지면 이 함수는 0을 리턴하며 그렇지 않으면 -1을 리턴한다.
  • int remove (const char *path);
    unlink (const char *filename);
    • 이 두 함수는 파일을 삭제한다.
    • 삭제하고자 하는 파일의 경로를 인수로 전달해 주기만 하면된다.
    • 두 함수는 이름만 다르며 실제로는 같은 함수다.
  • int rename (const char *oldname, const char *newname);
    • 이 함수는 파일 이름을 변경한다.
    • 변경하고자 하는 파일의 이름과 새로 설정한 파일의 이름을 인수로 지정하면 된다.
  • int chmod (const char *filename, __mode_t pmode)
    • __mode_tunsigned int 다.
    • 이 함수는 파일의 속성을 변경한다.
      • 즉, 읽기 전용으로 만들 것인지 아니면 읽기 쓰기가 가능한 파일로 만들 것인지를 변경한다.
    • 대상 파일의 이름과 새로 지정할 속성을 지정하되 속성은 __S_IREAD, __S_IWRITE 둘 중 하나를 주거나, 둘 다 줄 수도 있다.

4.2. 파일 검색 (생략, GCC에서 지원하지 않음)


4.3. 디렉토리 관리

  • int chdir (const char *dirname)
    int mkdir (const char *dirname, __mode_t __mode)
    int rmdir (const char *dirname)
    • chdir 함수는 현재 디렉토리를 변경한다.
      • 이 함수로 변경한 현재 디렉토리를 작업 디렉토리라고 하며, 이후 사용되는 상대 경로들은 작업 디렉토리를 기준으로 한다.
    • mkdir은 디렉토리를 생성하며,
    • rmdir은 디렉토리를 제거하되 비어있지 않은 디렉토리는 삭제할 수 없다.
  • char *getcwd (char *buffer, size_t __size)
    • 이 함수는 현재 작업 디렉토리를 조사한다.

4.4. 디스크 관리 (생략, GCC에서 지원하지 않음)


출처 : 혼자 연구하는 C/C++ 1 / 김상형 저 / 와우북스

profile
White book for everything I need.

0개의 댓글