여기서는 C언어 프로그램에서 문자와 문자열을 다루는 방법에 대해 살펴보겠습니다. 컴퓨터는 이진수를 다루는 기계이지만, 인간에게 가장 친숙한 건 알파벳 같은 문자들이죠. 컴퓨터도 결국 사람이 쓰는 도구이기 때문에, 프로그래밍 언어를 막론하고 문자열을 처리하는 법을 알아두면 상당히 좋습니다.
개인적으로 컴퓨터 시뮬레이션을 통해서 자연현상을 이해하는 것을 본업으로 해 왔기에, 문자보다는 숫자에 더 친숙했습니다. 그래서 이번 기회에 제가 유용하다고 생각되는 기능들을 중심으로, C언어에서 문자와 문자열을 다루는 법을 한번 자세히 짚어볼까 합니다. C언어 표준 라이브러리에 있는 함수들 이외에도, 문장으로부터 단어를 추출하거나 텍스트 파일로부터 프로그램을 실행하기 위한 설정 등을 불러오는 함수도 한번 만들어봅시다.
C언어나 C++ 에서 문자 하나를 저장하는 자료형은 char 입니다. 그리고 문자형 변수의 배열을 사용하거나 malloc 등의 함수를 이용해서 메모리 공간을 할당받게 되면, 여러개의 문자로 이루어진 문자열을 저장할 수 있게 되죠. 저장된 개별 문자들의 값을 이용하거나 수정함으로써, 문자열 처리가 가능하게 되겠습니다.
시작하기에 앞서서 짚고 넘어갈 점이 두 가지 있는데요. 첫번째는 문자열의 마지막에는 끝을 알리는 널 문자 (null character '\0')가 반드시 들어가야 한다는 것입니다. 이게 안되면 원하는 문자열 뒤에 쓰레기 값들이 주렁주렁 달리게 되죠. 두번째는 한 개의 문자라면 작은따옴표 안에 들어가는 반면에, 문자열은 큰따옴표 안에 들어간다는 점입니다. 예를 들어서 'a'는 말 그대로 알파벳 a를 나타내는 문자이지만, "a"는 'a'와 '\0'으로 이루어진 문자열인 것이죠.
문자열의 길이
가장 먼저 문자열의 길이를 구하는 함수를 직접 만들어 봅시다. 문자열을 따라가면서 문자열의 끝이나 줄바꿈이 나올때 까지 문자의 갯수를 세는 방법을 떠올려 볼 수 있습니다.
C언어 코드입니다.
unsigned int get_length_string(char *str_src) {
if (str_src == NULL) {
return 0;
}
char *ptr_src = str_src;
unsigned int i_char = 0;
while (*ptr_src != '\0' &&
*ptr_src != '\n') {
i_char += 1;
ptr_src += 1;
}
return i_char;
}
문자열의 길이를 부호가 없는 정수형 변수로 리턴하는 get_length_string 함수는 문자열의 포인터 str_src를 매개변수로 받고 있습니다. 여기서 문자열의 포인터라고 한 것은 맨 첫번째 글자의 메모리 주소가 되겠습니다. 문자 포인터인 지역변수 ptr_src가 함수 초반부에 등장하는데요. 이는 문자열의 맨 첫번째 문자의 주소로 초기화됩니다. 그리고 이 포인터 변수에 1을 더하게 되면 다음 문자의 주소로 넘어가는 방식이죠.
그렇게 넘어가면서, 문자열의 끝을 나타내는 '\0' 이나 줄바꿈을 나타내는 '\n' 이 나올 때까지, 공백이나 탭을 포함한 문자의 갯수를 카운트하게 됩니다. 반복문을 빠져나온 다음에는 문자열의 길이를 리턴합니다.
문자열 복사
그 다음으로 유용한 함수로는 문자열을 복사하는 함수를 생각해 볼 수 있는데요. 이는 사실 C언어 표준 라이브러리의 strcpy 함수가 이미 가지고 있는 기능입니다만, 연습삼아서 직접 구현해 보는것도 나쁘지 않을 것 같습니다.
C언어 코드입니다.
void copy_string(char *str_tar,
char *str_src,
unsigned int length_max) {
if (str_tar == NULL ||
str_src == NULL) {
return;
}
char *ptr_tar = str_tar;
char *ptr_src = str_src;
unsigned int i_char = 0;
while (*ptr_src != '\0' &&
*ptr_src != '\n' &&
i_char < length_max) {
i_char += 1;
*ptr_tar = *ptr_src;
ptr_tar += 1;
ptr_src += 1;
}
*ptr_tar = '\0';
}
문자열을 복사하는 copy_string 함수는 리턴 자료형이 없는 대신에, 원본과 사본 문자열의 포인터들인 str_src 및 str_tar을 매개변수로 받고 있습니다. 그리고 복사된 문자열의 최대 길이 역시 인자로 줄 수 있게 했습니다. 원본과 사본 문자열의 각 문자를 가리키기 위한 포인터 변수들인 ptr_tar 및 ptr_src 가 지역변수로 선언되어 있으며, 문자열의 맨 앞부분의 주소로 초기화됩니다. 복사가 끝나고 나면, 사본 문자열 뒤에 '\0'을 붙여서 마무리합니다.
문자열 이어붙이기
문자열에 다른 문자열을 이어붙이는 방법 역시 알아두면 좋습니다. 예를 들어서 프로그램에서 처리하고자 하는 데이터 파일의 경로 (디렉토리)와 이름이 별도의 문자열에 저장되어 있는 경우에, 경로 뒤에 파일 이름을 갖다붙여서 파일이 정확히 어느 디렉토리에 있는지를 명시해 두는것이 좋습니다. 이것 역시 C언어 표준 라이브러리의 strcat 함수로 구현되어 있지만, 직접 한번 만들어보도록 합시다.
C언어 코드입니다.
void concat_string(char *str_tar,
char *str_src,
unsigned int length_max) {
if (str_tar == NULL ||
str_src == NULL) {
return;
}
char *ptr_tar = str_tar;
char *ptr_src = str_src;
unsigned int i_char = 0;
while (*ptr_tar != '\0' &&
*ptr_tar != '\n' &&
i_char < length_max) {
i_char += 1;
ptr_tar += 1;
}
while (*ptr_src != '\0' &&
*ptr_src != '\n' &&
i_char < length_max) {
i_char += 1;
*ptr_tar = *ptr_src;
ptr_tar += 1;
ptr_src += 1;
}
*ptr_tar = '\0';
}
원본 문자열 str_src의 내용을 대상이 되는 문자열 str_tar의 끝에 붙이는 것은 앞서 언급한 copy_string 함수와 크게 다르지 않습니다. 차이점이 있다면, 문자 포인터 ptr_tar가 기존에 str_tar에 저장되어 있던 문자열의 끝을 먼저 가리키고, 그 다음 원본 문자열의 문자를 가리키는 포인터 ptr_src와 병행하면서 문자열을 이어붙인다는 것입니다.
위에 소개된 함수들을 활용해서, 간단한 "HelloWorld!" 예제 프로그램을 만들어 봅시다.
main 함수 코드입니다.
#include<stdio.h>
#define NCHAR_MAX_STRING 300
int main(int argc, char *argv[]) {
char *str =
(char *)malloc(sizeof(char) *
(NCHAR_MAX_STRING + 1));
fprintf(stdout, "\n");
fprintf(stdout, "Example 1\n");
copy_string(str, "Hello",
NCHAR_MAX_STRING);
concat_string(str, " World!",
NCHAR_MAX_STRING);
unsigned int n_length =
get_length_string(str);
fprintf(stdout,
" string : %s\n", str);
fprintf(stdout,
" length : %d\n", n_length);
free(str);
return 0;
}
먼저 동적 메모리 할당을 통해 문자열을 저장하기 위한 공간을 할당받았습니다. 문자열 복사를 위한 copy_string 함수를 호출하여 "Hello"라는 문자열을 저장한 다음, 여기에 " World!"라는 문자열을 이어붙였는데요. 그렇게 해서 "Hello World!"라는 문자열이 완성되었고 그 길이는 공백을 포함해서 12가 되겠습니다.
프로그램을 빌드하고 실행시켜보면 다음과 같은 결과가 나옵니다.
문자열 비교
두 개의 문자열을 비교해서 서로 같은지를 체크해야 하는 경우도 많이 있는데요. 문자 포인터를 이용하면 어렵지 않게 구현이 가능합니다. 비교하고자 하는 문자열의 포인터들을 매개변수로 받고, 일치하는지의 여부를 정수형 변수로 리턴하는 함수를 만들어 보겠습니다.
C언어 코드입니다.
int compare_string(char *str_tar,
char *str_src) {
if (str_tar == NULL ||
str_src == NULL) {
return 0;
}
unsigned int n_len_tar =
get_length_string(str_tar);
unsigned int n_len_src =
get_length_string(str_src);
if (n_len_tar != n_len_src) {
return 0;
}
if (n_len_tar == 0 &&
n_len_src == 0) {
return 1;
}
char *ptr_tar = str_tar;
char *ptr_src = str_src;
int is_same_string = 1;
while (*ptr_src != '\0' &&
*ptr_src != '\n') {
if (*ptr_tar != *ptr_src) {
is_same_string = 0;
break;
}
ptr_tar += 1;
ptr_src += 1;
}
return is_same_string;
}
먼저 두 문자열의 길이를 체크해야 합니다. 서로 같은 문자열이라면, 그 길이 역시 동일하죠. 다시말해서 문자열의 길이가 다르다면, 그건 서로 다른 문자열이라는 것입니다. 그 다음 두 문자열에 있는 문자들을 가리키기 위한 포인터 변수들인 ptr_tar 및 ptr_src가 선언되어 있는데요. 문자열 복사의 경우와 비슷하게 처음에는 문자열의 맨 첫글자를 가리키고, 다음 글자로 넘어가면서 두 개의 문자열에 있는 문자들이 서로 같은지 여부를 확인해 나가는 방식입니다.
간단한 예제 프로그램을 통해서 이 함수를 어떻게 사용할 수 있는지 살펴봅시다.
main 함수 코드입니다.
#include<stdio.h>
#define NCHAR_MAX_STRING 300
void verbose_compare(char *str_one,
char *str_two) {
int same_string =
compare_string(str_one, str_two);
fprintf(stdout, " %s ", str_one);
if (same_string != 0) {
fprintf(stdout, "is");
} else {
fprintf(stdout, "is not");
}
fprintf(stdout, " %s\n", str_two);
}
int main(int argc, char *argv[]) {
char *str =
(char *)malloc(sizeof(char) *
(NCHAR_MAX_STRING + 1));
fprintf(stdout, "\n");
fprintf(stdout, "Example 2\n");
copy_string(str, "apple",
NCHAR_MAX_STRING);
verbose_compare(str, "apple");
verbose_compare(str, "peach");
verbose_compare(str, "grape");
free(str);
return 0;
}
맨 먼저 2개의 문자열들을 비교합니다. 두 개가 서로 같으면 그 사이에 " is "를 출력하고, 그렇지 않으면 그 사이에 " is not "을 출력하죠. 프로그램을 실행하면 다음과 같은 결과를 얻을 수 있습니다.
문장으로부터 단어 추출
일반적인 문자열은 여러 개의 단어가 공백 (' ')이나 탭 ('\t', '\v')으로 분리된 형태를 가지고 있습니다. 이러한 문자열을 이루는 개별 단어들을 추출하여 서로 다른 문자열로 저장할 수 있으면 유용하겠죠. 이러한 기능을 구현하기 위한 함수를 하나 만들어 봅시다.
C언어 코드입니다.
char *extract_word(char *ptr_buf,
char *str_word) {
char *ptr_out = ptr_buf;
char *ptr_word = str_word;
unsigned int i_char = 0;
while (*ptr_out != '\0' &&
*ptr_out != '\n') {
if (*ptr_out != ' ' &&
*ptr_out != '\t' &&
*ptr_out != '\v') {
i_char += 1;
*ptr_word = *ptr_out;
ptr_word += 1;
} else if (i_char > 0) {
break;
}
ptr_out += 1;
}
*ptr_word = '\0';
if (i_char == 0) {
return NULL;
}
return ptr_out;
}
서로 분리된 여러개의 단어로 이루어진 문자열이 하나 주어져 있을 때, 그 중 임의의 문자나 공백을 가리키는 포인터 변수 ptr_buf를 인자로 받아갑니다. 그리고 다음 공백이나 탭이 나타날 때 까지, 마주치는 문자들을 차례대로 문자열 str_word에 저장하게 되죠. 그리고 단어 바로 다음에 있는 공백이나 탭의 주소를 가리키는 포인터를 리턴하는 방식입니다. 만약 문자열의 끝에 도달해서 더 이상 추출할 수 있는 단어가 없다면, 널 (NULL) 포인터를 리턴하고요. 이렇게 하면 extract_word 함수를 반복해서 호출함으로써, 문자열에 포함된 단어들을 차례대로 집어낼 수 있게 되겠습니다.
이제 간단한 예시를 하나 살펴봅시다.
main 함수 코드입니다.
#include<stdio.h>
#define NCHAR_MAX_STRING 300
int main(int argc, char *argv[]) {
char *str =
(char *)malloc(sizeof(char) *
(NCHAR_MAX_STRING + 1));
fprintf(stdout, "\n");
fprintf(stdout, "Example 3\n");
copy_string(str, "This is foo ~~~",
NCHAR_MAX_STRING);
fprintf(stdout,
" string : %s\n", str);
fprintf(stdout,
" words :\n");
char *ptr_buf_now;
char *ptr_buf_old;
ptr_buf_now = &str[0];
while (ptr_buf_now != NULL) {
char word[10];
ptr_buf_old = ptr_buf_now;
ptr_buf_now =
extract_word(ptr_buf_old,
word);
fprintf(stdout,
" %s\n", word);
}
free(str);
return 0;
}
앞서 언급한대로 extrac_word 함수를 반복해서 호출하면, 문장에 포함된 단어들이 서로 분리되어 저장되는 것을 볼 수 있습니다.
텍스트 파일을 이용한 셋업
프로그램을 초기화하는데 필요한 변수들을 텍스트 파일로부터 읽어들일 수 있게 하면 상당히 좋습니다. 물론 변수의 값들을 코드 안에 직접 적어 넣는 하드코딩을 해도 정상작동은 하겠지만, 다른 조건이나 상황을 상정해서 프로그램을 실행할 때 매우 번거롭겠죠. 초기화에 필요한 셋업 변수들의 값들을 텍스트 파일로부터 읽어들일 수 있도록 하는 것이 해결책이 될 수 있습니다. 예를 들어서 동역학 문제를 수치적으로 푸는 프로그램이라면, 물체의 질량, 초기조건 등을 텍스트 파일에서 지정하는게 있겠습니다.
C언어 코드입니다.
#define NCHAR_MAX_WORD 200
char *set_param_buf(char *str_in,
char *name_param) {
char *ptr_now;
char *ptr_old;
char word[NCHAR_MAX_WORD + 1];
ptr_old = str_in;
ptr_now = extract_word(ptr_old, word);
if (ptr_now == NULL) {
return NULL;
} else if (compare_string(word,
name_param) == 0) {
return NULL;
}
ptr_old = ptr_now;
ptr_now = extract_word(ptr_old, word);
if (ptr_now == NULL) {
return NULL;
} else if (compare_string(word,
"=") == 0) {
return NULL;
}
return ptr_now;
}
텍스트 파일로부터 프로그램을 초기화하는 기능을 구현하기 위한 기반이 되는 set_param_buf 함수입니다. 이 함수는 텍스트 파일에서 "[파라미터 이름] = [파라미터 값]"이라는 형식의 문자열이 있는 것을 상정하고 있는데요. 파일로부터 한 줄을 읽어들인 문자열과 파라미터 이름을 인자로 받고 있습니다.
이 함수를 호출하면 파일의 한 줄에 "[파라미터 이름]"이라는 문자열이 있는지를 먼저 확인합니다. 만약 있다면 공백이나 탭을 사이에 두고 등호 (=)가 따라오는지를 체크하죠. 그래서 등호가 있다면 등호 바로 다음의 공백의 주소를 포인터로 리턴합니다. 그래서 set_param_buf 함수를 extract_word 함수와 연계해서 사용하면, 등호 뒤에 등장하는 "[파라미터 값]"이라는 문자열도 얻을 수 있겠습니다.
예시를 위해서 다음과 같은 텍스트 파일을 준비합시다.
input.txt
######################
#
# This is test file,
# which is taken by
# a function to determine
# the setup parameters.
#
######################
# message to print
message = foo
# pi
const_pi = 3.1415
# Euler's number
const_e = 2.7183
End_of_data
위의 텍스트 파일로부터 "foo" 라는 문자열과 3.1415 및 2.7183 이라는 실수값을 읽어들이고, 서로 다른 변수들에 저장하는 프로그램을 만들어 볼텐데요. 여기서 구현하고 싶은 추가기능은 샵 (#)으로 시작되는 줄이 있다면, 이를 주석으로 간주하는 것입니다. 이를 위해서 beginning_with_char 이라는 별도의 함수를 작성했습니다.
int beginning_with_char(char *str_src,
char character) {
if (str_src == NULL) {
return 0;
}
char *ptr_src = str_src;
int begin_w_char = 0;
while (*ptr_src != '\0' &&
*ptr_src != '\n' &&
begin_w_char == 0) {
int is_empty = 0;
if (*ptr_src == ' ' ||
*ptr_src == '\t') {
is_empty = 1;
}
if (*ptr_src == character) {
begin_w_char = 1;
} else if (is_empty == 0) {
begin_w_char = 0;
break;
}
ptr_src += 1;
}
return begin_w_char;
}
이 함수는 파일로부터 한 줄을 읽은 문자열과, 주석으로 처리하는 기준이 되는 문자를 매개변수로 받고 있죠. 공백이나 탭을 제외하고 따졌을 때, 기준이 되는 문자가 맨 처음에 나오면 1을 리턴하고, 그렇지 않으면 0을 리턴합니다.
main 함수 코드입니다.
#include<stdio.h>
#define NCHAR_MAX_STRING 300
int main(int argc, char *argv[]) {
char *str =
(char *)malloc(sizeof(char) *
(NCHAR_MAX_STRING + 1));
char *fname_input =
(char *)malloc(sizeof(char) *
(NCHAR_MAX_WORD + 1));
if (argc > 1) {
copy_string(fname_input,
argv[1],
NCHAR_MAX_WORD);
} else {
copy_string(fname_input,
"input.txt",
NCHAR_MAX_WORD);
}
int have_message = 0;
char *message =
(char *)malloc(sizeof(char) *
(NCHAR_MAX_WORD + 1));
copy_string(message, "empty",
NCHAR_MAX_WORD);
int have_const_pi = 0;
double const_pi = 0.;
int have_const_e = 0;
double const_e = 0.;
FILE *ptr_fin =
fopen(fname_input, "r");
if (ptr_fin == NULL) {
fprintf(stderr,
"ERROR : %s not found.\n",
fname_input);
exit(1);
}
while (feof(ptr_fin) == 0) {
char *ptr_read =
fgets(str,
NCHAR_MAX_STRING,
ptr_fin);
if (ptr_read == NULL) {
break;
}
int is_comment =
beginning_with_char(str, '#');
if (is_comment != 0) {
continue;
}
int is_end_data =
compare_string(str, "End_of_data");
if (is_end_data != 0) {
break;
}
char *ptr_now;
char word[NCHAR_MAX_WORD + 1];
if (have_message == 0) {
ptr_now =
set_param_buf(str,
"message");
if (ptr_now != NULL) {
char *ptr_old = ptr_now;
ptr_now =
extract_word(ptr_old, word);
}
if (ptr_now != NULL) {
have_message = 1;
copy_string(message, word,
NCHAR_MAX_WORD);
}
}
if (have_const_pi == 0) {
ptr_now =
set_param_buf(str,
"const_pi");
if (ptr_now != NULL) {
char *ptr_old = ptr_now;
ptr_now =
extract_word(ptr_old, word);
}
if (ptr_now != NULL) {
have_const_pi = 1;
const_pi = atof(word);
}
}
if (have_const_e == 0) {
ptr_now =
set_param_buf(str,
"const_e");
if (ptr_now != NULL) {
char *ptr_old = ptr_now;
ptr_now =
extract_word(ptr_old, word);
}
if (ptr_now != NULL) {
have_const_e = 1;
const_e = atof(word);
}
}
}
fprintf(stdout, "\n");
fprintf(stdout,
"What we read from the file\n");
fprintf(stdout,
" message : %s\n", message);
fprintf(stdout,
" const_pi : %f\n", const_pi);
fprintf(stdout,
" const_e : %f\n", const_e);
fprintf(stdout, "\n");
fclose(ptr_fin);
free(message);
free(fname_input);
free(str);
return 0;
}
텍스트 파일을 한 줄씩 읽으면서, '#'으로 시작되는 부분은 주석으로 처리해서 건너뜁니다. 아무것도 없는 빈 줄 역시 건너뛰고 있죠. 그러다가 "message"라는 문자열을 발견하면, 그 뒤에 등호 (=)가 있는지를 확인합니다. 만약 있다면 등호 다음에 오는 문자열을 읽어들여서 프로그램의 변수에 저장하고 있습니다. "const_pi"와 "const_e"라는 문자열에 대해서도 동일한 방식으로 작동하여 등호 뒤에 오는 문자열을 읽은 다음, atof 함수를 통해 실수형 변수로 바꿉니다. 이 프로그램에는 등장하지 않지만 atoi 함수를 이용하면, 문자열을 정수형 변수로 변환할 수 있습니다.
프로그램을 실행해 보면 다음과 같은 결과를 얻을 수 있습니다.
이번 포스팅에서 사용한 함수와 프로그램들을 다음과 같이 정리해 봤습니다. 여러 개의 소스 파일과 헤더 파일이 있는데, 이들을 이용해서 프로그램을 빌드하는 과정에 대해서는 다음 포스팅을 참고하면 도움이 되리라 생각합니다.
문자열 함수들의 프로토타입이 선언된 헤더 파일입니다.
StringHandler.h [다운로드]
문자열 함수들이 정의된 소스 파일입니다.
StringHandler.c [다운로드]
위에서 언급한 3 개의 예시를 포함한 main 함수의 소스 파일입니다.
test0_str_example.c [다운로드]
마지막으로 텍스트 파일을 읽어서 파라미터를 저장하는 예시 프로그램의 소스 파일입니다.
test1_str_input.c [다운로드]
이상으로 C언어에서 간단한 함수들을 만들어보면서, 문자와 문자열을 처리하는 방법에 대해 짚어보았는데요. 프로그램을 실행할 때 커맨드 라인에서 임의의 문자열을 넘겨줄 수 있습니다. 이를 명령행 인자라고 부르며 이번 포스팅에서 소개된 함수들과 연계해서 사용하면 상당히 유용하겠죠. 명령행 인자에 대한 자세한 사항은 다음 포스팅에 소개되어 있습니다.
그리고 malloc 및 free 함수들이 호출되는 것도 확인할 수 있는데, 이들은 필요할 때 메모리 공간을 할당받아 변수를 저장하기 위한 목적으로 사용되는 함수들입니다. 동적 메모리 할당에 대해 더 자세한 내용이 궁금하시다면, 다음 포스팅을 한번 읽어보시기를 권합니다.