
1. 스트림과 데이터의 이동
데이터의 입력과 출력은 프로그램의 흐름을 뜻하는 것이다.
그렇다면 무엇이 '입력'이고 무엇이 '출력'일까?
프로그램을 중심으로 프로그램 안으로 데이터가 흘러 들어오는 것이 입력이고, 프로그램 밖으로 데이터가 흘러 나가는 것이 출력이다.
가장 대표적인 입력장치로는 키보드가 있으며, 파일도 입력의 대상이 될 수 있다.
그리고 대표적인 출력장치로는 모니터가 있으며, 파일 역시 출력의 대상이 될 수 있다.
우리들이 구현하는 프로그램과 모니터, 그리고 프로그램과 키보드는 기본적으로 연결되어 있는 개체가 아닌, 서로 떨어져 있는 개체이다.
따라서 프로그램상에서 모니터와 키보드를 대상으로 데이터를 입출력 하기 위해서는 이들을 연결시켜 주는 다리가 필요하다.
그리고 이러한 다리의 역할을 하는 매개체를 가리켜 '스트림'이라 한다.

위의 그림을 보면, 실행중인 프로그램과 모니터를 연결해주는 '출력 스트림'이라는 다리가 놓여있고, 실행중인 프로그램과 키보드를 연결해주는 '입력 스트림'이라는 다리가 놓여있음을 알 수 있다.
printf, scanf 함수를 통해서 데이터를 입출력 할 수 있는 근본적인 이유는 바로 이 다리에 있다.
그렇다면 다리의 역할을 하는 스트림의 정체는 무엇일까?
이는 운영체제에서 제공하는 소프트웨어적인(소프트웨어로 구현되어 있는) 가상의 다리이다.
다시 말해서, 운영체제는 외부장치와 프로그램과의 데이터 송수신의 도구가 되는 스트림을 제공하고 있다.
콘솔(일반적으로 키보드와 모니터를 의미함) 입출력과 파일 입출력 사이에는 차이점이 하나 있다.
그것은 파일과의 연결을 위한 스트림의 생성은 우리가 직접 요구해야 하지만, 콘솔과의 연결을 위한 스트림의 생성은 요구할 필요가 없다는 것이다.
다시 말해서, 콘솔 입출력을 위한 스트림은 자동으로 생성이 된다.
정리하면, 콘솔 입출력을 위한 '입력 스트림'과 '출력 스트림'은 프로그램이 실행되면 자동으로 생성되고, 프로그램이 종료되면 자동으로 소멸하는 스트림이다.
즉, 이 둘은 기본적으로 제공되는 '표준 스트림(standard stream)'이다.
그리고 표준 스트림에는 '에러 스트림'도 존재하며 이들 각각에는 다음과 같이 stdin, stdout, stderr라는 이름이 붙어있다.

