본문 바로가기

C/개념

포인터(Pointer) .. 왜 쓸까요? (2)

반응형
SMALL

이 전 글에 이어서 포인터끼리의 형변환과, 배열의 포인터, 동적할당에 대해 다뤄보도록 합시다.


배열도 자료형이기 때문에 당연히 배열을 위한 포인터 또한 존재합니다. 예제를 볼까요?

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) 였습니다. ^_^

반응형
LIST

'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