Ron Weasley 2022. 8. 16. 16:30

혼공 8기로 마지막이네요 ㅎㅎ 많은 것을 배워왔지만...유독 습득이 늦은 챕터입니다.. "포인터" 악마입니다.

 

우리가 포인터를 배우기 전에 잠깐 지금가지 배운것을 정리를 해보면 변수 선언으로 메모리에 공간을 확보하고, 그곳을 데이터를 넣고 꺼내쓰는 공간으로 사용했습니다. 변수명은 확보된 메모리 공간을 식별할 수 있는 이름이였습니다. 그러나 변수는 블록, 함수 내부로 사용이 제한되어 있었습니다. 같은 변수명을 사용했다 하더라도 블록이나 함수가 다르면 "별도의 저장곤간을 확보" 하므로 전혀 다른 변수로 사용되는 것이죠...그래서 사용 범위를 벗어난 경우도 데이터를 공유할 수 있는 새로운 방법인 "포인터"의 개념에 배워보도록 하겠습니다.

 

메모리 주소

메모리라는 것은 우리가 데이터를 넣고 꺼내 쓰는 공간으로, 그 위치를 식별할 수 있어야 합니다. 우리가 집에 라면을 꺼내먹을 때 라면이 담긴 통을 찾을 것입니다. 하지만 그것을 통이 어딨는지 찾을 수 없다면 라면은 새로 사러 가야겠죠? 이처럼 번거로운 일을 C언어는 프로그램이 사용하는 메모리의 위치를 주소 값으로 식별할 수 있게 해줍니다. 메모리의 위치를 식별하는 주소 값은 바이트 단위로 구분이 됩니다. 이 값은 0x0부터 시작하고 바이트 단위로 1씩 증가하므로 2바이트 이상의 크기를 갖는 변수는 여러 개의 주소 값에 걸쳐서 할당됩니다.

 

주소 연산자 : &

우리가 지금까지 scanf를 쓰면서 "&변수명" 을 입력을 했던것을 기억을 할 것입니다. 이제는 & 연산자를 알아야 할 때 입니다. 바로 주소 연산자입니다. 여기서 주소라 하면 변수가 할당된 메모리 공간의 시작 주소를 의미합니다. 시작 주소를 알면 그 위치부터 변수의 크기만킄 메모리를 사용할 수 있습니다. 예제 코드를 한번 볼까요?

위에서 나온 결과값으로 잠깐 살펴보겠습니다.

물론 저와 같은 주소가 나온 사람들은 없었을겁니다. 왜냐면 컴퓨터는 프로그램 실행 후 남아 있는 메모리를 활용하므로 실행결과가 매번 바뀌기 때문입니다. 그리고 주소 연산자를 사용하여 변수에 할당된 메모리의 시작 주소를 확인하고 시작 주소에 변수의 크기를 더하면 변수가 메모리의 어디서부터 어디까지 할당되었는지 확인할 수 있습니다.

 

제가 쓰고 있는 맥 같은 경우는 주소가 좀 이어져있네요 그래서 double, char형 변수의 메모리 크기는 바로 나옵니다.

double은 8바이트, char는 1바이트, 문제는 int형입니다. 하지만 어려울 것이 없는게 int형은 4바이트죠? 그래서 1862808328 + 3을 하시면 됩니다. 왜 +3이면 지금 프로그램 결과값에 나온 주소도 1바이트로 포함이 되기 때문입니다. 그럼 메모리의 시작 주소는! 1862808331 ~ 1862808328까지 int형, 1862808328 ~ 1862808320까지 double형, 1862808320 ~ 1862808319까지 char형 입니다. 이제 다 이해가 되십니까??

 

그리고 %u 서식 지정자를 사용하게 되면 에러가 날겁니다. 왜냐면 제가 저번에...%p를 쓰라고 말씀드렸었습니다. 그게 이 이유입니다.

 

포인터와 간접 참조 연산자 : *

이 연산자는 사용하고나면 이제 겉잡을 수 없이 포인터의 세계로 빠져드는 것입니다. 메모리 주소는 필요할 때마다 계속 주소 연산을 수행하는 것 보다 한 번 구한 주소를 저장해서 사용하면 편리한데, 포인터가 바로 "변수의 메모리 주소를 저장하는 변수" 입니다. 

