2022. 12. 16. 00:11ㆍC언어 복습
1. 파일 입출력
파일을 읽고 쓰는 것에 대한 입출력 스트림은 지난 글에서 봤던 표준 입출력 스트림의 방식과 동일하다. 사용자의 요청으로 별도의 파일을 읽고 쓰기 위한 스트림이 만들어지고 없어진다는 것만 제외하면 말이다. 다만 파일을 읽고 쓸 때는 일반적으로 이전 글과는 다르게 텍스트 형식이 아니라 바이너리 형식으로 데이터를 읽고 쓴다. 다만 주의할 것이 있는데 같은 형식의 데이터라고 해도 크기가 시스템마다 다를 수 있어서 파일에 저장할 데이터의 크기를 고정해두는 것이 좋다. 어떤 시스템에서 만들어진 파일을 다른 시스템의 실행 파일에서 사용할 때 문제가 생길 수 있기 때문이다. 그럼 바이너리 데이터를 읽고 쓰는 함수와 파일을 열고 닫는 함수를 알아보자.
/* 데이터를 몇 번 읽었는지 반환 */
size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
/* 데이터를 몇 번 썼는지 반환 */
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
/* 스트림을 반환하고 열기 실패하면 NULL 반환 */
FILE* fopen(const char* filename, const char* mode);
/* 파일 닫기를 성공하면 0을 반환하고 실패하면 EOF 반환 */
int fclose(FILE* stream);
/* 스트림의 버퍼를 잘 비웠으면 0을 반환하고 실패하면 EOF 반환 */
int fflush(FILE* stream);
/* 스트림의 EOF표시자의 상태를 반환하며 EOF가 아니면 0, EOF면 0이 아닌 값을 반환 */
int feof(FILE* stream);
/* 스트림의 오류 표시자의 상태를 반환하며 오류가 없으면 0, 있으면 0이 아닌 값을 반환 */
int ferror(FILE* stream);
/* EOF표시자, 오류 표시자의 값이 잘 지워지지 않아서 그 값을 강제로 지워주는 함수 */
void clearerr(FILE* stream);
여기서 fflush함수는 이전 글에서 출력 버퍼를 비우는 용도로 사용했었는데 파일 입출력에서도 그렇게 사용된다. 사실 만약 다음과 같은 코드를 짰다고 가정하면 데이터가 바로 파일에 써지지 않고 버퍼에 존재하는 상태가 되기 때문에 바로 파일에 변경 사항을 적용시키고 싶을 때 fflush함수를 사용한다. 다만 파일을 쓰기 용도로 열었다가 닫는다면 파일에 데이터를 출력하고 닫을 때, 출력 버퍼에 있는 내용은 전부 비워지면서 파일로 쓰기 후 닫히게 된다.
그리고 다들 까먹기 쉬운 부분이 파일을 닫는 것이다. 다른 언어는 프로그래머가 파일을 닫지 않아도 알아서 처리를 해주는데 C같은 언어는 그런 거 없다. 프로그래머가 닫아줘야 한다. 까먹지 않는 팁을 강의에서 알려줬는데 무슨 용도로 파일을 열든 간에 일단 파일을 여는 함수를 썼으면 무조건 묻지도 따지지도 말고 바로 아래줄에 fclose를 박고 시작하는 것이다. 이러면 열심히 코드를 짜는 와중에도 파일 닫기를 까먹을 일이 없다. 아래는 파일을 읽고 쓰는 예제 코드이다.
pocu_stream.h
#ifndef __POCU_STREAM_H__
#define __POCU_STREAM_H__
#define BLOCK_LENGTH 6
/* 기존에 있던 코드 생략 */
void read_file(const char* filename);
void write_file(const char* filename);
void append_file(const char* filename);
void copy_file(const char* src, const char* dest);
#endif
pocu_stream.c
#include "pocu_stream.h"
#include <stdio.h>
/* 기존에 있던 코드 생략 */
void read_file(const char* filename)
{
char block[BLOCK_LENGTH];
FILE* stream = fopen(filename, "rb");
if (stream == NULL)
{
perror("read_file");
return;
}
while (fread(block, sizeof(*block), BLOCK_LENGTH, stream) > 0)
{
fputs(block, stream);
}
fclose(stream);
}
void write_file(const char* filename)
{
char block[BLOCK_LENGTH];
FILE* stream = fopen(filename, "wb");
if (stream == NULL)
{
perror("write_file");
return;
}
while (fgets(block, sizeof(*block) * BLOCK_LENGTH, stdout) != NULL)
{
fwrite(block, sizeof(*block), BLOCK_LENGTH, stream);
}
fclose(stream);
}
void append_file(const char* filename)
{
char block[BLOCK_LENGTH];
FILE* stream = fopen(filename, "ab");
if (stream == NULL)
{
perror("append_file");
return;
}
while (fgets(block, sizeof(*block) * BLOCK_LENGTH, stdout) != NULL)
{
fwrite(block, sizeof(*block), BLOCK_LENGTH, stream);
}
fclose(stream);
}
void copy_file(const char* src, const char* dest)
{
FILE* src_file;
FILE* dest_file;
int c;
src_file = fopen(src, "rb");
if (src_file == NULL)
{
perror("src_file is NULL");
return;
}
dest_file = fopen(dest, "wb");
if (dest_file == NULL)
{
perror("dest_file is NULL");
goto close_src_file;
}
c = fgetc(src_file);
while (c != EOF)
{
fputc(c, dest_file);
c = fgetc(src_file);
}
fclose(src_file);
fclose(dest_file);
close_src_file:
if (fclose(src_file) == EOF)
{
perror("fclose(src_file) is EOF");
}
}
2. 예외 처리
위에서 5개의 함수를 소개했는데 각 함수가 어떤 값을 반환하는지 기억하는 것도 중요하다. 왜냐하면 그 반환값으로 예외처리를 하기 때문이다. 특히 이 강의에서는 fopen과 fclose에 대한 예외 처리를 위해 각 함수의 반환값을 사용했다. 파일 입출력을 할 때, 함수 안에서 지역 변수에 파일을 열어놓고 포인터로 가리키고 있었는데 닫지 않고 나가 버리면 누수가 발생할 수 있기 때문이다. 그래서 파일 입출력을 할 때는 파일이 제대로 열렸는지와 파일이 제대로 닫혔는지 검사하는 코드를 따로 넣어줘야 한다. 그리고 그런 코드를 작성할 때, 사용하는 것들이 있다.
#include <errno.h>
fprintf(stderr, "error no - %d\n", errno);
이전 글에서 stdin, stdout, stderr에 대해서 배운 적이 있는데 stdin, stdout만 쓰다가 이제 stderr을 쓸 차례가 왔다. 에러 메시지를 출력하기 위한 스트림이며 예외 처리 메시지를 바로 출력시켜야 하기 때문에 대부분 별도의 버퍼는 가지고 있지 않다. errno는 errno.h라는 헤더파일 어딘가에 저장되어 있는 에러 코드를 반환하는 매크로이다. 가장 최근에 어떤 함수가 실패했을 때, 그 원인을 숫자로 저장하고 있다가 원할 때 가져다 사용할 수 있는 것이다.
근데 여기서 문제가 있다. 저 errno라는 매크로에서 나오는 숫자는 어떤 의미를 가지고 있는지 알 방법이 없다는 것이다. 심지어 외우려고 해도 컴파일러마다 어떤 숫자가 어떤 오류인지에 대한 구현을 다르게 해놓고 있다... 이 문제를 해결하기 위한 함수가 있으니 바로 아래의 stderror함수이다.
#include <string.h>
char* strerror(int errnum);
여기에 erro라는 매크로를 넣어서 문자열로 출력하면 어떤 오류가 있었는지 친절하게 알려준다. 즉, 파일이 제대로 열리지 않을 때에 대한 예외 처리 코드는 아래와 같이 작성할 수 있는 것이다.
#include <stdio.h>
#include <string.h>
#include <errno.h>
void use_file(const char* filename)
{
FILE* stream = fopen(filename, "rb");
if (stream == NULL)
{
fprintf(stderr, "%s\n", strerror(errno));
return;
}
/* 굉장한 코드 */
}
근데 사실 저기서 에러 코드를 작성해주는 코드를 알아서 내부적으로 해주는 함수가 있다...
#include <stdio.h>
void perror(const char* str);
아까와 비교했을 때 장점은 결국 string.h와 errno.h를 굳이 include시키지 않아도 된다는 것이다. 무엇보다 귀찮게 길게 타이핑을 할 필요가 없다!
#include <stdio.h>
void use_file(const char* filename)
{
FILE* stream = fopen(filename, "rb");
if (stream == NULL)
{
perror("아무거나 출력해~ Something~");
return;
}
/* 굉장한 코드 */
}
/*
파일 열기에 실패하면 아래와 같은 결과가 나온다.
아무거나 출력해~ Something~: No such file or directory
*/
C에서 오류를 처리하는 방식은 이런 방식이다. 오류가 생기면 오류 코드를 반환을 바로 해주거나 어딘가에 오류 코드를 들고 있다가 프로그래머가 필요하면 오류 코드를 반환해주는 방식이다. 사실 꽤 불편하고 별로 좋은 방법은 아니라고 강의에서 언급했지만 어쨋든 C에서 오류 처리를 이런 식으로 하고 있다고 한다. 결론은 파일을 잘 닫고 예외 처리 잘 해야 한다.
3. 스트림 위치
C에서 스트림에는 3개의 표시자가 있으며 모두 FILE 구조체에 존재한다.
- EOF 표시자: 스트림을 읽고 쓰는 중 EOF를 만나면 세팅
- 오류 표시자: 스트림을 읽고 쓰는 중 EOF이외의 오류를 만나면 세팅
- 파일 위치 표시자: 스트림의 현재 위치를 가리키며 스트림에서 데이터를 읽고 쓸 때마다 세팅
이 글에서 다룰 표시자는 파일 위치 표시자이다. C는 스트림을 통해서 파일을 읽고 쓰는데 그 과정에서 파일의 어느 부분을 읽고 쓰고 있는지 알 수 있으며 원한다면 이 위치를 마음대로 바꿀 수 있다. 아래는 이와 관련된 함수들의 목록이다.
#include <stdio.h>
/* 파일 위치 표시자를 시작 위치로 되돌림 */
void rewind(FILE* stream);
/* 위치 이동 성공하면 0을 반환하고 실패하면 0이 아닌 값을 반환 */
int fseek(FILE* stream, long offset, int origin);
/* 파일 위치 표시자의 현재 위치를 반환하며 실패하면 -1 반환 */
long ftell(FILE* stream);
rewind함수는 쉬우니까 넘어가고 중점적으로 볼 것은 fseek와 ftell이다. 우선 fseek를 보면 3개의 매개 변수를 받는다. 스트림, 몇 칸 이동할지, 어디서부터 이동할지를 매개 변수로 받고 있다. 여기서 중요한 것은 origin에는 3개의 값 중 하나의 값만 넣어야 한다는 것이며 의미는 다음과 같다.
- SEEK_SET(0): 스트림의 시작 위치
- SEEK_CUR(1): 현재 위치
- SEEK_END(2): 스트림의 맨 끝
참고로 표준에서는 SEEK_END의 지원을 강제하고 있지 않지만 거의 모든 곳에서 정상적으로 동작할 수 있게 유효한 값을 반환하도록 지원하고 있다. 그리고 SEEK_END에서 1칸 뒤로 가는 식의 연산도 허용을 하기 때문에 컴파일러가 잡아주지 않는다. 그러니까 파일에 잘못 접근하지 않도록 조심하자!
ftell함수는 파일 위치 표시자의 위치를 반환하는데 fseek함수와 같이 쓰이는 경우가 많다. 단, ftell이 실패하는 경우가 있는데 일반적인 파일을 대상으로 하는 경우에는 발생하는 일이 거의 없지만 다음과 같은 경우에는 발생할 확률이 존재한다.
- 파일 크기가 없는 스트림 (소켓, 파이프 등등)
- 파일의 크기가 제공되지 않는 경우 (가상 파일 시스템)
다음은 강의에서 제시한 예제 문제이며 다음과 같이 직접 풀어보았다. 파일에 있는 문자열을 읽는데 그곳에 만약 ':'(콜론)이 있다면 콜론 사이에 있는 문자열은 한 번 더 출력하는 문제이다. 예를 들어 "ab:cde:fg:h:"라는 문자열이 있다면 최종 출력은 "abcdecdefghh"가 되어야 한다. cde와 h가 두 번 출력되어야 하기 때문이다. pocu_stream.h는 변경 사항이 함수 선언외에 거의 없으므로 생략한다.
pocu_stream.c
void print_with_repeat(const char* filename)
{
FILE* stream;
int c;
int colom_num = -1;
long repeat_begin_pos;
long repeat_current_pos;
stream = fopen(filename, "rb");
if (stream == NULL)
{
perror("stream is NULL");
return;
}
c = fgetc(stream);
while (c != EOF)
{
if (c == ':')
{
colom_num++;
switch (colom_num)
{
case 0:
repeat_begin_pos = ftell(stream);
if (repeat_begin_pos == -1)
{
perror("repeat_begin_pos is -1");
goto exit;
}
break;
case 1:
colom_num = -2;
repeat_current_pos = ftell(stream);
if (repeat_current_pos == -1)
{
perror("repeat_current_pos is -1");
goto exit;
}
break;
}
}
else
{
fputc(c, stdout);
}
c = fgetc(stream);
}
exit:
fclose(stream);
}
'C언어 복습' 카테고리의 다른 글
POCU C언어 정주행 11회차 - 비트 패턴, 공용체 (0) | 2022.12.26 |
---|---|
POCU C언어 정주행 10회차 - 구조체 패딩 (0) | 2022.12.22 |
POCU C언어 정주행 8회차 - 스트림, 입출력 함수, 버퍼, 입력 알고리즘 (0) | 2022.12.14 |
POCU C언어 정주행 7회차 - 문자열 함수 특징, 문자열 함수 구현 및 설명 (0) | 2022.12.12 |
POCU C언어 정주행 6회차 - 댕글링 포인터, 포인터 연산, 캐스팅, ASLR (0) | 2022.12.10 |