데이터의 입출력은 지금껏 공부하면서 접했던 개념이다. 프로그램에 데이터가 들어오는 것이 입력, 데이터가 나가는 것이 출력이다. 입력 장치로는 키보드, 마우스 등이 있고 출력 장치로는 모니터, 프린터 등을 생각해볼 수 있겠따.
데이터의 흐름을 가리켜 스트림이라 한다. 입력 스트림과 출력 스트림이 있는데, 프로그램과 키보드를 연결해주는 것은 입력 스트림, 프로그램과 모니터를 연결해주는 것은 출력 스트림이다. 이는 운영체제에서 제공하며 외부 장치와 프로그램이 데이터를 송수신할 수 있도록 한다.
입출력에는 콘솔(키보드, 모니터) 입출력과 파일 입출력이 있는데, 파일과의 연결을 위한 스트림의 생성은 운영체제에 요구해야 하지만 콘솔과의 연결을 위한 스트림의 생성은 자동으로 생성된다.
콘솔 입출력을 위한 입출력 스트림은 프로그램 실행 시 자동 생성되며 종료 시 자동 소멸된다. 기본적으로 제공되는 표준 스트림(standard stream)이다.
스트림은 한 방향으로만 데이터의 전송이 이루어진다.
모니터로 하나의 문자를 출력할 때 일반적으로 사용하는 두 함수는 다음과 같다.
#include <stdio.h>
int putchar(int c);
int fputc(int c, FILE * stream);
//함수호출 성공 시 문자 정보가, 실패 시 EOF 반환
putchar 함수는 인자로 전달된 문자 정보를 표준 출력 스트림으로 전송하는 함수이다. 인자로 전달된 문자를 모니터로 출력한다.
fputc 함수는 문자를 전송할 스트림을 지정하는 것이 가능하다. stdout뿐만 아니라, 파일을 대상으로도 데이터를 전송할 수 있다. 함수의 두 번째 매개변수 stream은 문자를 출력할 스트림을 지정할 때 사용된다. 따라서 이 인자에 stdout(표준 출력 스트림)을 전달하면 putchar와 동일한 함수가 된다. 이 인자에 파일의 스트림 정보를 전달하면, 그 파일로 문자가 전달되어 저장된다.
키보드로부터 하나의 문자를 입력받을 때 일반적으로 사용하는 두 함수는 다음과 같다.
#include <stdio.h>
int getchar(void);
int fgetc(FILE * stream);
//파일의 끝에 도달하거나 함수 호출 실패 시 EOF 반환
getchar 함수는 표준 입력 스트림으로부터 하나의 문자를 입력받아 반환하는 함수이다. 키보드로부터 하나의 문자를 받는 함수이다.
fgetc 함수 또한 하나의 문자를 입력받는 함수이며, 문자를 입력받을 스트림을 지정할 수 있다.
위의 함수들에서 반환형이 int형인 것을 확인할 수 있다. 이 때 문자를 int형으로 저장하고 처리하는 이유는 EOF가 정수 -1이기 때문이다.
EOF는 End Of File의 약자로 파일의 끝을 표현하기 위해 정의한 상수이다.
파일을 대상으로 fgetc 함수가 호출되었을 때 결과로 EOF이 반환되면, 파일의 끝에 도달하여 더 이상 읽을 내용이 없다는 의미이다.
파일 입력 외에 키보드 입력 시 fgetc와 getchar 함수는 다음과 같은 경우 EOF을 반환한다.
👉🏻getchar 함수 호출 시 하나의 문자만 입력할 필요는 없다. 단어나 공백을 포함한 문장을 입력해도 된다. 문장이 입력되면 문장을 구성하는 문자의 수만큼 gerchar 함수가 호출되어 모든 문자를 읽어들인다.
지금까지 사용해온 scanf 함수는 공백이 포함된 문자열을 입력받는데에 제한이 있었다. 문자열 입력 함수는 공백이 포함된 문자열도 입력받을 수 있다.
모니터로 하나의 문자열을 출력할 때 일반적으로 사용하는 두 함수는 다음과 같다.
#include <stdio.h>
int puts(const char * s);
int fputs(const char * s, FILE * stream);
//호출 성공 시 0이아닌 값을, 실패 시 EOF 반환
두 함수 모두 첫 번째 인자로 전달되는 주소값의 문자열을 출력하지만, 출력 형태에 있어 차이를 보인다.
puts 함수는 문자열 출력 후 자동으로 개행이 이루어지지만, fputs 함수는 문자열 출력 후 자동으로 개행이 이루어지지 않아 개행하고 싶다면 \n 서식문자를 붙여줘야한다.
키보드로부터 하나의 문자열을 입력받을 때 일반적으로 사용하는 두 함수는 다음과 같다.
#include <stdio.h>
char * gets(char * s);
char * fgets(char * s, int n, FILE * stream);
//파일의 끝에 도달하거나 함수호출 실패 시 NULL 포인터 반환
gets 함수는 다음과 같이 호출한다.
문자열의 길이가 배열의 길이를 넘어서는 경우를 대비해 다음과 같이 fgets 함수를 호출하는 것이 좋다.
이렇게 fgets를 호출하게 되면 stdin으로부터 문자열을 입력받아 배열 str에 저장하되, str의 길이만큼만 저장하고 나머지는 무시된다. 이 때 널문자 /0가 저장될 공간 1은 남긴 채로 저장된다. 문자열을 입력받으면 문자열 끝에 자동으로 널문자가 추가된다.
다음 예시를 보자.
사용자는 한 번만 입력하고, fgets는 3회에 걸쳐 문자열을 읽어들이고 있다.
같은 예제의 다른 입력을 보자.
printf 함수 호출 시 인자에 개행문자 \n
가 포함되어있는데, 출력 결과를 보면 개행이 두번 이루어짐을 확인할 수 있다. fgets 함수가 문자열을 입력받을 때 우리가 입력하는 엔터 \n
을 문자열의 일부로서 받아들여 저장하기 때문이다.
지금까지 공부한 입출력 함수들을 표준 입출력 함수라 한다. ANSI C표준에서 정의된 함수이기 때문이다. 이러한 표준 입출력 함수를 통해 데이터를 입출력하는 경우, 이 데이터들은 운영체제가 제공하는 메모리 버퍼를 통과하게 된다. 메모리 버퍼란 데이터를 임시로 저장하는 메모리 공간이다.
예를 들어 fgets 함수가 문자열을 읽어들일 때, 입력버퍼에 저장된 문자열을 읽어들이는 것이다. 키보드로부터 입력받은 데이터가 입력 스트림을 거쳐 입력 버퍼로 들어가는 것은 엔터키를 누르는 순간이다. 키보드로 문자열을 입력해도 엔터키를 누르기 전에는 문자열이 읽어지지 않는다. 엔터키가 눌리기 전에는 입력 버퍼가 지워져있다.
왜 이러한 버퍼링이 필요한 것일까? 데이터 전송의 효율성을 높이기 때문이다. 외부 장치와의 데이터 입출력은 시간이 걸리므로 문자가 눌릴 때마다 이동하는 것보다 중간에 메모리 버퍼를 두고 데이터를 모아 이동하는 것이 더 빠르고 효율적이다.
출력 버퍼를 비운다는 것은 저장된 데이터가 버퍼에서부터 목적지로 이동되는 것이다. 다음 함수는 인자로 전달된 스트림의 버퍼를 비우는 함수이다.
#include <stdio.h>
int fflush(FILE * stream);
//함수호출 성공 시 0, 실패 시 EOF 반환
다음과 같이 함수를 호출하는 경우, 표준 출력 버퍼가 비워진다.
fflush(stdout);
버퍼에 저장된 내용이 비워지면서 데이터가 목적지로 이동한다.
출력 버퍼를 비운다는 것은 버퍼에 저장된 데이터가 목적지로 이동하는 것이지만, 입력 버퍼를 비운다는 것은 데이터가 소멸되는 것이다.
다음 예시를 보자.
인자로 전달된 문자열의 길이를 반환하는 함수이다.
#include <stdio.h>
size_t strlen(const char * s);
//전달된 문자열의 길이 반환. 널 문자는 길이에 포함되지 않는다.
size_t가 리턴타입으로 선언되어 있는데, 다음의 형을 가진다.
typedef unsigned int size_t;
/*이 때, 다음 두 표현은 동일하다.
size_t len;
unsigned int len;*/
다음과 같이 사용한다.
문자열의 복사는 대입 연산으로는 이루어지지 않는다. 다음은 문자열 복사에 사용되는 함수 두가지이다.
#include <stdio.h>
char * strcpy(char * dest, const char * src);
char * strncpy(char * dest, const char * src, size_t n);
//복사된 문자열의 주소값 반환
다음과 같이 호출한다.
strncpy 함수를 호출할 때 유의해야 할 점은 널 문자가 존재해야한다는 것이다. 복사할 문자열보다 복사된 문자열의 길이가 더 짧은 경우, 마지막의 널문자가 복사될 수 없다. 따라서 문자열의 끝이 어디인지 알 수 없고 잘못된 출력 결과로 이어질 수 있다. 따라서 sizeof(str)
이 아닌 sizeof(str) - 1
을 세 번째 인자로 넘겨주고, str[sizeof(str) - 1] = 0
으로 마지막에 널문자를 넣어주어야 한다.
기존의 문자열 뒤에 다른 문자열을 복사하는 함수이다.
#include <stdio.h>
char * strcat(char * dest, const char * src);
char * strncat(char * dest, const char * src, size_t n);
//덧붙여진 문자열의 주소값 반환
다음과 같이 사용한다.
strncat 함수는 붙여넣을 문자열 중 최대 n개의 문자를 덧붙이는 함수이다.
간단히 생각해서, 문자열 비교를 하려면 == 동등 연산자를 사용하면 될 것 같다. 하지만 == 연산자를 사용하면, 두 문자열의 주소값을 비교하는 것이다.
문자열의 내용을 비교하려면, 다음 함수를 사용해야 한다.
#include <stdio.h>
int strcmp(const char * s1, const char *s2);
int strncmp(const char * s1, const char *s2, size_t n);
//두 문자열의 내용이 같으면 0, 같지 않으면 0이 아닌 값 반환
두 함수 모두 인자로 전달된 두 문자열의 내용을 비교하여 결과를 반환하며, strncmp 함수는 세 번째 인자로 전달된 수의 크기만큼만 문자를 비교한다.
여기서 크고 작음은 아스키 코드를 기준으로 한다.
#include <stdlib.h>
int atoi(const char * str); //문자열의 내용을 int형으로 반환
long atol(const char * str); //문자열의 내용을 long형으로 반환
double atof(const char * str); //문자열의 내용을 double형으로 반환
다음과 같이 사용한다.
이번 챕터의 양이 많고 외워야 할 함수 위주로 이루어지다보니 쫓기듯 넘어간 감이 없지않아 있다.
하지만 코딩을 하다보니 이러한 함수들은 꼭 알아둘 필요가 있다. 따라서 다시 제대로 복습도 하고, 연습문제와 과제를 통해 익숙해지도록 할 것이다.
교재 뒤쪽으로 갈수록 느슨해지는 것은 매 학기마다 있었던 일인데, 뒷부분도 매우 중요한만큼 놓치지 말아야겠다.