본문 바로가기

Programming/VC++

[C] C언어 강좌


원본 http://cafe.naver.com/csknouackr/6
 

C 언어 강좌


 총 3,500라인(약100여page)에 달하는 본 강좌는  C를 처음 시작하시려는 분과 포기하셨다가 다시 시작하시려는 새내기 여러분들을 위하여,    *** C에 대한 전반적인 사항을 단기간에 둘러볼 수 있도록 ***

  #개념과 예제 설명 위주로#  진행한 강좌입니다.  '언어 강좌'란의 다른 강좌들과 함께 본 강좌를 따라가시다 보면 어느새 C에 대해 자신감을 가지게 될 것입니다.


 특히, C에서 가장 난해하다고 인정되는 포인터에 관한 내용을 집중적으로 설명하였습니다. 필요하신 분은 그 부분만 참조하셔도 많은 도움이 될 것입니다.



강좌의 분량이 많으므로 캡춰를 해서 공부하시기를 바랍니다.

부디 본 강좌와 함께 C언어에 대한 자신감을 성취하시기를 바랍니다.


 #. 참고 서적 / 추천 서적

   **  박승제 저 'BASIC에서 C로' - 1989, 월간 마이크로소프트웨어(강의기본교재)

가. 황희융 편 'C언어 기초 + a' - 1986, 교학사 (입문서)

나. 임인건 저 '터보 C 정복' - 1990, 가남사 (입문/참고서)

다. '안녕하세요 Turbo C' - 1993, 정보문화사 (입문서)

라. 큰사람 저 '무서워요 C' - 1994, 도서출판 하늘소 (기술서)

(입문을 거친후, 실질적인 프로그램 작성의 요령과 여러가지 테크닉을 배우는데 아주 유용한 책이다)


  (참고로, C입문서는 1권쯤 가지고 있는 것이 좋습니다. 통신상의 강좌에서 보다 궁금한 것들은 더 자세히 살펴보는 것이 좋지요.

 또, 각 컴파일러의 레퍼런스 가이드란게 있는데, C 프로그래머에게는 필수입니다.  서점에 가면 아주 많으니, 자신의 구미에 맞는 것을 골라 보시기 바랍니다.)


제 1장 - C의 구조와 변수

  요즈음은 윈도우즈의 시대라고들 한다. Win95가 발표되고 아래 한글 3.0B가 대중화 되어가는 현 시점에서는 아마도 컴 사용환경의 80%이상이 Windows 기반일 것이다. 컴퓨터 언어를 이제 시작하고자 하는 사람들이나  기존의 프로그래머들도 이제는 Windows 프로그래밍으로 반드시 전환해야 하는 중요한 시기이다.

  하지만 그 Windows에서 돌아가는 프로그램을 작성하려고 해도 C언어가 기반이 되어야 한다. 그래야 훨씬 수월하다. Windows프로그래밍 공부의 시작요령은 마지막 회에서 언급하고자 한다.

  물론, 일반인들은 언어를 배울 필요가 없다. 하기야 이 글을 읽는 여러분들 중의 대부분은 컴퓨터에 대한 지적호기심을 가지고 있는 사람들과 전산관련분야로 진출하려는 사람들일 것이다. 모처럼 결심을 하고 컴퓨터 언어라는 전혀 새로운 언어 세계에 도전을 결심하고 남들이 모두들 배우라는 C언어라는 걸물을 만나게 되었을 것이다.

  그렇다! C언어는 분명 최고의 언어이다. 프로그램 개발에 최적을 효율성을 제공하고, C++, Assembly등 다른 언어로의 전환도 편리하며, 앞으로 표준이 될 윈도우즈 응용 프로그램도 C를 모르고서야 도무지 작성하기가  난감하다는 것이다.

  바로 여러분들은 최상의 선택을 하였으나, 앞을 가로막고 있는 난관이 보통이 아닐 것이다.

  C언어는 여타 언어에 비해 많은 장점과 다양한 기능을 가지고 있어 대부분의 프로그램들이 바로 이 C언어에 의해 코딩되고 있다.


  요즘의 주요 C 컴파일러들의 대표적인 특징으로는

가. 수행속도가 빠르며  사용이 편리한  통합환경에 의하여 최적 프로그램개발환경을 구축할 수가 있다.

나. 명령행 처리도 가능하여, 빠르고 간편한 개발이 가능하다.

다. ANSI 표준에 근거하고 있으며, 방대한 양의 자체 함수를 가지고 있다.

라. 6종류의 메모리 모델을 지원함으로서 큰 규모의 프로그램도 작성이 가능하다.

마. 어셈블리와 혼용하여 쓸 수 있으며, 다른 고급언어와의  연결(Link)도 가능하다.


  등등 다른 컴퓨터 언어에 비하여 많은 장점들을 가지고 있어 많은 사람들이 C언어로 프로그래밍을  하고 있고, 또 많이들 배우고 있다. 이러한  특성들이 전문 프로그래머에게는  다양한 표현방식을 제공하여 편리하겠지만 초보자의 경우에는 반대로 모든 특성을 이해하는데 많은 혼란을 가져올 수 있는 요소가 된다. 그렇기 때문에 C가 배우기 어려운 언어라는  말이 나오기 마련이다. 아울러 포기하는 사람도 많다.


  이 강좌의 목적은 이제 C를 배울려는 사람들과 과거에 C를 공부하다가 중도에 포기한 경험이 있는 이들로 하여금 자연스럽게 C의 세계로 입문할 수 있도록 하기 위한 것이다.  이 강좌를 읽다보면 모르는 사이에 저절로 C로의 입문이 가능해 지리라 믿는다.  이 글의 예제들은 모든 컴파일러가 지원을 하는 표준 C이므로 컴파일러가 무엇인지는  상관을 하지 않아도 된다.


1. C의 구조

  C에서는 프로그램의 형식이 있어 여기에 맞게 기술해야 한다. 형식이라고 해서 그리 대단한 것은 아니다. 여기 예제 프로그램이 있다.


/*  -- Sample --  */

 main()

 {

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

 } 

                     

  함수가 무엇인지는 여러분들 모두가 잘 알고 있을 것이다. 컴언어에서 함수란 어떠한 일을 처리하는 작은 프로그램이라고 보면 된다.

  C프로그램은 기본적으로 함수들의 집합으로 구성되어 있다.  그러므로 위의 프로그램은 main이라는 이름의 함수와  printf라는 이름의 함수로 구성되어 있음을 알 수가 있다. main()함수는 다른 자질구레한 기능을 하는 함수(여기서는 printf()함수)들을 모두 포함을 하는 가장 기본적인 함수이므로 프로그램에 반드시 포함이 되어야 하며, 프로그램의 내용은 { } 기호로 묶어서 시작과 끝을 나타낸다.

  C에서는 영어의 대문자와 소문자를 구별하므로 위의 예제를 반드시 소문자로 입력하여야 한다(abc와 ABC가 서로 다르게 인식된다).  C에서는 변수명을 제외한 프로그램의 대부분의 명령어를 소문자로 기술한다.

  C에서는 주석문을 /*과 */로 묶어서 표현한다. 주석문은 프로그램의 실행과는 무관하며, 적절히 붙여두면  후에  그 프로그램을 이해하는데 도움이 될 것이다.

  보통 프로그램의 본체는 관례적으로 { 다음에 서너칸 쯤 우측으로 물러나서 기술한다. 이러한 것을 indent(단짓기)라고 한다.

  베이직의 PRINT문과 유사한 기능을 하는 것이 출력의 기능을 담당하는 printf()함수이다. 화면에 출력하고 싶은 문자는 printf()함수의 괄호안에 "로 묶어서 나타낸다. C에서는 문장의  맨끝에 반드시  ; 을 붙여야 한다는 점을 명심해야 한다. 또한 main() 다음에는 ; 를 붙이지 않는다는 것도 명심하기 바란다. \n(개행문자라고 한다)은 행을 바꾸라는 의미이다.

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

        --------------## -> 출력후 줄을 바꿈(개행문자)

    화면에 그대로 출력.


우선 예제를 통해 알 수 있는 C 프로그램의 기초적인 사항은,

1) C에는 행번호가 없다.

2) 대문자와 소문자가 구별되며, 주로 소문자에 기초하여 작성된다.

3) C는 main()으로 시작한다.

4) 프로그램(함수)의 본체는 { 와 }로 묶는다.

5) 한 문장은 반드시 ; 로 끝난다.(초보자가 실수를 잘한다)

6) 주석문은 /*  */로 묶어 표시한다.


2. C의 변수

 일반적으로 상수란 어떠한 정해진 값을 말하고, 변수란 어떠한 값을 보존하는 상자에 비유할 수가 있는데, 그 상자를 구분하기 위해서 붙이는 이름이 변수명이다.


 C에서 변수명이 갖추어야 할 요건은 다음과 같다.

1) 변수명은 보통 8문자까지 인식한다.

2) 변수명의 첫문자는 영문자(A-Z,a-z:대소문자가 엄격히 구분된다)나  _ 로 시작하며, 그 이후의 문자는 영문자나 숫자, 밑줄 어느 것이라도 상관없다(밑줄로 시작하는 변수는 주로 시스템 프로그램에서  사용하므로 실제의 경우에는 피하는 것이 관례이다).  

3ds : X ,  Son <> son

3) 명령어나 기존의 함수명은 변수명으로 사용할 수는 없으나 포함할 수는 있다.

for : X        fortune : O

4) 프로그램에서 사용할 변수는 반드시 프로그램의 첫머리에서 그 형:Type을 정의해야 한다.

int a, float b, char c ...


  위의 사항 중 초보자들이 가장 번거롭게 생각하는 것이 바로 4번이다.


  습관이 되면 불편하지 않게 되며, 오히려  프로그램의 분석에 커다란 도움을 주는 요소이며 구조적 사고를 하는데 도움을 주는 요소임을 깨달아야 한다.

 그러면 다음의 예제를 통해 변수의 형을 정의하는 방법을 익혀 보자.

<예제1> 밑변이 21, 높이가 4인 삼각형의 넓이를 구하는 프로그램 작성.

1:   main()

2:   {

3:      int a,b;

4:      float c;

5:      a = 21;

6:      b = 4;

7:      c = a*b/2.0;

8:      printf("넓이 = %f\n", c);

9:   }


  3행과 4행에서 변수 a와 b를 정수형으로, c는 실수형으로 선언한 것을 볼 수 있다. int는 그 다음의 변수를 정수형으로, float는 실수형으로 선언해 주는 예약어이다.여기서 변수 c를 실수형으로 선언한 이유는 다음과 같다. 위 프로그램의 결과는 나눗셈(/)이 있으므로 소수부분이 발생하는데, 정수형의 변수에 소숫점을 포함하는 실수형의 자료를 기억시키면  소수 부분이 절삭된다. 그러므로 계산의 결과를 고려하여  적절한 형(Type)을 선언하여야 한다.

  C에서는 변수의 선언과 동시에 초기값을 부여할 수가 있다. 3 - 7행을 좀 더 간단히 하면 다음과 같다.

int a=21, b=4;

float c;

c=a*b/2.0;

  C에서는 연산결과가 변수나 상수의 형에 의해 달라지게 된다.

정수 (연산) 정수 = 정수   

정수 (연산) 실수 = 실수

실수 (연산) 실수 = 실수


  그렇다면 3/4*4의 연산 결과는 얼마겠는가?

  3이라고 생각하겠지만 정답은 0이다. 3/4가  정수와  정수의  연산이므로 소수점 이하가 절삭되어 0이 된다. 이 0에다 4를 곱했으니 결과가  0이 되는 것이다.

  그러면 3/4.0*4의 결과는 얼마이겠는가? 대다수는 3이라고  생각하겠지만 여기서는 3이 아니라 3.0이 된다. 3/4.0이 정수와 실수의 연산이므로 연산 결과는 실수로 보존되어 0.75가 된다. 다음의 0.75 * 4 역시  실수와 정수의 연산이므로 결과는 실수가 되어 3.0이 된다.  3은 정수이고 3.0은 실수이므로 3은 올바른 답이 아니다. C에서는 3과 3.0을 엄격하게 구분하고 있으므로 혼돈하는 실수를 범하지 말기를 바란다. 이러한 실수는 프로그램의 실행결과에 심각한 영향을 미칠수 있기 때문에 항상 주의해야 한다.

  8행에서 " 안의 문자열을 제어 문자열이라고 하며, 제어 문자열은 인쇄가능 문자열과 기능 문자열고 구분된다.  인쇄가능 문자열은 화면에  그대로 출력되며, 기능 문자는 화면에 출력되는 대신에 특수한 처리를 한다. 이를테면 printf()함수 안의 \n 따위가 기능문자에 속한다.

|------| <-- 서로 짝이 된다

printf("넓이 = %f \n",c);

=======++=**  #

== : 화면에 그대로 출력된다(인쇄가능 문자)

++ : 인수(변수)를 10진 실수로 출력한다(형변환기호:기능문자)

** : 출력후에 줄을 바꾼다(기능문자)

 , : 제어문자열과 인수(변수)와의 구분

 # : 변수

  위의 도해에서 보듯이 %f는  제어문자열 다음의 인수와  한 쌍을 이룬다.

그러므로 인수가 3개이면 변환 기호도 3개가 기술되어야 한다.  정수형 인수를 출력할 경우에는 %f대신 %d를 기술한다.  printf()는 인수를 제어 문자열의 형식(format)대로 변환하여  출력해 주는 함수이다. 그러면 다른 예제를 통해 형변환 기호의 기능을 좀더 살펴보자.

 <예제2> 10 *  50 =  500

         100 / 3.0 = 33.3 을 출력하는 프로그램 작성.

 1:   main()

 2:   {

 3:      int a=10,b=50,c=100;

 4:      float d=3.0;

 5:      int sum1; float sum2; <- 두 문장이 한줄에 들어가도 된다

 6:      sum1 = a * b;              (;로 구분되어 있음에 유의)

 7:      sum2 = c / d;

 8:      printf("%3d * %3d = %4d\n",a,b,sum1);

 9:      printf("%3d / %3.1f = %4.1f\n",c,d,sum2);

10:   }


 9행의 의미를 분석하면,

*       #     @     * #  @   -->기능문자와 인수의 순서가  일치한다.

printf("%3d / %3.1f = %4.1f\n",c,d,sum2);

###===@@@@@===$$$$$--------

### : 정수를 세자리 넓이로 출력

@@@: 실수를 총 세자리 넓이로, 소수점이하 한 자리로 출력

 $$$ : 실수를 네자리 넓이로, 소수이하를 한자리로 출력

=== : 화면에 그대로 출력(공백포함)

--- : 인수( , 로 분리)


  위의 예에서 인수 a에 대한 %3d는 3자리를 확보한 후에 우측을  기준으로 출력하라는  의미로서 좌측의 남는 한칸은 공백으로 채워진다. ( ' 10'  )  좌측을 기준으로 출력할 경우는 %-3d와 같이 수치앞에  -기호를 붙인다. 자리수를 실수로 지정해서 출력할 경우에는 %7.3f처럼 기술한다. 7은 전체 자리수이며, 3은 소수 이하 자리수이다. 만약 소수 이하가 3자리를 초과할 경우에는 반올림이 되어 출력된다. 이 경우에도 우측을 기준으로 출력되며 좌측을 기준으로 출력할 경우에는  %-7.3f처럼 기술하면 된다.  만약 인수(변수)의 값이 변환기호의 자리수를 초과할 경우에는  변환 기호는 무시되어 인수의 자리수 길이만큼 출력된다. %7.3f로 3.14159를 출력하면 다음과 같다.

'^^3.142' (^은 공백을 나타냄) -->소수 네째자리에서 반올림된다

  printf()함수의 인수란에서 수식을 쓸 수도 있다. 즉, 8행을 printf("%3d * %3d = %4d\n",a,b,a*b);와 같이 기술해도 된다. 이 때는 sum1을 선언하지 않아도 된다(당연한 얘기!)

  이번장에서는 형변환 기호중 %d와 %f에 대해 알아 보았다.  하지만  다른 것도 있는데 변수의 형에 대해 좀더 깊이 공부하는 2장에서 언급하겠다.  수학에서는 소숫점을 포함하지 않는 모든 수가 정수이지만,  컴퓨터에서는 정수의 범위가 정해져 있다. 16비트(1워드)를 기본 단위로 하는 컴퓨터(일반PC)에서  -32,768 -- 32,767사이중 소수점을 포함하지 않는 수치만을 정수로 취급한다.  32비트를 1워드로 하는 중형급 이상의 컴에서는 정수의 범위가 -2,147,483,648 -- 2,147,483,647사이의 수치로 확장된다.

  컴퓨터에서 변수형에 따라 범위가 제한되어 있는 이유와 원리는  다음 장에서  알아보기로 하자.


