2009년 4월 27일 월요일

씹어먹는 C 언어 - <5. 문자 입력 받기>

이번 강좌에서는...
  • 문자를 저장하는 변수
  • scanf 의 사용
  • 섭씨 → 화씨 환산 프로그램
등을 배우게 됩니다.


  지난번 강좌는 잘 이해 되셨는지요? 이번 강좌에서는 제목에서도 볼 수 있듯이 두 가지 내용을 한꺼번에 배우게 됩니다. 바로, 문자를 키보드로 부터 입력을 받는 것이지요. 문자를 입력 받을 수 있다면, 숫자도 당연히 입력 받을 수 있게 됩니다. 즉, 이번 강좌에서는 문자 형식의 변수와 키보드로 부터 입력을 받는 입력에 대해 알아 보도록 하겠습니다.


  일단, 컴퓨터에서 문자를 처리하는 방식에 대해 생각해 봅시다. 제가 누누히 말하지만 우리의 컴퓨터는 그다지 똑똑하지 못합니다. 아무리 최신 Intel CPU 를 장착해도 컴퓨터는 단지 0 과 1 만을 처리할 뿐이죠. 따라서, 2 와 3 같은 숫자도 처리하지 못하는데 어떻게 a ,b, 가, 나, 韓 과 같은 수 많은 문자를 처리할 수 있겠습니까? 하지만, 방법이 있습니다. 이러한 문자들을 숫자에 대응시키는 것입니다. 그런데, 숫자에 대응시킨다면 컴퓨터가 이 것이 숫자인지, 아니면 문자인지 어떻게 알까요? 물론 알 방법은 없습니다. 단지 이 숫자를 '문자' 형태로 사용하거나 '숫자' 형태로 사용하는 것이지요.

  문자를 저장하는 변수는 앞에서 살짝 본 적이 있습니다. 바로 char 이지요. int 가 integer 의 약자였다면 char 은 character 의 약자 입니다. 변수가 등장하면 어김없이 등장하는 아래의 표를 살펴 봅시다.


  보시는 것과 같이 char 은 맨 위에 위치해 있으며 크기는 1 바이트 입니다. 또한, 이를 통해 나타낼 수 있는 숫자의 범위를 알려주고 있는데, 이는 -128 부터 127 까지, 256 가지 입니다.

/* 문자를 저장하는 변수  */
#include <stdio.h>
main()
{
    char a;
    a = 'a';

    printf("a 의 값과 들어 있는 문자는? 값 : %d , 문자 : %c \n",a,a);
}

위 소스를 성공적으로 컴파일 했다면


  위와 같이 나옵니다. 일단, 소스를 분석해 보겠습니다.

    char a;

  이 부분은 char 형 변수를 선언하는 부분입니다. 기억이 안나시는 분들은 3강을 참조하세요.

a = 'a';


  이 부분은 a 라는 변수에 문자 a 를 대입하고 있습니다. 이 때, 모든 문자들은 모두 작은 따옴표로 묶어 주어야 합니다. 만약 작은 따옴표로 묶지 않고 그냥 썼다면

a = a;

  C 컴파일러는 이 a 가 변수 a 라고 착각하여 a 라는 변수의 값을 a 라는 변수에 대입하는 문장으로 인식하게 되죠. 따라서 a 에는 아무런 값이 들어있지 않은 쓰레기 값(NULL) 이 되어 나중에 a 라는 문자를 출력해 보았을 때, 이상한 값이 나오게 됩니다.
  문자를 대입하는 것도 숫자를 대입하는 것과 동일합니다. 대입 연산자를 이용하면 되죠.

    printf("a 의 값과 들어 있는 문자는? 값 : %d , 문자 : %c \n",a,a);

  마지막으로, 위 printf 문에 대해 보도록 하겠습니다. 앞에서 말했듯이 컴퓨터는 a 가 문자라는 것 자체를 모른다고 했습니다. 단지 우리가 a 를 문자로 보느냐 아니면 숫자를 보느냐에 따라 달라진다고 했는데, 이 말 뜻을 위 printf 문을 보면 알 수 있습니다.

  일단, %d 는 a 의 값을 숫자 (정수인 10 진수) 라고 출력하라는 뜻입니다. 그 옆의 %c 는 아마 예상했겠지만 a 의 값을 문자로 출력하라는 뜻이지요. 따라서, %c 에는 a 에 저장되어 있던 문자 'a ' 가 출력되게 됩니다. 그렇다면 %d 에는 무엇이 출력되었을까요? 앞에서 말했지만 컴퓨터는 문자와 숫자를 일대일 대응 시켜서 생각한다고 했습니다. 따라서, %d 에 출력되는 숫자가 바로 a 에 대응되는 숫자를 가리킵니다.

  이 때, 각 문자 마다 대응되는 숫자를 아무렇게나 하는 것이 아니라 일정하게 정해져 있는데 현재 우리가 쓰고 있는 컴퓨터에서는 다음과 같이 정의되어 있습니다.


    위 표는 미국 표준 학회(ASA) 에서 정한 아스키(ASCII, American Standard Code for Information Interchange) 코드로 8 비트 데이타를 이용하여 여러 문자에 번호를 붙인 것 입니다. 아까, a 의 숫자 값을 출력하였을 때 97 이 나왔는데 위 표에서 찾아 보면 a 의 값이 97 임을 볼 수 있습니다. 이 때, 위 표의 내용이 0 부터 127 까지 밖에 없는 이유는 위 표준을 정할 당시 그 당시 7 비트 만으로 충분하다고 생각했기 때문이죠. 하지만 IBM 에서 좀 더 많은 종류의 문자가 필요하게 되자 1 비트를 더 추가 시켜서 확장된 아스키 코드(Extended ASCII Code) 를 만들었습니다.

  하지만 위 256 개 가지고는 충분하지 못하죠. 왜냐하면 우리 글만 해도 자모음 24 개로 구성되어 있는데, 한 글자당 최대 초성/중성/종성 을 모두 표현해야 합니다. 또한 더욱 심각한 것은 한자와 같은 표의문자의 경우 수만 개가 넘는 한자 데이터들을 가지고 있어야 하는데 이를 256 개 안에 다 표현한다는 것은 불가능하기 때문이죠. 따라서, 컴퓨터가 전세계에 보급되자 좀 더 많은 종류의 문자를 표현해야 한다는 필요성이 대두되었습니다.

  결국에는 유니 코드(Unicode) 라는 새로운 형식의 문자 체계를 도입하게 됩니다. 유니코드는 한 문자를 2 바이트로 처리하였습니다. 이는, 기존 아스키의 1 바이트 체계 보다 대응할 수 있는 숫자의 양이 2 배가 늘어난 것이 아니라 256 배가 늘어나, 총 65536 가지의 문자를 처리할 수 있게 되었습니다. 유니코드는 이렇게 막강하여 현재까지 대부분의 언어의 문자 체계를 모두 표시하고도 2만 개 가량 더 대응시킬 수 있는 숫자가 남았다고 합니다. 따라서, 한글, 한자 등 영어를 제외한 대부분의 문자는 2 바이트를 차지합니다.

  하지만 2 바이트나 차지하는 유니코드는 다루기가 조금 복잡하여 이번 강좌에는 영어 공부도 할 겸 영문으로만 다루기로 하겠습니다.

