C언어에서 문자열 배열을 다루는 두 가지 방법 (feat. Bus Error)

이강록·2023년 11월 17일

C언어

목록 보기
1/1
post-thumbnail

사건의 발단

학교 프로그래밍 수업에서는 C언어만을 사용한다. 평소 JavaScript와 같이 너그러운 언어를 주로 사용하다 보니 C언어에서 문자열 다루는 것은 거의 고통에 가깝다. 2023년이 되던 겨울에 C언어를 독학했고, 당시에는 꽤 깊이있는 공부를 했다고 자부했지만 벌써 1년 가까이 지난 지금, 포인터에 대해 상당히 무뎌져 있었다. 그러던 중에 수업 실습 과제로 문자열의 배열을 만들어 manipulate해야 했는데, 나는 bus error를 마주하게 되었고, 왜 안 되는지 이해할 수 없었다. "zsh: bus error"라고만 뜰 뿐, 어느 줄에서 오류가 났는지 등 아무런 정보도 확인할 수 없었다. segfault는 많이 봤지만 이건 처음이었다.

문제의 코드

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int i;
    char input;

    char * fruits[5] = { "apple", "orange", "banana", "grape", "kiwi" };

    printf("초기 데이터: \n");
    for(i=0;i<5;i++)
    {
        printf("%s ", fruits[i]);
    }
    printf("\n");
    printf("\n");

    printf("알파벳 입력: ");
    scanf("%c", &input);

    for(i=0;i<5;i++)
    {               
        char * thisfruitletter = fruits[i]; //각 fruit의 첫문자

        while(*thisfruitletter) { //널 문자 도달하면 x
            if (*thisfruitletter == input) {
                *thisfruitletter = toupper((unsigned char)*thisfruitletter);
            }
            thisfruitletter++;
        }
    }

    for(i=0;i<5;i++)
    {
        printf("%s ", fruits[i]);
    }
    printf("\n");

    return 0;
}

문자열 배열을 초기화하고, 사용자에게 알파벳을 하나 입력받은 후, 문자열에서 해당 알파벳이 포함된 문자는 모조리 대문자로 바꿔주는 간단한 코드다.

나는 그 다음 날이 되어서야 오류가 난 이유를 알게 되었다. ChatGPT가 말해주길, char * fruits[5] 방식으로 문자열 배열을 초기화를 하면, 문자 리터럴로 저장되어 read-only기 때문에 변경할 수 없다는 것이다. 그제서야 다른 velog 글에서 리터럴 상수를 바꾸려할 때 bus error가 나타난다는 내용이 생각났다. char * fruits[5]는 결국 프로그램 메모리 어딘가에 문자열을 5개 만들어 놓고, 문자열 5개의 주소만을 각각 fruits라는 배열에 저장하는 것에 불과하다. 그래서 fruits의 type이 char*이기도 하다. 이 내용을 언젠가 배웠었겠지만 까맣게 잊고 있었다.

문자열 배열을 만드는 2가지 방법

1. 포인터 배열

위 문제의 코드에서 내가 사용한 방법이다.
char * fruits[5] = { ... } 을 사용함으로써 나는 문자열 5개를 '어딘가'에 저장하고, 그 문자열들의 주소, 즉 포인터로 fruits라는 배열을 만들었다.
그 '어딘가'는 대체 어디란 말인가?

바로 밑 그림의 가장 아래에 존재하는 text segment에 있다. 이 부분은 프로그램의 instruction이 존재하는 부분으로, 프로그램 실행 중에 자신의 내용이 바뀌어버린다면 심각한 오류를 부를 수 있기 때문에 read only이다. 리터럴로 문자열들이 이 부분에 저장되기에 fruits의 개별 문자열을 수정하려고 하면 undefined된 오류가 발생하고, 운영체제와 컴파일러에 따라 bus error로 표현되기도 한다고 한다.

반면 오해하면 안 되는 것이, fruits라는 배열 자체는 수정가능하다. fruits는 엄연히 stack에 존재하기 때문이다. (다르게 생각하면 main함수 밖에서 global이나 static등으로 선언되지도, 동적으로 메모리에 할당되지도 않았으니 stack일 수 밖에) 실제로 아래 코드처럼 fruits의 마지막 포인터를 다른 문자열로 바꿔도 아무런 문제가 없다.

#include <stdio.h>

int main(void)
{
    char * fruits[5] = { "apple", "orange", "banana", "grape", "kiwi" };
    char * mango = "mango";

    fruits[4] = mango;
    return 0;
}

2. 2차원 배열

무식해보이지만 효과적인 방법이다.

char fruits[m][n]과 같이 최대 길이가 n인 문자열 m개, 즉 m x n 짜리 2차원 배열을 만들어주면 된다.
한 가지 단점은 위 그림과 같이 저장하고자 하는 문자열의 길이가 저장할 수 있는 최대 길이보다 짧다면 남는 바이트가 있다는 것이다. 그러나 이 경우 배열의 내용물인 문자 자체가 stack에 저장되어 변경이 가능하다는 장점이 있다.

약간 꿀팁(?)같은건, 매번 2차원 배열을 다루긴 힘드니 단어 하나씩을 변수에 넣어주면 [ ]를 한 번만 쓸 수 있다.

요약 및 추후 공부

문자열 배열이 필요할 때...
포인터 배열은 수정이 안 된다.
2차원 배열은 메모리는 조금 더 차지하지만 수정이 가능하다.

첫번째 사진은 https://www.geeksforgeeks.org/memory-layout-of-c-program/ 에서, 두번째 사진은 교수님 강의자료에서 가지고 왔다.

아니 그래서 포인터를 꼭 써야해?

이 부분은 다음에 기회가 될 때 다루도록 하겠다.

profile
점심뭐먹지

0개의 댓글