결과값을 보시면 둘다 10을 출력합니다. 그럼 메모리 주소도 한번 볼까요?

제 맥에서는...참 이상해서 윈도우에서 실행을 시켜보니 메모리 주소가 0x1234로 출력되는 것과 0x0000000001234로 출력이 되었습니다.  제 맥이 이상한거라...여러분들은 잘 구해지셨죠? ㅎㅎ

 

아무튼 같은 주소가 나올겁니다! 나와야 정상입니다..! (전 비정상..)

 

일단, 포인터 선언하는 것부터 보자면 다음과 같습니다.

자료형 *변수명;
int *pa;

일반 변수명을 만드는 규칙에 따라 포인터 이름을 짓고, 변수명 앞에 *를 붙입니다. *는 포인터임을 표시하는 기호입니다. 그리고 자료형을 적는데, 포인터의 자료형은 변수의 자료형을 적습니다. 그리고 포인터 변수가 선언되면 일반 변수와 마찬가지로 메모리에 저장 공간이 할당되고 그 이후에는 변수명으로 사용할 수 있습니다.

pa = &a

이제 포인터 pa는 변수 a가 메모리 어디에 할당되었는지 그 위치를 기억하고 있습니다. 이렇게 포인터가 어떤 변수의 주소를 저장한 경우 "가리킨다" 라고 하고 둘의 관계를 pa -> a 처럼 화살표로 표현할 수 있습니다.

 

여기까지 볼 때 포인터 pa로 변수 a를 사용할 수 있으며, 포인터가 가리키는 변수를 사용할 때는 포인터에 특별한 연산자를 사용하는데 이를 간접 참조 연산자( * ) 라고 합니다. 또는 포인터 연산자입니다.

 

여러 가지 포인터 사용해보기

포인터가 어떤 변수를 가리키게 되면 그 이후에는 간접 참조 연산자를 통해 가리키는 변수를 자유롭게 쓸 수 있습니다.

간단하게 위에 변수 선언된 부분을 제외하고, 포인터 부분만 보자면 7 ~ 9행은 int *pa와 *pb는 초기화 되지 않은 상태로 선언을 하였고, *pt는 두 변수의 합을 구할 total 변수의 주소를 가르키고 있습니다. *pg는 두 변수의 평균을 가지는 avg 변수의 주소를 가르키고 있습니다.

 

10 ~ 15행은 이제 포인터 pa가 a의 주소를 가르킨다고 되어 있으며, pb는 b의 주소를 가르킨다고 되어 있네요..!

*pt는 이제 포인터 변수 pa와 pb가 값을 가지고 있으니 더 해주면 되고, 마찬가지로 *pg도 같습니다.

 

사람들이 포인터를 처음 할 때, 물론 저도 그랬지만 출력문을 할 때 pa를 해야하는지...*pa를 해야하는지 진짜 헷갈릴거 다 압니다.

왜냐면 저도 이거 이해하는데 몇 일 걸렸거든요 (물론 저는 바보니까 그런겁니다)

* 연산자가 있으면 뭐라고 했죠? 가르킨다고 했습니다. 그럼 pa는 뭘까요? 그냥 초기화 되지 않은 친구입니다. 한번 볼게요!

보세요! 그냥 a의 메모리 주소만 알려줄 뿐 값을 알려주진 않죠.

a가 가지고 있는 값을 접근하려면 * 연산자를 사용해야 하는 것을 비로소 알 수 있습니다.

 

그다음...하지말까? 고민 했는데 해야되는 const입니다...

 

Const를 사용한 포인터

이게...진짜 어렵습니다 ㅜㅜ 아직도 이해 안되는 부분이구요...! (도망갈까??)

일단 적어는 보겠습니다. const 예약어는 상수를 의미하죠? 그럼 포인터에 사용하면 가리키는 변수의 값을 바꿀 수 없다는 의미를 가지고, 변수에 사용하는 것과는 다른 의미를 가지게 됩니다.

