C 언어에서 숫자를 이진수로 변환하기
Intro
C언어는 숫자를 이진수로 변환하는 함수를 제공하지 않는다. 따라서 직접 이진수 변환 함수를 구현해야 한다.
이진수 변환 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
char *to_binary_string(int num){
// 비트 수 계산
int bits = sizeof(int) * CHAR_BIT;
// 이진수 문자열 : 동적 할당으로 함수 바깥에서도 메모리가 해제되지 않도록 함
char *binary_string = (char *)malloc(bits + 1 + 2);
for (int i = 0; i < bits; i++){
int bit_value = num >> i & 1; // num의 오른쪽에서부터 i 번째 이진수
binary_string[bits - 1 - i] = bit_value + '0'; // '+0' 을 하면 ASCII 문자 0 또는 1을 반환함
}
// 문자열 끝을 뜻하는 널문자 ('\0')를 추가
binary_string[0] = '0', binary_string[1] = 'B'; // 이진수임을 표현
binary_string[bits] = '\0'; // 문자열의 끝을 지정
return binary_string;
}
int main() {
char* binary_string = to_binary_string(10);
printf("%s", binary_string);
free(binary_string); // malloc 함수를 사용해 할당한 메모리 공간은 반드시 free 함수를 사용해 명시적으로 메모리 해제를 해줘야 함
return EXIT_SUCCESS;
}
원리 살펴보기
이 코드에는 살펴보면 좋은 포인트들이 많이 담겨있다. 하나씩 살펴보자.
포인터
1
char *to_binary_string(int num)
함수명 앞에 별표(*)가 붙은 것을 볼 수 있다. 이는 포인터(Pointer)인데, C 언어에서 문자열은 실제로 문자들이 메모리에 연속적으로 저장되어 있는 배열이며, 문자열을 다룰 때에는 첫 번째 문자의 메모리 주소(포인터)를 사용한다.
다시 한 번 짚지면, C 언어는 문자열을 다룰 때 포인터(첫 번째 문자의 메모리 주소)를 사용한다. 위 코드에서 to_binary_string 함수는 문자열을 반환해야 한다. 그리고 C 언어는 문자열을 다룰 때 포인터를 사용한다. 따라서 반환 자료형도 포인터여야 한다.
char *변수명으로 썼지만 *의 위치는 중요하지 않다.char* 변수명또는char * 변수명과 같이 사용해도 된다.
비트 수 계산
1
int bits = sizeof(int) * CHAR_BIT;
위 코드는 입력 데이터(int형)를 이진수 문자열로 표현할 때 필요한 총 자릿수(비트 수)를 계산하기 위한 구문이다. 쉽게 말해, int형 변수를 표현하기 위해 최대 몇 개의 이진 비트(‘0’ 또는 ‘1’)가 필요한지 그 자릿수를 시스템에 관계없이 정확하게 확보하는 것이다.
이진수 문자열 선언시 포인터
1
char *binary_string = (char *)malloc(bits + 1 + 2);
여기에서도 포인터가 사용되고 있다. 하나씩 뜯어보면 아래와 같다.
1
2
3
4
char *binary_string
// 변수 선언부이다.
// C 언어에서 문자열은 포인터를 사용하므로,
// 위 코드는 binary_string 이라는 문자열 변수의 주소(*)를 가리키는 것이다.
1
2
3
= (char *)
// 변수 정의부다.
// 이 또한 포인터 상수에 담을 주소값이 필요하므로, 메모리에 저장되는 데이터의 주소값을 가리키기 위해 포인터를 사용했다.
malloc
1
char *binary_string = (char *)malloc(bits + 1 + 2);
malloc 은 프로그램 실행 중에 필요한 만큼의 메모리 공간을 직접, 동적, Heap 영역에 할당받기 위해 사용된다. malloc 이 필요한 상황은 아래와 같다.
(1) 실행 시점에 메모리의 크기가 결정될 때
컴파일 시점에는 얼마나 많은 메모리가 필요할지 정확히 알 수 없고, 프로그램 실행 중에 사용자 입력이나 입력되는 데이터 크기에 따라 메모리 크기가 가변적으로 결정되는 경우 malloc을 사용한다.
(2) 함수 종료 후에도 데이터를 유지할 때 (Heap vs. Stack)
함수 내부에서 선언된 일반적인 지역변수는 함수 실행이 끝나면 자동으로 해제된다(Stack). malloc을 사용하면 메모리를 힙(Heap)영역에 할당하여, 이 메모리는 할당한 함수가 종료되에도 해제되지 않고 유지된다.
(3) 큰 메모리 블록을 할당해야 할 때
스택 메모리는 크기가 제한적(일반적으로 수 MB. 운영체제나 컴파일러에 따라 다름)이다. 크기가 매우 큰 배열이나 데이터 구조를 저장해야 할 때, 이때는 저장공간이 작은 스택 메모리에 저장하지 못하므로 힙 메모리에 저장해야 한다. 따라서 malloc 함수를 써서 힙 메모리에 저장토록 한다.
위 코드에서
malloc은 함수 종료 후에도 데이터를 유지하기 위해 사용되었다. 함수 안에 선언된 변수는 local variable 이고, 스택 메모리에 저장되어 있으므로 함수를 벗어나면 메모리가 해제된다. 그런데 그 바깥(예를 들어 위 코드에서 main 함수)에서도 해당 local variable 의 값을 쓰기 위해서는 메모리가 유지되어야 하는 문제 상황이 발생한다. 따라서 이 때에는 local variable 인 binary_string을 main 함수에서도 사용할 수 있게Heap Memory에 적재하는 것이다. 이 모든 게.. 배열, 문자열은 주소값을 기반으로 처리(콜 바이 레퍼런스)처럼 보이게 처리되기 때문이다.
malloc 함수는 매개변수로 받은 값 * byte 만큼의 메모리 공간을 확보한다.
malloc 으로 선언되는 변수값 캐스팅
1
char *binary_string = (char *)malloc(bits + 1 + 2);
malloc 함수 앞을 보면 (char *) 라는 부분을 볼 수 있다. C 언어에서 값 앞에 (자료형) 과 같이 사용하는 것은 타입캐스팅 연산자라는 것을 알 것이다. 즉 이것은, malloc 함수가 반환하는 값을 char * 즉, 문자 포인터 타입으로 명시적으로 변환하는 역할을 하는 것이다.
malloc 반환 값을 타입 캐스팅하는 이유를 살펴보자.
malloc 함수는 메모리 할당에 성공하면 void *타입의 포인터를 반환한다. 이는 “어떤 타입도 가리킬 수 있는 포인터”라는 의미로, 할당된 메모리를 아직 어떤 타입으로 사용할지 정해지지 않았을 때 사용한다. (char *) 타입 캐스팅 연산자는, 이러한 malloc 함수의 반환값을 문자 타입 포인터로 변환해주는 역할을 한다.
하지만 이는 선택사항이다. 순수한 C 언어에서는 void * 포인터에서 다른 타입 포인터로의 변환이 자동으로 이루어지기 때문에, 굳이 명시적으로 작성하지 않아도 된다. 하지만 C++ 의 경우 이를 명시적으로 선언해줘야 하므로, 호환성을 위해서 이 타입캐스팅을 하는 것은 아주 좋은 습관이라고 할 수 있다.
malloc 을 통해 확보하는 데이터 크기
1
char *binary_string = (char *)malloc(bits + 1 + 2);
여기서 malloc 을 통해 bits + 1 + 2 만큼의 메모리 공간을 확보하고 있다. 이게 정확히 얼마만큼의 공간인지 보면, bits 는 (int형 바이트 사이즈 * 1바이트의 비트 개수) 이므로 (4 * 8 = 32) 가 된다. 그리고 여기에 1 + 2 를 더하므로 총 35바이트를 확보할 것이다. 다시 정리해보면 아래와 같다.
| 표현 | 값(일반적) | 단위 | 설명 |
|---|---|---|---|
| sizeof(int) | 4 | byte | int 자료형 변수가 차지하는 메모리 크기. 보통 4바이트. |
| CHAR_BIT | 8 | bit | 1바이트가 차지하는 비트 수. int 형을 비트만큼 표현해야 하므로 이를 위와 곱한다. |
| 1 | 1 | byte | 문자열의 끝 문자 (\0)를 저장하기 위한 공간 |
| 2 | 2 | byte | 2진수임을 표시하기 위한 문자열(0B)를 저장하기 위한 공간 |
즉, int 형 숫자는 총 32비트이고, 이를 문자열로 표현하려면 기본적으로 32개의 char 형 공간이 필요하므로, 32바이트를 확보해야 한다. 여기에 더해 끝 문자와 2진수 표시 문자의 공간을 더해 총 35바이트의 공간이 필요한 것이다.
반복문 조건
1
for (int i = 0; i < bits; i++)
이 반복문 조건을 보면, i를 bits 번 순환하게 한다. 이는 int 형 변수인 num 의 각 비트 자릿수를 순환하면서 탐색하기 위함이다.
이진수 각 자릿수 값 뽑기
1
int bit_value = num >> i & 1;
가장 중요한 부분이다. 이 구문은 int 형 변수 num 의 이진수의 각 자릿수를 bit_value lvalue 에 담는 것이다. 하나씩 뜯어보면 아래와 같이 볼 수 있다.
| 부분 | 설명 |
|---|---|
| num | 이진수로 변환할 int 형 정수 |
| » | num 의 비트를 오른쪽으로 이동함(=2의 i승 나누기) |
| i | num 비트를 오른쪽으로 몇 번 이동할지 |
| & | 비트연산자 중 AND 연산자 |
| 1 | 십진수로 1, 이진수로 0B0001 을 뜻한다. 비트연산자 &와 함께 사용하면 pass-through 역할을 하는데, 1을 만나면 1을, 0을 만나면 0을 결과값으로 낸다. |
즉 이는, num 의 오른쪽부터 i 번째 비트 값을 그대로 출력하는 구문이다. 그리고 앞서 살펴본 for 문과 결합하여, num 의 모든 비트 자릿수를 순환하면서 그 값을 결과값으로 내게 된다.
1
2
3
4
5
6
7
8
9
//e.g. num = 10 인 경우
int num = 10;
num = 0B001010; // 이진수로 변환하면
// i 가 1일 때
0 & 1 => 0 // num 의 이진수 가장 오른쪽인 0, 그리고 이것과 1의 AND 비트연산
// i 가 2일 때
1 & 1 => 1 // num 의 이진수 오른쪽에서 두 번째인 1, 그리고 이것과 1의 AND 비트연산
문자열에 값을 담기
1
binary_string[bits - 1 - i] = bit_value + '0';
위 수식에서 binary_string[bits - 1 -i] 부분을 살펴보자. 이는 배열형 변수 binary_string 의 시작 주소에서 (bits - 1 -i)만큼 떨어진 위치에 있는 단일 문자 요소의 메모리 공간을 뜻한다.
1
*(binary_string + (bits - 1 - i)) = bit_value + '0';
이는 포인터 변수를 활용한 경우이다. C 언어에서 배열 표기 A[i] 은 내부적으로 포인터 연산 *(A + i)와 완전히 동일하게 처리된다. *binary_string 은 해당 문자열 데이터가 저장된 메모리 위치의 시작 주소이고, 뒤에 더해지는 숫자만큼 시작주소에서부터 떨어진 곳의 주소를 계산하게 된다.
숫자를 문자열로 변경하기
1
binary_string[bits - 1 - i] = bit_value + '0';
이번에는 위 수식에서 bit_value + '0' 구문을 살펴보자. 이 부분은 정수 값을 해당하는 문자로 변환하는 부분이다.
잘 알다 시피 C 언어에서 문자형은 정수형으로도 표현할 수 있다. 그리고 문자 ‘0’ 은 정수로 48에 해당한다. 그러면 여기서 bit_value 가 1이라면, 1+48 = 49가 되며, ASCII 코드 값 49는 문자로 ‘1’ 로 치환된다. 따라서 어떠한 한자리 숫자에 문자 ‘0’ 을 더하면, 원래 한자리 숫자의 문자값을 받을 수 있는 것이다.
문자의 시작 및 끝값 넣기
1
2
binary_string[0] = '0', binary_string[1] = 'B'; // 필수 아님
binary_string[bits] = '\0'; // 필수
(1) 첫 번째 줄 : 문자열 앞에 “0B” 를 붙이기 : 2진수라는 표현 (필수 아님)
(2) 두 번째 줄 : 문자열 마지막에 null 문자를 붙여 문자열의 끝임을 알림 (필수)
main 함수의 free(binary_string)
1
free(binary_string);
이 부분은 malloc 함수와 연관이 있다. malloc 함수로 할당받은 메모리 공간은 Heap 영역에 존재하며, 이는 사용자가 명시적으로 해제하기 전까지는 해당 메모리 공간이 유지된다. 따라서 malloc 으로 할당받은 메모리 공간은 반드시 free 함수로 해제해줘야한다.
실행 예시
1
2
# 10에 대한 출력값
0B000000000000000000000000001010
Comments