Chapter 3 계산하기

https://raw.githubusercontent.com/KuraiLuna/KuraiLuna.github.io/master/practical/C/%EC%94%B9%EC%96%B4%EB%A8%B9%EB%8A%94%20C%20%EC%96%B8%EC%96%B4/Chapter-03/Chapter-03-%EA%B3%84%EC%82%B0-%ED%95%98%EA%B8%B0.gif

산술 연산자, 대입 연산자

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", a % b);

    return 0;
}

만약 위 코드를 잘 컴파일 했다면 아래와 같이 나온다.

(컴파일 하는 법을 가먹었다면)

(1장으로 되돌아 가보도록 하자)

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test

a + b 는 : 13 
a - b 는 : 7 
a * b 는 : 30 
a / b 는 : 3 
a % b 는 : 1 
[Done] exited with code=0 in 0.323 seconds

그렇다면 코드를 살펴보도록 하자.

a = 10;
b = 3;

언뜻 보기에 그럴싸한 문장으로 보인다.

왜냐하면, 실제 수학을

공부를 한 사람이라면 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; 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", a % b);

자, 이제 산술 연산자들에

대해 살펴보도록 하자.

일단, 한 눈에 보게

a + b, a – b, a * b, a / b가 왜 3이

나왔는지는 이해하기 힘들다.

왜, a / b가 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같이 사용될 수 있기

때문이 표시가 되지 않는다.

나눗셈 시에 주의할 점

#include <stdio.h>

int main() {

    int a, b;
    a = 10;
    b= 3;
    printf("a / b 는 : %f \n", a / b); // 시도해서는 안됨
    return 0;

}

컴파일 후(아마 경고 메세지가 뜰 것이다.),

실행한다면 아래와 같이 이상한 결과가 나온다.

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
a / b 는 : 0.000000 

[Done] exited with code=0 in 0.318 seconds

이전 에서 우리는 %f가 오직 실수형

데이터 만을 출력하기 위해 있는

것이라하였다. 그런데, a / b가

나누기 3 이므로 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;
}

만약 제대로 컴파일 했다면

아래와 같이 나오게 된다.

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
a / b 는 : 3.333333 
b / a 는 : 0.300000 
[Done] exited with code=0 in 0.31 seconds

a는 정수형 변수, b는 실수형 변수이다.

그런데, 이들에 대해 연산을

한 후에 결과를 실수형으로 출력하였는데

정상적으로 나왔다. 그 것은

왜 일까? 이는 컴파일러가 산술 변환

이라는 과정을 거치기 때문이다.

즉, 어떠한 자료형이 다른 두 변수를 연산 할 떄,

숫자의 범위가 큰 자료형 으로 자료형들이 바뀐다.

https://raw.githubusercontent.com/KuraiLuna/KuraiLuna.github.io/master/practical/C/%EC%94%B9%EC%96%B4%EB%A8%B9%EB%8A%94%20C%20%EC%96%B8%EC%96%B4/Chapter-03/%EA%B3%84%EC%82%B0_%ED%95%98%EA%B8%B0.png

즉, 위 그럼에서도 보듯이 a 가 int형

변수이고 b가 double형 변수인데,

double이 int에 비해 포함하는

숫자가 더 크므로 큰 쪽으로 산술 변환된다.

일단, 정수형 변수와 실수형 변수가

만나면 무조건 실수형 변수쪽으로

상승되는데, 이는 실수형 변수의 수 범위가

int 보다 훨씬 넓기 때문이다.

위와 같은 산술 변환을 통해 애러가

없이 무사히 실행 될 수 있었다.

또한 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;

}

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

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
a 의 값은 : 6 
[Done] exited with code=0 in 0.349 seconds

일단, 변수 선언 부분 부터 살펴 보도록 하자.

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;
}

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

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
a : 2 
b : 2 
c : 2 
d : 2 
[Done] exited with code=0 in 0.327 seconds

모두 2가 되는 결과가 된다.

사실 위에 나온

4 개의 코드는 더하기 1을 한다는

점에서 모두 동일하다. 일단,

하나하나 차례대로 살펴보도록 하자.

