본문 바로가기
언어/C언어

[C] string.h 내장함수로 문자열 이항 계산기 만들기

by 배이즈 2025. 1. 19.

서론

숫자로 사칙연산 하는 것은 구현하기 어렵지 않을 수 있습니다. 하지만 피연산자가 '문자열'이라면? 대입연산자도 못 써서 strcpy를 이용해야하고, 문자열 또한 배열이고, 배열 또한 포인터이므로 포인터에 대한 심도 깊은 이해가 필요한 작업입니다.

저도 이 코드를 짜기 전에는 포인터에 대한 개념이 모호한 상태였고, 사실 아직도 여러 포인터가 나오면 뇌정지가 옵니다..

백문이 불여일타 렛츠고 (visual studio 2019 기준)

우선 필요한 라이브러리와 함수들을 선언합니다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void add(char*);
void subtract(char*);
void multiply(char*);
void divide(char*);
 

#define _CRT_SECURE_NO_WARNINGS 을 쓰는 이유는 버퍼 오버플로우 오류를 방지하기 위함인데요.

만약 이걸 쓰지 않으면

 
이런식으로 string.h 내장 함수가 실행이 안 되어서 빌드에 실패합니다. 꼭 작성해주시길..
 
그리고 함수를 호출하기 전에 선언을 통해 반환형을 미리 컴파일러에게 알려 반환값과 같은 형태의 저장 공간을 준비하도록 할 수 있습니다.

 

다음은 main함수입니다.

main함수의 기본 알고리즘은 다음과 같습니다.

 

  1. 사용자로 부터 문자열을 입력 받는다.
  2. 이항 연산자의 유무에 따라, 있다면 해당 연산자 함수를 호출하고, 없다면 계속해서 사용자 입력을 받는다.
  3. 연산자 함수를 호출할 때, 사용자 입력의 주소를 해당 함수의 매개변수(포인터)로 보낸다.
int main()
{
    char user_str[100] = { 0 };
    while (1) 
    {
        printf("\n문자열을 입력하세요: ");
        fgets(user_str, 99, stdin);
 

사용자 입력 문자열을 char형태의 배열 user_str로 할당해 주었습니다.

코드 진행이 끊기지 않고 진행되어야 하므로 하나의 루프 while(1)로 묶어줍니다.

 

fgets함수를 이용하여 입력을 받았습니다.

왜 scanf를 사용하지 않았냐

fgets() 는 공백 입력을 받지만, scanf() 는 입력을 공백을 기준으로 분리합니다. 즉 엔터 전 까지 읽어들입니다. 그래서 별도의 조건(%d %[^\n])을 넣어야하죠. 그러면 단어 하나를 입력받을 땐 편하겠지만, 여러 단어가 있는 문장을 받기엔 조금 번거롭겠죠?

 

그러나 fgets는 특별하게도 \n(enter)도 저장하고, 자동으로 \0으로 바꿔줍니다.

문자열에 특화된 이 함수를 두고 scanf를 쓴다? 오류의 스릴을 즐기시는 분이죠. 만약 주변에 그런 분이 계신다면 진정 코딩을 즐기시는 분입니다.

 

주의! gets함수는 버퍼 오버플로우의 위험이 있어 디버깅이 안될겁니다. 그러니 fgets함수를 사용해주세요.

int op_search = strchr(user_str, '+') || strchr(user_str, '-') || strchr(user_str, '*') || strchr(user_str, '/');
        
if (op_search == 1)
        {
            if (strchr(user_str, '+') != NULL)
            {
                add(&user_str[0]);
            }
            else if (strchr(user_str, '-') != NULL)
            {
                subtract(&user_str[0]);
            }
            else if (strchr(user_str, '*') != NULL)
            {
                multiply(&user_str[0]);
            }
            else if (strchr(user_str, '/') != NULL)
            {
                divide(&user_str[0]);
            }
        }
    }
    return 0;
}
 

논리연산자 || (OR)와 문자열에서 문자를 찾는 함수 strchr()을 사용해 연산자의 유무를 따집니다. 논리연산자는 0 또는 1의 값만을 반환하므로 op_search의 자료형을 int로 선언해주었습니다.

 