/* 섭씨온도를 화씨로 바꾸기  */
#include <stdio.h>
int main()
{
    double celcious; // 섭씨 온도

    printf("섭씨 온도를 화씨 온도로 바꿔주는 프로그램 입니다. \n");
    printf("섭씨 온도를 입력해 주세요 : ");
    scanf("%lf",&celcious); // 섭씨 온도를 입력 받는다.

    printf("섭씨 %f 도는 화씨로 %f 도 입니다 \n", celcious, 9 * celcious / 5 + 32);

    return 0;

}

위 소스를 성공적으로 컴파일 했다면 아래와 같이 나옵니다. 참고로 경고가 1 개 정도 나올 수 있는데, 이는 scanf 의 보안 문제 때문입니다. scanf 를 scanf_s 로 바꾸면 되지만 지금 이 수준에서 보안문제에 크게 신경 쓸 것은 없으므로 그냥 scanf 로 하셔도 됩니다.


이 때, 원하는 숫자를 쓴 후 엔터를 누른다면 (예 : 100)


 위와 같이 섭씨가 화씨 온도로 변경된 값이 출력됩니다. 와우! 드디어 쓸만한 프로그램을 처음으로 만들어 보게 된 것 같군요. 소스 코드를 찬찬히 살펴 보도록 합시다.

    double celcious; // 섭씨 온도

  일단, celcious 라는 double 형 변수를 선언하였습니다. 변수의 이름을 종전의 a , b 에서 celcious 라고 한 이유는 좀 더 이해하기 편하기 때문이죠. 좋은 소스 코드의 조건은 다른 사람이 이해하기 쉬운 소스 코드 이고, 다른 사람이 이해하기 쉬운 소스코드는 기본적으로 변수 이름을 보고도 변수를 한 눈에 파악하기 쉽게 만드는 것입니다.

    scanf("%lf",&celcious); // 섭씨 온도를 입력 받는다.

  이제, 새로운 함수가 등장하였군요. printf 에 이어 등장한 scanf 군. printf 가 화면에 결과를 출력해 주는 함수였다면, scanf 는 화면(키보드) 로 부터 결과를 받아들이는 입력 함수 입니다. 이렇게 흔히 printf 와 scanf 를 가리켜 입출력함수라 하죠. 이 때, scanf 함수는 우리가 어떠한 입력을 하기 전까지 계속 기다립니다. 또한, 입력을 할 때 엔터를 눌러야지만 입력으로 처리됩니다.

  scanf 와  printf 는 이름도 비수무리 할 뿐더러, 사용하는 방법도 비슷합니다. printf 에서 각 변수를 출력할 포맷(%d, %f, %c 등) 을 변수마다 다르게 하는 것처럼 scanf 도 각 변수의 타입마다 입력받는 포맷을 달리 해야 합니다.
 
  위 경우 처럼 double 형의 변수를 입력 받으려면 %lf (LF 이다, if 가 아니다) 로 해야 한다. 그런데, printf 보다 조금 까다로운 점은 printf 는 double 이나 float 모두 %f 로 출력하지만 이에 경우 flaot 은 %f 로 무조건 입력 받아야 한다. 마찬가지로 double 형 변수도 무조건 %lf  로만 입력 받아야 한다. 그 외에도, printf 는 정수형 변수는 모두 %d 로 출력 가능했던 반면에 scanf 는 각 자료형 마다 포맷이 다 정해져 있다. 아래 예제에서 잠시 scanf 의 포맷 들에 대해 정리해 보도록 할 것이다.

    printf("섭씨 %f 도는 화씨로 %f 도 입니다 \n", celcious, 9 * celcious / 5 + 32);

  마지막으로 위 프로그램의 중요한 부분을 살펴보자. 바로 이 부분에서 섭씨와 화씨의 환산 작업이 이루어 진다. 참고로, 화씨와 섭씨의 변환 공식은 아래와 같습니다.

