모두의 코드 - 4-1. 계산, 4-2. 컴퓨터가 음수를 표현하는 방법 (2의 보수)

2024. 9. 23. 00:22Programming Language/C

4 - 1. 계산

최초의 컴퓨터는 무슨 목적을 갖고 개발이 되었을까?

최초의 컴퓨터라고 불리우는 애니악의 경우는 포탄을 발사 했을때 어디에 떨어질지를 계산해서 예측하는 기계였다.

결국 컴퓨터는 인간이 하기 힘든 복잡한 계산을 하기 위해서 개발이 된 기계이다.

 

산술 연산자, 대입 연산자

이번엔 C언어에서 컴퓨터에 어떻게 연산 명령을 내리는지 확인해보자.

 

계산이라고 하면 사칙연산(덧셈, 뺄셈, 곱셈, 나눗셈)을 생각하게 된다.

코드에서는 여기에 나머지를 계산하는 연산을 포함한 다섯가지를 산술 연산자(Arithmetic Operator)라고 한다.

 

각각 코드에서 사용하는 연산자는 

  • 덧셈 : +
  • 뺄셈 : -
  • 곱셈 : *
  • 나눗셈 : / 
  • 나머지 : %

로 표현한다.

 

실 코드를 보면 

와 같이 모든 연산을 확인해보면 

이렇게 결과를 출력 받을 수 있다.

 

위 코드에서 먼저 

이 부분에 대해서 알아보자.

 

a = 10는 a에 10을 대입하는 건데 그렇다면 10 = a은 동일한것인가 라는 생각을 할 수 있다. 

수학에서는 a= 10과 10 = a는 다르지않기 때문에 그런 생각을 할 수 있는데 C언어의 컴파일러의 경우는 "="기호를 뒤에서부터 해석한다.

a = 10이라면 컴파일러가 해당 코드를 10을 a에 대입해라 라는 말로 이해하지만, 10 = a라면 a라는 값을 10에 대입해라 라는 이상한 문장이 되어 오류를 발생시킨다.

 

여기서 "="를 대입 연산자(Assignment Operator)라고 한다.

 

따라서 

이 문장은 

이 문장과 완전히 동일한 문장이 된다.

앞에서 말했듯 = 는 뒤에서 부터 해석하기에 제일 먼저 e = 5를 해석한 다음에 d = e를 해석하고 마지막 a = b 까지 차례대로 해석해 나가기 때문에 동일한 문장이 된다.

 

 

여기서 볼것은 나누기 연산을 하는 /의 경우, a, b 모두 정수형 데이터 타입을 갖기 때문에 3.3333...이 아니라 3 만을 출력하게 된다는 점이다.

 

그리고 %의 경우는 %%로 표시 했는데 이는 %가 %d, %o와 같이 사용되기 때문에 %를 그냥 하나만 작성한 경우에 문자로 인식하지 않고 어떤 특수 기호로 생각하기 때문에 %뒤에 아무것도 붙지 않았다고 에러를 만들게 된다.

그렇기에 %를 표시하기 위해서는 %%와 같이 입력해줘야만 문자 %로써 출력되게 된다.

 

그리고 나눗셈에서 주의해야할 점은 나눗셈의 결과가 실수라고 하더라도 a와 b가 정수형이기 때문에 %d가 아닌 %f로 값의 출력을 시도하면 에러를 발생시키고 결과가 이상하게 출력된다는 점이다.

 

그렇다면 a나 b중 하나의 변수의 타입이 실수형인 경우는 어떨까?

에러없이 실수형으로 잘 출력된다.

이건 왜이럴까?

 

이는 컴파일러가 산술변환이라는 과정을 거치기 때문이다.

산술변환이란 어떤 자료형이 다른 두 변수를 연산할 때, 숫자의 범위가 큰 자료형으로 자료형들이 변경되는 것이다.

a는 int형으로 double형인 b보다 숫자의 범위가 작기 때문에 a를 double형으로 산술변환해서 연산하기 때문에 문제 없이 연산하여 결과를 출력하는 것이다.

 

그러면 출력을 %f가 아니라 %d로 하면 어떻게 될까

에러를 발생시키고 이상한 값이 출력되게 된다.

이건 %d가 정수 데이터를 출력할때 사용되는 방법이기 때문이다.

 

대입 연산자

위 코드를 실행시켜보면 

이런 결과를 출력해준다.

 

여기서 볼 점은 

이 부분인데 수학적으로는 a = a + 3 이기 때문에 a를 이항 시켜 a - a = 3이니까 0 = 3이라는 결론을 유추할 수 있는데 이건 수학적으로는 맞지만 C언어에서 의미하는 바는 다르다.

 

=는 같다의 등호가 아닌 대입을 하는 연산자 이기 때문에 a + 3을 a에 대입한다고 이해해야 한다.

