[C] 5. 입출력은 프로그램의 근본입니다. <printf, scanf>
C | C++/C

[C] 5. 입출력은 프로그램의 근본입니다. <printf, scanf>

Woon2World :: Programming & Art Life

 

드디어 여태껏 쭉 써왔던 printf를 자세히 다룰 글이 나타났습니다.

printf의 사용 경험이 여러분에겐 충분하기 때문에,

오늘 글은 매일 봤던 친구를 보는 것처럼 편하게 읽으실 수 있을 겁니다.

출력을 담당하는 printf, 입력을 담당하는 scanf가 이번 글의 주제입니다.

scanf는 printf와 굉장히 유사합니다.

 

 

stdio는 standard input output의 준말입니다.



 

 

printf

 

printf는 입출력 중 출력에 해당하는 한 함수입니다.

프로그램을 실행시켰다면 유의미한 동작 결과가 있어야겠죠.

그래서 프로그램은 출력이 필요합니다.

결과로 얻어지는 게 없다면 프로그램은 무의미합니다.

 

printf는 문자열을 출력할 때 사용합니다.

printf를 사용할 때는 함수의 이름인 printf를 밝혀주고 괄호를 열어 출력할 문자열을 큰 따옴표 ""와 함께 적어줍니다.

printf("Hello World!\n");

printf를 통해 변수의 값을 출력할 수도 있습니다.

이럴 때에는 서식 문자를 사용합니다.

int a = 3;
int b = 5;
printf("%d * %d = %d\n", a, b, a*b);

문자열 안에 서식 문자를 집어 넣고,

그 서식 문자와 연결되는 값들을 문자열 뒤의 인자(argument)로 넣어줍니다.

 

인자는 함수가 실행될 때 함수의 동작에 필요한 재료들인데요.

printf는 기본적으로 문자열을 인자로 요구하고,

서식 문자가 있는 경우에는 서식 문자와 연결되는 변수나 상수, 혹은 식을 서식 문자의 개수만큼 추가로 요구합니다.

위의 예시의 경우에는 문자열 안에 %d라는 서식 문자가 3개이기 때문에 문자열과 더불어 a, b, a*b를 인자로 넘겨주었습니다.

위 사실을 암기하실 필요는 없습니다.

문자열 안에 어떠한 값을 출력하고 싶을 때 서식 문자를 사용한다는 점만 알고 계시면 됩니다.

 

scanf

 

scanf는 입출력 중 입력에 해당하는 한 함수입니다.

출력과 다르게 입력은 필수가 아닌 경우도 있습니다.

보통 입력이 없으면 언제 어떻게 실행해도 같은 결과가 나오는 프로그램이 되지만,

그것 자체로 유의미한 프로그램도 있고, 같은 결과가 나오지 않는 프로그램도 있거든요.

예를 들어 로또 번호 생성기 같은 프로그램은

아무런 입력을 받지 않고 매번 실행 결과가 다르게 로또 번호가 출력되는 프로그램일 것이라고 합리적인 추측이 가능합니다.

 

scanf는 사용자로부터 문자열을 입력받는 함수입니다.

scanf를 사용할 때에도 마찬가지로 함수의 이름인 scanf를 밝혀주고 괄호를 열어 입력받을 문자열을 큰 따옴표 ""와 함께 적어줍니다.

매번 똑같은 문자열을 입력받는 것은 의미가 없기 때문에, 통상적으로 scanf는 서식 문자를 항상 사용합니다.

사용자로부터 입력받은 값을 변수에 저장하는 것이죠.

int num;
scanf("%d", &num);

문자열 안에 서식 문자를 집어 넣고,

그 서식 문자와 연결되는 변수의 주소를 문자열 뒤의 인자로 넣어줍니다.

변수의 이름 앞에 &를 적음으로써 그 변수의 주소를 나타낼 수 있습니다.

&는 address of 연산자라고 읽습니다.

 

scanf는 printf와 마찬가지로 문자열을 인자로 요구하고, 서식 문자와 연결되는 변수의 주소를 서식 문자의 개수만큼 추가로 요구합니다.

 

변수는 선언을 통해 메모리를 확보했기 때문에 주소를 취할 수 있지만,

3,5와 같은 리터럴 상수나, a*b 같은 식은 주소를 취할 수 없습니다.

