인프런 커뮤니티 질문&답변

jiwon6760님의 프로필 이미지
jiwon6760

작성한 질문수

홍정모의 따라하며 배우는 C언어

10.13 포인터의 배열과 2차원 배열

포인터 2차원 배열

작성

·

770

3

안녕하세요 포인터 2차원 배열 9분 대부터 다시 공부하다가 이해가 가지 않아 문의드립니다.스크린샷 2023-05-08 오전 12.46.13.png위의 코드에서 *(*(parr + j) + i));와 (parr[j] + i);가 헷갈립니다. 일단 제가 처음에 이해한 대로 설명드려보겠습니다. parr은 parr[0]을 가리키는 포인터이며 즉 이 포인터가 최종적으로 가리키는 공간은 &arr0[0] 즉 arr 배열의 첫번째 공간입니다. 이 상태에서 예를 들어 parr + 1을 하면 &arr0[1]로 이동하며 를 하면 그 값은 2 이런 식이라고 생각했습니다. 또한 parr[1] + 1에서 parr[1]은 포인터이며 이 포인터가 가리키는 공간은 &arr1[0]이며 이 상태에서 포인터 연산 +1을 해주면 &arr1[1]이고 이를 로 접근하면 그 값은 5로 이해했습니다. 그치만 실제 결과를 보면 위에서 제가 제시한 두 코드의 결과는 같아야 되더군요. 그렇다면 그 결과를 도출하기 위해 다시 *(*(parr + j) + i));를 설명드리겠습니다. parr 배열 이름 자체는 &parr[0]이며 이 상태에서 포인터 연산 + 1을 하면 &parr[0] = &arr0[0], &parr[0] + 1 = &arr0[1]이 아니라 &parr[0] + 1 = &parr[1]이며 이 상태에서 로 접근하면 그 값은 4, 4+1은 5이며 이를 *로 간접 접근하면..이런 식인데 어디부터 제 개념이해가 잘못됐는지 모르겠습니다.. parr[1]로 먼저 arr1[0]에 접근한 뒤 포인터 연산 +1을 하는 것과 parr 자체에서 포인터 연산 +1을 하면 결과가 정확히 어떻게 다르며 *(*(parr + j) + i)) 이 코드의 정확한 개념이 궁금합니다.

답변 1

4

안녕하세요, 답변 도우미 Soobak 입니다.

해당 예시 코드에서,

  • arr0arr1 은 각각 3개의 정수를 저장하는 1차원 배열이며, parr 은 이 두 배열의 주소를 저장하는 포인터 배열 입니다. 그러므로, parr[0]arr0 의 첫 번째 원소의 주소를 가리키고, parr[1]arr1 의 첫 번째 원소의 주소를 가리킵니다.

  • 포인터 연산에서 +1 연산을 하면, 해당 포인터가 가리키는 데이터 타입의 크기만큼 이동합니다.
    int 포인터의 경우, int 자료형의 크기만큼 이동합니다.

위 내용을 바탕으로 코드를 살펴보면서 설명드리겠습니다.

1. parr[j][i]
parr[j][i] 에서, parr[j]arr0 또는 arr1 의 첫 번째 원소를 가리키는 포인터입니다.
따라서, parr[j][i]j 번째 배열의 i 번째 원소를 가리키게 됩니다.

2. *(parr[j] + i)
*(parr[j] + i) 의 경우, parr[j]j 번째 배열의 첫 번째 원소를 가리키는 포인터이고, 여기에 i 를 더하면 j 번째 배열의 i 번째 원소를 가리키는 포인터가 됩니다.
따라서, *(parr[j] + i)j 번째 배열의 i 번째 원소를 가리키게 됩니다.

3. *(*(parr + j) + i)
여기서, (parr + j)j 번째 배열의 첫 번째 원소를 가리키는 포인터를 가리키는 포인터 입니다.
따라서, *(parr + j)j 번째 배열의 첫 번째 원소를 가리키는 포인터가 되고, 여기에 i 를 더하면 j 번째 배열의 i 번째 원소를 가리키는 포인터가 됩니다.
즉, *(*(parr + j) + i)j 번째 배열의 i 번째 원소를 가리키는 것입니다.

따라서, parr[j][i], *(parr[j] + i) 그리고 *(*(parr + j) + i) 모두 같은 값을 가리키게 됩니다.

jiwon6760님의 프로필 이미지
jiwon6760
질문자

