3장 표준 입출력 도구

우리는 컴퓨터 와 상호작용을 하기 위해서 입력과 출력을 한다. 보통 키보드를 통해 입력을하고 모니터를 통해 출력한다. 사용자가 의도한 입력 정보가 정확하게 전달되기때문에 우리는 키보드를 여전히 사용한다. 최신 태블릿PC 도 마찬가지로 터치 방식의 키보드보다 물리 키보드가 정확하다. 여전히 키보드는 중요한 역할을 하는것 같다.

문자 입출력

영문 한 글자를 입출력하는 데 사용하는 자료형은 char형이다. 내부적으로 8비트 정수 형식이다. 그렇기 때문에 글자에 숫자를 더하거나 빼는 연산도 가능하다.

getchar() / putchar() 함수

int getchar(void);
전달인자 : 없음
반환값 : 입력 문자 하나, 에러가 발생하면 -1 반환
설명 : 표준입력장치로부터 문자 한 글자를 읽어 와서 반환.

int putchar(int c);
전달인자 : 출력할 문자 상수
반환값 : 출력 문자 하나, 에러가 발생하면 -1 반환
설명 : 표준출력장치인 콘솔에 영문 한 글자를 출력.

getchar() 함수는 문자 하나를 입력받기 위한 표준 입력 함수이다. 전달인자는 없고 반환값은 int 형 임을 알수있다. char 형이 아니다.

putchar() 함수는 전달인자로 정수형 변수 하나를 주어야 하며 호출 결과로 반환값으로 int 형 자료를 얻을수 있다.

#include <stdio.h>

int main(void){
  char ch1 = 0; //char형 변수 ch1 선언 및 정의

  ch1 = getchar(); //getchar함수를 통해 문자 한개를 입력받고 반환한 값을 ch1 저장.
  putchar(ch1); //ch1 에 저장된 문자 한개를 출력.
  putchar('Z'); // 'Z' 문자를 한개 출력.
  
  return 1; 
}

getchar(), putchar() 예시코드

실행 결과
J
JZ%    

실행 결과

위 코드는 char 형 변수 ch1 을 선언하고 0으로 초기화 하고,

getchar 함수를 통해 문자 하나를 입력받고 입력받은 문자 한개를 ch1 변수에 복사(저장) 하고 putchar 함수에 전달인자로 ch1 을 전달하여 ch1 에 있는 J 문자를 하나 를 출력한것이다. 또하나 중요한 점은 기존 0으로 초기화 되었던 ch1 변수에 '='을 통해 오버라이팅 되었다는 것이다.

putchar(ch1) 은 매개변수를 변수로 정의 한것이고 putchar('Z') 는 상수로 정의한것이다. 변수로 매개변수를 정의할 경우 변수 자체가 아니라 변수 안에 담긴 정보가 매개변수로 전달 된다는점이 중요하다.

_getch() / _getche() 함수

_getch() 와 _getche() 함수는 getchar() 함수와 사용방법과 기본 기능은 동일하다. 하지만 내부 동작 로직 자체는 다르다.

내부적으로 getchar()함수와 크게 다른 점은 Non-Buffered I/O 를 한다는 점이다. 풀어서 말하면 사용자가 입력한 정보가 버퍼를 거치지 않고 즉시 전달된다는것이다.

getchar() 함수는 사용자가 입력한 문자가 어떤 것인지 정보 자체를 다루는 목적이 강하다면 _getch(), _getche() 함수는 "아무 키나 눌러서 사용자 입력이 발생했음"을 감지하려는 목적이 더 강하다.

어떤 키라도 하나만 입력하면 함수는 입력한 정보를 즉시 반환한다.

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

int main(void){
  char  ch = 0;
  printf("아무 키나 입력하면 넘어가요\n");

  ch = _getch();

  printf("입력한 키는 ");
  putchar(ch);
  printf(" 입니다. \n");
  
  return 1;
}

_getch() 예제


아무 키나 입력하면 넘어가요 <- Z 입력.
입력한 키는 Z입니다.

실행결과

아무 키나 입력하면 넘어가요 라는 메세지를 출력하고 프로그램은 일시 정지상태가 된다. 그리고 아무런 키를 입력하면 다음 흐름으로 넘어가는것을 확인할수있다.

입력 받은 문자가 무엇인지 출력해주는것은 동일하나 글자를 입력하는 과정에서 Enter 키를 누를 필요가 없다. 어떠한 문자가 입력되는 즉시 _getch 함수가 반환해버리기 때문이다.

문자열 입출력