3. 문자형 변수

  C에서는 프로그램의 선두에서 변수의 형을 정의하는데에 따라 ab가  수치변수가 되기도 하고 문자변수가 되기도 한다.  문자변수 중에서도 단일 문자를 기억하는 변수와 문자열을 기억하는 변수의 선언방식이 조금 다르다.

<예제3> '우리는 지금 C를 배우고 있어요.'을 출력하는 프로그램을 문자변수를 이용하여 작성하라. 변수 s에 '우리는 지금'을, 변수 c에 'C'를  기억시키고, '를 배우고 있어요.'는 문자열 상수로서 출력하라.

1:    main()

2:    {

3:       char c, *s;

4:       s = "우리는 지금";      /*  문자열은 "로 둘러싸고, */

5:       c = 'C';                /* 단일문자는 '로 둘러싼다 */

6:       printf("%s %c %s\n",s,c,"를 배우고 있어요.");

7:    }


  위에서 편의상 한글을 사용하였는데 한글의 한 문자는  영문의 두 문자에 해당한다. 그러므로 5행을 c = '씨'라고 표현해서는 안된다.

  3행의 char은 다음의 변수가 문자형 변수임을 선언하는 예약어이다. 문자형변수는 단 1개의 문자만을 기억하는 것을 의미한다. 만약 문자열을 기억/시키고 싶으면 변수명 앞에 *를 붙여서 *s처럼 선언하면 된다. *는 포인터를  의미하는데, 우선은 문자열 변수는  변수명 앞에 *를 붙여서 선언한다라고 알고 있자.  * 는 선언할 때에만 쓰이고, 값을 대입하거나 출력하는 경우에는 쓰지 않는다(쓰면 안된다는 것은 아니고,  우선은 쓰지 않는  것으로 하자:6장에서 배울 것이다).

  4행은 문자열을 대입하는 보기이며, 5행은 문자를 대입하는 예이다. 그리고 6행에서 문자열 자체나 수식자체도 인수가 될 수 있다.  C에서는 변수의 형 선언과 함께  초기값을 부여할 수도  있다고 하였으므로, 3 -- 5행을 한 문장으로 표현하면 다음과 같다.

char c = 'C', *s= "우리는 지금";

  물론 위의 프로그램을 printf("우리는 지금 C를 배우고 있어요.\n");처럼 1행으로도 처리가 가능하다.


4. 포맷 코드(형변환 기호)

  포맷 코드, 즉 출력 양식을 의미한다. C에서는 다양한 출력 양식을  제공한다. 밑의 표에서 code앞에 printf를 붙여 반드시 실행해 보기를 바란다.

<표1> 포맷 코드        a=1234, b=3.14159, c='A', s="ABCD" (=:공백)

code

출력형태

설                 명

("%d",a)

1234

정수의 출력

("%8d",a)

====1234

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

("%-8d",a)

1234====

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

("%+8d",a)

===+1234

수치앞에 부호

("%08d",a)

00001234

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

("%+08d",a)

+0001234

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

("%f",b)

3.14159

실수의 출력

("%8.3f",b)

===3.142

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

("%-8.3f",b)

3.142===

좌측기준 (소수이하 반올림)

("%+8.3f",b)

==+3.142

부호를 붙여서 출력

("%08.3f",b)

0003.142

앞의 공백을 0으로 채움

("%+08.3f",b)

+003.142

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

("%c",c)

A

문자 출력

("%8c",c)

=======A

우측을 기준으로

("%-8c",c)

A=======

좌측을 기준으로

("%s",s)

ABCD

문자열의 출력

("%8s",s)

====ABCD

우측을 기준으로

("%-8s",s)

ABCD====

좌측을 기준으로

# 출력하려는 수치, 문자의 길이가 포맷코드보다 크면  포맷코드는 무시됨

5.Escape Sequence(Esc문자열)

  이 문장은 어째 이상하지 않은가?

printf("멀티미디어 - 덩달이 왈 "아니 얘가 뭘 디빈디어?"\n");

  그러면 "는 어떻게 표현을 할까? 문장내의 "를 표현할때는 "앞에 \을  삽입하면 된다. printf("He said, \"I love you.\"\n");와 같이.

  위에서 보는 것과 같이 \로 시작하는 문자의 나열을 ESC 문자열이라고 한다. 이 것은 \"처럼 \다음 문자가 화면에 그대로 출력되는 경우가 있고, \n 처럼 화면에 출력되는 대신에 줄을 바꾸는 역할도 있다.


<표2> 이스케이프 시퀀스

e.s

명    칭

기             능

ASC코드

\a

Beep

'삑'소리를 낸다

0x07

\n

Line feed

줄을 바꾼다

0x0a

\t

TAB

다음 TAB위치로 이동

0x09

\b

Back Space

인쇄반대방향으로 1칸 후진

0x08

\r

Carrige Return

Return(Enter) 역할

0x0d

\f

Form feed

출력용지를 1페이지 전진

0x0c

\\

Backslash

\ 출력

 

\'

Apostrophe

' 출력

 

\"

Quote

" 출력

 

\0

Null

아무런 동작도 하지않음

0x00

%%

Percent

% 출력

 

# 0x는 8진수를 기술할 때 앞에 붙여주는 기호이다.

# 또한 \뒤에 직접 8진수인 3자리 수치를 입력함으로써 아스키문자를 출력할 수 있다. (\x00a는 \n과 같다)

<예제4> 다음 프로그램의 실행결과를 적어보시오.

 1:    main()

 2:    {

 3:       int a = 66;

 4:       float b = 3.14159;

 5:       char c = 65, *s = "알파벳의 첫문자는 ";

 6:       printf("%6d\t%-6d\n",a,a);

 7:       printf("%6c\t%-6c\n",a,a);

 8:       printf("%8.5f\t%-8.5f\n",b,b);

 9:       printf("%4c\t%-4c\n",c,c);

10:       printf("%4d\t%-4d\n",c,c);

11:       printf("\n%s%2c",s,c);

12:       printf(". %s\n","끝이라우.");

13:    }


  위의 프로그램에 이상한 부분을 발견하였는가?

  char c = 65가 바로 그것인데, 문자 변수에 단일 문자를 기억시킬 때는 '로 묶는다고 하였는데 이 문장에서는 문자 변수에 수치상수를 대입하였으니  오타가 아니냐는 오인도 했을 것이다.

  c를 정수형으로 선언하였다면 65가 대입이 된다. 하지만 지금은 문자형으로 선언하였으므로 아스키 코드의 65번 문자 A를 대입하라는 의미가 된다.

  문자 변수에 수치상수를 대입하면 C에서는 그 수치에 해당하는 아스키 값을 문자변수에 대입하게 된다( 물론, 이때 수치의 범위는 0 -- 255 사이의 정수값이라야 한다).

  컴퓨터는 문자를 아스키 코드로 기억하고 저장하므로  이 코드를  수치로 보느냐, 문자로 보느냐는 출력하기에 달려있다. 다시 말하면, 자료의 변환이 자유롭다는 것이다. 그러므로 정수를 %c로 출력하면 그 값을 아스키 코드로 하는 문자가 출력되며, 문자를 %d로 출력하면 그 문자의 아스키 코드가 출력되게 된다.

  예제의 실행결과는 직접 실행시켜 보기 바란다(백견이 불여일Run이라).

  이번 장에서는 C의 구조와 변수, printf() 함수에 대하여 알아보았다. 혹시 이해가 잘안가는 부분이 있을지 모르겠다.  '언어 강좌'란의 다른 강좌를 참조하거나 소동회의 실력자들이 모여 있는  C & C++ 질문과 답란을 이용해 주기를 바란다.

  하지만 언어를 배우는 데에는 이해가 될 때까지 고민해보고, 간단한 개념이라도 직접 컴파일 시켜 가면서 공부하는 것이 가장 좋은  방법이라는 것이 진리이다. 그리고  이 강좌를  충실히 따라가다 보면 절로 모르는 부분이 이해가 될 날이 닥칠 것이다.  시작이 반이다.  이제 여러분들은 1회를 무사히 마쳤으니  이 패기로 C의 초보딱지를 떼게 되는 8장까지 힘차게 따라와 주길 바란다.


제 2장 - C의 입출력 함수 및 수의 표현

  제 1장에서 우리는 C의 기본 문법과 print함수에 대해서  공부하였다. 이번장에서는 기본적인 입출력 함수들 각각의 기능과 차이점을  알아보고 상황에 따라 알맞은 입출력 함수를 선택할 수 있도록 할 것이다. 그리고 C언어에서 여러가지 수치의 표현방법과  수치의 형을 지정하는 이유를 알아보자.


1. 라이브러리(Library)에 관하여

      - C의 배움의 과정은 문법과 라이브러리 함수 마스터 이다.

  프로그램을 작성할 때 모든 루틴을 프로그래머가 일일이 작성해야 한다고 생각해 보자. 얼마나 비효율적인 일인가? 프로그래머가 입,출력 등의 프로그램의 사소한 부분까지 코딩해야 한다면 시간낭비요, 인력낭비일 것이다. 그래서 C와 같은 고급언어에서는 프로그램에서 주로 사용되는 기본적인 기능들을(함수들을) 미리 작성하여 라이브러리로 구축하여 놓고 프로그래머들이 하나씩 호출하여 쓸 수 있도록 해 놓았다.(이것을 표준 라이브러리라고 한다) printf()의 경우에도 라이브러리에 미리 작성되어 있는 함수이다.

  전장에서 C프로그램은 함수들의 집합으로 이루어져 있다고 배웠다. 화면에 문자를 원하는 형식대로 출력해 주는 기능의  printf()함수를 비롯하여 C에서는 수백가지의 함수들을 지원한다.  프로그래머는 이러한 다양한  함수들 중에서 자기가 필요로 하는 함수들을 호출하여 사용하는 것이다. printf()함수뿐만 아니라 오늘 우리가 배우려 하는 입출력 함수들도  일반적으로 많이 쓰는 함수이다.

  요즘에는 이러한 C표준 라이브러리 뿐만 아니라, C프로그래머들의 편의를 위하여 각종 상용 라이브러리들이 나와 있다. 우리가 지금 자체 한글을 지원하는 회원관리 프로그램을 작성한다고 치자. 그러면, 정작 중요한  자료 관리 부분보다는 먼저 한글과 프로그램 인터페이스를 작성하여야 할  것이다. 프로그램의 가장 중요한 부분인  자료관리 부분에 많은 노력이 들어가야 하는데, 그 이전에 덜 중요한(?) 부분에 매달려야 한다면 이것은 시간, 노력낭비가 아닐 수 없다. 하지만 시중에 나와있는 한글 라이브러리를  사용하여 원하는 기능을 가진 함수들을 호출하기만 하면 시간과 노력을 훨씬 줄이면서 프로그램을 작성할 수가 있는 것이다. 참고로 필자는 한라프로라는 자신의 프로그램에서 한글과 GUI를 마음대로 구현할 수 있는 공개 라이브러리를 쓰고 있다. 이 라이브러리는 동호회 자료실에 등록되어 있으니 필요하다면 다운받기 바란다.

  우리는 초보이므로 일단은  C에서 제공하는 기본 라이브러리 함수를 이용하는 방법부터 배워야 한다. 그러면 오늘은 먼저 입출력 함수에 대하여 알아보기로 하자.


2. 출력 함수

  C에는 printf()함수 이외에도 단일문자를 출력하는 putchar()함수와 문자열을 출력하는 puts()함수가 있다.

putchar('C')  ;         --> 단일문자 출력(줄을 바꾸지 않는다)

puts("I Love You!") ;  --> 문자열 출력(출력한 후 자동으로 줄을 바꾼다)

  위의 두 함수로 전장에서 배웠던 Esc문자열을 그대로 쓸 수가 있다. 즉 putchar('\n')은 줄을 바꾸며, puts("\a Error!")는 삑 소리를 내며 에러메시지를 출력한다.


<예제1> 변수를 이용하여 'I' 'like' 'C' 'Programming'을 기억시키고 다음과 같이 출력되도록 하는 프로그램 작성.

              I like C Programming!

이 프로그램은 두 가지 관점에서 작성될 수가 있다.

<리스트1>

#include <stdio.h>

main()

{

char c1='I', c2='C';

char *s1="like",*s2="Program";

printf("%c %s\n%c %s\n",c1,s1,c2,s2);

}

 

<리스트2>

#include <stdio.h>

main()

{

char c1='I', c2='C';

char *s1="like",*s2="Program";

putchar(c1);

putchar(' ');

puts(s1);       putchar(c2);

putchar(' ');   puts(s2);

}


  먼저 각 프로그램 첫머리의 #include <stdio.h>라는 문장을 주시하라.

  stdio.h는 standard input/output의 준말로서  표준입출력 함수에 관한 선언을 모아둔 Header화일이다.

 위에서 우리는 라이브러리에 대하여 알아보았다. 고급언어로 작성된 프로그램은 <컴파일>이라는 과정을 거쳐 컴퓨터가 이해할 수 있는 기계어로 번역이 되고,  프로그램에서 사용된 표준 함수들을  라이브러리(미리 기계어로 변환 되어 있다)에서 추출해 와서 프로그램에 결합하는  <링크>라는 과정을 거쳐야 비로소 하나의 실행화일이 만들어 지는 것이다.

  변수와 마찬가지로 프로그램에서 사용되어질 함수들은 모두 프로그램  첫머리에서 선언이 되어야 한다. C에서는 방대한 라이브러리 함수들을 그 기능별로 세분화하여 선언을 해 둔 헤더화일을 가지고 있다.  위 프로그램에서 사용되는 printf(), putchar(), puts()등과 잠시후 배울 입력 함수들에 관한 선언들은 모두 <stdio.h>화일에 들어있으므로  #include 명령을 써서 <stdio.h> 화일을 포함시켜 준 것이다.  #include <화일명>은  파일명으로 지정하는 프로그램 모듈을 그 위치에 삽입하여 컴파일 하라'는 의미로 C컴파일러에 주는 의사명령의 하나이다. 의사명령이란 예약어가 아니고  컴파일러에게 지시하는 명령어를 말한다(이러한 것을 전처리기라고 한다).

  그러면, 지난 장에서는 예제에서 printf()함수를 사용하였는데 왜 #include문장이 없었지? 라고 생각하는 분도 계실텐데, 'stdio.h'는 기본 입출력 처리루틴을 위해 준비된 것으로 #include문으로  지시하지 않더라도  자동적으로 삽입이 된다. 하지만 기술하여 주는 것이 바람직한 습관이라  하겠다.

 위의 두 프로그램을 비교해 보면 <리스트1>이 훨씬 간단하고 편리함을 알 수가 있다. 하지만, printf() 함수는 수치, 수식 등의 계산 결과와 문자, 문자열 등을 처리해야 하므로 당연히 수학적인 라이브러리가 일부  <링크>시에 포함이 된다. 그러나, putchar()함수나 puts()함수는 문자와  문자열만 처리하면 되므로 수학적인 라이브러리가 필요없다. 그러므로 <리스트1>이 소스코드는 간단하지만 실행화일의 크기는 <리스트2>가 작다.


3. 입력 함수

  다음은 C의 표준 입력 함수들에 관하여 알아보자.

# getch()

단일문자를 입력받으며, 입력하는 문자는 화면에 표시되지 않는다(No Echo)

# getchar()

단일문자를 입력받으며, 입력하는 문자는 화면에 표시된다(Echo Back)

<Enter>를 누르면 입력이 종료되며 여러문자를 입력해도 처음의 한 문자만을 유효하게 받아들인다.

# gets()

<Enter>를 누를 때까지 공백을 포함한 모든 문자열을 입력받는다.

# scanf()

여러개의 변수값을 입력받는다. 자료와 자료사이는 공백, 탭, \n 등으로 구분한다. 서식 제어 문자열(%d,%f,%c,%s 등)을 사용하여 지정된 형식으로 입력받기도 한다. 또한 수치변수에 값을 읽어 들일 때에는 인수명 앞에 &를 붙여야 한다. (printf()함수는 변수명칭, 상수, 수식 등을 인수로 사용하나 scanf()함수는 변수의 포인터를 인수로 사용한다. - 일단은 수치형을 입력 받을 땐 &를 붙이고, 문자열을 입력받을 땐 &를 사용하지 않는다고 알고 있자.)

