----------------
title : 씹어먹는 C 언어 - <4 - 2. 컴퓨터가 음수를 표현하는 방법 (2의 보수)>
cat_title : 4 - 2. 컴퓨터가 수를 표현하는 방법 (2의 보수)
tex_title : 컴퓨터가 음수를 표현하는 방법 (2의 보수)
next_page : 9
publish_date : 2020-04-29
path : /C
--------------

이번 강의에서는

* 2 의 보수 표현법
* 정수 오버플로우란?

에 대해 다룹니다.

![씹어먹는 C 언어](/img/ChewingClogo.png)

안녕하세요 여러분! 지난 강좌에서 변수를 이용해서 여러가지 연산을 수행하는 방법에 대해 다루었습니다. 그런데 C 언어에서 아무런 제약 없이 연산을 수행할 수 있는 것은 아닙니다. 왜냐하면 변수 마다 각각의 타입에 따라서 **보관할 수 있는 데이터의 크기** 가 정해져 있기 때문이죠.

예를 들어서 `int` 의 경우 -2147483648 부터 2147483647 까지의 정수 데이터를 보관할 수 있습니다.

그렇다면 아마 여러분들은 아래와 같은 질문을 하실 수 있습니다.

> 만약에 변수의 데이터가 주어진 범위를 넘어간다면 어떻게 되나요?

한 번 직접 코드를 작성해봅시다.

```cpp-formatted
#include <stdio.h>

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

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

  return 0;
}
```

성공적으로 컴파일 하였다면

```exec
a : 2147483647 
a : -2147483648 
```

와 같이 나옵니다.

```cpp-formatted
int a = 2147483647;
printf("a : %d \n", a);
```

먼저 위와 같이 `a` 라는 변수를 정의한 뒤에 `int` 가 표현할 수 있는 최대값인 2147483647 를 대입하였습니다. 해당 문장은 문제가 없으며 `printf` 로도 2147483647 가 잘 출력되었습니다.

```cpp-formatted
a++;
printf("a : %d \n", a);
```

반면에 `a++` 을 해서 `int` 가 표현할 수 있는 최대값을 넘어가버렸습니다. 그런데 놀랍게도 전혀 예상하지 못한 값이 출력되었습니다. 바로 **-2147483648** 이 나온 것이죠. 어떻게 양수에서 1 을 더했는데 음수가 나올 수 있을까요?

이에 대한 대답을 하기 위해선 먼저 컴퓨터에서 어떻게 음수를 표현하는지 알아야 합니다.

### 음수 표현 아이디어

여러분이 CPU 개발자라면 컴퓨터 상에서 정수 음수를 어떤식으로 표현하도록 만들었을까요? 가장 간단히 생각해보자면 우리가 부호를 통해서 음수 인지 양수 인지 나타내니까, 비슷한 방법으로 부호를 나타내기 위해서 1 비트를 사용하는 것입니다. (예를 들어서 0 이면 양수, 1 이면 음수) 그리고 나머지 부분을 실제 정수 데이터로 사용하면 되겠죠.

예를 들어서 가장 왼쪽 비트를 부호 비트라고 생각하자면 (참고로 아래 표현하는 수들은 모두 이진법으로 작성한 것입니다.)

```info
0111
```

은 7 이 될 것이고

```info
1111
```

은 맨 왼쪽 부호 비트가 1 이므로 -7 을 나타내게 됩니다. 꽤나 직관적인 방식이기는 하지만 여러가지 문제점이 있습니다. 첫 번째로 0 을 나타내는 방식이 두 개라는 점입니다. 즉

```info
0000
```

도 0 이고

```info
1000
```

역시 0 이지요. (-0 은 0 이니까) *뭐 0 이 두 개일 수 도 있지* 라고 생각하실 수 있겠지만 사실 이는 매우 큰 문제 입니다. 왜냐하면 컴퓨터 상에서 어떠한 데이터가 0 인지 아닌지 비교하는 일을 굉장히 많이 하거든요.

0 을 표현하는 방법이 두 가지라면, 어떠한 데이터가 0 인지 확인하기 위해서 `+0` 인지 `-0` 인지 두 번이나 확인해야 하게 됩니다. 따라서 **이상한 데이터 표현법 덕분에 쓸데없이 컴퓨터 자원을 낭비하게 됩니다.**

또 다른 문제로는, 양수의 음수의 덧셈을 수행할 때 부호를 고려해서 수행해야 한다는 점입니다. 예를 들어서 `0001` 과 `0101` 을 더한다면 그냥 `0110` 이 되겠지만 `0001` 과 `1001` 을 더할 때에는 `1001` 이 사실은 `-1` 이므로 뺄셈을 수행해야 하죠. 따라서 덧셈 알고리즘이 좀 더 복잡해집니다.