문자열은 정확히 말해서 '문자의 배열'이라는 것이다. 문자들이 모여 묶인 집합체의 의미이다. 중요한 사실은 문자를 다루는 것과 문자열을 다루는 것의 차이는 한 인스턴스(char)를 다루는 것과 여러 인스턴스(char[n]) 를 묶어서 한꺼번에 다루는 것의 차이 라는 것이다.

배열의 이름은 일반 변수의 이름과 달리 메모리에 주소에 부여한 식별자 이다. 예를 들어 szName[16]; 라는 선언은 배열을 이루는 각 요소의 자료형이 char 형이고 개수가 16개 인 배열을 말한다. 그 배열을 대표하는 주소에 szName 이라는 이름(식별자) 를 부여한 것이다.

배열의 이름은 주소이다. 이 주소를 저장하기 위한 전용 변수가 바로 포인터 이다. 그렇기에 배열이 나오면 포인터가 따라 나올수밖에 없다.

주소는 메모리의 위치를 표시하는 정보이고, 주소를 저장하기 위한 전용변수가 포인터 이고 배열의 이름은 주소이며 배열은 문자형이 여러개 모인 집합체이고 배열의 이름은 포인터에 담을 수 있다.

gets() / puts() 함수

char *gets(char *buffer);
전달인자 : buffer 입력받은 문자열을 저장할 메모리의 주소
반환값 : 정상적이면 인자로 전달받은 메모리의 주소 에러시 NULL 반환
설명 : 표준입력장치에서 문자열을 입력받는 함수.

int puts(const char *string);
전달인자 : 출력할 문자열이 저장된 메모리의 주소
반환값 : 정상적이면 양수 반환 에러시 EOF 를 반환
설명 : 표준출력장치에 문자열을 출력하는 함수.

gets 함수와 puts 함수는 각각 입력과 출력을 해주는 함수이다. getchar 함수와 putchar 함수와 다르게 매개변수 전달인자로 포인터를 준다는 것이다. 그러므로 gets(), puts() 함수를 호출하려면 메모리의 주소를 전달인자로 주어야한다는것을 예상해볼수있다.

gets() 함수는 호출 과정에서 전달인자로 문자열이 저장될 '메모리의 주소'를 전달 받는데, 그 주소의 메모리에 문자열을 배달해주기 때문이다. 예시로 물건을 택배로 주문하면 기사님은 우리집 주소를 알아야 배달해줄것이다. 그리고 puts() 함수는 출력할 문자열이 저장된 메모리의 주소에 접근하여 문자열을 출력해준다. 예시로 물건을 환불하려고 기사님한테 와달라고 요청하면 우리집 으로 와주는 택배 기사님 같다.

#include <stdio.h>

int main(void){
  char szName[16] = { 0 }; //char 형 변수 16개가 한 덩어리인 배열 선언 및 정의

  printf("이름을 입력하세요: ");
  gets(szName); // 사용자가 입력한 문자들을 문자 배열에 저장.

  printf("이름은: ");
  puts(szName); // 문자 배열에 저장된 값을 불러와 출력.
  printf(" 입니다.\n");
  return 1;
}
실행결과

이름을 입력하세요: 수상한
이름은 : 수상한 
입니다.

szName[16]은 char형 변수 16개를 선언한다는 의미이고 문자배열에 0으로 초기화한다는것이다. 이후 gets() 함수의 전달인자로 szName 인 배열의 주소를 주어 입력 받은 문자열을 szName 배열에 저장하고, 이후 배열의 이름을 puts() 함수에게 주어 szName주소를 찾아가 저장된 문자열을 출력하는것이다.

gets() 함수와 보안 결함

char *gets_s(char *buff, size_t sizeInCharacters);
전달인자 : 입력받은 문자열을 저장할 메모리의 주소, 문자열이 저장될 메모리의 바이트 단위 크기.
반환값 : 정상적이면 인자로 전달받은 메모리의 주소.
설명 : 표준입력장치에서 문자열을 입력받는 함수.

위 코드를 컴파일하면 보안 경고 메세지가 나온다. 보안적으로 안전하지 않을 수 있다는 것과 gets() 함수 대신 gets_s() 함수를 사용하라는 의미이다. 보안적으로 안전하지 않다는것은 보안 결함이 존재함을 의미한다. 그 결함은 "버퍼 오버플로우에 의한 버퍼 오버런 공격에 대한 취약성"을 의미한다.

버퍼 오버런 취약점이 야기하는 보안 문제의 등급은 심각(critical) 수준인 경우가 많다. 허가 받지 않은 원격사용자가 관리자 권한을 불법적인 방법으로 획득한다는 식의 사고 사례와 동반한다. 그렇기 때문에 보안 결함이 알려진 함수를 사용하는 것보다는 대체 함수를 사용하는것이 현명하다.

