[C] TIL 5. 표준 입출력 도구


  1. 2진법
  2. 정보의 표현


boostcourse모두를 위한 컴퓨터 과학 (CS50 2019) 강의를 듣고 정리한 필기입니다. 😀


1. 문자 입출력 함수

문자 입출력 함수인 getchar()putchar()는 C 런타임 라이브러리(CRL:C Runtime Library) 함수 중에서도 가장 기본적 입출력함수이다. 이들은 한 번에 한 문자씩 입출력을 수행한다. 다소 답답한 방법이라고 생각할 수도 있는데, 컴퓨터의 능력으로는 한 문자씩 입출력을 수행하는 방법이 적당하다.

C 언어는 함수로 시작해서 함수로 끝나는 언어이며, C언어 프로그램을 개발한다는 것은 함수를 만다는 것으로 생각할 수 있다. 그러나 프로그램을 작성할 때 매번 함수를 만드는 것은 번거로운 일일 것이다. 때문에 누군가 만들어 놓은 함수를 사용하곤 하는데, 이 C 런타입 라이브러리는 C 언어 프로그램을 개발할 때 가장 많이 필요한 것을 모은 라이브러리이다.

1.1 getchar()putchar()

1.1.1 getchar()

int getchar(void);

getchar()의 인자는 없고, 인자 대신 사용자가 입력한 문자 하나를 반환한다.(오류가 발생하면 EOF나 -1을 반환) 그리고 TIL4에서 설명 했듯이 이 함수는 버퍼(Buffer)에서 문자를 퍼올린다. 다른 말로 표준입력장치(기보드)의 입출력 버퍼에서 퍼오는 것이다. 즉, 버퍼에 아무 것도 없다면 키보드의 입력을 기다릴 것이고, 버퍼에 내용이 채워져있다면 그것을 퍼올릴 것이다.

이 함수의 원형은 int getchar(void)이다. int는 int형 자료를 반환하는 함수임을 선언한 것이고, getchar는 이름, 괄호 안에 있는 void는 아무것도 입력받지 않는다(인자 없음)는 것을 뜻한다.

1.1.2 putchar()

int putchar(int c);

putchar()는 상수를 인자로 받으며, 함수 호출 결과로 int형 자료를 얻을 수 있다. 이것은 실행하면, 표준입력장치(stdout)인 콘솔(Console)에 영문 한 글자를 출력한다. 한글은 출력되지 않는다. (?만 출력된다.)

이 함수의 원형은 int putchar(int c)이다. int는 int형 자료를 반환하는 함수라는 것을 선언한 것이고 putchar는 함수의 이름, 괄호에 있는 int c는 출력할 문자가 상수라는 것이다.

1.1.3 코드

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

int main(void)
{
    int ch = 0;

    ch = getchar();
    putchar(ch);
    ch = getchar();
    putchar(ch);
    ch = getchar();
    putchar(ch);
    ch = getchar();
    putchar(ch);
    ch = getchar();
    putchar(ch);
    ch = getchar();
    putchar(ch);
    
    return 0;
}

위 코드는 영문자 하나를 입력했을 때와 여러개의 영문자를 한번에 입력했을 때의 결과가 다르다. 일단, 두 상황 모두 최초로 getchar()가 호출됐을 때는 사용자로부터 입력을 받는다. 왜냐하면 최조로 호출되었을 때는 버퍼가 비어있기 때문이다.

title

위는 최초로 호출했을 때는 물론이고 그 뒤로도 모든 문자를 입력받는다. 그 이유는 아마 한글자씩 입력하서 버퍼가 계속 비어지기 때문일 것 이다. (한글자 넣고, 한 글자 빼고를 반복.)

title

하지만 River라는 글자를 전부 입력했을 때, 기보드로 입력받는 것은 최초의 getchar() 뿐이다. 최초 입력 이후, 버퍼에는 이미 문자가 있을 것이기 때문에 별도의 사용자 입력 없이 River가 완전하게 출력된다. 러리이다.

1.2 _getch()_getche()

_getch()_getche()는 앞에서 배운 getchar() 함수와 기능적으로는 매우 비슷하다. 특히 내부 구조는 완전히 다르다.