<예제2> "당신의 이름은?"이라는 질문에 자신의 이름을 성과 이름을 구분하여 입력하여 "내 이름은 # ##입니다."를 출력하는 프로그램 작성.

<리스트3>

1:    #include <stdio.h>

2:    main()

3:    {

4:       char *s = "          ";

5:       printf("당신의 이름은? ");

6:       scanf("%s",s);

7:       printf("내 이름은 %s입니다.\n");

8:    }


  지난 장에서 문자열을 기억하는 변수는 변수명 앞에 *를 붙인다고 했다.

  변수명 앞에 * 를 붙인 것은 포인터를 의미하는데,  일단은 포인터 변수란 "어떤 자료가 메모리에 저장될 때 그 메모리의 번지를 기억하는 변수"  정도로 알고 있자. 문자열을 입력받기  위해서는 4행과 같이 먼저  문자열을 기억시킬 영역을 확보해 두어야 한다. 4행에서 공백은  입력받을 문자열의 길이와 같거나 길어야 한다. 이 문장의 s 변수는 공백 10칸이 저장된 메모리의 번지를 가지는 변수라고 해석할 수 있다. <리스트3>의 실행 결과는,

            당신의 이름은? 홍 길동

            내 이름은 홍입니다.

  6행에서 scanf()함수는 공백이 자료를 구분하는 역할을 한다고  했으므로 변수 s에는 '홍'만 기억되는 것이다.  그럼, <리스트3>을 어떻게 바꾸면 좋을까?

            4행을  char *s1 = "   ", *s2 = "       ";

            6행을  scanf("%s %s",s1,s2);          

  로 바꾸어 성과 이름을 넣는 변수를 각각 선언하고 값을 할당하면 된다.

  하지만 이것은 별로 바람직 하지가 못하다. 그러면 <리스트3>에서,

            6행을 gets(s);로 바꾼다.

  공백을 포함한 문자열을 입력받을 경우에는 gets()함수를 쓰면 된다.

  그리고 <리스트3>을 <리스트4>로도 기술할 수가 있다.

<리스트4>

1:  #include <stdio.h>

2:  main()

3:  {

4:     char *s, arr[20];

5:     s = arr;

6:     printf("당신의 이름은? ");

7:     gets(s);

8:     printf("내 이름은 %s입니다.\n",s);

9:  }


 위에서 'arr[20]'은 배열을 의미한다(배열은 후에 자세히 배울 것이다).

 char arr[20];의 뜻은 문자가 20개 저장될 만큼의 arr라는 이름의 빈 공간을 확보하라는 것이다.( int number[10];은 정수가 10개 저장될  number라는 방을 확보하라는 뜻이다)

 그러므로 5행은 20칸의 공백(을 가진 배열 arr)을 s에 대입한다는 의미이다.

 문자열을 입력받기 위해서는 먼저 문자열을 기억시킬 영역을 확보해 주어야 하는데, 그 방법은 다음의 2가지가 있다.

1) char *s = "          ";

2) char *s, arr[n];   ( arr: 임의의 배열명,

s = arr;              n: 임의의 수치     )


 <예제3>  사다리꼴의 3변(윗변,밑변,높이)를 입력하여 그 넓이를 구하는 프로그램 작성.

[입력]

DATA? 345 678 123 

[결과]

AREA =   62914.50

<리스트5>

 1:     #include <stdio.h>

 2:    

 3:     main()

 4:     {

 5:        int a, b, c;

 6:        float s;

 7:        printf("DATA ? ");

 8:        scanf("%3d %3d %3d", &a,&b,&c);

 9:        s = (a+b)*c/2.0;

10:        printf("AREA = %10.2f\n",s);

11:     }


 위에서 8행의 의미는

          @   #   $     @  #  $   <= 차례로 대응한다

          scanf("%3d %3d %3d", &a,&b,&c);   "   "안의 공백은 있어도 없어도 된다.

 %3d는 3자리의 정수를 입력 받는다는 의미이다.  scanf()함수의 제어코드는 지난 장에서 배운 printf()함수의 제어코드와 대부분 일치한다.

  그럼,  <리스트5>를 실행한 후, 345 678 123입력해 보라. 답이 제대로 나왔는가? 아마 -2621.50이라는 엉뚱한 수치가 출력될 것이다.

  그러면,34 56 78을 입력해 보자. 3510.00이라는 정확한 수치가 출력된다.

  얘가 왜 이럴까?

  전자의 경우는 답이 음수로 출력이 된다. 이것은 (a+b)*c 즉, (345+678)*123의 계산 결과가 int형 정수의 범위(-32,768 -- 32,767)을 넘어섰기  때문이다. 자료형의 범위를 넘어섰을 경우를 overflow가 발생했다고 한다. C에서는 이러한 프로그래머의 조그마한 실수의 책임을 전적으로 프로그래머 자신에게  떠맡긴다.  그래서 우리는 자료형의 범위를 알아놓을 필요가 있다.

  여기서 overflow가 발생하지 않도록 하려면,

    (1) 변수 c를 실수형으로 선언하고(c를 곱하는 과정에서 발생했으므로),

        8행을 scanf("%3d %3d %3f",&a,&b,&c);로 바꾼다.

 

c가 실수형이면,

(a + b)  *   c   /  2.0

(a + b)  *   c   /   2.0

정  수

정  수

실  수

정  수

실  수

실  수

 

 

 

 

 

 

 

 

 

 

 

 

 

정수(Overflow)

 

 

 

 

실수(Overflow발생안함)

 

 

 

 

 

 

 

 

 

 

 

 

 

실    수

 

 

 

실   수

 

 이 이유는 지난 장에서 자세히 설명하였다.

                                                             

   (2) 9행을 다음과 같이 기술한다.

       s = (float)(a+b)*c/2.0; 또는 s = (a+b)*(float)c/2.0;

       (float)의 의미는 뒤의 식의 결과나 변수값을 강제로 실수로 보존하라는 의미이다. 이렇게 자료형을 강제로 바꾸는 예약어를  cast 연산자라고 한다.

       물론 실수변수 float c;를 정수로 강제로 지정하려면 (int)c로 하면 된다.

   (3) 변수 a, b, c를 배정도 정수로 선언한다.(설명은 밑에 계속된다)

4. 수치자료의 표현

 컴퓨터에서의 수치자료의 형은 여러가지로 나누어 진다. 다음 표를 보자.


<표1> 기본 데이터형의 크기와 범위

구  분

종     류

선       언

크 기

범        위

문  자

부호있는 문자형

char

1

-128 -- 127

자  료

부호없는 문자형

unsigned char

1

0 -- 255

표  준

부호있는 정수

int

2

-37,768--32,767

정  수

부호없는 정수

unsigned int

2

0 -- 65,535

배정도

배정도정수(부호O)

long=long int

4

-21억4천만 -- 21억4천만

정  수

배정도정수(부호X)

unsigned long

4

0 -- 4,294,967,295

실  수

단일 실수

float

4

1.7*10^-308~ 1.7*10^308

배정도 실수

double

8

3.4*10^-308~ 3.4*10^308

# ^ 은 거듭제곱을 나타낸다.   # 위에서 크기의 단위는 Byte이다.

# 문자자료의 경우 C에서는 1 Byte정수형 자료로 처리한다.


  (수치의 형을 지정해야 하는 이유)

  위의 표에서 보듯이 수치의 범위에 따라 자유롭게  변수형을 선언할 수가 있다. 수치의 형을 왜 이리 복잡하게 나누어 놓았나 반문하는 사람도 있겠지만 표에서 위에 있는 형일수록 크기(Byte)가 작아 메모리 사용량도 줄어 들고 연산속도도 빨라진다. 작고 단순한 연산을 할 때에는 char형,  int형으로도 충분하겠지만, 심지어 우리가 가계부같은 프로그램을 작성하려  해도 최소한 배정도 정수형은 필요할 것이다. 그러므로, C에서는 사용자의 필요에 따라, 프로그램의 목적에 따라  속도와 메보리를 고려해 가면서 자유롭게 수치의 형을  선택할 수가 있는 것이다.


  (1) 정수 표현 방법

     정수형의 자료를 나타내는 int형은 2Byte(즉 1Word,16Bit)이므로  나타낼 수 있는 수치의 종류는 65,536가지이다.(2의 16승 - 수학시간에 졸지 않은 사람은 다 안다)

     부호를 사용할 필요가 없는 정수형 변수(Unsigned int)는 0에서 65,536까지 범위를 나타낼 수가 있으나, 일반 int형 변수는 양수, 0 ,음수를  모두 사용해야 하므로 반으로 나누어 -32,768 ~ 32,767 ( 0은 편의상 양수에 속한다 )사이의 범위를 가지게 되는 것이다.

     그러면, int a = 32767, b = 1, sum;

         sum = a + b ;           에서 sum의 값은 얼마일까?

     바로, 오버플로우가 발생한다. 실제는 32768이 되지만 범위를 넘어서므로 sum의 값은 -1이라는 어처구니 없는 값이 발생한다. 그러므로 우리는 연산을 할 때, 수치형의 범위를 고려하여야 괴짜 프로그램을 생산하지 않게 된다.

     이젠, <예제3>의 해결책 3가지 중 3번이 이해가 되리라고 본다.

      5행을 long a,b,c;

      8행을 scanf("%3ld %3ld %3ld",&a,&b,&c); 로 바꾸면 된다.

      (%ld는 배정도형 정수를 출력하거나 입력받을 때 쓰는 변환기호이다)

     컴퓨터에서는 모든 자료를 2진수로 취급한다.

     int a=15라고 했을 때, 컴퓨터 내부에서는 a를 000000000001111(int형은 2바이트 = 16비트이므로)로 기억한다.

    그러면, int b=-15라고 했을 때 어떻게 기억을 할까?

    컴퓨터는 2진수로 음수를 표현할 때 2의 보수를 취하는 방법을 이용하는데, 그 방법은 다음과 같다.

 (1) 양수값에다가 1의 보수를 취한다(비트반전).

 (2) (1)의 결과에 1을 더한다.

            15 => (1) 1111111111110000 (1의 보수를 취함)

                   (2) 1111111111110001 => -15 (1을 더함)

  부호를 고려하는 정수형(int)의 경우에는 맨좌측 최상위 비트가 0이면 양수로, 1이면 음수로 본다.

  하지만 unsigned int(부호없는 정수)형의 경우에는 양수만을  취급하므로 최상위 비트를 고려하지 않는다.

  예를 들어보자.    1111111111110001가 int형이라면, 최상위비트가 1이므로 이 값은 음수이므로 2의 보수를 취하여 0000000000001111(15)의 음수값인 -15가 된다. 반면에  1111111111110001이 unsigned int형이라면, 최상위비트와는 관계없이 10진수로 65521이 된다.

  장황하게 설명을 하였는데, 기본적으로 알아두어야 할 내용이므로 언급하였다.(전산학에서는 기본적으로 알아야 할 내용이다)

  일반적인  프로그램에서는  정수형이나 배정도 정수형 변수로도 프로그램작성이 가능하나, 수학적인 계산이  조금이라도  들어가는 프로그램에서는 실수형이 필요하게 된다. 그럼, 실수 표현 방식에 대하여 알아보자.

  (2) 실수 표현 방법 

  컴퓨터에서 실수(float형)를 표시하는 경우에는 부동소숫점 형식을  이용한다. 부동소수점 형식에서는 하나의 수치를 표시하는데에 4Byte를 할당한다.

첫 byte는 그 수치의 부호와 지수를 표시하며 남은 3 byte로는 1 byte 당 10진수 2자리를 BCD형식으로 표시한다.  BCD(Binary Coded Digit)는  우리말로 2진화 10진수라고 부르며, 4비트로 10진수 1자리를 표시하는  형식을 말한다.

  4비트로 표시할 수 있는 수의 종류는  2의 4승(0 ~ 15까지)가지가 있지만 BCD에서는 0에서 9까지만 표시한다.  1994를 BCD로 표시하면,

0001

1001

1001

0100

(4비트로 10진수 1자리를 표시)

1

9

9

4

 

     예를 들어 float a=1994; 일때 컴퓨터에서는,

 

 

(이 영역은 BCD형식으로)

xyyy

yyyy

0001

1001

1001

0100

0000

0000

(총 4 byte)

 

 

 

 

1.

9

9

4

0

0

 

 

 

 

 

 

 

 

 

     *소수점은 항상 이 위치에 있는 걸로 간주

 

 

지수(10의 멱승을 나타냄)    .여기선 0000011

부호(양수이면 0, 음수이면 1) .여기선 0

 

        ==> 1.99400 * 10의 3승 (1994.00) : 실수 표현 방식이다.


  위에서 보면 지수의 표현을 7bit로 하고 있다. 7비트로 할 수 있는  자료의 종류는 128가지인데, 음수, 양수를 구분해야 하므로 지수의 범위는  -64에서 63까지가 된다.

  위에서 보듯이 실수형(float) 자료의 유효 수치는 6자리이다. 그러므로 7자리 이상의 수치를 기억시키는 경우에는 7번째 자리에서 반올림이 행하여진다.

  C에서는 수치의 유효 자리수를 2배로 늘리기 위해  배정도 실수형(double)을 지원한다. 배정도 실수는 8byte로 표시하게 되며, 유효자리수는 14자리로 늘어난다.

( 총 8 byte )

xyyy

yyyy

0000

0000

0000

0000

0000

0000

....

0000

0000

0000

0000

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

지수비트

7 byte (유효자리수는 14자리로 늘어난다)

부호비트


5. 형 변환 기호(format code)

 지난 장에서 printf()함수를 배울 때 형변환 기호에 대해 언급하였다.

이번 장에서는 좀 더 자세히 알아보자.

<표2> 형 변환 기호

기  호

인수의 수형

변환의 내용

%d

정 수 형

인수를 부호있는 10진수로 출력한다

%u

정 수 형

인수를 부호없는 10진수로 출력한다

%ld

배정도 정수형

인수를 부호있는 배정도 정수로 출력한다

%lu

배정도 정수형

인수를 부호없는 배정도 정수로 출력한다

%o

정 수 형

인수를 부호없는 8진수로 출력한다

%x

정 수 형

인수를 부호없는 16진수로 출력한다(소문자로)

%X

정 수 형

인수를 부호없는 16진수로 출력한다(대문자로)

%c

문자(or정수형)

인수를 단일문자로 출력

%s

문자열 포인터

\0앞까지의 문자열이던가 지정된 길이만큼 출력

%f

부동소수점 실수

고정 소숫점 형식으로 출력 ex: 35.45600

%e

부동소수점 실수

부동 소숫점 형식으로 출력 ex: 3.54560e+01

%E

 

3.54560E+01

%g

부동소수점 실수

고정,부동 중 간단한 형식으로 출력(e)

%G

 

(E)

        # 제1장의 <표1>과 조합하여 출력이 가능하다.


