이제 포인터를 복습해보자. ( 포인터라는 이 단어만 듣더라도 겁부터 먹게 된다. )
📖UNIT34
📒포인터에 대하여 ...
이를 이해하기 위하여 먼저 변수와 메모리에 대해서 어느정도 알아야 한다.
int num1 = 10; 이런 코드를 작성한다고 생각해보자.
그럼 num1이라는 공간은 메모리 위에 자리를 잡게 된다. 그 주소는 어떻게 알 수 있을까.
간단한 코드를 작성해보자.
#include <stdio.h>
int main(){
int num1 = 10;
printf("%p ", &num1);
return 0;
}
변수의 주소를 출력하는 코드이다.
이렇게 실행할 때 마다 다른 메모리 주소를 출력한다. ( 마지막의 '%' 는 왜 출력이 되는 지 모르겠음, 터미널 오류 )
어쨌든, 그럼 이렇게 구한 변수의 메모리 주소를 어디에 저장을 해야할까?
그걸 바로 포인트 변수에 선언한다. 좀 더 자세히 알아보자.
#include <stdio.h>
int main(){
int num1 = 10;
int *prtnum = &num1;
printf("%p\n", &num1);
printf("%p\n", prtnum);
}
이렇게 코드를 작성해주고 gcc를 이용하여 컴파일 해보자.
결국 num1의 주소가 ptrnum에 저장되었다는 것을 확인할 수 있다.
printf("%p\n", &num1);
printf("%p\n", &prtnum);
물론 위와같은 코드를 작성하게 되면 ptrnum 자체의 주소를 출력하게 되므로 위 아래 출력값이 달라지게 된다.
📒역참조 연산 사용하기
이번엔 역참조 연산에 대하여 알아보자.
먼저 포인터 변수에는 메모리 주소가 저장되어있다.
그럼 그 메모리 주소의 값을 바꾸고싶으면 어떡하면 될까? 바로 그 때 역참조 연산을 사용하는 것이다.
역참조 연산자는 '*' 이다.
좀 더 자세히 이야기 해보자. (이야기는 코드로 하는 것이 낫다.)
자세히 이야기 하기 전에 내가 위에 쓴 글을 그림으로 표현해보자.
그래서 numPtr을 역참조하여 num1의 값을 바꾸는 과정을 의미하는 것이다.
코드를 짜보자.
#include <stdio.h>
int main(){
int *numPtr;
int num1 = 10;
numPtr = &num1;
*numPtr = 20;
printf("%d\n", *numPtr);
printf("%d\n", num1);
return 0;
}
이렇게 코드를 짤 수 있겠다.
실행을 해보면, 20과 20이 출력된다.
먼저 numPtr 포인터 변수에는 num1의 메모리 주소가 들어가진다. 즉 선언을 할 때도 *를 사용한다.
이 말은 numPtr을 포인터 변수로 선언하겠다. 정도로 생각할 수 있겠다.
그 밑에서 *numPtr = 20 이라는 코드가 나오는데, numPtr 포인터 변수를 역참조 하여 20을 넣겠다는 소리이다.
역참조를 하므로 numPtr이 가리키고 있는 메모리 주소의 값이 가리키는 값을 바꾸는 것이다.
즉, numPtr은 num1의 메모리 주소를 가리키고 있고, 이 메모리 주소에 해당하는 값을 바꾼다는 소리이다.
즉, num1의 근본적인 값이 변한다. 라고 생각할 수 있겠다 ?
( '근본적인' 이라는 표현을 사용한 이유는 함수가 등장하면 복잡해질까봐...)
이 결과를 통해서 우리가 알아야 할 것은 한가지 더 있다.
먼저 numPtr과 num1의 자료형은 다르다. ( numPtr은 포인터 변수이므로. )
하지만, *numPtr과 num1의 자료형은 같다. ( *numPtr은 역참조를 진행하므로 정수형이 된다. )
그러니까 int *numPtr을 쓰는 것 처럼, *numPtr은 정수구나... 라고 생각할 수 있겠다.
📒일반변수와 포인터변수
그럼 일반 변수와 포인터 변수의 차이를 좀 생각해보자.
int num1;
int *numPtr;
numPtr = &num1;
현재 이렇게 변수가 선언되어있다.
- num1 변수의 메모리 주소를 몰라도 변수에 값을 가져오거나 변경할 수 있다.
- &num1은 단지 num1의 메모리 주소만 가르킨다.
- *numPtr은 정확히는 메모리에 저장된 값에 접근하는 것이다. (근본을 ?) 그래서 값을 가져오거나 변경할 수 있다.
- numPtr은 단지 num1의 메모리 주소만 가르킨다.
📒디버거에서 포인터 확인하기
#include <stdio.h>
int main(){
int *numPtr;
int num1 = 10;
numPtr = &num1;
*numPtr = 20;
printf("%d\n", *numPtr);
printf("%d\n", num1);
return 0;
}
다시 이 코드를 작성해보자.
그리고 7번째 줄에 BP를 걸어보자.
lldb를 이용하여 디버깅해보자.
우선 numPtr이 가리키는 값을 확인해보자.
0x00007ffeefbff4dc 라는 주소값이 들어가졌다.
현재는 *numPtr = 20; 명령을 실행하기 전이다. 그러므로 0x00007ffeefbff4dc 이 주소의 값을 확인해보자.
그렇다 ... 흠... int는 4바이트다. 그러므로 0a 00 00 00 이 현재 *numPtr의 부분이다.
0a 00 00 00 은 리틀 엔디언 방식의 표기법이다. 원래 값은 00 00 00 0a가 된다.
그러므로 원래 0a를 가지고 있었다는 소리이다. => 10을 가지고 있었다.
이제 F10을 눌러서 한 줄 더 실행하여 0x00007ffeefbff4dc 의 값을 확인해보자.
( *ptrNum = 20; )
이렇게 값이 변함을 확인할 수 있다. 00 00 00 14의 값을 갖는다. 16진수 14는 10진수로 20이다.
헤헤 기분이 좋다. 내가 예측한 대로 프로그램이 흘러가면 너무 행복하다. 비록 쉬운 코드일지라도 ...
📒다양한 자료형의 포인터
지금까지는 int의 포인터에 대해 알아보았다. 자료형에는 int 뿐 만 아니라 char, float 등등 다양하게 있다.
이런 다양한 자료형들은 포인터 변수를 어떻게 선언해줘야 할까 ?
#include <stdio.h>
int main()
{
long long *numPtr1;
float *numPtr2;
char *cPtr1;
long long num1 = 10;
float num2 = 3.5f;
char c1 = 'a';
numPtr1 = &num1;
numPtr2 = &num2;
cPtr1 = &c1;
printf("%lld\n", *numPtr1);
printf("%f\n", *numPtr2);
printf("%c\n", *cPtr1);
return 0;
}
그냥 이정도의 코드를 작성해봐도 바로 이해할 수 있다.
포인터 변수는 원래 값을 담을 자료형이랑 똑같이 선언해줘야 한다.
📒void 포인터 선언
long numPtr1; 또는 long long numPtr2 는 자료형이 정해진 포인터이다.
하지만, 아직은 배우지 않겠지만 c언어를 공부하다 보면 자료형이 정해지지 않은 포인터를 다루어야 할 때도 있다.
그럴 때 사용하는 자료형이 바로 void 자료형이다.
이 표를 보면 알 수 있듯이, void로 선언한 포인터 변수는 모든 자료형이 될 수 있다.
하지만 자료형이 정해지지 않았으므로 역참조를 진행할 수 없다.
📒이중 포인터
이 내용은 꽤 흥미롭다. 사실 보자마자 미적분의 이계도함수를 떠올리게 되었다.
물론 개념은 다르지만 뭔가 느낌이 비슷하였다. 이계도함수는 도함수에 대한 정보들을 또 가져오는데,
그것과 유사하게 이중 포인터 역시 원래 포인터 변수 자체의 메모리 주소값을 또 가져온다는 점이 매우 신기했다.
#include <stdio.h>
int main()
{
int *numPtr1;
int **numPtr2;
int num1 = 10;
numPtr1 = &num1;
numPtr2 = &numPtr1;
printf("%d\n", **numPtr2);
return 0;
}
하지만 코드는 그리 신기하지 않았다.
**numPtr2가 가리키는 것이 무엇일지 한번 생각해보자. 먼저 *(*numPtr2)로 분리할 수 있겠다.
*numPtr2 는 현재 numPtr1의 값을 가리킨다.
그러므로 *(numPtr1)이 된다. *numPtr1은 num1의 값을 역참조한다.
이런 표현을 통해 역참조를 여러번 진행할 수도 있다.
출력은 당연히 10이 출력된다.
이런 포인터를 이렇게 저렇게 해서 해킹에 사용할 수도 있을까.
예를 들어 크롬이 차지한 메모리 영역을 내가 만든 프로그램에서 바꾼다던디 ....
📖UNIT35
지금까지는 포인터 변수를 이용하여 메모리 주소를 저장하는 방식으로 포인터를 사용해 보았다.
이번에는 포인터에 원하는 만큼 메모리를 할당받아 사용해보자.
메모리는 malloc -> 사용 -> free 의 패턴을 거친다.
📒메모리 할당
자, 그럼 먼저 메모리를 할당해보자.
할당은 memory allocation, 즉 malloc 함수를 사용한다. (stdlib.h)
포인터 = malloc(크기) 이것이 기본형이다.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num1 = 20;
int *numPtr1;
numPtr1 = &num1;
int *numPtr2;
numPtr2 = malloc(sizeof(int));
printf("%p\n", numPtr1);
printf("%p\n", numPtr2);
free(numPtr2);
return 0;
}
예제 코드이지만 이런 과정을 설명하기에 제일 좋은 코드라고 생각된다.
먼저 numPtr1에는 num1의 주소를 넣어준다. 그리고 numPtr2에는 4바이트 크기의 메모리 영역을 할당시킨다.
이렇게 원하는 시점에 메모리 할당하는 것을 동적 메모리 할당이라고 한다.
우선 우리가 변수로 선언해준 애들은 스택에 선언이 된다. 스택은 뭐 당연히 스택 포인터가 왔다갔다 하면서 값이 변하기 때문에 따로 free를 해줄 필요가 없다. 하지만 자료구조의 관점에서 힙을 바라보자. 힙은 우리가 따로 free를 하지 않으면 메모리 낭비가 된다.
그렇기에 힙 영역에 선언한 (malloc)은 꼭 free를 통해 메모리를 해제해줘야 한다.
free(numPtr2) 이렇게 메모리를 해제해줄 수 있다.
📒메모리에 값 저장하기
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *numPtr;
numPtr = malloc(sizeof(int));
*numPtr = 10;
printf("%d\n", *numPtr);
free(numPtr);
return 0;
}
위에서는 메모리 영역을 잡아주었다. 그럼 이번엔 그 메모리 영역에 값을 집어넣어보자.
먼저 numPtr에 정수크기의 영역을 잡아주고 값을 넣어주면 끝난다.
📒널(NULL) 포인터
만약에 메모리가 00000000000000을 가리킨다면, 그것이 널 포인터이다. 즉, 아무것도 가리키지 않는 상태이다.
'스터디 그룹 > ProjectH4C' 카테고리의 다른 글
ProjectH4C 2개월 3주차 과제 (포인터 5문제) (0) | 2021.03.07 |
---|---|
ProjectH4C 2개월 3주차 과제 (UNIT36, 37, 38) (0) | 2021.03.07 |
ProjectH4C 2개월 2주차 과제 - 10문제 write-up (0) | 2021.02.26 |
ProjectH4C 2개월 1주차 과제 C & Python (2) | 2021.02.20 |
ProjectH4C 2개월 1주차 과제 (UNIT16 ~ UNIT33) (0) | 2021.02.20 |