그리고 만약 op_search의 값이 1이다? 그럼 연산자가 하나 이상 존재한다는 것.

그럼 if문 안으로 들어가서 연산자에 맞는 함수 호출에 걸려 그 함수로 이동합니다. (user_str의 주소를 안고)

 

(원래는 switch문과 if문을 사용하여 구현하려고 했는데, 그러면 메인함수의 코드가 너무 길어질 것 같아서, 연산자 인식을 한 줄에 끝내고 싶은데..? 라는 생각에 논리 연산자를 사용했습니다.)

 

if문 안의 함수의 인수가 궁금하실겁니다. 왜 &user_str[0]이지?

처음에 전 그냥 주소에 의한 호출만 하면 된다고 생각해서 &user_str라고 작성했는데, 그러면 해당 함수로 이 주소값이 이동을 안 하는겁니다.

그래서 왜지? 생각해보니

&user_str은 배열 전체의 주소이고, user_str은 문자열이니까 - 포인터니까 - 배열이니까! 해당 배열의 첫번째 요소의 주소를 나타내는 것 입니다.

두 표현의 실제 값은 동일하지만 타입이 다르기 때문에 컴파일러 측에서 인식이 안되는겁니다.

그래서 전 &user_str[0]라고 했지만? 같은 의미인 user_str이라고만 써도 잘 작동하네요. 인자를 전달할 때 타입 체크 필수!

 

이제 각각의 연산자 함수로 넘어가 봅시다.

아! 그 전에 제가 구현한 4개의 연산자 함수는 공통점이 있는데 먼저 설명드리겠습니다. (매번 설명하기 번거로우니)

 

우선 저는 '이항연산'만 구현했습니다. 즉, 연산자를 기준으로 왼쪽과 오른쪽 항만 존재한다는 전제하에 봐주시면 됩니다.

위에 언급했듯이 연산자를 기준으로 왼쪽항과 오른쪽항을 나누어 배열에 저장했습니다.

다음은 subtract함수의 앞 부분 입니다.

char str[100] = { 0 }; 
strcpy(str, user_str);

char left_str[100] = { 0 };
char right_str[100] = { 0 };

char* pos_sub = strchr(str, '-');
size_t len_left = pos_sub - str;

strncpy(left_str, str, len_left);
strcpy(right_str, str + len_left + 2);
right_str[strlen(right_str) - 1] = '\0';
 

우선 main함수에서 전달받은 user_str을 새로운 배열인 str에 넣어줍니다.

그리고 왼쪽, 오른쪽 문자열을 담을 새로운 배열도 선언해줍니다.

 

pos_sub는 4개의 함수 모두 이름이 다른데, 그 이유는 이 포인터가 '연산자의 위치'를 나타내는 화살표이기 때문입니다.

subtract함수니까 pos_sub이겠죠? 찾는 건 strchr을 이용하여 찾아주면 됩니다.

 

이제 왼쪽, 오른쪽을 나눠야 하는데.. 어떤 함수를 쓸지 고민하다가..

strncpy는 특정 길이만큼만 복사할 수 있는 함수니까, 이걸 이용해서 왼쪽 문자열을 저장해야겠다는 생각이 들었습니다.

 

1. 우선 배열의 길이를 나타내는 음수가 아닌 자료형 size_t 를 이용하여 길이를 구합니다.

(B포인터보다 주소값이 큰)A포인터에서 B포인터를 빼면 그 사이만큼의 정수형 길이가 나오니까 pos_sub - str

즉 연산자의 위치에서 전체 문자열의 시작위치를 빼면 왼쪽 문자열+엔터 만큼의 길이가 들어갑니다.

 

2. 길이를 구했으니 strncpy를 사용할 수 있습니다.

left_str은 이걸 이용하여 쉽게 구할 수 있죠.

 

그러나 right_str은?

포인터는 화살표와 같으니까 가리키는 위치를 옮겨줄 수 있죠. 변수니까요.

 

