본문 바로가기

별걸다하는 IT/프로그래밍언어

[C언어, C++언어 강좌] 배열과 포인터 상관관계 완벽히 이해하기~! ARRAY and POINTER! 헷갈리는거 다 알려줄게!

반응형

[C언어 C++언어 프로그래밍 완전정복 목차]

안녕하세요 양햄찌 블로그 주인장입니다.

저번 포스팅에서 간략하게 배열과 포인터에 대해 알아봤는데요.

사실 배열과 포인터 사이에는 아주 밀접한 관계가 있습니다.

오늘은 배열과 포인터의 관계에 대해 살펴볼거예요.

 

▼ 배열에 대한 포스팅 다시 보러가기 

https://jhnyang.tistory.com/173

 

[Java, C, C++ ] 배열이란, 배열 선언 및 초기화 - 프로그래밍기초

[Java, C, C++ 프로그래밍 완전정복 목차] 오늘 포스팅: 배열 (Array) 기초 항상 프로그래밍 포스팅은 무엇을 할까 고민되는 것 같아요 ㅎㅎ 배열도 워낙 무궁무진해서리... 범위를 우케 나눠야 나중에

jhnyang.tistory.com

▼ 포인터에 대한 기초 포스팅 보러가기 

https://jhnyang.tistory.com/100

 

[C,C++ 강좌]C언어의 꽃 포인터 총정리(*, &), 포인터 사용 예시, 포인터 연산자

[C/C++ 완전정복 링크 ] C/C++ 목차, C/C++강좌, 링크 모음 C언어 문법 C언어란? C/C++언어 역사 및 특징 C/C++ 개발환경 비주얼스튜디오(Visual Studio) 설치 및 빈 프로젝트 생성 비주얼스튜디오 단축키 정리

jhnyang.tistory.com

따로 따로 포스팅을 했었지만~~~ 사실 기초를 설명하기 위함이었을 뿐 배열은 일종의 포인터이다!!

배열의 이름, 그 정체는 포인터?

우리가 만약 int ary[ ] = { 1, 2, 3 }; 이렇게 생성한다면,

ary[0]에는 1이 들어가 있고, ary[1]에는 2가 들어가 있고,, ary[2]에는 3이 들어가 있겠죠?

만약 ary를 출력했을 때에는 뭐가 나올까요??

 

[※ 배열의 이름 값 확인해보기]

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	cout << "배열의 이름 ary:" << ary << endl;
	cout << "배열의 첫 번째 원소 ary[0]:" << ary[0] << endl;
	cout << "배열의 두 번째 원소 ary[1]:" << ary[1] << endl;
	cout << "배열의 세 번째 원소 ary[2]:" << ary[2] << endl;
	return 0;
}

바로 주소 값이 나옵니다!

 

주소 값이 나오는 것을 확인.

 

주소값을 저장할 수 있는 변수는 포인터 변수뿐이예요. 여기서 우리는 일단 배열의 이름의 정체는 바로 포인터 변수였음을 추측할 수 있어요. (아직 정확히 정답은 아님)

 

[※ 배열의 이름 역참조 해보기]

ary가 포인터 변수라면, 역참조가 가능하겠네요?

ary가 가진 저 주소에 어떤 값이 저장되어 있을까요?? 한 번 코드로 확인해볼까요~?

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	cout << "배열의 이름 ary:\t" << ary << endl;
	cout << "배열의 이름 역참조 *ary:\t" << *ary << endl;
	cout << "배열의 첫 번째 원소 ary[0]:\t" << ary[0] << endl;
	return 0;
}

ary가 포인터 변수라면, *ary 하면 포인터 변수에 저장되어 있는 주소가 가리키는 값을 역참조할 수 있겠죠!

 

 

결과를 수행해보니, *ary값이 ary[0]과 같음을 알 수 있어요. 

우연히 이 둘이 같은 값을 가진게 아니라, 이 둘이 가리키는게 같은 공간이라 같은 값을 출력해준 건지 확인해보려면, 

ary랑 &ary[0] 값을 비교해보면 되겠죠? 

참고로 RAM은 휘발성이기 때문에, 종료 후 그 다음 실행하였을 경우 ary가 저장되는 번지가 바뀔 수 있습니다.

 

 

출력 결과 둘이 완전 가리키는게 같음을 알 수 있습니다.

즉!  배열의 이름은 배열의 시작 주소 값이다. 

 

[※ 배열의 이름은 포인터 상수]

배열의 이름이 배열의 시작 주소 값을 가진 포인터 변수라면, 

주소 값 변경도 가능하겠네요??

#include <iostream>
using namespace std;
int main()
{
	int num1 = 3, num2 = 5;
	int* ptr = NULL;
	ptr = &num1; //포인터 변수 ptr값을 num1의 주소 값으로 저장.
	ptr = &num2; //포인터 변수 ptr값을 num1의 주소 값이 아닌 num2의 주소 값으로 변경 
	return 0;
}