http://www.sitmo.com/gg/latex/latex2png.2.php?z=100&eq=%5Eo%20F%20%3D%20%5Cfrac%7B9%7D%7B5%7D%20%7B%7D%5Eo%20C%20%2B%2032%20

 따라서, 이 공식을 그대로 C 언어 수식을 바꾼 것이 
9 * celcious / 5 + 32 인 것이다. 곱셈과 나눗셈의 우선순위가 높으므로 9 * celcious / 5 가 먼저 계산 된 후 32 가 더해지므로 위의 식과  일치합니다. 따라서, 결국 printf 의 두번째 %f 부분에는 위 계산된 화씨의 값이 들어가게 되는 것입니다.

/* scanf 총 정리  */
#include <stdio.h>
int main()
{
    char ch; // 문자

    short sh; // 정수
    int i;
    long lo;

    float fl; // 실수
    double du;
   
    printf("char 형 변수 입력 : ");
    scanf("%c", &ch);

    printf("short 형 변수 입력 : ");
    scanf("%hd", &sh);
    printf("int 형 변수 입력 : ");
    scanf("%d", &i);
    printf("long 형 변수 입력 : ");
    scanf("%ld", &lo);

    printf("float 형 변수 입력 : ");
    scanf("%f", &fl);
    printf("double 형 변수 입력 : ");
    scanf("%lf", &du);


    printf("char : %c , short : %d , int : %d ", ch, sh, i);
    printf("long : %d , float : %f, double : %f \n",lo,fl,du);
    return 0;
}

성공적으로 컴파일 후 (경고가 6 개 정도 나올 수 있는데 무시하세요^^)



     printf("char 형 변수 입력 : ");
    scanf("%c", &ch);

  일단, 제일 먼저 문자를 입력 받는 부분을 봅시다. 예전에도 이야기 했지만 한글은 2 바이트를 차지하기 때문에 최대 1 바이트를 차지하는 char 형 변수인 ch 에 한글을 치면 오류가 납니다. 이와 같이 허용된 메모리 이상에 데이터를 집어넣어 발생하는 오류를 버퍼 오버플로우(Buffer Overflow) 라고 하며 보안 상 매우 취약합니다. 뿐만 아니라 근처의 데이터가 손상됨에 따라 큰 문제가 발생하게 될 수 도 있습니다.  따라서, 여러분들은 버퍼 오버플로우가 일어나지 않게 허용된 데이타 이상을 집어넣는지 안집어 넣는지 검사할 필요성이 있습니다.

  또한 앞으로 우리가 char 형 변수를 선언할 때 에는 '이 사람이 문자를 보관하는 변수를 선언하는 구나' 라고 생각하도록 합시다. 왜냐하면 보통 수 데이타를 보관하는 변수로는 int 를 쓰지 char 을 잘 쓰지 않을 뿐더러 char 이름도 character 에서 따왔을 만큼 문자와 무언가 관련이 있기 때문이죠.

    printf("short 형 변수 입력 : ");
    scanf("%hd", &sh);
    printf("int 형 변수 입력 : ");
    scanf("%d", &i);
    printf("long 형 변수 입력 : ");
    scanf("%ld", &lo);

 
  이 부분은 여러분들이 무난하게 이해하실 수 있으리라 봅니다. 단지 포맷에 %hd, %d, %ld 로 다른 것 뿐이지요. 참고로 short 형이나 long 형은 아직 다루지는 않았지만 int 와 똑같은 계열의 정수형 변수라고 생각하시면 됩니다.

    printf("float 형 변수 입력 : ");
    scanf("%f", &fl);
    printf("double 형 변수 입력 : ");
    scanf("%lf", &du);

  마찬가지로 float 형에서는 %f 로, double 형에서는 %lf 로 사용한다는 것을 기억하시기 바랍니다.

  이번 강좌는 지난번 강좌보다는 조금 짧습니다. 하지만 이번 강좌를 통해 응용할 수 있는 것들이 무궁 무진해졌습니다. 일단, 기본적으로 연습하실 것은 단위 환산 프로그램을 만들어 보세요! 아니면, 금리와 원금을 입력 받아서 일정 개월 후의 상환할 돈이라 든지 등등... 수 많은 프로그램을 만들 수 있습니다. 지금, 이러한 것들을 만들 수 있는 모든 도구들은 준비되어 있습니다. 이제 여러분이 스스로 창작할 세계가 남아 있을 뿐입니다.
 

2009년 4월 24일 금요일

씹어먹는 C 언어 - <4. 계산하리 >

이번 강의에서는
  • 산술 연산자
  • 대입 연산자
  • 비트 연산자, 쉬프트 연산자
  • 변수에 관한 추가적인 내용
  • 산술 변환, 우선 순위
