제목 : C 알고리즘 문제 scanf, printf 없이 입출력 수행하기
부제 : fgets, fputs 사용법
이 글을 작성하는 이유
처음 프로그래밍을 배울 때, 우리는 C 나 Java 로 시작한다.
나 또한 C, Java 를 처음 대학교에서 접했을 때 scanf 에 해당하는 기능으로
입력과 출력을 수행했다. (문자열을 통해 입력을 원하는 데이터로 변환)
그러나, 내가 알고리즘을 풀 뿐만 아니라, 파일을 읽는 과정에서
scanf 는 도리어 보안적으로 위험할 수도 있다는 것을 알게 되었으며,
또한 동적 입력에 대해 생각보다 잘 대처하지 못한다는 것이다.
그래서 왜 scanf, printf 를 사용하지 않느냐면,
내가 이전에 알고리즘을 풀 때 BufferedReader 객체를 이용해서,
StringTokenizer 객체에 넣어 토큰화 시킨 후 문제를 풀었다.
즉,
BufferedReader를 이용하여 하나의 줄을 통째로 읽는다.StringTokenizer생성시 읽은 줄을 통째로 넣어 문자열 토큰으로 변환했다.
또한, C 언어에서는 입력의 토큰화를 위한 도구가 strtok 라는 것이 존재하는데,
따로 토큰을 담아둘 장소도 외부에서 마련해야 하고,
복사를 외부에서 해야 한다는 번거로운 점들이 있었다. (C 자체의 함수 무결성을 보장하기 위함이 아닐까?)
위의 단점들을 극복하기 위해 직접 토큰화 메서드를 제작해서
지금까지 수십개의 문제를 풀어오며 잘 사용했다. (매번 직접 작성함)
단, 메모리 해제를 위한 메서드는 각 2차원 배열마다 잘 작성 해 주거나,
혹은 공통타입을 의미하는 void* 를 잘 사용하여 해제해야 한다.
C 언어는 C++ 에 비해서도 OOP(객체 지향.) 적인 부분이 굉장히 부족하다.
특정 데이터를 메인 로직에서 벗어난 다른 로직에서 관리하도록 설정 할 수는 있는데,
결국 메모리 관리는 메인 로직에서 엄격하게 관리해야만 한다.
이러한 문제를 풀기 위해 직접 tokenizer 메서드를 제작했다.
내가 사용하는 입출력 함수는?
stdio.h 라는 기본 라이브러리 함수는 다양한 입출력 함수들을 가지고 있다.
우리가 사용하는 scanf, printf 는, 첫 번째 인자로 %d, %s 와 같은
문자열 시그니쳐를 통해 메서드에게 현재 들어오는 문자열들에 대해 어떤 방식으로 파싱 할 것인지
지침을 내려주는 역할을 해 준다.
그러나, 나는 입출력 함수로 fgets, fputs, fputc 를 사용한다.
이 메서드들은 이러한 인자가 들어간다. (알고리즘 문제 기준.)
fgets:char* 입력을 저장할 메모리,몇 개의 문자를 저장할 것인지,stdin
fputs:char* 출력할 문자열 - 공백 및 개행문자 포함,stdout
fputc:char 출력할 문자 - 공백 및 개행문자 포함,stdout
여기서 정말 중요한 점은, 입력과 출력 모두, 개행, 혹은 빈 칸을 포함한다는 것이다.
특히, fputs 는 내부에 전달된 문자열에서 '\0' == 0 을 만날 때 까지 출력한다.
따라서, 출력할 문자열들의 맨 마지막에는 항상 '\0' 을 꼭 넣어주어야 한다.
fgets, fputs, fputc 로 입출력을 해 보자.
fgetc 는 없냐고 할 수 있는데,
있다!
단순하게 필요가 없어서 사용하지는 않았는데,
만약에 500만 개의 글자(공백개행 포함) 를 다뤄야 하는 경우,
따로 메서드를 만들어 fgetc 를 활용하는 예제를 만들면, 메모리를 굉장히 아낄 수 있을 것이다.
(char ch = fgetc(stdin) 이렇게 사용하면 됩니다)
fgets, fputs 사용해 보기
fgets 를 사용하는 것은 생각보다 단순하다.
그러나, 꼭 기억해야 할 요소가 있다. 이는 바로 밑의 예제에서 볼 수 있을 것이다.
#include<stdio.h>
int main(void) {
char input[255];
fgets(input, sizeof(input), stdin);
fputs(input, stdout);
return 0;
}
➜ testDir gcc test.c -Wall -Wextra -pedantic -O2 -o test
➜ testDir ./test
입력 한 대로 동일하게 출력되는 것을 볼 수 있다.
입력 한 대로 동일하게 출력되는 것을 볼 수 있다.
여기서 눈여겨 봐야 할 요소는 무엇일까?
이 입출력 메서드를 수백번을 사용하며 중요하게 생각하는 것들이 있다.
sizeof- 문자열의
'\0' == 0
왜 이 두 개가 중요할까?
이를 다루기 전에, 동적 메모리 할당을 통한 "동일한 수행 코드" 를 보자.
#include<stdio.h>
extern void* malloc(size_t byte);
extern void free(void* memory);
int main(void) {
char* input;
int inputSize = 255; // or size_t inputSize = 255;
input = (char*)malloc(sizeof(char) * (inputSize + 1));
*(input + inputSize) = 0;
// or input[inputSize] = 0;
// or input[inputSize] = '\0';
fgets(input, inputSize, stdin);
fputs(input, stdout);
free(input);
return 0;
}
➜ testDir gcc test.c -Wall -Wextra -pedantic -O2 -o test
➜ testDir ./test
abcdefg
abcdefg
무언가 조금 추가되었지만, 완벽하게 위, 아래 코드는 동일한 역할을 수행한다.
그리고, 이 2 개의 방식에 대한 차이를 다 알고 있어야
알고리즘에서 fgets 를 사용할 때 && 하나의 입력 줄이 500만 문자일 때,
대처할 수 있다.
그렇다면, 이 코드를 합쳐서 보자.
#include<stdio.h>
/** stdlib.h 라는 기본 내장 함수 중 일부를 추출하는 것.
extern void* malloc(size_t byte);
extern void free(void* memory);
*/
int main(void) {
char input[255];
/** 위의 코드는 밑과 동일하다.
int inputSize = 255;
input = (char*)malloc(sizeof(char) * (inputSize + 1));
*(input + inputSize) = 0;
*/
fgets(input, sizeof(input), stdin);
// fgets(input, inputSize, stdin);
fputs(input, stdout);
// 동일.
// free(input);
// 동적 메모리는 항상 해제 해 줘야 한다.
return 0;
}
주석으로 처리 된 부분은 동적 메모리에 해당하는 코드로서,
정적 메모리로 생성되었을 때와 아닐 때의 차이를 볼 수 있게 만들었다.
정적 메모리와 동적 메모리의 차이?
우선, 정적 메서드의 sizeof 의 의미와, 동적 메서드의 sizeof 의미는 다르다.
정적 메서드의 경우, 선언된 배열의 길이 자체를 반환한다.
정적 배열 --> sizeof(input) == 255
그러나 동적 메서드의 경우, sizeof(input) 을 다르게 인식한다.
input 은 char* 로서, 포인터로 선언되었다.
따라서, 동적 배열 --> sizeof(input) == sizeof(unsigned int) == 8? 와 동일하다.
즉, 동적 배열을 그대로 sizeof 해버리면, "주소값을 저장하는 타입의 바이트 크기" 로 인식한다.
따라서, 정적 배열과 동적 배열의 입력 차이가 존재할 수 밖에 없다는 것이다.
코드가 조금 많아진다는 차이를 제외하고,
동적 메모리 코드를 살펴보면, 내가 *(input + inputSize) = 0; 처리 한 부분을 볼 수 있다.
그런데, 왜 0 을 마지막에 넣어줄까??
정적 메모리의 경우, 내부를 0 으로 채워준다.
즉, 값을 사용하고 읽을 때, 나머지 부분이 0 이기 때문에, 마지막을 쉽게 알 수 있다.
그러나, 동적 할당, 특히 malloc 의 경우, 메모리가 초기화 되지 않는다.
쓰레기 값이 들어가 있는데, 이들은 0 값이 아니다.
fputs 함수는 입력된 문자열에 대해서, 0 을 인지 할 때 까지 문자를 출력한다.
정적 메서드의 경우 0 으로 초기화 되기 때문에 마지막이 보장되지만,
동적 메서드의 경우 스스로 지정해 주지 않는 이상 마지막이 보장되지 않는다.
따라서, 동적 메모리를 사용하기 위해서는 "마지막" 을 개발자가 기억해야 한다.
즉, 시스템이 인지하는 끝, 0 이라는 데이터를 개발자가 직접 넣어줘야 한다.
따라서, 나는 배열의 맨 마지막에 '\0' 에 해당하는 문자인 0 을 넣어 준 것이다.
이것이 매우 번거롭다면,
extern void* calloc(size_t size, size_t byte)
를 선언하여 사용하면 된다.
그러면, 동적 메모리는 fputs 를 통해 254 글자를 출력해야 하지 않나?
정적 메모리는 선언 시 입력된 크기 만큼 0 으로 초기화되기 때문에,
fgets 메서드를 통해 입력된 글자를 그대로 출력한다.
그런데, 방금 내가 만들어 둔 동적 메모리를 통해 입출력 또한 동일했다.
어떻게 이게 가능했던 것일까? 쓰레기 값이 나와야 하지 않았을까?
비밀은 fgets 의 처리에 있다.
scanf 와 달리, fgets 는 빈칸, 개행문자도 들어온다.
그런데, fgets 는 입력된 \n 에 대해, 그 다음 문자를 0 으로 입력 해 준다.
즉, 만약에 동적 메모리 input 이 255 바이트로 할당되었다면,
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... | 255 |
|---|---|---|---|---|---|---|---|---|---|
| 입력 | a | b | c | d | e | \n |
|||
| fgets | a | b | c | d | e | \n |
0 |
||
| input | a | b | c | d | e | \n |
0 |
무의미 값들 | 0 |
fgets 는 입력된 문자열에 대해 "개행" 을 발견하면, 이를 포함시키나, 그 다음 문자는 0 으로 고정한다.
이는 나중에 "정수 변환" 및 "토큰화" 에 굉장히 편리해진다.
즉, 동적 메모리 또한 fgets 덕분에 입력된 문자 마지막에 0 을 넣어,
fputs 메서드 실행 시, 0 을 정적 메서드와 동일하게 발견한다.
따라서, 정적 메모리와 동적 메모리 둘 다 동일한 출력 결과가 나온 것이다.
-그렇다면, 왜 마지막에 0 을 넣어줬을까?-
이는 일종의 보험과 같은 보호막으로 넣어준 것이다.
문자 배열에서는 마지막에 0 값을 넣어두는 것이 매우매우 중요하다.
(그렇지 않으면 수많은 에러의 향연을 맛볼 수 있어요~)
만약에 내가 C 에 존재하는 수많은 라이브러리를 사용하여 구현했다면,
"이렇게까지" 중요하게 생각하지는 않을 것 같다.
그러나, 나는 stdio.h 라는 기본 입출력 보조 메서드 외,
나머지 유틸리티 함수는 "전부" 직접 구현하고 있다. (메모리 할당 및 해제 제외)
이러한 방식으로 입출력을 진행 할 수 있으나, 너무나도 중요한 것이 빠졌다.
바로,
- 문자열 정수 변환
- 정수를 문자열로 변환
- 토큰화
이에 대해서는 앞으로 작성할 블로그 글에 나온다.
하나 하나가 내가 사용하는 메모리 방식과 연계되어 있으며,
어찌 보면 내가 무의식적으로 만들어 낸 일종의 컨벤션이 추가되어 있을 수도 있기 때문이다.
따라서, 나는 "이것이 정답이다!" 가 아니라, 이러한 방식으로 코드를 바라볼 수가 있구나,
하는 시각으로 글을 작성하고 싶다.
이 글을 작성하며 느낀 점
C 언어는 에러에 매우 취약하며, 하드웨어적 요소, 즉, Memory 와 매우 깊게 연관되어 있다.
또한 GC(Garbage Collector or Collection) 가 없으므로,
모든 동적 메모리를 "직접" 해제해 줘야 하는 기능을 추가로 작성해야 한다.
하지만, 나는 오히려 즐겁게 알고리즘을 풀고 있다.
이유는, 내가 사용했던 모든 유틸리티 라이브러리의 소중함을 깨닫는 것을 넘어,
내가 직접 최적화 할 수 있다는 자신감이 생기기 때문이다.
특히, 나는 이러한 C 언어에 대한 설명 글을 만들기 위해 굉장히 고심했다.
사유는 바로 적당한 수준의 전문성을 갖춰야 한다는 생각이 깊게 들었기 때문이다.
"틀릴 수도 있는" 정보를 전달하면 누군가는 이를 보고 치명적인 에러에 맞닥들일 수 있다.
이게 내가 가진 책임감이었다.
앞으로 작성 할 글에,
문자열 정수 변환, 정수를 문자열로 변환, 입력 문자열에 대한 토큰화
이러한 세 가지 주제를 다루고 심화로 넘어 갈 건데,
비교적 쉬운 "문자열 정수 변환" 그리고 "정수 문자열 변환" 을 다루어야
"입력 문자열에 대한 토큰화" 를 더 잘 이해 할 수 있을 거라는 생각이 들었다.
따라서,
- 문자열 정수 상호변환
- 토크나이저
순으로 글을 작성 할 것이다.
'Algorithm' 카테고리의 다른 글
| C 그리고 fgets 라인 입력만으로 tokenizer 메서드 제작하기 (하드코어) (7) | 2025.08.14 |
|---|---|
| C 프로그램에서 정수, 문자열 상호변환 메서드 만들기 (4) | 2025.08.04 |
| C 언어와 stdio.h 라이브러리만으로 백준 풀며 포텐셜 올리기 (3) | 2025.07.26 |