C, 입출력, getchar, putchar, getch, getche, gets, puts, fgets, printf, scanf, fflush


이번 챕터의 목표


다음과 같은 프로그램을 만들 수 있다.

c_stdio_goal




문자 입출력(getchar, putchar, _getch, _getche)


getchar / putchar


먼저 입력함수입니다.

int getchar(void);

  • 매개변수가 없습니다.(void)
  • 입력된 문자 하나를 반환합니다. 에러 발생시 EOF(End of File, -1)를 반환합니다.
  • 표준입력장치(stdin)의 버퍼에서 문자 한글자를 읽어와서 반환합니다.
  • 만약 버퍼가 비어있다면 사용자로부터 입력을 받아 버퍼를 채우고, 그 중 한글자를 반환합니다.


설명된대로 사용자로부터 문자 하나를 읽고 그걸 반환하는 함수입니다.

void라는 건 전에 말했듯이 아무것도 없다는 뜻입니다.

즉, getchar()함수는 아무 파라미터도 받지 않습니다.

그리고 반환값은 int입니다. 문자를 반환한다고 했지만,

문자도 결국 아스키코드의 숫자이므로 int값을 반환합니다.

다음은 출력함수를 살펴보겠습니다.

int putchar(int c);

  • 파라미터는 출력할 문자 상수인 c입니다.
  • 그리고 그 파라미터 값을 int로 반환합니다.


그럼 간단한 예제 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    char ch = 0;
    printf("Press any char: ");
    ch = getchar();
    int a = putchar(ch);
    int b = putchar('B');
    
    printf("\n");
    
    printf("ch = %c\n", ch);
    printf("(int)ch = %d\n\n", ch);
    
    printf("a = %c\n", a);
    printf("(int)a = %d\n\n", a);
    
    printf("b = %c\n", b);
    printf("(int)b = %d\n", b);
    
    return 0;
}
/*
출력결과

Press any char: A
AB
ch = A
(int)ch = 65

a = A
(int)a = 65

b = B
(int)b = 66
*/

처음에 ch라는 변수를 생성한 후,

getchar()함수를 통해 ch에 값을 입력 받고있습니다.

저는 여기서 A 를 입력했습니다.

그랬더니 AB가 출력이 됩니다.

이건 putchar() 두번의 결과입니다.

나머지는 모두 printf()함수의 결과입니다.


그런데 이 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    char ch1 = 0;
    char ch2 = 0;
    
    printf("ch1 = %d\n", ch1);
    printf("ch2 = %d\n\n", ch2);
    
    printf("Input ch1: ");
    ch1 = getchar();
    ch2 = getchar();
    
    printf("ch1 = %d\n", ch1);
    printf("ch2 = %d\n", ch2);
    
    return 0;
}

/*
 출력결과
 
 ch1 = 0
 ch2 = 0
 
 Input ch1: A
 ch1 = 65
 ch2 = 10
 */

실행시켜보면 분명 우리는 ch1값만 입력했는데,

ch2의 값이 0에서 10이란 값으로 바뀌어 있습니다.

ASCII(아스키) 코드를 확인해보면

10이란 값은 new line이라 되어있습니다.

ch1을 입력할 때 우리는 A를 치고 엔터를 칩니다.

그럼 아까 getchar()함수 설명에 나왔듯이,

A란 값과 엔터값을 버퍼라는 임시 저장소에 저장됩니다.

getchar()는 버퍼에 있는 값을 순서대로 가져가는 것이므로,

ch1은 앞에 있는 A를, ch2는 뒤에 있는 엔터값을 가져가는 것입니다.

이렇게 버퍼를 사용하는 입출력 방식을

Buffered I/O라고 합니다.


_getch / _getche (윈도우 한정, 맥 Xcode에서 불가)


_getch와 _getche는 모두 입력함수입니다.

하지만 getchar() 함수와는 조금 다릅니다.

바로 버퍼를 사용하지 않는 Non-buffered I/O라는 것입니다.

예제를 살펴보겠습니다.

#include <stdio.h>
#include <conio.h>

int main(void)
{
    char ch = 0;
    printf("Press any key\n");
    ch = _getch();
    
    printf("The value that you pressed is '");
    putchar(ch);
    printf("'\n");
    
    return 0;
}

출력결과는 다음과 같습니다.

getch_windows_ex_result

일단 _getch()함수를 사용하려면

conio.h 라는 헤더함수를 포함해야합니다.


우리는 한 문자를 입력하고 엔터를 누르지 않아도,

바로 다음 구문이 실행되는 것을 볼 수 있습니다.