물론 부호 비트를 도입해서 음수와 양수를 구분하는 아이디어 자체는 나쁜 생각은 아닙니다. 여기서는 `int` 와 같은 정수 데이터만 다루지만 `double` 이나 `float` 처럼 소수인 데이터를 다루는 방식에서는 (이를 *부동 소수점* 표현 이라고 하는데, 나중 강좌에서 자세히 알아봅시다.) 부호 비트를 도입하여서 음수인지 양수인지를 표현하고 있습니다.\sidenote{실제로 부동 소수점 표현법에서는 -0 과 +0 이 있습니다.}

하지만 적어도 정수를 표현하는 방식에서는 부호 비트를 사용하는 방식은 문제점이 있습니다.

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

그렇다면 다른 방법을 생각해봅시다. 만약에 어떤 $$x$$ 와 해당 수의 음수 표현인 $$-x$$ 를 더하면 당연히도 0 이 나와야 합니다. 예를 들어서 7 을 이진수로 나타내면

```info
0111
```

이 되는데 여기에 더해서 `0000` 이 되는 이진수가 있을까요? 이 때 덧셈 시에 컴퓨터가 4 비트만 기억한다고 가정합시다.

그렇다면 -7 의 이진수 표현으로 가장 적당한 수는 바로 `1001` 이 될 것입니다. 왜냐하면 `0111` 과 `1001` 을 더하면 `10000` 이 되는데, CPU 가 4 비트만 기억하므로 맨 앞에 1 은 버려져서 그냥 `0000` 이 되기 때문이지요.\sidenote{참고로 두 개의 자료형을 더했을 때 범위를 벗어나는 비트는 그냥 버려진다고 생각하시면 됩니다. 마치 왼쪽으로 쉬프트 했을 때 맨 왼쪽에 있는 비트들이 버려지는 것 처럼 말이죠.}

이렇게 덧셈을 고려하였을 때 가장 자연스러운 방법으로 음수를 표현하는 방식을 바로 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 개 더 많습니다.

자 그렇다면 이전 코드를 다시 살펴봅시다.

```cpp-formatted
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 언어 차원에서 오버플로우가 발생하였다는 사실을 알려주는 방법은 없기 때문에 **여러분 스스로 항상 사용하는 자료형의 크기를 신경 써줘야만 합니다!**

```lec-warning
실제로 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` 로 표현되니까,

```cpp-formatted
#include <stdio.h>

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

  return 0;
}
```

성공적으로 컴파일 하였다면

```exec
b 에 들어있는 값을 unsigned int 로 해석했을 때 값 : 4294967295 
```

와 같이 나옵니다. 참고로 `printf` 에서 `%u` 는 `unsigned` 타입으로 해석하라는 의미 입니다.

물론 `unsigned int` 상에서도 오버플로우가 발생하지 않으라는 법이라는 없습니다. 예를 들어서 `b` 에 최대값을 대입한 뒤에 1 을 추가한다면;

```cpp
#include <stdio.h>

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

  b++;
  printf("b : %u \n", b);

  return 0;
}
```

성공적으로 컴파일 하였다면

```exec
b : 4294967295 
b : 0 
```

와 같이 나옵니다. 즉, `b` 에 `0xFFFFFFFF` (`1111 1111 ... 1111`) 이 들어가 있다가 1 증가함으로써 (`1 0000 ... 0000`) 이 되었는데 앞서 이야기 하였듯이 자료형의 크기를 초과하는 비트는 그대로 버려지므로 그냥 0 (`0000 0000 ... 0000`) 으로 해석된 것입니다.

즉 `unsigned int` 역시, 아니 C 언어 상에 모든 자료형은 오버플로우의 위험으로 부터 자유롭지 않습니다.

자 그럼 이것으로 이번 강좌를 마치도록 하겠습니다. C 언어에서 코딩을 할 때에는 언제나 오버플로우 문제에 신경써야 합니다. 또한 *아니 int 에 분명히 양수만 더했는데 왜 음수가 나왔지?* 와 같은 상황에 당황하지 않고 대처할 수 있겠죠!

```lec-summary
컴퓨터 상에서 정수인 음수를 표현하기 위해서 2 의 보수 표현법을 사용합니다.
이에 따라 `int` 상에서 오버플로우가 발생하였을 때 양수에서 값을 증가시켰더니 음수로 바뀌는 기적을 볼 수 있습니다.
항상 오버플로우를 조심합시다.
```

##@ chewing-c-end