3. 그래서 연산자와 엔터를 건너뛰어 오른쪽 문자열 시작점으로 화살표를 옮겨줄겁니다.

그 과정이 strcpy(right_str, str + len_left + 2); 인 것이고요.

문자열은 대입연산자를 못 쓰는데 저는 이 코드를 쓰면서 자꾸만 right_str = str + ( len_left+2)로 써서 적응하기가 좀.. 어려웠습니다..

 

4. 마지막에 right_str[strlen(right_str) - 1] = '\0';을 쓴 이유는

strstr함수를 사용할 때, 오른쪽 문자열이 왼쪽 문자열에서 검색이 안 되는 오류가 발생하여 디버깅을 해보니

right_str에 \n값이 함께 저장되어 있었습니다.

 

그래서! right_str 배열의 가장 마지막 요소인 \n을 \0으로 바꾸어주는 과정이 반드시 필요합니다.

1을 빼는 이유는 배열의 인덱스는 0부터 시작하지만, 길이는 1부터 시작하기 때문에 참조할 경우에는 길이에서 1을 빼줘야 내가 원하는 답이 나온답니다.

 

자 그럼 드디어..

각각의 연산자 함수들을 뜯어봅시다.

 

먼저 더하기 함수인 add() 입니다.

이 함수는 문자열 두 개를 붙여줍니다. 예를들어 apple + banana 라면 apple banana를 출력합니다.

void add(char* user_str)
{
    char str[100] = { 0 };
    strcpy(str, user_str);
    char left_str[100] = { 0 };
    char right_str[100] = { 0 };
    char* pos_plus = strchr(str, '+');
    size_t len_left = pos_plus - str;
    strncpy(left_str, str, len_left);
    strcpy(right_str, str + len_left + 2);

    //여기부터!
    strcat(left_str, right_str);
    printf("%s\n", left_str);
}
 

왼쪽, 오른쪽 문자열만 잘 나누어 주면 정작 남은 코드는 복잡하지 않습니다.

string.h 내장함수가 다 도와줍니다.

문자열 끝에 문자열을 붙여주는 strcat을 이용해 왼쪽과 오른쪽을 이어주기만 하면 됩니다.

 

다음은 빼기 함수인 subtract()입니다.

이 함수는 문자열에서 문자열을 빼줍니다. 예를 들어, apple banana apple - banana 이면 apple apple만 출력합니다.

 

가장 번거로웠던 subtract함수..

처음 도전한 알고리즘은 2차원 배열과 strtok을 이용하여 왼쪽 문자열의 모든 문자를 토큰으로 분리하여 2차원배열에 단어 째로 넣어서 검색하는 것이었습니다.

구현하려고 했던 오류 투성이 소스코드를 보여드리겠습니다.

char arr[10][100];
    int i = 0;

    char left_copy[100];
    strcpy(left_copy, left_str);
    char* pch = strtok(left_copy, " ");

    while (pch != NULL)
    {
        strcpy(arr[i], pch);
        pch = strtok(NULL, " ");
        i += 1;
    }

    char result[100] = { 0 };

    for (int j = 0; j < i; j++)
    {
        if (strcmp(arr[j], right_str) != 0)
        {
            strcat(result, arr[j]);
            strcat(result, " ");
        }
    }
    printf("%s\n", result);
 

strtok의 인수는 해당 인수를 변형한다고 해서 left_str의 손상을 막기 위해 카피본을 만들어서 분리해줬습니다.

그리고 캐릭터 포인터인 pch 를 선언하여 left_copy의 단어 분리를 시작할겁니다.

 

while문을 이용해 pch가 문자열의 끝인 NULL을 만나기 전까지 토큰들을 보관용 배열인 arr에 넣을겁니다.

strcpy를 이용해 분리된 토큰 하나하나를 배열에 하나씩 넣어줍니다.

 

그리고 빼기 연산을 해야하니까, right_str에 해당하는 단어가 있다면 그걸 빼고 출력해야합니다.

그러면 right_str 단어가 아닌 경우에만 출력용 배열인 result에 담으면? right_str을 제외한 left_str이 나오겠다! 생각했습니다.

