오늘은 C언어를 입문하는 사람에게 가장 어렵다고 소문난 포인터를 쉽게 이해할 수 있도록 설명해보겠습니다. 잘 이해되지 않는 부분은 댓글로 남겨주세요.
연산자 ( 배경지식 )
&(변수 이름) = 해당 변수의 주소
*(주소) = 해당 주소의 값
int a = 10;
printf("a의 주소 = %d\n",&a);
printf("해당 주소의 값 = %d", *(&a));
/// a의 주소 = 6422044 ( 사용자 마다 다름 )
/// 해당 주소의 값 = 10
변수 ( 배경지식 )
모든 변수와 값은 메모리의 어딘가에 저장되며 저장된 위치를 가르키는 것이 주소이다. 예를 들면 int a = 5; 라고 할 때 사용자가 편히 사용할 수 있도록 그냥 a만 사용하면 되지만 컴퓨터 내부에서 해당 주소의 값을 검색하는 작업이 일어난다.
포인터(Pointer)는 뭘까?
포인터란 "가르키는 것" 이라는 의미를 갖고 있으며 이는 "변수의 주소값"을 저장하는 자료형이다.
의문) 주소값을 저장하기 위해 구지 왜 포인터를?
위에서 %d로 출력하여 주소를 int형이나 다른 형태로 저장해도 되지 않을까 싶지만 사실 16진수이며 해당 값이 주소임을 표기해줘야 한다.
int a = 1; /// 정수형 변수를 선언하고 정수 1을 저장한다.
int* p = &a; /// 정수형 변수의 주소를 저장하는 포인터를 선언하고 주소를 저장한다.
Tip) 선언 과정에서 *p, p 가 햇갈릴 때
int *p1=&a;
/// 포인터 p1선언하고 a의 주소 저장.
int *p2;
///포인터 선언
p2 = &a; /// O, p2에 a의 주소를 저장
*p2 = &a; /// X, p2가 가르키는 주소에 가서 a의 주소를 저장.
맨 윗줄의 " * " 는 선언을 위해 사용된 것이며 그 아래에서 사용되는 " * " 는 역참조 연산을 의미합니다.
활용) Swap 코드 작성해보기
#include <stdio.h>
void swap(int *p1, int *p2)
{
int temp=*p1;
*p1=*p2, *p2=temp;
}
int main()
{
int a = 10, b = 20;
printf("%d %d\n", a, b);
swap(&a,&b);
printf("%d %d\n", a, b);
}
swap이 한 번만 일어나는 것이라면 사실 main에다 포인터를 쓰지 않고 만들어도 됩니다. 하지만 여러 곳에서 사용해야 하는 경우에 함수를 만드는 것이 효율적인데요, return을 이용해서 여러 값을 보낼 수 없으며 지역 변수이기에 다른 지역의 변수를 수정할 수 없습니다.
만약 포인터를 사용하지 않았다면 Swap함수에서 우연히 이름이 같은 변수지만 Main의 a, b와 다른 변수가 Swap에 a, b가 생성되며 Swap안에서 a, b의 연산은 Main 의 a, b에 영향을 주지 않습니다.
만약 Swap함수에 포인터를 매개 변수로 받았다면, 해당 포인터에 저장되는 값은 Main함수의 변수 a, b 의 주소가 저장되며 해당 변수의 주소에 직접 찾아가서 값을 변경하기 때문에 Swap 함수에서 Main 함수의 변수에 변화를 줄 수 있습니다. 이 때 주의할 점은 x, y에 저장된 값이 Main의 a, b 의 주소값이며 x, y의 주소는 별도입니다.
배열과 포인터
int a[10] = {0,1,2,3,4,5,6,7,8,9};
printf("%d %d %d",a, &a[0], a[0], *a);
/// 6422000 6422000 0 0
int b[4][3] = {0,1,2,3,4,5,6,7,8,9,10,11,12};
printf("%d %d %d %d",b ,b[2], &b[2][0], b[2][0]);
/// 6422000 6422024 6422024 6
배열을 전부 indexing 한다면 해당 위치에 존재하는 값을 출력하지만 그렇지 않을 경우 첫 번째 위치의 주소를 출력하게 됩니다. 따라서 b는 b[0][0]의 주소를, b[2] 는 b[2][0]의 주소를 의미합니다.
int b[4][3] = {0,1,2,3,4,5,6,7,8,9,10,11,12};
int *p1 = &b[0][0], *p2 = &b[0][0];
for(int i=0;i<12;i++,p1++, p2+=1){
printf("p1값 = %d, 해당 주소 값 = %d\n", p1, *p1);
printf("p2값 = %d, 해당 주소 값 = %d\n\n", p2, *p2);
}
/// 여러번 사용하니 위 예제와 주소가 다릅니다...
/*
p1값 = 6421968, 해당 주소 값 = 0
p2값 = 6421968, 해당 주소 값 = 0
p1값 = 6421972, 해당 주소 값 = 1
p2값 = 6421972, 해당 주소 값 = 1
p1값 = 6421976, 해당 주소 값 = 2
p2값 = 6421976, 해당 주소 값 = 2
*/
p1++, p2+=1 을 했는데 주소가 주소가 4씩 증가하는 것을 볼 수 있습니다. 포인터에서 숫자 n을 더하거나 빼는 행위는 "n칸 앞으로, 뒤로 간다." 라고 볼 수 있습니다. 참고로 int가 아닌 다른 변수형일 경우 1, 8 씩 변할 수 있습니다.
포인터배열 / 배열포인터
포인터배열은 배열이고, 배열포인터는 포인터다.
이게 무슨말이냐 하면... 포인터배열은 포인터를 저장하는 배열이고 배열포인터는 배열을 가르키는 포인터를 뜻합니다.
int main()
{
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int *p1[3]; p1[0]=a[0], p1[1]=a[1], p1[2]=a[2]; ///포인터배열
int (*p2)[3]=a; ///배열포인터
printf("%d %d", p1[1], p2[1]);
printf("%d\n",(*p2[3]));
printf("%d %d %d",p2[3], p2[3][1], *(p2[3]+1));
/// 6422000 6421996
/// 10
/// 6422020 11 11
}
일단 p1은 3개의 포인터를 저장하는 배열이며 p2는 배열 a를 가르키는 1개의 포인터 입니다. 주의할 점은 p1[1] 은 a[1][0]을 가르키지만, p2[1]은 a[0][3]을 가르킵니다. 또한 (*p2)[3]으로 선언해서 [2] 까지 indexing 가능할 줄 알았는데 [3]도 되는 것을 볼 수 있습니다.
여기서 주의할 점은 (*p2)[3]는 3개의 포인터를 선언하는 것이 아닌 1개의 포인터를 선언한 것 입니다! 또한 저 3이 의미하는 것은 다음에 들여올 배열을 길이 3으로 짜르겠다는 뜻 입니다.
따라서 p2[1] 은 3+1번째 원소의 주소를 나타내며 p2[3]은 3*3+1 번째 원소의 주소를, p2[3][1]는 11번째 주소의 값을, *(p2[3]+1) 또한 11번째 주소의 값을 의미합니다.
이걸 대체 왜 사용하냐면.... 배열을 포인터로 사용하고 싶은 경우 그냥 포인터로 사용하는 것은 2차원이나 더 높은 차원의 배열을 1차원으로 사용하는 것 같아서 쫌 더 배열처럼 쓰고 싶어서 만들었다고 하네여... (저는 잘 모르겠네요 ㅎㅎ)
포인터상수, 상수포인터
포인터상수는 상수인데 포인터고, 상수포인터는 포인터인데 가르키는 값이 상수다.
(이름을 왜 이따구로 짓는건지..) 위 처럼 정말 햇갈리는 이름입니다....
int x = 10;
const int y = 20;
int* const p1 = &x; /// 포인터 상수
const int* p2 = &y; /// 상수 포인터
p1=&y; /// 오류!
포인터 상수는 상수인데 포인터다, 즉 p1이 상수이며 포인터라는 소립니다. 따라서 p1에 저장된 값(주소) 를 변경할 수 없습니다. 반면 상수 포인터는 포인터인데 가르키는 값이 상수인 경우를 말합니다. 따라서 p2에 저장할 주소는 변경할 수 있지만 해당 주소의 값을 변경할 수 없습니다.
int* const p1 = &x; /// int형 포인터인데 상수인 p1을 선언한다 -> 주소 변경x
const int* p2; /// const int 형을 가르키는 포인터 p2를 선언한다. -> 해당 위치 값 변경x
다중포인터
일단 이중포인터만 다룰건데요... 이중을 이해하면 다중 포인터가 대체 뭔지... 이해하실 수 있을겁니다. 그냥 포인터 p1은 바로 a를 가르키지만 이중 포인터는 p1->p2->a 2번을 걸쳐서 a를 가르킵니다.
예제) Pointer_Swap 함수
void Pointer_Swap(int** p1, int** p2)
{
///p1 = pa의 주소 , p2 = pb의 주소
int *temp = *p1; /// pa의 값 = a의 주소
*p1 = *p2; /// pa의 값 = pb의 값 = b의 주소
*p2 = temp; /// pb의 값 = a의 주소
}
int main()
{
int a[5] = {1,2,3,4,5}, b[5]={11,12,13,14,15};
int *pa=a, *pb=b;
printf("a = %d, b = %d\n", a, b);
printf("pa = %d, pb = %d\n",&pa,&pb);
Pointer_Swap(&pa, &pb);
for(int i=0;i<5;i++) printf("%d ",*(pa+i));
puts("");
for(int i=0;i<5;i++,pb++) printf("%d ",*pb);
}
위 함수는 Swap함수랑 별 다를바가 없는데요, 차이점은 swap하는 대상이 포인터라는 사실입니다. 여기서 주의할 점은 배열 a, b를 직접 변경할 수는 없다는 사실입니다. 왜냐하면 이는 포인터 상수이기에 배열의 주소를 다른 주소로 바꿀수는 없습니다.
오늘은 포인터의 기초부터 심화까지 알아봤는데요... 포인터는... 직접 주소값을 출력하면서 이해하셔야 합니다. 하지만 포인터가 주소를 저장하는 변수라는 사실만 잘 인지하셨다면 오늘 포스팅은 대성공입니다.
오늘 포스팅은 굉장히 많이 걸렸는데요, 다음 시간에는.. 포인터랑 sizeof에 관해서 포스팅해보겠습니다... 왜냐하면 시험에 나올 것 같거든요...
'코딩 > C, C++' 카테고리의 다른 글
[자료구조] Ordered Linked List 구현하기 (0) | 2024.10.18 |
---|---|
[자료구조] 포인터와 동적 할당, 자료구조 입문하기! (with c언어) (0) | 2024.10.18 |
[C/C++] 자료형의 프로모션(실수 방지!) (0) | 2024.07.10 |
[C/C++] 부동소수점 - 컴퓨터는 정확하다며... (0) | 2024.03.30 |
[C/C++] 자료형 계산에서 자주하는 실수들 (1) | 2024.03.29 |