스트림은 '한 방향으로 흐르는 데이터의 흐름'을 뜻한다.
즉, 스트림이란 단어에는 단 방향으로만 데이터의 전송이 이뤄진다는 뜻이 담겨있다.
실제로 입출력 스트림도 입력 스트림과 출력 스트림이 구분되어서 한 방향으로만 데이터의 흐름을 유지하고 있다.
2. 문자 단위 입출력 함수
#include <stdio.h>
int putchar(int c);
int fputc(int c, FILE * stream);
putchar 함수는 인자로 전달된 문자정보를 stdout으로 표현되는 표준 출력 스트림으로 전송하는 함수이다.
따라서 인자로 전달된 문자를 모니터로 출력하는 함수라 할 수 있다.
그리고 문자를 전송한다는 측면에서는 fputc 함수도 putchar 함수와 동일하다.
단, fputc 함수는 문자를 전송할 스트림을 지정할 수 있다.
즉, fputc 함수를 이용하면 stdout뿐만 아니라, 파일을 대상으로도 데이터를 전송할 수 있다.
그리고 fputc 함수에 대해서 부연설명을 하자면, fputc 함수의 두 번째 매개변수 stream은 문자를 출력할 스트림의 지정에 사용된다.
따라서 이 인자에 표준 출력 스트림을 의미하는 stdout을 전달하면, putchar 함수와 동일한 함수가 된다.
그리고 위의 두 함수 모두 오류가 발생해서 정상적인 결과를 보장하지 못할 경우 EOF를 반환하는데, EOF가 무엇을 의미하는지는 잠시 후에 설명하겠다.
#include <stdio.h>
int getchar(void);
int fgetc(FILE * stream);
getchar 함수는 stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력 받아서 반환하는 함수이다.
따라서 키보드로부터 하나의 문자를 입력 받는 함수라 할 수 있다.
그리고 fgetc 함수도 하나의 문자를 입력 받는 함수이다.
다만 getchar 함수와 달리 문자를 입력 받을 스트림을 지정할 수 있다.
#include <stdio.h>
int main(void)
{
int ch1, ch2;
ch1=getchar(); // 문자 입력
ch2=fgetc(stdin); // 엔터 입력
putchar(ch1); // 문자 출력
fputc(ch2, stdout); // 엔터 출력
return 0;
}
소스코드상에서는 분명 두 개의 문자를 입출력하고 있다.
그런데 실행결과만 놓고 보면, 하나의 문자가 입력되고 출력된 것으로 보인다.
그러나 실제로는 두 개의 문자가 입력되고 출력되었다.
다만 두 번째 문자가 '엔터 키'이다 보니 눈에 띄질 않았을 뿐이다.
사실 '엔터 키'도 아스키 코드 값이 10인 '\n'으로 표현되는 문자이다.
따라서 입출력의 대상이 되는 것은 당연하다.
"그런데 위 코드에서 문자를 int형 변수에 저장하는 이유는 무엇이죠?"
언뜻 생각하기에 문자를 저장하는 두 변수 ch1과 ch2는 char형으로 선언되어야 할 것 같다.
하지만 int형으로 선언해야 한다.
앞서 보인 함수의 원형에서도 알 수 있듯이 getchar 함수와 fgetc 함수의 반환형이 int이기 때문이다.
그래도 여전히 궁금중이 풀리지 않는다.
이제는 getchar 함수와 fgetc 함수의 반환형이 int인 이유도 이해가 가지 않는다.
이와 관련해서는 밑에서 EOF를 설명한 다음에 이야기하겠다.
EOF는 End Of File의 약자로서, 파일의 끝을 표현하기 위해서 정의해 놓은 상수이다.
따라서 파일을 대상으로 fgetc 함수가 호출되면, 그리고 그 결과로 EOF가 반환되면, 이는 '파일의 끝에 도달해서 더 이상 읽을 내용이 없다'는 뜻이 된다.
그렇다면 키보드를 대상으로 하는 fgetc 함수와 getchar 함수는 언제 EOF를 반환할까?
이는 다음 두 가지 경우 중 하나가 만족되었을 때이다.
- 함수호출의 실패
- Windows에서 Ctrl+Z 키, Linux에서 Ctrl+D 키가 입력되는 경우
그런데 키보드의 입력에 '파일의 끝'이라는 것이 존재할 수 있겠는가?
따라서 EOF의 반환시기를 Ctrl+Z 또는 Ctrl+D 키의 입력으로 별도로 약속해 놓았다.
int ch;
while(1)
{
ch=getchar();
if(ch==EOF)
break;
putchar(ch);
}
참고로 위 코드에서는 getchar 함수가 호출된다고 해서 하나의 문자만 입력하려고 노력하지 않아도 된다.
문자가 아닌 공백을 포함하는 문장을 입력해도 된다.
문장이 입력되면 문장을 구성하는 문자의 수만큼 getchar 함수가 호출되면서 모든 문자를 읽어 들이니 말이다.
앞서 소개한 getchar 함수와 fgetc 함수를 다시 한번 관찰하자
int getchar(void);
int fgetc(FILE * stream);
반환되는 것은 1바이트 크기의 문자인데, 반환형이 int이다.
이유가 무엇일까?
char형을 unsigned char로 처리하는 컴파일러도 존재하기 때문이다.
그런데 위의 두 함수가 반환하는 값 중 하나인 EOF는 -1로 정의된 상수이다.
따라서 반환형이 char형이라면, 그리고 char를 unsigned char로 처리하는 컴파일러에 의해서 컴파일이 되었다면, EOF는 반환의 과정에서 엉뚱한 양의 정수로 형 변환이 되어버리고 만다.
그래서 어떠한 상황에서도 -1을 인식할 수 있는 int형으로 반환형을 정의해 놓은 것이다.
물론 반환되는 값을 그대로 유지하기 위해서 우리도 int형 변수에 반환 값을 저장해야 한다.
'printf'와 'scanf' 함수가 있는데 문자 단위 입출력 함수를 제공하는 이유는,
printf와 scanf 함수가 차지하는 메모리 공간도 크고, 해야할 연산의 양도 많아서 상대적으로 속도가 느리기 때문이다.
뿐만 아니라 별도의 서식지정을 해야 하니 문장을 구성하는 것도 번거로운 편이다.
따라서 단순히 문자 하나를 입출력 하는 것이 목적이라면 문자 단위 입출력 함수를 사용하는 것이 낫다.
3. 문자열 단위 입출력 함수
#include <stdio.h>
int puts(const char * s);
int fputs(const char * s, FILE * stream);
puts 함수는 출력의 대상이 stdout으로 결정되어 있지만, fputs 함수는 두 번째 인자를 통해서 출력의 대상을 결정할 수 있다.
그리고 둘 다 첫 번째 인자로 전달되는 주소값의 문자열을 출력하지만, 출력의 형태에 있어 한가지 차이점이 있다.
어떠한 차이점이 있는지 다음 예제를 통해서 확인하기로 하자.
#include <stdio.h>
int main(void)
{
char * str = "Simple String";
puts(str);
puts("So Simple String");
// Simple String
// So Simple String 출력
fputs(str, stdout);
fputs("So Simple String", stdout);
// Simple StringSo Simple String 출력
return 0;
}
소스코드의 실행결과를 분석하였다면 다음 사실을 알 수 있다.
puts 함수가 호출되면 문자열 출력 후 자동으로 개행이 이뤄지지만,
fputs 함수가 호출되면 문자열 출력 후 자동으로 개행이 이뤄지지 않는다.
#include <stdio.h>
char * gets(char * s)
char * fgets(char * s, int n, FILE * stream);
위의 gets 함수는 다음의 유형으로 호출한다.
char str[7];
gets(str);
위의 문장구성만으로도 키보드로부터 문자열을 입력 받게 되니, 확실히 간단하다.
하지만 미리 마련해놓은 배열의 크기를 넘어서는 길이의 문자열이 입력되면, 할당 받지 않은 메모리 공간을 침범하여 실행 중 오류가 발생한다는 단점이 있다.
그래서 가급적이면 fgets 함수를 호출하는 것이 좋다.
char str[7];
fgets(str, sizeof(str), stdin);
위의 fgets 함수호출이 의미하는 바는 다음과 같다.
"stdin으로부터 문자열을 입력 받아서 배열 str에 저장하되, sizeof(str)의 길이만큼만 저장"
예를 들어서 사용자가 "123456789"를 입력하면, sizeof(str)의 반환 값인 7보다 하나 작은 6에 해당하는 길이의 문자열만 읽어서 str에 저장하게 된다.
즉, str에는 "123456"이 저장된다.
왜 하나가 작은 길이의 문자열이 저장될까?
이는 널(null) 문자의 저장을 위한 것이다.
아무리 공간이 부족해도 널 문자가 삽입되지 않으면 문자열이 아니기 때문이다.
그래서 문자열을 입력 받으면 문자열의 끝에 자동으로 널 문자가 추가된다.
또한 fgets 함수는 \n을 만날 때까지 문자열을 읽어 들이는데, \n을 제외시키거나 버리지 않고 문자열의 일부로 받아들인다.
따라서 sizeof(str)보다 적은 문자를 입력하고 엔터를 치면, 엔터도 str 안에 들어가 문자열을 이룬다.
엔터(\n)를 만날 때까지 읽어들이는 특성 덕분에, 중간에 삽입되는 공백문자도 문자열의 일부로 읽어들인다.
4. 표준 입출력과 버퍼
우리가 지금까지 공부해 온 입출력 합수들을 가리켜 '표준 입출력 함수'라 한다.
그런데 이러한 표준 입출력 함수를 통해서 데이터를 입출력 하는 경우, 해당 데이터들은 운영체제가 제공하는 '메모리 버퍼'를 중간에 통과하게 된다.
여기서 말하는 '메모리 버퍼'는 데이터를 임시로 모아두는 메모리 공간이다.
키보드를 통해 입력되는 데이터는, 일단 입력버퍼에 저장된 다음에(버퍼링 된 다음에) 프로그램에서 읽혀지는 것을 알 수 있다.
즉, fgets 함수가 읽어 들이는 문자열은 입력버퍼에 저장된 문자열이다.
그럼 키보드로부터 입력된 데이터가 입력 스트림을 거쳐서 입력버퍼로 들어가는 시점은 언제일까?
이는 엔터 키가 눌리는 시점이다.
그래서 키보드로 아무리 문자열을 입력해도 엔터 키가 눌리기 전에는 fgets 함수가 문자열을 읽어 들이지 못하는 것이다.
엔터 키가 눌리기 전에는 입력버퍼가 비워져 있으니 말이다.
버퍼링을 하는 이유는 비유를 통해 이해하면 간단하다.
창고에 물건을 나르는 경우, 손으로 하나씩 나르는 것보다 손수레에 가득 채워서 나르는 것이 보다 빠르고 효율적이라는 것을 알고 있을 것이다.
입출력의 과정에서도 마찬가지로 버퍼링을 하면 보다 빠르고 효율적이기 때문에 데이터를 목적지에 바로 전송하지 않고 중간에 출력버퍼와 입력버퍼를 두는 것이다.
char perID[7];
char name[10];
fputs("주민번호 앞 6자리 입력: ", stdout);
fgets(perID, sizeof(perID), stdin);
fputs("이름 입력: ", stdout);
fgets(name, sizeof(name), stdin);
printf("주민번호 앞 6자리: %s", perID);
printf("이름: %s", name);
위의 코드를 실행하고 주민번호 6자리를 입력하면, 입력한 주민번호 6자리와 엔터까지가 입력 버퍼에 들어간다.
그리고 fgets 함수가 입력 버퍼에서 널 문자를 제외하고 6개를 읽는데, 이 때 엔터는 여전히 입력 버퍼에 남아있게 된다.
따라서 다음 fgets 함수는 자연스레 입력 버퍼에 남아있는 엔터를 읽게 되고 원하는 출력이 나오지 않는다.
이럴 때는 fgets 함수 이후에 입력 버퍼를 비워주면 된다.
void ClearBuffer(void)
{
while(getchar()!='\n');
}
입력 버퍼에 저장된 문자들은 그냥 읽어 들이기만 하면 지워진다.
그래서 \n을 만날 때까지 문자를 읽어 들이는 함수를 정의하였다.
물론 읽어 들인 문자를 저장하거나 따로 사용하지는 않는다.
버리는 것이 목적이니 말이다.
그럼 이 함수를 추가하여 다시 코드를 작성해보겠다.
#include <stdio.h>
void ClearBuffer(void)
{
while(getchar()!='\n');
}
int main(void)
{
char perID[7];
char name[10];
fputs("주민번호 앞 6자리 입력: ", stdout);
fgets(perID, sizeof(perID), stdin);
ClearBuffer();
fputs("이름 입력: ", stdout);
fgets(name, sizeof(name), stdin);
printf("주민번호 앞 6자리: %s", perID);
printf("이름: %s", name);
return 0;
}
이제 첫 번째 입력 때 주민번호 6자리 뒤에 어떤 문자가 와도 fget 함수가 6자리만 읽고 입력 버퍼에 남은 문자들은 ClearBuffer 함수가 읽어 들여 지워진다.
위의 방법 말고도 fflush 함수를 사용하는 방법도 있다.
이 함수는 stdin이나 stdout, 즉 표준 입력 버퍼나 표준 출력 버퍼를 비워주는 함수이다.
사용 방법은 다음과 같다.
fputs("주민번호 앞 6자리 입력: ", stdout);
fgets(perID, sizeof(perID), stdin);
fflush(stdin)
하지만 Windows 계열의 컴파일러를 제외하고 다른 컴파일러들은 다른 결과를 보일 수도 있기 때문에, 내 소스코드가 다른 컴파일러에서 컴파일 될 수도 있다는 점을 생각하면 입력 버퍼를 비우는 함수로 만들어 놓는 편이 좋다.
5. 입출력 이외의 문자열 관련 함수
다음 함수는 인자로 전달된 문자열의 길이를 반환하는 함수로서 문자열과 관련해서 많이 사용되는 대표적인 함수이다.
#include <string.h>
size_t strlen(const char* s);
위 함수의 반환형 size_t는 일반적으로 다음과 같이 선언되어 있다.
typedef unsigned int size_t
아직 우리는 typedef 선언을 공부하지 않아서 위의 문장이 의미하는 바를 알지 못한다.
그러니 당분간은 위의 문장이 의미하는 바가 다음과 같다고만 기억을 하자.
"unsigned int의 선언을 size_t로 대신할 수 있습니다!"
즉, 위의 typedef 선언으로 인해서 size_t가 unsigned int를 대신할 수 있게 된 것이다.
따라서 다음 두 선언은 완전히 동일하다.
size_t len;
unsigned int len;
그럼 이어서 strlen 함수의 호출방법을 보이겠다.
#include <stdio.h>
#include <string.h>
void RemoveBSN(char* str)
{
int len=strlen(str);
str[len-1]=0;
}
int main(void)
{
char str[100];
fgets(str, sizeof(str), stdin); // Good morning 입력
printf("길이: %d, 내용: %s\n", strlen(str), str); // 13, Good morning 출력
RemoveBSN(str);
printf("길이: %d, 내용: %s\n", strlen(str), str); // 12, Good morning 출력
return 0;
}
strlen 함수는 전달된 문자열의 길이를 반환하되, 널 문자는 길이에 포함하지 않는다.
위 코드는 fgets 함수를 통해 문자열을 str에 저장하고, 끝에 저장되어 있는 엔터키를 없애고 싶은 경우 사용하는 함수이다.
이번에는 문자열의 복사에 사용되는 함수 둘을 소개하겠다.
#include <string.h>
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t n);
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[20]="1234567890";
char str2[20];
char str3[5];
strcpy(str2, str1);
puts(str2); // 1234567890 출력
strncpy(str3, str1, sizeof(str3));
puts(str3); // 이상한 숫자 출력
strncpy(str3, str1, sizeof(str3)-1);
str3[sizeof(str3)-1]=0;
puts(str3); // 1234 출력
return 0;
}
위의 코드 에는 다음 문장이 삽입되어 있다.
strncpy(str3, str1, sizeof(str3));
이 문장은 문제가 있다.
str3의 크기가 5인데 sizeof(str3)만큼 복사를 하면 5개가 그대로 복사가 되고 널문자가 존재할 공간은 없어진다.
따라서 strncpy 함수를 사용할 때 크기가 보다 작은 배열로 문자열을 복사할 때는 배열의 실제길이보다 하나 작은 값을 전달해서 널 문자가 삽입될 공간을 남겨두고 복사를 진행해야 한다.
그리고 이어서 배열의 끝에 널 문자를 삽입해야 한다.
이번에 소개하는 두 함수는 문자열의 뒤에 다른 문자열을 복사하는 기능을 제공한다.
간단히 다음과 같은 요구사항을 만족시키는 함수로 이해하면 된다.
"str1에 저장된 문자열의 뒤에 str2에 저장된 문자열을 좀 복사해줘"
#include <string.h>
char* strcat(char* dest, const char* src);
char* strncat(char* dest, const char* src, size_t n);
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[20]="First~";
char str2[20]="Second";
char str3[20]="Simple num: ";
char str4[20]="1234567890";
strcat(str1, str2);
puts(str1);
strncat(str3, str4, 7);
puts(str3);
return 0;
}
덧붙임이 시작되는 위치는 널 문자 다음이 아닌, 널 문자가 저장된 위치에서부터이다.
널 문자가 저장된 위치에서부터 복사가 진행되어야 덧붙임 이후에도 문자열의 끝에 하나의 널 문자만 존재하는 정상적인 문자열이 되지 않겠는가!
이어서 다음 문장을 보자.
strncat(str3, str4, 7);
이 문장이 의미하는 바는 다음과 같다.
"str4의 문자열 중 최대 7개를 str3의 뒤에 덧붙여라!"
즉 str4의 길이가 7을 넘어선다면 7개의 문자까지만 str3에 덧붙이라는 의미인데, 이 7개의 문자에는 널 문자가 포함되지 않는다는 사실에 주목하자.
따라서 널 문자를 포함하여 실제로는 총 8개의 문자가 str2에 덧붙여진다.
이렇듯 strncpy 함수와 달리 strncat 함수는 문자열의 끝에 널 문자를 자동으로 삽입해준다.
#include <string.h>
char* strcmp(const char* s1, const char* s2);
char* strncmp(const char* s1, const char* s2, size_t n);
위의 두 함수 모두 인자로 전달된 두 문자열의 내용을 비교하여 다음의 결과를 반환한다.
· s1이 더 크면 0보다 큰 값 반환
· s2가 더 크면 0보다 작은 값 반환
· s1과 s2의 내용이 모두 같으면 0
단, strncmp 함수는 세 번째 인자로 전달된 수의 크기만큼만 문자를 비교한다.
즉 strncmp 함수를 호출하면 앞에서부터 시작해서 중간부분까지 부분적으로만 문자열을 비교할 수 있다.
위에서 말하는 문자열의 크고 작음은 아스키 코드 값을 기준으로 결정된다.
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[20];
char str2[20];
scanf("%s", str1);
scanf("%s", str2);
if(!strcmp(str1, str2))
{
puts("동일합니다.");
}
else
{
puts("동일하지 않습니다.");
if(!strncmp(str1, str2, 3))
puts("그러나 앞 세 글자는 동일합니다.");
}
return 0;
}
str1과 str2가 동일하면 거짓을 의미하는 0이 반환된다.
그런데 이 반환 값을 대상으로 ! 연산을 하였으니 거짓이 참으로 바뀐다.
즉 위 if문은 str1과 str2의 문자열이 완벽히 동일할 때 참이 된다.
이제 마지막으로, 알아 두면 도움이 될 함수 몇개만 추가로 설명하면서 정리하고자 한다.
다음은 헤더파일 <stdlib.h>에 선언된 함수들이다.
#include <stdlib.h>
int atoi(const char* str);
long atol(const char* str);
double atof(const char* str);
위 함수들은 문자열로 표현된 정수나 실수의 값을 해당 정수나 실수의 데이터로 변환해야 하는 경우에 사용한다.
예를 들어 문자열 "123"을 정수 123으로 변환하거나 문자열 "7.15"를 실수 7.15로 변환해야 하는 경우에 사용할 수 있다.
다만, 문자열에서 숫자가 나오다가 문자가 다시 나오면 거기까지만 변환한다.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char str[20];
printf("정수 입력: ");
scanf("%s", str);
printf("%d \n", atoi(str)); // 정수 출력
printf("실수 입력: ");
scanf("%s", str);
printf("%g \n", atof(str)); // 실수 출력
return 0;
}