2차원 배열에 단어들이 담기긴 하지만, 출력이 계속 안되어서 결국 방법을 바꾸었습니다..

 

생각해보니 토큰으로 분류하면, programmers에서 pro만 빼고싶은 경우에는 연산이 안되는 한계가 있더군요.

그래서 생각한 다른 알고리즘은 다음과 같습니다.

 

strncat을 이용해 길이로 잘라 붙이기 방법입니다.

void subtract(char* user_str)
{
    char str[100] = { 0 };
    strcpy(str, user_str);
    char left_str[100] = { 0 };
    char right_str[100] = { 0 };
    char* pos_sub = strchr(str, '-');
    size_t len_left = pos_sub - str;
    strncpy(left_str, str, len_left);
    strcpy(right_str, str + len_left + 2);
    right_str[strlen(right_str) - 1] = '\0';

    //여기부터!!
    char res_str[100] = { 0 };

    while (1) {
        if (strstr(left_str, right_str) != NULL)
        {
            strncat(res_str, left_str, strstr(left_str, right_str) - left_str);
            strcpy(left_str, strstr(left_str, right_str) + strlen(right_str));
        }
        else {
            strcat(res_str, left_str);
            break;
        }
    }
    printf("%s\n", res_str);
}
 

left_str에서 right_str을 계속해서 찾습니다.

해당 문자열을 찾으면, 결과 문자열인 res_str에 left_str에서 strstr(left_str, right_str) - left_str길이만큼 저장합니다.

예를 들어 설명드리겠습니다.

 

위의 왼쪽 오른쪽 분리 알고리즘에서 말씀드린 길이 구하는 방법!

(B포인터보다 주소값이 큰)A포인터에서 B포인터를 빼면 그 사이만큼의 정수형 길이가 나온다 -> 를 이용한건데요.

 

C programming is hard programming - program 이라면

현재 left_str은 C를 가르키고 있고, right_str은 첫번째 p를 가르키고 있습니다.

그럼 strstr(left_str, right_str)은 첫번째 p의 위치겠죠.

 

첫번째 p의 위치에서 C의 위치를 빼면?

그 사이만큼의 정수형 길이, 즉 "C " 만큼의 길이가 나옵니다.

즉 처음 while 반복을 돌리면 res_str에 "C "가 들어갑니다.

 

자, 그럼 다음 검색을 위해 포인터를 옮겨야 겠죠? 그렇지 않으면 한 문자열에서 여러 문자열을 뺄 때 작동이 안되겠죠.

그 과정이 strcpy(left_str, strstr(left_str, right_str) + strlen(right_str)); 입니다.

 

strstr(left_str, right_str)은 첫번째 p의 위치입니다.

strlen은 right_str 문자열의 길이를 반환합니다.

 

이 과정은 왼/오 나누기 과정에서 사용했던 포인터 화살표 옮기기 과정과 같습니다. 검색 대상인 left_str도 포인터니까!

위의 예시를 적용해보면, 다음 검색부턴 p의 위치에서 7(program의 길이)만큼 오른쪽으로 옮긴 위치부터 검색을 시작합니다.

디버깅을 해보면 당시 left_str에는 ming is hard programming 이 저장되어 있습니다.

 

같은 방법으로 그 다음엔 ming 부터 검색할 텐데 그럼 이제 더 이상 right_str에 해당하는 문자열이 없습니다.

그럼 if을 빠져나와 else로 갑니다. 남은 left_str인 "ming"을 마지막으로 res_str의 끝에 붙여주면

결과적으로 우리가 원하는 program이라는 문자열이 빠진 "C ming is hard ming"이 출력됩니다.

 

그 다음은 곱하기 함수인 multiply입니다.

이 함수는 문자열을 원하는 횟수만큼 반복하여 출력합니다. 예를 들어, apple * 5 이면 apple apple apple apple apple이 출력됩니다.