결과적으론  3 + 3을 a에 대입하기에 a = 6이 나오는게 되는 것이다.

 

이렇게 될 수 있는 이유는 +를 =보다 먼저 연산하기 때문에 a +3을 연산한 후에 그 값을 대입(=)하는 순서를 갖기에 a에 6이라는 값이 들어갈 수 있는 것이고 이런 연산의 순서를 연산자 우선순위라고 한다.

 

더보기

너무 기초적인 내용이기에 내용을 조금 생략하려고 함

  • ++a, a++과 같은 전위, 후위연산자에 대한 내용
  • +=, -=과 같은 복합 대입 연잔사에 대한 내용
  • 비트연산자에서 네트워크 부분을 통해 공부했던 &(AND연산), |(OR연산), ^(XOR연산), ~(NOT연산)

 

비트 연산자에서 공부 되지 않은 내용

<< 연산(쉬프트 연산)

위 연산 기호에서 느껴지듯이 비트를 왼족으로 쉬프트(shift)시킨다.

예를들어 101011을 1 만큼 쉬프트 시키면(이를 a << 1 로 작성한다)

 

010110

 

이 된다.

쉬프트 시에 앞에 쉬프트 된 숫자가 갈 자리가 없다면, 그 부분은 버려진다.

또한 맨 뒤쪽에 새로 채워지는 부분은 앞에서 버려진 숫자가 가는게 아니라 무조건 0으로 채워진다.

 

>>연산

이 또한 동일하게 쉬프트 연산이나 방향이 반대이다.

오른쪽으로 쉬프트 하되 맨 오른 쪽 숫자가 갈 곳이 없다면 그 숫자는 버려진다.

이때 <<연산에서는 새로 생성되는 값은 무조건 0이 채워지는 것과는 달리 맨 앞부분에 맨 왼쪽에 있던 수가 채워진다.

예를 들면 맨 앞 숫자가 1이라면

 

11100010 >> 3 = 11111100

 

이 되고 맨 앞 숫자가 0이라면

 

00011001 >> 3 = 00000011

 

이 된다.

 

** 참고로 시스템에 따라서 >> 쉬프트의 경우도 무조건 맨 왼쪽에 0이 채워지는 경우도 있다.

 

비트연산을 이걸 뭐 어디에다 쓰는건데 자꾸 나오는거지 싶긴하다만, 아직은 그 쓰임새를 짚고 넘어가기 어렵기 때문이고 종종 나중에 등장할 테니 잘 기억하고 있는게 좋을것 같다고 한다.

 

아무튼 위 비트 연산자를 모두 코드로 입력해서 출력해보면 

 

여기서 첫 세줄은 문제 없이 결과를 유추할 수 있는데 

NOT 연산부터 이해가 안가는 점이 생긴다.

우리는 1010 1111의 값을 NOT연산한다면 0101 0000이 나와야 하는데 왜 1....1 0101 0000이 나오게 된것일까?

(ffffff50은 1111 1111 1111 1111 1111 1111 0101 0000 이다)

이건 a의 자료형이 int이기 때문에 하나의 데이터를 저장하기 위해서 메모리상의 4바이트(32비트)를 사용하기 때문에 기존에 우리가 넣었던 값인 1010 1111(0xAF)이 사실은 0000 0000 0000 0000 1010 1111이기 때문이다.

 

동일하게 생각해보면 

이 두 코드도 

a << 2 의 경우는 0000 0000 0000 0000 1010 1111 을 << 2 했기에 0000 0000 0000 0010 1011 1100이 되고 이걸 16비트로 변경하기 위해서 4bit마다 값이 없는 구간을 떼고 보면 0010 1011 1100이 되어서 2bc의 결과를 출력하는 것이다.

또한 b >> 3의 경우도 0000 0000 0000 0000 1011 0101 >> 3 이기 때문에 1111 0110이 아니라 0000 0000 0000 0000 0001 0110이 되는 것이고 이걸 또 16진수로 수정해보면 0001 0110인 16이 되는 것이다

 

복잡한 연산 - 연산자 우선순위에 대한 내용

1 (), [] ,->, . ,(expr)++, (expr)-- 왼쪽 우선
2 !, ~, +, -(부호), *p(포인터), , sizeof, ++(expr), --(expr) 오른쪽 우선
3 *(곱셈), /, % 왼쪽 우선
4 +, -(덧셈, 뺄셈) 왼쪽 우선
5 <<, >> 왼쪽 우선
6 <, <=, >, >= 왼쪽 우선
7 ==, != 왼쪽 우선
8 & 왼쪽 우선
9 ^ 왼쪽 우선
10 | 왼쪽 우선
11 && 왼쪽 우선
12 || 왼쪽 우선
13 ?:(삼항 연산자) 오른쪽 우선
14 =, +=, -=, *=, %=, /= 오른쪽 우선
15 , 왼쪽 우선

 

