C 프로그래밍 입문/정수형 데이터
정수형 데이터
+/-C 에서 가장 많이 다루는 데이터 형태는 정수형이고, 그만큼 데이터 타입도 다양하다. 먼저 C 정수형 데이터의 기본 데이터를 확인해 보도록 하자:
타입 | 바이트수 | 최소값/최대값 | limits.h 상수 | 접미사 | 비고 |
---|---|---|---|---|---|
(signed) short (int) unsigned short (int) |
2 | -32768 - 32767 0 - 65535 |
SHRT_MIN, SHRT_MAX USHRT_MAX |
||
(signed) int unsigned (int) |
4* | -2147483648 - 2147483647 0 - 4294967295 |
INT_MIN, INT_MAX UINT_MAX |
U |
|
(signed) long (int) unsigned long (int) |
4** | -2147483648 - 2147483647 0 - 4294967295 |
LONG_MIN, LONG_MAX ULONG_MAX |
L UL |
|
(signed) long long (int) unsigned long long (int) |
8 | -9223372036854775808 - 9223372036854775807 0 - 18446744073709551615 |
LONG_LONG_MIN, LONG_LONG_MAX ULONG_LONG_MAX |
LL ULL |
*, **: 이 크기는 컴파일러에 따라 달라집니다. 이 아래에 자세한 내용을 설명하였습니다.
표의 타입에 괄호가 쳐진 것은 원래 있어야 하는 내용이지만 생략이 가능하고 보통은 생략하여 사용한다는 뜻 입니다. 예를 들어 unsinged int 타입은 그냥 unsinged 로 사용할 수 있습니다.
정수 타입의 데이터는 많은 변화를 거쳐왔기 때문에 약간의 혼동의 여지가 있다. 먼저 int 타입이 있는데, 이 int 타입은 가장 효율적으로 처리될 수 있는 정수타입이라는 의미이다. 문제는 효율적으로 처리라는 문구에서 시작되는데, 이 이야기를 하기 전에 먼저 해야할 이야기가 하나 있다.
위에서 설명했듯이 컴퓨터를 분류하는 방법 중에는 n 비트 컴퓨터 - 8비트 컴퓨터, 16비트 컴퓨터 등등 - 라는 표현이 있고, 이 n 비트 컴퓨터라는 말의 기본적인 정의는 n 비트 단위로 데이터를 처리하는 컴퓨터라는 뜻이다. 다른 표현을 사용 한다면 8비트 컴퓨터는 데이터를 처리할 때 8비트씩 읽어들여서 처리 하고, 16비트 컴퓨터는 16비트씩 읽어서 처리 한다는 뜻이다. 의미 상으로는 16비트 데이터를 처리할 때 8비트 컴퓨터는 두 번 일을 해야 하고, 16비트 컴퓨터는 한 번만 해도 된다는 의미로, 수치적으로는 16비트 컴퓨터의 속도가 8비트 컴퓨터의 두 배 빠른 컴퓨터 라는 의미 이다[1].
다시 int 타입에 대해서 이야기 하자면, int 타입은 '가장 효율적으로 처리될 수 있는 정수 타입'이기 때문에 16비트 컴퓨터에서는 16비트, 그러니까 2바이트일 때 가장 효율적이므로 int 타입의 크기는 2바이트 이다. 그리고 32 비트 컴퓨터 일 땐 32비트 즉, 4 바이트일 때 가장 효율적으로 32비트 컴퓨터에서는 int 타입의 크기는 4 바이트가 된다. 그래서 8 비트 컴퓨터나 16비트 컴퓨터의 int 타입은 short int 와 동일한 데이터 타입이 되고, 32비트 컴퓨터의 int 타입은 long int와 동일한 데이터 타입이 된다.
문제는 64비트 컴퓨터가 대두 되었을 때 발생 하였는데, int 타입을 64비트로 잡아주면 long int가 int 보다 다룰 수 있는 데이터의 크기가 적어지는 모순이 생기게 되었다. 그래서 64비트 컴퓨터에서는 int를 32비트로 쓰고 long int를 64비트를 쓰는 경우와, int와 long int 모두를 32비트 컴퓨터와 동일하게 32비트를 쓰고, long long int 라는 새로운 64비트 타입을 도입한 경우가 있다.
일단 위의 표는 C99에서 제시한 표준에 따라 작성되었으며, 상황에 따라서는 위의 표와 다른 경우가 있을 수 도 있다는 것을 알려두기 위해서 위의 내용을 기술하였다.
정수형 상수는 기본적으로 int 타입으로 간주되며 short 타입을 강제하는 접미사는 없다. unsigned 타입의 상수임을 명기 하기 위해서는 u나 U를 사용하면 된다. 예를 들어 그냥 '1'이라고 상수를 사용하면 signed int 타입이 되지만, '1U'혹은 '1u'라 표기하면 unsigned int 타입의 상수가 되고, '1ul' 혹은 '1UL'이라 표기하면 unsigned long int 타입의 상수로 다루어 진다.
정수형 상수를 표기할때 사용할 수 있는 진법은 8진법, 10진법, 16진법이다. 8진법 상수를 표기 할때는 0을 시작으로 8진 숫자를 사용한다. 예를들어 8진수 72를 표기하기위해서는 '072'라 표기해 주면 된다. 16진법을 표기하기 위해 사용되는 접두어는 0x 혹은 0X이고, 16진 숫자를 쓰면 된다. 예를 들어 16진수 ae4f를 표기하기 위해서는 '0xae4f'라 쓰면 된다. 8진수나 16진수 상수를 사용하는 경우에도 10진수에서 사용하는 타입지정 접미사는 그대로 사용할 수 있다.
고정폭 정수 타입
+/-이렇듯 데이터의 크기가 환경에 따라 조금씩 달라지기 때문에, 데이터를 쓸 수 있는 값의 차이가 아닌 데이터가 차지하는 메모리 공간이라는 입장에서 봐야 할 땐 이런 일괄적이지 못한 데이터 타입은 프로그래머의 혼란을 야기할 뿐 아니라, 프로그램 코드가 컴파일되는 환경에 따라 다른 코드를 사용해야 하는 문제가 발생한다. 그래서 새로 제시된 타입이 다음과 같은 것 들이 있다.
타입 | 바이트수 | 최소값/최대값 | stdint.h 상수 | 비고 |
---|---|---|---|---|
int8_t uint8_t |
1 | -128 - 127 0 - 255 |
INT8_MIN, INT8_MAX UINT8_MAX |
|
int16_t uint16_t |
2 | -32768 - 32767 0 - 65535 |
INT16_MIN, INT16_MAX UINT16_MAX |
|
int32_t uint32_t |
4 | -2147483648 - 2147483647 0 - 4294967295 |
INT32_MIN, INT32_MAX UINT32_MAX |
|
int64_t uint64_t |
8 | -9223372036854775808 - 9223372036854775807 0 - 18446744073709551615 |
INT64_MIN, INT64_MAX UINT64_MAX |
데이터 폭이 정해져 있는 타입들과 관련된 상수는 <limits.h> 파일이 아닌 <stdint.h> 파일에 정의되어 있다. 위에 언급된 타입들 외에도 최소 n 비트 이상의 정수 데이터를 저장할 수 있는 타입을 의미하는 int_leastn_t, uint_leastn_t (이때 n은 8, 16, 32, 64)타입과 n비트 이상의 데이터 타입중 가장 빠른 타입이라는 의미의 int_fastn_t, uint_fastn_t 타입, 현 시스템에서 사용할 수 있는 가장 큰 정수형 데이터 타입을 의미하는 intmax_t와 uintmax_t 타입도 표준에는 정의 되어있다.
위 표에 나열 되어있는 데이터 타입들은 사용되는 메모리의 양이 분명해야 하는 경우에 사용되며 산술연산을 위해서는 거의 사용되지 않는다. 주로 네트워크 패킷을 처리하는 프로그램 코드에서 자주 볼 수 있다.
조금 어려운 이야기 -- 바이트 오더 혹은 엔디언
+/-위에서 8비트 컴퓨터는 데이터를 8비트 단위로 처리한다는 이야길 했었다. 그러나 8비트로 표현할 수 있는 정수 값은 0 ~ 255 사이의 256개의 숫자 뿐이다. 그러나 실제 프로그램을 작성할 때에는 255보다 큰 값을 처리 해야 할 상황이 발생하기 마련이다. 그런 경우에 2개의 8비트 데이터를 합쳐서 16비트 데이터로 처리 하거나 4개의 8비트 데이터를 합쳐서 32비트 데이터를 처리 해야만 한다.
2개의 8비트 데이터를 합쳐서 덧셈을 하는 경우에는 먼저 2개의 8비트 데이터를 더한 다음에 자리 올림을 해서 다음번 8비트 데이터를 덧셈 하는 방식을 사용하게 된다. 좀더 간단하게 설명하자면 19 + 27을 계산할 때 먼저 9 + 7을 한 다음에 자리 올림 1과 6이라고 계산하고 다시 1 + 2 + 1 (자리올림)을 계산해서 46이라는 값을 얻게 된다.
10진 덧셈 | 2바이트 덧셈 | |||||
올림 | 1 | 1 | ||||
1 | 9 | 1E | 2F | |||
+ | 2 | 7 | + | 3A | E0 | |
4 | 6 | 59 | 0F |
위의 표에서와 같이 10진 덧셈이나 바이트 단위의 덧셈 모두 첫번째 자리에서 먼저 덧셈을 한 다음에 올림 값을 포함해서 윗자리의 덧셈을 하게 된다. 문제는 이 덧셈방식이 사람의 감각을 기준으로 해서 진행되는 것 이다. 실제 8비트 컴퓨터에서 16비트 - 2바이트 덧셈을 한다면 방법이 복잡해 진다. 변수는 메모리 공간 이라고 했던 것을 기억 할 것이다. 그렇기 때문에 실제로 8비트 컴퓨터가 하는 덧셈 이라는 것은 먼저 메모리에서 덧셈을 할 데이터를 두개 꺼내서 덧셈을 하고 두 값을 더한 다음에 다시 메모리에 넣는 세 단계 작업을 의미한다. 그림으로 그린다면 다음과 같다.
8비트 컴퓨터에서 16비트 덧셈을 수행하는 것은 조금 더 복잡하다. 먼저 아랫자리 1바이트 두개를 읽어 더한 다음 자리 올림을 확인하고, 더한 결과를 아랫자리 1바이트 영역에 저장해 둔다. 그 다음 윗자리 1바이트 두개를 읽고 자리 올림과 함께 더해 윗자리 1바이트 영역에 넣어 줘야 한다. 문제는 변수의 메모리 영역을 표시할 때 아랫자리의 위치를 표시하는 것이 아니라 윗자리의 위치를 기준으로 한다는 것이다. 그렇기 때문에 윗자리의 메모리 위치를 얻어낸 다음에 아랫자리 위치를 구하기 위해 메모리 위치 계산을 한번 해야 한다는 것이다. 그리고 위치 계산을 한 값을 가지고 더할 값을 꺼내서 더한 다음 비슷한 과정을 거쳐 저장할 위치를 계산해 내서 값을 저장한다. 아랫자리를 계산하기 위해 덧셈을 세번이나 더 해야 하고, 기준값을 유지하는 작업을 해야 하기 때문에 메모리를 더 필요로 하거나 복잡한 절차를 거처야 한다.
하지만 위의 그림에서 윗자리에 해당되는 바이트와 아랫자리에 해당되는 바이트를 바꾸면 위와 같은 복잡한 절차를 거치지 않더라도 아랫자리를 더한 다음에 결과를 넣고, 메모리 위치를 1증가 시킨 다음에 윗자리를 더하면 되므로 연산의 수가 줄어들고 연산의 복잡도 역시 줄어들게 된다. 실상 대부분의 CPU에는 인덱스드 모드(indexed mode)라는 연산 방식이 있어 여러자리의 연산을 쉽게 할 수 있도록 하는 어셈블리 명령이 별도로 존재하지만 아무래도 상대적으로 오래된 CPU들은 위에 설명한 방식으로 연산을 수행해야 했기 때문에 연산의 복잡도를 낮추기 위해 아랫자리와 윗자리를 바꿔서 저장을 했다.
아랫 자리와 윗 자리를 바꿔넣는 형태의 데이터 저장방식을 리틀 엔디언(little-endian)이라 하며, 이 방식을 사용하는 가장 대표적인 CPU로는 Intel의 x86 계열의 CPU들이 있다. 반대로 아랫 자리와 윗자리를 바꾸지 않고 그대로 저장하는 방식을 빅 엔디언(big-endian)이라 하며 네트워크를 통해 전송할 때에는 이 형태로 전송 하도록 약속되어 있다. 또한 빅 엔디언을 사용하는 CPU중에 가장 알기 쉬운 것은 매킨토시에서 사용되는 모토롤라의 68계열의 CPU들이다.
빅 엔디언과 리틀 엔디언을 통틀어 엔디언혹은 바이트오더 라고 하며 한 시스템이 아닌 여러 시스템에서 공통적으로 사용되는 데이터를 다룰 때, 특히 네트워크를 통해 데이터를 전송하는 경우에는 엔디언 - 바이트 오더 문제를 반드시 고려해야만 한다.[2]
주석 및 참고 자료
+/-- ↑ 물론 실제적으로는 여러가지 이유에 의해서 두 배 정도가 아니라 몇배에서 수십배 이상의 속도 차이가 납니다. 그 위에 이 n 비트 컴퓨터라는 용어에 대한 수많은 의견이 있기 때문에 이렇게 한마디로 정의해 버리기엔 여러가지 불편한 점이 많기는 하지만, C 언어를 배우는데 있어 가장 필요한 정보만 제공한다는 의미에서 필요 이상으로 용어의 정의에 있어야 할 이야기들을 가지쳐 버렸습니다.
- ↑ 일반적으로 네트워크를 통해 데이터를 전송 할 때엔 빅 엔디언으로 전송을 합니다. 네트워크를 통해 전송할 때 사용해야 하는 바이트 오더라는 의미로 네트워크 프로그래밍을 할 때엔 빅 엔디언 이라는 용어 보다는 '네트워크 오더'라는 용어를 더 많이 사용합니다. 네트워크를 통해 데이터를 전송할 때엔 상대방 프로그램 외에도 중간에 거쳐야 하는 여러 네트워크 장비를 거쳐야 하기 때문에 오로지 자신이 만든 두 프로그램 사이에서 통신을 하는 경우에라도 데이터, 특히 네트워크 전송시 사용되는 패킷의 해더 정보는 반드시 네트워크 오더로 변환하는 작업을 해야만 합니다.