등을 배우게 됩니다. 



  안녕하세요 여러분. 지난 강의에서 모두들 변수에 대해 감이 잡혔을 것이라 믿고 강의를 진행하도록 하겠습니다.

  최초의 컴퓨터는 무엇을 하기 위해 태어났을까요? 오락용? 영화 시청? (물론 그 때에는 불가능 했을 터이지만). 아닙니다. 최초의 컴퓨터라 일컫어 지는 에니악(ENIAC.. 물론 에니악이 최초의 컴퓨터이냐 아니냐에 관한 논쟁은 길다. 한편에서는 콜로서스라는 주장도 있는데 아무튼) 은 포탄을 어떤 각도로 발사했을 때, 어디에 떨어질 지를 예측하는 기계였습니다. 단지, 계산 만 할 뿐. 그 후에서 수 많은 컴퓨터들이 나왔지만 20세기 후반 까지만 해도 컴퓨터는 단지 계산에 이용될 뿐이였습니다. 물론 지금도 컴퓨터의 가장 중요한 역할은 인간이 할 수 없는 복잡한 수식을 계산하는 것입니다. 다시말해, 컴퓨터는 '계산' 을 위해 태어난 기계인 것입니다.

 산술 연산자, 대입 연산자

 따라서, 이번 강의는 컴퓨터의 목적 달성 욕구를 채워주기 위한 강좌라 볼 수 있다. 컴퓨터의 역할에 걸맞게 C 프로그램을 통해 실컷 계산을 할 수 있게 해주자.


 일단, '계산' 이라 하면 머리속에 가장 먼저 떠오르는 것은 사칙연산, 즉 ,-,× ,÷ 을 가리킨다. 컴퓨터 상에서는 × 와 ÷ 기호를 쓰기 힘드므로, * 와 / 로 대체한다. 즉, 8 × 5 는 8 * 5 와 같은 것이고, 10 ÷ 7 은 10 / 7 과 같은 것이다. 또한, 색다른 연산자로 % 가 있는데 이는 나눈 나머지를 뜻한다. 예를들어 10 % 3 은 1 이 된다. 왜냐하면 10 을 3 으로 나눈 나머지가 1 이기 때문이다. 이러한 ,-,× ,÷ 들을 산술 연산자(Arithmetic Operator) 라고 합니다.

/* 산술 연산 */
#include <stdio.h>
int main()
{
    int a,b;
    a = 10;
    b = 3;
    printf("a + b 는 : %d \n", a + b);
    printf("a - b 는 : %d \n", a - b);
    printf("a * b 는 : %d \n", a * b);
    printf("a / b 는 : %d \n", a / b);
    printf("a %% b 는 : %d \n \n", a % b);

    printf("a / b 는 : %f \n", a / b); // 해서는 안될 짓
    return 0;
}

만약 위 코드를 잘 컴파일 했다면 아래와 같이 나온다. (컴파일 하는 방법을 까먹은 사람들은 1강을 참조하세요)


참고로, 맨 마지막 부분에 나오는 이상한 숫자는 저와 다를 수 있으니 걱정하지 마세요

a = 10;
b = 3;

  이러한 문장은 딱 봐도 한눈에 알 수 있다. a 의 값을 10, b 의 값을 3 이라 하는 것이다. 그런데 간혹가다가 위 문장을 아래와 같이 적는 사람도 있습니다.

10 = a;
3 = b;

  언뜻 보기에 맞는 문장인 것 같다. 왜냐하면, 실제 수학을 공부한 사람이라면 a = 10 이나 10 = a 나 별반 다를 것이 없기 때문이죠. 하지만, C 언어 컴파일러는 '=' 라는 기호를 뒤에서 부터 해석합니다. 즉, a = 10 은 '10 을 a 에 대입하라' 라는 문장이 되지만, 10 = a 는' a 의 값을 10 에 대입하라' 라는 이상한 문장이 되에 오류가 뜨는 것 입니다. 이렇게 '=' 를  유식한 말로 대입 연산자(Assignment Operator) 라고 합니다.  왜냐하면 우측의 값을 좌측에 '대입' 하는 것이기 때문이죠.

따라서,

a = 5;
b = 5;
c = 5;
d = 5;

라는 문장이나,

a = b = c = d = 5;

  라는 문장은 완전히 같은 것이 됩니다. 왜냐하면, 앞에서 말했듯이 = 는 뒤에서 부터 해석한다고 했으므로, 제일 먼저 d = 5 를 해석한 후, 그 다음에 c = d, b = c, a = b 로 차례대로 해석해 나가기 때문에 a = 5; b = 5; c = 5; d = 5; 라는 문장과 같은 것이지요.

    printf("a + b 는 : %d \n", a + b);
    printf("a - b 는 : %d \n", a - b);
    printf("a * b 는 : %d \n", a * b);
    printf("a / b 는 : %d \n", a / b);
    printf("a %% b 는 : %d \n \n", a % b);

  자, 이제 산술 연산자들에 대해 살펴보도록 합시다. 일단, 한 눈에 보게 a + b, a - b, a * b, a / b 는 각각 덧셈, 뺄셈, 곱셈, 나눗셈을 하여서 그 값이 %d 에 들어가 출력된 것 같습니다. 그런데, a + b, a - b, a * b 는 각각 계산 결과가 13, 7, 30 이 나온 사실을 쉽게 받아들일 수 있지만, a / b 가 왜 3 이 나왔는지는 이해하기 힘듭니다. 왜, a ÷ b 가 3 이 되었을 까요?

  사실, 3 강에서 말했지만 a 와 b 는 모두 int 형으로 선언된 변수 입니다. 즉, a 와 b 는 오직 '정수' 데이터만 담당합니다. 즉, a 와 b 는 모두 정수 데이터만 처리하기 때문에 a 를 b 로 나누면, 즉 10 을 3 으로 나누면 3.3333... 이 되겠지만 정수 부분인 3 만을 처리하게 되는 것 입니다. 따라서, 값은 3 이 출력됩니다.

  마지막으로 생소한 % 라는 연산자에 대해 살펴봅시다. +,-,*,/ 연산자는 모두 정수,실수형 데이터에 대해서 모두 연산이 가능한데, % 는 오직 정수형 데이터에서만 연산이 가능합니다. 왜냐하면 % 는 나눈 나머지를 표시하는 연산자 이기 때문이죠. a % b 는 a 를 b 로 나눈 나머지를 표시합니다. 즉, 10 % 3 = 1 이  되는 것이지요.

  이 때,

    printf("a %% b 는 : %d \n \n", a % b);

  %% 는 % 를 '표시'하기 위한 방법입니다. 왜냐하면 % 하나로는 %d , %f 같이 사용될 수 있기 때문이 표시가 되지 않습니다.

    printf("a / b 는 : %f \n", a / b); // 해서는 안될 짓

  위 프로그램을 실행 시켜 보신 분들은 알겠지만, 마지막 문장에서 엄청나게 긴 숫자가 튀어나오게 되죠. 물론, 그 숫자는 아무런 의미가 없는 숫자 입니다. 이러한 이상한 숫자가 출력된 이유는 바로 우리가 '해서는 안될 짓' 을 하였기 때문이죠. 3강 에서 우리는  %f 가 오직 실수형 데이터 만을 출력하기 위해 있는 것이라 하였습니다. 그런데, a / b 가 3.3333 이라고 해서 실수형 데이터가 되는 것이 아니라, (정수형 변수) (연산) (정수형 변수) 는 언제나 (정수) 이기 때문에 실수형 데이터를 출력하는 %f 를 이용하면 이와 같이 엄청난 오류가 뜨는 것 입니다.

  그렇다면 아래의 경우 어떻까요?