버퍼를 거치지 않고, 바로 ch에 입력되기 때문입니다.

게다가 getchar()함수는 입력값이 화면에 보이는 반면,

_getch()함수는 입력값이 화면에 보이지 않습니다.


참고로 맥의 Xcode에는 conio.h 라는 헤더 파일이 없어서

_getch()함수를 사용할 수 없습니다.




문자열 입출력


이전 챕터에서 문자열은 char의 배열로 다룬다고 하였습니다.

예를 들어

char name[30] = {“TEEMO”};

라고 했다면, name[0] = ‘T’, name[1] = ‘E’…

이런식으로 여러 char 인스턴스가 모여 문자열을 이룹니다.

그냥 name이라고 하면, 이 배열의 주소를 뜻합니다.

그리고 이 주소값을 저장하는 주소저장 전용 변수를

나중에 알아볼 포인터(Pointer)라고 합니다.


gets / puts / fgets


먼저 문자열 입력 함수에 대해 알아보겠습니다.

char *gets(char *buffer);

  • 문자열 주소(*buffer)을 매개변수로 받아
  • 그 주소에 해당하는 문자열에 입력받은 문자열을 저장합니다.
  • 정상처리되면 입력받은 매개변수의 주소를 반환하고,
  • 에러가 발생하면 NULL을 반환합니다.

변수앞에 *가 붙으면 주소값임을 의미합니다.


다음으로 문자열 출력 함수에 대해 알아보겠습니다.

int puts(const char *string);

  • 출력할 문자열의 주소값(*string)을 매개변수로 받아,
  • 해당 주소의 문자열을 출력합니다.
  • 정상처리되면 음수가 아닌 값을,
  • 에러가 발생하면 EOF를 반환합니다.


간단한 예제 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    char name[30] = {0};
    printf("Write your name: ");
    gets(name);
    
    printf("\nYour name is ");
    puts(name);
    printf(".\n");
    
    return 0;
}

/*
 Write your name: onsil
 
 Your name is onsil
 .
*/

그런데 gets()를 사용하면 보안상 취약하다 또는 unsafe하다 라고 뜹니다.

이유는 다음과 같습니다.

이전 챕터에서 말했듯이, 문자열의 끝은 NULL문자(‘\0’)입니다.

그리고 gets()함수는 문자열의 주소값만 받지, 그 문자열의 크기는 모릅니다.

그러니 gets()는 NULL문자가 나올때까지 계속 읽을 것이고,

그러다보면 문자열의 크기를 오버해서 읽게되는,

버퍼 오버플로우(buffer overflow)현상이 나타날 수 있습니다.

이 현상은 버퍼 오버런 공격(buffer overrun attack)으로 이어질 수 있어,

보안상 취약점입니다.


이런 이유로 윈도우에서는 gets_s(name, sizeof(name)) 함수 사용을 권장합니다.

두번째 인자인 sizeof(name)를 이용해 버퍼 오버플로우를 방지합니다.

하지만 이 함수는 맥과 같은 다른 OS에서는 사용할 수 없습니다.


이러저러한 이유로 결국 제일 권장되는 함수는 fgets()함수입니다.

다음 예제는 위 코드와 같지만 fgets()를 이용한 코드입니다.

#include <stdio.h>

int main(void)
{
    char name[30] = {0};
    printf("Write your name: ");
    fgets(name, sizeof(name), stdin);
    
    printf("\nYour name is ");
    puts(name);
    printf(".\n");
    
    return 0;
}

/*
 Write your name: onsil
 
 Your name is onsil
 .
*/

이번에는 별 경고메세지 없이 잘 실행되는 것을 볼 수 있습니다.

그리고, 실행결과를 잘보면 마침표가 개행되서 나옵니다.

이는 puts()함수가 자동으로 개행하기 때문입니다.




printf()


int printf(const char *format [, argument]…);

  • *format: 형식 문자열이 저장된 메모리의 주소
  • [, argument]: 형식 문자열에 대응하는 가변 매개변수들
  • 리턴값: 출력할 문자열의 개수


설명을 보면 형식 문자열이란 말이 나오는데

우리가 지금까지 써왔던 방식입니다. 예를 들어

내가 제일 좋아하는 영웅은 A이다.

이런 문장이 있을 때, A가 아이언맨이다면

내가 제일 좋아하는 영웅은 아이언맨이다.

라는 문장이 되고, A가 티모라면

내가 제일 좋아하는 영웅은 티모이다.

라는 문장이 됩니다.

teemo_question


이렇게 형식이 정해져있고, 매개변수 자리만 바뀌는 문자열을

