2025. 2. 16. 14:44ㆍProgramming Language
C언어 C++을 공부하다 보면 정수를 이야기할 때 항상 2의 보수법을 공부해야하는 경우가 많다.
매번 배울 때마다 정리할 수 도 없고 나도 기억에 두고 싶어 따로 정리해보고자 한다.
1. 2의 보수법(Two's Complement)이란?
2의 보수법은 컴퓨터에서 음수를 표현하는 방법으로, 양수와 음수를 동일한 방식으로 덧셈과 뺄셈 연산이 가능하도록 설계된 이진수 표현 방식이다.
1-1. 2의 보수법은 왜 사용해야할까?
1-1-1. 부호-크기(Sign-Magnitude)방식
초창기 컴퓨터에서는 부호-크기(Sign-Magnitude)방식을 사용했었다
부호-크기방식은 가장 왼쪽의 비트인, MSB(Most Significant Bit, 최상위 비트)를 부호 비트로 사용하여 0이면 양수, 1이면 음수를 나타내고 나머지 비트는 숫자의 절댓값으로 표현되었다.
예를 들어서 8비트의 환경에서 +5와 -5를 표현한다면
+5 → 00000101
-5 → 10000101 (부호 비트만 다름)
이렇게 최상위 비트만 다르게 설정해서 표현했었는데 연산을 하게 되면 문제점이 발생한다.
만약 +5와 -5를 더한다면 0이 되어야만 하고 이때 이를 2진수로 표현하면 00000000이 되어야 한다.
그러나 위에 표현된 비트를 단순한 비트연산을 할 경우
00000101
+ 10000101
---------------
10001010 (-10)
과 같은 결과가 도출되게 된다.
그렇기에 별도의 논리를 추가하고 회로 설계가 복잡해지는 결과를 불러 냈다.
또한 +0과 -0이란 값이 존재하기에 불필요한 중복이 발생하게 되었다.
이러한 문제가 있기에 개선 된 방식이 1의 보수법(One’s Complement)이였다.
1-1-2. 1의 보수법(One’s Complement)
1의 보수법은 음수를 표현할 때 모든 비트를 반전(0 → 1, 1 → 0)하여 저장하는 방식으로 아래와 같이
+5 → 00000101
-5 → 11111010 (모든 비트를 반전)
수를 표현했었다.
이를 통해서 덧셈 연산을 수행할 때, 캐리 추가(Carry Add)를 하면 정확한 값을 도출할 수 있었다.
00000101 (+5)
+ 11111010 (-5, 1의 보수)
-----------------
11111111
(이진수로는 -0, -0은 캐리가 넘쳤다고 생각하여 캐리를 추가해줘야만 함)
//캐리 추가 작업
11111111
+ 1
----------------
00000000 (정확하게 0이 됨)
그러나 이렇게 사용하면 매번 -0일때 1을 더해주는 작업이 필요하게 된다.
결국 +0과 -0으로 0이 두개가 존재하기 때문이다.
그래서 결국 더 복잡해지는 결과를 만들어 내게 된다.
이런 문제를 해결 하기 위해 최종적으로 2의 보수법(Two’s Complement)이 개발되었다.
1-1-3. 2의 보수법(Two’s Complement)
2의 보수법의 경우는 1의 보수법에서 마지막에 1을 더해준다.
예를 들어 +5와 -5를 보자면
00000101 - +5
↓ - 모든 숫자 반전
11111010
↓ - + 1
11111011 - -5
이러면 별도의 추가적으로 캐리 추가같은 과정 없이 일반적인 비트연산을 수행하면 결과가 도출되고 그로 인해서 하드웨어의 구현이 좀 더 단순해 질 수 있었고 +0과 -0이 따로 존재하지 않게 만들어주는 결과를 만들어 냈다
2. 2의 보수법의 사용 방법
2의 보수법은 위에서 말한것과 같이 양의 수를 표현하는 2진수에서 모든 숫자를 반전시킨 후에 1을 더해준값을 음수로 만든다
예를 들어 +5와 -5를 표현한다면 기본적으로 +5를 가지고 -5를 만든다
00000101 - +5
↓ - 모든 숫자 반전
11111010
↓ - + 1
11111011 - -5
2-1. 덧셈 연산
8비트 환경에서 5와 -3을 더한다고 할때 5의 경우는 00000101이고 -3의 경우는 00000011을 반전시킨 후 1을 더한 값인 11111101이 된다.
이를 더하는 과정은 아래와 같다
00000101 (+5)
+ 11111101 (-3)
-----------------
00000010 (+2, 올바른 결과)
2-2. 뺄셈 연산
2의 보수법에서 만약 5 - 3 이란 연산을 수행한다면 +5 + (-3)으로 변경하여 연산을 진행한다.
그럼 결국 위 덧셈 연산과 동일한 방법이 된다
// 5 - 3
00000101 (+5)
- 00000011 (3)
↓ // 3을 -3으로 전환
00000011 (3)
11111100 (1의 보수)
+ 1
-----------------
11111101 (-3)
↓ // 5 - 3을 5 +(-3)으로 전환
00000101 (+5)
+ 11111101 (-3)
-----------------
00000010 (+2)
3. 주의해야할 점
2의 보수법은 장점이 많으나 특정 상황에서는 주의해야할 부분이 있다.
3-1. 오버플로우(Overflow)에 주의
2의 보수법에서는 특정 범위를 초과하는 값이 발생하면 오버플로우(Overflow)가 발생할 수 있다.
오버플로우가 발생한다면 부호가 바뀌는 결과를 도출할 수도 있다.
예시를 보자면 8비트 환경에서 127 + 1이란 연산을 한다면
01111111 (+127)
+ 00000001 (+1)
-----------------
10000000 (-128, 잘못된 결과!)
와 같이 오버플로우가 발생하며 예상과 다른 값이 나오게 된다.
음수에서도 동일하다.
8비트 환경에서 -127 + (-1)을 연산한다면
10000000 (-128)
+ 11111111 (-1)
-----------------
01111111 (+127, 잘못된 결과!)
와 같이 양수와 동일하게 예상과 다른 값이 나오게 된다.
2의 보수법에서 연산을 진행할때는 최댓값(8비트 환경에선 +127)과 최솟값(8비트 환경에선, -128)을 초과하는 연산을 주의 해야 한다
3-2. 비트 크기를 초과하는 값의 연산(변수에 비트 크기를 초과하는 값을 넣지 말아라)
2의 보수법은 정해진 비트 크기(8비트, 16비트, 32비트 등) 내에서만 정상적으로 동작한다.
만약 비트 크기를 초과하는 값이 발생하면 연산 결과가 잘못될 수있다.
8비트에서 200 - 50을 계산한다고 가정해보면
11001000 (200, 8비트에서는 unsigned)
- 00110010 (50)
200은 8비트 부호 있는 정수에서는 표현할 수 없는 값이므로, 올바른 연산이 불가능할 수 있다.
따라서, 부호 있는 연산을 수행할 때는 비트 크기를 반드시 고려해야 한다.
3-3. int vs unsigned int 타입 혼용
C++ 같은 언어에서는 int와 unsigned int를 혼용할 경우 잘못된 결과가 나올 수 있다.
#include <iostream>
int main() {
int a = -1;
unsigned int b = 1;
std::cout << (a < b) << std::endl; // 예상: 1(참)
return 0;
}
위 코드를 실행해보면 우리는 결과가 참으로 나올 것으로 예상하지만
결과는
0 // -1이 1보다 크다고 나옴! (타입 변환 문제)
과 같이 나온다.
이는 부호 있는 정수(int)와 부호 없는 정수(unsigned int)를 비교할 때, 내부적으로 변환이 일어나면서 예상과 다른 결과가 나올 수 있기 때문이다.
구체적인 변환 과정과 왜 다른 결과를 만드는지에 대한 설명
C++에서는 비교 연산(>, <, ==, !=, >=, <=)을 수행할 때, 두 개의 타입이 다르면 내부적으로 변환이 발생하고 이런 변환 규칙을 정수 승격(Integer Promotion) 및 정수 변환 규칙(Usual Arithmetic Conversion) 이라고 한다.
이 정수 승격, 정수 변환 규칙을 따를때 C++에서는 두 피연산자의 타입이 다르면 더 큰 범위를 가지는 타입으로 변환하는데 unsigned int와 int를 비교할 경우, int가 unsigned int로 변환시킨다.
이는 부호가 없는 숫자를 부호가 있는 수로 변환하는 것보다 부호가 있는 수를 부호가 없는 수로 변환하는게 더 안전하기 때문이다.
위에서
int a = -1;
unsigned int b = 1;
이 코드가 있고 이걸 a < b 연산을 하게 되면 a는 unsigned int형으로 변경 되는데 이 변경 과정에서는 2의 보수법을 유지한 채 변환을 하게 된다.
int의 -1을 2의 보수로 변환하면
1111 1111 1111 1111 1111 1111 1111 1111 (-1, 32비트)
이 되고 이를 유지한채로 양의 숫자를 유지한 채로
4,294,967,295 < 1 // false!
와 같은 비교를 하게 되는 것이다.
추가로 부호 없는 정수와 뺄셈 연산 시 내부 변환에 대해서도 보면
#include <iostream>
int main() {
unsigned int x = 0;
int y = -1;
std::cout << "x - y = " << (x - y) << std::endl;
return 0;
}
이 코드를 수핼하면
x - y = 1
이런 결과를 보여준다.
왜 0 - (-1)이 1이 되는 걸까?
이는 위와 동일하게 내부 변환으로 인해 -1이 unsigned int로 변환되었기 때문이다.
y = -1을 unsigned int로 변환하면 -1을 2의 보수 표현을 통해서
1111 1111 1111 1111 1111 1111 1111 1111 (-1, 32비트)
으로 구성되어 있는데 이걸 unsigned int로 해석하면
4,294,967,295
가 되게 되고 x - y는 결국
0 - 4,294,967,295
가 수행되는 것이다.
그러면 아래와 같이 뺄셈을 덧셈으로 변경하게 되고
0 + ( -4,294,967,295 )
-4,294,967,295을 표현하기 위해서 2의 보수법을 또 사용하게 되면
0000 0000 0000 0000 0000 0000 0000 0000 + 1
이 되면서 양의 정수인 1이 되어 버린다
그래서 x - y 의 결과가 1로 나오는 것이다.
이래서 부호가 다른 정수를 비교하기 위해서는 static_cast를 사용하여 unsigned int를 int로 변환해서 사용 하던가
#include <iostream>
int main() {
int a = -1;
unsigned int b = 1;
if (a < static_cast<int>(b)) {
std::cout << "-1 < 1 (올바른 결과)" << std::endl;
} else {
std::cout << "-1 >= 1 (잘못된 결과!)" << std::endl;
}
return 0;
}
C++20 이상의 컴파일러에서는 부호가 다른 정수를 std::cmp_less를 사용하여
#include <iostream>
#include <utility>
int main() {
int a = -1;
unsigned int b = 1;
if (std::cmp_less(a, b)) {
std::cout << "-1 < 1 (올바른 결과)" << std::endl;
} else {
std::cout << "-1 >= 1 (잘못된 결과!)" << std::endl;
}
return 0;
}
부호가 다른 정수끼리 비교할때 문제가 없을 수 있다.
2의 보수법을 사용할 때 unsigned int와 int를 혼용하지 않도록 주의해야 한다.
'Programming Language' 카테고리의 다른 글
부동 소수점 (1) | 2025.02.16 |
---|---|
Backend RoadMap (0) | 2023.10.12 |
방향성 (0) | 2023.10.12 |