scanf()함수는 printf()함수와 거의 같은 변환 기호를 사용한다. 가장 큰 차이점은 다음과 같다.

  1) %g, %G 옵션이 없다.(생각해 보면 당연한 것이다)

  2) %f와 %e는 같다.(둘 다 부호,소수점,지수부의 유무에 관계없이  받아들인다.

<예제4> 다음 프로그램의 실행결과를 적어 보아라.

/* 출력 형태 실습 - 여러가지 수를 입력해 보고 출력형  */

/*                   태를 예상하는 연습을 한다        */

#include <stdio.h>

  main()

  {

    float a;

    int b;

    printf("\n임의의 숫자(실수) 입력:");

    scanf("%f",&a);               /* 874563을 입력해보고, */

                                   /* 0.04568을 입력해보고,*/

                                   /* 756.355를 입력해보라 */

    puts("당신이 입력한 수치를");

    printf("고정소수점형식으로 출력하면 :%f\n",a);

    printf("부동소수점형식으로 출력하면 :%E\n",a);

    printf("\n임의의 양수를 입력하라 :");

    scanf("%d",&b);                 /* 3654를 입력해 본다 */

    printf("10진수==>8진수==>16진수\n");

    printf("%d ==> %o ==> %X\n",b,b,b);

    printf("\n 끝낼려면 아무키나 누르시오.");

    getch();

   }


  제 2장에서는 기본적인 C의 입출력 함수와 수치형의 종류에 대하여  알아 보았다. 여러분들께 자세히 전달하려고 노력하였으나, 혹시 애매한 부분이 있을지도 모르겠다. 하지만 10번 읽으면 이해되지 않는 것이 없다고 했다.

  C언어는 기본 개념을 확실히 잡지 않으면 후에 가서 고생을  하게 되므로 꼭  이번장의 내용을 이해하기를 바란다.

  다음 장에서는 제어 구조에 대하여 공부하겠다. 반복되는 이야기지만  예제들은 직접 입력하여 실행시켜 보고, 왜 그렇게 되었는지를 완전히  이해하는 것이 프로그램 언어를 공부하는 가장 좋은 방법이라고 했다. 이제 여러분들은 2장을 마쳤으니 1/4을 끝낸 셈이다. 부디 본 강좌와 함께 자연스러운 C언어 입문이 되기를 바라겠다.


제 3장 - 제어 구조

  제 2장에서 우리는 C의 기본 입출력 함수들과 수치형의 종류와 그 원리에 관하여 공부하였다. 우리가 단순한 입출력이나 계산을 할 때에도 프로그램에 융통성을 주어 작성할 수가 있다. 이번 장에서는 프로그램의 흐름을 다양하게 해주는 순환과 제어에 관한 방법에 대하여 배우겠다.


1. 제어문

  제어문이란 프로그램의 실행을 강제로 바꾸는 명령어이다.

  goto 문 ( goto label; ) - ## 있다는 것만 알고, 쓰지 말기를 바란다 ##

  goto문은 프로그램의 실행을 지정된 레이블이 있는 위치로 강제로 옮기는 명령어이다.  레이블명 뒤에는 반드시 :를 붙이며,goto명령과 결합된 레이블 뒤에는 :를 붙이지 않는다.


<예제/리스트1> 짝수가 계속 출력되는 프로그램



 한편 위의 프로그램은 멈추지 않는 무한루프에 빠지게 된다.  프로그램의 실행을 강제로 멈추게 하려면 <Ctrl> + <Break>키를 누르면 된다.  일반적인 경우에는 goto문을 쓰지 않는다. goto문은 프로그램의 논리적 측면에도 역행하고, 프로그램의 분석을 어렵게 하므로 우리는 이러한 게 있다는  것만 알아두자.


 if ... then ... else 문 (조건 제어문)

  조건 제어문이란 어떤 조건이 참, 거짓 여부를 판단하여 실행을 제어하는 구조를 말하며, 중첩사용도 허용이 된다.


 if(     )안의 조건식이 참이면 실행부1을 실행하고 거짓이면  실행부2를 실행하라는 뜻이다.


<예제2> 하나의 정수를 입력받아 짝수인지 홀수인지 판별하는 프로그램

         (리스트들의 단짓기 요령을 유심히 보아주기 바란다)


<리스트2>


  베이직에서는 = 가 대입할 때도 쓰이고 같다는 등호로도 쓰이지만, C에서는 = 과 == 을 엄격히 구별한다. 즉 = 는 대입 연산자로 쓰이고, == 는 같음을 나타내는 관계 연산자로 쓰인다.


          sum = a + b  => (대입연산자) a+b의 값을 sum에다 대입시켜라

          if ( a == b ) => (관계연산자)  만약, a와 b가 같다면,,


  첫번째 실행문은 복문이므로 { }로 묶었으나, 두번째 실행문은  단문이므로 { } 로 묶지 않아도 된다. 그리고 if나 else뒤에는 ; 가 붙지 않는다는 것도 주시하여야 한다.


 <예제3> 입력받는 정수가 양수면 '양수', 음수면 '음수', 0이면 '제로'라고 출력하는 프로그램 작성


<리스트3>


  위와 같이 else 다음에 다시 if..else문을 포함시킬 수가 있다. 그리고 C에는 elseif가 없으므로 else와 if를 반드시 띄어야 한다.

 <예제4> 1+(1+2)+(1+2+3)+...+(1+2+3+...+200)의 합을 구하는 프로그램

<리스트4>

  6행의 k = n = 0;은 n에 0을 대입하고, n값을 다시 k에 대입하라는  의미이다. 이와 같이 식을 우변부터 평가하는 성질을 '우결합성'이라고 하는데 연산자에 따라 우결합성, 좌결합성이 다르다. 일단은 대입연산자 =는 우결 합성을 가진다고 알고 있자. 몇가지 예를 더 들어보자.

 a = b = c = d = e = 1   ; (이 때 1이 기억되는 순서는 e,d,c,b,a순이다)

     a = 2*b = 3*c = 4*d = 5 ; (d에 5를 기억시키고, d에 기억된 값에 4를 곱해서 c에 기억시키고, c에 기억된 값에 3을 곱하여 b에 기억시키고, b에 기억된 값에 2를 곱하여 a에 기억시키라는 의미이다)


   하지만, int k=n=0;의 경우에는 에러이므로 쓰지 말기를 바란다.

   8 ~ 10행에서 +=란 걸 썼는데, 이것은 각각  k=k+1, n=n+k, sum=sum+n 과 같다. +=를 쓰는게 컴파일 후 실행속도가 더 빠르다고 한다. -=, *=,/= 도 마찬가지이다.


switch .. case문 (선택제어문)


  여기서 식1~식n은 반드시 상수 또는 상수식이어야 한다. 실행시 switch다음의 식(또는 변수)의 값가 case다음의 식(혹은 상수)을 비교하여 값이 같은 곳으로 제어를 옮긴다. 만약 값이 일치하는 곳이 없으면 default다음의 문장을 수행한다. case 다음의 문장들은 여러 개의 문장을 나열할 수 있지만 블럭이 아니므로 { }로 둘러싸선 안된다. case 다음의 명령문을 수행한 후에도 switch 블럭 ({ }안의 부분)을 빠져나가지 않고 그 밑의 case문 부터 최종 deault문까지 실행을 하므로 필요한 경우 case 명령문 끝에 break를 붙인다. break를 만나면, switch블럭{ }밖으로 빠져 나가게 된다.


<예제5> 점수를 입력받아 90점 이상이면 A, 80-89점이면 B, 70-79점은 C, 그 이하는 F를 출력하는 프로그램 (만일 점수로 -999를 입력하면 처리를 끝내게 한다)

<리스트5>

  9행의 !=는 관계연산자이다. ==가 등호를 나타낸다고 하였는데,  BASIC에서는 <>가 같지 않음을 나타내나 C에서는 !=가 부등호이다. 그러므로 score가 -999가 아니면 { }안의 문장을 수행하라는 의미이다.

  10행에서 score/10에 대하여 이상하게 생각하는 사람이 있을 것이다.  만약 95를 입력하면 9.5가 나오는데 case에서는 9.5가 없으니  default 값인 F가 출력되는게 아니냐하고 말이다. 하지만 지난장에서  배웠듯이  정수와 정수의 연산은 정수의 결과를 낳으므로 95/10은 9.5가 아니라 소수점 이하가 절삭되어 9가 된다는 것을 명심하기 바란다.


2. 순환문

 순환문이란 특정한 부분을 반복수행하도록 하는 명령들을 일컫는 말이다.

for문

[형식]  for(초기식;조건식;반복식)

           {반복할 부분;}   <= 반복할 부분이 단문이라면 { } 생략한다

 초기식은 for문이 처음 실행될 때 단 한번 실행이 되며 여러개의 식을 ,로 구분하여 기술할 수가 있다.

 조건식은 루프의 실행 중 매회 조사를 하며, 단 한 개의 문장만을 기술할 수 있으며, 조건식이 참(1)이면 루프를 반복하며 거짓(0)이면 루프를 빠져 나간다.

 반복식은 루프의 다음 실행을 준비하는 단계로서, 여러개의 식을 ,로  구분하여 나열할 수가 있다. 위에서 공부한 제어문들과 마찬가지로 for(...) 다음에는 ; 를 붙이지 않는다.

 for문의 블럭에서 강제로 빠져나갈 때는 switch문에서와 같이 break 명령어로 빠져나간다.(goto는 되도록 쓰지 말자)

<예제6> 1부터 100까지의 합을 구하여 1+2+3+....+100 = 5050를 출력하는 프로그램 작성

<리스트6>


 for 다음의 루프가 단문일 때는 { }를 쓰지 않고 일반적으로 5행과  같이 표현한다. 그럼, 5행을 분석해 보자.



while

 [형식1]    while (조건식) {      [형식2]    do {

                  .                              .

             반복할 부분;                      반복할 부분;

                  .                              .

            }                                } while (조건식)

 [형식1]은 조건식의 결과가 참(1)이면 반복할 부분을 수행하고, 거짓이면 블럭을 빠져 나간다. 그러므로 루프안에 조건식의 결과를 변화시키는 문장이 있어야 하며, 조건식의 변수를 변화시켜 주지 않은 경우엔 무한 루프를 돌게된다.  while문의 블럭에서 빠져나갈 경우에도 break문을 사용한다.

 [형식2]의 경우에는 루프를 먼저 실행한 다음에 while다음의 조건식을 검사하는 차이가 있다. [형식1]의 경우에 조건식이 바로 거짓이 되어 루프를 실행하지 않을 경우가 있으나 [형식2]에서는 루프를 반드시 한 번은  실행한다.( 이러한 것을  '판단후 처리형의 구조'와 '처리후 판단형의 구조'로 나누어 부르기도 한다 )


<예제7> <예제6>의 프로그램을 while문을 이용하여 구하라.

<리스트7>

 <리스트6>의 for안에는 i<=100 이지만 여기서는 i<100이다. 왜  그런지는 곰곰히 생각해 보면 알게 될 것이다.  그리고, <리스트4>와 <리스트5>에서 goto문을 남발하였는데 이제  순환문을 배울만큼 배웠으니, goto을 쓰지말고 for나 while문을 사용하여 반드시 작성해 보길 바란다. 자신이 직접 프로그램을 짜 보고 오류를 찾아보는 것이 가장 좋은 방법이다.


3. 중첩된 루프(nested loop)와 루프 예제들

 중첩된 루프란 다른 루프 내에 포함된 루프를 말한다. 그럼, 예제를 통해 루프를 중첩시키는데 대하여 알아보자.

<예제8> 구구단을 출력시키는 프로그램을 작성하라

<예제9> 입력된 어떤 수까지 그 이내에 들어 있는 모든 소수를 찾는 프로그램을 작성하라.

 위 프로그램은 프로그램 다운 골격을 어느정도 갖추었다.

 6,7행에서 보듯이 변수명에 의미를 부여하면, 프로그램의 판독도 쉽고 보기도 좋아진다. 또한, 12 - 15행에서 처럼 입력오류를 검사하는 루틴도 가지고 있다. 프로그램 중간중간에 주석문을 넣어줌으로서 판독이 쉽게 하였다. 이렇한 주석문들은 프로그램의 오류(버그)를 수정하기 쉽게 한다(이러한 것을 디버깅이라고 한다).  18행에서 27행까지의 루프는 2에서부터 limit까지의 숫자를 차례로  검사하는 루프이다.

 소수란 1과 자신으로만 나누어지는 수를 말한다.

 19,20행은 number와 중간에 나누어 떨어지는 수가 하나라도 있으면  21행으로 가라는 의미이다. 중간에 나누어 떨어지는 수가 없으면 결국  number는 자신으로 나누어 떨어져(number와 divisor이 같아져-결국은 소수이다), 21행의 조건을 만족시키므로 화면에 출력되는 것이다. count는 한 줄에 소수를 10개씩 찍기 위하여 만들어 놓은 변수이다.

 20행의 ;는 반드시 있어야 한다. 만일 없으면 if블럭이 for루프안에 속하게 되어 예기치 않은 결과가 발생하게 된다. 조건식(number%divisor!=0)이 참이면 계속 19,20행을 돌게 된다.

 앞에서 break는 switch, for, while문에서 루프를 강제로 빠져 나올때 쓰이는 명령어라고 배웠다. goto나 break같은 것은 프로그램의 구조적인  면을 반감시키므로 되도록이면 쓰지 않도록 하자.

 이번 장에서는 C언어의 제어문과 순환문에 대하여 알아보았다. 이 순환문과 제어문은 C뿐만 아니라 모든 언어에서 가장 많이 쓰이는 것이므로 여러분들은 각 명령어의 구조와 기능을 확실히 이해해 두어야 한다.

제 4장 - 연 산 자

 이번 장에서는 프로그램에서 연산을 가능하게 만들어 주는 연산자들에 관하여 알아 보겠다.

 연산자란 대상 자료에 대해 어떤 조작을 하는 부호를 말하며, 이 때 대상이 되는 자료들을 오퍼랜드(Operand)라고 부른다.  이러한  연산의 대상이 되는 오퍼랜드의 갯수에 따라 C의 연산자는 1항 연산자, 2항 연산자,  3항 연산자 등으로 구분할 수 있다.

 또한 각 연산자들은 우선 순위라는 게 있어 한 문장에서 여럿의 연산자들이 쓰였을 때 그 우선순위에 의거하여 연산을 수행하게 된다. 그리고 동일한 우선순위의 연산자들이 한 수식에 동시에 쓰였을 경우에는, 이 식을 좌측부터 평가하느냐 우측부터 평가하느냐에 따라 좌결합성과  우결합성으로 나누어 지는 성질을 가지는 것도 있다.

 위에서 언급한 연산자의 특징들이 초보자들에게는 혼란스럽겠지만,  일단 익혀두면 복잡한 알고리즘도 쉽고 함축성 있게 표현할 수 있어 유용하다.


<표1> 각 연산자들의 종류와 우선 순위

대 분 류

소분류

연      산      자

결합규칙

 

일 차 식

primery

( ) [ ] -> .

->

높다

단항 연산자

단 항

! ~ ++ -- - cast연산자 * & sizeof

<-

 

이항 연산자

승 제

* / %

->

가 감

+ -

->

쉬프트

<< >>

->

비 교

< <= > >=

->

등 가

== !=

->

비트AND

&

->

비트XOR

^

->

비트 OR

 

 

논리AND

&&

->

논리 OR

||

->

삼항 연산자

조 건

? :

<-

치환연산자

치 환

= += -= *= /= %=

<-

>>= <<= &= ^= !=

순차 연산자

순 차

,

->

낮다


1. 산술 연산자

 + : 오른쪽에 있는 값을 왼쪽에 있는 값에 더한다.

 - : 왼쪽에 있는 값에서 오른쪽의 값을 뺀다.

 * : 오른쪽의 수를 왼쪽의 수로 곱한다.

 / : 왼쪽의 수를 오른쪽의 수로 나눈다. 정수와 정수의 연산일 땐,소수점 이하는 버린다.

 % : 왼쪽의 수를 오른쪽의 수로 나눌때 나머지를 결과로 취한다. (정수에서만 사용)

 ++ : 오른쪽이나 왼쪽에 있는 변수의 값을 하나 증가시킨다.

 -- : 오른쪽이나 왼쪽에 있는 변수의 값을 하나 감소시킨다.


 그러면, ++와 --연산자의 사용에 대한 예를 들어보자.


 <예제1> ++,-- 연산자의 사용

<리스트1>


 프로그램의 실행결과는  a가 2, aplus가 1, b가 2, plusb가 2가 된다.

 애매하지만 아래를 보자.

 aplus = a++;  후위형 : a가 사용된 후, a의 값이 하나 증가한다.

 plusb = ++b;  전위형 : b의 값이 하나 증가한 후, b가 사용된다.


 그러면,  while (num<21) {     /* num이 5일때, 어떤 값이 나올까? */

             printf(%d  %d\n", num, num*num++);

             }

 이 식을 보면, 5  25를 찍고, num을 6으로 증가시키는 자연스러운 식처럼 보이나, 실제로 실행결과는 6  25가 찍힌다. 왜냐하면 printf()함수는  마지막 인자부터 먼저 넘긴다는 오묘한 진리가 있다.

          ans = num/2 + 5*(1 + num++);

 보통, num/2가 먼저 계산이 된다고 믿고 싶으나, 실제로는 먼저 num이 증가하고 나서 num/2에 사용된다. () 때문이다. 위의 여러가지 예에서 보듯이 ++/--연산자의 전위와 후위의 차이점을  명확히 이해하고 이 연산자를 너무  남용하거나  복잡하게 사용하지  말자는 뜻에서 함정을 미리 파 헤쳐 보았다.


2. 치환 연산자 ( +=, -=, *=, /=, %=, &=, |=, =, >>=, <<= ..)

 대입 연산자라고 하는 이러한 연산자들은 모두 =와 결합되어서 식의 복잡성을 줄여준다.

 x += 3; 은  x = x + 3;과 같다.

 x *= y + 1; 은  x = x * (y + 1);과 같다.

 x += y += z = 1; 은  z=1; y=y+z; x=x+y와 같다.

 치환은 a=b;처럼 단순치환일 수도 있고,  a=b=c=d; 처럼 다중치환일 수도 있으며, a += b처럼 복합치환일 수 있다.  다중 치환의 경우에는 식을 우측에서 평가하여  좌측의 변수로  차례대로 치환하게 된다.( 표1참조 - 우결합성 )

 a = b = c = d = 1;은

 a = ( b = ( c = ( d = 1 )));과 같으며, d=1;c=d;b=c;a=b;와 같다.

 a = b*c = d*e = 1;은 e=1; c=d*e; a=b*c;과 같다.


3. 관계 연산자 ( <, >, <=, >=, ==, != )

 이 연산자들은 자료의 크기 등을 판단할 때 사용하며, 식의 값이  참이면 1, 거짓이면 0의 값을 갖는다. 그리고 <=, >=는 부호의 순서가 바뀌면 안된다. 그리고, 관계 연산자들 중 ==, !=는 다른 관계 연산자들보다 우선순위가 낮다. 그리고 초보자들은 ==을 쓴다는 게 그만 =(대입연산자)를 쓰는 경우가 있다. 이러한 것은 오류를 찾기에도 힘들 뿐 아니라 실행 결과에도 심각한 영향을 미친다. ==와 =는 엄연히 다르므로 기억하자! 6.25! (?)

 a < b < c 의 평가 과정

 1) a < b 이면 참(1)이므로 1(a<b)<c이면 1(참)이 된다.

 2) a < b 가 아니면 거짓(0)이 되므로 0(a>=b)<c이면 1(참)이 된다.

 그 외의 경우는 모두 0이다.

 x > y + 2 는 관계 연산자들의 우선순위가 산술 연산자들보다 낮으므로  ( x > y ) + 2 가 아니라, x > (y+2) 와 같다. 그리고 관계 연산자들은 좌결합성을 가지므로  ex != wye == zee 는 실제로는 ( ex != wye ) == zee;와 같다. 위에서 보았듯이 처음에는 그냥 지나치기 쉬우나, 좌결합성이냐 우결합성이냐와 연산자들의 우선 순위는 중요하므로 잘 알아두자. 만일 마냥 헷갈린다면, ( )를 십분 활용하여 에러의 소지를 없앨 필요가 있다.