a = a + 1;

이것 은 무엇일까?

처음 본 연산인 += 이다.

이러한 연산을 복합 대입연산

이라 하며, b = b + 1 과 같다.

이렇게 쓰는 이유는 단지, b = b + 1 을 쓰기

귀찮아서 간략하게 쓰는 것이다.

물론, b = 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을 더해준 후 결과를

돌려주는데 반해,

후위형의 경우 결과를

돌려준 이후 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;
}

위 소스를 성공적으로

컴파일 했다면 아래와 같이 결과가 나온다.

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
++a : 2 
a++ : 1 
a : 2 
[Done] exited with code=0 in 0.311 seconds

분명히, 위에서 ++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같은 것은 a++로

바꾸어 컴파일 해버린다.

비트 연산자

마지막으로 비트 연산자라고 불리는 생소한

연산자들에 대해 기록 해보자한다.

이 연산자들은 정말 비트(bit)하나에 대해

연산을 한다. 비트는 컴퓨터에서 숫자의

최소 단위로 1 비트는 0 혹은 1을 나타낸다고 한다.

쉽게 말해 이진법의 한 자리라고 생각하면 된다.

보통 8개의 비트(8 bit)를 묶어서 1바이트(byte) 라고 하고,

이진법으로8 자리 수라 고 볼수 있다. 따라서, 1 바이트로

나타낼 수 있는 수의 범위가 0 부터 11111111로 십진수로 바꾸면

0 부터 255까지 나타낼 수 있다.

비트 연산자에는 & (And 연산자),

| (\위에 있는 것, 영문자 i의 대문자가 아니다! Or 연산),

(XOR 연산), <<, >>(쉬프트 연산), ~(반전) 등이 존재한다. 일단,

각 연산자가 어떠한 역할을 하는지 살펴보도혹 하자.

AND 연산 (&)

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

    결과
1 1 1
1 0 0
0 1 0
0 0 0
 
비트 연산은 각 자리를
연산하는데, 예를들어,
1010 & 0011 의 경우
 
 
https://raw.githubusercontent.com/KuraiLuna/KuraiLuna.github.io/master/practical/C/%EC%94%B9%EC%96%B4%EB%A8%B9%EB%8A%94%20C%20%EC%96%B8%EC%96%B4/Chapter-03/%EA%B3%84%EC%82%B0_%ED%95%98%EA%B8%B02.png
 

위와 같이 한자리 각각 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
1 0 1
0 1 1
0 0 0
 
 

XOR 연산은 특이하게도 두 수가

달라야지만 1이 된다.

예를들어, 1100 ˆ 1010의

경우 결과가 0110이 된다.

마치 두 비트를 더한

다는 식으로 생각하면 된다.

반전 연산(~)

번전연산은 간단히 말해 0을 1로 1을 0으로 바꿔주는 것이다.

예를들어서 ~1100을 하면 그 결과는 0011이 된다.

<< 연산 (쉬프트 연산)

위 연산 기호에서 느껴지듯이

비트를 왼쪽으로 쉬프트(Shift)시킨다.

예를 들어, 101011를

1만큼 쉬프트 시키면

(이를 a << 1 이라 나타낸다)

https://raw.githubusercontent.com/KuraiLuna/KuraiLuna.github.io/master/practical/C/%EC%94%B9%EC%96%B4%EB%A8%B9%EB%8A%94%20C%20%EC%96%B8%EC%96%B4/Chapter-03/%EA%B3%84%EC%82%B0_%ED%95%98%EA%B8%B03.png

위 처럼 결과가 010110이 된다. 이 때,

<< 쉬프트 시, 만일 앞에 쉬프트된

숫자가 갈 자리가 없다면,

그 부분은 버려진다.

또한 뒤에서 새로 채워지는

부분은 앞에서 버려진 숫자가

가는것이 아니라 무조건 0으로 채워진다.

 

>> 연산

이는 위와 같은 종류로 << 와 달리

오른쪽으로 쉬프트 해준다. 이 때,

오른쪽으로 쉬프트 하되, 그 숫자가

갈 자리가 없다면 그 숫자는 버려진다.

