제목 : C 프로그램에서 정수, 문자열 상호변환 메서드 만들기
이 글을 작성하는 이유
C 에서도 문자열을 수로 바꾸는 메서드가 존재한다.
이러한 내장 메서드는 어셈블리 급으로 최적화를 해 놓았고,
또한 검증되었기 때문에 사용하는 것이 더 정확하고 편하다.
그러나, 나는 C 언어로 알고리즘을 푸는 데 있어, 제약을 걸었다.
stdio.h 내장 라이브러리를 제외한 모든 유틸리티 메서드를 "직접" 제작하는 것.
물론, malloc, free, realloc, calloc 과 같은
동적 메모리 할당과 해제에 필요한 메서드는 extern 으로 가져와서 사용한다.
나는 이러한 제약을 스스로 걸어서, 각 라이브러리가 "어떻게" 동작하는지 이해하기 위해
이러한 제약을 지키고 있다.
(물론 어떤 문제들은 까마득하기도 하다.)
알고리즘 문제를 풀기 위해 "나만의 메서드" 를 만들어서 사용하고 있는데,
혹시라도 특정 상황에서 문자열과 정수 상호변환을 직접 구현할 필요가 있는 경우,
내가 만든 메서드가 도움이 될 것이라고 판단했다.
따라서, 이 글을 작성한다.
생각보다 유의해서 봐야 할 점
타입의 변환은 항상 문자, 그리고 문자열에 초점을 둬야 한다.
다루고 있는 정수는 어떤 과정으로 문자열로 변할 수 있는지,
그리고 현재 문자열은 어떻게 정수로 변할 수 있는지 상상 해 보자.
이 과정을 이해하기 위해서는,
- 10진수를 사용하고 있다.
- 문자와 문자열에 집중해야 한다.
- 그렇다면 "음수" 는 어떻게 구현하지?
이 3 가지를 꼭 기억해야 한다.
isBlank - 현재 문자는 공백, 혹은 마지막인가?
문자열을 다룰 때 가장 중요한 것이, 현재 지정한 문자는 공백, 혹은 마지막인 0 에 해당하는
데이터인지 확인하는 것이다.
알고리즘을 풀 때, 주어지는 데이터의 분할은 "공백" 혹은 "개행" 으로 이루어진다.
예를 들어, 입력은 이러한 방식으로 주어진다.
3 2 4
fgets 메서드로 이 데이터를 담았을 때, 이러한 형상을 이룬다.
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... |
|---|---|---|---|---|---|---|---|---|---|
| '3' | ' ' |
'2' | ' ' |
'4' | '\n' |
0 |
or
3
2
4
6
5
다행히도, fgets 는 공백 다음 주소에 0 을 삽입해 준다.
그러나, 여전히 "개행 문자" 들은 그대로 담아서 준다.
그렇다면, 우리는 이 숫자 문자들을 하나의 토큰 으로 만들기 위해서,
개행 혹은 빈 칸을 인식 할 필요가 있다. (제거하기 위함)
그런데, 나는 C 를 이용하여 알고리즘을 풀 때, stdio.h 를 제외한 모든 유틸리티를
제작하기로 결정했다.
따라서, 빈칸이나, 개행에 해당하는 모든 문자의 유니코드를 간단히 외워 사용하면 된다.
개행이나, 탭과 같은 문자들도 포함하는 영역이 있으므로, 간단히 사용하면 된다.
' '== 320== 0\nor\t및 잡다 --> 9 ~~ 13
따라서, 32 or 0 or 9 ~ 13
이것만 외우면 된다.
_Bool isBlank(char ch) {
// if(ch == 32 || ch == 0)
if(ch == ' ' || ch == '\0') {
return 1;
} else if(ch >= 9 && ch <= 13) {
return 1;
} else {
return 0;
}
}
맨 위는 문자 형식으로 표현했는데, 나는 사실 숫자로 편하게 지정한다. (수백번은 사용한듯)
'\0' == 0 은 문자 상 "빈 칸" 혹은 "개행" 에 비슷한 과에 속할 수 있나?
생각을 했었는데,
바로 밑에서 parseInt 를 입력받은 수에 바로 사용하기 위해서는 추가하는게 훨씬 편하다는 것을
깨닫게 되어 이러한 방식으로 추가했다.
parseInt - 문자열을 정수로 변환하기
아무래도 Java 를 이용하여 알고리즘을 풀다가, C 로 다시 시작하여
나에게 가장 직관적인 메서드 이름인 parseInt 로 정하게 되었다.
그렇다면, 어떻게 문자 배열(문자열) 을 "정수" 로 변환할까?
나는 처음에 이러한 역할을 하는 유틸리티 메서드를 제작할 때 많은 고민이 있었다.
만약에 헷갈린다면, 이러한 생각을 해 보자.
만약에, 기존 정수를 "이진값" 으로 만들려면 어떻게 해야 하는가?
또한, "이진값" 을 기존 정수로 만들려면 어떻게 해야 하는가?
이러한 발상에서 나의 문자열로 정수 상호변환에 대한 유틸리티 메서드가 탄생했다.
설명은 이따가 하는게 낫다는 생각이 들고, 일단 코드를 보자.
int parseInt(char* str) {
// 음수 양수 구별 위함.
int sign = 1;
// 메서드에서 만들게 될 최종 값을 담아둘 장소.
int result = 0;
// 순회용 포인터를 생성
char* strPtr = (char*)str;
char first = *strPtr; // or str[0];
if(first == '-') {
// 파싱하게 될 문자열은 "음수" 이다.
sign = -1;
// 순회용 포인터를 한 칸 옮긴다. (그 다음부터는 숫자를 의미하므로)
strPtr++;
}
// 숫자로 파싱하려는 현재 문자가 빈 공간을 의미하는 문자가 아닐 때 까지 반복한다!
while(isBlank(*strPtr) != 1) {
// 현재 포인터가 가르키는 문자를 가져온다.
char currCh = *strPtr;
// 포인터를 그 다음으로 한 칸 옮긴다.
strPtr++;
// 숫자 문자에서 숫자로 변환한다.
int currNum = currCh - '0';
// 기존 수를 10 배로 해서 1 번째 자리를 열어준다.
result *= 10;
// 확장된 1 번째 자리에 현재 수를 넣어준다.
result += currNum;
}
// 순수 숫자와, 이 문자열이 가르키는 음수 혹은 양수로 만들어 주기 위해 sign 을 곱한다.
return result * sign;
}
위의 방식은 숫자 문자열을 처리하기 위한 방식이며, 고정된 지식이 아니다.
자신만의 다른 의견이 있다면, 이를 참조하여 더 깔끔하게 만들 수 있다.
위의 코드는 예시를 들기 위한 것이고, 이를 단축하여 만든다면,
int parseInt(const char* str) {
int sign = 1;
int result = 0;
char* strPtr = (char*)str;
if(*strPtr == '-') {
sign = -1;
strPtr++;
}
while(isBlank(*strPtr) != 1) {
result *= 10;
result += *strPtr++ - '0';
}
return result * sign;
}
이렇게 깔끔한 형식을 유지 할 수 있다.
물론, for 문을 사용하여 관리하는 것이 직관적이다.
그러나, 나는 포인터를 적극적으로 사용하고, 이 포인터를 루프로 돌리는 것이 개인적으로 더 직관적이었다.
C 를 사용하면서 든 생각은, 생각보다 포인터를 자주 쓰다 보면 편하다는 것이다.
intToStr - 정수를 문자열로 변환하기
이름을 좀 생소하게 지었는데,
Java 처럼 toString 으로 지을까 하다가, 메서드가 조금 더 직관적으로 보이도록 조정했다.
말 그대로, int --> char*(문자열) 로 변환하는 메서드이다.
이 때 중요한 것은, 문자열의 마지막은 개행이나 빈칸 따위가 아닌, 정확히 0 이 들어가야 한다는 것이다.
그렇다면, 이 유틸 메서드에서 중요하게 여겨야 할 요소는 무엇일까? 생각해 보았다.
- 변환될 문자열의 정확한 길이를 알아야 한다.
- 각 자리수의 문자를 "어떻게" 문자열에 넣을 것인지 고민해야 한다.
- 만약에, "음수" 라면 어떻게 할 것인가?
- 정적 문자열을 사용할 것인가? 동적 문자열을 사용할 것인가?
나는 동적 문자열을 사용하고, 다 쓴 후에는 free(메모리 해제) 를 시켰다.
물론, malloc 을 먼저 사용했다.
먼저 설명에 가까운 코드를 먼저 짜 보겠다
extern void* malloc(size_t byte); // stdlib.c 에서 추출됨.
char* intToStr(int target) {
// 음수인지 양수인지 알아야 한다.
int sign = 1;
// 만약 변환 타겟이 음수라면, 기호를 바꿔야 한다. target, sign 둘 다
if(target < 0) {
sign = -1;
target *= -1;
}
// 우리가 10 을 곱해가며 정수에 자리를 마련했었다.
// 이를 역으로 이용하여, 10을 나눠가며 마련해야 할 문자열 길이를 추출한다.
int len = 0;
// temp 는 길이 추출용 임시 변수
int temp = target;
// do - while 구문을 사용하는 이유는, 0 일 수도 있기 때문이다.
// target 이 0 이라면, 문자열 길이는 0 으로 도출되어 버리는 것을 방지한다.
//
// 즉, 이 함수는 단순 숫자로서의 문자열 길이를 도출한다.
do {
// 우리는 10 진수를 사용하므로.
temp /= 10;
len++;
} while(temp != 0);
/**
여기서 가장 중요한 점은, 양수인가? 음수인가? 이다.
음수일 경우, 문자열 "가장 앞"에 '-' 라는 문자가 와야 한다.
이를 염두에 두고 계산해야 한다.
그런데, 어떻게 가장 앞에 '-' 를 둬야 할까?
맨 앞의 수부터 추출해야 할까? 맨 뒤 부터 추출해야 할까?
나의 경우, 1, 10, 100, ... 순으로 추출하여 문자를 넣는다.
즉, 뒤에서부터 문자를 채운다는 의미이다.
뒤에서부터 앞 부분까지 문자를 채우되,
음수의 경우 "맨 앞" 이 '-' 가 되고, 양수는 이 분기를 무시한다.
*/
int size;
// 양수 일 경우, 계산된 len 변수는 정확한 문자열 길이가 된다.
if(sign == 1) {
size = len;
} else { // sign == -1
// 음수일 경우, 맨 앞에 '-' 를 놓아야 하기 때문에, 공간을 하나 더 만든다.
size = len + 1;
}
// 우리가 만들 문자열의 길이는 구해졌으므로, 동적 할당을 이용하여 문자 공간을 형성한다.
char* result = (char*)malloc(sizeof(char) * (size + 1));
// 맨 마지막에 "종료" 를 알리는 것은 필수필수필수이다.
*(result + size) = 0;
// 문자를 채우는 과정은 뒤에서부터이며, 문자열의 마지막 인덱스는 정확히 size - 1 이다.
int endIdx = size - 1;
// 음수일 경우, '-' 를 넣어야 하고, 양수일 경우 넣지 않는다.
int startIdx;
if(sign == 1) {
// 양수일 경우, 숫자만 채워진다.
startIdx = 0;
} else {
// 음수일 경우, 인덱스 0 에 '-' 를 넣어줘야 한다.
startIdx = 1;
}
// 순회용 인덱스. 뒤에서부터 숫자 문자를 넣어준다.
int idx = endIdx;
// startIdx 는 음수일 경우 1, 양수일 경우 0 이다.
// 즉, 음수일 경우 맨 앞을 비워 '-' 를 넣을 준비를 한다.
while(idx >= startIdx) {
// 숫자로 따졌을 때, 가장 낮은 수를 추출한다.
int bottomNum = target % 10;
// 그 다음 수를 추출하기 위해 가장 낮은 자리를 자른다.
target /= 10;
// 현재 1 자리 수를 정확한 문자 수로 곧바로 변환.
char currChar = bottomNum + '0';
// result[idx] = currChar; 과 동일하다.
*(result + idx) = currChar;
idx--;
}
// 만약, 음수라면, 비어있는 첫 번째 자리를 '-' 로 채워주면 된다.
if(sign == -1) {
// result[0] = '-'; 과 동일하다.
*reseult = '-';
}
return result;
}
위의 함수는 동적 할당을 통해서 결과물을 내놓으므로, 반드시 외부에서 free 해 줘야 한다.
만약에 정적 할당을 하고 싶다면, 변수로 문자열 공간을 받고,
해당 공간에 문자를 작성하면 된다.
하지만, 반드시 문자열의 마지막에 0 == '\0' 을 넣어주는 것을 잊으면 안된다.
간략화 버전
char* intToStr(int target) {
int sign = 1;
int len = 0;
if(target < 0) {
sign = -1;
target *= -1;
}
int temp = target;
do {
temp /= 10;
len++;
} while(temp != 0);
int size = sign == 1 ? len : len + 1;
int endIdx = size - 1;
int startIdx = sign == 1 ? 0 : 1;
char* result = (char*)malloc(sizeof(char) * (size + 1));
*(result + size) = 0;
int idx = endIdx;
while(idx >= startIdx) {
*(result + idx--) = (target % 10) + '0';
target /= 10;
}
if(sign == -1) {
*result = '-';
}
return result;
}
이러한 방식으로 간결하게 작성 할 수도 있다.
간결한 코드를 작성하기 위해서, 전위, 후위 연산자에 대한 개념을 숙지하는 것은 매우 중요하다.
이는 비단 단순 숫자만이 아니라, 포인터 그 자체에도 적용되기 때문이다.
그리고, 삼항 연산자에 대한 이해도 필수라고 생각한다. (코드를 간결하게 만들고 싶다면)
실제로 사용 해 보기
이러한 유틸 메서드가 생성된 데에는,
내가 알고리즘 문제를 C 로 풀 때, stdio.h 와 동적 메모리와 관련된 함수 빼고,
모든 유틸 메서드를 "직접" 전부 일일이 작성하여 푸는 제약 때문이다.
따라서, fgets, fputs, fputc 를 이용하여 입출력을 진행한다.
(scanf, printf 도 사용하지 않는다는 제약을 걸음.)
예제
#include<stdio.h>
extern void* malloc(size_t byte);
extern void free(void* memory);
char* intToStr(int target);
int parseInt(const char* str);
_Bool isBlank(char ch);
int main(void) {
char input[255];
fgets(input, sizeof(input), stdin);
int result = parseInt(input);
char* resultStr = intToStr(result);
fputs(resultStr, stdout); fputc('\n', stdout);
free(resultStr);
return 0;
}
char* intToStr(int target) {
int sign = 1;
int len = 0;
if(target < 0) {
sign = -1;
target *= -1;
}
int temp = target;
do {
temp /= 10;
len++;
} while(temp != 0);
int size = sign == 1 ? len : len + 1;
int endIdx = size - 1;
int startIdx = sign == 1 ? 0 : 1;
char* result = (char*)malloc(sizeof(char) * (size + 1));
*(result + size) = 0;
int idx = endIdx;
while(idx >= startIdx) {
*(result + idx--) = (target % 10) + '0';
target /= 10;
}
if(sign == -1) {
*result = '-';
}
return result;
}
int parseInt(const char* str) {
int sign = 1;
int result = 0;
char* strPtr = (char*)str;
if(*strPtr == '-') {
sign = -1;
strPtr++;
}
while(isBlank(*strPtr) != 1) {
result *= 10;
result += *strPtr++ - '0';
}
return result * sign;
}
_Bool isBlank(char ch) {
if(ch == 32 || ch == 0) {
return 1;
} else if(ch >= 9 && ch <= 13) {
return 1;
} else {
return 0;
}
}
Result 1 :
$ gcc int-str.c -Wall -Wextra -pedantic -O2 -o int-str
$ ./int-str
100
100
Result 2 :
$ gcc int-str.c -Wall -Wextra -pedantic -O2 -o int-str
$ ./int-str
15792024
15792024
Result 3 :
$ ./int-str
-12345
-12345
이렇게 정수-문자열 변환을 쉽게 만들 수 있다.
물론, 자기만의 메서드를 만드는 과정은 어렵다.
특히, 앞으로 이 메서드를 사용함에 있어 오류가 존재할지, 어느 상황에 사용할 지는
직접 사용해 보며 알아야 한다.
printf 를 쓰지 않으니까 맞는 답인데도 오류가 난다?
그건 fputs 의 특성으로, 정말 전달된 문자열만 전달한다.
그게 백준 결과에서 어떤 문제가 되는가?
백준의 정답은 우리가 출력 할 형식만 지정하고 출력하면 끝이 아니다.
기본적으로 printf 를 사용한다는 가정 하에 제작된다.
즉, 마지막에 개행 문자를 통해 다음 줄로 넘어가야 정답으로 인정이 된다.
만약에 마지막에 fputc('\n', stdout); 을 작성하지 않는다면,
이건 출력 부족과 동일한 상황이다. 물론, 백준의 에러 메세지에서는 표현되지 않겠지만.
마무리
C 에서의 문자열 입출력은 원래부터도 간단한 문제는 아니었다.
그러나, fgets, fputs 는 문자열을 다루는 데 있어 더 고난도의 작업을 요구한다.
그러나, 내가 직접 함수를 제작한다면, 이미 누군가가 작성해 놓은 기능을 따라가기 위해
따로 공부할 필요는 없다. (물론, 이미 작성되어 있는 기능을 사용하는 것이 훨씬 편하다)
즉, 우리가 다룬 것은 이러하다.
- 알고리즘의 입출력 관리를 위해 "빈 공간" 에 해당하는 문자를 검사하는 메서드 작성
- 입력된 문자열을 숫자로 바꾸는 메서드 작성
- 결과 출력을 위해 문자열을 숫자로 바꾸는 메서드 작성.
참고로, isBlank 메서드는 추후 작성하게 될,
하나의 입력 줄에 대해서 토큰화를 시키는 데 지대한 영향을 끼친다.
꼭 기억해 두는 것이 좋지 않을까? 생각된다.
이 글을 작성하며 느낀 것.
확실히, 어떤 기능을 만들었다고 곧바로 포스팅하지 않고,
기능에 충실하며 일관성을 유지하는 메서드를 곧바로 작성할 수준까지 올라오고 나서
이러한 주제에 대해서 작성한 것이 옳바른 선택이었다고 생각한다.
그렇지 않았다면, 나의 설명은 분명히 빈약했을 것이라고 생각한다.
분명히 개발자 특성상 (나 또한 어느정도) 빠르게 훑겠지만,
누군가에게는 분명히 도움이 될 것이라고 생각한다.
혹시라도 궁금한 것이 있다면, rhdwhdals8@gmail.com 으로 질문 해주셔도 됩니다.
환영할게요!
만약 나의 C 알고리즘 프로그램 행보가 궁금하다면,
이 사이트가 적정할 것이라고 판단됩니다.
이 레포에서 C 에 해당하는 코드들을 살펴보시면, 제가 어떻게 작성하는지 알 수 있으실 겁니다.
'Algorithm' 카테고리의 다른 글
| C 그리고 fgets 라인 입력만으로 tokenizer 메서드 제작하기 (하드코어) (7) | 2025.08.14 |
|---|---|
| C 알고리즘 문제 scanf, printf 없이 입출력 수행하기 - (fgets, fputs) (1) | 2025.08.01 |
| C 언어와 stdio.h 라이브러리만으로 백준 풀며 포텐셜 올리기 (3) | 2025.07.26 |