왼쪽 우선의 계산법은 아래와 같다

 

a = b + c + d

 

라면 

 

b+ c 를 제일 먼저 하고 그 결과인 C를 C + d를 하게 되고 마지막으로 오른쪽우선인 a = D를 하게 된다

수학적으로 순서를 확인하기 쉽게 괄호로 감싸보면 아래와 같은 형태가 된다. 

 

(a = ((b + c) + d))

 

오른쪽 우선을 사용하는 연산자는 몆 안되지만 그중 하나인 = 대입연산자를 확인해보면 

 

a = b = c = d = 3;

 

이라고 한다면 위에서 확인했듯이 

 

d = 3을 가장 먼저 수행 c = D를 수행(D는 d = 3의 결과물), b = C를 하고 a = B를 마지막으로 연산한다.

그러면서 a, b, c, d 모두 3이란 값을 가질 수 있게 된다.

 

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

변수를 이용해서  여러가지 연산을 수행하는 방법을 여태까지 배웠는데 사실 C언어에서는 아무 제약없이 연산을 수행할 수 있는 것은 아니다.

데이터의 타입마다 보관할 수 있는 데이터이 크기가 한정되어 있기 때문이다.

 

그렇다면 테이터의 타입의 크기를 넘어서는 데이터가 저장될 경우는 어떻게 될까?

위 코드를 실행해보면

와 같은 결과가 출력된다.

 

먼저 이 부분에서 a에 int의 최대 범위인 수를 넣었을땐 아무런 문제도 없고 출력또한 문제없이 잘됐는데 

이 부분이 문제이다.

최대범위를 넘어 섰기에 전혀 예상하지 못했던 -2147483648이 출력되었다.

왜 음수가 나오게 되었을까?

이걸 이해하기 위해서는 컴퓨터는 어떻게 음수를 표현하는지를 이해해야 한다.

 

음수 표현

CPU 개발자는 컴퓨터 상에서 정수 음수를 표현하기 위해서 부호와 비슷한 방식으로 1비트를 사용해서 음수와 양수를 표현하도록 만들었다.

 

예를 들어 가장 왼쪽 비트를 부호 비트라고 한다면

 

0111

 

의 경우는 10진법으로 7이 되고 

 

1111

 

은 10진법으로 -7을 나타낼것이다.

 

이는 직관적이나 여러 문제점이 있다.

첫번째로 0을 나타내는 방식이 두개라는 점이다.

즉 0000과 1000은 동일하게 0이 된다는 점이다.

이렇게 된다면 0인지 비교하는 연산에서 0000과 1000을 동일하게 보기 때문에 +0인지 -0인지를 확인해줘야 하는 문제가 발생하게 된다.

이게 문제인 이유는 한번 비교할 내용을 두번 비교하게 되기 때문에 컴퓨터의 자원을 낭비하게 되기 때문이다.

 

또 다른 문제로 양수의 음수의 덧셈을 수행할 때 부호를 고려해서 수행해야 한다는 점이다.

예를 들어 0001과 0101을 더한다면 그냥 0110이 되나 0001과 1001을 더할 때는 1001이 사실은 -1이기 때문에 뺄셈을 수행해줘야 하기 때문에 계산이 너무 복잡하게 된다.

 

int의 경우는 그렇지 않지만 double과 float처럼 소수인 데이터를 다루는 방식에서는(이걸 부동 소수점표현 이라고 하는데 이건 나중에 잘 알아보자) 부호 비트를 도입해서 음수인지 양수인지를 표현하고 있다(그렇기에 +0과  -0이 실제로 존재한다.)

 

그러나 적어도 int 타입의 데이터에서는 부호비트를 사용하는 방식은 문제점이 있다.

 

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

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

만약 x와 해당 수의 음수 표현인 -x를 더하면 0이 나오게 될것이다.

예를 들어 7을 이진수로 표현하면 0111이 된다.

만약 덧셈을 수행해서 0000이 되는 이진수가 있을까?

이때 덧셈 시에 컴퓨터가 4비트만 기억한다고 가정해보자.

이때 참고로 두개의 자료형을 더했을때 범위를 벗어나는 비트는 왼쪽 shift 연산 처럼 버려진다고 보면 된다.

그렇다면 -7의 이진수 표현으로 가장 적합한 이진수는 1001이 될것이다.

왜냐하면 더해졌을때 10000이 될것이고 CPU는 4비트만 기억하기 때문에 0000만 남기 때문이다.

 

이렇게 덧셈을 고려했을때 가장 자연스러운 방법으로 음수를 표현하는 방식이 2의 보수 표현이라고 한다.