상식적으로 우리가 생각할 때 const가 일반 변수처럼 포인터 값을 고정시킨다면 에러가 나는 프로그램입니다. 하지만 출력 결과를 볼까요?

엥...값이 바뀌었네요? 도대체 뭐하는놈이지 생각할겁니다. 저는 지금 포스팅하고 있는 입장인데도 그런 생각이 듭니다.

왜그러냐면! pa가 가리키는 변수 a는 pa를 간접 참조하여 바꿀 수 없다는 것입니다. 그래서 이건 또 무슨말인데??

*pa = 20;

이렇게 선언했을 때 에러가 난다는 말입니다.

그니까, 결국엔 *pa로 값을 바꾸고자 할 때 에러 메세지를 띄운다는 것이네요... (진짜 더럽네여..)

나중에 const int const *pa 막 이런 요상한 애들 나오는데...전 못할거 같아요.. (여러분들은 성공하세여!)

 

주소와 포인터의 차이

주소와 포인터의 차이는 뭘까요?? 주소는 변수에 할당된 메모리 저장 공간의 시작 주소 값 자체이고, 포인터는 그 값을 저장하는 또 다른 메모리 공간입니다. 따라서 특정 변수의 주소 값은 바뀌지 않지만 포인터는 다른 주소를 대입하여 그 값을 바꿀 수 있습니다.

 

주소와 포인터의 크기

포인터도 저장 공긴이므로 크기가 있습니다. 포인터의 크기는 저장할 주소의 크기에 따라 결정되는데 크기가 클수록 더 넓은 범위의 메모리를 사용할 수 있습니다. 포인터의 크기는 컴파일러에 따라 다를 수 있으나 모든 주소와 포인터는 가리키는 자료형과 상관없이 그 크기가 같다는 것에는 변함이 없습니다. sizeof 연산자로 확인을 해보겠습니다.

여기서 포인터에 간접 참조 연산자를 사용하여 가리키는 변수의 크기만 다른 결과가 나오는 것을 볼 수 있습니다.

 

포인터를 사용하는 이유

변수를 사용하는 가장 쉬운 방법은 이름을 쓰는 겁니다. 포인터를 사용하려면 추가적인 변수 선언이 필요하고 주소 연산, 간접 참조 연산 등 각종 연산을 수행해야 합니다. 그러니 포인터를 일부러 사용하실 이유는 없습니다. 하지만 저 처럼 바보들은 멋부린다고 사용하거든요?? ㅋㅋㅋㅋㅋㅋ 진짜 사용해야 되는 곳은 임베디드 프로그래밍을 할 때 메모리에 직접 접근하는 경우나 동적 할당한 메모리를 사용하는 경우에 포인터가 반드시 필요합니다..!

 

나중에 배우시겠지만 포인터를 사용하게 되면 "참조에 의한 호출", "값에 의한 호출", "포인터에 의한 호출" 이라는 call by value, call by reference, call by pointer를 듣게 되실건데..이게 예제를 한번 볼게요

값이 바뀌었네요? 이게 바로 참조에 의한 호출입니다.

만약에 그냥 a, b를 줘볼까요?

변경되지 않았습니다..

먼저 위에서 바뀐 프로그램은 call by pointer(포인터에 의한 호출)을 사용한 것이고, 바뀌지 않은 프로그램은 call by value(값에 의한 호출)을 사용한 것 입니다. 이 차이점은 함수를 호출할 때 주소를 넘기냐, 값을 넘기냐 차이입니다. 위에서 보면 swap() 함수에 &a, &b로 호출을 한 것을 보이시죠? 이겁니다!

 

<도전 실제 예제>

미니 정렬 프로그램
키보드로 실수 3개를 입력한 다음 큰 숫자부터 작은 숫자로 정렬한 뒤 출력하는 프로그램을 작성합니다. 다음 코드와 출력 결과를 참고하여 line_up 함수를 작성하세요. line_up 함수에는 이미 정의된 swap 함수를 호출하여 구현하세요.

 

 

여기까지 혼공단은 마쳤지만? 저는 더 공부할것입니다.