지난번에는 여러 연산과 변수에 대해서 배웠었다.
그러나 컴퓨터가 연산을 수행할 때 아무런 제약 없이 연산을 수행하는 것은 아니다.
왜냐하면 변수마다 각각 보관할 수 있는 데이터의 크기가 정해져 있기 때문이다.
예를들어서 int
형의 경우 -2147483648 부터 2147483647 까지의 정수 데이터를 보관할 수 있다.
그렇다면 만약 2147483648 을 넣으면 어떻게 될까?
#include <stdio.h>
int main(){
int a = 2147483647;
printf("%d \n", a++);
printf("%d \n", a);
return 0;
}
<결과값>
a : 2147483647
a : -2147483648
단지 2147483647 에 + 1 만 했을 뿐인데 int
형 최대값을 넘어서 -2147483648 가 나온다.
왜 이런 결과가 나올까?? 이를 알기 위해서는 먼저 컴퓨터가 음수형을 어떻게 표현하는지 알아야 한다.
만약 우리가 컴퓨터 CPU 개발자라면 컴퓨터상에 정수 음수를 어떻게 표현하도록 만들었을까??
가장 간단하게 생각해보면 우리가 음수와 양수를 부호로 표현하듯 비슷한 방법으로 부호를 나타내기 위해서
1비트를 사용하는 것이다.(예를 들어서 정수는 0, 음수는 1로) 그리고 나머지 부분을 실제 정수 데이터로 표현한다.
예를 들어서 가장 왼쪽의 비트를 부호라고 하자면(아래 예시들은 이진법으로 나타낸 것이다.)
0111
는 7이고
1111
는 -7이다.
꽤나 직관적이지만 여기에는 여러가지 문제점이 존재한다.
첫 번째로 0을 표현하는 방식이 두개가 존재한다는 것이다.
1000 // -0
0000 // 0
0과 -0 둘다 0이기 때문에 위 두가지 모두 0이 된다.
뭐 단순히 "0이 두개일 수 있지" 라고 생각할 수 있지만 이는 매우 큰 문제가 된다.
왜냐하면 어떠한 데이터값이 0인지 확인하는 경우가 매우 자주 있기 때문이다.
그렇게 되면 0인지 확인할 때 마다 +0
-0
인 경우 두번을 확인해야된다.
따라서 이상한 표현법 덕분에 컴퓨터의 자원을 낭비하게 되는 것이다.
또한 양수와 음수의 덧샘을 수행할 때 부호를 고려해서 수행해야 한다는 점이다.
예를 들어서 0001
과 0101
를 더할 때는 단순히 0110
이지만
0001
과 1001
를 더하면 사실 1001
이 -1
이므로 뺄샘을 수행해야 한다. 따라서
덧샘 알고리즘이 더욱 복잡해진다.
물론 부호 비트를 도입하여 양수와 음수를 구분하는 방법이 나쁜것은 아니다.
실제로 double
이나 float
처럼 소수인 데이터를 다루는 방식에서는 부호 비트를 도입하여 사용한다.
하지만 적어도 정수형에서는 부호 비트를 사용하는 것은 문제점이 있다.
그렇다면 다른 방법을 생각해보자.
만약 어떠한 수 의 음수는 가 될 것이고 이 둘의 합은 0이 나와야 할 것이다.
예를 들어서 7을 이진수로 나타내면
0111
이 되는데 여기서 어떠한 이진수를 더해서 0000
이 되게 하는 수가 있을까?
이때 덧셈시 컴퓨터가 4비트만 기억한다고 가정해보자
그렇다면 -7를 이진수로 표현하기 적당한 수는 1001
이 될 것이다.
왜냐하면 0111
+ 1001
= 10000
가 되는데 컴퓨터가 4비트만 기억하기 때문에 맨 앞에 1은 버려져 0000
가 되기 때문이다.
이렇게 덧셈을 고려했을 때 가장 자연스러운 방법으로 음수를 표현하는 방식을 바로 2 의 보수 표현 이라고 한다.
2의 보수 표현 체계 하에서 어떠한 수의 부호를 바꾸려면 먼저 반전 시킨 다음에 1을 더하면 된다.
예를 들어서 -7을 표현하기 위해서는 7의 이진수 표현인 0111
를 반전시킨 1000
에 1을 더해서 1001
로 표현하면 되는 것이다.
반대로 -7에서 7로 바꾸기 위해서는 1001
를 모두 반전시킨 다음(0110
)에 1을 더하여 0111
즉, 7이 나오게 된다.
(여기서 주의해야할 점은 앞에 0111
과 뒤에 1001
는 서로 다른 것이다. 이게 무슨말이냐면 전자는 7을 2진수로 표현한 것이고 후자는 -7을 2의 보수 표현법으로 나타낸 것이다.
즉 0111
+ 1001
는 사실 (0111) + (0111 의 보수) 라는 것이다. 둘다 그냥 이진수로 해석하면 안된다.)
이 체계에서 중요한 점은 0000
의 2의 보수는 그대로 0000
이 된다는 점이다.
왜냐하면 0000
의 반전 1111
에 1을 더하면 0000
이 되기 때문이다.
또한 어떠한 수가 양수인지 음수인지 확인하기도 쉽다. 그냥 맨 앞에 비트가 부호 비트라고 생각하면 되기 때문이다. 예를 들어서 1101
의 경우 맨 앞에 비트가 1이기 때문에 음수이다. 실제로 1101
의 부호를 구해보면 0011
이고 이 값은 3이기 때문에 1101
의 경우 -3인 것을 알 수 있다.
이와 같은 2의 보수 표현법을 통해 다음과 같은 장점을 얻을 수 있다
이러한 장점들 때문에 컴퓨터에서 정수는 2의 보수 표현법을 통하여 나타낸다.
여기서 한가지 재미있는 점은 2의 보수 표현법은 음수를 한 개더 표현할 수 있습니다. 왜냐하면 1000
같은 경우 음수이지만 다시 변환을 시켜도 1000
이 나오기 때문이죠(1000 → 0111 → 1000)
실제로 int
의 범위를 보면 -2,147,483,648 부터 2,147,483,647 까지로 음수가 한 개더 많습니다.
이에 대해서 조금만 더 자세히 살펴보면 우리가 4바이트에서 2진수로 최대로 표현할 수 있는 수는 1111 1111 ... 1111
일 것입니다(2의 보수 표현법 생각하지 않고). 이 수를 2의 보수 표현법으로 해석해보면 반전한 다음에 1을 더하면 0001 즉 1이 나옵니다. 따라서 1111 1111 ... 1111
는 -1이라는 것을 알 수 있죠.
-2,147,483,647부터 -1 까지를 1000 0000 ... 0001
부터 1111 1111 ... 1111
까지라고 말할 수 있고 반대로 양수에서는 0000 0000 ... 0001
부터 0111 1111 ... 1111
까지가 1부터 2,147,483,647 까지이다. 여기서 4바이트로 표현할 수 있는 수중에 빠진것이 두가지 있다.
먼저 0000 0000 ... 0000
즉 0이 빠졌다 그리고 또하나 바로 1000 0000 ... 0000
가 빠졌다.
이 수가 무엇인지 알아보기 위해 반전한 다음에 1을 더하여도 1000 0000 ... 0000
가 나온다. 이 수를 알아보기 위해서 2의 보수의 특성을 이용하여 1000 0000 ... 0000
+ 1 = 1000 0000 ... 0001
이고
1000 0000 ... 0001
는 -2,147,483,647 이기 때문에 즉, 1000 0000 ... 0000
는 -2,147,483,648 라는 것을 알 수 있다. 따라서 결론적으로 4바이트에서 표현할 수 있는 수의 범위는 -2,147,483,648 부터 2,147,483,647 까지가 되는 것이다.
그렇다면 아까 위에서 보았던 연산을 다시 확인해보자
int a = 2147483647;
printf("a : %d \n", a);
a++;
printf("a : %d \n", a);
처음에 a
에 int
의 최대값을 넣었을 때 아마 a
에는 0x7FFFFFFF
,이진수로 0111 1111 ... 1111
라는 값이 들어 있었을 것이다.(왜냐하면 맨 앞 비트는 0이여야 양수이기 때문이다.)
그런데 여기에 1을 더하면 어떻게 될까?
우리는 a
의 현재값이 int
가 가질 수 있는 최대값이기 때문에 1을 더해면 오류를 내뿜게 하거나 아니면 그냥
현재 그 값을 유지하고 싶을 것이다.
하지만 CPU는 0x7FFFFFFF
에 그냥 1을 더한다. 따라서 1을 더한 이후에 a
의 값은 0x80000000
, 이진수로는 1000 0000 ... 0000
값이 들어갈 것이다. 문제는 0x80000000
를 2의 보수 표현법 체계하에 해석하자면 반전하면 0111 1111 ... 1111
이 되고 여기서 1을 더하면 1000 0000 ... 0000
이 되므로 -0x80000000
즉 -2147483648 이 된다.
따라서 위와 같이 양수에 1을 더했을 때 음수가 나와버리는 불상사가 나타나게 된다. 이와 같이 자료형의 최대값보다 큰 수를 대입하게 되어 발생하는 문제를 오버플로우(overflow) 라고 하고 C언어 차원에서 오버플로우가 발생했다고 알려주는 방법이 없기 때문에 우리가 항상 사용하는 자료형의 크기를 신경써줘야 한다.
그렇다면 음수형이 없는 정수형인 unsigned int
의 경우는 어떨까?? 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 a = -1;
printf("%u 는 unsigned int 이다.\n",a);
return 0;
}
<결과값>
4294967295 는 unsigned int 이다.
물론 unsigned int
라고 해서 오버플로우가 발생하지 않는것은 아니다.
예를 들어서 b 에 unsigned int
의 최대값을 넣은 다음에 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
b에는 0xFFFFFFFF
, 이진수로 1111 1111 ... 1111
가 들어 있는데 여기에 1을 더하여
1 0000 0000 ... 0000
이 되고 앞서 얘기 했듯이 자료형의 크기를 넘는 비트는 그대로 버려지기 때문에
그냥 0000 0000 ... 0000
로 해석이 된다. 그렇기 때문에 0이 출력이 된다.
즉 unsigned int
역시, 아니 C 언어 상에 모든 자료형은 오버플로우의 위험으로부터 자유로운 것은 아니다.