4. 논리 연산자 ( !, &&, || )

 (1) 논리부정 연산자(NOT) - 식의 값이 0(거짓)이면  '!식'의 값은 1(참)이 되고 식의 값이 참이면 거짓이 된다.

 (2) 논리곱 연산자(AND &&) - 여러 식중 한개라도 거짓의 값이 있으면 거짓이 된다.

 (3) 논리합 연산자(OR ||) - 여러 식중 한개라도 참이면 참의 값을 가지게 된다.

 (cat > rat) && ( debt == 100 ) 은 두 식이 모두 참이어야 참이 된다.

 (cat > rat) || ( debt == 100 ) 은 둘 중 하나만 참이어도 참이 된다.

 5>2 && 4>7 은 하나만 참이므로 거짓이다.

 5>2 || 4>7 은 둘 중 하나가 참이므로 참이 된다.

 !(4>7)은 4>7이 거짓이므로 참이 된다.


<예제2> x에 관한 여러 수식이 주어져 있다. 각 식의 x의 값이 어떻게 수행 되는 지 예측해 보라.

<리스트2>


 우선, 위 프로그램의 실행결과는 1 0 1 2 2이다. 예상결과와 맞았는가? 다르다면, 아래의 설명을 잘 읽어보자. 4,5,6행에서 6행은 우선순위에 의거하면 x = (x ||(y && z)); 로  괄호를 칠 수가 있다. 여기에 수를 대입해 보면, x = (1 ||(2 && 3)) = (1 || 1)= 1이 된다. (C에서는 식의 값이 0이 아닐 때는 모두 참으로 간주한다) 8,9,10행에서 9행을 우선순위에 의거하여 괄호를 쳐 보면, x = (x > y)||(z == y) && (x < z))와 같다. 여기에 수를 대입해 보면 결과는 0이 된다.

 11,12,13행에서 12행은 x = -( y++ ) + ( ++z ) = -(1)+(2) 로서 x=1, y=2, z=2가 된다.

 그러면, 아래를 보자.

 k = a && b++; 의 식은 어떻게 평가가 될까?

 1) k = a가 거짓일 때: b의 값은 변하지 않는다.

 2) k = a가 참일 때: b의 값은 1만큼 증가한다. 

                                 =======> 대강 감이 오리라고 믿는다.

                   

5. 비트 연산자

  비트 연산자란 비트조작을 위해 쓰이는 연산자이다. 주의할 점은 오퍼랜드는 정수 또는 정수형이라야 한다.


 (1) 비트합 연산자(OR- |) : 좌우의 식을 이진수로 하는 비트합을 구해준다.

    a = 117, b = 216일 때 a|b의 결과

    117  =>  0000 0000 0111 0101

    216  =>  0000 0000 1101 1000

    ============================

               0000 0000 1111 1101  =>  253

 비트합 연산자의 용도는 바로 특정한 비트를 1로 세트시킬때 이용된다.

    a = 117때 상위 4비트를 1로 한다.

    117  =>  0000 0000 0111 0101

             1111 0000 0000 0000

    ============================

             1111 0000 0111 0101  =>  1로 세트하려는 부분만 0으로


 (2) 비트곱 연산자(AND- &) : 좌우의 식을 2진수로 하는 비트곱을 구해준다.

    a = 117, b = 216일 때 a&b의 결과

    117  =>  0000 0000 0111 0101

    216  =>  0000 0000 1101 1000

    ============================

               0000 0000 0101 0000  =>  80

 비트곱 연산자의 용도는 특정 비트를 0의로 리셋하려는 경우에 이용된다.

    a = 44149일 때 상위 4비트를 0으로 한다.

    44149  =>  1010 1100 0111 0101

                 0000 1111 1111 1111

    ==============================

                 0000 1100 0111 0101  =>  0으로 리셋하려는 부분만 1로


 (3) 배타적 논리합 연산자(XOR- ^) : 좌우의 식을 2진수로 하는 배타적논리합을 구한다.

    a = 117, b = 216일 때 a^b의 결과

    117  =>  0000 0000 0111 0101

    216  =>  0000 0000 1101 1000

    ============================

               0000 0000 1010 1101  =>  173

 두 개의 비트가 동일하면 0, 다르면 1이 된다.

 (4) 비트반전 연산자(NOT- ~) : ~는 부호를 반전하는 연산자로서 비트1은 0으로, 0은 1로 만든다. (1의 보수 연산자라고도 한다)

     a = 117일 때 ~a의 결과

     117  =>  0000 0000 0111 0101

     ============================

                1111 1111 1000 1010  =>  -118

 (5) shift 연산자( <<,>> ) : 좌측 식의 결과를 우측식만큼 비트를  이동시킨다. 이 때 밀리는 쪽의 맨 끝 비트는 밀려나오고, 반대쪽이  마지막 비트는 0으로 채워진다.

  좌측으로 쉬프트한 경우에는 맨 우측에 무조건 0이 채워지지만  우측으로 쉬프트한 경우에는 부호가 없는 정수이거나 맨 좌측의  비트가 0(양수)이면 0을 채우고, 부호가 있는 정수이고 맨 좌측의  비트가 1(음수)일 때는 1을 채운다. 즉, 쓸데 없이 부호를 바꾸지  않는다고 볼 수 있다.

      a = 117일 때 a << 2 의 결과

      117  =>  0000 0000 0111 0101

      ============================

              00 0000 0001 1101 0100   <=  00

 또한 비트 연산자는 등호와 결합해서 치환 연산자를 이룰 수 있다.

  a = a >> b; 는  a >>= b, a = a & b; 는  a &= b와 같다.

 초보자들은 비트 연산자들의 쓰임에 대해 의구심을 가질지 모르나,  고급프로그램에서는 아주 요긴하게 쓰이고 있다. 비트 연산이 C의  저급언어의 특성을 대변하는 특징 주의 하나라고 볼 수 있다. 특히, 한글 출력에 있어서의 비트 연산의 쓰임은 필수적이다. 여러분들이 고급 과정으로 올라가면 이러한 이야기들이 모두 이해가 갈 것이다. 지금 우리는 이러한 비트 연산의 기능에 대해서만 알고 있자.


6. 순차 연산자( , ) - comma 연산자

 여러개의 식을 한 줄에 나열하는 기능을 가진 순차 연산자는 우선 순위가 가장 낮으며, 좌결합성을 가지며, 컴머 우측에 있는 식을  평가하여  얻은 값을 결과로 한다. (리스트2의 5행에 순차 연산자가 쓰였다)

 x = (y = 1, y++); 는  x = 2, y = 2 가 된다.

 x = (y = 1, ++y); 는  y = 1, x = ++y이므로 x = 2, y = 2가 된다.

 x = (y = 1, y+1); 은  y = 1, x = y+1이므로 x = 2, y = 1이 된다.

 x = (y = 1, z = 2); 는  y = 1, z = 2, x = 2가 된다.

 x = (y = 3, y+2); 는  y = 3, x = 5가 된다.


  <예제3> 아래 프로그램에서 x와 y의 값은 어떤 결과가 나오겠는가?

<리스트3>

결과는 직접 실행해 보시기 바란다.


7. 조건 연산자 ( ? : ) - 식1 ? 식2 : 식3

 if - else문을 연산자로 구현한 것으로서 3항 연산자이다.

 식1을 평가하여 참(1)이면 식2의 값을, 거짓(0)이면 식3의 값을 가진다.

우결합성을 가지므로 여러번 기술하였을 때는 맨 우측부터 평가된다.


<예제4> if (a > b) c = a;

         else c = b; 를 조건연산자로 구현하여라.

         또, a=1,b=2일 때  조건 연산을 행한뒤 a, b, c의 값은 어떻게 되겠는가?

<리스트4>  위의 조건식을 c = (a > b)? a: b;로 표현할 수 있다.


 x = (y > z) ? 1 : (y < z) ? 2 : 3;은 다음과 같다.

 i) y > z 이면 x = 1이 된다.

 ii) y > z가 아니면 y < z를 평가한다. y < z이면 x = 2가 된다.

 iii) i) ii) 둘다 아니면 x = 3 이 된다.


 위의 식을 if - else문으로 고치면 아래와 같다.

      if (y > z)  x = 1;

      else if (y < z) x = 2;

            else x = 3; 


8. 간접 연산자 ( * )

 포인터라고도 하며, 식이 가르키는 번지에 저장된 내용을 표시하는  연산자이다. 단항 연산자로서 곱하기(*)하고는 질적으로 틀린다.  모든 자료는 컴퓨터의 메모리에 저장되며, 메모리는 바이트 단위로  번지가 붙어있다. 포인터는 그 변수의 값이 가리키는 번지의 자료를  지정하는 것이라고 우선 알고 있자.


9. 번지 연산자 ( & ) - & 변수

 번지 연산자는 바로 다음에 오는 변수의 번지 자체를 구해 주는 연산자이다. 수치 자료를 입력받을 경우(scanf)에 쓴 기억이 날 것이다. &는  상수에는 쓸 수 없으며, 이 연산자가 붙은 경우에는 새로운 값을 대입할 수 없다. 즉 &3 이나 &x = 1234 등은 에러이다. 위 <리스트3>의 맨 마지막 행에 printf("%x ,&i)를 삽입시켜 보아라. 변수 i의 주소값이 16진수로 표시 될 것이다.


10. cast 연산자 -  (형) 연산자

 형변환 연산자는 원하는 데이터의 형을 괄호로 묶어 연산자 앞에  지정해 줌으로써 데이터 형을 변환시키는 역할을 한다.


<예제5> cast연산의 보기 - 실행결과를 예측해 보라.

<리스트5>


<설명>

8 , 9행 : 문자 'A'는 변수 ch에 문자로 지정됨. 정수변수 i는 'A'를 정수로 변환한 65를 받음.  fl은 65를 65.00으로 변환한 값을 받음. 

11,14행 : 문자변수 'A'를 정수 65로 변환하고 여기에 1을 더함. 그리고 정수 66을 문자 B로 변환하여 ch에 저장함 12,14행: ch의 값을 정수로 변환한 다음 2와 곱함. 결과(132)를 실수로 변환해 fl과 더함. 결과 197.00은 정수로 바뀌어 i에 저장됨.

13,14행 : ch의 값 'B'를 실수로 변환해 2.0과 곱함. i의 값(197)을 실수로 변환해 더한다. 결과(329.00)은 fl에 저장됨.

16,17행 : ch를 매우 큰 수로 치환하므로 예기치 않은 결과가 일어남.


<예제6> cast 연산자의 사용 예

<리스트6>


11. sizeof 연산자

 sizeof는 바로 다음에 나오는 변수나 자료형이 차지하는 메모리의 바이트 수를 구해주는 연산자이다.

 char c;

 sizeof(c); 1의 값을 가진다.

 sizeof(int); 2의 값을 가진다.

 이렇게 해서 C의 모든 연산자에 대해 간략하게 나마 알아 보았다.  초보자들에겐 부담스러운 양이겠지만- 또, 쓸데없는 연산자가 왜이리 많냐고 -,  C언어는 고급언어와 저급언어의 특징을 골고루 갖추고 있고, 저급언어의 특성을 십분 활용하려면 이러한 연산자들을 잘 사용할 줄 알아야 한다.


12. 연산자를 사용한 예제들

 몇 가지 예제를 통하여 연산자들의 실제 사용법에 관하여 살펴보자.

<예제7> 임의의 정수를 입력받아 그수를 2진수로 출력하는 프로그램작성.

         (C는 이진수를 지원하지 않는다)

<리스트7>


<참고> C에서 8진수의 표기는 수치앞에 0을 붙여 표시하고, 16진수는 0x를 붙여 표시한다. 이러한 수들은 항상 부호없는 정수로 취급한다. printf()함수의 포맷코드는 각각 %o와 %x이다.

 C에서 2진수를 출력하려면 최상위 비트부터 그 비트 패턴(1/0)을  출력시키는 방법을 사용한다.

 비트 패턴을 출력하는 방법은 우선 출력하려는 비트를 최하위 비트가  되도록 우로 쉬프트시킨 다음 이 값을 0000000000000001과 &(AND) 연산을 시켜, 최하위 비트를 제외한 다른 모든 비트들은 0의로 ?한다. 만약,  자리수가 16개이면 이런 작업을 16번 반복하여 얻어진 값을 하나씩 출력한다.

 <그림1> 상위에서 13번째 비트패턴을 조사하는 과정

        *

  0 1 0 1 1 0 1 0 1 0 0 0 1 1 1 0     원래의 수

  0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1     우로 13회 쉬프트

  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1     1과 & 연산

====================================

  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1     13번째 패턴이 구해졌다!


 <예제8> 123456789    와 같이 출력하는 프로그램을 작성하여라.

         *0000000* 1    (힌트: 가로+세로의 값이 10일 때와

         0*00000*0 2           가로와 세로의 값이 같을 때

         00*000*00 3            *을 출력하는 프로그램)

         000*0*000 4

         0000*0000 5

         000*0*000 6

         00*000*00 7

         0*00000*0 8

         *0000000* 9

<리스트8>


 위의 프로그램에서 단문과 복문 혼동으로 인한 에러가 있다.  그것을  직접 찾아보기 바란다.

 이번 장에서는 C의 연산자에 대해 알아 보았다. 되도록  이해하기 쉽도록 설명하려 노력을 했다. 10번 정도 읽으면 이해 못 할 것도 없다. 왠만한 C입문서에 버금가는 분량의 내용이므로 '학'실히 예제를 수행해 보면  모두 이해할 수 있을 것이다.


제 5장 - 함수 / 기억부류

 이번 장에서는 C에서의 함수 제작법과 내장 함수들을 간략히 소개하고 변수의 기억범위에 대해서 설명하고자 한다.

1. 내장 함수

 컴퓨터에서는 함수를  '주 프로그램으로부터  인수(Argument)를 전달받아 일련의 작업을 수행한 후 생성된 결과를 주프로그램(main함수)으로 전달하는 하나의 단위 프로그램'으로 정의한다.

 이 함수에는 컴파일러의 내장 함수(라이브러리 함수)와 사용자가 직접 만들어 사용하는 사용자 함수의 2가지 종류가 있다. 함수는 상호간의 독립성을 유지함으로써 프로그램을 구조적으로 구현하는데 많은 공헌을 한다.(제 2장 참고) 우리가 지금까지  사용해온 printf(), scanf(), getch() 등등은 모두  컴파일러에서 제공되는 내장 함수이다.  이 내장 함수의 종류는 수 백가지나 되는데,  이러한 함수들의 사용법은 컴파일러와 함께 제공되는 'Reference guide'에 모두 나와 있다. 그러므로 이 레퍼런스 가이드는 프로그래머의 필수품이라고 할 수 있다. 여러분들도 C에서 지원되는 풍부하고 다양한 함수들을 이용하려면 반드시 레퍼런스 가이드를 장만하기를 권한다.(필수사항)

 C에서는 이러한 수백개의 내장 함수들을 용도별로 나누어 놓았다.

먼저, 제2장으로 돌아가서 2번 항목을 자세히 읽고, 이 글로 다시 돌아와 주기 바란다. .. 

 2장에서 언급하였듯이 모든 함수들( main()함수 제외 => main()도 함수이다 )은 사용되기 전에 변수의 경우와 같이 프로그램 첫머리에 선언이 되어야 한다고 했다. 확장자가  .h인 화일들(헤더화일(Header file)이라고 함). 이 바로 내장 함수들을 기능별로 구분하여 선언 해 놓은 화일인데 우리는 프로그램 첫머리에   #include문을 사용함으로써 이 함수들을 선언한 화일을 프로그램에 포함시킬 수가 있는 것이다.

 이러한 헤더화일은 터보 C에서 30여가지가 있는데, 각 함수들을 30여가지 기능들로 분류하여 선언해 놓은 것이다. 예를 들어 printf()같은 기본  입출력 함수는  stdio.h 화일에 선언되어 있고, 그래픽 관련 함수들은 graphics.h 화일에, 문자열 처리 함수들은 string.h화일에 선언되어 있으며, 수학적 처리를 담당하는 함수들은 math.h에 선언되어 있다. 이러한  내장 함수들은 여러분들이 실제 프로그램 작성시  필요에 의해서 배울 수 있을 것이다.

 하지만, 이러한 내장 함수들도 함수의 개념이 잡혀 있어야만 무리없이 잘 사용할 수 있다. 그러면, 사용자함수를 제작하는 방법에 대하여 알아보자.

2. 사용자 함수

 특별한 경우가 아니라면 C에서 말하는 함수는 사용자가 정의한 함수를 가리킨다. C의 함수는 모두 횡적인 관계를 유지하고 있어, 함수들이  독립적으로 호출, 운용되며, 함수내에서 다른 함수를 호출하는 것은 가능하나 함수내에서 다른 함수를 정의할 수는 없다. 물론 사용자 함수도 내장 함수들 처럼 선언이 되어야 한다.  내장 함수와는 달리 사용자가 직접 선언을 해야 한다.

 함수는 하나의 단위 프로그램이므로 필요시 변수를  선언하여  사용할 수 있다. 이 변수들을 지역변수(local variable)라고 하는데, 함수내에서  선언된 변수들은 그 함수내에서만 효력을 가질 뿐 실행이 그 함수를  벗어나면 그 값을 상실하게 된다. 변수들의 통용범위에 대해서는 뒷부분에서  자세히 설명될 것이므로 우선 사용자 함수 제작법에 대해 알아보자.

 1) 함수의 형식

 type 함수명(가인수 리스트)

 가인수 형선언;

 {

    내부 변수의 선언;

    ......

    ...... <=함수의 본체

    return(계산 결과);

 }