이 때, 무조건 0이 채워지는 <<

연산과는 달리 앞부분에 맨 왼쪽에 있었던

 

수 가 채워지게 된다.

예를들어서

11100010 >> 3 = 11111100이 되고,

00011001 >> 3 = 00000011 이 된다.

비트 연산자를 자세히 다룬

이유는 이 연산자가 여러분들이 아마

여태까지 살면서 다루어왔던

연산자들 (덧셈, 뺄셈 같은 것들) 과는

조금 뭔가다르기 때문이다. 또한,

처음에 비트 연산자를 접할 때,

저런거 뭐에다 사용할까?

라는 의문이 들기도 한다. 아직은 그

쓰임새를 짚고 넘어가기

어렵지만 나중에 종종 출몰할 때가 있을

터이니 잘 기억해두도록 하자.

/* 비트 연산 */
#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 = 00010110

    return 0;

}

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

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
a5 
bf 
1a 
ffffff50 
2bc 
16 
[Done] exited with code=0 in 0.582 seconds

위와 같이 나오게 된다. 일단,

첫 세줄은 그럭 저럭

이해는 된다. 그런데,

네 번째 줄인 ~a연산에 대해

의문이 생기는 사람이 많을것이다.

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

우리의 기억을 되돌아 가보면 이전

중간쯤에 보면 여러가지 자료형 들에

대한 언급과 함께 적은 표가 있을

텐데 말이지 이를 다시 아래에

불러와보자

Name Size* Range*
 char  1byte  signed:-128 to 127
 unsigned:0 to 255
 short int (short)  2byte  signed:-32768 to 32767
 unsigned:0 to 65535
 int  4byte  signed:-2147483648 to 2147483647
 unsigned:0 to 4294967295
 long int (long)  4byte  signed:-2147483648 to 2147483647
 unsigned:0 to 4294967295
 bool  1byte  true or false
 float  4byte  +/- 3.4e +/- 38 (~7 digits)
 doble  8byte  +/- 1.7e +/- 308(~15digits)
 long doble  8byte  +/- 1.7e +/- 308(~15digits)
 

 

int 형 변수에 대한

설명을 보니 옆에 Size이라

표시된 것이 있다. 이는 int형 변수의

크기를 나타내는데

4바이트라고 되어 있다.

 

맞다. int 형 변수

는 하나의 데이터를

저장하기 위하여

메모리 상의 4 바이트

 

– 즉 32비트를 사용한다. ( 1 byte = 8 bits)

아까, 하나의 비트가 0과

1을 나타낸다고 했으므로

 

(즉 1 개의 비트가 2진수의)

(한자리를 나타내게 된다),

하나의 int 형 변수는 32자리의

이진수라고 볼 수 있다. 예를들어

우리가 a = 1 이라

한 것은 실제로 컴퓨터에는 a =

00000000 00000000 00000000 00000001 이라

저장되는 것과 같게 되는 것.

즉, 우리가 int a = 0xAF; 라고 한 것은

a = 10101111; 이 맞지만 사실 컴퓨터 메모리

상에서는 a가 int 형이기 때문에

a = 00000000 00000000 00000000 10101111

(10101111 앞 에 0 이 24 개 있다) 이라

기억하는 것이 된다. 따라서, 이 숫자를

반전 시키게 되면a = 11111111 11111111 11111111 01010000,

즉 0xFFFFFF50 이 되는 것.

마찬가지로 생각해 보면,

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

이 두 문장도 사실은 각각

00000000 00000000 00000000 10101111 과

00000000 00000000 00000000 10110101 을

쉬프트 연산한 것과 같다.

따라서 a의 경우

00000000 00000000 00000000 10101111

을 왼쪽으로2 칸 쉬프트 하면

00000000 00000000 00000010 10111100

이 되어 0x2BC가 된다.

b의 경우 마치 앞에 1이 있는 것 같지만

실제로는 int 형 데이터에 저장되어

있으므로 4 바이트로,

00000000 00000000 00000000 10110101 이므로

맨 왼쪽의 비트는 0이다.

따라서 쉬프를 하게

