들어가며
C 언어에는 파일을 읽을 수 있는 여러 함수가 있습니다. 그중 가장 많이 사용되는 것이 바로 read()와 fread()이며, getchar()처럼 문자 단위로 파일을 읽는 함수도 함께 제공됩니다.
파일 처리 함수들은 프로그래머가 시스템에 저장된 정보를 읽고 활용할 수 있게 해주는 핵심 자원입니다. 따라서 각 함수의 호출 방식, 입력·출력 인자, 반환값 등을 정확히 이해하고 능숙하게 다룰 수 있는 것이 매우 중요합니다.
이 글에서는 read() 함수를 사용해 파일을 읽는 방법을 체계적으로 알아보겠습니다.
- read() 함수의 문법과 호출 방법
- 입력·출력 인자와 각 인자가 받아들이는 데이터 타입
- 실제 동작하는 예제 코드
- 발생 가능한 모든 에러와 이를 감지·식별하는 기법
read() 함수의 문법
read() 함수의 기본 문법은 다음과 같습니다.
size_t read(int fd, void *buf, size_t count);
read() 함수의 동작 원리
read() 함수는 입력 인자 fd로 지정된 열려 있는 파일의 내용을 읽습니다. 이 함수는 파일 내용을 count에 지정한 바이트 수만큼 읽어 buf가 가리키는 버퍼로 복사합니다. 여기서 fd는 파일을 열 때 open() 함수가 반환하는 정수형 파일 디스크립터입니다.
read() 함수가 성공적으로 실행되면 실제로 읽은 바이트 수를 반환합니다. 반환값이 0이면 파일의 끝(EOF)에 도달했다는 의미이며, -1이면 에러가 발생했음을 나타냅니다.
구체적인 에러 종류는 전역 변수 errno에서 코드를 확인하여 식별할 수 있습니다. 뒤에서 read() 함수 사용 시 발생할 수 있는 에러 목록과 이를 감지·식별하는 방법을 자세히 다루겠습니다.
read() 함수는 unistd.h 헤더에 선언되어 있으므로, 사용하려면 코드에 다음과 같이 헤더를 포함해야 합니다.
#include <unistd.h>
read() 함수로 파일 읽기 실습
이번 예제에서는 open()과 read() 함수를 함께 사용하여 파일을 열고 내용을 읽어 보겠습니다.
먼저 리눅스 환경에서 "example.txt"라는 이름의 텍스트 파일을 생성하고 "Documents" 디렉터리에 저장한 뒤, 임의의 텍스트를 작성해 두었습니다.
1단계: 헤더 포함 및 변수 선언
파일을 읽는 코드 작성의 첫 번째 단계는 필요한 헤더를 포함하고 빈 값을 반환하는 main() 함수를 만드는 것입니다. 여기서는 파일 디스크립터 역할을 하는 정수형 변수 fd, read()가 읽어 온 데이터를 저장할 1024바이트 크기의 버퍼 buff, 그리고 읽으려는 파일의 경로와 이름을 담는 배열 path를 선언합니다.
2단계: open()으로 파일 열기
변수 선언 후 open() 함수를 호출하여 파일을 엽니다. 첫 번째 인자로 경로와 파일명이 담긴 path 배열을 전달하고, 두 번째 인자로 읽기 전용 플래그인 O_RDONLY를 지정합니다. open()은 반환값으로 파일 디스크립터를 돌려주며, 이 값을 fd에 저장해 이후 read()에서 사용합니다.
3단계: read()로 내용 읽기
파일이 열리면 read() 함수를 호출하여 내용을 읽습니다. 첫 번째 인자로 open()이 반환한 fd 디스크립터를, 두 번째 인자로 읽은 내용을 저장할 buff 버퍼의 포인터를, 마지막으로 버퍼 크기인 1024바이트를 전달합니다.
4단계: 결과 출력
마지막으로 printf() 함수를 사용해 buff에 저장된 내용을 콘솔 화면에 출력합니다. 완성된 코드는 다음과 같습니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd;
char buff[1024];
char path[] = "Documents/example.txt";
fd = open(path, O_RDONLY);
read(fd, buff, 1024);
printf("\n\n%s\n\n", buff);
}
코드를 컴파일하고 실행하면 read()가 "example.txt" 파일의 전체 내용을 buff에 담고, printf()가 그 내용을 명령 콘솔에 출력하는 것을 확인할 수 있습니다.
read() 함수 사용 시 발생하는 에러 감지 및 식별 방법
read() 함수는 다양한 에러를 일으킬 수 있으며, 에러 발생 시 반환값으로 -1을 리턴합니다.
if 조건문으로 에러 감지하기
에러 발생 여부를 확인하는 가장 간단한 방법은 if 조건문에서 반환값이 -1인지 검사하는 것입니다.
int n;
n = read(fd, buff, 1024);
if (n == -1) {
printf("파일을 읽는 도중 에러가 발생했습니다.");
}
read() 함수가 에러와 함께 반환되면 if문으로 분기하여 에러 메시지를 출력하게 됩니다.
errno 변수로 에러 코드 확인하기
에러가 발생하면 errno.h 헤더에 정의된 전역 변수 errno에 숫자 코드가 자동으로 저장됩니다. 이 코드를 활용하면 어떤 에러가 발생했는지 정확히 식별할 수 있습니다.
다음은 read() 함수가 생성할 수 있는 대표적인 에러들로, errno.h에 정의된 매크로 이름과 정숫값, 그리고 각 에러에 대한 간단한 설명입니다.
| 정의 | errno 값 | 에러 설명 |
|---|---|---|
| EAGAIN | 11 | 재시도 필요 |
| EBADF | 9 | 잘못된 파일 디스크립터 |
| EDESTADDRREQ | 89 | 목적지 주소가 지정되지 않음 |
| EFAULT | 14 | 잘못된 메모리 주소 |
| EFBIG | 27 | 파일 크기 초과 |
| EINTR | 4 | 시스템 호출이 중단됨 |
| EINVAL | 22 | 유효하지 않은 인자 |
| EIO | 5 | 입출력(I/O) 오류 |
| EPERM | 1 | 허용되지 않는 연산 |
switch문으로 에러 식별하기
에러를 식별하는 가장 손쉬운 방법은 switch문을 열어 점프 조건으로 errno 변수를 사용하고, 각 case에 에러 정의 매크로를 배치하는 것입니다.
다음 예제에서는 잘못된 디스크립터(99999999)를 전달하여 의도적으로 에러를 발생시킵니다. 앞서 살펴본 if 조건문으로 에러 발생 여부를 확인하고, switch문으로 이 함수에서 가장 흔히 발생하는 세 가지 에러를 식별합니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int fd, n;
char buff[1024];
char path[] = "Documents/example.txt";
fd = open(path, O_RDONLY);
n = read(99999999, buff, 1024);
if (n == -1) {
switch(errno) {
case EBADF:
printf("Bad file number. Error: %i\n", errno);
break;
case EINVAL:
printf("Invalid argument. Error: %i\n", errno);
break;
case EIO:
printf("I/O error. Error: %i\n", errno);
break;
}
}
}
실행 결과를 보면, 유효하지 않은 디스크립터가 입력 인자로 전달되자 read() 함수가 에러를 반환하고, errno 변수의 값을 점프 조건으로 사용하여 EBADF 케이스에 진입함으로써 "Bad file number" 에러임을 정확히 식별하는 것을 확인할 수 있습니다.
마무리
이 글에서는 C 언어에서 파일을 읽을 때 가장 널리 사용되는 함수 중 하나인 read()의 사용법을 알아보았습니다.
먼저 함수의 문법과 동작 원리, 입출력 인자 및 데이터 타입을 이론적으로 살펴본 뒤, open()과 read()를 함께 사용하여 파일을 열고 읽는 실전 예제를 코드와 함께 구현했습니다.
또한 이 함수를 사용할 때 발생할 수 있는 에러에 신속하게 대응할 수 있도록, C 언어가 제공하는 에러 감지 및 식별 방법(if 조건문과 errno, switch문 활용)을 별도 섹션으로 정리했습니다. 이 내용을 바탕으로 안정적인 파일 입출력 코드를 작성해 보시기 바랍니다.