포인터 변수 또한 변수이기 때문에 이렇게 저장하는 주소 데이터를 변경할 수 있잖아요~

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	int num = 5;
	// ary = &num;  // 컴파일 에러 발생! 
	return 0;
}

하지만 실제 수행을 해보면 컴파일 에러가 발생하는 것을 확인할 수 있습니다.

여기서 우리는 배열의 이름은 포인터 변수라고 하기보단, 변경할 수 없는 포인터 상수에 가까움을 확인할 수 있습니다.

배열의 이름의 정체는, 배열의 시작 주소 값을 가리키는 상수 포인터이다.

 

[※ 배열 이름, 배열 이름의 주소, 배열 시작 주소는 다 동일??]

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	cout << "배열의 이름 ary:\t" << ary << endl;
	cout << "배열의 이름 주소 &ary:\t" << &ary << endl;
	cout << "배열 시작 주소 &ary[0]:\t" << &ary[0] << endl;
	return 0;
}

포인터 상수의 주소 값을 보고 싶어서 &ary를 출력해봤어요. 

 

 

ary가 포인터 변수라면(정확힌 포인터 상수), ary의 주소값과 ary가 가진 값이 어떻게 동일할 수 있죠?

ary와 &ary는 같은값을 가지고 있어 동일해보이지만 사실 동일하지 않아요, 서로 다른 타입을가지고 있습니다.

이걸 확인할 수 있는 방법은 간단해요.

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	cout << "배열의 이름 ary:\t" << ary << endl;
	cout << "배열의 이름 주소 &ary:\t" << &ary << endl;
	cout << "배열 시작 주소 &ary[0]:\t" << &ary[0] << endl;

	cout << "ary + 1:\t" << ary + 1 << endl;
	cout << "&ary + 1:\t" << &ary + 1 << endl;
	return 0;
}

만약 ary와 &ary가 같다면 (ary) +1 값과 (&ary) +1 값이 같아야겠죠?

 

 

근데 실제로 코드를 실행해보면 값이 다른것을 확인할수 있습니다.

ary + 1은 ary주소 보다 4바이트가 늘어났고 &ary + 1은 C만큼 늘어났으니 기존 값보다 12바이트가 증가했네요.

결국 ary와 &ary는 완전 같은게 아니다! ary와 &ary는 타입이 다릅니다.

 

[※ 배열 이름 원리 ary + i 는 &ary[i]이다]

위의 소스코드에서, 배열이름인 ary에다가 1을 더한, ary +1 했을 때 4바이트만큼 차이가 난 것은 ary가 int 타입의 포인터기 때문이겠죠? 포인터 연산의 특징을 이용해서 우리는 각 배열의 원소에 접근할 수 있습니다.

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	cout << "ary:\t\t" << ary << endl;
	cout << "&ary[0]\t\t" << &ary[0] << endl;
	cout << "ary + 1:\t" << ary + 1 << endl;
	cout << "&ary[1]\t\t" << &ary[1] << endl;
	cout << "ary + 2:\t" << ary + 2 << endl;
	cout << "&ary[2]\t\t" << &ary[2] << endl;
	cout << "ary + 3:\t" << ary + 3 << endl;
	cout << "&ary[3]\t\t" << &ary[3] << endl;
	return 0;
}

확인을 위해 위 코드를 수행해볼게요.

 

 

ary + i는 ary[i] 주소 값을 가리킨 다는 것을 확인할 수 있습니다. ary + i 는 &ary[i]와 동일하다.

그렇다는 건 결국 *(ary + i) 는 ary[i]와 동일하다 라는 의미 또한 되겠죠!

 

[포인터 연산 체크]

#include <stdio.h>
int main()
{
	char ch = 0x30;
	char* ptr = &ch;
	printf("ptr %p\n", ptr);
	printf("ptr+1 %p\n", ptr+1);

	int num = 3;
	int* pnum = &num;
	printf("pnum %p\n", pnum);
	printf("pnum+1 %p\n", pnum+1);
	return 0;
}

혹 포인터 개념 헷갈리시는 분이 계실까봐 같은 유형인 포인터 연산 기본 코드를 첨부해봤어요.

 

 

ptr은 char타입의 포인터 변수라 ptr+1 했을 때 ptr과 1바이트 차이만 나는거고ㅡ

pnum은 int 타입의 포인터 변수라 pnum+1 했을 때 4바이트만큼 차이가 나는 것. 

 

[※ ary주소 (&ary)의 정체]

그럼 위에서 &ary + 1는 왜 12바이트 차이가 났을까요?

일단 이 배열의 크기는 int 타입이 3개의 공간 즉 4*3 = 12 바이트이죠

즉 &ary에서 ary는 이 배열 오브젝트 그 자체를 의미함을 알 수 있습니다. 배열 요소들에 대한 포인터가 아니라 배열 그 자체에 대한 포인터 인거죠.

 

이를 심플하게 확인할 수 있는 코드 하나 살펴볼게요.