(1)형선언

함수는 계산된 결과를 되돌려주므로 당연히 그  형을 명시하여야 한다. 형언 int, float, double 등 여태 배운  형선언 문자이면 된다. 함수의 형이 정수형(int)이거나 되돌려주는 값이 없을 때에는 형선언을 생략할 수 있다. 되돌리는 값이 없는 함수일 경우에는 void를 쓰거나 아무 것도 쓰지 않는다.

(2) 함수명

함수명은 변수명의 규칙에 어긋나지 않는  문자열이면 아무것이나 좋다. 가인수 리스트는 호출 프로그램에서 전달받은 인수(실인수)와 갯수가 동일해야 하며, 인수가 없는 경우에는 가인수 리스트를 생략할 수 있지만 ()자체를 생략하면 안된다.

(3) 가인수의 형선언

가인수가 있는 경우에는 가인수의 형을 선언해야 한다. 이 형선언은 main()프로그램에서 변수에 대해 형선언을 하는 것과 동일하다.

(4) 내부변수의 선언

함수에서 연산에 필요한 변수들을  선언하여 쓸 수가 있다. 이 변수들은 그 함수내에서만 통용되므로(지역 변수) 다른 함수의 내부 변수나 main()함수의 변수와 같아도 된다.

(5) return();

연산 결과를 주프로그램에 전달할 경우 ()안에  그 값이나 변수를 기술하면 된다. 전달할 값이 없으면 return()자체를 생략할 수 있다.

 <예제1> 3개의 실수 자료를 전달받아 최대값을 구하는 함수를 작성하라.

<리스트1>

 위에서 함수명은 max이며 인수로 실수형 a, b, c를 전달받는다. 이  함수는 중간 결과를 저장하기 위해 mx라는 변수를 사용하고 있으며, 이 변수의 형은 실수형으로 선언되어 있다.

 위의 1행과 2행을 아래와 같이 표현할 수 있다.

  float max(float a, float b, float c)

  {  

    ...

  }     <= 프로그래머들은 이 방식을 주로 애용한다.


 2) 함수의 호출

 함수는 일종의 부프로그램으로 스스로 수행되지는 않고 다른  프로그램에서 호출해야만 실행된다. 그러므로 주프로그램보다 앞에 있더라도  호출하지 않는한 실행되지 않는다.

 함수의 호출은 함수명과 인수만 기술하면 되는데, 식의 일부로도  사용할 수 있고 호출자체가 하나의 문장을 이루기도 한다. 예를 들어 다음의 문장은 함수 max()를 호출하며, 함수에서 계산된 결과를 k에 기억시킨다.

     k = max(x,y,z);

 위에서 x,y,z는 실제 값이 기억된 변수이므로  이들을  실인수라고 하고, max()함수의 인수란에 기술된 a,b,c를 가인수라고 한다. 실인수와  가인수는 이름이 서로 달라도 되지만 갯수와 형은 일치하여야 한다

<리스트1>에서 작성된 함수를 호출하는 주프로그램을 만들어 보면,


 우선 main()도 함수라는 것을 잊지 말기를 바란다. main()함수가 하는 일은 DOS로 부터 필요한 인수들을 받아 자신의 일을 행한 후, 어떤 값을 DOS에게로 돌려주는 함수이다. 자세한 것은 8장에 언급되어 있다.

 위에서 void main(void)란 것은 아무 인수도 받지 않고 되돌리는 값도 없을 때 void라는 키워드를 쓴다. 위 프로그램은 3개의 실수 자료를 입력받아 max()함수로 전달하여 연산결과를 구해온다. 연산된 결과는 max()함수 자체로 되돌려지므로  이 함수를 식의 일부로, 혹은 다른 함수의 인수로 사용할 수도 있다. 그러므로  다음과 같이 사용하는 것이 허용된다.

          k = max(x, y, z) * 10;

          printf("%f\n",max(x,y,z);


 C에서는 주프로그램인 main()도 하나의 함수로 취급되며, 이 함수는 프로그램상 단 한번만 표시되어야 한다.  함수는 main()함수의 앞에 둘 수도 있고, 뒤에 둘 수도 있다. 함수가 main()함수의 뒤에 있을 경우에는 프로그램의 첫머리 즉, #include다음, main()앞에서 선언을 해야 한다. 함수의 선언은 다음과 같이 함수의 형과 함수명, 그리고 (가인수 형선언)을 기술한다.


 그리고, 함수 선언시에는 정의할 때와는 달리 반드시 문장 끝에 ;가 붙는다는 것을 까먹지 말기를 바란다.

<예제2> 인수를 갖지 않으며 되돌리는 값이 없는 함수의 보기

<리스트2>

   이 예제에서는  함수의 선언을 myfunc.h라는 사용자 헤더 화일에 기술해 두고 #include문으로 포함시키는 방법을 섰다. < >는 C의 표준 헤더 화일을 표시할 때 쓰고, " "는 사용자 헤더화일을 표시할 때 주로 사용한다. (내부적으로는 <>는 C의 include디렉토리에서 먼저 화일을 찾고, ""는 현재 사용자의 디렉토리(TC를 실행시켰던 디렉토리)에서 먼저 헤더 화일을 찾는다)

<예제3> 정수 2의 0부터 10까지의 제곱승을 구하는 프로그램

<리스트3>

 3) 인수의 전달

 주프로그램(main()또는 함수)에서 부프로그램(함수)으로 인수를 전달하는 방법으로는 두 가지가 있다.

 Call by value(값에 의한 전달) - 주프로그램에서 전달된 인수를  부프로그램에서 전달받을 때 인수의 값을 받아오며, 그 값은 실인수와 다른 기억장소에 저장된다. 그러므로 함수에서 가인수의 값을 아무리  바꾸더라도 실인수에는 아무런 영향을 미치지 않는다. (<예제1>의 경우와 같다)

 Call by Reference(참조에 의한 전달) - 함수에 인수가 전달될 때는 실인수가 저장된 메모리의 번지가 전달된다. 그러므로 함수에서는  메모리의 번지를 참조하여 인수의 값을 꺼내온다. 결국 주프로그램의 실인수와 부프로그램(함수)의 가인수는 이름만 다를 뿐 같은 기억장소를 배정받으므로 함수에서 가인수의 값을 바꾸게 되면 실인수 자체가 바뀐다.  이러한 참조에 의한 전달은 기억장소를 절약할 수 있다는 장점과 함께 함수에서 주프로그램으로 여러 개의 계산 결과를 전달하고자 할 때 유리한 방법이다. ( scanf()함수를 상기하라)

 C에서는 참조전달이 허용되지 않으나 메모리 주소 연산자(&)와 포인터 연산자를 효과적으로 사용하면 참조전달과 같은 효과를 거둘 수 있다.

   호출측 : func(&a,&b,&c);      <= 각 실인수의 주소값을 전달한다

             ....

   피호출측 : func(int *x, int *y, int *z)   

             {                     

                 *x = 1; *y = 2; *z = 3;

                  ...

 위에서 호출측 프로그램에서는 피호출측 프로그램에 인수를 전달할 때 그 인수가 저장된 주소(&)를 전달하고 있으며, 피호출측 함수에서는 그  인수를 포인터(*)로 받아온다. 포인터란 어떤 값이 가리키는 곳(번지)의  내용을 나타내므로 포인터 자체의 값은 주소와 일치하며 그 주소에 1,2,3을 할당하므로 결국 호출 프로그램의 변수 a, b, c는 각각 1, 2, 3으로  바뀌게 되는 것이다. 그러므로 계산 결과를 함수의 가인수에 기억시켜 두면  주프로그램의 실인수 자체가 바뀌게 되어 여러 개의 계산 결과를 전달할  수가 있다.

 <예제4> 3개의 정수 자료를 전달받아 크기순으로 나열하는 프로그램 작성

<리스트4>

3. 기억 부류 (storage class)

 변수란 어떤 자료를 저장하기 위한 기억 장소로서, 구분방식이 두 가지가 있다. 그 두가지 구분 방식이란, 그 변수가 프로그램의 전체에 통용되느냐 일부분에 국한되느냐는 활용 범위에 따라, 지역 변수(Local variable)와 전역 변수(global variable)로, 또 기억장소와 기억방식에 따라, 자동(auto)변수,     정적(static)변수,  외부(external)변수, 레지스터(register)변수의 4가지 종류로 나뉜다.

1) 지역 변수

 하나의 단위 프로그램에서 선언된  변수를 말한다.  지역 변수는 그 단위 프로그램에서만 통용되며 그 프로그램을 벗어나면 변수의 효력을 상실한다. 우리가 이 강좌에서 여태 사용해 온 모든 변수가 바로 지역 변수이며 main()함수내에서만 사용 가능한 것들이었다. 지금까지는 모든 것을 main()함수에서만 처리하였으므로  전역 변수이냐, 지역 변수이냐 하는 것을 의식하지 않아도 되었다. 그러나 이제부터는  함수를 이용한 프로그램의 모듈화(단위화=구조화)를 해야하므로 변수의 이런 특징을 잘 알아야 한다.

 C는 블럭({}로 묶여진 부분) 내부에서 변수를 선언하는 것을 허용하며,이렇게 선언된 변수는 블럭내부에서만 통용된다. 블럭이 중첩된 경우에는 맨바깥쪽 블럭에서 선언된 변수가 안쪽의 블럭에도 영향을 미치지만  안쪽의 블럭에서 선언된 변수는 바깥쪽에 영향을 미치지 않는다.  만약  변수명이 일치할 경우에는 그 블럭에서 선언된 변수가 선택된다.

  블럭A:   {              

           int a,b,c;                

             ...                     

  블럭B:      {                    

              int k,b,s;             

                ...                  

  블럭C:         {                     변수의 통용 범위

                 int l,m,n;          

                   ...               

                 }                  

              }                     

           }              

  블럭D:   {              

           int a,b,c;      

             ...           

           }                    

  위의 경우에는 블럭A에서 선언된 변수 b를  블럭B에서도 선언하고  있다. 이 경우에 블럭A에서 선언된 변수 b는 블럭B에서는 효력을 발휘하지 못 한다. 그러므로 블럭이 중첩된 경우에는 변수명이 같지 않도록 주의해야  한다. 그러나 블럭이 다를 경우에는 변수명이 동일해도 상관이 없다(블럭A와 블럭D의 경우). 이와 같이 특정한 지역안에서만 통용되는 모든 변수의  무리를 지역 변수라고 한다.


2) 전역 변수( 외부 변수 )

 지역 변수와 상대되는 개념으로 전역 변수라는 것이 있다. 이 것은 말 그대로 프로그램의 모든 부분에 걸쳐서 영향을 미치는 변수를 말한다.  전역 변수는 프로그램 첫머리의 사용자 함수를 선언하는 부분에서 선언한다. 또한 두번째 관점에서의 외부 변수와 전역 변수는 같은 것이다.

    #include <라이브러리 모듈>

      (함수의 선언)     

      (전역 변수의 선언)               일반적으로 *.h화일에 모아둔다.

    main()

      ...

 전역(외부) 변수는 모든 함수에서 통용되며 함수 내부에서 그 값을  바꾸면 전역(외부) 변수 자체의 값이 바뀌게 된다. 즉, 주프로그램과 부프로그램이 변수를 같이 사용하는 것이 바로 전역(외부) 변수이다. 함수에서  전역(외부) 변수를 참조할 경우에는 함수의 첫머리 함수명 다음에  변수명을 같이 기술한다.

   extern type 변수명;


 extern(al)은, 외부에서 선언된 변수임을 나타내는 예약어이다. extern은 외부 변수가 함수의 뒤에서 선언된 경우에는  꼭 기술해야 되지만  앞에서 선언된 경우에는 생략할 수도 있다. 그러나 함수가 main() 앞에 오는 경우라면 반드시 기술해야 한다.


 초보자의 경우에는 주프로그램을 먼저 기술하고 함수는 나중에  기술하려 하겠지만 일반적으로 숙달된 프로그래머는 그 프로그램에서 사용할 함수를 모두 설계해서 기술한 다음 main()은 맨 나중에 배치한다. 처음에는  모든 변수를 전역 변수로 선언해서 사용하는 편이 프로그램을 작성하기에  편리하나, 이런 식으로 전역(외부) 변수를 남용하게 되면  함수의  독립성 즉, 프로그램의 모듈화에 역행을 하게 되므로 되도록 사용하지 말자.


 예를 들어 앞에서 설명했던 최대값을 구하는 함수(max)의 경우에는  어떤 프로그램에서든지 곧바로 삽입하여 사용할 수 있으며, 최대값을 구하는 경우에는 이 함수를 호출하면 된다. 이 때 계산 결과는 함수 이름(max) 자체에 의해 전달되므로 주 프로그램의 변수와  무관하게 되고,  이에 따라 이 함수만 따로 파일로 만들어 두고 필요할 때 마다 불러내어 사용할수 있다.

그러나 이 함수를 전역 변수에 의해 값을 전달할 경우에는 모든  프로그램의 전역 변수명을 일치시켜야 하므로 비효율적이다.

<예제5> 전역(외부) 변수 사용 예 보기.