말씀해주신 부분에서 parr이라는 포인터 자체가 가리키는 것은 parr[0]-->arr0[0]인가요?? parr[0]이 arr0[0]을 가리키고 parr[1]은 arr1[0]을 가리키는 것은 이해했으나 parr이라는 포인터가 자체적으로 가리키는 것은 무엇인지 궁금합니다. 예를 들어 arr0이라는 배열 이름 자체는 arr0[0]을 가리키듯이요.. 또한 위에서도 말씀드렸었지만 parr[j]+i가 parr의 j번째 배열의 i번째 원소를 가리키는 과정과 *(*(parr + j)+i)가 j번째 배열의 i번째 원소를 가리키는 과정이 어떻게 다른지 구체적으로 알고 싶습니다. parr[j]가 parr의 j번째 배열의 첫 번째 원소를 가리킨다면 parr + j는 parr[0] 즉 parr의 첫번째 배열을 가리키는 포인터에 j를 더해 parr의 j번째 배열에 접근하는 것인가요..? 마지막으로 parr + j가 parr의 j번째 배열의 첫 번째 원소를 가리키는 포인터를 가리키는 포인터 즉 이중 포인터가 여기에 어떻게 적용되는지 궁금합니다. parr + j가 왜 배열의 j번째 배열의 첫 번째 원소를 가리키는 포인터가 아니라 그 포인터를 가리키는 포인터이며 과정이 어떻게 다른지요..ㅠㅠ 이해가 가지 않습니다.

안녕하세요, 답변 도우미 Soobak 입니다.

배열의 이름이 가리키는 것, 그리고 [] 으로의 접근, 포인터 연산 3가지 개념에 대해서 혼동하고 계신 것 같습니다.
위 개념들에 대해서 정리해보시면 좋을 것 같습니다.

우선, 질문주신 내용에서 잘 이해하고 계신 것 같은 특정 부분을 다른 문장에서는 잘못 표현하고 계셔서, 제가 질문자님께서 정확히 어느 것을 오해하고 계신 것인지 알기가 어렵네요. 🥲
그래서 이번에는 질문주신 문장 모두에 대해서 설명드리는 식으로 답변을 드려보도록 하겠습니다.

말씀해주신 부분에서 parr이라는 포인터 자체가 가리키는 것은 parr[0]-->arr0[0]인가요?? parr[0]이 arr0[0]을 가리키고 parr[1]은 arr1[0]을 가리키는 것은 이해했으나 parr이라는 포인터가 자체적으로 가리키는 것은 무엇인지 궁금합니다. 예를 들어 arr0이라는 배열 이름 자체는 arr0[0]을 가리키듯이요.

: int* parr[2] = {arr0, arr1}; 로 선언된 parr 은 포인터의 배열 입니다.
배열의 이름은 배열의 첫 번째 원소의 주소를 가리키며, C언어에서 문법적으로 포인터와 호환이되는 형태입니다.
따라서, parrarr0 의 주소를 가리킵니다.

이 때, arr0int arr0 = {1, 2, 3}; 으로 선언된 int의 배열 입니다.
따라서, arr0 이 가리키는 것은 배열의 첫 번째 원소인 1 이 저장된 메모리 공간의 주소를 가리킵니다.

위에서 설명드린 것 처럼 parrarr0 의 주소를 가리키는 포인터, arr01 의 주소를 가리키는 포인터이므로, parr1 의 주소를 가리키는 포인터를 가리키는 포인터, 즉 이중 포인터로 볼 수 있습니다.

또한 위에서도 말씀드렸었지만 parr[j]+i가 parr의 j번째 배열의 i번째 원소를 가리키는 과정과 *(*(parr + j)+i)가 j번째 배열의 i번째 원소를 가리키는 과정이 어떻게 다른지 구체적으로 알고 싶습니다. parr[j]가 parr의 j번째 배열의 첫 번째 원소를 가리킨다면 parr + j는 parr[0] 즉 parr의 첫번째 배열을 가리키는 포인터에 j를 더해 parr의 j번째 배열에 접근하는 것인가요..?

: C언어에서 배열 인덱스 연산자인 [정수] 으로 배열의 요소에 접근하는 것과, *(포인터 + 정수) 로 요소에 접근하는 것은 기본적으로 동일한 과정입니다. 다르지 않습니다.
즉, parr[j]*(parr + j) 는 동일하며, *(parr[j] + i)*(*(parr + j) + i)) 은 동일한 표현입니다.

