이 전 글에 이어서 포인터끼리의 형변환과, 배열의 포인터, 동적할당에 대해 다뤄보도록 합시다.
배열도 자료형이기 때문에 당연히 배열을 위한 포인터 또한 존재합니다. 예제를 볼까요?
int a[10]; // int [10] 자료형 변수 a, 크기는 40 바이트
int (*p)[10] = &a; // int[10] 자료형의 포인터 변수 p p의 자료형은 int(*)[10]
(*p)[3] = 3; // a[3] = 3과 같음
*p[3] = 3; // a[30] = 3과 같음 오버플로우
int *pa = a; // int 자료형의 포인터 변수 pa
pa[3] = 3; // a[3] = 3과 같음
변수 p는 변수 a의 주소를 담고 있습니다. 여기서 p의 크기는 포인터 변수이기 때문에 8바이트입니다.
p를 통해 a에 접근할 때는 *p 또는 p[0]을 적어주면 됩니다. 이 때의 자료형은 int[10]이며, 크기는 40바이트 입니다.
*p는 배열을 가리키기 때문에 배열안에 원소로 접근할 수 있습니다. 이 경우는 (*p)[3]과 같은 방법으로 접근이 가능합니다.
간혹 *p[3]과 같이 접근하는 경우가 있는데, 이는 p[3][0]과 같기 때문에 위 코드에서는 오버플로우가 됩니다. (*p = p[0], *p[3] = p[3][0]) 이는 * 연산자의 우선순위가 낮기 때문에 발생하는 문제이므로 주의합시다.
그런데 C언어 강의 시간에는 배열을 저렇게 다미 않고, 변수 pa와 같이 int형 포인터를 통해 배열을 담았습니다. 사실 저 방법은 엄밀히 말하면 배열의 주소를 포인터변수로 담은것이 아닙니다. a[3]의 주소를 포인터로 담은 것입니다.
a는 int[10] 자료형이지만, a + 0은 int * 자료형이 됩니다. 따라서 sizeof(a)는 40 바이트이고, sizeof(a+0)은 8바이트가 되죠.
변수 pa의 경우는 후자처럼 처리가 된 것이고, 이것을 강의시간에는 배열의 이름은 배열의 첫 번째 원소의 주소를 가리킨다고 배웠을 것입니다. 다차원 배열의 경우도 위와 같은 방법으로 표시할 수 있습니다.
포인터 또한 자료형이기 때문에 포인터를 위한 포인터도 정의가 가능합니다.
int a; // int형 변수 a 크기는 4 바이트
int *pa = &a; // int * 자료형 pa 크기는 8 바이트
int **ppa = &pa; // int ** 자료형 ppa 크기는 8 바이트
int b[10]; // int [10] 자료형 b 크기는 40 바이트
int (*pb)[10] = &b; // int (*)[10] 자료형 pb 크기는 8 바이트
int (*ppb)[10] = &pb; // int (**)[10] 자료형 ppb 크기는 8 바이트
int *c[10]; // int *[10] 자료형 c 크기는 80바이트
int *(*pc)[10] = &c; // int *(*)[10] 자료형 pc 크기는 8 바이트
배열의 경우와 크게 다르지 않지만, 만약 변수 ppa를 통해 변수 a에 접근하려면 **ppa 또는 ppa[0][0]과 같이 적어주면 됩니다.
변수 ppb는 int[10] 자료형을 가리키는 포인터 int(*)[10]을 가리키는 포인터입니다. 괄호와 *의 위치를 기억합시다.
변수 c는 int *의 10개 짜리 배열입니다. 따라서 크기는 80바이트가 됩니다. 변수 pc는 변수 c의 자료형을 가리킵니다.
좀 더 쉽게 생각하기 위해서 다른 예제를 들고 왔습니다.
int main() {
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i = 0; i < 10; i++) {
printf("%d", arr[i]);
} // 1 2 3 4 5 6 7 8 9 10
printf("\n");
for (int i = 0; i < 10; i++) {
printf("%d", *(arr + i));
} // 1 2 3 4 5 6 7 8 9 10
printf("\n");
for (int *ptr = arr; ptr < arr + 10; ptr++) {
printf("%d", *ptr);
} // 1 2 3 4 5 6 7 8 9 10
printf("\n");
}
위의 3개가 전부 똑같은 값을 나타냅니다. 3번째 for문에서 선언과 동시에 주소값을 넣었습니다. 또한, arr의 값 자체를 뽑아내면 &arr[0]의 값과 똑같습니다.
즉, arr의 값은 arr[0]의 주솟값을 나타냅니다. 그렇기에 arr + 1이 &arr[1]을 나타냅니다. 그렇기에 arr + i는 &arr[i]임을 알 수 있습니다.
int main() {
int a = 10;
int *ptr_a = &a; // 포인터가 가리키는 값이 아니라 포인터 자체에다가 주소값을 넣었습니다.
*ptr_a = 20; // 포인터가 가리키는 값에다 20을 넣었습니다.
}
포인터 변수를 선언하면서 집어넣는 것과 포인터 변수를 선언하고 값을 집어넣는건 엄연히 다릅니다. 더 나아가보도록 합시다.
int main() {
int arr[3] = { 1, 2, 3};
int *ptr = arr; // ptr = &arr[0]
for (int i = 0; i < 3; i++) {
printf("%d", *(ptr + i));
}
printf("\n");
for (int i = 0; i < 3; i++) {
printf("%d", ptr[i]));
printf("\n");
// arr[i] == *(arr + i) == *(ptr + i) == ptr[i] == *(i + ptr) == i[ptr]
for (int i = 0; i < 3; i++) {
printf("%d", i[ptr]));
}
printf("\n");
맨 마지막 for문은 실무에서 쓰지 않는 것이 좋습니다. 남들이 보면 헷갈리잖아요
/*
1. ptr == &ptr[0]
2. *ptr == ptr[0]
3. ptr + 1 == ptr에 sizeof(*ptr)을 더한 값
*/
int main() {
int arr[3] = { 1, 2, 3};
printf("arr = %d\n", arr);
printf("arr + 1%d\n", arr + 1);
printf("&arr = %d\n", &arr);
printf("&arr + 1 = %d\n", &arr + 1); // &arr + 1 == &arr에 sizeof(*(&arr))을 더한 값
// &arr + 1 == &arr에 sizeof(arr)을 더한 값
}
arr는 4바이트의 수가 3개 있기 때문에 12바이트입니다. 그렇기 때문에 아래와 같은 결과가 나옵니다.
arr = 1636420
arr + 1 = 1636424
&arr = 1636420
&arr + 1 = 1636432
&arr 같은 경우엔 배열을 가리키는 포인터입니다. 배열 안의 값이 아니라
그렇다면 이제 배열 포인터를 사용할 수 있습니다.
int main(){
int arr[3] = { 1, 2, 3};
int (*ptr_arr)[3]; // 길이 3인 int형 배열을 가리키는 포인터를 선언
ptr_arr = &arr;
for (int i = 0; i < 3; i++) {
printf("%d\n", (*ptr_arr)[i]); // 여기서 *ptr_arr 자체가 가르키는게 배열이니까 이렇게 사용해도 됩니다.
}
}
포인터의 형변환을 알아봅시다.
포인터는 메모리 주소를 담는 자료형이고, 메모리 주소를 어떤 자료형으로 접근한다고 해도 메모리 주소가 바뀌진 않습니다.
즉, 포인터 변수를 다른 포인터 변수로 형변환 한다고 해도, 그 포인터 변수의 값인 메모리 주소는 변하지 않습니다.
그렇기 때문에 포인터의 형변환은 자유롭게 이루어질 수 있습니다. 값도 크기도 고정되고 목적만 바뀌는 것이니까요.
char a[8] = "hello!!";
// a 배열 null로 초기화 하기 1
for(int i=0; i < 8; i++)
a[i] = '\0';
// 2
*(long long *)&a = 0;
배열 a의 자료형은 char [8] 입니다. 크기는 8바이트 입니다.
2번 코드는 long long도 똑같이 8바이트 자료형이라는 것을 이용하여 초기화한 것입니다.
&a로 인해 자료형은 char(*)[8]이 되었고, 형변환을 통해 long long * 자로형이 되어 long long으로 접근하여 0으로 초기화 한 코드입니다.
C언어의 레퍼런스 함수인 memset도 위와 비슷한 방법을 이용하여 초기화 해줍니다.
참고로 위와 같은 형변환을 할 시엔 접근하려는 자료형의 크기가 다르게 될 경우의 오버플로우 문제에 주의합시다. 만약 a 배열이 char [4] 자료형이었다면 위 코드는 오버플로우가 될 것입니다.
포인터의 형변환을 이용하면 다음과 같은 것도 할 수 있습니다.
// 1차원을 2차원으로 확장하기
int a[100]; // int [100] 자료형 a 크기는 400 바이트
int (*p)[10] = (int (*)[10])a; // int (*)[10]자료형 p 크기는 8 바이트
p[3][1] = 3; //a[31] = 3과 동일
1차원 배열을 2차원 배열로 생각하는 것을 포인터를 이용하여 간단하게 나타낸 코드입니다. 이렇게 하면 배열 복사를 통하지 않아 시간도 절약되고, 배열을 따로 선언하지 않기 때문에 메모리도 아낄 수 있습니다.
동적할당은 프로세스의 실행중에 메모리를 할당하는 것을 말합니다. 배열의 크기를 변수에 의해 결정할 때 자주 사용됩니다.
C언어에서는 malloc 이라는 함수를 통해 동적할당을 수행할 수 있습니다.
// int 형 변수 동적할당
int *a = malloc(sizeof(int));
// 1차원 배열의 동적할당
int *b = (int *)malloc(sizeof(int) * N);
1차원 배열까지는 비교적 간단한 방법을 동적할당이 가능합니다.
주의할 점은 배열에 대한 동적할당 이라 해도, 변수 b는 int * 자료형이기 때문에 크기는 8 바이트입니다.
이상 포인터(Pointer) .. 왜 쓸까요? (2) 였습니다. ^_^
'C > 개념' 카테고리의 다른 글
Hash 구현 (0) | 2019.08.18 |
---|---|
Hash 알고리즘 (0) | 2019.08.10 |
C언어의 메모리 동적 할당 (0) | 2019.06.23 |
C언어의 메모리 구조 (2) | 2019.06.23 |
포인터(Pointer) .. 왜 쓸까요? (1) (0) | 2019.06.19 |