/* 산술 변환  */
#include <stdio.h>
int main()
{
    int a;
    double b;

    a = 10;
    b = 3;
    printf("a / b 는 : %f \n", a / b);
    printf("b / a 는 : %f \n", b / a);
    return 0;
}

만약 제대로 컴파일 했다면 아래와 같이 나오게 됩니다.


  a 는 정수형 변수, b 는 실수형 변수 입니다. 그런데, 이들에 대해 연산을 한 후에 결과를 실수형으로 출력하였는데 '정상적' 으로 나왔습니다. 그 것은 왜 일까요? 이는 컴파일러가 '산술 변환' 이라는 과정을 거치기 때문입니다. 즉, 어떠한 자료형이 다른 두 변수를 연산 할 때, 숫자의 범위가 큰 자료형으로 자료형들이 바뀝니다.
  즉, 위 그림에서도 보듯이 a 가 int 형 변수이고 b 가 double 형 변수인데, double 이 int 에 비해 포함하는 숫자가 더 크므로 큰 쪽으로 산술 변환됩니다. 일단, 정수형 변수와 실수형 변수가 만나면 무조건 실수형 변수쪽으로 상승되는데, 이는 실수형 변수의 수 범위가 최소 10
38 이상 되기 때문입니다. 위와 같은 산술 변환을 통해 애러가 없이 무사히 실행 될 수 있었습니다. 또한 double 형태로 산술 변환 되므로 결과도 double 형태로 나오기 때문에

printf(" a / b 는 : %d \n" , a / b);

와 같이 하면 오류가 생기게 됩니다. 왜냐하면 전에도 누누히 말했듯이 %d 는 정수형 값을 출력하는 방식이기 때문이죠.

/* 대입 연산자   */
#include <stdio.h>
int main()
{
    int a = 3;
    a = a + 3;
    printf("a 의 값은 : %d \n", a);
    return 0;
}

위 결과를 컴파일 하면 아래와 같이 나옵니다.


일단, 변수 선언 부분 부터 살펴 봅시다.


    int a = 3;

사실, 위 문장을 딱 보고 감이 바로 옵니다. "음... a 라는 변수를 선언하고 a 변수에 3 의 값을 집어 넣는구나" . 맞습니다. 사실 위 문장이나 아래 문장이나 다를 바가 없습니다.

int a;
a = 3;

  그냥, 타이핑 하기 귀찮아서 짧게 써 놓은 것 뿐입니다.

    a = a + 3;


그 다음 부분은 대입 연산자와 산술 연산자가 함께 나와 있군요. 만일, 우리가 방정식에 대해서 공부해 본 사람이라면 다음과 같이 이의를 제기할 수 도 있습니다.

a = a + 3
따라서 양변에서 a 를 빼면
0 = 3 ???

  물론, 위는 수학적으로 맞지만 C 언어 에서 의미하는 바는 다릅니다. 위에서 말했듯이, = 는 등호가 아닙니다. '대입' 연산자 입니다. 무엇을 대입하냐구요? 오른쪽의 값을 왼쪽으로 대입합니다. 즉, a + 3 의 값(6) 을 a 에 대입합니다. 따라서, a  =  6 이 되는 것이지요.

  이 때, 이와 같이 계산 될 수 있는 이유는 + 를 = 보다 먼저 연산하기 때문이죠. 즉, a + 3 을 먼저 한 후(+), 그 값을 대입(=) 하는 순서를 거치기 때문에 a 에 6 이라는 값이 들어갈 수 있게 됩니다. 이러한 것을 '연산자 우선순위' 라고 하는데, 밑에서 조금 있다가 다루어 보도록 하겠습니다.

/* 더하기 1 을 하는 방법  */
#include <stdio.h>
int main()
{
    int a = 1, b = 1, c = 1, d = 1;

    a = a + 1;
    printf("a : %d \n", a);
    b += 1;
    printf("b : %d \n", b);
    ++ c;
    printf("c : %d \n", c);
    d ++;
    printf("d : %d \n", d);

    return 0;
}

