끝나지 않는 프로그래밍 일기



1. 프로그램 기본 구성


자, 이제 Visual Studio를 열어 새 C 프로젝트를 만들어 봅시다. (프로젝트 생성시 응용 프로그램 마법사에서 빈 프로젝트에 체크) 그다음 프로젝트가 생성되었으면, 솔루션 탐색기에서 소스 파일에 우클릭, 추가 -> 새 항목으로 새 소스 파일을 추가합니다. 그런 다음 아래와 같이 작성하여 봅시다.

#include <stdio.h>

int main()
{
	printf("Hello, world!\n");

	return 0;
}

그다음 컨트롤 키와 F5를 동시에 눌러 디버깅을 생략하고 바로 컴파일된 파일을 실행해보도록 해봅시다. 실행하면, 콘솔창이 화면에 표시되면서 화면 내에 아래와 같이 표시됩니다.


Hello, world!
계속하려면 아무 키나 누르십시오 . . .


출력된 결과물을 살펴보니, 'Hello, world!'가 출력되고 개행되어 그 다음 행에 '계속하려면 아무 키나 누르십시오 . . .'라는 글이 보이시죠? 저 위의 코드가 어떻게 해서 이러한 결과물을 만들어 냈는지 간단히 살펴보도록 하겠습니다. 우선 1행부터 살펴봅시다.

#include <stdio.h>

위의 문장을 간단히 살펴보니 stdio.h란 녀석을 포함(include)하라는 문장인것 같죠? 맞습니다. stdio.h란 파일을 포함하라는 문장입니다. 그렇다면 stdio.h는 무엇일까요? 우선 .h는 헤더(header)를 의미하는 확장자이며, stdio는 표준 입출력 라이브러리(Standard Input and Output Library)의 약자입니다. 왜 위와 같은 문장을 달아 주었냐면, stdio.h 내에 우리가 사용할 기능(function)들이 stdio.h 내에 모두 정의되어 있기 때문입니다. 우리는 그것들을 사용하기 위해 이렇게 포함(include)을 해주어야만 사용이 가능한 것입니다. 그리고 그 아래 문장을 살펴봅시다.

int main()

위의 문장에서 int는 함수의 반환형식으로써, 지금은 return 키워드의 옆에오는 데이터의 자료형이라고만 알아두도록 합시다. 그리고 main은 프로그램의 진입점(Entry Point)으로써, 메인 함수부터 프로그램이 시작됩니다. 시작점인 메인 함수를 반드시 만들어야 한다는 것을 알아두시기 바랍니다. 그리고 그 다음 문장을 살펴봅시다.

{ ... }

위의 문장은 영역을 구분짓는 녀석으로, 위의 코드에선 제일 처음의 { 부터, 마지막의 } 까지는 메인 함수만의 영역입니다. 'int main() { ... }'에서의 {와 } 사이는 메인 함수의 고유 영역임을 기억해두시기 바랍니다. 대충 이해를 하셨으면, 다음 문장으로 넘어가도록 합시다. (아직은 완벽히 이해가 되지 않더라도 걱정하실 필요는 없습니다)

printf("Hello, world!\n");

위의 문장은 결과물을 보아, 다음과 같이 유추할 수 있겠죠? Hello, world!라는 녀석을 화면에 출력(print)하는 녀석임을 알 수 있습니다. 그렇다면 \n라는 녀석은 무엇을 하는 녀석일까요? 이는 \n를 코드에서 제외하고 다시 Ctrl+F5를 눌러 무엇이 달라졌는지 비교를 해보시기 바랍니다. (\n에 대해선 아래에서 다시 다룰 예정입니다) 참고로 문장의 끝에 항상 세미콜론(;)을 사용합니다. 어디에서부터 어디까지가 한 문장인지 구분하며, 프리포맷을 지원하여 문법에만 맞다면 제대로 컴파일이 가능합니다.마지막 코드를 살펴보도록 합시다.

return 0;

