이 섹션에서는 비트를 사용하여 정수를 인코딩하는 두 가지 방법을 설명합니다. 하나는 음수가 아닌 수(0과 양수)만 표현하는 방식이고, 다른 하나는 음수, 0, 양수를 모두 표현하는 방식입니다.
C언어는 정수의 유한한 범위를 표현하는 다양한 정수형 데이터 타입을 지원합니다. 각 타입은 char, short, long 같은 키워드로 크기를 지정할 수 있으며, unsigned 키워드를 통해 음수가 없는 수만 표현할지, 아니면 기본값인 부호 있는 수로 사용할지 결정할 수 있습니다.
C언어 자료형의 실제 크기와 표현 범위는 프로그램이 32비트용인지 64비트용인지에 따라 달라질 수 있습니다.
long 타입: 32비트 프로그램에서는 보통 4바이트, 64비트 프로그램에서는 8바이트를 사용합니다. 이 때문에 64비트 프로그램의 long 타입이 훨씬 더 넓은 범위의 수를 표현할 수 있습니다.char * 등): 32비트에서는 4바이트, 64비트에서는 8바이트입니다. (이전 섹션 내용)int 타입: 64비트 프로그램에서도 보통 32비트와의 호환성을 위해 4바이트를 유지합니다.char, short, int, long과 같은 부호 있는(signed) 정수형의 표현 범위를 보면, 음수의 범위가 양수의 범위보다 1만큼 더 넓습니다. (예: char는 -128 ~ +127) 이 이유는 음수를 표현하는 방식 때문이며, 이후 섹션에서 자세히 다뤄집니다.

C언어 표준은 각 데이터 타입이 '최소한 이 범위는 보장해야 한다'고 정의합니다. 실제 대부분의 컴퓨터는 이 최소 보장 범위보다 더 넓은 범위를 지원합니다.
int는 최소 -32,767 ~ +32,767)int가 2바이트로 구현될 수도 있음을 허용하는데, 이는 16비트 머신 시절의 흔적입니다.long이 4바이트로 구현될 수 있음을 허용하며, 32비트 프로그램에서는 실제로 그렇게 구현됩니다.
결론적으로, 프로그래머는 int나 long 같은 기본 자료형의 크기가 컴파일 환경에 따라 변할 수 있다는 점을 인지해야 하며, 크기에 의존하지 않는 프로그램을 작성하거나 int32_t, int64_t 같은 고정 크기 자료형을 사용하여 이식성을 높이는 것이 좋습니다.
w비트 정수 데이터 타입을 생각해 봅시다. 비트 벡터 를 [$x_{w-1}$, $x_{w-2}$, ..., $x_0$] 와 같이 각 비트로 표현할 때, 이를 이진법 표기법으로 취급하면 부호 없는(unsigned) 정수 값을 얻을 수 있습니다.
w비트로 이루어진 비트 벡터 를 부호 없는 정수로 변환하는 함수 (Binary to Unsigned)는 다음과 같이 정의됩니다.

