728x90

예제 2.1 다시 살펴보기

앞에서 예제 2.1을 대략 살펴봤으니, 이번에는 프로그램의 각 라인을 보면서 더 자세히 들여다보는 것을 목표로 할 것입니다.


#include 지시자와 헤더 파일

#include <stdio.h>

이것이 프로그램의 첫 번째 라인입니다. #include <stdio.h>#include 라인이 적힌 그 위치에 stdio.h 파일의 전체 내용을 불러옵니다. 공통으로 사용하는 파일이 있을 때 include로 그 파일을 불러온다면 상당히 편리하게 작성할 수 있습니다.

#include 문은 C의 전처리기 지시자입니다. 일반적으로 C 컴파일러는 컴파일하기 전에 몇 가지 작업을 미리 실행하는데 이 작업을 전처리(preprocessor)라 합니다.

stdio.h 파일에는 입력 함수와 출력 함수에 대한 정보가 들어있으며, 이것을 호출함으로써 컴파일러가 이 함수들을 사용할 수 있게 해줍니다. stdio.h 파일과 같이 소스 코드 파일의 상단에 놓여서 정보를 제공해주는 파일을 헤더(header)라고 부릅니다.

보통 헤더 파일에는 컴파일러가 실행 프로그램을 만들 때 사용하는 정보가 들어있습니다. 예를 들면 상수를 정의하거나, 함수를 정의해서 그것들을 다른 파일에서 사용할 수 있게 해줍니다. 하지만 함수의 실제 코드는 헤더 파일이 아닌 라이브러리 파일에 적혀있으며, 이 라이브러리 파일은 컴파일된 코드로 구성되어있습니다. 프로그램을 컴파일할 때 필요한 라이브러리를 불러오는 일은 링커가 합니다.

ANSI/ISO C는 C 언어의 기능들을 컴파일러에 미리 정의해두지 않고, 헤더 안에 정의하고 그것을 불러와서 사용하는 방법을 채택했습니다. 그렇게 한 이유는 어떤 프로그램을 만들 때 모든 기능을 사용하는 것이 아니라서 필요한 기능만 불러올 수 있게 해놓은 것입니다. 이 때문에 함수를 사용할 때는 그 함수가 들어있는 헤더 파일을 찾아서 불러와야 합니다. C 컴파일러의 설명서를 보면, C 라이브러리 함수에 대한 설명이 담겨 있는데, 이러한 설명을 찾아보면 각각의 함수들이 어떤 헤더 파일을 요구하는지 알 수 있습니다.

예를 들어, printf()에 대한 설명을 찾아보면 stdio.h를 사용한다고 표시되어 있습니다. 간혹, 헤더 파일을 생략해도 프로그램이 정상적으로 컴파일될 수 있는데, 이것은 C99와 C11에서는 지원하지 않는 방법이니 사용하지 않는 것이 좋습니다. 함수를 사용할 때마다 표준으로 지정된 파일을 include 해서 사용하는 것이 번거로울 수 있지만, 이렇게 해주는 것이 버전별 호환성을 고려한 방법입니다.

입출력 기능이 C에 내장되어 있지 않은 이유

왜 표준은 입출력 함수와 같이 기본적인 것을 C 언어에 내장시키지 않았을까요? 이에 대한 대답은, 이 입출력 함수를 모든 프로그램에서 사용하는 것이 아닐 뿐만 아니라, C의 철학 중 하나가 불필요한 것을 제외하겠다는 것이기 때문입니다.

자원을 경제적으로 사용하겠다는 이 원칙때문에 임베디드 프로그래밍 분야에서 오늘날 C 언어가 인기를 누리고 있습니다. 덧붙여 말하면, #include에서 첫 글자인 # 기호는 컴파일하기 전에 이 라인을 전처리기로 먼저 처리하라고 지시하는 역할을 합니다.


main() 함수

int main(void)

main이라는 이름의 함수를 선언했습니다. C언어 프로그램의 시작점은 main() 함수입니다. 그렇기에 프로그램에서 사용하는 다른 함수들의 이름은 마음대로 지정해도 되지만, main() 함수는 반드시 존재해야 합니다. 그렇다면 괄호는 무엇일까요? 괄호는 main()이 함수라는 것을 알려줍니다. 지금으로써는 함수들이 C 프로그램을 구성하는 기본 모듈이라는 것만 기억하면 됩니다.