printf() 함수

처음 작성하는 코드에서 포함되는 printf() 함수는 매우 유용한 함수이다. 실제로 콘솔 기반 응용 프로그램에서 정보를 출력하기위해 가장 많이 사용하는 함수이다. 문자, 숫자, 문자열 등의 정보를 하나로 조합하여 출력 할 수 있는 강력한 기능을 제공한다.

int printf(const char *format [, argument]...);
전달인자 : 형식 문자열이 저장된 메모리의 주소, 형식 문자열에 대응하는 가변인자.
반환값 : 출력할 문자열의 개수
설명 : 형식 문자열에 맞춰 표준 출력 장치에 문자열을 출력하는 함수.

printf() 함수를 제대로 사용하려면 형식 문자열(format string)에 대해 알아야한다. 그런데 이 형식 문자열은 문자열과 형식 문자,이스케이프 시퀀스로 구성된다. 그리고 printf() 함수를 호출할 때 기술해야 할 전달인자의 개수는 형식 문자의 개수와 일치해야한다.

형식 문자와 이스케이프 시퀀스

#include <stdio.h>

int main(void){
  int Num = 10;
  putchar('A');
  putchar('\n');

  printf("%c\n", 'B');
  printf("Num 은 %d 입니다.\n", Num);
  return 1;
}
A
B
Num 은 10 입니다. 

실행결과

예제 코드를 보면 putchar함수를 여러번 호출하여 printf 함수를 사용한것 처럼 비슷한 결과를 얻을 수 있다. 그러나 int 형 자료는 출력할 수 없다. printf함수를 이용하면 변수 안에 담긴 숫자를 출력하는 것을 넘어 각종 안내 문구를 조합해 사용할수 있다.

"Num 은 %d 입니다.\n" 이라는 문자열이 형식 문자열이고, %d 가 형식 문자이다. %d 는 10진수 형식으로 출력하라는 의미이다.

\n 다음줄로 개행
\t Tab 
\r Carriage return

자주사용하는 이스케이프 시퀀스

자료형에 맞는 형식 문자를 연결해주어야 한다. 그렇게 하지 않으면 예상했던 결과가 아닌 다른 결과가 나올수 있다.

이스케이프 시퀀스(escape sequence, 확장 비트열)은 출력장치에 전달되는 제어명령이다. 주로 사용하는 문자는 \n, \t 등이 있다.

문자와 정수 출력

정수를 표현하기 위한 자료형 중 가장 많이 사용하는 자료형은 char, int 이다. 각각 형식 문자는 %c, %d 이다.

char형과 int형은 분명 다르지만 '부호가 있는 정수형'이라는 점은 같다.

#include <stdio.h>

int main(void){
    
    printf("%c\n", 'A');
    printf("%c\n", 'A' + 1);
    printf("%c\n", 65);
    printf("%c\n", 66);

    printf("%d\n", 'A');
    printf("%d\n", 'A' + 1);
    printf("%d\n", 65);
    printf("%d\n", 66);
    return 1;
}

예제코드

A
B
A
B
65
66
65
66

실행결과

'A' 문자 를 %c 형식문자로 받으면 캐릭터 형식으로 A 를 출력하게 되고 정수 65를 %c 형식문자로 받으면 동일하게 A 로 출력하게 된다.

또한 문자 'A' 를 %d 형식 문자로 받으면 65를 출력한다. 즉, 정보의 본질이 달라지는것이 아니라, 출력형식을 바꿔주는 것만으로도 전혀 다른 정보로 인식 될수있다.

실수와 지수 출력

float형 실수 상수는 끝에 'F' 가 붙고 실수상수 끝에 'F'표기가 없다면 이것은 double형 실수상수이다.

float형은 크기가 4바이트이고, double형은 8바이트 이다. 실수를 출력할때는 각각 %f 와 %lf 형식문자를 사용하면된다.

그러나 실수를 표현할때는 가급적 double 형을 사용하는것이 좋다. 소수점 이하 15번째 정보까지 신뢰할수 있기 때문이다. float형에 비해 정확도가 좋다.

scanf() 함수

int scanf(const char *format [, argument]...);
전달 인자 : format 형식 문자열이 저장된 메모리의 주소
반환값 : 입력할 문자열의 개수
형식 문자열에 맞춰 표준입력장치로부터 정보를 읽어드리는 함수이다. 가변인자는 사용자가 입력한 값이 저장될 메모리의 주소이다.

