앞서 포인터에 대한 기초 개념을 간단하게 살펴보았다.
포인터가 어렵다고 많이 얘기하지만, 사실 그 악명에 비해 포인터의 개념자체는 크게 어렵지 않다.
포인터와 배열의 관계를 살펴보기에 앞서 다음 내용을 읽어보고 가도록하자.
배열의 또 다른 이름은 포인터이다.
정확하게는 그 값을 바꿀 수 없는 상수형 포인터이다.
#포인터와 배열의 관계
다음 코드를 먼저 살펴보자.
#include <stdio.h>
int main()
{
int arr[3] = { 1, 2, 3 };
printf("배열의 이름 : %p \n", arr); // 0000001DD735F838
printf("배열의 첫번쨰 원소 : %p \n", &arr[0]); // 0000001DD735F838
printf("배열의 두번쨰 원소 : %p \n", &arr[1]); // 0000001DD735F83C
printf("배열의 세번쨰 원소 : %p \n", &arr[2]); // 0000001DD735F840
// arr = &arr[1]; 이 문장은 컴파일 에러를 일으킨다
return 0;
}
위 코드에서 우리가 주목해야 할 것은 두 가지이다.
첫 번째로, 배열의 이름과 배열의 첫번쨰 원소의 주소 값이 동일한 것이 보이는가?
이것이 의미하는 바는 배열의 이름은 배열의 시작 주소 값을 의미한다는 것과 완전히 동일하다.
또한 arr = &arr[1]이 컴파일 에러를 일으키는 것으로 보아, 배열의 이름은 값의 저장이 불가능한 상수라는 것을 알 수 있다.
두 번째로, 배열의 원소간 주소의 간격이 int형이 할당받는 메모리 크기인 4byte로 동일한 것을 볼 수있다.
즉, 모든 배열요소가 메모리 공간에 나란히 할당된다는 사실을 알 수 있다.
정리하면 배열은 포인터와 같이 이름이 존재하며, 특정 메모리 공간의 주소를 나타내거나 저장한다.
단 한가지 차이점은 포인터 변수는 그 이름에서 나타나듯이 "변수"이므로 그 값을 바꿀 수 있는 반면, 배열은 그 대상의 변경이 불가능한 상수이다.
그래서 배열을 "상수 형태의 포인터" 라고 부를 수 있는 것이다.
#포인터 연산과 이를 이용한 배열접근
포인터 변수를 대상으로 여러가지 형태의 연산을 진행할 수 있다. 다음 코드를 살펴보자.
#include <stdio.h>
int main()
{
int A = 5; // int형 변수 A에 5를 초기화.
int* ptr = 0x0004; // 포인터에 0x0004라는 주소 값을 초기화
// 포인터의 적절한 초기화는 아니나, 이해를 돕기위해 임의로 위와 같이 초기화를 시켰다.
printf("%d %d %d\n", A, A + 1, A + 2); // 5, 6, 7
printf("%p %p %p \n", ptr, ptr + 1, ptr + 2); // 0x0004 0x0008 0x000C
return 0;
}
위 코드에서 A와 같이 int 자료형을 대상으로 증가연산을 해주면 그 정수값 자체가 변하는 것과 달리,
포인터를 대상으로 증가연산을 해주었을때는 메모리 공간의 주소값이 정확히 4byte씩 증가하는 것이 보이는가?
(int형 포인터를 대상으로 n 증가시켰으므로 n x 4byte 의 크기만큼 메모리 주소 값이 증가)
포인터 변수는 정수나 실수값을 저장하는 것이 아닌, 메모리 공간의 주소 값을 저장하는 변수이니 사실은 당연한 말이다.
위와 같은 포인터의 연산특성으로 인해서 다음 형태의 배열접근이 가능하다.
#include <stdio.h>
int main()
{
int arr[3] = {11, 22, 33};
int* ptr = arr; // int *ptr = &arr[0]; 과 완벽히 동일한 표현
printf("%d %d %d\n", arr[0], arr[1], arr[2]); // index를 이용한 배열 접근 // 11 22 33
printf("%d %d %d\n", *ptr, *(ptr + 1), *(ptr + 2)); // 포인터 연산을 이용한 배열 접근 // 11 22 33
printf("%d %d %d\n", *arr, *(arr + 1), *(arr + 2)); // 배열의 이름을 이용한 배열 접근 // 11 22 33
for (int i = 0; i < 3; i++) {
printf("%d ", *ptr);
* ptr++; // 증감연산자를 사용한 배열 접근 // 11 22 33
}
return 0;
}
위 코드의 출력 값은 모두 동일하다. 배열이 포인터의 특성을 갖기 때문에 포인터 연산을 배열에도 동일하게 적용 가능한 것이다. 결론적으로, arr[i] == *(arr + i) 이다.
#증감연산자에서 ()의 위치에 따른 값의 변화
++나 --와 같은 증감연산자에서 ()의 위치는 매우 중요하다.
() 위치가 조금만 달라져도, 그 식의 의미하는 바는 완전히 달라지므로 확실하게 정리하고 넘어가는 것이 좋다.
(개인적으로 굉장히 헷갈리는 부분이였다)
포인터 연산의 특성을 떠올리면 다음의 표를 쉽게 이해할 수 있을 것이다.
#이중 포인터
포인터의 포인터인 이중 포인터 변수에 대해 간단하게 알아보자.
포인터 변수는 종류에 상관없이, 무조건 주소 값을 저장하는 변수이다.
따라서 이중 포인터도 어떤 주소 값을 저장하는 하나의 변수일뿐이다.
다시 말해, 이중 포인터는 어떤 포인터 변수의 주소값을 저장할 뿐이다.
다음의 코드를 살펴보면 쉽게 이해가 될 것이다.
#include <stdio.h>
int main()
{
int a = 10;
int* ptr = &a;
int** dp = &ptr; // 포인터 변수 ptr의 주소 값을 저장하는 이중 포인터 변수이다.
printf("%d %d %d", a, *ptr, **dp); // 10 10 10
return 0;
}
#포인터 배열
포인터 배열에 대해 알아보자. 우리가 int형 변수에 배열을 선언했듯이, 포인터 변수도 변수이므로 마찬가지로 배열을 선언할 수 있다. 계속해서 얘기했지만 포인터도 하나의 변수이다. 다음의 코드를 살펴보자.
#include <stdio.h>
int main()
{
int a[3] = { 11, 22, 33 };
int* ptr[3];
ptr[0] = &a[0];
ptr[1] = &a[1];
ptr[2] = &a[2];
int** dp[3] = { &ptr[0], &ptr[1], &ptr[2] }; // 이중 포인터의 배열 선언
//int** dp[3] = { ptr , ptr + 1, ptr + 2 }; 11행과 완벽히 동일 한 코드
printf("%d %d %d\n", *ptr[0], *ptr[1], *ptr[2]); // 11 22 33
printf("%d %d %d", **dp[0], **dp[1], **dp[2]); // 11 22 33
return 0;
}
ptr이라는 포인터 변수에 배열을 선언해주었고, 변수 a의 주소값을 각 인덱스에 맞게 저장해주었다.
그리고, dp라는 이중포인터 변수에 배열을 선언함과 동시에 마찬가지로 포인터 변수 ptr의 주소값을 저장해주었다.
ptr이 참조하는 값은 연산자 *를 이용하여 알 수 있고, 이는 코드 결과에서 볼 수 있듯이 변수 a에 저장된 값과 동일하다. 또한 dp가 참조하는 값은 ptr이 참조하는 값이고 이는 변수 a에 저장된 값이다.
다중 포인터의 경우에는 꼬리물기와 같이 연쇄적으로 작용하는 것으로 이해함이 편하겠다.
이처럼 포인터 변수도 변수이므로 다른 변수와 같이 배열을 선언할 수 있다.
#2차원 포인터 배열
간단하게 2차원 배열 포인터의 선언과 이용에 대해 알아보자.
#include <stdio.h>
int main()
{
int ary[][3] = { {11, 22, 33}, {44, 55, 66}, {77, 88, 99 } };
int(*dp)[3] = ary; // 이차원 포인터 배열의 선언. ()는 꼭 필요하다!
printf("%d %d %d \n", dp[0][0], dp[0][1], dp[0][2]); // 11 22 33
return 0;
}
위 코드에서 볼 수 있듯이 1차원 배열 포인터의 선언에서 차이나는 것은 괄호가 추가되었다는 점과, 그 뒤에
[3]과 같이 이차원 배열의 열을 함께 표시해주는 것말고는 차이나는 것이 없다.
'C' 카테고리의 다른 글
[C] 포인터의 기초 개념 (0) | 2022.05.12 |
---|