리터럴 상수나 식도 분명히 저장할 메모리가 필요합니다.

하지만 우리가 정식으로 선언을 통해 요청하지 않은 메모리는,

그 메모리가 필요한 단 한 줄에서만 임시적으로 확보되었다가 반납됩니다.

그런 임시 메모리의 주소를 취하는 것은 불합리한 것으로 여겨집니다.

선언이 이루어진 것들, 쉽게는 이름이 있는 것들만 주소를 취할 수 있습니다.

 

서식 문자

서식 문자는 % 기호를 통해 서식 문자임을 나타내고, 그 뒤에 붙는 알파벳이 무엇이냐에 따라 어떤 타입(자료형)의 값과 연결되는지 구분됩니다.

 

아래 표는 여러분이 전공생이시고 교수님이 외우라 하지 않은 이상

절대 외울 필요가 없습니다!

서식문자 연결되는 타입 출력 예시
%d char, short, int -3, -2, -1, 0, 1, 2, 3 (char의 경우도 숫자로 출력됨)
%ld (LD) long -3, -2, -1, 0, 1, 2, 3
%lld (LLD) long long -3, -2, -1, 0, 1, 2, 3
%u  unsigned int 0, 1, 2, 3
%f float -0.1, 0.0, 0.1
%lf (LF) double, long double -0.1, 0.0, 0.1
%c char a, b, c
%s char* "Hello World", "Good Night" (문자열)
%o unsigned int 0, 1, 2, 3, 4, 5, 6, 7, 10, 11 (연결된 값을 8진수로 출력)
%x unsigned int 0, 2, 4, 6, 8, a, c, e, 10 (연결된 값을 16진수로 출력)
%e, %E float, (double, long double) 1.234500e+02, 3.141592E+00 (지수 표기법으로 출력)
(Visual Studio에서, 연결되는 타입이 double이든
long double이든 float 타입으로 출력됨)
%g, %G float, (double, long double) 3.14159, 1.23456e+06, 6.54321E+08
(숫자의 크기가 작으면 일반 표기법, 숫자의 크기가 크면 지수 표기법으로 출력)
(Visual Studio에서, 연결되는 타입이 double이든
long double이든 float 타입으로 출력됨)

 

왜 외울 필요가 없는지 코딩을 같이 하면서 알아봅시다.

 

이번 소스의 이름은 "5. printf, scanf.c"입니다.

실제 코딩할 때에 scanf를 사용하면 위와 같이 오류가 발생합니다.

scanf 함수의 보안 상의 문제 때문에 사용을 막는 것인데요.

버퍼 오버플로우(buffer overflow)라고 불리우는 상황 때문에 그렇습니다.

어떠한 메모리 주소에 정보를 입력할 때, 그 메모리 크기보다 더 큰 크기의 정보를 입력할 가능성이 있죠.

예를 들어 메모리 크기는 4바이트인데, 정보는 8바이트인 경우가 있습니다.

하지만 scanf는 이런 상황에도 정보를 모두 기록합니다.

따라서 확보하지 않은 메모리에 정보가 기록되는 현상이 발생하는데요.

이럴 때에는 미정의 동작(undefined behavior)이 발생합니다.

말그대로 어떤 일이 일어날지 모르는 상황이죠.

 

최초의 웜 바이러스인 Morris Worm이 바로 버퍼 오버플로우를 이용하는 프로그램입니다.

궁금하시면, 한 번 재미삼아 읽어보셔도 좋습니다.

http://news.grayhash.com/html/category/malware/c16f59d4c0.html

 

The Graynews - [Malware] 최초의 웜, 모리스 웜

 

news.grayhash.com

 

일단 우리 프로그램의 작성자도 사용자도 우리이기 때문에,

scanf를 사용한다고 크게 무섭지는 않습니다.

보안 속성을 해제하기 위해 두 가지 방법이 있는데요.

첫 번째 방법부터 소개하겠습니다.

"디버그(D)"의 하위 항목인 "프로젝트 디버그 속성"을 클릭합니다.

프로젝트 속성 페이지가 열리면 "C/C++" 항목을 클릭합니다.

그리고 "SDL 검사" 항목의 "예(/sdl)"이라고 되어있는 부분을