위 코드를 컴파일 하면 아래와 같이 나옵니다.


  음, 모두 2 가 되었군요. 사실 위에 나온 4 개의 코드는 더하기 1 을 한다는 점에서 모두 같습니다. 일단, 하나하나 차례대로 살펴봅시다.

    a = a + 1;

  가장, 기초적으로 1 을 더하는 방법입니다. 위 문장은 "a 에 a 에 1 을 더한 값을 대입한다. " 라는 뜻을 가지고 있죠?

    b += 1;

  이게 뭔가요! 처음 본 연산인 += 입니다. 이러한 연산을 복합 대입연산 이라 하며, b = b + 1 과 같습니다. 이렇게 쓰는 이유는 단지, b = b + 1 을 쓰기 귀찮아서 간략하게 쓰는 것입니다. 물론, b = b+1 과 b += 1 은 엄밀히 말하자면 같은 것은 아니지만 이에 대해서는 나중에 다루어 보도록 하겠습니다(우선 순위에서 약간 차이가 있습니다). 복합 대입 연산은 아래와 같이 여러 가지 형태로 이용될 수 있습니다.

b += x; // b = b + x; 와 같다
b -= x; // b = b - x;
와 같다
b *= x; // b = b * x; 와 같다
b /= x; // b = b / x; 와 같다

  마지막으로, 비슷하게 생긴 두 부분을 함께 살펴 보도록 하겠습니다.

    ++ c;
    d ++;


  위와 같은 연산자(++)를 증감 연산자라고 합니다. 둘 다, c 와 d 를 1 씩 증가시켜 줍니다. 그런데, ++ 의 위치가 다릅니다. 전자의 경우 ++ 이 피연산자(c) 앞에 있지만 후자의 경우 ++ 이 피연산자(d) 뒤에 있습니다. ++ 이 앞에 있는 것을 전위형 (prefix), ++ 이 뒤에 있는 것을 후위형(postfix) 라 하는데 이는 본질적으로 다릅니다. 전위형의 경우, 먼저 1 을 더해준 후 결과를 돌려주는데 반해, 후위형의 경우 결과를 돌려준 이후 1 을 더해줍니다. 사실, 이해가 잘 안될테니 아래를 보세요.

/* prefix, postfix  */
#include <stdio.h>
int main()
{
    int a = 1;

    printf("++a : %d \n", ++a);

    a=1;
    printf("a++ : %d \n", a++);
    printf("a : %d \n", a);

    return 0;
}

위 소스를 성공적으로 컴파일 했다면 아래와 같이 결과가 나온다.



 분명히, 위에서 ++c 나 d++ 이나 결과를 출력했을 때 에는 결과가 1 이 잘 더해져서 2 가 나왔는데 여기서는 왜 일까? 앞서 말했듯이 ++ a 는 먼저 1 을 더한 후 결과를 반환한다고 했고 a++ 은 먼저 결과를 반환 한 후, 1 을 더한 다고 했습니다.

    printf("++a : %d \n", ++a);

  즉, 위의 경우 a 에 먼저 1 을 더한 값인 2 를 printf 함수에 반환하여 %d 에 2 가 들어가게 됩니다. 그런데,

    printf("a++ : %d \n", a++);

  이 경우, 먼저 a 의 값을 printf 에 반환하며 %d 에 1 이란 값이 '먼저' 들어 간 후, 1 이 출력된 이후 a 에 1 이 더해집니다. 따라서, 다시 printf 문으로 a 의 값을 출력하였을 때 에는 2 라는 값이 나오게 되는 것입니다. 참고로, 위 4 개의 연산 중에서 가장 빨리 연산되는 것은 a++ 과 같은 증감 연산입니다(왜냐하면, a = a + 1 의 경우 ADD a  1 로 하지만, a++ 은 INC a 로 좀 더 빨리 계산된다. 자세한 내용은 나중에... ) 하지만, 요즈음의 컴파일러는 최적화가 잘 되어 있어, a = a + 1 같은 것은 a ++ 로 바꾸어 컴파일 해버립니다.

  비트 연산자

  마지막으로 생소한 연산자에 대해 알아보록 하겠습니다. 비트 연산자라고 부르는 이 것들은 정말 비트(bit) 를 가지고 연산을 합니다. 비트는 컴퓨터 기억 장치의 최소 단위로 1 비트는 0 과 1 만을 구분합니다. 이진법의 1 자리라 볼 수 있죠. 보통, 8개의 비트(8 bit) 를 묶어서 1 바이트(byte) 라고 하고, 이진법으로 8 자리 수라 볼 수 있죠. 따라서, 1 바이트로 나타낼 수 있는 수의 범위가 0 부터 11111111b 로 십진수로 바꾸면 0 부터 255 까지 나타낼 수 있습니다.
 
  비트연산자에는 & (And 연산), | (\ 위에 있는 것이다. 영문자 i 가 아닌다. Or 연산), ^ (XOR 연산), <<, >> (쉬프트 연산) , ~ (반전) 등이 있습니다. 일단, 각 연산자가 어떠한 역할을 하는지 살펴보도록 합시다.

AND 연산 (&)

AND 연산은 아래와 같은 규칙으로 연산된다. 

1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0


비트 연산은 각 자리를 연산하는데, 예를들어, 1010 & 0011 의 경우
위와 같이 한자리 한자리 각각 AND 연산하여, 위에 써 놓은 규칙대로 연산이 된다. 만약 두 숫자의 자리수가 맞지 않을 경우 ,예를들어 1111100 과 11 을 AND 연산 할 때 에는 11 앞에 0 을 추가하여 자리수를 맞추어 준다. 즉, 1111100 과 0000011 의 연산과 같습니다.