intmain() 함수의 리턴형입니다. 이것은 main() 함수가 리턴(반환)하는 값이 정수라는 것을 의미합니다. 그런데 값을 어디로 리턴한다는 것일까요? 결론부터 말하면 main() 함수는 값을 운영체제로 리턴합니다. 이에 대한 자세한 설명은 뒤에서 다시 나올 것입니다.

일반적으로 함수 이름 뒤에 오는 괄호에는 그 함수에 전달될 값을 적어줍니다. 위 코드는 함수에 전달할 값이 없는 프로그램이므로 비어있다는 뜻인 void를 괄호 안에 넣어주었습니다.

오래전에 작성된 코드를 보면 main() 함수를 다음과 같이 적어놓은 프로그램도 있습니다.

main()

C90 표준은 이 형식을 어쩔 수 없이 지원했으며, C99와 C11에서는 위와 같이 적는 것을 지원하지 않습니다. 따라서 컴파일러가 이 형식을 지원하더라도 사용하지 않는 것이 좋습니다.

void main()

몇몇 컴파일러들은 이것을 지원합니다. 하지만 어떤 표준에서도 이와 같은 코드를 찾아볼 수 없습니다. 그러므로 컴파일러는 이 형식을 굳이 지원할 필요가 없으며, 실제로 몇몇 컴파일러들은 지원하지 않습니다. 다시 강조하지만, 표준 형식을 지키는 것이 중요합니다.


주석

/* 주석 */

프로그램에서 /**/로 둘러싸인 부분을 주석이라 부릅니다. 주석의 장점은 어디에서나 작성할 수 있다는 것이며, 컴파일러는 주석을 여는 기호인 /*과 닫는 기호인 */ 사이에 적힌 모든 문자를 무시합니다. 다음은 주석의 몇 가지 예입니다.

/* 이것이 주석입니다. */

/* 이 주석은 두 줄에
   걸쳐 있습니다. */

/*
   주석을 이렇게 넣을 수도 있습니다.
*/

/* 이것은 닫는 기호가 없으므로 유효하지 않습니다.

C99에서 새로운 주석 형식이 추가되었습니다. 이 형식은 C++와 Java에서 사용되었으며, 한 라인에 한정된 주석을 만들기 위해 // 기호를 사용합니다.

// 한 라인에 한정된 주석을 만들 수 있습니다.

int num;     // 이렇게 붙일 수도 있습니다.

이 주석 형식은 라인의 끝이 주석의 끝이 되기 때문에, 주석을 시작하는 위치에만 주석 기호를 적어주면 된다는 장점이 있습니다. 새로운 주석 형식은 옛날 방식이 일으켰던 문제점 때문에 새로 생겼습니다. 다음과 같은 코드가 있다고 해봅시다.

1.  /*
2.     주석
3.  */
4.  num1 = 100;
5.  num2 = 200;
6.  /* 또 다른 주석 */

그런데 네 번째 라인(num1 = 100;)을 지우려다 잘못해서 세 번째 라인(*/)도 같이 지웠다면 코드는 다음과 같아집니다.

1.  /*
2.     주석
3.  num2 = 200;
4.  /* 또 다른 주석 */

이제 컴파일러는 첫 번째 라인의 /*과 네 번째 라인의 끝에 있는 */를 한 쌍의 주석으로 인식해서 네 라인 모두 하나의 주석으로 처리해버립니다. 하지만 새로운 주석 형식 //은 한 라인을 넘을 수 없으므로 이와 같은 "사라지는 코드"를 만들지 않습니다.

몇몇 컴파일러는 이 새로운 주석 형식을 지원하지 않습니다. 또 몇몇 컴파일러에서는 C99 또는 C11의 기능을 사용하기 위해 컴파일러의 설정을 변경해야 할 수도 있습니다.


중괄호, 몸체, 블록

{
...
}

