일반적으로 포인터를 사용하는 목적은 동적으로 메모리를 할당하여 사용하기 위해서 사용합니다. 그리고 메모리가 할당된 후에는 배열처럼 사용하면 된다고 이전에 설명하였습니다.
int arr[100];
int *ptr;
ptr = (int *)malloc(sizeof(int) * 100);
위에 2개의 변수를 선언하였습니다. 배열로 100개, 포인터로 100개의 int 공간을 할당한 변수입니다. 우선 이들의 사용상의 유사점을 알아보겠습니다.
포인터와 배열의 유사점
1). 배열 표기법으로 접근
int i = 10;
printf("arr[%d] = %d\n", i, arr[i]);
printf("ptr[%d] = %d\n", i, ptr[i]);
위와 같이 i번째 데이터를 접근하는 방법으로 포이터든 배열이든 모두 배열처럼 사용하여 접근이 가능합니다. 따라서 포인터도 배열처럼 편하게 사용하면 됩니다.
2. 포인터 접근법으로 표기
int i = 10;
printf("*(arr + %d) = %d\n", i, *(arr + i));
printf("*(ptr + %d) = %d\n", i, *(ptr + i));
포인터에 저장된 i번째 데이터를 접근하는 방법은 *(ptr + i)처럼 접근을 하는 데, 배열도 마찬가지로 같은 방법으로 접근이 가능합니다.
포인터와 배열의 차이점
1). 데이터 저장 공간
배열은 변수 선언과 함께 자신의 데이터를 저장할 공간을 배열의 크기만큼 연속적인 공간을 가집니다. 포인터는 자신이 데이터를 저장할 공간을 가지지 않고 저장할 공간이 있는 위치를 저장하는 메모리 번지를 저장합니다.
2. 변수의 메모리 번지
int arr[100];
int *ptr;
ptr = (int *)malloc(sizeof(int) * 100);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("ptr = %p\n", ptr);
printf("&ptr = %p\n", &ptr);
결과>
arr = 0x7fff0322adb0
&arr = 0x7fff0322adb0
ptr = 0x130b010
&ptr = 0x7fff0322ada8
위의 예제의 결과를 보면, arr의 데이터가 저장되는 메모리 번지와 arr 변수 자신의 메모리 번지가 일치합니다. 그러나 포인터는 ptr은 메모리가 할당된 메모리 번지이고, ptr 변수 자신의 메모리 번지가 다릅니다. 위의 예에서 arr, &arr, &ptr은 모두 변수가 stack 영역의 메모리 번지이고, ptr은 실제로 메모리가 할당된 heap 영역의 메모리 번지가 할당 되었음을 알 수 있습니다.
만약, ptr에 arr을 대입했다면,
int arr[100];
int *ptr = arr;
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("ptr = %p\n", ptr);
printf("&ptr = %p\n", &ptr);
결과>
arr = 0x7fff0322adb0
&arr = 0x7fff0322adb0
ptr = 0x7fff0322adb0
&ptr = 0x7fff0322ada8
위와 같이 ptr 변수 자신의 메모리 번지만 다름을 알 수 있습니다.
3). 포인터(변수) vs. 배열(상수)
int tmp[100];
int arr[100];
int *ptr = arr;
ptr = tmp; // OK
arr = tmp; // Error
포인터와 배열 둘 다 변수(arr, ptr) 자체는 메모리 번지를 뜻하지만, 배열은 포인터 상수이므로 다른 번지를 가리킬 수 없고, 포인터는 변수이므로 대입이 가능합니다.
위와 같은 이유로 인하여 문자열 상수를 정의할 때에,
포인터로 상수를 정의하는 경우
const char *URL = "https://www.it-note.kr";
strcpy(URL, "https://test.it-note.kr"); // 쓰기 오류
URL = "https://test.it-note.kr"; // 정상 처리
vs.
배열로 상수를 정의하는 경우
const char URL[32] = "https://www.it-note.kr";
strcpy(URL, "https://test.it-note.kr"); // 쓰기 오류
URL = "https://test.it-note.kr"; // 대입 오류
로 동작하여 서로 다르게 동작함을 확인할 수 있습니다.
따라서 정확하게 문자열 상수를 정의하려면 const char 상수명[] = "..."; 형태로 배열로 정의해야만 명확하게 됩니다.
만약 포인터로 문자열 상수를 정의한다면,
const char * const URL = "https://www.it-note.kr";
처럼 아주 복잡합니다.
const char * URL = "https://www.it-note.kr";
vs.
const char * const URL = "https://www.it-note.kr";
위의 두 선언의 차이에 대해서 잠깐 알아보겠습니다.
const char * URL = "https://www.it-note.kr";
은 URL이라는 포인터 변수가 가리키고 있는 곳의 데이터가 const char 라는 뜻입니다. 이것은 가리키고 있는 곳의 데이터를 수정할 수 없음을 의미합니다. 그래서,
URL[1] = 'a'; 또는 *(URL + 1) = 'a';
처럼 값을 변경하려고 하면 오류가 발생합니다.
그러나 URL 자체는 변수이므로 URL = 다른 상수; 형태의 대입은 가능합니다.
const char * const URL = "https://www.it-note.kr";
이 경우에는 URL이 가리키고 있는 곳의 데이터는 const char이라 값을 수정할 수 없을 뿐만 아니라, URL 변수 자체도 상수로 선언되어 다른 주소를 대입할 수 없는 형태가 됩니다.
4). 증감 연산자 사용 여부
위에서 얘기한 것처럼 포인터는 변수이고, 배열은 상수이므로 문자열의 복사 등을 위하여 증감연산을 사용할 수 있느냐? 없느냐?도 마찬가지 입니다.
char arr[100];
char *ptr = arr;
ptr++; // 정상
ptr += 2; // 정상
arr++; // 오류
arr += 2; // 오류
5). 다차원 배열도 단일 포인터
배열은 1차원 배열, 2차원 배열 등... 다차원 배열으로 선언하고 사용할 수 있습니다. 이들 배열은 차원에 관계없이 모두 메모리상에 연속적으로 데이터가 저장되고 관리되므로 차원에 관계없이 포인터는 단일 포인터로 간주합니다.
int arr[10][100];
int *ptr = (int *)arr; // warning 방지용
arr[1][4] = 256;
은
ptr[1 * 100 + 4] = 256;
또는
*(ptr + 1 * 100 + 4) = 256;
과 같이 다차원 배열은 포인터를 사용할 경우에 번지를 상대적 위치를 계산해서 더하는 방식으로 처리합니다.
'C언어 > 문법' 카테고리의 다른 글
24. 포인터(Pointer) - void * 포인터 (0) | 2019.12.02 |
---|---|
23. 포인터(Pointer) - 다중 포인터 (0) | 2019.11.28 |
21. 포인터(Pointer) - 동적 메모리 할당 (0) | 2019.11.26 |
20. 포인터(Pointer) - Call by value와 Call by Reference (4) | 2019.11.23 |
19. 포인터(Pointer) - 기초 (2) | 2019.11.20 |