이 글에서는 Signed Binary와 Unsigned Binary의 차이점, 각각의 표현법, 그리고 C언어 프로그래밍 시의 주의사항을 알아보겠습니다.

사용자 관점에서 컴퓨터가 다루는 데이터 유형은 크게 문자(Character) 혹은 숫자(Number), 2가지로 생각할 수 있습니다. 하지만, 컴퓨터 관점에서는 문자 또한 결국 숫자로 처리되므로 오늘은 숫자 데이터 유형, 그 중에서도 정수형을 컴퓨터에서 다루는 방법을 살펴볼 예정입니다.

wiki-ascii-table
문자를 숫자로 표현하기 위한 ASCII 테이블이 존재

정수의 개념

컴퓨터에서 다루는 숫자는 크게 정수(Integer)와 실수(Real Number)로 나누어 볼 수 있습니다. 수학의 실수체계에서는 각각의 특성에 따라 아래와 같이 분류하고 있습니다.

정수와 실수

classification-of-real-number
실수범위에 포함되는 정수의 정의. 양수,음수 그리고 0이 될 수 있다.

컴퓨터에서 정수의 의미

모든 데이터는 2진수(Binary)형태로 컴퓨터 기억공간에 저장됩니다. 다시 말해, 정수 데이터가 컴퓨터 메모리에 저장된다는 것은 메모리의 특정 공간을 차지한다는 것 입니다. 우리가 일상적으로 사용하는 정수는 10진수 표현법으로 하나의 자릿수(예: 10의 자리, 100의 자리 등)에 0부터 9까지, 총 10개의 숫자를 표현할 수 있습니다. (그래서 10진수입니다.)

기억공간?
기억공간이라는 용어를 사용한 이유는 꼭 데이터가 메모리(Memory)에만 저장되지는 않기 때문 입니다. 가령 CPU 레지스터(Register)에도 데이터가 저장됩니다.
편의를 위해 본 포스팅에서는 메모리라는 용어와 혼용하여 사용합니다.

컴퓨터는 모두가 알다시피 0과 1로 이루어진 데이터를 처리합니다. 따라서, 숫자를 포함한 모든 데이터는 0과 1로 표현되어야 하기 때문에 2진수 표현법이 필요합니다. 하지만 이는, 표현방법의 차이일 뿐, 의미가 변하는 것은 아닙니다. 예를 들어 ‘5’라는 숫자는 10진수 표현이 5일 뿐, 2진수 표현으로는 ‘101’입니다.

앞선 정수의 개념에서, 정수는 양수와 음수, 그리고 0으로 구성된다고 정의했습니다. 컴퓨터에서는 양수와 음수를 나타내기 위한 방법으로 부호 있는 정수(Signed Integer)부호 없는 정수(Unsigned Integer) 로 나누어 표현합니다.

부호 있는 정수라는 것은 +기호와 -기호를 포함하여 컴퓨터 메모리에 저장한다는 의미이고, 부호 없는 정수라는 것은 항상 양의 방향(+)을 가정하고 정수의 크기만을 컴퓨터 메모리에 저장한다는 의미입니다. 이것은 컴퓨터 시스템에서 매우 중요한 의미를 가지는데, 동일한 2진 데이터라고 하더라도 해당 데이터가 어떤 표현법을 나타내는냐에 따라 실제로 의미하는 값이 다르기 때문입니다.

Unsigned Binary: 개념과 변환 방법

Unsigned Binary는 부호 없는 정수를 2진수로 표현한 데이터 입니다. 부호가 없는 경우, 부호를 나타낼 별도의 비트가 필요하지 않기 때문에 수학적인 2진수 $\leftrightarrow$ 10진수 변환을 그대로 적용할 수 있습니다.

자연수(Natural Number)

부호 없는 정수는 자연수와 동일합니다.

개념

예를 들어, 4비트 공간에서 부호 없는 정수의 2진수/10진수/16진수 표현은 아래와 같습니다.

4비트 unsigned binary

2진수(bin) 10진수(dec) 16진수(hex)
0000 0 0x0
0001 1 0x1
0010 2 0x2
0011 3 0x3
0100 4 0x4
0101 5 0x5
0110 6 0x6
0111 7 0x7
1000 8 0x8
1001 9 0x9
1010 10 0xA
1011 11 0xB
1100 12 0xC
1101 13 0xD
1110 14 0xE
1111 15 0xF