2의 보수 표현 체계에서 어떤 수의 부호를 바꾸려면 비트를 반전시킨 뒤에 1을 더하면 된다.

 

이전에 봤던 7을 한번 보자 7은 이진수로 0111인데 비트를 반전시키면 1000이 되고 거기에 1을 더하면 1001으로 위에서 봤던 값이 나온다.

반대로 음수에서 양수로 가고 싶다면 1001을 반전한 뒤에 1을 더해주면 된다(0110 + 1 = 0111)

 

여기서 중요한건 0000의 2의 보수는 그대로 0000이 된다는 것이다

0000을 반전시키면 1111이고 거기에 1을 더하면 10000이 되고 4비트만 떼내보면 0000이 되기 때문이다.

 

또 어떤 수가 음수인지 양수인지 판단하는 방법도 매우 쉽다.

그냥 맨 앞 비트가 부호 비트라고 생각하면 된다.

예를 들어 1101의 경우는 맨 앞 비트가 1이기 때문에 음수이다. 

따라서 이 수가 어떤 값인지 알고 싶다면 보수를 구한후에 (1101 -> 0010 -> 0011) - 만 붙여주면 될것이다.

0011이 3이므로 1101은 -3이 된다.

 

** 위 단락에서 의문이 생기는건 그렇다면 1101의 이진법이 원래 의미하던 13은 없는 숫자가 되는것인가..?

이게 CPU가 4개의 비트만 연산한다는 가정이기 때문인가? 그렇다면 CPU는 7이상의 숫자를 표현할 수 없게되는건데 그렇다면 8~16까지의 숫자는 어떻게 표현할 것인가? 

 

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

  • 음수나 양수 사이 덧셈시 굳이 부호를 고려해서 덧셈을 수행할 필요가 없다
  • 맨 앞 비트를 사용해서 부호를 빠르게 알아낼 수 있다.

와 같은 장점으로 컴퓨터에서 정수는 2의 보수 표현법을 사용해서 나타내게 된다.

 

한가지 재미 있는 점은 2의 보수 표현법에선 음수를 하나 더 표현 할 수 있다.

1000의 경우 음수이지만 변환시켜도 다시 1000이 나오기 때문이다.

 

1000 -> 0111 -> 1000

 

실제로 int 의 범위를 살펴보면 -2,147,483,648 부터 2,147,483,647로 음수가 하나 더 많다.

 

그럼 다시 전에 생성했던 코드를 한번 확인해보자.

처음에 a에 int의 최대값을 넣은 경우(2147483647) a에는 0x7FFFFFFF(이진수로 0111 1111 ... 1111)이라는 값이 들어 있었을 텐데 여기서 1을 더하게 되면 CPU는 그냥 0x7FFFFFFF의 값을 1증가 시킨다. 그러면 0x80000000이 되면서 이진수로는 1000 0000 ... 0000이 될텐데 이 0x80000000을 2의 보수 표현법 체계하에 해석해서 반전하여 음수를 만든다면 먼저 0111 1111 ... 1111이 되고 이걸 +1 하면 또 1000 0000 ... 0000이 되면서 -0x80000000, 즉 -2147483648이 되게 되는 것이다.

 

이처럼 자료형의 최대 범위보다 큰 수를 대입하면서 생기는 문제를 오버플로우(overflow)라고 하며, C언어에서는 오버플로우가 발생한 사실을 알려주는 방법이 없기에 항상 자료형의 크기를 신경 써야만 한다.

 

음수가 없는 자료형이라면?

unsigned int의 경우는 음수없이 0 ~ 4294967295을 표현하는데, unsigned int가 양수만 표현한다고 int와 다르지 않고 int와 동일하게 32비트를 차지한다.

다만 unsigned int이 경우 int 였으면 2의 보수 표현을 통해 음수로 해설될 수를 양수로 생각할 뿐이다.

 

unsigned int에 -1을 대입하면 0xFFFFFFFF가 되기에 

( -1 = 0000 ... 0001 => 1111 1111 ... 1110 => 1111 1111 ... 1111 => 0xFFFFFFFF )

4294967295가 나오게 된다.

(1111 1111 ... 1111로 unsigned int의 최대값인 4294967295가 계산된다)

 

** 여기서 %u는 unsigned 타입으로 해석하라는 의미

 

그리고 unsigned int 또한 오버플로우가 발생할 수 있는데

 이 경우는 int와는 다르게 1111 1111 ... 1111에서 1이 추가되면서 1 0000 0000 .. 0000이 되면서 맨앞에 1이 버려지면서 0이 된다.

 

이렇게 C언어 상에 모든 자료형은 오버플로우의 위험으로 자유롭지 않기에 자료형의 범위를 잘 알아둬야 한다.