모두의 코드
씹어먹는 C 언어 - <4 - 2. 컴퓨터가 음수를 표현하는 방법 (2의 보수)>

작성일 : 2020-04-29 이 글은 2900 번 읽혔습니다.

이번 강의에서는

  • 2 의 보수 표현법

  • 정수 오버플로우란?

에 대해 다룹니다.

씹어먹는 C 언어

안녕하세요 여러분! 지난 강좌에서 변수를 이용해서 여러가지 연산을 수행하는 방법에 대해 다루었습니다. 그런데 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;
}

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

실행 결과

a : 2147483647 
a : -2147483648 

와 같이 나옵니다.

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 인지 두 번이나 확인해야 하게 됩니다. 따라서 이상한 데이터 표현법 덕분에 쓸데없이 컴퓨터 자원을 낭비하게 됩니다.

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

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

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

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

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

0111

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

그렇다면 -7 의 이진수 표현으로 가장 적당한 수는 바로 1001 이 될 것입니다. 왜냐하면 01111001 을 더하면 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 을 대입하게 되면, -10xFFFFFFFF 로 표현되니까,

#include <stdio.h>

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

  return 0;
}

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

실행 결과

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

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

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

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

실행 결과

b : 4294967295 
b : 0 

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

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

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

뭘 배웠지?

컴퓨터 상에서 정수인 음수를 표현하기 위해서 2 의 보수 표현법을 사용합니다.

이에 따라 int 상에서 오버플로우가 발생하였을 때 양수에서 값을 증가시켰더니 음수로 바뀌는 기적을 볼 수 있습니다.

항상 오버플로우를 조심합시다.

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <4 - 2. 컴퓨터가 수를 표현하는 방법 (2의 보수)> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 2 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요