모든 컴퓨터는 포인터 데이터의 기본 크기를 나타내는 워드 크기(word size)를 가집니다. 워드 크기가 결정하는 가장 중요한 시스템 매개변수는 가상 주소 공간의 최대 크기입니다.
최근에는 과학 계산, 데이터베이스, 데스크톱, 스마트폰에 이르기까지 32비트에서 64비트 시스템으로의 전환이 널리 이루어졌습니다. 대부분의 64비트 컴퓨터는 하위 호환성을 위해 32비트용으로 컴파일된 프로그램도 실행할 수 있습니다. 프로그램이 32비트인지 64비트인지는 실행되는 기계가 아니라 어떻게 컴파일되었는지에 따라 결정됩니다.
컴퓨터와 컴파일러는 다양한 길이와 방식으로 데이터를 표현합니다. C언어는 정수와 실수를 위한 여러 자료형을 지원하며, 그 크기는 32비트 또는 64비트 프로그램으로 컴파일하는지에 따라 달라질 수 있습니다.
C언어 기본 자료형의 일반적인 크기 (바이트 단위)
| C 선언 | 32비트 프로그램 | 64비트 프로그램 | 설명 |
|---|---|---|---|
char | 1 | 1 | 단일 바이트 또는 작은 정수를 표현 |
short | 2 | 2 | 작은 정수 |
int | 4 | 4 | 기본적인 정수 (64비트에서도 보통 4바이트) |
long | 4 | 8 | 큰 정수 (64비트 프로그램에서 크기가 변하는 대표적인 자료형) |
char * (포인터) | 4 | 8 | 포인터는 항상 해당 프로그램의 워드 크기를 따름 |
float | 4 | 4 | 단정밀도 실수 |
double | 8 | 8 | 배정밀도 실수 |
Sheets로 내보내기
컴파일 환경에 따라 자료형의 크기가 달라지는 문제를 피하기 위해, ISO C99 표준에서는 크기가 고정된 자료형을 도입했습니다.
int32_t: 부호 있는 32비트(4바이트) 정수uint32_t: 부호 없는 32비트(4바이트) 정수int64_t: 부호 있는 64비트(8바이트) 정수uint64_t: 부호 없는 64비트(8바이트) 정수이러한 고정 크기 자료형을 사용하는 것은 프로그램의 이식성(portability)을 높이는 가장 좋은 방법입니다. 프로그래머는 다른 컴퓨터나 컴파일러에서도 데이터가 항상 동일한 크기를 가질 것이라고 확신할 수 있습니다.
과거 32비트 시대에는 많은 프로그래머들이 포인터를 저장하기 위해 int 자료형을 사용하곤 했습니다. 이는 32비트 프로그램에서는 포인터와 int가 모두 4바이트로 크기가 같았기 때문에 문제없이 동작했습니다. 하지만 이런 코드를 64비트 환경으로 옮기면, 포인터는 8바이트인데 int는 여전히 4바이트이므로 심각한 버그가 발생하게 됩니다.
여러 바이트로 구성된 데이터를 메모리에 저장할 때는 두 가지 규칙이 필요합니다.
거의 모든 컴퓨터에서 여러 바이트로 이루어진 데이터는 연속된 메모리 공간에 저장되며, 그중 가장 작은 주소가 해당 데이터의 주소로 사용됩니다.
데이터를 구성하는 바이트들을 메모리에 나열하는 방식에는 두 가지가 있습니다.
예를 들어, 4바이트 정수 0x01234567을 메모리 주소 0x100에 저장한다고 가정해 봅시다.
016701 23 45 67)| 주소 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 값 | 01 | 23 | 45 | 67 |
67 45 23 01)| 주소 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 값 | 67 | 45 | 23 | 01 |
인텔 호환(x86, x86-64) 머신은 대부분 리틀 엔디언 방식을 사용하며, IBM이나 Oracle(Sun)의 머신들은 전통적으로 빅 엔디언 방식을 사용해왔습니다. 최근의 ARM 프로세서처럼 두 방식을 모두 지원하는 바이 엔디언(bi-endian) 칩도 있지만, 안드로이드나 iOS 같은 운영체제가 리틀 엔디언으로 작동하기 때문에 사실상 방식이 고정됩니다.
두 방식 사이에 기술적인 우위는 없으며, 일관되게 한 가지 방식을 사용하는 것이 중요합니다. 이 용어는 조너선 스위프트의 소설 '걸리버 여행기'에서 달걀을 큰 쪽으로 깨는 '빅 엔디언'과 작은 쪽으로 깨는 '리틀 엔디언' 파벌의 싸움에서 유래했습니다.
대부분의 경우 프로그래머는 자신의 컴퓨터가 어떤 바이트 순서를 사용하는지 신경 쓸 필요가 없습니다. 하지만 다음과 같은 특정 상황에서는 문제가 될 수 있습니다.
43 0b 20 00이라고 쓰여있다면, 실제 숫자 값은 이를 뒤집은 0x00200b43일 수 있습니다.C언어에서 문자열은 널(null) 문자(\0, 값이 0)로 끝나는 문자(char) 배열로 표현됩니다. 각 문자는 보통 아스키(ASCII) 코드와 같은 표준 인코딩 규칙에 따라 표현됩니다.
예를 들어, "12345"라는 문자열의 바이트를 살펴보면 다음과 같은 16진수 값을 얻게 됩니다.
31 32 33 34 35 00
31 32 33 34 35: 각각 아스키 코드에서 문자 '1', '2', '3', '4', '5'에 해당하는 값입니다. (숫자 x의 아스키 코드는 0x3x입니다.)00: 문자열의 끝을 알리는 널(null) 종료 문자 \0 입니다.이러한 표현 방식은 빅 엔디언/리틀 엔디언과 같은 바이트 순서나 워드 크기에 전혀 영향을 받지 않습니다. 문자열은 단순히 1바이트짜리 문자들의 연속일 뿐이기 때문입니다.
결과적으로, 정수나 실수 같은 이진 데이터(Binary Data)에 비해 텍스트 데이터(Text Data)가 훨씬 더 플랫폼 독립적이며 호환성이 높습니다.
아래와 같은 간단한 C 함수가 있다고 가정해 봅시다.
int sum(int x, int y) {
return x + y;
}
이 함수를 서로 다른 컴퓨터에서 컴파일하면, 아래와 같이 완전히 다른 바이트(기계어) 코드가 생성됩니다.
55 89 e5 8b 45 0c 03 45 08 c9 c355 89 e5 8b 45 0c 03 45 08 5d c381 c3 e0 08 90 02 00 0955 48 89 e5 89 7d fc 89 75 f8 03 45 fc c9 c3이처럼 소스 코드가 기계어로 번역되고 나면, 그 결과물인 이진 코드는 특정 하드웨어와 운영체제에 매우 종속적이어서 다른 환경에서는 실행될 수 없습니다.
컴퓨터가 정보를 저장하고 다루는 핵심에 이진 값(0과 1)이 있기 때문에, 이 값들을 연구하는 풍부한 수학적 지식 체계가 발전해 왔습니다. 이는 1850년경 조지 불(George Boole)의 연구에서 시작되었으며, 그의 이름을 따 불 대수(Boolean Algebra)라고 불립니다.
불은 참(true)과 거짓(false)이라는 논리 값을 1과 0이라는 이진 값으로 표현함으로써, 논리적 추론의 기본 원리를 대수학으로 공식화할 수 있다는 것을 발견했습니다.
가장 단순한 불 대수는 {0, 1} 두 원소에 대해 정의됩니다. C언어의 비트 연산자와 일치하는 기호를 사용하여 다음과 같은 연산들을 정의할 수 있습니다.
~ (NOT): 논리 부정(¬)에 해당합니다. 0은 1로, 1은 0으로 바꿉니다.& (AND): 논리곱(∧)에 해당합니다. 두 입력이 모두 1일 때만 결과가 1이 됩니다.| (OR): 논리합(∨)에 해당합니다. 두 입력 중 하나라도 1이면 결과가 1이 됩니다.^ (XOR): 배타적 논리합(⊕)에 해당합니다. 두 입력이 서로 다를 때만 결과가 1이 됩니다.정보 이론의 창시자인 클로드 섀넌(Claude Shannon)은 그의 1937년 석사 논문에서 불 대수와 디지털 논리 회로의 연관성을 처음으로 증명했습니다.
이 네 가지 불 연산은 비트 벡터(bit vector), 즉 고정된 길이의 0과 1로 이루어진 문자열에도 확장하여 적용할 수 있습니다. 각 연산은 비트 벡터의 같은 위치에 있는 비트끼리 수행됩니다.
예를 들어, 4비트 벡터 a = [0110]와 b = [1100]가 있을 때, 연산 결과는 다음과 같습니다.
0110 0110 0110
& 1100 | 1100 ^ 1100 ~ 1100
------- ------- ------- -------
0100 1110 1010 0011
비트 벡터는 유한 집합(finite set)을 표현하는 데 유용하게 사용될 수 있습니다. {0, 1, ..., w-1}의 부분집합 A를 비트 벡터로 표현할 때, i번째 원소가 집합 A에 포함되면 i번째 비트를 1로, 아니면 0으로 설정합니다.
[01101001][01010101]| (OR) 연산은 합집합(Union, ∪)에 해당합니다.& (AND) 연산은 교집합(Intersection, ∩)에 해당합니다.~ (NOT) 연산은 여집합(Complement)에 해당합니다.위 예시에서 a & b를 계산하면 [01000001]이 나오는데, 이는 집합 {0, 6}을 의미하며, 이는 A와 B의 교집합인 A ∩ B와 같습니다. 이러한 집합 표현은 프로그램 실행을 중단시키는 여러 신호(signal) 중 어떤 것을 활성화할지 결정하는 마스크(mask)를 지정하는 등 실제 프로그래밍에 널리 사용됩니다.
C언어의 유용한 특징 중 하나는 비트 단위(bitwise) 불 연산을 지원한다는 것입니다. C언어는 불 대수에서 사용된 기호와 동일한 기호, 즉 | (OR), & (AND), ~ (NOT), ^ (XOR) 를 사용합니다. 이 연산자들은 char, int, long 등 모든 '정수형' 데이터 타입에 적용될 수 있습니다.
비트 수준 연산의 결과를 확인하는 가장 좋은 방법은, 16진수 값을 2진수 비트 형태로 확장하여 비트 단위로 연산을 수행한 뒤, 다시 16진수로 변환하는 것입니다.
char 타입 기준):~0x41 (NOT 연산)~[0100 0001] → [1011 1110] → 0xBE0x69 & 0x55 (AND 연산)[0110 1001] & [0101 0101] → [0100 0001] → 0x410x69 | 0x55 (OR 연산)[0110 1001] | [0101 0101] → [0111 1101] → 0x7D비트 수준 연산의 일반적인 용도 중 하나는 마스킹(Masking) 연산입니다. 마스크는 특정 비트들만 선택하거나 변경하기 위해 사용하는 비트 패턴입니다.
0xFF는 8개의 비트가 모두 1인 값(1111 1111)입니다.x에 x & 0xFF 연산을 수행하면, x의 다른 모든 바이트는 0으로 바뀌고 가장 낮은 자리의 1바이트 값만 남게 됩니다.x = 0x89ABCDEF 일 때, x & 0xFF의 결과는 0x000000EF 입니다.int가 32비트일 때 0xFFFFFFFF라고 직접 쓸 수도 있지만, 코드의 이식성을 높이려면 ~0을 사용하는 것이 더 좋습니다. ~0은 데이터 타입의 크기와 상관없이 항상 모든 비트가 1인 값(예: 1111....1111)을 생성합니다.C언어는 논리 연산을 위한 연산자 || (OR), && (AND), ! (NOT) 을 제공합니다. 이 연산자들은 앞서 설명한 비트(bit) 연산자 (|, &, ~)와 비슷해 보이지만, 동작 방식은 완전히 다릅니다.
논리 연산자는 인자를 '참(True)' 또는 '거짓(False)'으로만 판단합니다.
비트 연산은 숫자의 각 비트(0과 1) 하나하나에 대해 연산하지만, 논리 연산은 숫자 값 전체를 참 또는 거짓으로만 판단합니다.
0x69 && 0x55 → 참 && 참 → 결과는 1 (0x01)0x69 & 0x55 → 01101001 & 01010101 → 01000001 → 결과는 65 (0x41)두 연산의 결과가 완전히 다른 것을 볼 수 있습니다.
논리 연산자 &&와 ||의 가장 중요한 특징은 '단락 평가'를 한다는 것입니다. 이는 첫 번째 인자만으로 전체 결과가 결정되면, 두 번째 인자는 아예 평가(실행)하지 않는 동작 방식입니다.
&& (AND) 연산:a && 5/aa가 0이라면, 첫 번째 인자가 '거짓'이므로 전체 결과는 무조건 '거짓'입니다. 따라서 뒤따라오는 5/a는 아예 실행되지 않습니다. 덕분에 0으로 나누는 에러가 발생하지 않습니다.|| (OR) 연산:a || ba가 '참'(0이 아님)이라면, 전체 결과는 무조건 '참'입니다. 따라서 b는 아예 실행되지 않습니다.p && *p++p가 NULL 포인터(값이 0)라면, 첫 번째 인자가 '거짓'이므로 뒤의 p++(NULL 포인터를 역참조하는 위험한 연산)는 실행되지 않아 프로그램이 안전하게 유지됩니다.이러한 단락 평가 특성은 코드를 더 간결하고 안전하게 만들어주는 매우 유용한 기능입니다.
C언어는 비트 패턴을 왼쪽이나 오른쪽으로 이동시키는 시프트(Shift) 연산을 제공합니다.
<<x << k는 변수 x의 비트 패턴을 왼쪽으로 k칸 이동시키는 연산입니다.
>>x >> k는 변수 x의 비트 패턴을 오른쪽으로 k칸 이동시키는 연산입니다. 오른쪽 시프트에는 두 가지 종류가 있습니다.
| 연산 | 01100011 | 10010101 |
|---|---|---|
| x << 4 | [00110000] | [01010000] |
| x >> 4 (논리) | [00000110] | [00001001] |
| x >> 4 (산술) | [00000110] | [11111001] |
기울임꼴은 새로 채워진 비트를 의미합니다. 음수를 산술 시프트할 때 왼쪽이 1로 채워지는 것을 볼 수 있습니다.
>>: 산술 오른쪽 시프트>>>: 논리 오른쪽 시프트LIFO 구조를 갖는 자료구조이다.
TOP을 항상 가르키는 변수를 하나 지정한다. (head라고 하자)
head가 가르키는 곳에 값을 넣고 head를 하나 증가시킨다.
시간 복잡도는 O(1)이 될 것이다.
head-1의 값을 출력하고 head를 하나 감소시킨다.
시간 복잡도는 O(1)이 될 것이다.
head-1의 값만 출력하면 된다.
시간 복잡도는 O(1)이 될 것이다.
원소의 개수를 출력하는 함수이며 head를 출력하면 된다.
FIFO 구조를 갖는 자료구조이다.
맨 앞(head), 맨 뒤(rear)을 가르키는 변수를 지정한다.
rear에 데이터를 넣고 rear-1을 하자.
시간 복잡도는 O(1)이 될 것이다.
head에 데이터를 빼자.
모든 원소를 앞으로 한 칸씩 이동시켜야 하기 때문에 O(N)의 시간복잡도가 걸린다.
배열로 구현했을 때 pop의 문제점을 해결하기 위해서 나왔다. 모듈러 연산을 통해 head와 rear을 관리하자.
자세한 내용은 이전에 정리해놓았다! 여기서 확인해보자