되면 왼쪽에 0이 채워지면서

00000000 00000000 00000000 00010110

이 되어 0x16 이 된다.

복잡한 연산

마지막으로 여러 연산이 중첩된

혼합 연산에 대해 살펴 보도록 하자.

우리가 연산을하는데 에도 순서가 있듯이

컴퓨터에도 연산을 하는데 무엇을

먼저 연산을 할 지 우선 순위가

정해져 있을 뿐더러 연산 방향 까지도 정해져 있다.

이를 간단히 살펴 보자면 아래와 같다.

 1  () [] ->. (expr)++(expr)–  왼쪽 우선
 2  !~+-(부호) *p & sizeof 캐스트 –(expr)++(expr)  오른쪽 우선
 3  *(곱셈) / %  왼쪽 우선
 4  + -(덧셈, 뺄셈)  왼쪽 우선
 5  << >>  왼쪽 우선
 6  < <= > >=  왼쪽 우선
 7  == !=  왼쪽 우선
 8  &  왼쪽 우선
 9  ^  왼쪽 우선
 10  |  왼쪽 우선
 11  &&  왼쪽 우선
 12  ||  왼쪽 우선
 13  ?:  오른쪽 우선
 14  = 복합대입  오른쪽 우선
 15  ,  왼쪽 우선
 
 
 

이와 같이 순위가 매겨져 있다. 이 때,

눈여겨 보아야 할 점은 괄호들이 제 1 우선

순위에 위치 하였다는 점 이다. 따라서,

연산이라도 괄호로 감싸 주게 되면 먼저 실행 된다.

마지막으로, 결합 순위에 대해 잠시

다루어 보도록 하자, 표의 오른쪽을 보면

결합 순위가 나와 있는데, 대부분이

‘왼쪽 우선’ 이지만 몇 개는 ‘오른쪽 우선’

이다. 이 말이 뜻하는 바가 무엇이냐면,

아래와 같은 문장을 수행할 때 계산하는

순위를 이야기 한다.

a = b + c + d + e;

위 표에서 보듯이, 덧셈의 결합 순서가

왼쪽 우선이므로 위 계산과정은

아래의 순서대로 진행된다.

1. b + c 를 계산하고 그 결과를 반환( 그 결과를 C 라 하면)

2. C + d 를 계산하고 그 결과를 반환( 그 결과를 D 라 하면)

3. D + e 를 계산하고 그 결과를 반환(그 결과를 E 라 하면)

따라서, 위 식은

a = E

가 된다. 따라서, a에 E의 값,

즉 b + c + d + e의 값이 들어가게 된다.

또한, 위 표에서 몇 안되는

‘오른쪽이 우선’인 대입 연산자(==)

를 살펴보자, 만약 대입 연산자가

왼쪽 우선이였다면 아래의

식이 어떻게 계산될 지 생각해 보자.

a = b = c = d = 3;

만약 왼쪽 우선이였다면

a = b; b = c; d = 3의 형식으로 계산되어

a, b, c에는 알 수 없는 값이

들어가게되겠지, 하지만 오른쪽

의 우선이므로 위 식은

d = 3, c = d, b = c, a = b의 형식으로

계산되어 a,b,c,d의

같이 모두 3이 될 수 있었다.

자, 이제 연산자에 대한

공부 기록이 끝났다. 연산자는 C 언어에서

가장 기초적인 부분이라 할 수 있다.

마치 수학에서 덧셈, 뺄셈을

가장 처음에 배우는 것 처럼,

정 리

  • +, -, /, *, =, % 가 무슨 역할을 하는 연산자 인지 배웠습니다.

  • &, |, <<, >> 가 무슨 역할을 하는 연산자 인지 배웠습니다.

  • 연산자 우선 순위에 대해 다루었습니다.

  • 우선 순위가 헷갈리거나, 복잡한 수식이면 괄호를 적극 활용합니다.

 
 
 

 

컴퓨터가 음수를 표현하는 방법 (2의 보수)

지난 기록에서 변수를 이용해서

여러가지 연산을 수행하는 방법에 대해 다루었다

