포인터(Pointer)에 대하여

2021. 5. 12. 01:10Layer7/C

728x90

- 포인터의 정의 

 

포인터란 주소 값을 저장하는 변수를 뜻합니다.

 

 

- 주소 값이란?

 

그럼 주소 값이란 무엇일까요?

 

주소 값의 정의는 데이터가 저장된 메모리의 시작하는 주소를 의미합니다.

이렇게만 말하면 감이 잘 오지 않을 겁니다.

아래에 나와있는 그림이랑 같이 설명을 덧붙혀보겠습니다.

 

메모리 구조 그림

int형 변수 a를 선언하고, 여기에 11을 대입한다고 하면 비어있던 메모리의 구조는 그림과 같이 바뀔 것입니다.

 

그럼 여기서 주소 값은 무엇일까요?

바로 0x01입니다.

 

int형 자료는 메모리에서 4byte만큼의 공간을 차지합니다.

현재 a가 차지하고 있는 메모리는 0x01~0x04까지인데,

여기서 시작 주소는 가장 처음에 나오는 주소를 뜻하는 것이니 0x01이 되는 것입니다.

 

 

- 포인터에서 쓰이는 연산자

 

포인터에서 쓰이는 연산자는 두 가지가 있습니다.

 

  1. & (주소 연산자)
  2. * (참조/역참조 연산자)

우리가 알고 있는 &와 *은 C/C++에서 and와 곱셈으로 쓰일 것입니다.

하지만 포인터에서는 주소 연산자와 참조/역참조 연산자로 쓰입니다.

 

&를 이용해서 변수의 주소 값을 나타낼 때에는 &변수 명의 형태로 나타냅니다.

이는 변수가 저장된 메모리의 주소 값을 나타낸다는 뜻입니다.

 

*은 선언이나 참조를 할 때 쓰입니다.

선언을 할 때에는 자료형 *변수 명의 형태를 사용하고, 참조를 할 때는 *변수 명의 형태로 사용합니다.

 

아래 코드를 보며 추가 설명을 하겠습니다.

 

#include<cstdio>
#include<iostream>
using namespace std;

int main(){
	int num=10;
	int *p=&num;
	cout<<num<<endl<<p<<endl<<&num<<endl<<*p;
}

 

코드 실행 결과

int형 변수 num은 10의 값을 가지고 있고, 포인터 변수 p는 num의 주소 값을 가지고 있습니다.

 

num을 출력하면 당연히 그 안에 들어있는 값인 10이 나올 것입니다.

 

그리고 p도 선언을 할 때 num의 주소 값을 넣었으니 주소 값인 0x6ffe04가 나올 것입니다.

 

&num은 num의 주소 값이란 뜻일 테고, 이는 곧 p를 그냥 출력했을 때와 같습니다.

 

*p는 p안에 들어가 있는 주소 값의 위치에 찾아가서 그 위치에 있는 값을 출력하는 것입니다.

그렇기에 num의 주소 값을 따라가고, num의 값인 10이 출력되는 것입니다.

 

 

- 포인터 연산의 개념

 

포인터 연산은 간단하게 개념만 설명하고, 자세한 내용은 아래에서 다루겠습니다.

중요하게 생각해야 할 개념은 4가지가 있습니다.

 

  • 포인터 연산에서 포인터끼리의 덧셈, 포인터의 곱셈, 포인터의 나눗셈은 무의미합니다.
  • 포인터끼리의 뺄셈은 두 포인터의 거리를 의미합니다.
  • 포인터끼리 비교, 대입은 가능합니다.
  • 포인터와 수의 연산에서 수의 범위는 정수로 한정됩니다.

 

- 포인터 연산(덧셈)

 

위에서 말했던 것처럼 포인터 연산에서 포인터끼리의 덧셈은 무의미합니다.

 

무의미하다는 건 무엇이냐?

말 그대롭니다.

애초에 C/C++에서는 포인터끼리의 연산이 문법적으로 허용이 되지 않습니다.

 

#include<cstdio>
#include<iostream>
using namespace std;

int main(){
	int num1=10,num2=20;
	int *p1=&num1,*p2=&num2;
	cout<<p1+p2;
}

Error

이렇게 포인터 변수끼리 덧셈 연산을 시도하려 하면 문법적으로 오류가 발생하게 됩니다.

 

그럼 포인터에서 덧셈을 어떻게 활용할 수 있을까요?

 

포인터에서의 덧셈은 "포인터끼리" 할 때 불가능한 것입니다.

포인터에 정수를 더하는 형태로는 충분히 활용이 가능합니다.

아래에서 예시 코드 보면서 설명하겠습니다.

 

#include<cstdio>