title

위와 같은 차이 때문에 _getch()_getche()는 여러 문자를 한번에 입력했다가 꺼내는 등의 것은 할 수 없다. 어떤 정보라도 입력하면 함수는 입력한 정보를 바로 출력한다.

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

int main(void)
{
    char ch = 0;

    ch = _getch();
    printf("_getch()를 사용했습니다. 입력한 키는%c", ch);
    printf(" 입니다.\n", ch);

    ch = _getche();
    printf("_getche()를 사용했습니다. 입력한 키는%c", ch);
    printf(" 입니다.\n", ch);

    return 0;
}

title

_getch()는 <Enter> 키를 누를 필요가 없다. 한 글자라도 입력되면 함수가 즉시 반환하기 때문이다. 하지만 화면에는 출력되지 않는다. 반면 getche()는 어떤 키가 눌렸는지 화면에 출력한다는 점에서 차이가 있다.



2. 문자열 입출력 함수

문자열은 여러 개의 문자가 연속으로 나열된 것이다. R이나 i는 한 글자지만(이건 문자), River는 영문 글자가 연이어져 하나의 문자열을 이룬 것이다. 그리고 이들의 자료형은 char이고 1byte 정수형이다. R는 한 글자이기 때문에 1byte에 해당한다. River은 1byte가 5개이기 때문에 5byte이상의 메모리가 필요하다. 그리고 1byte 는 8bit 이기 때문에 문자열은 8bit 정수형이라고 할 수 있다. 이 때문에 글자에서 숫자를 하나 더하거나 빼는 연산도 가능하다.

그리고 만약 River라는 5글자를 담기 위해서는 char인 변수가 4개가 필요하다.

#include <stdio.h>

int main(void)
{
    char ch1 = 0;
    char ch2 = 0;
    char ch3 = 0;
    char ch4 = 0;
    char ch5 = 0;

    return 0;
}

위 코드처럼 같은 코드를 다섯 개를 복사할 수 있지만, 우리는 TIL3에서 대괄호([])를 사용하는 방법으로 char형 메모리 5개가 확보했다.

#include <stdio.h>

int main(void)
{
    char ch[5];

    return 0;
}

2.1 gets()puts()

2.1.1 gets()

char *gets(char *buffer);

gets()gethar() 가 문자 혹은 문자열을 입력받아 입출력 버퍼에 저장한 후, 하나씩 꺼내 반환하는 것과 매우 비슷하다. 다만, 이것은 문자가 아니라 문자열을 반환한다는 것에서 다르다. gets()는 buffer를 인자로 받으며, 정상적이면 전달받은 메모리의 주소를, 에러가 발생하면 NULL을 반환하는 함수이다. 역기서 인자로 받는다던 buffer는 사용자로부터 입력받은 문자열을 저장할 메모리의 주소이다.

이 함수의 원형은 char *gets(char *buffer);가 된다. char *은 자료형으로 캐릭터 포인터라고 읽는다. char형과는 엄연히 다른 자료형이다. (나중에 자세히 배우겠지만 *는 포인터를 의미하며 메모리 주소를 저장하는 변수이다.)

무엇보다 get() 는 보안에 취약하다는 것을 알고 있어야 한다. 때문에 get_s()를 사용하는 것이 좋다.(나중에 배우겠지만 퍼버 오버런이 발생할 수도 있다.) 그리고 이런 오버런은 해킹을 동반하는 경우가 허다하다.

2.1.2 puts()

int puts(const char *string);

puts()는 인자로 출력할 문자열이 저장된 메모리의 주소를 받는다. 정상이면 음수가 아닌 값을 반환하며, 아니라면 EOF(-1)을 반환한다. 그리고 이 함수는 문자열을 콘솔 화면에 출력한다.

이 함수의 원형은 int puts(const char *string)이다. gets()처럼 매개변수의 자료형은 char이 아니라, char *이다. 그리고 앞에 const라는 예약어가 붙어있고, string이 붙었지만, char *이기 때문에 인자로 전달된 메모리의 주소에 저장된 문자열을 출력한다. 그리고 이 문자열을 출력하고 나면 자동으로 개행 문자(\n)를 출력하기 때문에 별도의 명령을 하지 않아도 새로운 행에 출력된다.