그런데 C 언어에서 아무런 제약 없이

연산을 수행할 수 있는 것은 아니다.

왜냐하면 변수 마다 각각의 타입에 따라서

보관할 수 있는 데이터의 크기 가

정해져 있기 때문이다.

예를 들어서 int 의 경우

-2147483648 부터 2147483647 까지의

정수 데이터를 보관할 수 있다.

그렇다면 아마 여러분들은

아래와 같은 질문을 하실 수 있다.

만약에 변수의 데이터가 주어진

범위를 넘어간다면 어떻게 될까?

한번 직접 코드를 작성해보도록 하자.

#include <stdio.h>
int main(){
    int a = 2147483647;
    printf("a : %d \n", a);

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

    return 0;
}

성공적으로 컴파일 했다면

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
a : 2147483647 
a : -2147483648 
[Done] exited with code=0 in 0.329 seconds

와 같이 나온다.

int a = 2147483647;
printf("a : %d \n", a);

먼저 위와 같이 a라는 변수를 정의한

두ㅣ에 int 가 표현할 수 있는 최대값인

2147483647 를 대입하였다.

해당 문장은 문제가 없으며 printf로도

2147483647 가 잘 출력되었다.

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

반면에 a++을 해서

int가 표현할 수 있는 최대값을 넘겨버렸다.

그런데 놀랍게도 전혀

예상하지 못한 값이 출력되었다.

바로 -2147483648 이

나온것 어떻게 양수에서 1 을

더했는데 음수가 나왔을까?

이에 대한 대답을 하기

위해선 먼저 컴퓨터에서 어떻게 음수를

표현하는지 알아야 한다.

음수 표현 아이디어

만약 여러분이 CPU 개발자라면

컴퓨터 상에서 정수 음수를

어떤식으로 표현하도록 만들었을까?

가장 간단히 생각해보면

우리가 부호를 통해서 음수

인지 양수 인지 나타내니까, 비슷한

방법으로 부호를나타내기

위해서 1 비트를 사용하는것 이다.

(예를 들어서 0이면 양수, 1 이면 음수)

그리고 나머지 부분을

실제 정수 데이터로 사용하면 된다.

예를 들어서 가장 왼쪽 비트를

부호 비트라고 생각하자면

(참고로 아래 표현하는 수들은)

(모두 이진법으로 작성한 것)

0111
 
은 7 이 될 것이고
 
1111

 

은 맨 왼쪽 부호 비트가 1

이므로 -7을 나타내게 된다.

꽤나 직관적인 방식이기는

 

하지만 여러가지 문제점

이 있다. 첫 번째로 0 을

나타내는 방식이 두 개 라는 점

0000

 

도 0 이고

1000

 

역시 0 다. (-0은 0 이니까)

뭐 0이 두 개일 수도 있지라고

생각하 실 수 있겠지만

사실 이는 매우 큰 문제이다.

 

왜냐하면 컴퓨터 상에서

어떠한 데이터가 0인지 아닌지

비교하는 일을 굉장히 많이 한다.

0을 표현하는 방법이 두 가지라면,

어떠한 데이터가 0 인지

확인하기 위해서 +0 인지

 

-0인지 두 번이나 확인해야 된다.

따라서 이상한 데이터 표현법

덕분에 쓸데없이 컴퓨터 자원을

낭비하게 된다.

또 다른 문제로는,

양수의 음수의 덧셈을 수행할 때 부호를

고려해서 수행해야 한다는 점이다.

예를 들어서 0001 과 0101 을

더한다면 그냥 0110 이 되겠지만

0001 과 1001 을 더할 때에는

1001 이 사실은 -1 이므로

뺄셈을 수행해야 한다.

따라서 덧셈 알고리즘이

좀 더 복잡해진다.

물론 부호 비트를

도입해서 음수와 양수를

구분하는 아이디어

 

자체는 나쁜 생각은 아니다.

여기서는 int 와 같은

정수 데이터만 다루지만

double 이나 float 처럼

소수인 데이터를 다루는

방식에서는

(이를 부동 소수점 표현)

(이라고 하는데,)

(나중 기록에서 자세히)