이 수식은 각 비트 위치 i의 비트 값(, 0 또는 1)에 그 자릿값()을 곱한 것을 모두 더한다는 의미입니다. 즉, 우리가 흔히 아는 2진수를 10진수로 변환하는 방법과 같습니다.
B2U₄([0001]) = 0⋅8+0⋅4+0⋅2+1⋅1=1B2U₄([0101]) = 0⋅8+1⋅4+0⋅2+1⋅1=5B2U₄([1011]) = 1⋅8+0⋅4+1⋅2+1⋅1=11B2U₄([1111]) = 1⋅8+1⋅4+1⋅2+1⋅1=15w비트를 사용한 부호 없는 인코딩으로 표현할 수 있는 값의 범위는 다음과 같습니다.
[00...0]이며, 정수 값은 0입니다.[11...1]이며, 정수 값 는 입니다.부호 없는 이진 표현 방식의 중요한 특징은 0부터 까지의 모든 정수가 각각 고유한 w비트 인코딩을 가진다는 점입니다. 즉, 하나의 숫자를 표현하는 비트 패턴은 단 하나뿐이며, 그 반대도 마찬가지입니다.
이를 수학적으로 '전단사 함수(bijection)'라고 합니다. 이는 정수를 비트 벡터로, 또는 비트 벡터를 정수로 언제든지 유일하게 상호 변환할 수 있음을 의미합니다.
음수를 포함한 정수를 표현하기 위해 컴퓨터에서 가장 널리 사용되는 방식은 2의 보수(two's-complement) 형태입니다. 이 방식은 최상위 비트(MSB)를 음수의 가중치로 해석하는 것이 핵심입니다.
w비트 벡터 를 2의 보수 정수로 변환하는 함수 (Binary to Two's-complement)는 다음과 같이 정의됩니다.

B2T₄([0101]) = −0⋅8+1⋅4+0⋅2+1⋅1=5B2T₄([1011]) = −1⋅8+0⋅4+1⋅2+1⋅1=−8+3=−5B2T₄([1111]) = −1⋅8+1⋅4+1⋅2+1⋅1=−8+7=−1w비트 2의 보수 숫자로 표현할 수 있는 값의 범위는 다음과 같습니다.
[10...0]입니다. 값은 입니다.[01...1]입니다. 값은 입니다.|TMin| = TMax + 1). 이는 0이 음수가 아닌 수에 포함되기 때문에 발생하며, 미묘한 프로그램 버그의 원인이 될 수 있습니다.[11...1]로 표현됩니다. 이는 부호 없는 수의 최대값(UMax)과 비트 패턴이 동일합니다.[00...0]으로 표현됩니다.이 원칙은 w비트로 표현 가능한 2의 보수 정수 범위 내의 모든 숫자는 각각 고유한 비트 패턴을 가진다는 것을 의미합니다. 즉, 하나의 숫자를 표현하는 비트 패턴은 단 하나뿐이며, 그 반대도 마찬가지입니다.
'전단사'란 두 집합 사이의 완벽한 '일대일 대응' 관계를 말합니다.
TMin부터 TMax까지)이 두 집합은 원소의 개수가 개로 정확히 같으며, 서로 하나씩 빠짐없이, 겹치지 않게 짝을 이룹니다. 이 덕분에 데이터 변환 시 모호함이나 손실이 전혀 없습니다.
$B2T_w$ 함수가 완벽한 일대일 대응 관계(전단사)이므로, 그 관계를 거꾸로 되돌리는 역함수가 반드시 존재합니다. 이 역함수를 (Two's-complement to Binary)라고 정의합니다.
[1111] → 1)1 → [1111])C언어는 서로 다른 숫자 자료형 간의 형 변환(casting)을 허용합니다. 부호 있는 int를 unsigned로 바꾸거나, 그 반대의 경우 어떤 일이 일어날까요?
C언어는 수학적인 관점(음수는 0으로 변환)이 아니라, 비트 수준(bit-level)의 관점에서 형 변환을 처리합니다.
같은 워드 크기를 가진 부호 있는(signed) 정수와 부호 없는(unsigned) 정수 사이의 형 변환에서, 숫자 값은 바뀔 수 있지만 기저의 비트 패턴은 절대 변하지 않습니다.
short int v = -12345;
unsigned short uv = (unsigned short) v;
// 출력: v = -12345, uv = 53191
short 타입 -12345의 16비트 2의 보수 표현은 [1100111111000111] (0xCFC7)입니다. 이 비트 패턴을 부호 없는 정수로 해석하면 숫자 53191이 됩니다. 형 변환은 이 해석 방식만 바꾼 것입니다.
unsigned u = 4294967295u; // 32비트 부호 없는 정수의 최댓값
int tu = (int) u;
// 출력: u = 4294967295, tu = -1
32비트 부호 없는 정수 4294967295의 비트 표현은 모든 비트가 1인 [11...1]입니다. 이 비트 패턴을 부호 있는 정수(2의 보수)로 해석하면 숫자 -1이 됩니다.
이러한 변환 관계는 수학적으로 다음과 같이 표현할 수 있습니다.
2의 보수 값 x를 부호 없는 값으로 변환할 때:
음수 는 를 더한 큰 양수가 됩니다. (예: -1 → -1 + $2^w$ = $UMax_w$)
양수와 0은 그대로 유지됩니다.
부호 없는 값 를 2의 보수 값으로 변환할 때:
(표현 가능한 최대 양수)보다 작거나 같은 수는 그대로 유지됩니다.
보다 큰 수는 를 뺀 음수가 됩니다.
결론적으로 C언어에서 부호 있는 타입과 없는 타입 간의 형 변환은 메모리에 있는 0과 1의 비트 패턴을 그대로 둔 채, 그 비트를 해석하는 숫자 체계(부호 없음 vs. 2의 보수)만 바꾸는 방식으로 동작합니다. 이 때문에 예상치 못한 큰 양수가 되거나 음수가 될 수 있어 주의가 필요합니다.
C언어는 모든 정수 자료형에 대해 부호 있는 연산과 부호 없는 연산을 모두 지원합니다. 별다른 지정이 없으면 모든 정수는 기본적으로 부호 있는(signed) 것으로 간주됩니다. 12345U처럼 숫자 뒤에 U나 u를 붙이면 부호 없는(unsigned) 상수가 됩니다.
C언어에서는 부호 있는 값과 없는 값 사이의 변환이 가능합니다. 대부분의 시스템은 이 변환을 할 때, 기저의 비트 패턴은 그대로 유지하고 해석 방식만 바꾸는 규칙을 따릅니다.
이러한 형 변환은 두 가지 경우에 발생합니다.
(unsigned)나 (int)처럼 직접 타입을 지정하는 경우입니다.int tx;
unsigned ux;
tx = (int) ux;
int ty;
unsigned uy;
uy = ty; // ty(signed)가 uy(unsigned)에 할당되면서 암묵적으로 unsigned로 변환됨`
printf 함수는 %d(signed), %u(unsigned), %x(hex) 지시자를 통해 값을 출력하며, 변수의 실제 타입과 상관없이 지정된 지시자에 따라 비트 패턴을 해석합니다.
C언어에서 부호 있는 값과 부호 없는 값을 함께 사용하는 연산이 수행될 때, 프로그래머가 예상치 못한 결과가 발생할 수 있습니다.
규칙: 연산에 참여하는 두 값 중 하나라도 unsigned이면, C는 나머지 signed 값을 강제로 unsigned로 변환한 뒤 연산을 수행합니다.
이 규칙은 덧셈이나 곱셈 같은 산술 연산에서는 큰 문제가 없지만, <나 > 같은 관계 연산자(비교 연산자)에서는 직관에 어긋나는 결과를 만듭니다.
1 < 0U1은 0보다 작으므로 참(True)일 것이다.0U가 unsigned이므로, 1을 unsigned int로 암묵적 형 변환합니다.1의 비트 패턴(111...1)을 unsigned로 해석하면 매우 큰 양수(4,294,967,295)가 됩니다.4294967295U < 0U를 비교하게 됩니다.이처럼 부호 있는 값과 없는 값을 비교할 때는 음수가 의도치 않게 거대한 양수로 변환되어 비교 결과가 뒤바뀔 수 있으므로 각별한 주의가 필요합니다.
작은 데이터 타입의 정수를 큰 데이터 타입으로 변환하는 것은 값의 손실 없이 항상 가능해야 합니다. 이 과정은 부호 없는 수와 부호 있는 수에 따라 서로 다른 규칙을 따릅니다.
부호 없는(unsigned) 숫자를 더 큰 데이터 타입으로 변환할 때는, 비어있는 상위 비트들을 단순히 0으로 채웁니다. 이를 '0 확장'이라고 합니다.
w비트 패턴 [uw-1, ..., u0]을 더 큰 w'비트로 확장하면 [0, ..., 0, uw-1, ..., u0]이 됩니다.2의 보수(signed) 숫자를 더 큰 데이터 타입으로 변환할 때는, 원래 숫자의 최상위 비트(부호 비트)를 복사하여 비어있는 상위 비트들을 채웁니다. 이를 '부호 확장'이라고 합니다.
w비트 패턴 [xw-1, ..., x0]을 더 큰 w'비트로 확장하면 [xw-1, ..., xw-1, xw-1, ..., x0]이 됩니다.short를 int로 확장하기16비트 short를 32비트 int로 확장하는 예시를 통해 두 규칙의 차이를 명확히 볼 수 있습니다.
short sx = -12345; → 16진수 CFC7unsigned short usx = 53191; → 16진수 CFC7int x = sx; (signed 확장)sx의 부호 비트(C의 첫 비트, 즉 1)가 앞 16비트에 복사됩니다.FFFFCFC7 (값은 여전히 -12345)unsigned ux = usx; (unsigned 확장)usx는 부호가 없으므로 앞 16비트가 0으로 채워집니다.0000CFC7 (값은 여전히 53191)C언어에서 크기와 부호가 동시에 변하는 형 변환이 일어날 때, 크기 변경이 먼저, 그 다음 부호 변경이 일어납니다.
short sx = -12345;
unsigned int uy = sx; // short -> unsigned int
이 코드는 다음과 같이 두 단계를 거쳐 실행됩니다.
sx(-12345)가 먼저 signed int로 확장됩니다. 부호 확장이 일어나 32비트 값 FFFFCFC7 (-12345)가 됩니다.FFFFCFC7의 해석 방식만 unsigned로 바뀝니다. 따라서 uy는 매우 큰 양수인 4,294,954,951이 됩니다.만약 부호 변경 후 크기 변경이 일어났다면((unsigned short)로 먼저 변환), 결과는 53191이 되었을 것입니다. 하지만 C 표준은 크기 변경을 우선하도록 규정하고 있습니다.
숫자를 더 큰 데이터 타입으로 확장하는 것과 반대로, 더 작은 데이터 타입으로 변환하여 비트의 수를 줄이는 경우가 있습니다. 이를 '잘라내기(Truncation)'라고 합니다.
예를 들어, 32비트 int를 16비트 short로 형 변환하면 상위 16개의 비트는 버려지고 하위 16개의 비트만 남게 됩니다.
int x = 53191; // 32비트: 0000 0000 0000 0000 1100 1111 1100 0111
short sx = (short) x; // 상위 16비트를 잘라냄. sx = -12345
// 16비트: 1100 1111 1100 0111
int y = sx; // 부호 확장으로 다시 32비트 int로 변환. y = -12345
// 32비트: 1111 1111 1111 1111 1100 1111 1100 0111
이처럼 숫자를 잘라내면 원래의 값이 바뀔 수 있는데, 이는 일종의 오버플로우입니다.
w비트의 부호 없는 정수 x를 k비트로 잘라낸 결과 는 원래 값 x에 2k로 나눈 나머지와 같습니다.
w비트의 2의 보수 정수 x를 k비트로 잘라낸 결과 는, 먼저 x를 2k로 나눈 나머지를 구한 뒤, 그 결과를 k비트 2의 보수로 해석한 값과 같습니다.
int x = 53191을 short(16비트)로 잘라낼 때53191
53191의 16비트 패턴은 11001111 11000111 입니다.
이 비트 패턴을 16비트 2의 보수로 해석(U2T₁₆)하면 12,345가 됩니다.
결론적으로, 숫자를 더 작은 비트 크기로 잘라낼 때는 상위 비트들이 버려지면서 원래의 값과 전혀 다른 값이 될 수 있으며, 그 결과는 나머지(modulo) 연산으로 예측할 수 있습니다.
앞서 보았듯이, C언어에서 부호 있는 수가 부호 없는 수로 암묵적으로 형 변환될 때 직관에 어긋나는 동작이 발생할 수 있습니다. 이러한 동작은 버그의 원인이 되기 쉬우며, 특히 코드에 명확한 표시 없이 일어나기 때문에 프로그래머가 놓치기 쉽습니다.
unsigned 사용을 피해야 하는 이유이러한 미묘한 버그를 피하는 한 가지 방법은 부호 없는(unsigned) 숫자를 아예 사용하지 않는 것입니다.
실제로 C/C++를 제외한 대부분의 프로그래밍 언어는 부호 없는 정수를 지원하지 않습니다. 다른 언어 설계자들은 부호 없는 정수가 유용함보다는 문제를 더 많이 일으킨다고 판단한 것입니다. 예를 들어, Java는 부호 있는 정수만 지원하며, 2의 보수 방식으로 구현하도록 명시하고 있습니다.
unsigned가 유용한 경우그럼에도 불구하고, 부호 없는 값은 다음과 같은 특정 상황에서 매우 유용합니다.
unsigned 타입을 유용하게 사용합니다.우선순위 큐를 구현하는 방법은 많지만 대표적으로 힙으로 구현할 수 있다.
힙은 완전 이진 트리이며 노드의 값이 부모 >= 자식 조건이 충족해야한다.
마지막 노드에 넣고 위 조건을 만족하도록 노드를 스왑해준다. 이를 업 힙이라고 하며 시간복잡도는 O(logN)이 된다.
루트 노드를 없애고 마지막 노드를 루트노드로 대체한다. 이후 다운 힙을 적용하며 힙 조건을 만족시키도록 한다. 이 역시 시간복잡도는 O(logN)이 된다.
따라서 힙으로 구현된 우선순위 큐에 경우 입력, 출력 모두 O(logN)이 된다는 것을 알 수 있다.