포인터를 배우기 전까지 우리는 변수를 선언하여 메모리에 공간을 확보하고, 그 공간에 데이터를 넣고 쓰면서 변수를 사용했습니다. 그러나 main()함수 안에 작성된 변수는 지역 변수로, 사용할 수 있는 영역이 main()함수로 한정되어 있습니다. 그래서 어떠한 함수에서 다른 함수에서 선언된 변수의 메모리 공간에 접근하기 위해 포인터를 사용합니다.
메모리에는 주소가 있습니다. 현재 대부분의 컴퓨터에서는 64bit 운영체제를 쓰기 때문에 주소도 64bit로 나타냅니다. Byte단위로 1씩 증가할 때마다 주소 값도 1씩 증가합니다. 보통 주소는 16진수로 표현하기 때문에 16글자로 나타낼 수 있습니다.(그런데 왜 %p로 출력하면 12글자가 나오는 지 궁금합니다.)
포인터는 선언된 변수가 위치한 메모리 주소를 저장하는 변수입니다. 그래서 참조할 변수가 선언된 함수가 아닌 다른 함수에서는 변수명만 사용해선 접근할 수 없었지만, 포인터를 사용함으로써 메모리 공간에 직접적으로 접근하여 그 변수에 영향을 미칠 수 있게 됩니다.
포인터 변수에 저장된 메모리 주소에 저장된 값을 참조할 때는 *연산자를 씁니다. *연산자는 참조할 변수의 자료형 뒤에 붙여서 포인터다!라는 표시를 해주는 역할도 합니다.
주소 자체를 나타내는 연산자는 &연산자인데, 포인터 변수에 메모리 주소를 넣을 때, int* ptr = &a; 같은 식으로 사용합니다.
이제부터 nefus 수업 때 본 예제를 살펴보도록 하겠습니다.
다음은 변수 a를 선언하고, a의 주소를 포인터 변수 ptr에 저장한 모습입니다. a의 자료형인 int에 *을 붙여서 포인터 변수라고 알려준 것을 확인할 수 있습니다.
포인터 변수는 항상 주소값인 64비트를 저장하기 때문에 8byte입니다. 그런데 왜 자료형에 차이를 두는가하면, 저장할 변수의 자료형에 따라 메모리 공간의 크기가 다르기 때문에, 메모리 주소에 가서 몇 byte만큼 참조해 올 것인가가 달라지기 때문입니다. 예로, int형 변수의 메모리 주소를 저장했으면, 그 주소로 가서 값을 가져올 때에는 4byte만큼만 가져와야 할 것이고, double형 변수의 메모리 주소를 저장했으면, 8byte만큼만 값을 가져와야 할 것이기에 차이가 있습니다.
다음 예제입니다. 아까와 같은 코드에 printf문을 사용하여 &a와 p의 값을 출력해 주었습니다. 처음에 말했다시피 &는 주소를 나타내는 연산자이기에, 첫 줄에는 a의 주소가 나옵니다. 그리고 p에는 a의 주소를 저장해 두었기에, p의 값을 출력하면 a의 주소가 나옵니다. 참고로 %x는 값을 16진수로 출력하는 서식문자입니다.
결론적으로 p의 값과 a의 주소는 같다는 것을 확인할 수 있습니다.
다음 예제입니다. 아까와 같이 우선 p의 값을 출력합니다. 그리고 그 다음에는 마찬가지로 처음에 말했던, 포인터 변수에 저장된 메모리 주소에 저장된 값을 참조하는 *연산자를 사용하여 p에 저장된 a의 주소에 저장된 값을 출력합니다. 따라서 100이 출력되는 것을 확인할 수 있습니다.
다음 예제입니다. 이번에는 *연산자로 p에 저장된 a에 주소의 값에 100을 더해주었습니다. 이런 식으로 연산을 하면, 주소에 직접 들어가서 값을 변경해 준 것이기에 실제로 a의 값이 변화합니다.
출력을 해보면, p에 저장된 메모리에 저장된 값인 a의 값, 200을 확인할 수 있습니다. 다음 줄에 나온 것처럼 실제로 a의 값을 출력한 값과 동일합니다.
다음 예제입니다. 이 예제를 출력해서 알 수 있는 것은 p와 num과 &num[0]이 같고, *p와 num[0]의 값이 같다는 것입니다. 포인터 변수는 메모리 공간의 첫번째 주소를 참조하고, 배열은 변수명 자체가 주소를 나타내고, &num[0]은 변수 num의 첫번째 공간의 주소이기에 같습니다.
이를 통해 p를 통해 불러온 값과 num의 첫번째 값이 같다는 것도 쉽게 알 수 있습니다.
*p의 값이 왜 10 20 30이 아니라 10인가 하면, int*를 사용해 4byte만을 불러오겠다고 약속했기 때문에, 첫 주소에서 4byte까지의 공간, 즉 배열의 첫번째 공간에서만 값을 긁어왔기 때문입니다.
다음 예제입니다. 이번에는 포인터는 없지만, 아까 말했듯이 배열은 변수명 자체가 주소값을 가진다라고 했던 것을 기억하며 보도록 합니다. 일반적으로 for문으로 배열에 값을 입력할 때에는 num[i]를 사용했을 것입니다. 그런데 위 예제와, num[i]로 입력받고 출력한 프로그램의 결과값이 똑같습니다. 우리는 여기에서 num[i]와 num+i는 똑같은 것이구나를 알 수 있습니다. 다만 한 공간이 4byte임에도 그저 주소에 +0, +1, +2를 해줌으로 +0, +4, +8의 효과를 보는 것이 참 신기한 점입니다.
다음 예제입니다. 입력만 없을 뿐, 아까와 똑같은 내용입니다. 포인터 변수 p에 num의 주소를 저장했기에 p+i가 num+i와 같은 역할을 한다는 것을 확인할 수 있습니다.
이 예제도 위에서 num+i는 num[i]와 같다고 말한 것처럼 p+i는 p[i]와 같다는 것을 보여줍니다.
하지만 그러면 num을 사용하나, p를 사용하나 똑같은 게 아닌가? p와 num은 똑같은 건가? 생각이 들 수 도 있습니다.
이 예제를 보면 num과 p의 차이를 확실하게 알 수 있습니다. p는 8byte 크기를 가지고, num은 12byte 크기를 가집니다. 이는 포인터 연산자가 64비트 값을 저장하기에 8byte이고, num은 4byte int형 3공간을 가지고 있기에 12byte입니다. 하는 역할은 비슷하지만, 메모리를 얼만큼 차지하느냐에서 차이가 보입니다.
다음 예제입니다. 변수 num의 첫번째 공간과 세번째 공간이 주소 몇만큼 차이가 나는지를 확인할 수 있습니다.
이 코드는 간단한 사용 예시입니다. 여기서는 처음에 말했던 포인터의 특징이 보이는데, main함수에서 선언한 변수를 nump함수에서 변형시킬 수 있다는 것이 참 흥미롭습니다.
포인터를 사용한 함수들입니다. get함수는 변수 str에 문자를 받다가 엔터를 치면 함수를 종료시키고, put함수는 변수 str에 들어있는 문자를 출력해줍니다. 이 함수들은 포인터를 사용했기에, 이 함수를 불러온 main함수에 있는 char형 배열에 영향을 끼치고(값을 저장시키고), 값을 불러올 수 있습니다.
이번에는 오른쪽에 있는 코드를 왼쪽의 strcpy 함수의 내용으로 적어주면 됩니다. 배열을 두 개 가져와서 첫번째 배열에 있는 내용을 두번째 배열에 넣어주는 코드입니다. 포인터를 썼기 때문에 main함수에서 선언된 배열변수의 값을 변경시킬 수 있습니다.
이중포인터, 더블포인터는 자료형에 *연산자를 두 깨 쓰고 변수명을 써서 선언합니다. 그리고 더블포인터에는 포인터변수의 메모리 주소값을 저장합니다. 작성하자면
int a = 10;
int* ptr = &a;
int** ptr2 = &ptr;
이 됩니다.
이중포인터는 **연산자를 사용해서 a의 값을 불러올 수 있고, 이것은 이중포인터 변수에 저장된 포인터변수의 메모리 주소에 저장된 메모리 주소에 들어있는 값을 불러온다는 뜻입니다. 다시 말하자면 ptr2에 저장된 ptr의 주소에 저장된 a의 주소에 저장된 값 10을 불러온다는 것입니다. 복잡하지만 간단한 개념입니다.
이러한 이중 포인터는, 단일 포인터의 주소를 가지고 이것저것 변형시키고, 만지고 싶을 때 사용합니다.
[참고]
수업 중에 포인터를 사용하면 long long 형 변수를 그대로 가져오는 게 아니라 주소를 가져오기 때문에 8byte(16-8)를 절약할 수 있기에 효율적이다. 라고 하셨습니다. 제가 거기에 *연산자를 많이 사용하면 cpu가 많이 돌고, 시간이 걸리는데, 그러면 포인터를 사용해서 메모리를 아끼고 처리 시간이 오래 걸리는 게 효율적이냐, 포인터를 사용하지 않아서 메모리를 늘리고 처리 시간을 줄이는 게 효율적이냐 물어봤지요.
제가 조사하고 계산한 결과, 결론적으로 프로그램 크기와 원하는(포인터를 사용한 함수의 계획한) 처리 속도에 따라 다르겠지만, 평균적으로(1MB프로그램에 처리속도 1밀리초) *연산자 하나를 사용할 때 2byte의 메모리를 절약하는 것이 효율적이었습니다. 그러니까 *연산자 2개를 사용할거면 4byte의 메모리를 절약해야 효율적이란 것이죠. *연산자 2개 써놓고 2byte 절약하면 비효율적이란 말입니다. 굉장히 흥미롭지요?
'Nefus' 카테고리의 다른 글
NEFUS_Arduino_20230531 (0) | 2023.05.31 |
---|---|
NEFUS_Arduino_20230517 (0) | 2023.05.17 |
NEFUS_Arduino_20230515 (0) | 2023.05.14 |
NEFUS_Arduino_20230508 (0) | 2023.05.08 |
NEFUS_프로젝트 (1) | 2023.04.20 |