형식 문자열이라고 합니다.

간단한 예를 들어보겠습니다.

#include <stdio.h>

int main(void)
{
    char ch = 'A';
    
    printf("ch = %c\n", ch);
    printf("ch+1 = %c\n", ch+1);
    printf("'C' = %c\n\n", 'C');
    
    printf("(int)ch = %d\n", ch);
    printf("(int)(ch+1) = %d\n", ch+1);
    printf("(int)'C' = %d\n\n", 'C');
    
    int in = 65;
    printf("in = %d\n", in);
    printf("(char)in = %c\n", in);
    
    return 0;
}

/*
 ch = A
 ch+1 = B
 'C' = C
 
 (int)ch = 65
 (int)(ch+1) = 66
 (int)'C' = 67
 
 in = 65
 (char)in = A
*/

이렇게 같은 변수를 출력한다 하더라도

형식문자에 따라 출력값이 다릅니다.

다양한 형식문자를 정리해보았습니다.


형식 문자 자료형 출력 형식
%c int(char) character, ASCII 문자로 출력
%d int Decimal. 부호가 있는 10진수로 출력
%o int Octal. 8진수로 출력
%u unsigned int Unsigned. 부호가 없는 10진수로 출력
%x, %X   Hexa. 16진수로 출력
%e, %E float, double Exponent. 지수형 소수로 출력
%f double(float) Float. 10진형 소수로 출력
%g double 지수형 소수(%e)나 10진형 소수(%f) 중 짧은 것으로 출력
%p Pointer 16진수 주소로 출력
%s String 매개변수가 가리키는 메모리의 내용을 문자열로 출력


출력값을 조심해야하는 상황도 있습니다.

다음 예제를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    printf("%d\n", 10);
    printf("%u\n", 10);
    printf("%d\n", 10U);
    printf("%u\n\n", 10U);
    
    int intMax = 2147483647;
    printf("intMax = %d\n", intMax);
    printf("intMax+1 = %d\n", intMax+1);
    printf("(unsigend)(intMax+1) = %u\n\n", intMax+1);
    
    printf("%d\n", -1);
    printf("%u\n", -1);
    
    return 0;
}

/*
 10
 10
 10
 10
 
 intMax = 2147483647
 intMax+1 = -2147483648
 (unsigend)(intMax+1) = 2147483648
 
 -1
 4294967295
*/

처음 4개의 printf는 10와 10U는 사실 똑같은 값이고,

int나 unsigned int의 범위 내에 있는 값이므로 문제가 없습니다.

그런데 intMax값을 보면 int값 중 최대값이죠.

+1을 하니 %d로 출력할 때는 int값중 최소값이 나옵니다.

이유는 살펴보면 intMax의 값은

0111 1111 1111 …. 1111

입니다. 여기다 +1을 해주면

1000 0000 0000 …. 0000

이 됩니다. 맨앞이 1이므로 음수이고, 그 중 가장 작은 값이니

int값의 최소값이 나오게 됩니다.


하지만 unsigned int 자료형은 intMax+1 값도 포함하므로 그대로 잘 나옵니다.

마지막에 -1의 출력 결과도 위의 이유와 똑같습니다.


이번엔 다른 조심해야 하는 예제를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    long long lldata = 4294967295LL;
    
    printf("%d\n", lldata);
    printf("%u\n", lldata);
    
    printf("%u\n", lldata+1);
    printf("%u\n", lldata+2);
    
    // 64비트 정보를 64비트 형식문자로 출력
    printf("%lld\n", lldata+1);
    printf("%lld\n", lldata+2);
    printf("%lld\n", lldata+3);
    
    return 0;
}

/*
 -1
 4294967295
 0
 1
 4294967296
 4294967297
 4294967298
*/

참고로 lldata의 값은 unsigned int의 최대값입니다.

첫번째, 세번째, 네번째는 형식문자값의 범위가 lldata보다 작아서

값이 돌아돌아 나오는 걸 볼 수 있습니다.

하지만 훨씬 큰 단위인 lld(long long, 64비트)로 출력하면

lldata값보다 범위가 훨씬 커서 의도한 대로 값이 잘 출력됩니다.


다음의 예제는 예제를 보고 직접 어떤 식으로 출력하는지 확인할 수 있습니다.

#include <stdio.h>

int main(void)
{
    printf("%d, %d\n", 1234, -5678);
    printf("%+d, %+d\n", 1234, -5678);
    
    printf("%8d\n", 1234);
    printf("%08d\n", 1234);
    printf("%-08d\n", 1234);
    
    return 0;
}