즉, 4비트 공간에서 부호 없는 정수의 10진수 정수의 표현 범위는 $0 \leqq \Z \leqq 2^4-1$ 이고, 16진수로는 0x0부터 0xF까지 입니다.

비트 크기 별 16진수 표현

컴퓨터 시스템에서 숫자를 표현하기 위해 16진수 또한 굉장히 많이 사용됩니다. 따라서 비트 공간의 크기에 따라 최대 표현 가능한 16진수를 알고 있어야 합니다.
4비트 = 0xF
8비트 = 0xFF
16비트 = 0xFFFF
32비트 = 0xFFFF_FFFF

변환

10진수를 2진수로 변환

2진수로 변환하기 위해서는 숫자가 구성되는 원리에 의해 나누기 연산(/)과 나머지 연산(%)을 사용할 수 있습니다.

나누기(/) 나머지(%)
45 / 2 = 22 45 % 2 = 1
22 / 2 = 11 22 % 2 = 0
11 / 2 = 5 11 % 2 = 1
5 / 2 = 2 5 % 2 = 1
2 / 2 = 1 2 % 2 = 0
1 / 2 = 0 1 % 2 = 1
\[\begin{aligned} 45_{10} &= 22 \times 2 + 1 \newline &= (11 \times 2 + 0) \times 2 + 1 \newline &= ((5 \times 2 + 1) \times 2 + 0) \times 2 + 1 \newline &= (((2 \times 2 + 1) \times 2 + 1) \times 2 + 0) \times 2 + 1 \newline &= ((((1 \times 2 + 0) \times 2 + 1) \times 2 + 1) \times 2 + 0) \times 2 + 1 \newline &= (((((0 \times 2 + 1) \times 2 + 0) \times 2 + 1) \times 2 + 1) \times 2 + 0) \times 2 + 1 \newline &= 1 \times 2^5 + 0 \times 2^4 + 1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \newline &= 101101_2 \end{aligned}\]

2진수를 10진수로 변환

2진수 역시도 10진수의 표현원리와 동일하게 각 자릿수를 2의 거듭제곱으로 표현합니다.

\[\begin{aligned} 101_2 &= 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 \newline &= 1 \times 4 + 0 \times 2 + 1 \times 1 \newline &= 5_{10} \end{aligned}\]

파이썬을 활용한 변환

사실 가장 간단한 방법은 일일히 계산하는 것보다 파이썬 REPL을 사용하는 것 입니다.
단, 유의할 점은 bin이나 hex를 사용할 경우 반환값은 정수가 아닌 문자열입니다.

>>> bin(1)
'0b1'
>>> bin(2)
'0b10'
>>> int(0b11)
3
>>> int(0b110)
6
>>> hex(0b1)
'0x1'
>>> hex(1)
'0x1'
>>> int(0xf)
15

Signed Binary: 표현 방법과 파이썬 활용

Signed Binary는 부호 있는 정수를 2진수로 표현한 데이터 입니다. Unsigned Binary와 다르게 부호를 나타내기 위한 비트 패턴이 필요합니다. 아래에 3가지 방법을 소개하겠지만, 현대 컴퓨터에서는 주로 2의 보수 표현을 사용합니다.

표현 방법

기본적인 음수 표현의 원리는 부호를 나타내기 위한 1비트(0 혹은 1)를 할당하는 것 입니다.

  • 0 : 양수 (Positive)
  • 1 : 음수 (Negative)

방법1: 부호-크기 (Sign-Magnitude)

부호-크기 방법에서는 단순히 최상위 비트(MSB: Most Significant Bit)에 부호 비트를 추가합니다.

예를 들어, 8비트 공간에서 양의 부호를 가진 정수 5와 음의 부호를 가진 정수 -5를 2진수로 표현하면 아래와 같습니다.

+5 = 0000 0101
-5 = 1000 0101

0101이 숫자 5를 나타내고, 최상위 비트에 각각 0(양의 부호, +)과 1(음의 부호, -)을 사용했습니다.
동일한 방식으로, 8비트 공간에서 부호 비트를 추가하는 방법을 사용할 때 나타낼 수 있는 수는 아래와 같이 정리할 수 있습니다.

