C언어 - (11) : 포인터와 배열
배열과 포인터에는 공통점과 차이점이 존재한다.
간단한 예시로 배열과 포인터의 공통점, 차이점에 대해 살펴볼 수 있다.
#include <stdio.h>
int main(void)
{
int arr[3] = {0,1,2};
printf("배열의 주소 : %p \n", arr);
printf("첫번째 요소의 주소 : %p \n", &arr[0]);
printf("두번째 요소의 주소 : %p \n", &arr[1]);
printf("세번째 요소의 주소 : %p \n", &arr[2]);
return 0;
}
위 코드를 실행시켜보면, `arr`의 주소값과 `arr[0]`의 주소값이 일치하는 것을 확인할 수 있다.
따라서, 배열의 이름(`arr`)는 포인터 변수와 마찬가지로 저장된 데이터의 첫번째 메모리 주소를 가리키는 것을 확인할 수 있다.
arr = &arr[i];
배열의 이름이 포인터의 성질을 갖고 있더라도, 배열의 이름은 변수가 아닌 상수 형태의 포인터이기 때문에
위와 같이 주소를 대입하는 연산이 불가하다.
배열의 이름이 포인터의 성질을 갖고 있기 때문에, 배열의 이름을 활용해서 포인터 연산을 하는 것도 가능하다.
#include <stdio.h>
int main(void)
{
int arr1[3] = {1,2,3};
double arr2[3] = {1.1,2.2,3.3};
// 배열의 이름을 활용한 포인터 연산
printf("%d %g \n", *arr1, *arr2); // 1 1.1
*arr1 += 100;
*arr2 += 120.5;
printf("%d %g \n", *arr1, *arr2); // 101 121.6
return 0;
}
이러한 배열의 이름을 활용한 포인터 연산을 확장시켜, index를 통해 배열에 접근하는것 처럼
포인터 역시 index를 활용해 배열에 접근할 수 있다.
#include <stdio.h>
int main(void)
{
int arr[3] = {1,2,3};
int *ptr = arr; // == (int *ptr = &arr[0];)
printf("%d, %d \n", ptr[0], arr[0]);
printf("%d, %d \n", ptr[1], arr[1]);
printf("%d, %d \n", ptr[2], arr[2]);
printf("%d, %d \n", *ptr, *arr);
return 0;
}
위 코드를 실행시켜보면, `ptr[idx]`와 `arr[idx]`의 값이 동일하게 나오는 것을 확인할 수 있다.
printf("%d, %d \n", *ptr[0], *arr[0]); // ERROR
위의 코드에 대해서는 컴파일 에러가 발생하게 되는데, 이를 통해서 index를 활용해 접근하는 것은
주소값에 접근하는 것이 아닌 저장된 값 자체에 접근하는 것이라고 유추해볼 수 있다.
추가적으로, 포인터+index를 활용해 배열에 접근하는 방식은 다음과 같은 방식으로도 대체할 수 있다.
#include <stdio.h>
int main(void)
{
int arr[3] = {1,2,3};
int *ptr = arr; // == (int *ptr = &arr[0];)
printf("%d, %d, %d \n", ptr[0], *ptr, arr[0]);
printf("%d, %d, %d \n", ptr[1], *(ptr+1), arr[1]);
printf("%d, %d, %d \n", ptr[2], *(ptr+2), arr[2]);
return 0;
}
포인터 변수에서 1을 증가시키는 것은 가라키는 변수의 자료형에 맞는 1만큼의 byte수를 증가시키는 것과 같은데,
배열에서 index를 하나씩 증가시키는 것 또한 이와 같은 방식이므로 동일한 값을 출력하게 된다.
-> (`int`자료형인 경우에는 메모리 크기가 4씩 증가할 것이고, `double`인 경우에는 8씩 증가하게 된다)
printf("%p, %p, %p \n", ptr, ptr+1, ptr+2);
printf("%p, %p, %p \n", &arr[0], &arr[1], &arr[2]);
이를 출력해보면 동일한 메모리 주소를 출력하고 있음을 확인해 볼 수 있다.
문자열의 표현 : 포인터 vs 배열
문자열 역시 포인터와 배열을 활용해 표현할 수 있는데, 두 방법의 성격이 상이하다.
- 배열 : 배열 형태
- 포인터 : 상수 형태
위의 예시에서 `str1`의 경우에는 문자열의 길이만큼(\0 포함)할당된 배열에 각 문자(character)를 저장하는 방식이고,
`str2`는 문자열 "Your String"에 자동으로 할당된 주소값을 `str2`에 저장하는 상수 형태의 문자열이다.
따라서, `str1`의 경우에는 문자열의 부분(문자)에 접근해 그 값을 변경할 수 있지만, `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'; // 문자 변경 O
// str2[0] = 'X'; // 문자 변경 X -> ERROR
printf("%s %s \n", str1, str2);
return 0;
}
문자열 중 일부 문자를 변경하는 것은 배열 형태로 저장된 문자열이 자유롭지만,
문자열 전체를 변경하는 경우에는 상수 형태로 저장된 문자열이 더 자유롭다.
#include <stdio.h>
int main(void)
{
char str1[] = "My Team";
char * str2 = "Your Team";
printf("%s %s \n", str1, str2);
// 문자 전체 변경
// str1 = "Our Team"; // ERROR
str2 = "Our Team";
printf("%s %s \n", str1, str2);
}
배열 형태로 저장된 `str1`에서 문자열 전체를 교체하려면, 각 원소에 접근해 문자열 전체가 아닌 문자마다 교체를 해주어야 가능할 것이다.
포인터 배열
포인터 배열은 기존의 배열과 동일하지만, 배열에 주소값을 저장하는 배열이다.
따라서, 선언하는 방식도 기존의 배열의 선언과 동일하지만 포인터 연산자(`*`)가 추가된다.
int * ptr_arr[] = {&num1, &num2, &num3};
포인터 배열과 위에서 설명한 상수 형태로 저장되는 문자열의 특성을 활용하면,
하나의 배열 안에 여러개의 문자열을 저장하는 것도 가능하다.
char * str_arr[] = {"Simple", "String", "Array"};