/*
 1234, -5678
 +1234, -5678
     1234
 00001234
 1234
*/

참고로 마지막 출력은 1234만 나온게 아니라

뒤에 빈칸이 4개가 있습니다.

마지막 형식문자인 %-08d에서 -는 왼쪽 정렬을 의미합니다.


다음은 실수 출력 예제입니다.

#include <stdio.h>

int main(void)
{
    printf("%d\n", sizeof(123.456f));
    printf("%d\n", sizeof(123.456));
    
    printf("%f\n", 123.456f);
    printf("%f\n", 123.456);
    printf("%lf\n", 123.456);
    
    return 0;
}

/*
 4
 8
 123.456001
 123.456000
 123.456000
*/

%f와 %lf는 똑같이 실수를 출력하는 형식문자입니다.

역시 float는 부정확하게 나오는 걸 볼 수 있습니다.

그 이유에 대해서는 이전 챕터에 설명되어 있습니다.


실수에서 소수점을 좀 더 자유자재로 표현하는 예제를 살펴보겠습니다.

#include <stdio.h>

int main()
{
    double dou = 123.456;
    printf("%f, %f\n", dou, -dou);
    
    printf("%.1f\n", dou);
    printf("%.2f\n", dou);
    printf("%.3f\n", dou);
    
    printf("%8d\n", 123);
    printf("%12.3f\n", dou);
    printf("%012.3f\n", dou);
    
    return 0;
}

/*
 123.456000, -123.456000
 123.5
 123.46
 123.456
 123
 123.456
 00000123.456
 */

이것도 그냥 보면 알 수 있을 것 같습니다.

참고로 소수점을 제한하는 경우엔 반올림이 적용됩니다.

그리고 하나만 설명하자면, %12.3f가 의미하는 바는

소수점 이하 3자리까지만 나타내되,

소수점 이상과 소수점 이하 모두 포함해서 12자리까지만 출력하겠다는 것입니다.




scanf / scanf_s


int scanf(const char *format [,argument]);

  • *format: 형식 문자열이 저장된 메모리의 주소
  • [, argument]: 형식 문자열에 대응하는 가변 매개변수들
  • 리턴값: 입력할 문자여의 개수(int)


scanf()함수는 printf()와 비슷하게

형식문자열을 이용하여 사용자의 입력값을 받습니다.

즉, 예를 들어 getchar()같은 경우에는 문자 하나만 받을 수 있지만,

scanf()는 모든 형식의 데이터를 받을 수 있습니다.

다음 예제는 scanf()를 이용하여 정수를 입력받는 예제입니다.

#include <stdio.h>

int main(void)
{
    int age = 0;
    printf("Write your age: ");
    scanf("%d", &age);
    
    printf("Your age is %d\n", age);
    
    return 0;
}

/*
 Write your age: 30
 Your age is 30
*/

참고로 &age는 변수 age의 주소를 뜻합니다.

하지만 만약에 숫자가 아닌 다른 데이터를 입력하면 다음과 같이 출력됩니다.

Write your age: A
Your age is 0

즉, age의 값은 변하지 않고, 그대로 0으로 출력됩니다.


그런데 scanf()를 사용하면 윈도우에서는 보안경고가 뜨면서

scanf_s()를 쓰라고 할 것입니다.

윈도우에서는 권고하는대로 쓰면 되지만,

맥같은 다른 OS에는 scanf_s()함수는 없으므로 그냥 scanf()를 사용하면 됩니다.


또한 한가지 주의할 점이 있습니다.

다음 예제를 실행하면 뭔가 이상할 것입니다.

#include <stdio.h>

int main(void)
{
    int age = 0;
    printf("나이를 입력하세요: ");
    scanf("input: %d", &age);
    
    printf("당신의 나이는 %d세 입니다.", age);
    
    return 0;
}

/*
 나이를 입력하세요: 20
 당신의 나이는 0세 입니다.
 */

20을 입력했는데 age값이 바뀌지 않았습니다.

대신 입력값을 이렇게 하면…

나이를 입력하세요: input: 30
당신의 나이는 30세 입니다.

즉, scanf()안에 들어간 형식 문자열대로 입력을 해야합니다.

주의해야합니다.


여러 값을 받을 때는 다음과 같이 받을 수도 있습니다.

#include <stdio.h>

int main(void)
{
    int x = 0, y = 0;
    
    printf("두 수를 입력받아 합을 구하는 프로그램입니다.\n");
    printf("두 수를 입력해주세요.\n->");
    
    scanf("%d%d", &x, &y);
    printf("두 수의 합은 %d입니다.\n", x+y);
    
    return 0;
}