예제 2.1에서 중괄호는 main() 함수의 시작과 끝을 나타낸다 했습니다. 일반적으로, C의 모든 함수는 중괄호를 이용해서 함수의 시작과 끝을 나타냅니다. 함수에서 중괄호는 빠지면 안 되는 필수적인 요소입니다.


선언문

int num;

위와 같은 라인을 선언문이라 부르며, 선언문은 C의 가장 중요한 특징 중 하나입니다. 이 예제는 먼저, 프로그램의 어딘가에서 데이터형이 intnum이라는 변수를 사용할 것이라고 알려주는 역할을 합니다. 컴파일러는 이 선언문을 발견하면 num 변수를 저장할 적당한 메모리 공간을 할당합니다. 또, 문장의 끝에 있는 세미콜론(;)은 그 문장이 끝났다는 것을 의미합니다.

int라는 단어는 C의 기본 데이터형 중 하나를 나타내는 키워드입니다. 키워드는 그 언어를 표시하는 데 사용하는 단어들이기 때문에 다른 곳에서는 사용할 수 없습니다. 예를 들면 int를 함수나 변수의 이름으로 지정하는 것은 불가능합니다.

위의 예제에서 num이라는 단어는 식별자입니다. 실별자는 변수, 함수 등의 개체를 식별하기 위해 사용자가 직접붙인 이름입니다. C에서 모든 변수는 사용하기 전에 미리 선언해야 하며, 이것은 컴파일러에 각 변수의 이름과 데이터형에 대해 미리 알려주는 역할을 합니다. 전통적으로 C는 블록의 시작 위치에 선언문을 작성할 것을 요구했습니다. 따라서 초기의 main() 함수는 다음과 같은 모습이었습니다.

int main() {
    int num1;
    int num2;

    num1 = 100;
    num2 = 200;
}

하지만, C99와 C11에서는 C++의 관행을 받아들여서 블록 안 어디에서나 변수를 선언할 수 있어졌습니다. 하지만 변수를 사용하기 전에 선언해야 한다는 것은 변하지 않았습니다.

int main() {
    int num1;
    num1 = 100;

    int num2;
    num2 = 200;
}

이 시점에서 몇 가지 궁금증이 생길 수 있습니다. 먼저 데이터형이란 무엇인지, 변수의 이름은 무엇으로 해야 하는지 그리고 왜 굳이 변수를 선언해야 하는지 이제 이 의문에 대한 답을 찾아봅시다.

데이터형
C는 정수, 문자, 부동 소수점 수와 같은 다양한 데이터를 처리합니다. 변수의 데이터에 따라 데이터형을 알맞게 작성해야 컴퓨터가 그 데이터를 제대로 해석할 수 있습니다.

이름 선택
변수의 이름(식별자)은 그 변수가 하는 일이 무엇인지 한 번에 알 수 있도록 적는 것이 좋습니다. 만약 그것이 힘들다면 주석을 이용해서 설명을 적어주어야 나중에 혼란이 오는 것을 방지할 수 있습니다.

C99와 C11은 식별자의 이름 길이에 딱히 제한을 두지 않습니다만 컴파일러의 경우에는 63자까지, 외부 식별자의 경우 31자까지 유효한 것으로 간주할 필요가 있습니다. 각각 31자와 6자를 요구했던 C90과 비교하면 이것은 상당히 증가한 것입니다. 그보다 더 오래된 컴파일러들은 최대 8자까지 지원합니다. 실제로는, 이보다 더 많이 작성해도 에러가 나지는 않지만, 컴파일러가 그 변수를 인식하고 있는지는 알 수 없습니다. 예를 들면 63자로 이루어진 두 식별자가 한 글자만 다르다면 컴파일러는 이들을 서로 다른 식별자로 인식하지만 64자로 이루어진 두 식별자가 있고 마지막 글자 하나만 다르다면 컴파일러는 이 두 식별자를 구별할 수도 있고, 구별하지 못할 수도 있습니다.

변수의 이름에는 대문자, 소문자, 숫자, 밑줄(_)을 임의로 조합해서 사용할 수 있으며, 첫 문자는 반드시 영문자나 밑줄을 사용해야 합니다.

적절한 이름 