#include <stdio.h>
int main()
{
	char ary[10];
	printf("&ary :%p\t (&ary + 1) :%p\n", &ary, (&ary + 1)); //ary배열 전체 크기인 10 차이가 남.
	printf("ary :%p\t  (ary + 1) :%p\n", ary, ary+1);
	printf("sizeof(ary) :%d\n", sizeof(ary)); // 배열 크기 10바이트
	return 0;
}

자 ary는 1바이트 char 타입의 문자열 배열 이름이예요. 

 

 

출력을 해보면 &ary와 ary는 동일하지만

&ary와 &ary + 1은 10바이트 차이가 나는 것을 확인할 수 있어요.  이는 곧 ary 배열 전체 크기와 같은 사이즈이죠.

즉, ary와 &ary는 값은 똑같지만 의미하는 것은 다르다. 

 

조오끔만 더 깊이 들어가보자면 &ary에서 ary는 이 배열 오브젝트 그 자체의 주소를 의미하니 배열포인터에 대입할 수 있겠죠?

#include <iostream>
using namespace std;
int main()
{
	int ary[10] = { 1,2,3 };
	int(*ptr)[10] = &ary;
	cout << &ary << endl;
	cout << ptr << endl;
	cout << sizeof(ary) << endl;
	cout << sizeof(*ptr) << endl;
	return 0;
}

결국 두 코드가 가리키고자 하는 것은 같음을 알 수 있습니다.

결과!

포인터를 배열처럼 사용하기. 포인터로 배열 접근하기

배열의 정체가 결국 포인터라면,, 배열 연산자 대괄호'[ ]'를 쓰지 않고도 접근할 수 있어야겠죠?

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	int* ptr = NULL;
	ptr = ary;
	//index 0 원소 주소 접근
	cout << "------------index 0 요소 주소 접근-----------" << endl;
	cout << "ptr :\t\t" << ptr << endl;
	cout << "&ary[0]:\t" << &ary[0] << endl;
	//index 0 원소 값 접근  
	cout << "------------index 0 요소 값 접근------------" << endl;
	cout << "*ptr:\t" << *ptr << endl;
	cout << "ary[0]:\t" << ary[0] << endl;
	cout << "*ary:\t" << *ary << endl;
	//index 1 원소 주소 접근
	cout << "------------index 1 요소 주소 접근-----------" << endl;
	cout << "ptr+1:\t\t" << ptr + 1 << endl;
	cout << "&ary[1]:\t" << &ary[1] << endl;
	//index 1 원소 값 접근  
	cout << "------------index 1 요소 값 접근------------" << endl;
	cout << "*(ptr+1):\t" << *(ptr+1) << endl;
	cout << "ary[1]:\t\t" << ary[1] << endl;
	cout << "*(ary+1):\t" << *(ary+1) << endl;
	//index 2 원소 주소 접근
	cout << "------------index 2 요소 주소 접근-----------" << endl;
	cout << "ptr+2:\t\t" << ptr + 2 << endl;
	cout << "&ary[2]:\t" << &ary[2] << endl;
	//index 2 원소 값 접근  
	cout << "------------index 1 요소 값 접근------------" << endl;
	cout << "*(ptr+2):\t" << *(ptr + 2) << endl;
	cout << "ary[2]:\t\t" << ary[2] << endl;
	cout << "*(ary+2):\t" << *(ary + 2) << endl;

	return 0;
}

앞의 내용들을 다 이해했으면 이제 해당 코드가 왜 이렇게 매핑될 수 있는지 아실거예요!

 

 

출력값을 보면서 다시 한 번 배웠던 개념을 정리하기!

 

[※ 포인터를 이용해 배열원소 값 출력하기]

#include <iostream>
using namespace std;
int main()
{
	int ary[] = { 1,2,3 };
	
	for (int index = 0; index < sizeof(ary)/sizeof(ary[0]); index++)
		cout << *(ary + index) << endl;
	return 0;
}

간단하죵?

 

앞으로 2차원 배열, 3차원 배열, 이중 포인터, 삼중 포인터로 들어가게 되면 포인터는 매우매우 헷갈려지기 시작합니다..

요 기본 개념을 확실히 알고 넘어가는 것과, 대강 넘어가는 것의 차이는 커요! 

 

도움이 되셨다면 공감/댓글/광고보답 감사합니다. 

더 나은 정보공유를 위하여`~~ 오늘 하루도 수고하셨어요. 화이팅~!

 

반응형
  • ㅇㅇ 2021.08.30 00:30

    햄찌님 좋은 글 감사합니다. 한가지 살짝 아리까리해서 질문 드려요.

    포인터 변수에 +1 을 했을 때 가리키는 자료형의 크기에 종속되는 이유는 메모리상에서 그 자료형이 차지하는 공간 때문인가요?

    예를 들어 4byte 자료형 int a = 1; 이 00x1 00x2 00x3 00x4 의 공간을 차지하고 있을 때

    int *b = &a; 로 포인터 변수 선언시, b는 00x1을 가리키고 있고 b + 1을 하면 다음에 차지할 수 있는 공간인 00x5가 되는 거죠?

  • c부터 배우자 2021.09.07 20:01

    오 답변 감사드립니다.