(알아보도록하자.)

 

부호 비트를

도입하여서 음수인지

양수인지를 표현하고 있다.

하지만 적어도 정수를

표현하는 방식에서는

부호 비트를 사용하는

방식은 문제점이 있다.

 

2의 보수(2’s complement) 표현법

그렇다면 다른 방법을 생각해보자.

만약에 어떤 x 와 해당 수의

음수 표현인 −x 를 더하면 당연

히도 0 이 나와야 한다.

예를 들어서 7 을 이진수로 나타내면

0111
 

이 되는데 여기에 더해서

0000 이 되는 이진수가 있을까?

이 때 덧셈 시에 컴퓨터가 4 비트만

기억한다고 가정해보자.

그렇다면 -7 의 이진수 표현으로

가장 적당한 수는 바로 1001 이

될 것. 왜냐하면 0111 과

1001 을 더하면 10000 이 되는데,

CPU 가 4 비트만 기억하므로

맨 앞에 1 은 버려져서 그냥 0000

이 되기 때문이다.

 

이렇게 가장 덧셈을 고려하였을 때

가장 자연스러운 방법으로 음수를

표현하는 방식을 바로 2 의

보수 표현이라고 한다.

2 의 보수 표현 체계 하에서

어떤 수의 부호를 바꾸려면

먼저 비트를 반전

시킨 뒤에 1 을 더하면 된다

예를 들어서 -7 을

나타내기 위해서는,

7 의 이진수 표현인 0111 의

비트를 모두 반전시키면 1000

이 되는데 여기다

1 을 더해서 1001

로 표현하면 된다.

반대로 -7 에서 7 로

가고 싶다면 1001

의 부호를 모두 반전 시킨뒤

(0110) 다시 1 을

더하면 양수인 7 (0111) 이

나오게 된다

이 체계에서 중요한 점은

0000 의 2 의 보수는 그대로

0000 이 된다는 점이다.

왜냐하면 0000

을 반전하면 1111 이 되는데,

다시 1 을 더하면 0000 이 되기 때문!

또한 어떤 수가 음수 인지 양수인지

판단하는 방법도 매우 쉽다.

그냥 맨 앞 비트가 부호 비트라

고 생각하면 됩니다. 예를 들어서

1101 의 경우 맨 앞 비트가

1 이기 때문에 음수 다. 따라서

이 수가 어떤 값인지 알고싶다면

보수를 구한 뒤에

(1101 –> 0010 –> 0011)

– 만 붙여주면 되겠지.

0011 이 3 이므로, 1101 은 경우 -3 이 된다.

이와 같이 2 의 보수 표현법을 통해서

• 음수나 양수 사이 덧셈 시에

굳이 부호를 고려하지 않고 덧셈을 수행해도 되고

• 맨 앞 비트를 사용해서 부호를 빠르게 알아낼 수 있다

와 같은 장점 때문에 컴퓨터에서

정수는 2 의 보수 표현법을

사용해서 나타내게 된다.

한 가지 재미있는 점은

2 의 보수 표현법에서

음수를 한 개더 표현할 수 있다.

왜냐하면 1000

의 경우 음수 이지만 변환 시켜도

다시 1000 이 나오기 때문

(1000 –> 0111 –> 1000) 실제로

int 의 범위를 살펴보면

-2,147,483,648 부터

2,147,483,647 까지

음수가 1 개 더 많다.

자 그렇다면 이전 코드를

다시 살펴보도록 하자

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

처음에 a 에 int

최대값을 집어 넣었을 때

아마 a 에는 0x7FFFFFFF

(이진수로 0111 1111 …)

(1111) 라는 값이 들어가있을 것.

그런데 여기서 1 을

더하게 되면 어떻게 될까?

우리는 a 의 현재 값이

int 가 보관할 수 있는

최대값이므로 1을 더 증가

시킨다면 오류가 발생하거나

아니면 그냥 현재 값

그대로 유지하게 하고 싶었을 것.

하지만 CPU 는 그냥 0x7FFFFFFF

값을 1 증가 시킨다.

 

따라서 해당 a++ 이후에 a 에는

