포인터(Pointer)
다른 변수, 혹은 그 변수의 메모리 공간주소(시작 주소값)를 가리키는 변수
포인터 선언
포인터를 선언하는 방식은 2가지가 있으며, 아래와 같다. 둘 중 무엇을 쓰더라도 같은 동작을 한다.
- (포인터에 주소값이 저장되는 데이터의 Type) *(포인터 변수 명); # asterisk(*)가 변수 명 앞
- (포인터에 주소값이 저장되는 데이터의 Type)* (포인터 변수 명); # asterisk(*)가 타입 뒤
- 포인터에 타입이 있는 이유는 컴퓨터에 메모리 공간을 얼마나 할당해야 할지 알려주기 위함이다.
int *ptr;
int* ptr;
& 연산자
- 데이터의 주소값을 알고 싶을 때 사용하는 연산자
- '&변수명' 의 형태로 사용된다.
#include <iostream>
int main() {
int a = 7;
int* p1 = &a;
int *p2 = &a;
std::cout << &a << std::endl;
std::cout << p1 << std::endl;
std::cout << p2 << std::endl;
return 0;
}
0093FCA0
0093FCA0
0093FCA0
> 모두 동일한 주소값을 가리키고 있음을 알 수 있다.
* 연산자
- 포인터가 보관하고 있는 주소값에 대응하는 데이터를 가져올 때 사용하는 연산자이다.
- 'int *p = &a;'로 포인터를 선언하면, *p는 변수 a 자체를 의미하게 된다.
#include <iostream>
int main() {
int a = 7;
int* p1 = &a;
int *p2 = &a;
std::cout << "a의 값: " << a << std::endl;
std::cout << "p1이 가리키는 데이터: " << *p1 << std::endl;
std::cout << "p2이 가리키는 데이터: " << *p2 << std::endl;
return 0;
}
a의 값: 7
p1이 가리키는 데이터: 7
p2이 가리키는 데이터: 7
포인터를 사용하는 이유
포인터를 사용하는 이유는 다양하다.
- 메모리 주소를 직접 참조하므로 다양한 자료형 변수에 대한 접근 및 조작이 용이하다.
- Call by Reference 방식을 이용할 수 있다. ▶ 전역변수의 사용을 억제
- 배열, 구조체 등 복잡하고 다양한 자료구조의 데이터에 쉽게 접근하고 조작할 수 있다.
- 메모리의 동적 할당(힙 영역)을 가능하게 한다.
- 이외에도 포인터를 사용하는 이유는 다양하다.
포인터 값 변경
- 포인터는 변수이기 때문에 값을 변경할 수 있다.
포인터에 저장된 주소값 바꾸기
#include <iostream>
int main() {
int a = 7, b = 10;
int* ptr;
ptr = &a;
std::cout << "ptr이 가리키는 데이터: " << *ptr << std::endl;
ptr = &b;
std::cout << "ptr이 가리키는 데이터: " << *ptr << std::endl;
return 0;
}
ptr이 가리키는 데이터: 7
ptr이 가리키는 데이터: 10
포인터가 가리키는 데이터의 값 바꾸기
- *p는 가리키는 변수 그 자체이므로, 포인터를 이용하여 가리키는 변수의 값을 변경할 수 있다.
#include <iostream>
int main() {
int a = 7;
int *ptr = &a;
std::cout << "a의 값(변경 전): " << a << std::endl;
*ptr = 20;
std::cout << "a의 값(변경 후): " << a << std::endl;
return 0;
}
a의 값(변경 전): 7
a의 값(변경 후): 20
이중 포인터
- 포인터는 포인터를 가리킬 수 있다.
- asterisk(*)를 2개 연속하여 사용하여 선언한다. EX) int **p;
- 'int **ptr2 = &ptr1;' 으로 포인터 변수 ptr2를 초기화하면, ptr2는 포인터 변수 ptr1의 주소값을 저장한다.
- asterisk(*)를 2개 연속하여 사용하면 원본 데이터의 값을 출력할 수 있다.
이중 포인터 선언
#include <iostream>
int main() {
int a = 7;
int *ptr = &a; // a의 주소를 담는 ptr 선언
int** pptr = &ptr; // ptr의 주소를 담는 pptr 선언
std::cout << "a의 값: " << a << std::endl;
std::cout << "ptr이 가리키는 값: " << *ptr << std::endl;
std::cout << "pptr이 가리키는 값: " << **pptr << std::endl;
std::cout << std::endl;
std::cout << "a의 주소값: " << &a << std::endl;
std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;
std::cout << "*pptr이 가리키는 값: " << *pptr << std::endl;
std::cout << "포인터 변수 ptr의 주소값: " << &ptr << std::endl;
std::cout << "pptr이 담고있는 주소값: " << pptr << std::endl;
std::cout << "포인터 변수 pptr의 주소값: " << &pptr << std::endl;
return 0;
}
a의 값: 7
ptr이 가리키는 값: 7
pptr이 가리키는 값: 7
a의 주소값: 0076FAE8
ptr이 담고있는 주소값: 0076FAE8
*pptr이 가리키는 값: 0076FAE8
포인터 변수 ptr의 주소값: 0076FADC
pptr이 담고있는 주소값: 0076FADC
포인터 변수 pptr의 주소값: 0076FAD0
- &a와 ptr, *pptr은 모두 같은 주소값을 가리킨다.
- pptr은 포인터변수 ptr이 저장된 메모리 주소값을 담고 있다.
- **pptr은 변수 a 그 자체를 나타낸다.
이중 포인터를 사용하는 이유
- 값을 전부 넘기는 것보다는 값을 가리키는 주소값을 넘기는 것이 리소스와 처리시간 측면에서 효율적이다.
- 특히나 영상 등의 2차원 배열은 데이터 크기가 매우 크므로, 값을 직접 넘기는 경우(Call by Value) 처리시간이 길어지고, 리소스 낭비가 심해진다.
- 따라서 이러한 경우에는 이중 포인터를 이용하여 주소를 넘기는(Call by Reference) 방식으로 처리하는 것이 좋다.
상수 포인터
- 포인터도 변수이기 때문에 const 키워드를 이용하여 상수 처리할 수 있다.
const datatype*
- const datatype*는 포인터 변수가 가리키는 변수의 값을 포인터 변수로는 변경할 수 없도록 한다.
- 포인터 변수는 저장된 메모리 주소를 다른 변수의 메모리 주소로 옮길 수는 있어도, 포인터 변수를 이용하여 저장된 메모리 주소에 해당하는 변수의 값은 변경할 수 없다.
예제 1) ptr로 가리키는 변수 값 변경하기 (오류 발생)
#include <iostream>
int main() {
int a = 7;
const int *ptr = &a;
// ptr이 가리키는 값 변경하기
*ptr = 3;
return 0;
}
- 포인터 변수 ptr은 변수 a를 가리키며, const 키워드가 붙어있기 때문에 ptr가 가리키는 변수 a의 값은 변경할 수 없다.
예제 2) ptr이 가리키는 변수를 직접 변경하기 (오류 미발생)
#include <iostream>
int main() {
int a = 7;
const int* ptr = &a;
std::cout << "a의 값(변경 전): " << *ptr << std::endl;
std::cout << "ptr 주소값(변경 후): " << ptr << std::endl;
// a의 값 변경하기
a = 10;
std::cout << "a의 값(변경 후): " << *ptr << std::endl;
std::cout << "ptr 주소값(변경 후): " << ptr << std::endl;
return 0;
}
a의 값(변경 전): 7
ptr 주소값(변경 후): 00D3FD6C
a의 값(변경 후): 10
ptr 주소값(변경 후): 00D3FD6C
- 변수 a 자체는 const가 아니기 때문에 변경하더라도 오류가 발생하지 않는다. (변수 a의 값을 변경하더라도 변수 a가 저장된 메모리 주소는 바뀌지 않는다.)
- 변수 a를 가리키는 포인터 ptr을 통해서 a의 값을 변경하는 것은 불가능하지만, 변수 a 자체를 직접적으로 변경하는 것은 가능하다.
예제 3) ptr의 값(담고 있는 주소값) 변경하기 (오류 미발생)
#include <iostream>
int main() {
int a = 7;
int b = 10;
const int* ptr = &a;
std::cout << "ptr에 담긴 값(변경 전): " << *ptr << std::endl;
std::cout << "ptr 주소값(변경 전): " << ptr << std::endl;
// ptr가 담고 있는 주소값 변경하기
ptr = &b;
std::cout << "ptr에 담긴 값(변경 후): " << *ptr << std::endl;
std::cout << "ptr 주소값(변경 후): " << ptr << std::endl;
return 0;
}
ptr에 담긴 값(변경 전): 7
ptr 주소값(변경 전): 010FFBE8
ptr에 담긴 값(변경 후): 10
ptr 주소값(변경 후): 010FFBDC
- 변경이 불가능한 것은 ptr이 가리키는 변수의 주소값이다.
- 따라서 ptr이 가리키는 변수 자체를 변경하는 것은 오류가 발생하지 않는다. (변수들의 주소는 그대로 유지)
datatype* const
- datatype* const는 포인터 변수의 값을 변경할 수 없게 한다.
- 즉, 한번 포인터 변수의 값을 초기화하면, 다른 변수를 가리키도록 변경할 수 없다.
예제 1) ptr의 값(담고 있는 주소값) 변경하기 > 다른 변수 가리키기 (오류 발생)
#include <iostream>
int main() {
int a = 7, b = 10;
int* const ptr = &a;
// ptr가 담고 있는 주소값 변경하기(다른 변수 가리키기)
ptr = &b;
return 0;
}
- 변수 a와 변수 b의 데이터는 서로 다른 주소값에 매칭되어 있다.
- ptr이 담고 있는 주소값은 변경할 수 없기 때문에 변수 b의 주소값으로 변경할 시 오류가 발생한다.
예제 2) ptr이 가리키는 변수를 직접 변경하기 (오류 미발생)
#include <iostream>
int main() {
int a = 7;
int* const ptr = &a;
std::cout << "a의 값(변경 전): " << *ptr << std::endl;
std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;
// a의 값 변경하기
a = 10;
std::cout << "a의 값(변경 후): " << *ptr << std::endl;
std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;
return 0;
}
a의 값(변경 전): 7
ptr이 담고있는 주소값: 00CFFBAC
a의 값(변경 후): 10
ptr이 담고있는 주소값: 00CFFBAC
- ptr의 값(가리키는 변수의 주소값) 자체가 변하지 않기 때문에 오류가 발생하지 않는다.
예제 3) ptr로 가리키는 변수 값 변경하기 (오류 미발생)
#include <iostream>
int main() {
int a = 7;
int* const ptr = &a;
std::cout << "a의 값(변경 전): " << a << std::endl;
std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;
// ptr이 가리키는 값 변경하기
*ptr = 10;
std::cout << "a의 값(변경 후): " << a << std::endl;
std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;
return 0;
}
a의 값(변경 전): 7
ptr이 담고있는 주소값: 0135FE38
a의 값(변경 후): 10
ptr이 담고있는 주소값: 0135FE38
- 포인터 변수 ptr로 변수 a의 값을 변경하더라도 주소값 자체가 바뀌는 것이 아니므로 오류가 발생하지 않는다.
포인터 연산
- 포인터는 변수이기 때문에 덧셈과 뺄셈 연산이 가능하다.
포인터 덧셈
예제 1) 포인터에 상수 더하기 & 증감연산자 사용 (오류 미발생)
#include <iostream>
int main() {
int a;
int* ptr = &a;
std::cout << "ptr의 값: " << ptr << std::endl;
std::cout << "(ptr + 1)의 값: " << ptr + 1 << std::endl;
ptr++;
std::cout << "ptr의 값(증감 후): " << ptr << std::endl;
return 0;
}
ptr의 값: 00EFFD94
(ptr + 1)의 값: 00EFFD98
ptr의 값(증감 후): 00EFFD98
- 덧셈이 가능하므로, 당연하게도 증감연산자 사용이 가능하다.
- ptr이 4byte 메모리 크기를 할당하는 int를 가리키는 포인터 변수이므로, 해당 메모리 크기만큼 주소가 증가한다.
예제 2) 포인터 + 포인터 (오류 발생)
#include <iostream>
int main() {
int a, b;
int* ptr1 = &a;
int* ptr2 = &b;
int* ptr = ptr1 + ptr2;
return 0;
}
- 포인터끼리의 덧셈은 허용하지 않는다.
- 포인터끼리 더하는 경우, 의미없는 프로그램의 지점을 가리키기 때문에 할 이유가 없다.
- 반면, 포인터끼리 빼는 경우는 허용한다. 포인터끼리 뺄셈을 진행함으로써, 포인터 사이에 데이터가 몇 개가 있는지 확인할 수 있다. 이처럼 의미가 있는 연산이기 때문에 허용된다.
포인터 뺄셈
예제 1) 포인터에 상수 빼기 & 증감연산자 사용 (오류 미발생)
#include <iostream>
int main() {
int a;
int* ptr = &a;
std::cout << "ptr의 값: " << ptr << std::endl;
std::cout << "(ptr - 1)의 값: " << ptr - 1 << std::endl;
ptr--;
std::cout << "ptr의 값(증감 후): " << ptr << std::endl;
return 0;
}
ptr의 값: 0053FD44
(ptr - 1)의 값: 0053FD40
ptr의 값(증감 후): 0053FD40
- 뺄셈 역시 가능하며, 증감연산자 사용도 가능하다.
- 포인터 변수가 가리키는 데이터형의 크기만큼 주소값이 변경된다.
예제 2) 포인터 - 포인터 (오류 미발생)
#include <iostream>
int main() {
int* ptr1 = (int*)1000;
int* ptr2 = (int*)1012;
int count;
count = ptr2 - ptr1;
std::cout << "ptr2와 ptr1 사이에 존재하는 데이터의 개수: " << count << std::endl;
return 0;
}
ptr2와 ptr1 사이에 존재하는 데이터의 개수: 3
- 포인터끼리의 덧셈과는 달리, 포인터끼리의 뺄셈은 가능하다.
- 포인터끼리의 뺄셈 연산 자체는 데이터의 개수를 세는 기능을 수행하므로 따로 sizeof(int)로 나누는 과정을 진행하지 않아도 된다.
- ptr2 - ptr1 = (1012 - 1000) / sizeof(int)
포인터 대입
- 포인터끼리의 대입이 가능하다.
#include <iostream>
int main() {
int a;
int* ptr1 = &a;
*ptr1 = 10;
int* ptr2;
ptr2 = ptr1;
std::cout << "ptr1이 가리키는 값: " << *ptr1 << std::endl;
std::cout << "ptr2이 가리키는 값: " << *ptr2 << std::endl;
return 0;
}
ptr1이 가리키는 값: 10
ptr2이 가리키는 값: 10