
1. 포인터와 배열의 관계
배열의 이름은 포인터(변수)이다.
단, 그 값을 바꿀 수 없는 '상수 형태의 포인터(변수)'이다.
int arr[3]={1,2,3};
printf("%p", arr);
printf("%p", &arr[0]);
위의 출력 값은 같으므로, arr과 arr[0]의 주소 값은 같다.
우리가 선언한 배열이 int형 배열이므로 각 요소 별로 할당되는 메모리 공간의 크기는 4바이트이다.
따라서 int형 배열요소간 주소 값의 차는 4바이트이다.
배열의 이름은 대입 연산자의 피연산자가 될 수 없으므로(값의 저장이 불가능하므로) 다음의 결론을 내릴 수 있다.
"배열의 이름은 배열의 시작 주소 값을 의미하며, 그 형태는 값의 저장이 불가능한 상수이다."
배열의 이름이나 포인터 변수나 둘 다 이름이 존재하며, 특정 메모리 공간의 주소값을 지닌다.
다만 포인터 변수는 그 이름이 의미하듯이 변수지만, 배열의 이름은 가리키는 대상의 변경이 불가능한 상수라는 점에서만 차이를 보인다.
즉 배열의 이름은 '상수 형태의 포인터'이다.
그래서 배열의 이름을 가리켜 '포인터 상수'라 부르기도 한다.
"배열의 이름도 포인터라면 포인터 변수를 대상으로 하는 * 연산도 가능한가요?"
배열의 이름도 포인터이기 때문에 배열의 이름을 피연산자로 하는 * 연산이 가능하다.
사실 배열의 이름과 포인터 변수는 변수냐 상수냐의 특성적 차이가 있을 뿐, 둘 다 포인터이기 때문에 포인터 변수로 할 수 있는 연산은 배열의 이름으로도 할 수 있고, 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있다.
int arr[3]={1,2,3};
int* ptr=arr;
printf("%d, %d", ptr[0], arr[0]);
2. 포인터 연산
포인터 변수를 대상으로 다양한 형태의 증가 및 감소연산을 진행할 수 있다.
int* ptr1=0x0010;
double* ptr2=0x0010;
printf("%p %p\n", ptr1+1, ptr1+2); // 4가 증가하고 8이 증가한다.
printf("%p %p\n", ptr2+1, ptr2+2); // 8이 증가하고 16이 증가한다.
int형 포인터를 대상으로 1을 증가시키면 4가 증가하고 double형 포인터를 대상으로 1을 증가시키면 8이 증가한다.
int num=20;
int* ptr=#
*(++ptr)=20;
*(ptr+1)=20;
*(++ptr)은 ptr에 저장되어 있는 주소 값 자체를 1증가시켜 20을 저장하는 것이고, *(ptr+1)은 이 문장에서만 1증가시켜 20을 저장하는 것이다.
int arr[3]={1,2,3};
int* ptr=arr;
printf("%d, %d, %d, %d", *(ptr+1), ptr[1], *(arr+1), arr[1]);
위 출력은 모두 같은 값을 보인다.
3. 상수 형태의 문자열을 가리키는 포인터
마지막에 널 문자가 삽입되는 문자열의 선언방식에는 두 가지가 있다.
하나는 전에 설명한 배열을 이용하는 방식이다.
그리고 다른 하나는 char형 포인터 변수를 이용하는 방식이다.
char str1[]="Hello";
이는 배열을 기반으로 하는 '변수 형태의 문자열' 선언이다.
변수라 하는 이유는 문자열의 일부를 변경할 수 있기 때문이다.
반면 다음과 같이 포인터를 기반으로 문자열을 선언하는 것도 가능하다.
char* str2="Hello";
이렇게 선언을 하면 메모리 공간에 문자열 "Hello"가 저장되고, 문자열의 첫 번째 문자 H의 주소값이 반환된다.
그리고 그 반환 값이 포인터 변수 str2에 저장된다.
str1도 str2도 문자열의 시작 주소 값을 담고 있다는 측면에서는 동일하다.
다만 다음의 차이가 있을 뿐이다.
"str1은 계속해서 문자 H가 저장된 위치를 가리키는 상태여야 하지만 포인터 변수 str2는 다른 위치를 가리킬 수 있다."
str2는 변수형태의 포인터이기 때문에 다음과 같은 문장이 가능하다.
char* str2 = "Hello";
str2 = "World";
하지만 str1은 상수형태의 포인터이기 때문에 위와 같이 가리키는 대상을 변경할 수 없다.
그리고 또 한 가지 중요한 차이점은, 아래와 같이 선언이 되면 애초에 문자열은 배열에 저장된다.
그리고 배열을 대상으로는 값의 변경이 가능하기 때문에 다음과 같이 선언되는 문자열을 가리켜 앞서 말했듯이 '변수 형태의 문자열'이라 한다.
char str[] = "Hello";
반면, 다음과 같이 선언되는 문자열은 '상수 형태의 문자열'이라 한다.
char* str = "Hello";
다음과 같이 선언이 되는 문자열은 상수 형태의 문자열이라 하였다.
char * str = "Const String";
그렇다면 어떠한 순서를 거쳐서 이 문장은 처리가 될까?
위의 문장이 실행되면 먼저 문자열이 메모리 공간에 저장된다.
그리고 그 메모리의 주소 값이 반환된다.
즉 문자열이 0x1234번지에 저장되었다고 가정하면, 위의 문장은 문자열이 메모리 공간에 저장된 이후에 다음의 형태가 된다.
char * str = 0x1234;
그리하여 포인터 변수 str에는 문자열의 주소 값 0x1234가 저장되는 것이다.
그렇다면 다음과 같이 함수의 호출과정에서 선언이 되는 문자열은 어떻게 처리가 될까?
printf("Hello World");
이 경우도 마찬가지이다.
큰따옴표로 묶여서 표현되는 문자열은 그 형태에 상관없이 메모리 공간에 저장된 후 그 주소 값이 반환된다.
따라서 위의 함수호출 문장도 메모리 공간에 문자열이 저장된 이후에 다음의 형태가 된다.
printf(0x1234);
이렇듯 printf 함수는 문자열을 통째로 전달받는 함수가 아닌, 문자열의 주소 값을 전달받는 함수이다.
따라서 다음의 함수 호출문을 보면,
Func("Hong");
이 함수의 매개변수 선언이 다음과 같음을 짐작할 수 있다.
실제로 전달되는 값은 H의 주소 값이기 때문이다.
void Func(char * str)
{
...
}
4. 포인터 변수로 이뤄진 배열: 포인터 배열
포인터 변수로 이뤄진, 그래서 주소 값이 저장이 가능한 배열을 가리켜 '포인터 배열'이라 한다.
int num1, num2, num3;
int* arr[3]={&num1, &num2, &num3};
printf("%d, %d, %d", *arr[0], *arr[1], *arr[2]); // arr[i]가 주소값이므로 *연산으로 접근가능
이를 이용해서 '문자열 배열'을 만들 수 있다.
char* arr[3]={"Hello", "World", "Wow"};