OR 연산 (|)

1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0

  OR 연산은 AND 연산과 대조적이다. 어느 하나만 1 이여도 모두 1 이 되는데, 예를들어 1101 | 1000 은 결과가 1101 이 됩니다.

XOR 연산 (^)

1 ^ 1 = 0
0 ^ 1 = 1
1 ^ 0 = 1
0 ^ 0 = 0


  XOR 연산은 특이하게도 두 수가 달라야지만 1 이 된다. 예를들어, 1100 ^ 1010 의 경우 결과가 0110 이 됩니다.

반전 연산(~)

  반전연산은 간단히 말에 0 을 1 로 1 을 0 으로 바꿔주는 것입니다. 예를들어서 ~ 1100 을 하면 그 결과는 0011 이 된다.

<< 연산 (쉬프트 연산)

위 연산 기호에서 볼 수 있듯이, 비트를 왼쪽으로 쉬프트(Shift) 시킨다. 예를 들어, 101011 를 1 만큼 쉬프트 시키면 ( 이를 a << 1 이라 나타냅니다)
  위 처럼 결과가 010110 이 됩니다. 이 때, << 쉬프트 시, 만일 앞에 쉬프트된 숫자가 갈 자리가 없다면, 그 부분은 버려집니다. 또한 뒤에서 새로 채워지는 부분은 앞에서 버려진 숫자가 가는 것이 아니라 무조건 0 으로 채워집니다.

>> 연산

 이는 위와 같은 종류로 이는 << 와 달리 오른쪽으로 쉬프트 해줍니다. 이 때, 오른쪽으로 쉬프트 하되, 그 숫자가 갈 자리가 없다면 그 숫자는 버려집니다. 이 때, 앞 부분에는 무조건 0 이 채워지게 되죠. 예를들어서 11100010 >> 3 = 0001110 이 됩니다.

비트 연산자를 자세히 다룬 이유는 이 부분이 약간 생소하기 때문입니다. 또한, 처음에 비트 연산자를 접할 때, '저런거 뭐에다 쓰지?' 라는 생각이 들기도 합니다. 그런데, 사실 비트 연산은 암호 분야에서 많이 쓰이며 쉬프트 연산도 가끔씩 유용하게 쓰입니다.

/* 비트 연산 */
#include <stdio.h>
int main()
{
    int a=0xAF; // 10101111
    int b=0xB5; // 10110101

    printf("%x \n", a & b); // a & b = 10100101
    printf("%x \n", a | b); //
a | b = 10111111
    printf("%x \n", a ^ b); // a ^ b = 00011010
    printf("%x \n", ~ a); // ~a = 1....1 01010000
    printf("%x \n", a << 2);// a << 2 = 1010111100
    printf("%x \n", b >> 3); // b >> 3 = 00010101

    return 0;
}

위를 성공적으로 컴파일 했다면


 위와 같이 나오게 됩니다. 일단, 첫 세줄은 그럭 저럭 이해가 잘 갑니다. 그런데, 네 번째 줄인 ~ a 연산에 대해 의문을 품는 사람들이 많습니다.

    printf("%x \n", ~ a); // ~a = 1....1 01010000

  우리의 기억을 되돌려 3 강으로 가 봅시다. 강의 중간쯤에 보면 여러가지 자료형 들에 대한 설명과 함께 작은 표가 있을 텐데 말이죠. 이를 다시 아래에 불러와 봅시다.


  int 형 변수에 대한 설명을 보니 옆에 'Size*' 이라 표시된 것이 있습니다. 이는 int 형 변수의 크기를 나타내는데 4 바이트라고 되어 있군요. 맞습니다. int 형 변수는 하나의 데이터를 저장하기 위하여 메모리 상의 4 바이트 - 즉 32 비트를 사용합니다. ( 1 byte = 8 bits) 아까, 하나의 비트가 0 과 1 을 나타낸다고 했으므로 (즉 1 개의 비트가 2 진수의 한 자리를 나타내게 되죠), 하나의 int 형 변수는 32 자리의 이진수라고 볼 수 있습니다. 예를들어 우리가 a = 1 이라 한 것은 실제로 컴퓨터에는 a =
00000000000000000000000000000001 이라 저장되는 것과 같게 되는 거죠.


  즉, 우리가 int a = 0xAF; 라고 한 것은 a = 10101111; 이 맞지만 사실 컴퓨터 메모리 상에서는 a 가 int 형이기 때문에 a = 00000000
000000000000000010101111 ( 10101111 앞에 0 이 24 개 있다 ) 이라 기억하는 것이 됩니다. 따라서, 이 숫자를 반전 시키게 되면
a = 11111111
111111111111111101010000, 즉 0xFFFFFF50 이 되는 것이지요. 마찬가지로 생각해 보면,

    printf("%x \n", a << 2);// a << 2 = 1010111100
    printf("%x \n", b >> 3); // b >> 3 = 00010101

  이 두 문장도 사실은 각각
00000000000000000000000010101111 과 00000000000000000000000010111111 을 쉬프트 연산한 것과 같습니다. 이 때, a 의 경우 00000000000000000000000010101111 을 왼쪽으로 2 칸 쉬프트 하면 00000000000000000000001010111100 이 되어 0x2BC 가 되고, b 의 경우 00000000000000000000000000010101 이 되어 0x16 이 됩니다.