2진수 10진수
0000 0000 +0
0000 0001 +1
0000 0010 +2
0111 1111 +127
1000 0000 -0
1000 0001 -1
1111 1111 -127

따라서 표현할 수 있는 숫자는, +0부터 +127까지 127개와 -0부터 -127까지 127개, 총 254개의 숫자를 표현할 수 있습니다.

방법2: 1의 보수 (One’s Complement)

1의 보수법은 부호 없는 정수의 2진 데이터 형태를 모든 거꾸로 뒤집는 것 입니다. 2진수는 0 혹은 1이라는 두개의 값만 가질 수 있고 거꾸로 뒤집는 다는 것은 0 은 1로, 1은 0으로 바꾸는 것 입니다.

비트 뒤집기

0 -> 1
1 -> 0

1의 보수 표현에 의한 8비트 $\pm$ 5

+5 = 0000 0101
-5 = 1111 1010

1의 보수 표현법에서 8비트 정수는 아래와 같이 정리할 수 있습니다.

2진수 10진수
0000 0000 +0
0000 0001 +1
0111 1110 +126
0111 1111 +127
1000 0000 -127
1000 0001 -126
1111 1101 -2
1111 1110 -1
1111 1111 -0

방법3: 2의 보수 (Tow’s Complement)

2의 보수법은 1의 보수법과 같이 모든 비트를 거꾸로 만들고 1비트의 1을 더하는 방식으로 표현됩니다. 앞선 2가지 방법의 단점을 보완하기 때문에 현재 대부분의 컴퓨터에서 음수를 표현하기 위한 방법입니다.

음의 정수를 2의 보수법에 의한 2진수로 표현하기 위해서는 다음의 순서를 따릅니다.

  1. 부호 없는 정수의 1의 보수를 구한다.
  2. LSB에 1을 더한다.

2의 보수 표현에 의한 8비트 $\pm$ 5

+5 = 0000 0101
-5 = 1111 1010 (1의 보수 표현)
   +         1
   -----------
   = 1111 1011
2진수 10진수 변환(양수 표현 > 1의 보수 > +1)
0000 0000 +0  
0000 0001 +1  
0000 0010 +2  
 
0111 1110 +126  
0111 1111 +127  
1000 0000 -128 1000 0000 > 0111 1111 > 1000 0000
1000 0001 -127 0111 1111 > 1000 0000 > 1000 0001
1000 0010 -126 0111 1110 > 1000 0001 > 1000 0010
 
1111 1101 -3 0000 0100 > 1111 1011 > 1111 1101
1111 1110 -2 0000 0010 > 1111 1101 > 1111 1110
1111 1111 -1 0000 0001 > 1111 1110 > 1111 1111

파이썬을 활용한 2의 보수 <-> 10진수 변환

>>> int("-128", 10).to_bytes(1, byteorder="little", signed=True)
b'\x80'

>>> int("-3", 10).to_bytes(1, byteorder="little", signed=True)
b'\xfd'
>>> bin(0xfd)
'0b11111101'

Signed vs Unsigned 비교

지금까지 설명드린 부호 있는 정수와 부호 없는 정수를 비교 정리하면 아래와 같습니다.

구분 부호 있음(Signed) 부호 없음(Unsigned)
값의 범위 음수,0,양수 0,양수
범위(8비트) -128 ~ 127 0~255
사용사례 센서,오프셋 등 메모리 주소, 카운터/타이머 등
부호비트 존재(MSB) 없음

2의 보수 표현에 따른 부호 있는 정수와 부호 없는 정수의 표현 가능한 범위는 아래와 같이 수식으로 정리할 수 있습니다.

부호 없는 정수(Unsigned Integer) \(0 \leqq \Z \leqq 2^{비트수}-1\)

부호 있는 정수(Signed Integer) \(-2^{비트수-1} \leqq \Z \leqq 2^{비트수-1} - 1\)

프로그래밍 주의사항

지금까지 살펴본 것 처럼 부호의 여부에 따라 컴퓨터가 해석하는 방법이 달라지기 때문에, 그 특성을 이해하고 프로그램을 작성할 때도 이점을 유의해야 합니다. 주로 사용하는 C언어에서 2가지 예제를 살펴보면서 조심해야할 부분을 확인해보겠습니다.

예제1: signed int 와 unsigned int의 비교