int main(){
	int num=0x10;
	int *pNum=&num;
	char ch='A';
	char *pCh=&ch;
	
	printf("pNum : %d\n", pNum);
	printf("pCh : %d\n\n", pCh);
	printf("== Operate +1 ==\n");
	printf("pNum : %d\n", pNum + 1);
	printf("pCh : %d\n", pCh + 1);
}

코드 실행 결과

이렇게 int, char형 포인터 변수인 pNum, pCh와 정수 1의 연산은 오류 없이 잘 되는 걸 볼 수 있습니다.

 

하지만 뭔가 좀 이상함을 느끼셨을 겁니다.

pNum의 값이 1을 더했으니 6553101이 되어야 할 텐데, 6553104로 결과값이 출력된 것을 볼 수 있습니다.

 

이는 여러가지 요인이 있습니다.

우선 이와 관련해서 몇 가지 개념들을 늘어놓아보겠습니다.

 

  • 포인터 변수와 정수의 연산에서 값의 증가폭은 포인터가 가리키는 변수의 자료형의 크기입니다.
  • 포인터 변수의 크기를 할당할 때 운영체제에 따라 메모리의 크기가 달라집니다.
  • 포인터 변수를 참조할 때 자료형의 크기만큼 참조합니다.

위 코드에서 pNum은 int형 변수였고, pCh는 char형 변수입니다.

int는 4byte 자료형이고, char는 1byte 자료형입니다.

 

또한, 저 코드를 돌린 운영체제는 window 64bit입니다.

(현재 제가 말하는 것들의 기준은 모두 64bit라는점 알아두셨으면 좋겠습니다.)

 

결국은 변수값현재 자료형의 byte값 * 증가값 만큼 더해졌다는걸 알 수 있습니다.

 

 

- 포인터 연산(뺄셈)

 

위에 나열해놓은 개념 2번째를 보면 "포인터의 뺄셈은 두 포인터간의 거리를 의미합니다."

 

이것 역시 코드와 함께 보겠습니다.

 

#include<cstdio>
#include<iostream>
using namespace std;

int main(){
	int num1=10,num2=20;
	int *p1=&num1,*p2=&num2;
	cout<<p1-p2;
}

코드 실행 결과

아까 위에서 썼던 덧셈 코드를 그대로 가져와 출력문에서 '+'를 '-'로 바꿨습니다.

 

이 코드의 출력 화면에 나온 1은 num1과 num2가 위치한 메모리 주소값의 거리를 의미합니다.

메모리 상에서 두 변수의 거리가 1만큼 떨어져 있다는 뜻입니다.

 

 

- 포인터 연산(곱셉/나눗셈)

 

#include<cstdio>
#include<iostream>
using namespace std;

int main(){
	int num1=10,num2=20;
	int *p1=&num1,*p2=&num2;
	cout<<p1*p2<<endl;
	cout<<p1/p2;
}

Error

곱셈과 나눗셈은 정말 무의미합니다.

덧셈보다 더 무의미하죠.

 

C/C++에서 아예 문법적으로 불가능하니 잘 기억해두고 사용하지 말도록 합시다.

 

 

- 인수 전달하기

 

이제 포인터를 가지고 함수에 활용 해보겠습니다.

포인터 변수를 함수에 인자로서 전달하여 사용하는 방식에는 두 가지가 있습니다.

 

  1. Call by value (값에 의한 호출)
  2. Call by reference(참조에 의한 호출)

Call by value는 함수에서 값을 복사하여 전달하는 방식입니다.

 

메인 함수에 num1, num2라는 두 int형 변수가 있다고 가정을 했을 때,

f라는 void형 함수에 인자로 보낸 num1과 num2가 있습니다.

이 때 함수 안에서 같은 이름, 같은 값(인자로 받은 값)의 num1, num2를 선언하고,

이의 값을 바꿔도 메인 함수에 있던 num1, num2는 값이 바뀌지 않습니다.

 

Call by reference는 함수에 값을 복사하여 전달하되,

주소에 접근 할 수 있도록 포인터 변수를 이용합니다.

 

아까와 같은 상황에서 함수 안에서 선언한 num1, num2의 값을 바꾸면

주소값에 영향이 미치는 것이니 메인 함수에 있던 num1, num2의 값이 바뀌게 됩니다.

 

말로 하면 잘 이해가 안될 수도 있으니 예시 코드와 함께 보도록 하겠습니다.

 

#include<cstdio>
#include<iostream>
using namespace std;

void f(int num1,int num2){
	int t;
	t=num1;
	num1=num2;
	num2=t;
	cout<<num1<<" "<<num2<<endl;
}

int main(){
	int num1=10,num2=20;
	f(num1,num2);
	cout<<num1<<" "<<num2;
}

함수 실행 결과

이 코드는 위에서 설명한 Call by value를 활용하여 num1과 num2의 값을 바꾸는 과정을 코드로 구현한 것입니다.

 

int형 변수 num1의 값은 10으로, num2의 값은 20으로 초기화를 했습니다.

