[C] 9 - 2. 포인터(Pointer)

Wonder_Land🛕·2022년 7월 22일
0

[C]

목록 보기
10/18
post-thumbnail
  1. 배열의 이름과 &연산자
  2. 이중 포인터
  3. 1차원 배열과 포인터
  4. 2차원 배열과 포인터
  5. Q&A
  6. 마치며

([모두의 코드] 작성자분께서 이번 강좌가 제일 어려운 내용일 수 있다고 하시네요....나 자신 화이팅 😂)

지난 시간에 배운 내용을 떠올려보겠습니다.

  • 포인터
    : 메모리 상에 특정 데이터가 저장된 주소값을 가리키는 변수

  • &연산자
    : 메모리 상에 특정 데이터가 저장된 주소값을 가져옴

  • *연산자
    : 메모리 상에 특정 주소값에 저장된 데이터를 가져옴

  • 배열은 배열, 포인터는 포인터이다.(같은 것이 아니다)
    단, sizeof와 &연산자와 함께 쓰일 때를 제외하면, 배열의 이름은 첫 번째 원소를 가리킨다.

  • arr[i]와 같은 표현은 컴파일러에 의해 *(arr + i)로 변환된다.


1. 배열의 이름과 &연산자

앞서 계속해서 배열의 이름은,
sizeof함수&연산자를 함께 사용하는 경우를 제외하고는,
암묵적으로 포인터로 변환이 된다고 배웠습니다.

아래 예시를 참고하세요.

#include <stdio.h>

int main() {
	int arr[3] = { 1, 2, 3 };
	printf("%d\n", sizeof(*arr));
	printf("%d", sizeof(arr));
}

[Result]
4
12

sizeof함수를 이용하면 배열 전체의 크기를 반환하기 때문인거는 지난 시간에 배웠는데,
&연산자는 왜 그렇죠?

결론부터 말씀드리면, 배열의 이름과 &연산작 함께 쓰이면,
배열의 첫번째 원소의 주소를 가리키는 포인터가 아닌,
전체 배열을 가리키는 포인터로 변환됩니다.

다음 예제를 봅시다.

#include <stdio.h>

int main() {
	int arr[3] = { 1, 2, 3 };

	for (int i = 0; i < 3; i++) {
		printf("arr[%d] : %p\n", i, &arr[i]);
	}
	printf("\n");
    
	printf("arr  : %p\n", arr);
	printf("&arr : %p\n\n", &arr);
	printf("arr + 1 :  %p\n", arr + 1);
	printf("&arr + 1 : %p", &arr + 1);
}

[Result]
arr[0] : 0000002739D0F7A8
arr[1] : 0000002739D0F7AC
arr[2] : 0000002739D0F7B0

arr : 0000002739D0F7A8
&arr : 0000002739D0F7A8

arr + 1 : 0000002739D0F7AC
&arr + 1 : 0000002739D0F7B4

위의 예시에서,
우선 int형 배열 arr의 각 원소의 주소값을 출력했습니다.

이후에 arr&arr의 주소값을 각각 출력했습니다.
그런데 arr[0]과 주소값이 동일합니다.
그러면 &연산자를 사용하든 말든 똑같은 거 아닌가요??

아닙니다!!
사실 어떻게 변수와 변수의 &가 어떻게 같겠어요...

사실 두 놈은 주소는 같지만 다른 Type의 주소입니다.

그래서 위의 코드에서 arr + 1&arr + 1의 주소값을 출력했습니다.

오잉 값이 다르네요.
만약 두 개가 동일하다면 똑같은 값이 나와야 정상이죠?

그래서 결과를 보니
arr + 1arr[0]과 주소값이 4만큼 차이나고,
&arr + 1arr[0]과 주소값이 12만큼 차이납니다.

즉, arr + 1은 int형 배열 arr내에서 연산이 된 것이고,
&arr + 1은 int형 배열 arr만큼 연산이 이루어진 것이죠.

결론적을 말씀드리면,
arr은 배열의 첫번째 원소를 가리키는 포인터고,
&arr은 크기가 3인 배열 전체를 가리키는 포인터입니다.

2차원 배열에서도 마찬가지입니다.

arr[5][4]라는 배열에서,
배열의 이름 arr은 크기가 4인 첫번째 배열 가리키는 포인터로 변환됩니다.
그러나 &arr은 5 * 4 배열 전체를 가리킵니다.

  • 배열 이름과 sizeof함수 / &연산자

  • 배열 이름 : 첫번째 원소를 가리키는 포인터로 암묵적으로 변환

  • sizeof : 전체 배열의 크기를 반환
  • &연산자: 전체 배열을 가리키는 포인터로 반환

2. 이중 포인터

말 그대로, 이중으로 참조하는 포인터입니다.

아래의 예시는,
int를 가리키는 포인터를 가리키는 포인터입니다.