실수하기 쉬운 첫번째는 서로 다른 부호 유형간의 비교입니다. 아래의 예제에서는 signed형 정수인 -1이 unsigned형으로 변환 해석 되어 1보다 큰 값으로 처리됩니다.

comp.c

#include <stdio.h>

void main(void)
{
  int a = -1;
  unsigned b = 1;

  if (a < b) {
    printf("a is less than b\n");
  } else {
    printf("a is greater than or equals to b\n");
  }
}

실행 결과

$ gcc comp.c && ./a.out
a is greater than or equals to b

우리가 직관적으로 생각하기에는 -11보다 작으므로 “a is less than b”가 출력되야할 것으로 보입니다.
하지만 비교연산을 할 때, 부호 있는 정수인 **a가 부호 없는 정수(unsigned)로 변환되기 때문에 b보다 큰 값으로 해석**됩니다.

int a = -1의 비트패턴
a = -1 = 0xFFFFFFFF (-1의 2의 보수 표현) = 4,294,967,295

위와 같은 예제는 컴파일 시에 -W 옵션을 사용해서 예방할 수 있습니다.

$ gcc -W comp.c
comp.c: In function ‘main’:
comp.c:8:9: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare]
    8 |   if (a < b) {
      |         ^

예제2: 형 변환 (Type Casting)

다른 예제는 흔히 사용하는 형 변환 실수입니다. int형과 unsigned int형과의 상호 형 변환시에는 해당 변수가 나타내는 수의 의미가 달라지는 것을 알고 있어야 합니다.

main.c

#include <stdio.h>
#include <stdint.h>

void main(void)
{
  int8_t i8 = -5;  // 1111 1011
  uint8_t u8 = 5;  // 0000 0101
  uint8_t ui8 = (uint8_t)i8;  // 1111 1011

  printf("UINT8_MAX: %d\n", UINT8_MAX);
  printf("INT8_MAX: %d\n", INT8_MAX);

  printf("0x%02x [%d]\n", i8, i8);
  printf("0x%02x [%d]\n", u8, u8);
  printf("0x%02x [%d]\n", ui8, ui8);
}

실행 결과

$ gcc main.c && ./a.out
UINT8_MAX: 255
INT8_MAX: 127
0xfffffffb [-5]     # (1)
0x05 [5]            # (2)
0xfb [251]          # (3)

첫번째, int8_t형은 메모리 공간에서 8비트를 차지하는 변수입니다. 하지만 실제 변수가 가진 값이 int형보다 작은 메모리 공간을 차지하는 경우, printf문에서 포맷팅을 할때는 int형으로 승격되기 때문에 (1)의 결과는 0xFB가 아닌 0xFFFFFFFB가 되는 것 입니다.

부호 있는 정수 -5는 2진수로 표현하면 $11111011_2$ 이고 16진수로는 0xFB입니다.

부호 확장(Sign Extension)
8비트 정수를 32비트 정수로 승격할 때, 부호 확장이란 것이 발생합니다.
부호 확장은 말 그대로 정수의 부호 비트를 확장된 메모리 크기에 맞도록 확장시키는 것 입니다.
위의 예시의 경우, 부호 비트가 음수를 나타내는 1이므로 최상위 비트(MSB)의 1을 나머지 상위비트에 채웁니다.

두번째, 변수 ui8은 부호 있는 정수형 변수 i8을 부호 없는 정수형으로 형변환을 하여 할당했습니다. 하지만, 출력 결과 (3)에서 볼 수 있듯이 실제 메모리 패턴(0xFB)는 변하지 않고, 0xFB의 부호 없는 정수값, 251을 출력합니다.

C언어의 형변환은 컴퓨터에 실제 저장되는 값을 변경하는 것이 아니라, 컴퓨터가 어떻게 해석할지를 알려주는 것 입니다.(형변환 뿐 아니라 타입시스템 자체가 그렇다고 봐야합니다.)

결론 (Conclusion)

이번 글에서는 Signed와 Unsigned Binary의 차이점, 표현 방식, 프로그래밍 주의사항을 살펴보았습니다.

  • Signed Binary: 음수와 양수를 모두 표현 (예: 센서 데이터, 오프셋)
  • Unsigned Binary: 양수만 표현 (예: 메모리 주소, 타이머)

이번 글이 도움이 되었나요? 궁금한 점이나 더 알고 싶은 부분이 있다면 댓글로 남겨주세요!