그리고 void형 f에 num1, num2의 값을 인자로 보내고 함수 내부에서 두 변수의 값을 교환합니다.

 

그리고 출력을 해보면, num1=20, num2=10으로 교환이 된걸 볼 수 있습니다.

하지만, 함수를 종료하고 다시 main함수로 돌아왔을 때 num1, num2를 출력해보면

값이 바뀌지 않고 그대로인걸 확인 할 수 있습니다.

 

이처럼 Call by value 방식으로 값만을 인자로 함수에 보내 활용하게 되면 본래의 값은 바뀌지 않습니다.

 

#include<cstdio>
#include<iostream>
using namespace std;

void f(int *num1,int *num2){
	int t;
	t=*num1;
	*num1=*num2;
	*num2=t;
	cout<<*num1<<" "<<*num2<<endl;
}

int main(){
	int num1=10,num2=20;
	f(&num1,&num2);
	cout<<num1<<" "<<num2;
}

코드 실행 결과

이 코드는 Call by reference의 방식을 활용한 코드입니다.

 

int형 변수 num1, num2를 10, 20으로 정의하고 두 변수의 주소값을 f함수에 인자로 보냅니다.

그리고 변수 내에서는 인자로 들어온 값으로 num1, num2라는 포인터 변수를 선언합니다.

그리고 두 변수의 값을 교환하고 출력, 다시 main 함수로 돌아와서 또 다시 출력을 합니다.

 

여기서 아까와는 다른 차이점이 눈에 보이게 됩니다.

 

f함수에서 두 변수의 값을 바꾼 결과가 main함수에도 영향을 미칩니다.

이유는 아까 f함수에 보낸 인자가 단순 값이 아닌 주소값이였기 때문입니다.

주소값을 교환하면 아예 각 변수의 메모리 위치가 바뀌는 것이니 main함수에 있는 변수에도 영향을 미치게 됩니다.

 

Call by value와 Call by reference는 서로 반대라고 생각하면 좋습니다.

 

 

- 다중 포인터

 

다중 포인터는 말 그대로 포인터를 여러번 쓰는 것입니다.

 

포인터 변수가 가리키는 대상이 또 다른 포인터 변수인 경우에 그 변수는 포인터의 포인터가 되는 것입니다.

이게 두 번 반복되면 이중, 세 번 반복되면 삼중 . . . . . 이런 식으로 무수히 많은 중첩의 포인터를 만들 수 있습니다.

 

사실 4개 이상 겹쳐 쓰는 경우는 거의 없다고 보면 됩니다. 별로 안씁니다.

 

다중 포인터의 선언은 자료형 뒤에 필요한 중첩의 개수만큼 *을 붙히고, 그 뒤에 변수명을 적습니다.

ex) int형 이중 포인터 p1 선언 -> int **p1;

 

이 다중 포인터의 구조는 헷갈리기 쉬우니 코드와 함께 보도록 하겠습니다.

 

#include<cstdio>
#include<iostream>
using namespace std;

int main(){
	int num=10;
	int *p=&num;
	int **pp=&p;
	int ***ppp=&pp;
	cout<<p<<" "<<pp<<" "<<ppp<<endl;
	cout<<*p<<" "<<*pp<<" "<<*ppp<<endl;
	cout<<*p<<" "<<**pp<<" "<<**ppp<<endl;
	cout<<*p<<" "<<**pp<<" "<<***ppp<<endl;
}

코드 실행 결과

int형 삼중 포인터 ppp를 한 번 선언하고 실행해봤습니다.

 

우선 n중 포인터는 n-1중 포인터의 주소값을 나타낼 수 있다는 사실만 머릿속에 넣어두도록 합시다.

결과를 보고서 우선 알 수 있는 사실들부터 유추를 해보겠습니다.

 

맨 마지막 줄의 cout<<*p<<" "<<**pp<<" "<<***ppp<<endl; 는

모두 같은 값(=num의 값)이 나오는걸 보니

저렇게 구현을 했을 경우에 참조 연산자를 모두 붙히면

가장 처음에 집어넣은 값(=&num)이 나온다는걸 확인 할 수 있습니다.

 

사실 이게 머리로는 이해하기 어렵지 않지만 설명을 하려니 조금 뇌정지가 오네요.

 

정리를 하자면

 

  1. p=*pp=**ppp
  2. pp=*ppp
  3. *p=**pp=***ppp
  • n중 포인터에서 참조할 값의 중첩수가 n이라면 그 값은 가장 처음 넣었던 첫 값 (=num의 값)입니다.
  • n중 포인터에서 참조할 값의 중첩수가 n-1이라면 그 값은 n-1중 포인터에서 중첩수가 n-2인 포인터와 같습니다.
728x90

'Layer7 > C' 카테고리의 다른 글

Socket Programming  (0) 2021.06.14