첫 번째 문장, "...parr[j]+i가 parr의 j번째 배열의 i번째 원소를 가리키는 과정과..." 에서, parr[j] + ij 번째 배열의 i번째 원소를 가리키는 것이라고 표현하기 보다는 해당 원소를 가리키는 포인터, 즉, 해당 원소의 주소값을 가리키는 것으로 이해하시는 것이 보다 정확합니다.
마지막 문장에서 이해하신 내용은 옳습니다.

마지막으로 parr + j가 parr의 j번째 배열의 첫 번째 원소를 가리키는 포인터를 가리키는 포인터 즉 이중 포인터가 여기에 어떻게 적용되는지 궁금합니다. parr + j가 왜 배열의 j번째 배열의 첫 번째 원소를 가리키는 포인터가 아니라 그 포인터를 가리키는 포인터이며 과정이 어떻게 다른지요..ㅠㅠ 이해가 가지 않습니다.

: parr + jj 번째 배열의 첫 번째 원소를 가리키는 포인터가 아니라는 표현은 옳지 않습니다. parr + jj 번째 배열의 첫 번째 원소를 가리키는 포인터가 맞습니다.
다만, int* parr[] = {arr0, arr1}; 으로 선언된 parr 배열 자체의 자료형이 int* [] 즉, 포인터의 배열이고, 배열의 이름인 parr 은 배열의 첫 번째 원소의 주소를 가리키는 포인터이므로, parr 은 이중 포인터가 되는 것입니다.
마찬가지로, parr + j 는 배열의 j 번째 배열의 첫번째 원소를 가리키는 포인터의 포인터입니다.

질문해주신 내용에 대한 개인적인 의견을 보태보자면, 강의 10.4 포인터와 배열10.9 포인터 연산 총정리 의 복습을 추천드려봅니다. 해당 강의들에서 교수님의 설명이 질문자님께 보다 많이 도움되실 것 같습니다.

jiwon6760님의 프로필 이미지
jiwon6760
질문자

정말 정성스럽게 답변해주셔서 감사합니다..ㅠㅠ 말씀해주신 부분 꼭 다시 복습하겠습니다!! 마지막으로 ((parr + j) + i))에서 궁금한 것이 있습니다. 말씀을 정리해보면 parr + j는 parr의 j번째 배열의 첫 번째 원소를 가리키는 포인터를 가리키는 포인터라고 하셨는데 이 과정을 좀 더 자세히 봐서 &parr[0]=&arr0[0], &parr[0]+1=&arr0[0]+1=&arr0[1]이라고 보면 안된다는 건 알겠지만 parr[0]+1을 하면 parr[1]을 가리키는 포인터가 되는 것과 어떤 차이인지 모르겠습니다.

안녕하세요, 답변 도우미 Soobak 입니다.

우선, 질문하신 내용 중 "parr[0]+1을 하면 parr[1]을 가리키는 포인터가 되는 것과 어떤 차이인지" 에서, 차이에 대해서 비교하고 싶은 대상이 어떤 것인지 정확히 언급해주지 않으셔서, parr + 1 과의 비교를 가정하고 답변드리겠습니다.