위의 문장에서 return은 말그대로 무언가를 반환(return)하는 녀석입니다. 0을 누군가에게 반환하게 되는데, 이때 이 0을 반환받는 대상은 운영체제(OS)가 됩니다. 아직은 return 0;을 함수의 종료를 알리는 문장으로만 알도록 합시다. 여기까지만 이해하고, 차차 이해를 해나가도록 합시다.


2. 이스케이프 시퀀스(Escape sequence)


이번에는 \n이 어떤 기능을 하는지, 이런 것들은 무엇이 있는지, 이러한 녀석들을 무엇이라 하는지에 대해 간략히 알아보도록 하겠습니다. 우리가 보았던 \n 문자는 행을 바꾸는 개행 문자였습니다. 이런 특수한 기능을 하는 문자를 가리켜 이스케이프 시퀀스(Escape sequence)라 하며, 이스케이프 시퀀스는 아래 표에 정리를 해두었으니 직접 아래 문자들을 이용하여 예제를 직접 한번 만들어보시기 바랍니다. 


이스케이프 시퀀스

기능

\a

경고음

\b

백스페이스(backspace)

\f

폼 피드(form feed)

\n

개 행(new line)

\r

캐리지 리턴(carriage return)

\t

수평 탭

\v

수직 탭

\'

작은 따옴표 출력

\"

큰 따옴표 출력

\\

역슬래시 출력

\ooo

아스키 문자 8진수 표시

\xhhh

아스키 문자 16진수 표시


아래 예제를 한번 실행시켜 보시고, 예제가 어떠한 기능을 하는지 직접 살펴보도록 하세요.

#include <stdio.h>

int main()
{
	printf("경고음! \a\n");
	printf("백스페이스! abcdd\befg\n");
	printf("역슬래시 출력! \\\n");

	return 0;
}

결과:

경고음!
백스페이스! abcdefg
역슬래시 출력! \
계속하려면 아무 키나 누르십시오 . . .


5행: \a는 경고음을 출력합니다.

6행: \b는 뒤에 있는 문자 하나를 지웁니다.

7행: \를 화면에 출력합니다.


3. 출력 포맷(print format) - printf


이제 다시 돌아와, printf가 무엇을 하는 녀석인지 더 깊게 들어가보도록 합시다. printf 마지막의 f는 format의 약자로 출력 포맷(print format)을 의미합니다. 출력 포맷이 무엇인지 알아보기 위하여 예제를 하나 보도록 해봅시다.

#include <stdio.h>

int main()
{
	printf("내 나이는 %d살이다.\n");

	return 0;
}

결과:

내 나이는 0살이다.
계속하려면 아무 키나 누르십시오 . . .


코드를 보니 "내 나이는 %d살이다."가 출력될 줄 알았더니, 결과에서는 "내 나이는 0살이다."라고 출력이 되었습니다. 이는 왜 그런것일까요? "내 나이는 %d살이다." 에서의 %d는 서식 문자(conversion specifer)라는 것으로, 각 서식 문자마다 출력 형태를 가집니다. 우선은 아래의 표를 먼저 보도록 합시다.


서식 문자

출력 형태

%c

단일 문자

%d

부호 있는 10진 정수

%i

부호 있는 10진 정수

%f

부호 있는 10진 실수

%s

문자열

%o

부호 없는 8진 정수

%u

부호 없는 10진 정수

%x

부호 없는 16진 정수, 소문자 사용

%X

부호 없는 16진 정수, 대문자 사용

%e

e 표기법에 의한 실수

%E

E 표기법에 의한 실수

%g

값에 따라서 %f, %e 둘 중 하나를 선택

%G

값에 따라서 %f, %E 둘 중 하나를 선택

%%

% 기호 출력