int **p

끔찍한 혼종처럼 생겼죠!

#include <stdio.h>

int main() {
    int a = 1;
    int* pa;
    int** ppa;

    pa = &a;
    ppa = &pa;

    printf("a    : %d\n", a);
    printf("*pa  : %d\n", *pa);
    printf("**ppa : %d", **ppa);
    
    return 0;
}

[Result]
a : 1
*pa : 1
**ppa : 1

위의 예시를 보면
우선, 포인터 pa는 변수 a의 주소값을 참조하고 있습니다.
즉, *paa와 동일하죠.

그리고, 이중포인터 ppa는 포인터 pa의 주소값을 참조하고 있습니다.
즉, *ppapa와 동일하죠.

따라서 **ppaa와 동일합니다.


3. 1차원 배열과 포인터

#include <stdio.h>

int main() {
	int arr[3] = { 1,2,3 };
	int* p;

	/* p = &arr[0]와 동일 */
	p = arr;

	
	printf("arr[1] : %d \n", arr[1]);
	printf("p[1]   : %d \n", p[1]);
	
	return 0;
}

[Result]
arr[1] : 2
p[1] : 2

위의 예제를 살펴보면,
arr[1]은 컴파일러에 의해 *(arr + 1)이 되어 2를 출력합니다.

p[1]도 마찬가지로 *(p + 1)이 되어 2를 출력합니다.

포인터에 1을 더하게 되면,
p에 저장된 주소값에 1이 더해지는 것이 아니라,
1 * (포인터가 가리키는 Type의 크기)가 더해집니다.

위의 경우에는 int형을 가리키므로, 4가 더해지겠죠.

#include <stdio.h>

int main() {
  int arr[10] = {100, 98, 97, 95, 89, 76, 92, 96, 100, 99};

  int* parr = arr;
  int sum = 0;

  while (parr - arr <= 9) {	//..??
    sum += (*parr);
    parr++;
  }

  printf("내 시험 점수 평균 : %d \n", sum / 10);
  return 0;
}

원본글에 있던 예제 중 하나입니다.

전 이 while (parr - arr <=9)부분이 헷갈렸는데요,
생각해보니 쉬웠습니다.

parr은 while문에서 증감연산자를 통해 계속해서 증가합니다.
(parr + 0) ~ (parr + 9)까지죠.

arr은 sizeof함수나 &연산자와 쓰이지 않았으므로, 암묵적으로 배열의 첫번째 원소를 가리킵니다.

따라서 parr - arr
= (parr + n) - (arr)
= (arr + n) - (arr)로써 사용할 수 있습니다.

while문을 10번 수행하게 되면,
parr은 마지막 원소은 arr[9]를 가리키므로,
parr - arr9가 됩니다.

(이해하는데 오래 걸렸습니다 🤣)


4. 2차원 배열과 포인터

2차원 배열은 1차원 배열이 여러 개가 있다고 생각하면 됩니다.

하지만, 실제 컴퓨터 메모리 구조는 1차원이기 때문에 항상 선형으로 존재합니다.

#include <stdio.h>

int main() {
  int arr[2][3];

  printf("arr[0] : %p \n", arr[0]);
  printf("&arr[0][0] : %p \n", &arr[0][0]);

  printf("arr[1] : %p \n", arr[1]);
  printf("&arr[1][0] : %p \n", &arr[1][0]);

  return 0;
}

[Result]
arr[0] : 0x7ffda354e530
&arr[0][0] : 0x7ffda354e530
arr[1] : 0x7ffda354e53c
&arr[1][0] : 0x7ffda354e53c

위의 예제에서는,
arr[0]arr[0][0]의 주소값은 동일하고,
arr[1]arr[1][0]의 주소값이 동일합니다.

즉, 1차원 배열과 마찬가지로
sizeof나 &연산자와 사용하지 않을 경우,
arr[0]arr[0][0]을 가리키는 포인터로 암묵적으로 Type 변환이 됩니다.


1) 배열 포인터

그렇다면 2차원 배열의 이름을 포인터에 전달하기 위해서는 포인터의 타입을 어떻게 정의해아할까요?

arr[0][0]은 int형,
arr[0]은 int *형,
그러면 int **형이 되어야 하지 않을까요?

그러나 아닙니다!
바로 배열을 가리키는 포인터, '배열 포인터'로 접근해야 합니다.

그 이유에 대해 살펴봅시다.

(1) int**형으로는 배열의 원소에 접근할 수 없다.

다음을 실행해 보면 컴파일 오류가 등장합니다.

#include <stdio.h>

int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  int **parr;

  parr = arr;

  printf("arr[1][1] : %d \n", arr[1][1]);
  printf("parr[1][1] : %d \n", parr[1][1]);

  return 0;
}