/*
 두 수를 입력받아 합을 구하는 프로그램입니다.
 두 수를 입력해주세요.
 ->2 5
 두 수의 합은 7입니다.
 */

여기선 두 정수를 받고있는데 띄어쓰기로 구분하고 있습니다.


이번엔 3개의 다른 데이터를 입력받는 예제코드입니다.

#include <stdio.h>

int main(void)
{
    int x = 0, z = 0;
    char y = 0;
    
    printf("세 데이터 입력: ");
    scanf("%d%c%d", &x, &y, &z);
    
    printf("x = %d, y = %c, z = %d\n", x, y, z);
    
    return 0;
}

/*
 세 데이터 입력: 1A2
 x = 1, y = A, z = 2
 */

이렇게 다른 종류의 데이터를 받을땐

입력값이 붙어있어도 됩니다.


이번엔 문자열을 받아보겠습니다.

#include <stdio.h>

int main(void)
{
    char string1[30] = {0};
    char string2[30] = {0};
    
    scanf("%s%s", string1, string2);
    printf("%s %s\n", string1, string2);
    
    return 0;
}

/*
 cute teemo
 cute teemo
 */

문자열은 같은 %s를 두번받는거니 띄어쓰기로 구분해야합니다.




Back to the goal


이제 지금까지 살펴본 내용으로

이번 챕터의 목표를 코딩해봅니다.

c_stdio_target

혹여나 이렇게 코딩을 했다면 잘못됐습니다…

#include <stdio.h>

int main(void)
{
    int age = 0;
    char name[30] = {0};
    
    printf("나이를 입력하세요: ");
    scanf("%d", &age);
    
    printf("이름을 입력하세요: ");
    gets(name);
    
    printf("당신은 %d세, '%s'입니다. \n", age, name);
    
    return 0;
}

/*
 나이를 입력하세요: 30
 이름을 입력하세요:
 당신은 30세, ''입니다.
 */

나이 입력하고 엔터만 치면 그냥 프로그램이 종료됩니다.

이 이유는 getchar()부분에서 설명했던 buffer 때문입니다.

나이를 입력하고 엔터를 치면

버퍼에는 30이라는 값과 엔터값이 입력되고,

그 값 중 30은 age가 가져가고,

버퍼에 남은 값은 엔터값은 name이 바로 가져가게 됩니다.

즉, 버퍼에 있는 엔터값을 없애고 이름 입력을 받아야 합니다.


이런 방법으로 접근할 수 있습니다.

#include <stdio.h>

int main(void)
{
    int age = 0;
    char name[30] = {0};
    
    printf("나이을 입력하세요: ");
    scanf("%d", &age);
    getchar();
    
    printf("이름을 입력하세요: ");
    fgets(name, sizeof(name), stdin);
    name[strlen(name)-1] = '\0';
    
    printf("당신의 나이는 %d살이고 이름은 '%s'입니다.\n", age, name);
}

/*
 나이을 입력하세요: 30
 이름을 입력하세요: onsil
 당신의 나이는 30살이고 이름은 'onsil'입니다.
 */

여기선 엔터값을 없애기 위해, 나이를 입력받고

getchar() 구문을 추가했습니다.

name[strlen(name)-1] = ‘\0’; 이 구문을 설명하자면

fgets()에서 입력받을 때 엔터값까지 name에 저장되었기 때문에,

마지막 값을 엔터값에서 NULL값으로 바꿔주는 것입니다.


다음과 같은 방법으로도 엔터값을 없앨 수 있습니다.

 #include <stdio.h>

int main(void)
{
    int age = 0;
    char name[30] = {0};
    
    printf("나이를 입력하세요: ");
    scanf("%d%*c", &age);
    
    printf("이름을 입력하세요: ");
    gets(name);
    
    printf("당신의 나이는 %d세이고 이름은 '%s'입니다. \n", age, name);
    
    return 0;
}

/*
 나이를 입력하세요: 30
 이름을 입력하세요: onsil
 당신의 나이는 30세이고 이름은 'onsil'입니다.
 */

여기서 엔터키를 없앤 방법은

scanf(“%d%*c”, &age);

에서 %*c 입니다. 이렇게 형식문자 앞에 *을 두면 입력값을 그냥 버린다는 뜻입니다.


제 책에도 그렇고 간혹 예전에 출간된 책에는

버퍼값을 날리기 위해 fflush()라는 함수를 사용하라고 써져있을 수 있습니다.

그런데, fflush()는 예전부터 윈도우에만 있던 함수였고,

그마저도 Visual Studio 2015 부터 삭제되었다 합니다.