위 표를 보고, "내 나이는 %d살이다."가 어떤 형태로 출력되는지 대충 감이 오시죠? %d는 부호 있는 10진 정수를 출력하기 위한 서식 문자로, 출력하기 전 %d 서식 문자와 대응하는 인수를 조립하는 과정을 먼저 거칩니다. 위 예제를 해석하자면 "저기 있는 데이터를 부호가 있는 10진수 정수형으로 출력해!" 라는 말입니다. 그런데 결과에선 0살이라고 출력되었죠? 이는 우리가 쓰인 서식문자 수와 인수의 수가 같지 않기 때문입니다. %d와 매치되는 인수가 없다는 말입니다. 예제를 조금 바꾸어 다시 컴파일 해보도록 합시다.

#include <stdio.h>

int main()
{
	printf("내 나이는 %d살이다. 나는 %d년에 태어났다.\n", 18, 1996);

	return 0;
}

결과:

내 나이는 18살이다. 나는 1996년에 태어났다.
계속하려면 아무 키나 누르십시오 . . .


이미 이해를 한 분도 계실테지만, 그렇지 않은 분들을 위하여 어떻게 이러한 결과가 출력이 되는지 설명을 해드리도록 하겠습니다. 위의 코드에서 쓰인 서식 문자는 printf의 인수와 하나의 쌍을 이루게 되는데, 첫번째 %d와 18 그리고 두번째 %d와 1996은 각각 한묶음으로 보셔야 합니다.

이렇게 서식문자와 인수의 조합을 거치고 나서 "내 나이는 18살이다. 나는 1996년에 태어났다."가 출력되는 것입니다. 이해 되셨으리라 생각하고, 포맷 코드(형변환 기호)에 대해 잠깐만 설명을 하도록 하겠습니다.


포맷 코드

기능

%8d

우측을 기준으로 8자리 출력

%-8d

좌측을 기준으로 8자리 출력

%+8d

수치 앞에 부호 출력

%08d

수치 앞에 공백을 0으로 채움

%+08d

부호 붙이고 공백을 0으로 채움

%8.3f

전체 8자리, 소수 3자리 출력

%-8.3f

좌측을 기준으로 전체 8자리, 소수 3자리 출력

%+8.3f

수치 앞에 부호 출력

%08.3f

수치 앞에 공백을 0으로 채움

%+08.3f

부호 붙이고 공백을 0으로 채움

(위의 포맷 코드는 예일 뿐이고, 굳이 8.3이라던가, 8이 아니더라도 됩니다.)


포맷 코드를 사용한 예제를 하나 살펴보도록 합시다. 아래 예제는 정수와 실수에 대하여 포맷 코드를 통해 출력 형태를 변환하여 출력한 것입니다.

#include <stdio.h>

int main()
{
	int num1 = 1234;
	float num2 = 12.34f;

	printf("%8d\n", num1);
	printf("%-8d\n", num1);
	printf("%+8d\n", num1);
	printf("%08d\n", num1);
	printf("%+08d\n", num1);
	printf("%8.1f\n", num2);
	printf("%-8.1f\n", num2);
	printf("%+8.1f\n", num2);
	printf("%08.1f\n", num2);
	printf("%+08.1f\n", num2);

	return 0;
}
결과:

    1234
1234
   +1234
00001234
+0001234
    12.3
12.3
   +12.3
000012.3
+00012.3
계속하려면 아무 키나 누르십시오 . . .


표를 보고 출력된 결과물과 비교를 해보시기 바랍니다. 포맷 코드에 따라 어떻게 출력되는지도 살펴보시고, 이것 말고도 C언어에서는 다양한 출력 양식을 제공하고 있습니다. 참고로 출력하려는 수치, 문자의 길이가 포맷코드보다 크면 포맷코드는 무시되니 참고로 알아두시기 바랍니다.
 

4. 입력 포맷(scan format) - scanf


scanf도 printf와 마찬가지로 뒤의 f가 format의 약자입니다. 앞서 만났던 printf 함수 처럼 서식 문자가 쓰이며, printf가 화면에 무언갈 출력하는 함수였다면 scanf 함수는 무언가를 입력받는 함수입니다. 사실은 scanf 함수를 제대로 이해하기 위해서는 포인터에 대한 이해가 필요합니다. 그러나 지금은 넘겨두고 아래 예제를 보며 기본적인 이해만 하도록 하겠습니다.