<리스트5>

==sample1.c 파일==

 #include <stdio.h>

   int x; /*외부변수 선언*/

     main()

       {

         x = 16;

         sub1();

         sub2();

         sub3();

         sub4();

       }

         sub1()

           {

              x++;

              printf("sub1 x=%d\n",x);

           }

         sub2()

           {

              x++;

              printf("sub2 x=%d\n",x);

           }

==sample2.c 파일

 sub3()

   {

      extern int x;

      x++;

      printf("sub3 x=%d\n",x);

      x = 200;

    }

 sub4()

    {

       int x = 34;

       printf("sub4 x=%d\n",x);

    }

== sample.prj 화일 ==

 sample1.c

 sample2.c

 


 이러한 식으로 프로그램을 분할해서 모듈화를 시켜서 분할 컴파일이 가능 하다. prj화일은 프로젝트 화일이라는 것으로 여러개의 소스 화일을  각각 컴파일해서 하나의 실행화일로 만들어 주는 역할을 담당하는 화일하다. 이러한 모듈별 분할 컴파일은 여러분이 64kb를 넘는 대형 프로그램을  작성할 때 반드시 필요한 개념이다.( 자세한 사항은 각 컴파일러 메뉴얼을 참조하기 바란다 ) 프로그램의 실행결과는 여러분이 직접 실행해 보길 바란다.


3) 자동 변수

 자동 변수는 선언된 함수 내부나 혹은 그 함수내의 복문속에서만  효력을 가지고, 그 블럭이나 함수를 벗어나면 자동으로 소멸되는 지역 변수를  일컫는다. 이러한 종류의 변수들은 선언시 메모리를 할당받아 자료를 기억시키는데 쓰이지만 사용후에는 메모리에서 완전히 제거된다. 자동 변수는 변수의 선언시 형 앞에 auto를 붙이거나 아무 것도 붙이지 않는다. 이  때까지 사용해 온 변수들이 지역 변수이자 자동 변수이다. 자동 변수는 초기화하지 않으면 그 값은 미정이며, 컴파일시 변수의 값이 저장될 메모리의 위치만 할당할 뿐 초기화는 프로그램 실행시에 한다.  이 변수는  *그 함수의* 실행이 끝나면 자동적으로 소멸되며,  호출될 때마다 초기값을 가진다.


4) 정적 변수

 정적 변수는 프로그램의 종료 후에도 그 값이 소멸되지 않고 보관되며, 다시 호출될 때는 그 직전의 값을 참조할 수 있다. 정적 변수의 초기화는 프로그램의 컴파일시에 단 한번 하게 된다. 정적변수는 형 앞에 static이라 기술함으로써 선언된다. 예를 들어,

   static int a = 0, b = 0;

   a =+ 1 ; b += 1;

 에 의해 변수 a,b가 정적 변수로 선언되며, 초기값은 0이 된다. 초기값에 0을 대입하는 것은 컴파일시에 단 한번하고, 호출시에는 0이 대입되지  않는다. 이렇게 선언된 변수는 실행이 끝난 후에도 그 값이 남아있게 된다.  그러므로 호출될 때마다 변수 a, b가 1,2,3,4,...로 계속  증가된  결과를 얻게 된다.

 변수를 초기화하는 부분은 위와 같이 반드시 static 다음에  기술해야 한다. 만약  행을 바꾸어 기술하면 컴파일시에 초기화하지 않고 실행중에 초기화가  되므로 애써 기억한 자료가 다시 초기화되어 버린다.  정적 변수는 실행된 후에도 메모리에 상주하므로 기억장소 절약이라는 측면에서 볼 때 바람직한 형태가 아니므로 꼭 필요한 변수의 경우에만  사용하는 것이 좋다. 정적 변수는 지역 변수나 전역 변수에 모두 선언하여  사용할 수 있다.


 <예제6> 함수 내부에서 선언하는 static 변수

<리스트6>

 함수 내부에서의 static선언은 그 함수내의 국소적인 변수가 된다.(위의 예는 지역 정적 변수라고 할 수 있다)  위에서 k는 자동변수이므로 호출할 때마다 초기화가 되고, i는 정적 변수이므로 컴파일시 한 번만 초기화를 행하여 메모리에 초기치 1을  저장하고 뒤에는 sub가 호출될 때마다 +1씩 값을 증가시켜 변화시켜 간다.

<예제7> 함수 외부에서 선언되는 static 변수

<리스트7>


 함수 외부에서의 선언은 그 소스화일 내에서는 어디서나 참조가능한 내부 변수로 된다. (전역 정적 변수) 그러므로 변수값은 프로그램의 종료시까지 보존된다. 위 프로그램의 실행값은 차례로 4, 5, 6이 된다.  만일 다른 화일에서 k를 참조하려면, 그 화일의 선두에 extern static int k;를 첨가하면 된다.

 <예제7>의 경우처럼 외부적인 정적 변수 등의 폭 넓은 영역에 걸쳐  참조 할 수 있는 변수들은 절제해서 사용하여야 한다. auto변수(우리가  지금까지 사용한 변수들)를 되도록 많이 사용하여 프로그램의 모듈화, 구조적 특징을 손상시키지 않도록 하자.


5) 레지스터 변수

 레지스터 변수는 기억할 자료의 값을 메모리에 저장하는 대신 직접 CPU내의 기억장소인 레지스터(register)내에 기억시키는 변수이다. 이것은  for문 등의 제어 변수를 직접 레지스터에 기억시켜 둠으로써 실행 속도를  높일 목적으로 쓰인다. 그 형은 int형에 한한다. 사실 함수에서 선언되는 변수 중 처음 2개의 int형 변수는 자동적으로 레지스터 변수로 사용이  되므로 속도를 요하는 변수(루프안을 도는 변수 등)는 맨 처음에 선언하는  것이 유리하다. 레지스터 변수는 컴파일러에게 지시하는 것이지만 강제적인 것은 아니다.

 레지스터의 갯수가 한정되어 있을 뿐 아니라 또 그 시각에 사용하지  않는 레지스터가 있는지도 미지수이기 때문이다. 만약 사용 가능한  레지스터가 없다면 자동 변수로 할당 된다.

 <기억부류 사용예>

  int a;           /* 외부변수 a의 정의 겸 선언 */

                      (다른 모듈(-화일)에 알려질 수 있다)

  extern int b;    /* 외부변수 b의 선언 */

                       (통상 b는 다른 모듈에 정의되어 있을 것이다)

  static int c;    /* 외부 정적변수 c의 정의 겸 선언 */

                      (다른 모듈에는 알려지지 않는다)

  void main(void)

  {

     int d;        /*  자동변수 d의 정의 겸 선언  */

     auto int e;   /*   "       e        "     (auto는 생략가) */

     static int f; /* 내부정적변수 f의 정의 겸 선언 */

     register int g; /* 레지스터 변수 g의 정의 겸 선언 */

     .......

   }

  <기억부류 선택요령>       참고: Turbo C정복(임 인건 저)

  (1) 피치 못할 경우를 제외하고는 가능한 한 자동변수를 선택

  (2) 외부변수의 사용은 최대한 자제하고 공용성이 매우 높은 변수에 한해 외부변수로 정의한다.

  (3) 초기화가 꼭 필요하거나 공용성을 가지는 배열은 주로 외부형으로 정의한다.

  (4) 모든 외부변수(정적변수 포함)는 가능한 한 초기화를 한다.

  (5) 외부변수는 가능한 한 읽기 전용으로 한다.

  (6) 특정 외부 변수의 값을 변경하는 함수는 가능한 한 하나의 함수로 제한한다.

  (7) 자동변수의 값을 보존할 필요가 있을 경우 내부정적변수로 정의

  (8) 프로그램이 하나의 모듈로 구성되어 있을 경우 외부정적변수를 사용할 필요는 전혀 없다.


 4. 예제들

<예제8> 임의의 정수 n을 입력받아 n의 계승(!)을 구하는 프로그램 작성.

<리스트8>


 10행에서 return()은 함수의 계산 결과를 호출 프로그램으로 되돌리는 명령어이다. return(0)은 그 시점에서 의미가 없는  0을 되돌림으로서  함수 수행을 종료시키는 명령이다. main()도 일종의 함수이므로 return(0)을 사용할 수가 있다.

 <예제9> 두 점 (x1,y1),(x2,y2) 사이의 직선거리는 아래에 식에 의하여 구할 수 있다. n개의 점을 입력받아 각각의 점을 직선으로 연결 할 때의 직선거리의 합을 구하라.

                         ______________________

         ( 두 지점간 거리 = / (x1-x2)^2 + (y1-y2)^2 )

<리스트9>


 2행 : 외부라이브러리의 선언 ( 제곱근을 구하는 함수 pow()의 선언 )

 8행 : 두 지점간의 거리 계산 / 그냥 2를 쓰지않고 2.을 쓴 이유는 pow() 함수의 인수의 형이 실수이기 때문이다.

 10행 : 계산 결과를 되돌림

 15,16행 : 내부 변수의 선언

 22,23행 : 2번째 이후의 좌표 입력

 24행 : 두 지점간의 거리 누적

 25,26행 : 다음 지점을 입력하기 위한 준비


 함수는 프로그램을 기능별로 묶는 모듈러 프로그래밍을 가능하게 한다.

 프로그램은 물론 계산 결과가 정확해야 하지만 같은 계산 결과를  제공하는 것이라면 프로그램을 이해하기 쉽고 또,  디버깅하기  쉽도록 작성하는 것이 좋다.  2장에서 언급하였듯이 C에서는 외부  라이브러리를 컴파일러  제작사에서 제공하는 것은 물론 사용자 자신이 작성하는 것도 허용한다. 위에서  작성 했던 함수들을 적당한 이름으로 디스크에 수록하여 둔다면 그 함수가 필요한 경우에 #include문에 의해여 소스상에 삽입하여 실행할 수 있다.  그러므로 C에서 표준 라이브러리에 없는 함수라 할 지라도 자신이 설계해 둔다면 다른 여타 언어보다 훨씬 더 풍부한 함수 및 처리 체계를 구축할  수가 있을 것이다. 라이브러리를 만드는 법은 참고 서적을 참조하기 바란다.


제 6장 - 배열과 포인터

 지난 장까지의 내용은 여러분들도 무리없이 잘 따라왔을 것이다.  하지만 6장부터는 C의 가장 중요하고도 이해하기 어려운 포인터, 구조체. 공용체, 등등의 난관이 여러분 앞에 버티고 있다. 포인터에 관한 내용으로만 책 한권을 써재끼는 사람이 있는 것으로 봐서는 포인터라는 것은 난해한 것임에 틀림이 없다. 하지만 여기에 수록되는 예제들을 직접  수행시켜 보고,  설명을 잘 읽어 본다면 이해가 그렇게 어려운 것만은 아닐 것이다.


 1. 배열(Array)

 배열과 포인터는 서로 밀접하게 관련되어 있다. 그래서 보통 이 두가지는 동시에 논하여 진다. 먼저 배열에 관해서 알아보자.  배열은 일련된 공간에 저장된 자료의 집합이다. 여기서 자료란 배열 요소라고 하는데 이 요소들은 어떤 규칙에 의해 저장된다.  그리고 전장에서 배운 변수의 기억 부류의 통용 범위의 규칙도  배열에서도 똑같이 적용된다. (배열도 변수의 일종이다)

  int a[10], b[3][6], c, d;       /* 배열선언의 보기 */

 위의 배열 선언에 의해 생성되는 배열의 실체는 아래와 같다.

 

0

1

2

3

4

5

0

 

 

 

 

 

 

0

1

2

3

4

5

6

7

8

9

 

 

 

 

 

 

 

 

 

 

                           

                 

2

 

 

 

 

 

 

배열a                     


 위에서 보듯이 a[10]은 a라는 이름으로 int형의 자료를 넣을 수 있는  방을 10개 확보하라는 명령이다. (1차원 배열이다) 2차원 배열의 경우에는 b[3][6]과 같이 하는데, b라는 이름의 int 배열을 3행 6열로 선언하는 예이다. 지난 장에서 정적변수와 동적 변수를 배웠는데  배열에서도  정적 배열과 동적 배열이 있다. 정적 배열은 앞에 static를 붙이며, 1회에 한하여 모든 배열요소에 초기치를 부여할 수 있다. 초기치는 {}로 묶어서 표현한다. 동적 배열은 전체를 초기화할 수 없고 모든 요소를 하나씩 지정해서  초기화해야 한다.

 1)static char a[6] = {'a','b','c','d','e'};   /* 이 네가지 초기화 */

 2)static char a[6] = "abcde";                 /* 는 모두 쓸 수있다*/

 3)static char a[] = {'a','b','c','d','e'};    /*   (모두 같다)    */

 4)static char a[] = "abcde";      /* 대중적 */

 정적 배열의 경우 배열의 크기를 선언하지 않으면  컴파일러가  초기치의 갯수대로 방의 갯수를 결정해 준다. 그리고 문자 배열은 배열의 끝에 문자열의 끝임을 알리는 자동적으로 '\0'이 추가되므로  1), 2)의 경우처럼 문자열을  배열내에 저장할 때는 문자열의  길이보다 적어도 1개 이상 크게 배열 크기을 확보하여야 한다.(위에서 주로 많이 쓰이는 양식은 4번이다)

이차원 정적 배열에 초기화를 행하는 예를 보자.

   static int b[2][5] = {0,1,2,3,4},{5,6,7,8,9};

   static int b[2][5] = {{0,1,2,3,4},{5,6,7,8,9}};

 위의 두가지 표현은 서로 동일한 것이다.

 배열명은 배열의 선두 번지를 가리키는 포인터 상수이다. 그러므로  변수처럼 값을 대입할 수는 없지만 그 값을 참조하여 배열의 1행, 혹은 배열의 요소 전부를 읽어낼 수 있다. 다음과 같은 배열을 생각해 보자.

 1) static char c[3][7] = {{"ABCDEF\0"},{"GHIJKL\0"},{"MNOPQR\0"}};

 2) static char c[3][7] = {"ABCDEF\0",GHIJKL\0","MNOPQR\0"};

 3) static char c[3][7] = {"ABCDEF",GHIJKL","MNOPQR"}; 

 4) static char c[][7] = {"ABCDEF","GHIJKL",MNOPQR"};  /* 대중적 */

 위의 세가지 표현은 모두 동일한 표현이다. ( '\0'은 자동적으로  붙으므로 생략이 가능하다 ) 4번의 경우처럼 배열 첨자를 생략할 수 있으나 마지막 첨자까지는 생략할 수 없다. 그러면 저장 형태를 그림으로 보자.

 

0

1

2

3

4

5

6

 

0

A

B

C

D

E

F

\0

배열 c[3][7] 요소의 지정과 출력 형태

1

G

H

I

J

K

L

\0

printf("%c",c[1][2]);  => I 출력

2

M

N

O

P

Q

R

\0

printf("%s",c[1]; => GHIJKL  출력

 

 

 

 

 

 

 

 

* 문자열 출력이므로 %s 사용



배열c         



배열에서의 중요한 점을 간추려 보면 다음과 같다.

 1) 정적 배열이 아닌 이상 배열 크기의 기억 장소만을 확보할 뿐  거기에 대한 초기화는 하지 않는다.

 2) 배열명, 혹은 세로측(행)의 첨자만 기술하여 배열 전체, 혹은 그 행의 전체를 나타낼 수 있다.

 3) 배열 크기를 선언할 때 변수를 사용하지 못한다. 즉 int arr[n];과 같은 문장은 에러이다.

 4) 배열 첨자가 허용 범위를 벗어났는지의 여부를 조사하지 않는다.


<예제1> 초기화 되지 않은 배열의 값(정적 배열, 자동 배열)

<리스트1>


 위 프로그램을 실행시키면 자동 배열은 기억 장소만을 확보할 뿐 초기화하지 않으므로 그 기억장소의 쓰레기 값이 출력되며,  정적 배열은 초기화하지 않았을 경우 자동으로 0으로 채워지므로 0이 출력될 것이다.

<예제2> 배열의 초기화 - 크기보다 초기값의 갯수가 적은 경우