"아니요(/sdl-)"로 바꿔주세요.

그리고 "확인"을 눌러 속성 페이지를 닫습니다.

 

두 번째 방법은 같은 효과이면서 훨씬 간단한데요.

#define _CRT_SECURE_NO_WARNINGS를 코드에 적어주면 됩니다.

두 방법 중 원하는 방법을 선택하시면 됩니다.

 

원의 반지름을 입력받아 그 넓이와 둘레를 출력하는 프로그램을 만들어 보겠습니다.

 

#include <stdio.h>
 
int main()
{
    float radius;
 
    return 0;
}
cs

 

먼저 반지름을 입력받을 것이니, 입력받은 값을 저장할 변수를 선언해야겠죠.

타입은 int가 아닌 float인 편이 좋을 것입니다.

반지름을 정수로만 한정할 것이 아니라면요.

이름은 반지름을 영어로 번역한 radius라 지었습니다.

 

#include <stdio.h>
 
int main()
{
    float radius;
 
    printf("반지름을 입력하세요. : ");
    scanf_s("%d"&radius);
 
    return 0;
}
cs

 

그리고는, 진짜로 입력을 받아봅시다.

scanf_s는 안전한 scanf 함수인데요, 이 함수를 사용할 경우 보안 설정을 해제하지 않아도 됩니다.

ㅎㅎㅎㅎㅎㅎ 괜히 어렵게 보안 설정을 해제했다 생각하실 수 있겠지만은

scanf 뿐만 아니라 다양한 입력 함수에서 버퍼 오버플로우 관련 보안은 중요한 사항이니 그 공부를 하셨다 생각해주세요.

그리고 scanf_s는 Visual Studio에서만 제공되는 비표준 함수이기 때문에 다른 개발 환경에선 사용 불가합니다.

 

float 타입은 원래 %f라는 서식문자로 받아야 하지만, 일부러 %d로 받아봤습니다.

실행시킨 후 바로 꺼봅시다.

5. printf, scanf.c(8,10): warning C4477: 'scanf_s' : 서식 문자열 '%d'에 'int *' 형식의 인수가 필요하지만 variadic 인수 1의 형식이 'float *'입니다.

Visual Studio에서 위와 같은 경고 메시지를 띄웠을 겁니다.

화면 하단의 "출력" 창에 나타날 것입니다.

 

float 타입의 변수에 값을 저장해야 하는데 서식 문자가 %d였기 때문에 경고를 한 것입니다.

이 경고 메시지 덕분에 여러분들은 잘못된 서식 문자를 사용하였을 때에 바로 알 수가 있습니다.

 

%d 대신 %f를 써야겠죠.

코드를 수정해주시기 바랍니다.

여러분들이 실수를 하더라도 Visual Studio에서 알려주기 때문에 외우실 필요가 없는 겁니다.

서식 문자를 잘못 썼다는 사실만 알면, 그 때 저 표를 참고하면 되니까요.

그리고 자주 쓰는 %d, %f, %lf, %c, %s 같은 경우 쓰다보면 금방금방 외워집니다.

그러니 표를 외우려 하지 마세요.

 

프로그램을 마저 작성해봅시다.

원의 넓이는 πr2, 원의 둘레는 2πr죠?

 

#include <stdio.h>
 
int main()
{
    float radius;
 
    printf("반지름을 입력하세요. : ");
    scanf_s("%f"&radius);
 
    printf("원의 넓이 : %f\n"3.141592f * radius * radius);
    printf("원의 둘레 : %f\n"2 * 3.141592f * radius);
 
    return 0;
}
cs

 

모두 float 타입이니 %f를 써줍니다.

여러분들이 원하는 값을 입력해보세요.

오늘 여러분들은 여러분들의 입력에 따라 출력이 달라지는 최초의 프로그램을 작성하셨습니다.

자부심을 가지시기 바랍니다!

프로그래밍이 의미 있어지는 단계에 도착하셨습니다.

 

printf와 관련하여 추가적인 내용이 다음 글이 될 것 같습니다.

입출력은 프로그램의 근본이고, stdio는 standard input output의 준말이라는 점 잊지 마시고요!

제 글이 여러분들의 학습에 큰 도움이 됐으면 좋겠습니다.

감사합니다.