#include <stdio.h>

int main()
{
	int age;

	scanf("%d", &age);
	printf("나의 나이는 %d살 입니다.", age);

	return 0;
}

결과:

18
나의 나이는 18살 입니다.
계속하려면 아무 키나 누르십시오 . . .


차례차례 살펴보자면, 7행에서 scanf 함수가 호출되고 10진수 정수를 입력받습니다. 이렇게 해서 입력된 데이터는 age 변수에 들어갑니다. 그리고 8행에서 age의 값이 첫번째 서식문자 %d에 들어가는 것입니다.
 
참고로 scanf는 자료와 자료 사이는 공백, 탭, \n등으로 구분하며 서식제어 문자열을 사용하여 지정된 형식으로 입력받기도 합니다. 설명 안해둔게 있는데 scanf의 변수명 옆에 쓰인 &이 궁금하시나요?  이 기호를 레퍼런스라고 하며, 연산자 이기도 합니다. 이 연산자는 포인터와 관련이 있으므로 뒤에서 간단히 설명하도록 하겠습니다.

&를 변수의 이름앞에 붙이면 그 변수의 주소값을 얻게됩니다. scanf는 변수명을 필요로 하지 않습니다, 대신 값을 기억시킬 변수의 주소를 묻습니다. 그래서 scanf 함수 호출시 주소값을 전달해줘야만 합니다.


  scanf에서 C4996 에러가 발생합니다. 어떻게 해야하나요?

error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.


보시는 바와 같이 '이 함수 또는 변수는 안전하지 않을 수 있습니다. scanf_s를 대신 사용하는 것이 좋습니다. 사용 중단을 사용하지 않도록 설정하려면 _CRT_SECURE_NO_WARNINGS를 사용합니다. 자세한 내용은 온라인 도움말을 참조하세요.'란 내용을 담고 있습니다.


즉, '보안상의 이유로 scanf가 아닌 scanf_s를 사용해주세요!'라고 권장하는 것입니다. MSDN에서도 '일부 CRT(C Runtime Library) 및 표준 C++ 라이브러리 함수는 보안이 강화된 새 함수로 대체되어 더 이상 사용되지 않습니다.'와 같은 내용을 확인할 수 있었습니다. (버퍼 오버플로우나 버퍼 오버런 등과 같은 취약점에 대응하기 위하여 기존의 scanf에 버퍼의 크기를 넘겨줄 수 있도록 scanf_s를 사용함)


그렇다면 이 에러를 어떻게 해결할 수 있을까요? 가장 좋은 방법은 scanf 대신 scanf_s로 대체하여 사용하는 것이겠지만, 다른 방법으로도 이 에러를 무시하고 진행할 수 있습니다.


첫번째는 코드의 가장 위에 아래와 같은 전처리기 정의를 통해 해결할 수 있습니다.

#define _CRT_SECURE_NO_WARNINGS

_CRT_SECURE_NO_WARNINGS 대신 _CRT_SECURE_NO_DEPRECATE를 정의해 주어도 무방합니다. 그리고, 위의 define를 통한 해결법 대신 아래처럼 pragma를 통해 해결할 수도 있습니다.

#pragma warning(disable: 4996)

아니면 프로젝트를 생성하기 전에 나타나는 응용 프로그램 마법사에서 'SDL(Security Development Lifecycle) 검사'의 체크를 해제하고 프로젝트를 만드는 방법이 있습니다.



SDL 검사는 권장 옵션이기 때문에 기본적으로 체크가 되어있습니다. 이 검사는 '오류와 같은 추가 보안 관련 경고 및 추가 보안 코드 생성 기능'을 하며, 체크를 해제하면 C4996 에러가 경고 수준으로 바뀌며 무시하고 컴파일을 할 수 있게 됩니다.