<리스트2>


 위 프로그램에서 days는 외부 배열로 선언되고 초기화되었다. 외부  배열도 정적 배열과 같이 초기화하지 않으면 0으로 자동적으로 채워진다. 위에서는 초기화를 하긴 하였으나, 배열요소 12개 전체를 한 것이 아니라 10개밖엔 초기화하지 않았다. 하지만 컴파일러는 자동적으로 남은  요소를 0으로 채워 버린다. 출력 결과를 보면 알 수 있을 것이다.


 배열 선언시 첨자를 생략하는 경우는 어떻게 될까? 1행을

  int days[] = {31,28,31,30,31,30,31,31,30,31};

 로 바꾸면 출력 결과는 10월까지만 나올 것이다. 왜냐하면 자동적으로 컴파일러가 배열 크기를 초기화 갯수와 같은 10으로 간주하기 때문이다.  배열 첨자를 생략할 경우에는 초기화시 주의가 필요하다.


 그리고      int i, arr[10];

             for (i=0;i<10;i++) {

                arr[i] = i+1;

                printf("arr[%d] = %d\n",i,i+1);

             }

 와 같이 배열 선언과 동시에 초기화하지 않을 시에는 절대로 배열 첨자를 생략하면 안된다. 위의 예는 10개의 방에 1에서 10까지 대입하는 보기이다

(** 변수나 배열은 선언과 동시에 초기화 (0으로라도) 하는 습관을 가지는게 쓰레기값 해소에 좋다 **)


<예제3> 1 - 12월의 이름을 영어로 배열에 기억시켜두고  1-12 의 수치를 입력하여 그에 대응하는 월의 이름을 출력하는 프로그램 작성.

<리스트3>


 6행 : 문자열 자체는 1차원 배열로 간주되므로 당연히 2차원 배열로 선언. 초기화에서 행의 갯수가 명확히 제시되므로 첫 배열 첨자 생략 가능 (마지막 첨자 [10]은 생략하면 않된다.)

 6행 - 9행 : ~C에서는 ;가 있어야 문장의 끝으로 간주하므로 한 행을 보기 좋게 여러 행에 걸쳐 기술할 수 있다.

 12행 : C에서는 첨자의 범위가 유호한 것인지 체크하는 일을 프로그래머가 알아서 해야 한다. 즉, 우리가 15를 입력해도 결과는 나온다.



<예제4> 매개 변수가 배열인 함수 (거품정렬을 행하는 프로그램)

<리스트4>

 1행 : 내장 함수인 ramdomize(),random()함수는 stdlib.h에 선언되어 있다

 2행 : #define은 매크로로써 프로그램 안의 MAX를 20으로 치환하라는 의미이다. 예를 들어 이런 식으로 매크로를 지정해 두면 x[]배열의  크기를 변화시킬 필요가 있을 때  x[]배열이 사용된  모든 부분을 찾아가 수를 변화시켜야 한다. 하지만, 이 경우에는 #define MAX 20에서  20을 다른 수로만 바꿔주면 된다.(매크로에 관해서는 마지막 장에서 공부할 것이다)

 4행 : 사용자 함수의 선언

 10행 : 난수 발생자를 초기화 하는 함수

 11행 : random(n)은 0에서 n-1까지의 정수 난수를 발생시켜 돌려주는 함수

 14행 : x는 배열 x[20]의 첫번째 원소 x[0]의 번지를 가리키는 포인터이다

 19행 : void bubble(x,n);

        int x[];          ===>  void bubble(int x[],unsigned n)

        unsigned n;      

        가인수 형선언을 함수명의 가인수 리스트에 위와 같이 포함시킬 수 있다.

 19-30행 : bubble()함수는 버블 정렬을 하는 함수이다(각종 정렬의 원리는 자료구조 관련 서적을 참고하길 바란다).

 배열도 변수처럼 함수끼리 주고받을 수 있다. 14행과 같이 배열명과 배열의 크기를 넘겨주면,  피호출측 함수는 배열을  처리할 수 있다. 배열명은 배열을 가리키는  주소값을 가지고 있으므로 참조에 의한 전달에 속한다고 할 수 있다. 그러므로 bubble()함수에서는 배열의 내용을 직접 바꾸는  것이 가능한 것이다.  ( 5장 #1 참고 )

<예제5> 매개 변수가 배열인 함수 - II

<리스트5>


<예제6> 2차원 배열을 함수에 넘겨주는 예제

<리스트6>


2. 포 인 터 (Pointer) - 포인터는 주소값(번지)이다.

 포인터는 다른 변수의 주소를 기억하는 변수이다. 포인터는 포인터  연산자 '*'를 이용해서 선언한다. 포인터는 번지를 기억한다고 했으므로  연산자 '&'와 동일하다고 생각할지 모르겠으나 실제로는 전혀 다르다.'&'는 번지 자체를 말하는 것이지만  포인터 변수는 그 포인터가 가리키는 '번지의 내용'을 말한다. 이와 같이 포인터를 이용하여 자료를 지정하는 것을 ' 자료의 간접 지정'이라고 말한다.

 포인터 변수는 그 포인터가 가리키는 실체의 형에 맞추어 선언해야 한다.

정수형의 자료를 가리키는 포인터라면 정수형으로 선언되어야 하며, 장차 실수형의 자료를 가리킬 포인터라면 실수형으로 선언해야 한다. 또한 문자열이나 단일 문자를 가리킬 포인터는 문자형으로 선언해야 한다.  예를 들어,

   int a = 10, b = 20, c = 30, *p;

   p = &a;

 는 정수형 변수와 정수형 포인터 변수를 선언하고 초기화한 것이다. 여기서 변수 a,b,c가 연속된 공간에 저장되며  그 선두 번지가 1000H라고 가정 할 경우 저장형태는 다음과 같다.

a

b

c

10

20

30

1000H

1002H

1004H

 이 때 &a는 1000H, &b는 1002H, &c는 1004H가 된다. 여기서 'p=&a'라고 명령하면 포인터 p의 실체는 1000H가 되지만  우리에게 되돌려 주는 값은 1000H에 저장된 값 즉 10이 된다.

 그리고 p += 1;에 의해 p이 값을 하나 증가시키면 포인터 p의 값은  그 자신이 가리키는 자료의 형(int)에 맞추어 자동으로 2가 증가되어 1002H가 되고 우리에게 20이라는 값을 되돌려 준다. 이와 같이 포인터를 이용하면 임의의 메모리 번지에 접근해서 그 값을 꺼낼 수 있게 된다. 또한 프로그래머는 포인터의 형만 적절히 지정하면 굳이 메모리 번지를 의식하지 않아도 메모리내의 자료를 정확히 엑세스할 수 있는 것이다. 즉, 포인터의 형이 정수형이면 연속된 2Byte이 값을 꺼내와 정수로 치환하여 돌려주고, 포인터가 실수형으로 선언되었을 때는 일련의 자료들을 실수형에 맞추어 변환한 다음 돌려준다.

(*하지만 볼랜드 C의 경우에는 변수가 메모리 공간에 역순으로 저장이 된다. 여기서는 설명의 편의상 TC를 기준으로 하였다*)

 포인터가 문자형일 경우에는  그 위치의 문자를 되돌려 주거나 '\0'을 만날 때까지의 문자열을 돌려준다.


<예제7> 정수형 변수에 각각 10,20,30,40,50,60을 기억시킨 후 포인터를 참조하여 그 값을 출력하는 프로그램 작성.

<리스트7>

변수(x)

주 소(&x)

값(*p)

포인터(p)

a

FFD2

10

FFD2

b

FFD4

20

FFD4

c

FFD6

30

FFD6

d

FFD8

40

FFD8

e

FFDA

50

FFDA

f

FFDC

60

FFDC

 <실행결과>  # TC의 경우이다.            

옆의 표에서 보듯이 포인터 자체의 값은 변수의 번지와 동일하나, 포인터를 참조한 값은 변수의 내용과 일치한다. 여기서 주목할 것은 p++로 포인터를 1씩 증가시키고 있지만 포인터는 자신의 형인 int형에 맞추어 2씩 증가하고 있다는 점이다.

  그렇다면, 여러분들이 직접 변수들을 실수형으로 선언하여, 실수형 포인터에서는 어떤 현상이 일어나는지 실행해 보라.

# 주소는 시스템에 따라 다르다.     <참고>. 문자열 포인터(문자열을 가리키는 포인터) - '터보 C 정복'

우선 예제를 보면서 시작하자.


     (실행결과)

     abcdefghijklmnopqrst

     abcdefghijklmnopqrst

     2000                 <--- 시스템에 따라 다르다.

     2000                       (두 개의 값이 같다는 점에 주목하라)

     Str length(array) : 20

     Str length(ptr)   : 20

     array[0] - a

     array[9] - j ptr[9] - j

     End of ptr: 0

     *(ptr + 9) = j

     ================================

문자 배열 array(상수)

a

b

c

d

e

f

g

h

i

j

k

l

m

n

o

p

q

r

s

t

\0

6행 수행 :

             

7행 수행 :   포인터 ptr -+   (변수)

 6행을 수행하면 배열 array는 내부적으로는 문자열"abcd.."가 저장된 공간의 첫째 영역의 주소값(2000)을 가지게 된다.   배열명은 그 배열의 시작점의 주소값을 가지는 <포인터 상수>이다.

  7행에서 문자배열의 시작 주소값(array)을 포인터 <변수>ptr에 대입하였음으로 ptr의 값도 array와 같은 주소값(2000)을 가지게 된다.  하지만 개념적으로는

         <ptr은 문자열"abcd..."의 선두번지를 가리킨다> 라고 한다.

9행, 10행 수행 :

 printf함수의 %s서식은 인수를 문자열 포인터로 인식하고 그 포인터가 가리키는 번지부터 널 종료문자(\0)까지의 문자들을 출력하는 기능을 가진다 그러므로, array와 ptr가 가리키는 주소를 찾아가 그 곳에 있는 문자(a)로부터 \0문자까지 출력을 하는 것이다.

11행, 12행 수행 :

 %p서식은 인수가 가지는 주소값을 출력하라는 서식이다. 위에서 array와 ptr을 같은 주소값을 가지는 것을 알 수 있다.

13행, 14행 수행 :

 strlen함수는 포인터를 전달받아 그 포인터가 가리키는 번지부터 널 종료문자 직전까지의 문자의 갯수를 세어서 정수값으로 리턴한다. 위에서 array와 ptr이 같은 문자열 영역을 가리킴을 알 수 있다.   포인터의 입장에서 보면 문자열과 문자 배열을 전혀 구분할 수 없고, 완전히 동등하게 취급된다. 문자 배열을 문자열로 간주하면 문자 배열이 곧 문자 배열이 되는  것이다.

15, 16, 17행 수행 :

 char array[21] = "...."배열에서 일반적으로 array[n]은 n번째(처음이 0번째) 문자를 나타내는 것을 잘 알고 있을 것이다. 그러면, ptr[n]이란 것은 무엇을  의미할까? 곧, <문자열> 포인터ptr이 가리키는 번지부터 n번째 문자열 요소를 가진다. 

 위에서 보듯이 배열과 포인터는 매우 밀접한 관계를 가짐을 알 수가 있다

 17행에서 strlen(ps)의 결과는 20이다. 그러므로 ptr[strlen[ptr])은 ptr[20]과 동일하다.  위의 그림에서 그 위치에는 널 종료문자 \0이 있으므로 화면에는 0이 출력된다.

18행 수행:

 px + 1; 의 의미는 과연 무엇일까?

이것은 px가 가리키는 배열 요소의 바로 다음 배열요소를 가리킨다

                               (배열요소가 있는 곳의 번지값을 가진다)

 여기서 주의 할 점은 px 바로 다음 바이트의 번지를 가리키는 것이 아님에 유의해야 한다. px+1이 바로 다음 바이트의 번지를 가리키게 되는 경우는 px가 문자열 포인터일 때뿐이다.(각 요소가 1byte크기의 문자이므로) 만일 px가 int형 포인터라면 px + 1은 2바이트 다음의 번지를 가진다.

 이렇게 C에서는 포인터가 무슨 형임에 상관없이 자동적으로 번지를 증가시켜줌에 유의해야 한다.

 그럼, ptr+9의 의미는 무엇일까? ptr이 가리키는 번지(a가 저장되어 있는 곳의 번지)로부터 9번째 요소가 저장되어 있는 곳(j)의 번지를 가리킨다.

 그런데 위에서 *(ptr + 9)라고 했다.  이것의 의미는 (ptr + 9)가 가리키는 곳에 있는 내용을 나타내라는 의미이다.

출력결과를 두고 보았을 때

라는 사실을 유추할 수 있다. 아예, 위 문장을 암기해 버리자.


     

a

b

c

d

e

f

g

h

i

j

k

l

m

n

o

p

q

r

s

t

\0


    ptr+0 ptr+2 ....             ptr+9  ==> 이 각각의 내용은 주소값

   *(ptr+0) ,  .....            *(ptr+9) ==> 이 각각의 내용은 문자

                            &array[9] == &(*(ptr+9)) == ptr+9

                             array[9] == *(ptr+9) == 'j'

###################################################################

* 필자가 본 책 중 포인터에 관하여 가장 자세히 설명된 책은 임인건 (turbo28)님의 강좌들과 '터보C정복'이었다. 하이텔 한글프로그래밍동호회의 초청강좌란에  포인터에 대해 집중적으로 파헤친 '정복 터보 C' 라는 강좌를 반드시 받아보길 권한다.

3. 포인터와 배열

 포인터는 배열과 아주 밀접한 관계를 가진다. 실제로 배열명은 배열의 선두 번지를 가리키는 '포인터 상수'이다. 배열은 그 내용이 배열 영역에 저장되나 문자열을 포인터 변수로 지정할 경우에는 그 내용이 단순 변수  영역에 저장된다. 또 배열은 첨자의 값을 계산해야 하지만  포인터는 연속된 메모리 영역의 '내용'을 꺼내오므로 처리속도도 빠르다.

 만일 arr[]가 배열이라면,

   arr == &arr[0] (배열명은 배열의 첫번째 요소의 번지를 나타내는)

                               ( 포인터 상수이다 )

<예제8> 포인터와 배열

<리스트8>


  위 프로그램의 실행 결과에서 번지의 증가량을 주목하라!             

  위의 예제는 배열과 포인터의 밀접한 관계를 보여준다.  이것은 배열의 각 원소를 구별하고  그 원소의 값을 획득하는데  포인터를 사용할 수 있음을 의미한다. 실제로 컴파일러는 배열 표기를 포인터로 변환시켜 처리한다.

 *(date+2)와 *date + 2는 엄연히 다른 것이다. 포인터 연산자'*'는 +보다높은 우선순위를 가지고 있으므로 후자는 (*date)+2를 의미한다.

     *(date + 2)  => date의 세번째 원소의 값

     *date + 2  => 첫번째 원소의 값에 2를 더한 값

 배열과 포인터의 이러한 관련성 때문에 프로그램 작성시 아무것이나 선택하여 사용할 수 있다. 함수가 배열을 인자로 넘겨받는 경우가 그 일례이다

.          ( <예제5> , <예제6> , <예제7> 참조 )


 그럼, 아래의 두 가지 비교를 보자.

*** 배열을 사용한 평균값을 구하는 함수

*** 포인터를 사용한 배열의 평균값을 구하는 함수


  그렇다면, 위의 두 함수를 호출하는 호출문은 어떻게 될까?

   mean(numb,size);

 위의 numb는 배열명이다. 앞에서 이야기 한 것 처럼,  int pa[]; 와 int *pa;는 같은 것이다.

 단지 차이점은 pa[]에서의 pa는 포인터 상수(변화시킬수 없다)이고, *pa에서의 pa는 포인터 변수(변화시킬 수 있다)라는 것이다. 다시 말해서 포인터 상수 pa는 pa++라는 표현이 불가능하나 포인터 변수 pa는 pa++는 표현이 가능하다.  비록, 배열과 포인터가 위와 같이 밀접하게 관련되어 있다고는 하지만 차이점도 가지고 있다. 주로 포인터가 훨씬 광범위하게 이용된다. 하지만 초보자들은 배열이 더 분명하고 혼동이 덜 간다고 느낄 것이다. 그러나,  여러가지 이점으로 볼 때 포인터가 훨씬 효율성이 높다고 할 것이다.


 문자열을 포인터 변수로 초기화했을 경우에는 배열처럼 처리할 수 있다.

 <배열과 포인터의 비교>

'Programming > VC++' 카테고리의 다른 글

[VC++] C/C++/MFC FAQ 모음  (0) 2007.08.01
[C] const와 pointer의 조합  (0) 2007.08.01
[C] 소켓 기본 함수  (0) 2007.08.01
[C] 고수준 파일 입출력 함수 정리  (0) 2007.08.01
[C] 시스템 호출 함수 정리  (0) 2007.08.01