[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 = # // 컴파일 에러 발생!
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 = #
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차원 배열, 이중 포인터, 삼중 포인터로 들어가게 되면 포인터는 매우매우 헷갈려지기 시작합니다..
요 기본 개념을 확실히 알고 넘어가는 것과, 대강 넘어가는 것의 차이는 커요!
도움이 되셨다면 공감/댓글/광고보답 감사합니다.
더 나은 정보공유를 위하여`~~ 오늘 하루도 수고하셨어요. 화이팅~!
최신 댓글