부적절한 이름 

 wing

@#$T% 

 num1

1num

can_nse

can use 

운영체제와 C 라이브러리는 종종 밑줄 문자로 시작하는 식별자를 사용하기 때문에 이와 같은 형태의 식별자는 작성하지 않는 것이 좋습니다. 이미 정의된 식별자를 사용한 경우 Syntax Error(문법 에러)는 일어나지 않지만, 충돌 현상이 일어날 수 있습니다.

C는 대소문자를 구별해서 num, Num, NUM을 각각 다른 이름으로 인식합니다.

C99부터 Universal Character Names 메커니즘에 의해 확장문자를 사용할 수 있습니다. 이것은 알파벳이 아닌 다른 문자들도 식별자로 사용할 수 있게 해줍니다.

변수를 선언하는 이유
몇몇 오래된 언어들은 변수를 선언하지 않고 바로 사용할 수도 있습니다. 이렇게 쉬운 접근 방법이 있는데 왜 C는 그렇게 하지 않았을까요? 다음과 같은 이유가 있기 때문입니다.

1. 변수의 이름을 의미가 있도록 작성한 경우 모든 변수를 한곳에 모아놓으면 그 변수가 하는 일에 대해 더 쉽게 파악할 수 있습니다. 변수의 이름으로 의미 전달이 힘들다면 그 변수가 하는 일을 주석으로 설명하는 것이 좋습니다.

2. 변수를 선언하는 것은 변수의 이름을 잘못 작성하는 것을 방지해줍니다. 예를 들면, 선언이 필요 없는 언어에서 다음과 같은 문장을 작성했다고 가정해봅시다.

num1 = 10;

그리고 프로그램의 어딘가에서 다음과 같이 입력했다고 가정합시다.

num2 = numl * 100;

잘못해서 숫자 1을 영어 l로 잘못 입력했습니다. 선언이 필요 없는 언어는 numl이라는 새로운 변수를 만들고, 그 변수를 사용합니다. 결국, num2에는 의도한 값이 들어가지 않을 것이고, 프로그래머는 버그를 잡기 위해 많은 시간을 낭비할 것입니다. C에서는 (이처럼 비슷한 두 개의 변수 이름을 선언하지 않는다면) 이런 버그는 방생하지 않습니다. 왜냐하면, 선언되지 않은 변수 numl을 발견하는 즉시 컴파일러가 에러 메시지를 내보내기 때문입니다.

3. 변수를 선언하는 가장 큰 이유는 변수를 선언하지 않으면 컴파일되지 않습니다.

변수를 선언할 때, 어디에 작성해야 할까요? 앞에서 말했듯이 C99 이전에는 선언을 블록의 시작 위치에 둘 것을 요구했으며, 이 관행을 따르는 것이 좋습니다. 변수 선언을 한 곳에 모아두면 프로그램이 어떤 일을 하는지 이해하기가 더 쉬워집니다. 물론 C99가 지원하는 것처럼 선언을 여기저기 흩뿌려 작성하는 것이 더 좋을 때도 있습니다. 변수에 값을 넣기 바로 전에 그 변수를 선언하면 변수에 값을 넣는 것을 잊지 않을 수 있습니다. 하지만 아직 C99 규칙을 지원하는 컴파일러가 많이 없다는 것을 생각해야 합니다.


대입문

num = 1;

이와 같은 라인을 대입문이라 합니다. 위 코드는 "변수 num1을 대입하라"는 뜻이며, 이보다 앞에 있는 int num; 라인이 변수 num을 생성하면 이 대입문은 그 변수에 데이터를 대입합니다. 원한다면 나중에 num에 다른 값을 대입할 수도 있으며, 이런 특징 때문에 변수(變數)라 불리는 것입니다. 또한, 대입문은 오른쪽에 있는 값을 왼쪽의 변수에 대입하며, 문장은 세미콜론(;)으로 끝나야 합니다.


printf() 함수

printf("안녕");

printf("세상! \n");

printf("num에는 %d이 저장되어 있습니다. \n", num);