또한, 이 함수는 출력할 문자열 길이를 따로 명시하지 않아도 된다. c언어의 문자열은 모두 \0(NULL 종단문자)로 끝나기 때문에, 길이를 알지 않아도 그 끝을 찾을 수 있다.

2.1.3 코드

#include <stdio.h>

void main()
{
	// 변수 선언
	char szName[32] = { 0 };
	
	// 이름 입력 받기
	printf("What's your name: ");
	gets(szName);

	// 내려 받은 이름을 출력하기.
	printf("Hi,");
	puts(szName);
}

title

2.3 gets()의 보안 결함와 대안.

2.3.1 gets()의 보안결함.

gets()는 개행문자(\n)에 도달할 때까지 한 줄을 전부 읽고, 개핸문자를 제거하고 C 문자열을 만들기 위해 널종단문자(\0)를 추가하여 남은 문자를 저장하는 식으로 작동한다. 이러한 gets()의 문제는 입력행이 실제로 배열에 딱 맞는지를 점검하지 않는다는 것이다. 즉, 배열이 어디서 시작하는지만 알 뿐 원소가 몇 개 있는지는 모른다.

C는 문자열이 너무 길면, 버퍼 오버플로우(buffer overflow)가 나타나는데, 이는 지정된 길이를 초과하여 문자들이 들이 넘친다는 말이다. 그 외에 “세그멘테이션 오류(Segmentation fault)”등의 오류도 발생할 수 있다. Unix 시스템에서 이 메세지가 뜨면 프로그램이 할당되지 않은 메모리에 접속하려고 한 것을 말한다.

물론 C는 gets() 외에도 결함이 있는 함수들이 있다. 그럼에도 앞서 소개한 4개의 함수 중 gets()만을 따로 언급하는 이유는 “버퍼 오버플로우에 의한 버퍼 오버런 공격(buffer overrun attack)에 대한 취약성” 때문이다. 버퍼 오버런 취약점은 보안 문제 등급을 심각(critical) 수준으로 올려놓을 때가 있다. 해서, C99 표준을 만드는 위원회 역시 표준을 위한 근거로 gets()의 문제점을 인정하고 사용하지 않을 것을 권했다. C11는 더 나아가 표준에서 제외하기까지 했다.

2.3.2 gets()의 대안

gets()의 대안으로 fgets()gets_s()라는 것이 있다. gets_s()fgets()보다 늦게 탄생했는데, fgets()가 인터페이스가 복잡하고 입력을 약간 다르게 처리하는 점이 혼동을 줄 수 있어 C11 표준은 gets_s()를 추가했다. 그래서 gets_s()gets()와 더 비슷하여 좀 더 쉽게 대체된다.

fgets()

이 함수는 읽을 문자의 최대개수를 지정함으로써, 두 번 째 전달 인자를 취할 때 생길 수 있는 오버플로우 문제를 해결한다. 하지만, fgets()는 파일 입출력용으로 설계되었기 때문에 gets()는 다르게 처리된다. gets()와의 차이점은 아래와 같다.

  • 최대 개수 지정을 위해 두 번 째 전달인자를 사용한다. 그 전달 인자 값이 n이라면, n-1개까지 문자들을 읽거나, n-1개를 다 읽기 전에 개행문자가 나오면 읽는 것을 멈춘다.
  • 개행문자를 제거하고 널 종단 문자를 추가하는 gets()와는 달리, fgets()는 개행문자까지 저장한다.
  • 이것은 파일 입출력용이다. 어떤 파일을 읽을 것인지는 세 번째 전달 인자로 결정된다. 키보드로 입력하는 것을 익기 위해서는 stdin(키보드와 같은 standard input)을 전달 인자로 사용하면 된다.(식별자는 stdio.h로 정의되어있다.)

fgets()는 개행문자를 포함하기 때문에 fputs()와 쌍을 이루어 사용하곤 한다. fputs()의 두 번째 인자로 stdout(표준 출력용)을 사용하면, 컴퓨터 모니터에 출력된다.

gets_s()