또한, 해당 게시판에서 사용하고 있는 Markdown의 문법에서 *와* 사이의 문자는 기울어짐 으로 자동 변환이 되어, 문자를 기울이는 것이 의도한 것이 아니라면, 따로 수동으로 변환해주어야 합니다.
따라서, 말씀하신 "마지막으로 ((parr + j) + i))에서 궁금한 것이 있습니다." 에서 ((parr + j) + i))
*(*(parr + j) + i)) 에 대해서 질문을 주신 것으로 가정하고 답변 드리도록 하겠습니다.
질문자님의 질문에서, 맨 앞의 ( 가 기울어져 있기 때문입니다.

즉, parr[0] + 1parr + 1 의 차이점에 대해서 설명드리도록 하겠습니다.

  • parr[0] + 1

    : parr[0]arr0 입니다. 배열의 이름은 그 배열의 첫 번째 원소의 주소를 나타내는 포인터이므로, parr[0]arr0 의 첫 번째 원소의 주소를 나타내는 포인터가 됩니다.
    이 때, int* parr[] 으로 선언된 배열의 원소인 arr0 의 자료형은 int* 입니다.
    따라서, parr[0] + 1 연산을 하게 되면, int 의 크기만큼 주소값이 증가하게 됩니다.
    즉, parr[0] + 1arr0 의 두 번째 원소인 2 의 주소를 가리키는 포인터가 됩니다.

  • parr + 1
    : parr 은 포인터들의 배열입니다. 이 경우, int* 타입의 포인터들을 저장하므로, parr 자체는 int** 타입의 이중 포인터로 볼 수 있습니다. 이중 포인터인 이유에 대해서는 위 2개 질문에서의 답변에서 충분히 이해가 되셨으리라 생각됩니다. 따라서, parr + 1 을 수행하면, int* 타입의 크기 만큼 주소값이 증가하게 됩니다.
    즉, parr + 1 은 포인터 배열 parr 의 두 번째 원소인 arr1 의 주소를 가리키게 됩니다.

결론적으로, parr[0] 의 자료형은 int* 타입의 포인터이며, parr 의 자료형은 int** 타입의 이중 포인터이므로, 포인터 연산의 결과가 달라지는 것입니다.
간단히 요약하면, 각 포인터의 자료형이 다르기 때문에 연산의 결과가 달라지는 것으로 이해하시면 좋을 것 같습니다.

C언어에서 포인터 개념은 처음에는 낯선 개념이지만, 한 번 이해를 하고 나면 어렵지는 않은 개념 같습니다.
이 과정에서, 인내심을 갖고 꼼꼼히 하나 하나 따져보고, 직접 각 포인터 변수들을 출력해보시는 과정이 큰 도움이 되는 것 같습니다.

질문자님께서 혼동하시고 계신 것 같은 개념인 배열과 포인터의 관계, 포인터 연산, 이중 포인터와 다차원 배열 등과 관련된 섹션 10 배열과 포인터 에서 관련 내용들을 복습하시고, 스스로 다시 한 번 정리를 하신 후 질문해주시면 질문자님께서도 더 명확하게 스스로가 혼동하시는 부분에 대해서 알 수 있으실 것 같으며, 저도 더 양질의 정확한 답변을 드릴 수 있을 것 같습니다.

제 개인적인 생각으로, 강의에서의 교수님이 설명이 기초적인 원리와 동작 방식 등 '원리' 에 대해서도 설명해주시기 때문에, 질문자님의 학습에 더 도움이 되실 것 같기 때문입니다. 원리를 바탕으로 이해를 하는 것이, 단순히 암기를 하려 하는 것 보다 양질의 학습을 할 수 있는 것 같습니다. 또한, 제 답변보다 훨씬 명쾌하게 정리를 잘 해주신다는 것도 이유 중 하나입니다.
질문자님의 이전 질문들에서 '복습' 중이시라고 하셨기에 의견을 보태보았습니다.

최대한 자세히 설명드리고자 노력해보았으나, 만약 아직 이해가 어려우시다면 편하게 추가 댓글 남겨주세요.

jiwon6760님의 프로필 이미지
jiwon6760
질문자

정말 감사드립니다. 이제 좀 제대로 이해한 것 같습니다!! 정성스럽게 답변해주셔서 감사합니다!!

진짜 수박은 신인가? 이것보다 더 정성스럽고 아름다운 답변은 없을듯...

마지막 답글에서

"따라서, parr + 1 을 수행하면, int* 타입의 크기 만큼 주소값이 증가하게 됩니다." 라고 하셨고

"parr[0] 의 자료형은 int* 타입의 포인터이며, parr 의 자료형은 int** 타입의 포인터이므로, 포인터 연산의 결과가 달라지는 것입니다." 라고 하셨는데

parr의 자료형이 int**인데 +1을 수행하면 int* 타입의 크기만큼 주소값이 증가하는 것을 어차피 c언어에서는 포인터 변수의 크기는 int*든 float*든 뭐든간에 동일(운영체제에 따라 바뀌기는 하지만)하기때문에 상관없다라고 봐도 될까요?

감사합니다.

안녕하세요, 답변 도우미 Soobak 입니다.

네, 말씀하신 내용이 맞습니다.


포인터 변수 자체의 크기포인터 연산 시 주소값이 변하는 크기는 별개의 개념임을 강조하기 위해 조금 혼동스러운 답변을 하고 말았네요.

따라서, parr 이 자료형이 int** 일 때, parr + 1 연산은 int* 타입의 포인터 크기 만큼 주소값이 증가한다라고 말씀드리는 것보다, 시스템에서 1 개의 포인터 변수 크기만큼 주소값이 증가한다고 정정드리는 것이 더 정확한 것 같습니다.

올바른 추측으로 좋은 지적을 해주셔서 진심으로 감사드립니다.

감사합니다. 진짜 대성하십쇼.

jiwon6760님의 프로필 이미지
jiwon6760

작성한 질문수

질문하기