그 이유는 parr[1][1]에서, 이상한 메모리 공간의 값에 접근했기 때문입니다.

왜 그럴까요?

parr[1][1]*(*(parr + 1) +1과 동일한 문장입니다.

우선 내부의 parr + 1을 봅시다.
parr은 int*형을 가리키는 포인터이므로,parr + 1을 하면 8바이트가 증가합니다.
(포인터의 크기는 주소값을 저장하므로, 64비트에서는 8바이트입니다.)

따라서, arr배열의 세 번째 원소인 3을 가리킵니다.

그 다음에, *(parr + 1) + 1을 하면,
int형이므로 4바이트만큼 증가해서, 7이 됩니다.
즉, index 범위를 벗어나게 됩니다.

그렇다면 어떻게 해야하나요?

우선, int arr[10]이라는 배열에서 x번째 원소의 주소값을 알아봅시다.
시작주소를 arr이라고 한다면 arr[x]의 주소값은

arr+4xarr + 4x가 됩니다.

그렇다면 2차원 배열 int arr[a][b]는요?
int arr[a][b]int arr[b]짜리 배열이 a개 존재하는 형태입니다.

따라서 arr[x][0]의 주소값은 x번째의 int arr[b]짜리 배열이 됩니다.
그렇다면 해당 주소는 arr+4bxarr + 4bx입니다.

그렇다면 arr[x][y]의 주소값은
arr+4bx+4yarr + 4bx + 4y가 됩니다.

그렇다면 해당 주소값을 정확히 알기 위해서는
x, y뿐만 아니라 b도 알아야 합니다.

즉, 2차원 배열을 가리키는 포인터를 통해서, 원소들에 정확히 접근하기 위해서는

  1. 가리키는 원소의 크기(여기서는 int형이므로 4)
  2. b의 값

포인터 타입에 명시되어야만 접근할 수 있습니다.

#include <stdio.h>

int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  int(*parr)[3];  // 괄호를 꼭 붙이세요

  parr = arr;  // parr 이 arr 을 가리키게 한다.

  printf("parr[1][2] : %d , arr[1][2] : %d \n", parr[1][2], arr[1][2]);

  return 0;
}

[Result]
parr[1][2] : 6 , arr[1][2] : 6

위의 예시에서는 int(*parr)[3];로 2차원 배열의 원소에 접근 가능합니다.

우선 1번 조건(가리키는 원소의 크기)은 int로 만족합니다.
그리고 2번 조건은 3으로 알 수 있습니다.

즉, parr은 크기가 3인 배열 전체를 가리키는 포인터입니다.

  • 배열 포인터의 구성
(배열의 형) (*포인터 이름)[2차원 배열의 열 개수];

2) 포인터 배열

포인터 배열은 말 그대로 포인터들의 배열입니다.

앞서 설명한
<배열 포인터>는 배열을 가리키는 포인터이면,
<포인터 배열>은 포인터를 원소로 가지는 배열입니다.

배열은 배열이고, 포인터는 포인터이므로,
두 놈은 사실 별 관계가 없습니다.

#include <stdio.h>
int main() {
  int *arr[3];
  int a = 1, b = 2, c = 3;
  arr[0] = &a;
  arr[1] = &b;
  arr[2] = &c;

  printf("a : %d, *arr[0] : %d \n", a, *arr[0]);
  printf("b : %d, *arr[1] : %d \n", b, *arr[1]);
  printf("b : %d, *arr[2] : %d \n", c, *arr[2]);

  printf("&a : %p, arr[0] : %p \n", &a, arr[0]);
  return 0;
}

[Reuslt]
a : 1, *arr[0] : 1
b : 2, *arr[1] : 2
b : 3, *arr[2] : 3
&a : 0x7ffe8a2fa4e4, arr[0] : 0x7ffe8a2fa4e4

위의 예시에서 arr의 각 원소들은 포인터로써,
각 원소들은 a, b, c를 각각 가리키고 있습니다.


5. Q&A

-


6. 마치며

...정말 어려웠습니다😢

사실 지난 시간에 배운 내용은 그나마 쉽게 이해할 수 있었는데,
이번 파트는 이해하는 것 자체도 정말 어려웠네요.

혼동이 많이 됩니다.

공부하면서 하나하나 음미하며 이해하는 건 정말 오랜만이네요.

특히나, 배열과 포인터는 정말......
배열 포인터는 정말 이해하기가 쉽지 않았습니다.

하지만 원본 작성자분께서도 무수히 많은 자료를 접하고 공부하셔서 나아지셨다고 하니,
저도 그런 과정이 필요하겠죠?

노력하겠습니다...😊

[Reference] : 위 글은 다음 내용을 참고, 인용하여 만들어졌습니다.

profile
아무것도 모르는 컴공 학생의 Wonder_Land

0개의 댓글