읽는데 약 4분
포인터
포인터
포인터는 변수가 저장될 주소 값을 저장합니다. 선언할 때 에스터리스크(*) 을 사용하여 선언합니다. 변수가 저장될 주소 값만 저장하므로 어떤 데이터 타입을 가리키는지 또한 나타내야 합니다.
#include <iostream>
using namespace std;
int main() {
int a = 10;
double b = 10.0;
int* ptr = &a; //주소값을 알려주는 &연산자
double* ptr2 = &b;
cout << ptr << ' ' << ptr2; //주소값에 해당하는 00FBF9A4, 00FBF994 을 출력한다
return 0;
}
포인터의 특징
1. 포인터의 크기는 모두 동일하다
각 데이터형은 저장하는 값의 범위가 다르므로 할당되는 바이트가 다릅니다. 하지만 큰 데이터를 가리키는 주소든 작은 데이터를 가르키는 주소든 포인터의 크기는 가리키는 데이터에 따라 달라질 이유가 없습니다. “어디"에 위치한지만 저장하기 때문입니다. 즉, 주소 값을 나타내는 포인터는 모두 동일한 바이트를 사용합니다.
#include <iostream>
using namespace std;
int main() {
int a = 10;
double b = 10.0;
int* ptr = &a;
double* ptr2 = &b;
cout << sizeof(a) << ' ' << sizeof(b) << '\n'; //4 8
cout << sizeof(ptr) << ' ' << sizeof(ptr2);// 4 4, 포인터의 크기는 동일하다.
return 0;
}
2. 포인터를 선언하고 참조 연산자를 사용하거나 포인터를 사용하기 전, 반드시 포인터의 주소를 초기화해야 한다.
포인터를 생성하면 컴퓨터는 해당 주소를 저장하기 위한 메모리를 생성합니다. 하지만 이는 포인터가 가리키는 값을 저장하는 메모리를 만드는 것이 아니므로, 포인터를 초기화하지 않고 접근하게 되면 컴파일 에러가 발생합니다.
#include <iostream>
using namespace std;
int main() {
int a = 10;
int* ptr;
//ptr = &a; 이 줄을 제외하면
cout << *ptr; // 에러가 발생 error C4700: 초기화되지 않은 'ptr' 지역 변수를 사용했습니다.
return 0;
}
3. 직접 주소 값을 넣어줄 땐, 형 변환을 한 후 대입해야 한다.
일반적으로 주소는 정수로 표현되지만 포인터는 정수가 아닙니다. 16진수를 넣는다고 모두 주소가 되는 것이 아니므로 직접 대입을 하려면 (int*)와 같이 형 변환을 해야 합니다.
#include <iostream>
using namespace std;
int main() {
int* ptr;
ptr = (int*)0x00A0FD7C;
cout << ptr; //주소값 0x00A0FD7C 이 제대로 출력된다.
return 0;
}
4. new를 사용하여 메모리 대입이 가능하다.
new는 뒤에 오는 타입을 인식해 필요한 메모리 블록을 대입하여 주소를 리턴합니다. 이를 통해 해당 메모리 주소가 가리키는 데이터는 포인터를 제외하고는 접근이 불가능해집니다.
#include <iostream>
using namespace std;
int main() {
// *ap는 a를 통해서도 접근이 가능하지만
// bp가 가르키는 int는 오직 bp를 통해서만 가능하다.
int a = 10;
int* ap = &a;
int* bp = new int;
return 0;
}
4-1. delete는 new를 사용한 곳에만 사용해야만 합니다.
new를 사용한 곳에만 delete를 사용해야 하므로 변수를 선언하고 변수의 주소를 delete 하는 행위를 허용하지 않습니다. 또한 널 포인터를 제외하고 이미 delete를 통해 메모리를 해제한 곳에 다시 delete를 사용해선 안됩니다.
#include <iostream>
using namespace std;
int main() {
int* pa = new int;
delete pa;
// delete pa; 이미 해제한 메모리를 또다시 delete 해선 안된다.
int b = 10;
int* pb = &b;
// delete pb; new를 사용하여 만든 메모리 블록이 아니므로 delete 해선 안된다.
return 0;
}
4-2. 배열을 해제할 땐 delete [] 을 사용합니다.
동적 배열을 선언할땐 []을 사용하고 해제할때 또한 []을 사용합니다. 선언과 해제가 짝이 맞지 않으면 컴파일러가 오류를 일으키거나 경고를 줍니다. 마찬가지로 new를 통해 하나의 값을 가르키는 메모리 블록을 만들었다면 delete 만 사용하여 짝을 맞춥니다.
#include <iostream>
using namespace std;
int main() {
int* arr = new int[10];
int* pv = new int;
// something
// ..
//delete[] pv; new int와 [] 는 짝이 맞지 않으므로 틀림
delete pv;
//delete arr; new int[]로 선언했으므로 delete []를 사용해야함.
delete[] arr;
return 0;
}
5. 포인터에 +1을 하면 포인터가 지시하는 데이터형의 바이트만큼 증가하게 된다.
이는 c,c++이 내부적으로 배열을 처리할 때, 포인터를 이용하기 때문에 가능합니다. 컴파일러가 arr[1]을 처리할 때 *(arr+1)와 완전히 동등한것으로 취급합니다. 이 때문에 배열의 이름을 포인터처럼 사용하기도 합니다.
하지만 둘의 차이점은 배열이름에 해당하는 arr는 값을 변경할 수 없는 상수인 반면, 포인터는 변수라는 점입니다. 이 외에도 sizeof arr를 하면 전체 배열의 크기가 얻어지지만 포인터는 포인터의 크기가 얻어집니다.
C, C++은 배열범위 밖 에러에 대해 체크를 하지 않습니다. 이는 위에서 설명했듯이, 배열에 접근할 때 내부적으로는 포인터를 사용하기 때문입니다. 만약 범위밖에 대해 체크를 하고 싶다면 vector나 array클래스의 at 멤버함수를 사용해야합니다.
배열 포인터에 +1 을 하게 되면 배열의 전체 크기에 해당하는 바이트만큼 증가하게 됩니다.
#include <iostream>
using namespace std;
int main() {
short tell[10] = { 1,2,3,4,5,6,7,8,9,10 };
cout << tell << '\n'; //배열의 이름
cout << &tell[0] << '\n';// 배열의 첫번째 원소의 주소, tell와 완벽하게 동일
cout << &tell << '\n'; //전체 배열의 주소, 배열 포인터
//short (*)[10] 형이라고 할 수 있다.
cout << *&tell << '\n'; // *을 통해 배열에 접근
cout << *(tell + 1) << '\n';
cout << *(&tell[0] + 1) << '\n';
cout << **(&tell + 1) << '\n'; // 배열 포인터에 +1 했으므로
// 2(short)*10(원소갯수) 바이트 만큼 이동하게 된다.
// 0116FB30
// 0116FB30
// 0116FB30
// 0116FB30
// 2
// 2
// - 13108, 예상치 못한 값이 나온다.
return 0;
}