복잡한 연산

마지막으로 여러 연산이 중첩된 혼합 연산에 대해 살펴 보도록 합시다. 우리가 연산을 하는데 에도 순서가 있듯이 컴퓨터에도 연산을 하는데 무엇을 먼저 연산을 할 지 우선 순위가 정해져 있을 뿐더러 연산 방향 까지도 정해져 있습니다. 이를 간단히 살펴 보자면 아래와 같습니다.

이와 같이 순위가 매겨져 있습니다. 수학에서 우리가 혼합 계산을 할 때, 소괄호 → 중괄호 → 대괄호 순으로 괄호를 써 주어야 되었었는데, C 언어에서는 소괄호 하나만으로 충분할 뿐더러 중괄호는 사용하면 안됩니다.

예를들어 수학에서 {a + b * (c + d) } / (23 * 12) 라고 했던 문장을 C 언어 에서는 그냥 (a + b * (c +d) ) / (23 * 12) 와 같이 소괄호 만으로 사용해도 괜찮습니다. 이 때, 눈여겨 보아야 할 점은 괄호들이 제 1 우선 순위에 위치하였다는 점 입니다. 따라서, 어떠한 연산이라도 괄호롤 싸 주게 되면 먼저 실행 됩니다.

위 결과를 토대로 아래의 연산식들이 어떠한 결과를 낳게 되는지 예측해 보세요

a = 1; b = 1;    a = a + a++;  b = b + ++b; // a,b 의 값은?

그 결과는 아래와 같습니다.

a = 3; b = 4;

왜 일까요? 일단, 연산자 우선순위를 먼저 살펴볼 필요성이 있습니다. 일단, ++ 이 2 위로 계산되고, 그 아래에 + 가 4위, 그 밑에 = 가 14 위로 계산됩니다. 즉, 컴퓨터는 ++ 을 가장 먼저 실행한다는 것이지요. 그렇게 따지면, 왜 a = a + a++ 과 b = b + ++b 가 차이가 생길까요?

우리가 컴퓨터라고 생각하고 각 계산 과정을 수행해 봅시다. 일단, a ++ 부터 살펴 봅시다.

a = a + a++;

이 때, a ++ 은 ++ 이 뒤에 있으므로 먼저 a 를 반환 한 후 ++ 을 해줍니다.
즉, 위 식은 아래와 같이 바뀌죠.

a= a + 1 (원래의  a 값 인 1 을 반환) , a = 2 (그다음에 ++ 을 해주어 2 가 됨) ;

따라서, a 가 2 가 된 상태에서

a = a + 1

을 실행해 주면 a = 2 + 1 = 3 이 되는 것이지요. 이제, 마찬가지 방법으로 아래의 식을 살펴 보도록 합시다.

b = b +  ++b;

  일단, ++ b 가 먼저 연산되는데 이 때, ++ b 는 ++ 을 먼저 실행 한 후 그 결과를 반환해 주므로, b = b + 2, b = 2 가 되는 것 입니다. 따라서,

b = 2 + 2 = 4

가 되는 것이지요. 마지막으로, 결합 순위에 대해 잠시 다루어 보도록 하겠습니다. 표의 오른쪽을 보면 결합 순위가 나와 있는데, 대부분이 '왼쪽 우선' 이지만 몇 개는 '오른쪽 우선' 입니다. 이 말이 뜻하는 바가 무엇이냐면, 아래와 같은 문장을 수행할 때 계산하는 순위를 이야기 합니다.

a = b + c + d +e;

  위 표에서 보듯이, 덧셈의 결합 순서가 왼쪽 우선이므로 위 계산과정은 아래의 순서대로 진행됩니다.

b + c 를 계산하고 그 결과를 반환( 그 결과를 C 라 하면)
C + d 를 계산하고 그 결과를 반환( 그 결과를 D 라 하면)
D + e 를 계산하고 그 결과를 반환(그 결과를 E 라 하면)

 따라서, 위 식은

a = E

  가 되죠. 따라서, a 에 E 의 값, 즉 b + c + d + e 의 값이 들어가게 됩니다.

또한, 위 표에서 몇 안되는 '오른쪽이 우선' 인 대입 연산자(=) 를 살펴봅시다. 만약 대입 연산자가 왼쪽 우선이였다면 아래의 식이 어떻게 계산될 지 생각해 봅시다.

a = b =  c = d = 3;

만약 왼쪽 우선이였다면 a = b; b = c; c = d; d = 3 의 형식으로 계산되어 a , b, c 에는 알 수 없는 값이 들어가겠죠. 하지만 오른쪽이 우선이므로 위 식은 d = 3, c = d, b = c, a = b 의 형식으로 계산되어 a,b,c,d 의 값이 모두 3 이 될 수 있었습니다. 

자, 이제 연산자에 대한 강의가 끝났습니다. 연산자는 C 언어에서 가장 기초적인 부분이라 할 수 있습니다. 마치 수학에서 덧셈, 뺄셈을 가장 처음에 배우는 것 처럼 말이죠. 이번 강좌에서는 특별히 예제를 많이 만들어 보지는 않았지만 여러분 께서 C 언어를 통해 복잡한 수식의 계산을 하거나, 복잡한 수식을 보고 이러한 연산은 이 순서로 연산될 것이다 라고 예측해 보는 것도 우선순위를 이해하는데 도움이 될 것입니다. 보통, 우선순위를 잘못 고려하여 나는 오류들은 찾기가 매우 힘들기 때문에 우선순위를 빠삭하게 잘 이해하는 것이 좋습니다.