저번 포스팅까지는 포인터와 배열의 관계에 대해서 알아보았다.
오늘 알아볼 것은 상수 형태의 문자열을 가리키는 포인터에 대해서 알아보겠다.
1. 두 가지 형태의 문자열 표현
아래와 같이 배열을 기반으로 하는 문자열의 선언은 우리에게 익숙하다.
char str1 [ ] = "My String"; //배열의 길이는 자동으로 계산
위와 같이 str이라는 배열을 선언하고 그 배열을 "My String"라는 문자열로 초기화했다.
이는 배열을 기반으로 하는 '변수 형태의 문자열' 선언이다. 변수인 이뉴는 문자열의 일부를 변경할 수 있기 때문이다.
반면 다음과 같이 포인터를 기반으로 문자열을 선언하는 것도 가능하다.
char* str2 = "Your String";
위와 같이 선언을 하면 대입 연산이 진행되기 전에 문자열 "Your String"가 메모리 공간에 저장되고, 문자열의 첫 번째
주소 Y의 주소값이 반환된다. 그리고 그 반환값이 포인터 변수 str2에 저장된다. 그래서 str2를 char형 포인터로 선언한 이유이다. 왜냐하면 char형 문자 a의 주소 값이 저장되기 때문이다.
위의 그림에서 보듯이 str1은 그 자체로 문자열 전체를 저장하는 배열이고, str2는 메모리상에 자동으로 저장된 문자열 "Your String"의 첫 번째 문자를 단순히 가리키고만 있는 포인터 변수이다. 그러나 배열이름 str1이 의미하는 것도 실제로는 문자 M의 주소 값이기 때문에 str1도 str2도 문자열의 시작 주소 값을 담고 있다는 측면에서 동일하다.
다만 다음과 같은 차이가 있다.
str1은 문자열이 저장된 배열이다. 즉 문자 배열이다. 따라서 변수성향의 문자열이다.
str2는 문자열의 주소 값을 저장한다. 즉, 자동 할당된 문자열의 주소 값을 저장한다. 따라서 상수성향의 문자열이다.
하지만 배열이름인 str1은 상수형태의 포인터이기 때문에 위와 같이 가리키는 대상을 변경할 수 없다. 즉, str1은 계속해서 문자 M이 저장된 위치를 가리키는 상태여야 한다. 그러나, 포인터 변수 str2는 변수형태의 포인터이기 때문에 다른 위치를 가리킬 수 있다.
따라서, 총정리를 해보면 아래와 같다.
배열이름인 str1은 상수형태의 포인터이기 때문에 위와 같이 가리키는 대상을 변경할 수 없다. 그러나 위와 같이 문자열을 선언하면 문자열은 배열에 저장된다. 그리고 배열을 대상으로는 값의 변경이 가능하기 때문에 다음과 같이 선언되는 문자열을 가리켜 '변수 형태의 문자열'이라고 한다.
반면에 str2는 변수형태의 포인터이기 때문에 가리키는 대상을 변경할 수 있다. 그러나 위와 같이 str2가 가리키는 문자열은 그 내용의 변경이 불가능하다. 따라서 다음과 같이 선언되는 문자열을 가리켜 '상수 형태의 문자열'이라고 한다.
처음 보면 좀 많이 난감하고 헷갈릴 수 있다. 다음 예제를 같이 한번 살펴보도록 하자.
#include<stdio.h>
int main(void) {
char str1[] = "My String"; //변수 형태의 문자열
char* str2 = "Your String"; //상수 형태의 문자열
printf("%s %s\n", str1, str2);
str2 = "Our String"; //가리키는 대상 변경
printf("%s %s\n", str1, str2);
str1[0] = 'X'; //문자열 변경 성공
str2[0] = 'X'; //문자열 변경 실패
printf("%s %s\n", str1, str2);
return 0;
}
위의 예제는 컴파일은 가능하다. 하지만 str2 [0] = 'x' 여기서 문제가 발생한다.
왜냐하면 6행에서 선언한 문자열은 상수 형태의 문자열이기 때문이다. 간혹 상수 성향의 문자열 변경을 허용하는 컴파일러가 있으나, 이러한 형태의 변경은 바람직하지 못하다.
즉, 컴파일러의 특성에 관계없이 위 예제의 6행에서 보이는 형태로 선언된 문자열은 상수로 간주하여 그 값을 변경시키지 말아야 한다.
※어디서든 선언할 수 있는 상수 형태의 문자열
다음과 같이 선언되는 문자열은 상수 형태의 문자열이라 하였다.
char* str = 'Const String';
그렇다면 어떠한 순서를 거쳐서 이 문장이 처리가 될까? 위의 문장이 실행되면 먼저 문자열이 메모리 공간에 저장된다. 그리고 그 메모리의 주소 값이 반환된다. 즉, 문자열이 0x1234 번지에 저장되었다고 가정하면 위의 문장은 문자열이 저장된 이후 아래와 같은 형태가 된다.
char* str = 0x1234;
문자열이 먼저 할당된 이후에 그때 반환되는 주소 값이 저장되는 방식이다.
그렇다면 함수 호출과정에서 선언되는 문자열은 어떻게 처리가 될까?
printf("Show your string");
▼
printf(0x1234);
이 경우도 마찬가지로 문자열이 선언된 위치로 주소 값이 반환된다. 큰 따옴표로 묶여서 표현되는 문자열은 그 형태에 상관없이 메모리 공간에 저장된 후 그 주소 값이 반환된다. 따라서 위의 함수호출 문장도 메모리 공간에 문자열이 저장된 이후에 printf(0x1234); 와 같은 형태가 되는 것이다. (참고로 주소값은 0x1234라 가정한 것이다.)
이렇듯 printf함수는 문자열을 통째로 전달받는 함수가 아닌, 문자열의 주소 값을 전달받는 함수이다.
따라서 다음 함수의 호출문을 보면
WhoAreYou("Kim"); //WhoAreYou라는 이름의 함수호출
▼
void WhoAreYou(char* str){......}
이 함수의 매개변수 선언이 다음과 같음을 짐작할 수 있다. (반환형은 void라고 가정). 실제로 전달되는 값은 문자 K의 주소 값이기 때문이다.
2. 포인터 변수로 이뤄진 배열 : 포인터 배열
지금까지는 기본 자료형의 변수를 요소로 지니는 배열만 선언했다. 하지만 포인터 변수도 변수이니 이를 대상으로도 배열을 선언할 수 있다.
포인터 배열의 이해
포인터 변수로 이뤄진, 주소 값이 저장이 가능한 배열을 가리켜 '포인터 배열'이라 한다. 그리고 이러한 배열의 선언방식은 다음과 같다.
int* arr1 [20]; //길이가 20인 int형 포인터 배열 arr1
double* arr2 [30]; //길이가 30인 double형 포인터 배열 arr2
포인터 배열이라 해서 일반 배열의 선언과 차이가 나지는 않는다. 다만 변수의 자료형을 표현하는 위치에 int나 double을 대신해서 int* 나 double* 가 올뿐이다. 예시를 한번 살펴보자.
#include<stdio.h>
int main(void) {
int num1 = 10, num2 = 20, num3 = 30;
int* arr[3] = { &num1,&num2,&num3 };
for (int k = 0; k < 3; k++) {
printf("%d\n", *arr[k]);
}
return 0;
}
위 예제의 4행에 선언된 배열과 3행에 선언된 변수 num1, num2, num3의 관계를 그림으로 표현하면 다음과 같다.
문자열을 저장하는 포인터 배열
문자열을 저장하는 포인터 배열도 별반 다를 건 없다. 이는 문자열의 주소 값을 저장할 수 있는 배열로서 사실상 char형 포인터 배열이다. 따라서 여기서 말하는 문자열 배열은 다음과 같다.
char* strArr [3]; //길이가 3인 char형 포인터 배열
char형 포인터 배열은 문자열의 주소 값을 저장할 수 있는 배열이다 보니 문자열 배열로 불릴 뿐이다.
예제를 한번 살펴보자.
#include<stdio.h>
int main(void) {
char* strArr[3] = { "simple","string","array" };
for (int k = 0; k < 3; k++) {
printf("%s\n", strArr[k]);
}
return 0;
}
3행을 살펴보면 simple, string, Array.. 이것만 보면 최소 20Byte 이상은 필요해 보인다. 그러나 실제로 문자열을 저장할 수 있는 메모리 공간이 마련된 것은 아니다. 그저 char형 변수의 주소값 3개를 담을 수 있는 메모리공간만 마련되었다.
즉, 위의 문장이 실행되면 메모리공간에 일단 할당된 후 그 위치에 주소값을 반환받고 임의의 메모리 공간에 저장되면서 상수 형태를 띠는 것이다.
따라서 문자열이 저장된 이후에는 다음과 같은 형태가 된다.
char* strArr[3] = {0x1004, 0x1048, 0x2012}; //반환된 주소값은 임의로 결정한 것이다.
그런데 반환된 주소 값은 문자열의 첫 번째 주소 값이니, 이렇든 char형 포인터 배열에 저장이 가능한 것이다. 따라서 이 배열은 다음과 같은 구조가 된다.
결국 int형 포인터 배열의 그림과 비교해 보면, 구조적으로는 차이가 없다. 결국 포인터배열은 가리키는 대상의 차이는 있지만, 메모리 공간을 가리킨다는 점에서는 동일하다.
이로서 이번 포스팅에서는 상수형태의 문자열을 가리키는 포인터, 포인터 배열에 대해서 알아보았다.
오늘 배운 내용은 조금 솔직히 조금 까다롭고 어렵다. 앞선 포스팅하고 잘 연계해 가면서 잘 정리하길 바란다.
필자도 최대한 이해하기 쉽게 글을 작성하도록 노력하겠다:)
다음에는 포인터와 함수와 관련해서 들고 오도록 하겠다.
※(오타, 잘못된 내용, 틀린 내용 댓글로 알려주시면 정말 감사하겠습니다 :))
'C언어' 카테고리의 다른 글
C언어 - 다차원 배열 (0) | 2023.07.26 |
---|---|
C언어 - 포인터와 함수에 대한 이해 (0) | 2023.07.21 |
C언어 - 포인터와 배열 (0) | 2023.07.12 |
C언어 - 포인터의 이해(3) (0) | 2023.07.09 |
C언어 - 포인터의 이해(2) (0) | 2023.07.07 |