char *gets_s(char *buffer, size_t sizeInCharacters);

gets_s() 또한 전달 인자로 읽는 문자 수의 한계를 지정할 수 있다. 이 함수는 기본적으로 표준 입력(stdin)만 읽기 때문에 세 번째 전달 인자가 필요하지 않다. 그리고 개행문자의 경우, gets()처럼 읽고 버린다. (이것 때문에 fgets()보다 gets_s()gets()를 대체하기 쉽다.)만약 정해진 문자 수를 다 읽었음에도 개행을 읽지 못했다면(즉, 허가된 메모리 이상을 사용하게 될 경우), 예외를 발생시킨다. 목표 배열의 첫 번째 문자를 널 문자에 맞추어 놓는다. 개행 또는 파일의 끝(End-of-file)을 만날 때까지 그것을 읽고 이후의 입력을 버린다. 널 포인터를 리턴한다. 구현에 의존하는 “handler”함수를 호출하는데, 이것은 프로그램의 종료를 발생시킨다. 이것이 비정상적으로 종료되는 것처럼 보일지라도 최소한 해킹을 당할 리는 없다. 완벽하게 보안을 보장할 수는 없지만 어찌 되었건 gets()보다는 낫다.



3. printf()scanf()

printf()scanf()는 입출력 함수(input/output function), 줄여서 I/O 함수 라고 불린다. C언어에서 유일한 I/O 함수는 아니지만 가장 다용도로 사용된다. prinf()는 출력 함수이고, scanf()는 입력함수지만, 둘 다 하나의 포맷 문자열과 전달인지 리스트를 사용한다.

3.1 출력 함수

3.1.1 변환 지정자

title

프로그래밍 언어에서는 데이터를 출력할 때 그 데이터를 어떤 포맷으로 변환해야하는지 지정해야한다. 이것을 변환지정(conversion specification) 이라고 부른다.

위 표는 포맷문자와 그에 상응하는 출력 데이터형을 보여준다. 여기서 중요한 것은 자료형이다. 만약 자료형과 출력할 형식이 맞지 않으면 엉뚱한 결과를 출력하거나 프로그램이 비정상 종료된다. 애석하게도 컴파일러는 아무런 경고를 해주지 않는다.

특히 문자열을 출력하는 %s를 주의해야한다. %s는 대응되는 매개변수가 문자열이다. 정확히 말하면 배열형태의 문자열이 아니라, 메모리 주소이다. %s는 메모리 주소를 출력하는게 아니라, 메모리 주소에 접근해 데이터를 출력한다. 그리고 보통 %s에 char 배열의 이름이 대응되도록 코드를 구성한다.

그리고 %라는 기호를 출력하고자 한다면 %%를 사용해야한다. 왜냐하면 % 기호는 형식문자를 의미하는 예약문자이기 때문이다.

3.1.2 지정 변경자

%와 문자 사이에 변경자를 삽입하면, 기본적인 변환 지정을 변경할 수 있다. 이러한 지정 변경자는 다수인데, 만약 한번에 여러개가 쓰였을 경우, 아래 표의 순서를 따른다. 그리고 아래 종류의 모든 조합이 가능한 것은 아니다.

title

3.1.3 printf()

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

printf()는 포맷문자를 사용하여 문자열을 출력하는 함수이다. 이 함수의 첫번째 매게 변수는 get()같은 포인터 캐릭터 포인터지만 나머지는 가변이다. 여기서 가변이라는 뜻은 그 갯수가 정해지지 않다는 뜻이다.

#include <stdio.h>

int main(void)
{
    int userAge = 0;
    char userName[32] = { 0 };

    printf("안녕하세요. 성함이 어떻게 되세요?\n");
    gets_s(userName, sizeof(userName));
    printf("제 이름은 %s 입니다.\n", userName);

    return 0;
}

printf()의 사용은 위처럼 할 수 있다.

#include <stdio.h>