void multiply(char* user_str)
{
    char str[100] = { 0 };
    strcpy(str, user_str);
    char left_str[100] = { 0 };
    char right_str[100] = { 0 };
    char* pos_mul = strchr(str, '*');
    size_t len_left = pos_mul - str;
    strncpy(left_str, str, len_left);
    strcpy(right_str, str + len_left + 2);

    //여기부터!!
    int repeat = atoi(right_str);

    for (int i = 0; i < repeat; i++)
    {
        printf("%s", left_str);
    }
}
 

subtract보단 월등히 쉬운 코드입니다.

연산자 오른쪽 부분이 문자열이 아닌 정수이기 때문에

문자 형태를 정수 형태로 바꾸어주는 ascii to integer 함수를 이용하여 right_str을 정수형으로 만들어줍니다.

이는 즉 반복 횟수이므로 int형 변수인 repeat를 만들어서 for문으로 반복 출력하면 끝

 

다음은 나누기.. 라기 보단 개수 찾기 함수인 divide 입니다.

이 함수는 문자열에서 원하는 문자열의 포함 개수를 알려줍니다. 예를 들어, apple banana apple / apple 이면 2가 출력됩니다.

void divide(char* user_str)
{
    char str[100] = { 0 };
    strcpy(str, user_str);
    char left_str[100] = { 0 };
    char right_str[100] = { 0 };
    char* pos_div = strchr(str, '/');
    size_t len_left = pos_div - str;
    strncpy(left_str, str, len_left);
    strcpy(right_str, str + len_left + 2);
    right_str[strlen(right_str) - 1] = '\0';

    //여기부터!!
    char* search = strstr(left_str, right_str);
    int num = 0;

    while (search != NULL)
    {
        num += 1;
        search = strstr(search + 1, right_str);
    }
    printf("%s에서 %s의 개수는 %d개 입니다.\n ", left_str, right_str, num);
}
 

left_str에서 right_str이 나올 때마다 num을 1씩 증가시켜 마지막으로 누적된 num을 출력하면 left_str에 포함된 right_str의 개수를 알 수 있습니다.

while문을 이용했고, 중요한 부분은 포인터 옮겨주기 입니다.

(포인터 위치를 옮겨주는 것은 검색 로직에서 필수적인 과정이라는 걸 이번 프로젝트를 통해 뼈저리게 느꼈습니다)

 

search와 strstr는 포인터 형식이므로 대입연산자를 이용하였고,

right_str을 찾을 때 마다 search + 1을 해줘서 그 이후부터 다시 검색하도록 했습니다.

 

 

항상 GPT의 크고 작은 도움을 받았어서 C언어를 배웠다고 하기도 부끄러울 정도로 지식이 부족했습니다. 오래 걸리고 머리가 아프더라도 직접 코드를 계획해보고 실현해보는 이 과정이 그 어떤 이론 수업보다도 도움이 많이 되었습니다. 이 경험을 제가 할 거라곤 생각을 못했는데요.. (왜냐면 귀찮으니까..) 특히 C언어는 파이썬과 달리 너무도 컴퓨터 메모리와 직결된 언어라서, 조금의 실수도 용납하지 않아 초보자에겐 엄청난 스트레스를 주는 언어라고 생각합니다.. 하지만 그만큼 제가 원하는 대로 프로그램이 돌아갔을 때의 쾌감은 참 짜릿하네요.. ㅎㅎ 이제야 포인터, 배열, 문자열을 배운 병아리의 얄팍한 느낀점이었으며 앞으로 더 광활한 C언어의 세상을 탐험해보고 싶습니다. 귀찮은 과정인줄 알았던 디버깅은 이번 프로젝트에서 저에게 가장 큰 도움이 되어준 나만의 GPT 였습니다. 왜 출력이 안되지? 싶으면 디버깅을 합니다. 그러면 어디가 문제인지 메모리에 저장된 값을 보면 알 수 있습니다.

 

이상 저의 첫 글을 마무리 해보겠습니다.

좀 길었지만, 다 쓰고 나니 굉장히 뿌듯하군요. 모두들 소소한 행복을 느끼시길 바랍니다 ♬

 

 

 

최근댓글

최근글

skin by © 2024 ttuttak