0x80000000

(이진수로 1000 0000 … 0000)

이 들어가겟지.

문제는 0x80000000 을

2 의 보수 표현법 체계하에서

해석한다면 반전 하면

(0111 1111 … 1111) 이 되고 다시 1 을

더하면 (1000 0000 … 0000) 이 되므로

-0x80000000, 즉 -2147483648 이 된다.

따라서 위와 같이 양수에 1 을 더했더니

음수가 나와버리는 불상사게 생기게 된다.

이와 같이 자료형의 최대 범위보다

큰 수를 대입하므로써 발생하는

문제를 오버플로우(overflow) 라고 하며,

C 언어 차원에서 오버플로우가 발생하였다는

사실을 알려주는 방법은 없기 때문에 여러분 스스로

항상 사용하는 자료형의 크기를 신경 써줘야만 한다!

주의 사항

실제로 1996년에 발사한 Arian 5 로켓은 제어 프로그램 상에서

발생한 오버플로우로 인해서 가속도를 제대로

계산하지 못해서 추락한 사례가 있습니다.

참고로 해당 로켓의 발사 비용은

3억 7천만 달러 (한화로 대략 4000 억원 정도 되죠) 였다고 합니다.

음수가 없는 자료형은 어떨까?

unsigned int 의 경우 음수가 없고

0 부터 4294967295 까지의

수를 표현할 수 있다.

unsigned int 가 양수만 표현한다고 해서

int 와 다르게 생겨먹은 것이 아니다.

unsigned int 역시

int 와 같이 똑같이 32 비트를 차지 한다.

다만, unsigned int 의 경우 int 였으면

2 의 보수 표현을 통해

음수로 해석될 수를 그냥 양수

라고 생각할 뿐이지.

따라서 unsigned int 에 예를 들어서

-1 을 대입하게 되면,

-1 은 0xFFFFFFFF 로 표현되니까,

#include <stdio.h>
int main() {

    unsigned int b = -1;

    printf("b 에 들어있는 값을 unsigned int 로 해석했을 때 값 : %u \n", b);

    return 0;
}

성공적으로 컴파일 했다면

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
b 에 들어있는 값을 unsigned int 로 해석했을 때 값 : 4294967295 
[Done] exited with code=0 in 0.326 second

와 같이 나온다.

참고로 printf 에서 %u 는

unsigned 타입으로 해석하라는 의미다.

물론 unsigned int 상에서도

오버플로우가 발생하지

않으라는 법이라는 없다.

예를 들어서 b 에

최대값을 대입한 뒤에 1 을 추가한다면

#include <stdio.h>

int main() {
    unsigned int b = 4294967295;
    printf("b : %u \n", b);
    b++;
    printf("b : %u \n", b);
    return 0;
}

성공적으로 컴파일 했다면

[Running] cd "d:\□□□□□_fFF□\□□□g□□□m□□□\F\" && gcc test.c -o test && "d:\□□□□□_fFF□\□□□g□□□m□□□\F\"test
b : 4294967295 
b : 0 
[Done] exited with code=0 in 0.323 seconds

와 같이 나온다.

즉, b 에 0xFFFFFFFF

(1111 1111 … 1111)이

들어가 있다가 1 증가함으로써

(1 0000 … 0000) 이 되었는데 앞서

이야기 하였듯이 자료형의

크기를 초과하는 비트는

그대로 버려지므로 그냥

0 (0000 0000 … 0000)

으로 해석된 것다.

즉 unsigned int 역시,

아니 C 언어 상에 모든 자료형은

오버플로우의 위험으로 부터 자유롭지

않는다.

자 그럼 이것으로

이번 공부기록 를 마치도록 한다.

C 언어에서 코딩을 할 때에는

언제나 오버플로우 문제에 신경써야 된다!

또한 아니 int 에 분명히 양수만

더했는데 왜 음수가 나왔지?

와 같은상황에 당황하지 않고 대처할 수 있겠지!

 

https://raw.githubusercontent.com/KuraiLuna/KuraiLuna.github.io/master/Art%20of%20Web%20Hacking/Chapter10/007.png