int main()
{
    int nData = 0x41;
    char nChar = 'A';
    char *nString = 'River';
    
    printf("%d\n", nData);
    printf("%X\n", nData);
    
    printf("%c\n", nChar);
    printf("%c\n", nChar + 1);
    printf("%d\n", nChar);
    
    printf("%c\n", 65);
    printf("%c\n", 65 + 1);
    
    // nString의 자료형은 char형인 변수의 주소인 포인터 변수이다.
    // %s는 변수에 저장된 주소를 근거로 출력.
    printf("%s\n", nString);
    // %p는 변수에 저장된 주소 형식으로 출력.
    printf("%p\n", nString);
    printf("%p\n", &nString);
    
}

위 코드는 주석과 함께 읽으면 무리 없이 이해할 수 있을 것 이다.

// 16진수를 10진수(%d)로 출력
printf("%d\n", nData);
// 부호없는 16진수(%X)로 출력
printf("%X\n", nData);

%d%X는 각각 10진수, 16진수로 변수 nData에 저장된 16진수를 출력한다.

// 문자 상수는 ASCII 코드 문자형식으로 출력.
printf("%c\n", nChar);
// 문자 A의 ASCII 코드값(65)에 1을 더한 값을 목표로 출력.
printf("%c\n", nChar + 1);
// 문자 A의 ASCII 코드값을 10진수(%d)로 출력.
printf("%d\n", nChar);

변수 nChar는 A라는 문자가 저장되어있다. %c는 변수에 저장된 문자를 출력한다. 첫 번째 줄 코드는 A가, 두 번째 줄 코드는 B가 출력될 것이다. 특이한 건 두 번째 줄이다. nChar 은 char 형인데, 여기에 1을 더하면 int가 된다. 이유는 char 형보다 i56nt가 더 덩치가 크기 때문이다. 그리고 65에 1을 더한 66 즉, B를 출력한다. 세 번째 줄은 A에 해당하는 ASCII 코드 값을 10진수로 출력한다.

// 65에 해당하는 ASCII 값(A)을 출력.
printf("%c\n", 65);
// 65 + 1에 해당하는 ASCII 값(B)을 출력.
printf("%c\n", 65 + 1);

코드 중에 위 코드를 조금 더 살펴보기로 한다.

3.2 : 입력 함수

scanf()printf()와 거의 동일한 변환 지정자와 변환 변경자를 사용한다. 하지만, 몇몇 다른게 있으니, 일단 그것들 부터 살펴보고자 한다.

3.2.1 변환 지정자

title

printf()는 float 형과 double형 모두에 %f, %e, %E, %g, %G를 사용하지만, scanf() 는 float형에만 사용하고, double ㅎㅇ은 l 변경자를 요구하는 형태로 사용된다.

3.2.2 변환 변경자

title

여기서 주의 깊게 볼 것은 * 변경자이다. 이것은 printf()scanf() 둘 다에서 지정자의 의미를 변경시킬 수 있지만, 그 방식이 다르다.

3.2.3 scanf()

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

C라이브러리는 여러 개의 입력 함수를 가지고 있는데, 그 중 scanf()는 다양한 포맷의 데이터를 읽을 수 있기 때문에 가장 일반적으로 사용된다. 이는 printf() 처럼 형식 문자열을 사용하지만, 가변 인자로는 주소가 와야한다는 특징이 있다. 또, 우리가 키보드로 입력하는 데이터는 전부 텍스트이다. 2021이라는 것도 텍스트로 인식되는데, 이것을 문자열이 아닌 숫자로 저장하기 위해선 변환이라는 과정이 필요하다. scanf()가 이 역할을 하는 것이다.

printf()는 변수 이름, 상수, 수식을 사용하지만, scanf()는 변수를 가리키는 포인터를 사용한다. 하지만, scanf()를 사용하기 위해서는 다음과 같은 간단한 규칙만 알면 포인터를 알 필요는 없다.

  • scanf()를 사용하여 기본 데이터형의 값을 읽는 다면 변수 이름 앞에 & 기호를 사용한다.
  • scanf()를 사용하여 문자열을 읽어 문자 배열 안에 넣으면 &를 사용하지 않는다.
#include <stdio.h>

void main()
{
    int nInput = 0;
    printf("Input : ");
    scanf("%d", &nInput);
    printf("%d\n", nInput)
}





© 2020. by RIVER

Powered by RIVER