이 라인들은 모두 printf()라는 C의 표준 함수를 사용하고 있으며, 괄호는 printf()가 함수라는 것을 알려주는 역할을 합니다. 괄호 속에 있는 문장들은 main() 함수에서 printf() 함수로 전달되는 정보인데, 예를 들면, 첫 번째 라인은 main() 함수가 "안녕"이라는 정보를 printf() 함수에 전달해줍니다. 이러한 정보를 전달인자(argument) 또는 함수의 실 전달인자(actual argument)라고 합니다.  printf() 함수가 전달인자를 입력받아서 하는 일은 큰따옴표 사이에 들어있는 문장을 화면에 출력하는 것입니다.

첫 번째 printf() 라인은 C가 어떻게 함수를 호출(실행)하는지 보여주는 좋은 예입니다. 함수 이름을 적어주고, 괄호 안에 전달인자를 넣어주면, 프로그램이 라인에 도달했을 때 그 이름을 가진 함수(이 경우에는 printf() 함수)로 제어가 넘어가고, 그 호출된 함수가 일을 끝내면 다시 원래의 함수(이 경우에는 main() 함수)로 넘어갑니다.

두 번째 printf() 라인은 \n 기호를 포함하고 있지만, 이것들이 출력되지는 않았습니다. 어디로 사라졌을까요? \n 기호는 printf()의 측면에서 볼 때, "다음 라인의 처음 칸에서 시작하라"는 의미입니다. 다시 말하면, \n(개행 문자)는 키보드의 [Enter] 키와 같은 기능을 수행합니다. 그렇다면 왜 printf()의 전달인자를 입력할 때 [Enter] 키를 사용하지 않았을까요? 소스 코드 작성 중에 [Enter] 키를 누르게 되면, 에디터는 작업 중인 현재 라인을 끝내고 새로운 라인을 시작하라는 것으로 인식하기 때문입니다. 반면에 개행 문자는 소스 코드에 영향을 주지 않고, 프로그램의 출력이 표시되는 방식에 영향을 줍니다.

개행문자는 이스케이프 시퀀스(escape sequence)의 한 예입니다. 이스케이프 시퀀스는 파이핑하기 어렵거나 불가능한 문자를 나타내는 데 사용하며, 언제나 백슬래스(\) 문자로 시작합니다. 이 주제에 대해서는 다음에 다시 자세하게 설명할 것입니다.

그렇다면 왜 세 개의 printf()문이 있는데, 두 라인만 출력됐을까요? 첫 번째 printf()문에는 개행 문자가 들어있지 않아서 줄 개행을 하지 않은 상태로 두 번째 printf()문이 실행되었기 때문입니다.

마지막 printf()문에 있는 %d에 무슨 일이 일어났길래 1이 출력됐을까요? 앞에서 보았듯이 이 라인의 출력은 다음과 같습니다.

num에는 1이 저장되어 있습니다.

출력을 보면 기호 %d1로 대체되었는데, 1은 변수 num의 값입니다. 즉, %d는 변수의 값이 출력될 위치를 지정하는 깃발의 역할을 합니다.

% 기호는 %가 있는 위치에 변수를 출력하라는 것을 의미하며, d는 그 변수를 10진수의 정수로 출력하라고 알립니다. printf() 함수는 16진수 정수와 부동소수점이 있는 수를 포함하여 변수의 출력 포맷에 대한 몇 가지 형식을 제공하며, printf()의 f는 포맷팅(formatting) 출력 함수임을 뜻합니다.


리턴문

return 0;

int main(void)에서 intmain() 함수가 하나의 정숫값을 리턴한다는 것을 나타냅니다. C언어는 이처럼 값을 리턴하는 함수에서는 리턴문을 사용합니다. 리턴문을 작성할 때는 return 키워드 뒤에 리턴값을 적어주고,  그 뒤에 세미콜론(;)을 적어줘야 합니다. main()에서 리턴문을 적지 않았다라도 자동으로 0을 리턴해주기 때문에 main()문에 있는 리턴문은 생략할 수 있지만, 다른 함수는 자동으로 리턴해주지 않아서 생략하면 안 됩니다. main()에서의 리턴문은 Unix 등의 운영체제에서 실질적인 용도가 있습니다.


728x90
728x90