
학교 프로그래밍 수업에서는 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*이기도 하다. 이 내용을 언젠가 배웠었겠지만 까맣게 잊고 있었다.
위 문제의 코드에서 내가 사용한 방법이다.
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;
}
무식해보이지만 효과적인 방법이다.

char fruits[m][n]과 같이 최대 길이가 n인 문자열 m개, 즉 m x n 짜리 2차원 배열을 만들어주면 된다.
한 가지 단점은 위 그림과 같이 저장하고자 하는 문자열의 길이가 저장할 수 있는 최대 길이보다 짧다면 남는 바이트가 있다는 것이다. 그러나 이 경우 배열의 내용물인 문자 자체가 stack에 저장되어 변경이 가능하다는 장점이 있다.
약간 꿀팁(?)같은건, 매번 2차원 배열을 다루긴 힘드니 단어 하나씩을 변수에 넣어주면 [ ]를 한 번만 쓸 수 있다.
문자열 배열이 필요할 때...
포인터 배열은 수정이 안 된다.
2차원 배열은 메모리는 조금 더 차지하지만 수정이 가능하다.
첫번째 사진은 https://www.geeksforgeeks.org/memory-layout-of-c-program/ 에서, 두번째 사진은 교수님 강의자료에서 가지고 왔다.
이 부분은 다음에 기회가 될 때 다루도록 하겠다.