주소값의 이해
포인터를 배우기 앞서 포인터를 이해하기 위해서는 주소 값이 뭔지 알아야 한다.
변수 등에서 어떤 데이터를 저장한다고 선언을 하면 메모리에서 저장할 공간을 할당받는다.
그럼 이 데이터를 다른 곳에서 사용하려고 호출하기 위해서는 이 데이터가 메모리의 어느 부분에 저장되어 있는지를 알아야 한다.
이때, 데이터가 저장된 메모리의 시작 주소를 주소 값이라고 한다.
예를 들어 int형 변수를 하나 선언했다면, 메모리 어딘가에 4bytes 크기로 공간이 할당될 것이다.
(0x01, 0x02, 0x03, 0x04) 이렇게 할당 받았다면, 이 공간의 시작 주소인 0x01이 해당 변수를 가리키는 주소 값이 된다.
--------------------------------------------------------------------------------------------------------------------------------
1. 포인터란?
C++에서 포인터(pointer)란 메모리의 주소값을 저장하는 변수이며, 포인터 변수라고도 한다.
char형 변수가 문자를 저장하고, int형 변수가 정수를 저장하는것 처럼 포인터는 주소값을 저장하는 데 사용된다.
포인터 변수의 메모리 크기는 사용자의 컴퓨터에 따라 다르다.
32비트 시스템에서는 32비트(4byte) / 64비트 시스템에서는 64비트(8byte)를 차지한다.
int a = 100; // 정수형 변수의 선언
int* ptr = &a;; // 포인터 변수의 선언
타입* 포인터이름
cout << a << endl;
cout << ptr << endl;
/*실행 결과
100
00000047154FFCD4
*/
타입이란 포인터가 가리키고자 하는 변수의 타입을 명시한다. (이 포인터를 타고 가면 해당 타입의 변수가 있다)
int형 변수 a에 100이라는 값이 저장되어 있고 a의 메모리 시작 주소가 0x0100이라고 하자.
그러면 포인터 변수 ptr은 100이라는 값이 아닌, 0x0100이라는 시작 주소를 저장하는 것이다.
실제 메모리의 위치는 아니지만, int변수의 시작 주소를 저장해 저 변수를 가리키고 있다고 생각하면 된다.
또한, 이 포인터도 변수이기 때문에 메모리 어딘가에 저장되고 포인터 변수도 주소 값을 가지고 있다.
포인터를 선언하지 않고 사용하면 어딘가에 있는 메모리에 값을 저장하는 것이므로, 포인터는 선언과 동시에 초기화를 함께 하는 것이 좋다.
--------------------------------------------------------------------------------------------------------------------------------
2. 포인터 연산자
포인터 변수를 사용하면서 2가지의 연산자가 사용되었다.
& 과 *이다. 여기 &은 비트 AND연산자가 아닌 주소 연산자이고, *은 곱하기가 아닌 참조 연산자이다.
- 주소 연산자(&)
주소 연산자는 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환한다.
앰퍼샌드(ampersand)라고 읽음.
- 참조 연산자(*)
참조 연산자는 포인터의 이름이나 주소 앞에 사용하여, 포인터에 저장되어 있는 값을 반환한다.
에스크리터(asterisk)라고 읽음.
int a = 100;
int* ptr = &a;;
cout << &a << endl;
cout << *ptr << endl;
/*실행 결과
00000074F20FF634
100
*/
&은 a변수가 할당된 메모리의 시작 주소를 반환하는 모습이다.
포인터 ptr에 a의 주소를 저장하였으므로, *을 이용해 출력하게 되면 a변수의 값이 반환된다.
--------------------------------------------------------------------------------------------------------------------------------
3. 이중 포인터
이중 포인터도 존재한다. 이는 포인터를 가리키는 포인터 라는 뜻이다.
int main()
{
int a = 100;
int* ptr = &a;
int** pptr = &ptr;
cout << a << endl;
cout << *ptr << endl;
cout << **pptr << endl;
return 0;
}
/*실행 결과
10
10
10
*/
**pptr -> *(*ptr) -> *(ptr의 주소) -> a의 값으로 이해하면 된다.
--------------------------------------------------------------------------------------------------------------------------------
4. 포인터 연산
1. 포인터는 주소 값을 저장하는 변수이기 때문에 포인터끼리의 사칙 연산중 뺄셈을 제외한 나머지는 의미가 없다.
2. 뺄셈의 경우 두 포인터 사이의 거리를 나타낸다.
3. 포인터에 정수 값을 더하거나 빼는 것은 가능하지만 실수 값은 안된다.
int main()
{
int a = 1;
int* ptr1 = &a;
double b = 0.1;
double* ptr2 = &b;
cout << ptr1 << endl;
cout << ptr1 + 1 << endl;
cout << ptr2 << endl;
cout << ptr2 + 1 << endl;
return 0;
}
/*실행 결과
0000007D980FFA84
0000007D980FFA88
0000007D980FFAC8
0000007D980FFAD0
*/
포인터의 +1은 단순히 1이 증가하는것이 아니라, 가리키는 주소가 달라지는 것이다.
데이터 타입별로 연산 후 가리키는 주소의 증가량이 다르다.
이는 각 데이터가 저장되는 크기가 다르기 때문에 할당되는 메모리의 크기의 차이 때문에 그렇다.
int의 경우를 보면 +1을 했으니 int형의 크기인 4만큼 증가했고, double의 경우 8만큼 증가하는 것을 확인할 수 있다.
--------------------------------------------------------------------------------------------------------------------------------
5. 포인터와 배열
포인터와 배열은 매우 긴밀한 관계를 맺고 있으며, 어떤 부분에서는 서로를 대체할 수 있다.
배열의 이름은 그 값을 변경할 수 없다는 상수라는 점을 제외하곤 포인터와 같다.
C++에서는 배열의 이름이 주소로 해석되며, 해당 배열의 첫 번째 요소의 주소와 같다.
int main()
{
int arr[] = { 10, 20, 30 };
int* str = arr;
cout << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;
cout << str[0] << ", " << str[1] << ", " << str[2] << endl;
cout << "배열의 크기 계산" << endl;
cout << sizeof(arr) << endl;
cout << sizeof(str) << endl;
return 0;
}
/*실행 결과
10, 20, 30
10, 20, 30
배열의 크기 계산
12
8
*/
위의 예제는 포인터에 배열의 이름을 대입 후, 해당 포인터를 배열의 이름처럼 사용했다.
배열의 크기를 계산할 때 큰 차이가 발생하는데 배열의 이름을 크기 계산 한 경우 12바이트 제대로 출력되지만,
포인터를 크기 계산 한 경우 배열의 크기가 아닌, 포인터 변수의 크기가 출력되는 차이가 발생한다.
--------------------------------------------------------------------------------------------------------------------------------
6. 포인터 / 참조
주의해야 할 점은 '선언'에서의 *와 &는 '표현'에서의 *와 &라는 아무 상관이 없다.
연산자들의 모습만 똑같이 생겼지, 상황에 따라 전혀 다르게 동작하기 때문에 많이 헷갈릴 수 있다.
- 참조 & (reference)
int a = 100;
int& b = a; // b는 a를 표현하는 또 다른 이름이다. b를 수정하면 a도 수정된다.
b = 5; // a도 5가 된다. 실체는 하나만 존재하고 a와 b는 실체를 부르는 서로 다른 이름
= 연산자 왼쪽에 있는 &, 즉 변수 타입을 선언하는 데 사용된 &은 왼쪽 변수가 오른쪽 변수의 값을 부르는 또 다른 이름이라는 뜻이 된다.
- 주소 &
int a = 10;
cout << &a << endl; // a의 메모리 주소값이 나옴 (000000D6F2D4F654)
int* ptr = &a; // 주소에가서, 그 주소를 저장
= 연산자 오른쪽에 있는 &, 또는 이미 선언된 변수나 값에 대한 연산자로 사용하는 &는 값이 위치한 주소를 가리킨다.
- 함수 입력에 선언된 참조 & (reference)
Change(int& a)
{
a = 10; // 함수를 호출한 측에서도 값이 10으로 변경됨.
}
a = 5;
Change(a); // a는 10이 됨.
함수 입력에 있는 &는 입력값의 복제본이 아니라 원본값을 받겠다는 뜻이다.
즉, 함수 내부에서 수정한 값이 함수 외부에서도 그대로 적용이 된다.
이 동작은 '- 참조 &'의 연장선이다. 함수에 입력된 값의 복제본이 아닌, 입력한 값을 가리키는 또 다른 별명을 만들어 함수 내부에서 사용한다는 뜻이다.
- 포인터 *
int a = 0;
int* str = &a; // a의 주소에 가서 str에 a의 주소값을 저장함.
str = 10; // a의 값은 변함이 없음.
int* str = &a; // a의 주소에 가서 str에 a의 주소값을 저장함.
*str = 10; // a의 값이 10이 됨.
= 연산자 왼쪽에 있는 *은, 즉 변수 타입을 선언하는 데 사용된 *은 그냥 주소값을 저장하는 변수이다.
포인터는 친구의 집 주소이고, 참조는 친구의 또 다른 이름이다.
참조값을 수정하면 원본 그 자체를 수정하는 것이고, 포인터 타입 변수는 그 값을 수정한다고 원본이 수정되지 않는다.
포인터 타입의 변수가 가리키는 곳에 가서 직접 값을 수정해야 원본이 변경되는 것이다.
- 역참조 *
int a = 10;
int* str = &a; // a의 주소에 가서 str에 a의 주소값을 저장함.
cout << str << endl; // a의 주소값이 나옴
cout << *str << endl; // a의 주소로 이동하여 찾은 값 10이 나옴
선언용 *가 주소를 받는 변수 타입(포인터 타입)을 선언하는데 사용된다.
이미 선언된 변수에 사용되는 *연산자는, 해당 주소로 이동하는 연산자다.
'📕Programming > 📝C/C++' 카테고리의 다른 글
[C / C++] 객체 지향 - 상속성, 은닉성, 다형성 (0) | 2023.09.27 |
---|---|
[C / C++] 문자열 (0) | 2023.09.14 |
[C / C++] 배열 (array) (0) | 2023.09.11 |
[C / C++] 반복문 (0) | 2023.09.10 |
[C / C++] 조건문 (0) | 2023.09.10 |