형식 문자열을 이용한 사용자 입력을 지원하여 문자,문자열,정수,실수와 같은 정보를 입력받을수 있다. 주의 사항은 형식 문자에 대응하는 가변인자는 모두 주소라는 점이다.

정수 입력

나이를 입력받는 받아 출력하는 예제이다. 중요한것은 scanf() 함수를 호출할때 이름이 Age 변수의 주소를 의미하는 &Age가 인자로 전달되었음을 알수있다.

#include <stdio.h>

int main(void){
    
    int Age = 0;
    printf("나이 입력 : ");
    scanf("%d", &Age);
    
    printf("나이는 %d 입니다.\n",Age);
    return 1;
}

실행결과1
❯ ./welcome
나이 입력 : 15
나이는 15 입니다.

실행결과2
❯ ./welcome
나이 입력 : A
나이는 0 입니다.

예제 코드

실행 결과1를 보면 사용자 입력으로 정상적으로 정수를 입력하였고 정상적 결과를 출력한것에 반해 실행 결과2를 보면 사용자 입력으로 'A' 를 주었다. 그런데 결과로 기존 0 초기화 한 값이 출력되었다. 문자를 입력하는 경우 scanf() 함수는 이를 정수로 인식하지 못한다.

사용자가 입력한 정보를 제대로 해석할수 없으면 scanf()함수는 인수로 전달된 주소로 식별되는 메모리에 아무것도 담아주지 않는다.

문제1 나이와 이름을 입력받아 출력하는 프로그램
사용자로부터 나이와 이름을 입력받아 출력한다. 이름을 입력받을때는 gets() 함수를 사용하고 나이를 입력받을때는 scanf() 함수로 입력 받아야한다.

입력
이름 입력 : 성수
나이 입력 : 26

출력
당신의 나이는 26살이고 이름은 '성수' 입니다.
#include <stdio.h>

int main(void){
    char Name[16] = {0};
    int Age = 0;
  
    printf("이름 입력 : ");
    gets(Name);
    printf("나이 입력 : ");
    scanf("%d", &Age);
    printf("당신의 나이는 %d살이고 이름은 '%s' 입니다.\n", Age, Name);
  
    return 1;
}

여기서 실수를 많이 하는 부분은 scanf() 함수의 가변 인자에 변수 이름 앞에 & 기호(주소 연산자)를 붙여야한다. 또한 scanf() 함수의 형식 문자열에 개행문자(\n) 을 포함하는것은 매우 잘못된것이다. 입력 완료를 의미하는 개행과 형식 문자가 요구하는 개행문자를 구별할 수 없기 때문이다.

두 정수의 입력 및 구분

scanf()도 printf() 함수와 마찬가지로 형식 문자열 안에 여러 형식 문자를 사용할 수 있다. 형식 문자의 개수만큼 매개변수도 대응되야한다. 두 정수를 입력받는 경우 scanf() 함수는 어떻게 구별되는지 알아야한다.

#include <stdio.h>

int main(void){
    int a = 0, b = 0;
    printf("두 정수를 입력 : ");
    scanf("%d%d", &a,&b);
    printf("두수의 합은 %d 입니다.\n", a+b);
    return 1;
}

실행결과
❯ ./welcome
두 정수를 입력 : 2 4
두수의 합은 6 입니다.

입력에서 2스페이스바4 라고 입력했다. %d 가 나란히 붙어있어서 이런 방식으로 하면 안된다고 생각할수 있지만 아니고 빈칸, Tab 키 ,Enter키 입력으로 각 형식 문자에 대한 입력을 구별한다.

문자 입력

scanf() 함수를 통해 문자 를 입력받으려면 %c 형식문자를 사용하여 입력 받으면 된다.

int main(void){
    char nChar = 0;
    printf("문자 하나를 입력하세요 : ");
    scanf("%c", &nChar);
    printf("입력한 문자는 %c 입니다.\n", nChar);
    return 1;
}

실행결과
❯ ./welcome
문자 하나를 입력하세요 : Z
입력한 문자는 Z 입니다.

문자열 입력

scanf() 함수를 통해 문자열을 입력받으려면 %s 형식문자를 사용하여 입력받으면 된다.

#include <stdio.h>

int main(void){
    char nChar[16] = {0};
    printf("문자열을 입력하세요 : ");
    scanf("%s", nChar);
    printf("입력한 문자열은 %s 입니다.\n", nChar);
    return 1;
}

실행결과
❯ ./welcome
문자열을 입력하세요 : helloworld
입력한 문자열은 helloworld 입니다.