CPU 개발자라면 컴퓨터 상에서 정수 음수를 어떤식으로 표현하도록 만들었을까요? 가장 간단히 생각해보자면 우리가 부호를 통해서 음수 인지 양수 인지 나타내니까, 비슷한 방법으로 부호(sign bit)를 나타내기 위해서 1 비트를 사용하는 것입니다. (예를 들어서 0 이면 양수, 1 이면 음수) 그리고 나머지 부분을 실제 정수 데이터로 사용하면 되겠죠.
예를 들어서 가장 왼쪽 비트를 부호 비트라고 생각하자면 (참고로 아래 표현하는 수들은 모두 이진법으로 작성한 것입니다.)
0111(2)은 7(4+2+1)이 될 것이고
1111(2)은 맨 왼쪽 부호비트(sign bit)가 1 이므로 -7 을 나타내게 됩니다. 꽤나 직관적인 방식이기는 하지만 여러가지 '문제점'이 있습니다.
0000(2)과 1000(2) 둘 다 0을 의미하는 문제.
왜냐하면 컴퓨터 상에서 어떠한 데이터가 0 인지 아닌지 비교하는 일을 굉장히 많이 하거든요.
0 을 표현하는 방법이 두 가지라면, 어떠한 데이터가 0 인지 확인하기 위해서 +0 인지 -0 인지 두 번이나 확인해야 하게 됩니다. 따라서 이상한 데이터 표현법 덕분에 쓸데없이 컴퓨터 자원을 낭비하게 됩니다.
또 다른 문제로는, 양수에 음수의 덧셈을 수행할 때 부호를 고려해서 수행해야 한다는 점입니다. 예를 들어서 0001 과 0101 을 더한다면 그냥 0110 이 되겠지만 0001(2) 과 1001(2)을 더할 때에는 1001 이 사실은 -1 이므로 뺄셈을 수행해야 하죠. 따라서 '덧셈 알고리즘'이 좀 더 복잡해집니다.
여기서는 int 와 같은 정수 데이터만 다루지만 double 이나 float 처럼 소수인 데이터를 다루는 방식에서는 (이를 부동 소수점 표현 이라고 하는데, 나중 강좌에서 자세히 알아봅시다.) 부호 비트를 도입하여서 음수인지 양수인지를 표현하고 있습니다. (실제로 부동 소수점 표현법에서는 -0 과 +0 이 있습니다.)
그래서 정수에서 음수표현방법으로 2의보수 표현법을 빌려
만약에 어떤 x 와 해당 수의 음수 표현인 -x 를 더하면 당연히도 0 이 나와야 합니다. x + (-x) =0
0111(2) 는 7(10, 십진)
그렇다면 -7 의 이진수 표현으로 가장 적당한 수는 바로 1001 이 될 것입니다. 왜냐하면 0111 과 1001 을 더하면 10000 이 되는데, CPU 가 4 비트만 기억하므로 맨 앞에 1 은 버려져서 그냥 0000 이 되기 때문이지요.(이 때 덧셈 시에 컴퓨터가 4 비트만 기억한다고 가정합시다.)
0111(2) + 1001(2) = 10000(2) //1 0000
1001(2)은 0111(2) 에서 반전한 후 (1000) +1 한 보수 1001(2)
+A + (-A) = 0
참고로 두 개의 자료형을 더했을 때 범위를 벗어나는 비트는 그냥 버려진다고 생각하시면 됩니다. 마치 왼쪽으로 쉬프트 했을 때 맨 왼쪽에 있는 비트들이 버려지는 것 처럼 말이죠.
이렇게 덧셈을 고려하였을 때 가장 자연스러운 방법으로 음수를 표현하는 방식을 바로 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 개 더 많습니다.
처음에 a 에 int 최대값을 집어 넣었을 때 아마 a 에는 0x7FFFFFFF (이진수로 0111 1111 ... 1111) 라는 값이 들어가있을 것입니다. 그런데 여기서 1 을 더하게 되면 어떻게 될까요?
0x7FFFFFFF ( 0111 1111 1111 1111 1111 1111 1111 1111 )
0x80000000 ( 1000 0000 0000 0000 0000 0000 0000 0000 )
2의 보수 표현법에 의해
반전 (0111 1111 1111 1111 1111 1111 1111 1111)
다시 1을 더하면 (1000 0000 0000 0000 0000 0000 0000 0000 )
-0x80000000, 즉 -2147483648
우리는 a 의 현재 값이 int 가 보관할 수 있는 최대값이므로 1을 더 증가 시킨다면 오류를 내뿜게하거나 아니면 그냥 현재 값 그대로 유지하게 하고 싶었을 것입니다.
하지만 CPU 는 그냥 0x7FFFFFFF 값을 1 증가 시킵니다. 따라서 해당 a++ 이후에 a 에는 0x80000000 (이진수로 1000 0000 ... 0000) 이 들어가겟죠. 문제는 0x80000000 을 2의 보수 표현법 체계하에서 해석한다면 반전 하면 (0111 1111 ... 1111) 이 되고 다시 1 을 더하면 (1000 0000 ... 0000) 이 되므로 -0x80000000, 즉 -2147483648 이 됩니다.
'자료형'의 최대 '범위'보다 큰 수를 대입하므로써 발생하는 문제를 오버플로우(overflow) 라고 하며, C 언어 차원에서 오버플로우가 발생하였다는 사실을 알려주는 방법은 없기 때문에 여러분 스스로 항상 사용하는 자료형의 크기를 신경 써줘야만 합니다!
참고:
https://ndb796.tistory.com/4(보수)
https://life-with-coding.tistory.com/298(보수)
https://modoocode.com/308(2의 보수 표현법과 정수 오버플로우)
'C/C++' 카테고리의 다른 글
[C/C++] 포인터 (정리 및 수정 중) (0) | 2023.03.23 |
---|---|
[C/C++] 진법변환 (0) | 2023.03.22 |
[C/C++] C 증감연산자 (전위prefix, 후위postfix) (0) | 2023.03.20 |