Priv's Blog
포인터 기초 개념과 new 키워드 본문
1. 메모리 주소
포인터 변수는 일반적으로 데이터를 저장하는 것 대신, 메모리의 주소를 저장하는 변수라고 알려져 있습니다.
메모리 주소는 말 그대로 데이터가 메모리 상에서 어디에 저장되어 있는지에 대한 주소로, 데이터 그 자체가 아닌 데이터의 위치를 의미합니다.
C++에서 이 메모리 주소를 변수에 담기 위해서는 포인터 변수를 정의해 사용해야 하며, 포인터 변수는 메모리 주소만 저장할 수 있습니다.
2. & 연산자와 * 연산자
포인터 변수는 값이 아닌 값의 주소를 저장합니다.
포인터 변수를 선언하기 위해서는 * 연산자를, 포인터 변수에 저장할 데이터의 주소를 나타내기 위해서는 & 연산자가 사용됩니다.
& 연산자는 해당 값의 메모리 주소를 가져오는 역할을 합니다.
#include <iostream>
using namespace std;
int main() {
int donuts = 6;
cout << "Donuts value : " << donuts << endl;
cout << " Donuts Address : " << &donuts << endl;
}
위 코드에서 donuts는 정수형 변수로, 정수형 데이터 6을 저장합니다.
donuts의 값을 출력하면 6이 출력되는 것이 당연합니다.
&donuts의 의미는 조금 다릅니다.
& 연산자가 붙었기 때문에 이는 donuts 변수의 데이터인 6이 아닌, donuts 변수의 주소를 의미합니다.
이 때문에 결과는 아래처럼 출력됩니다.
Donuts Value : 6
Donuts Address : 0x0065fd40
Donuts Address의 값은 상황에 따라 달라질 수 있다는 점을 기억하셔야 합니다.
여기서 donuts는 int형 변수이며, 데이터 6을 저장하고 있어 4byte를 사용합니다.
만약 donuts이 double형 변수였다면, 8byte를 사용했겠죠.
이를 직접 확인해보고 싶다면 double 형 변수를 하나 더 선언하여 위와 동일하게 코드를 작성하고 출력해 보면 됩니다.
3. 런타임과 컴파일 타임
객체 지향 언어는 일반적인 절차 지향 언어와 달리 컴파일 시간보다 런타임 시간에 더 익숙합니다.
런타임은 말 그대로 프로그램이 실행되고 있는 시간을 말합니다.
컴파일 타임은 컴파일러가 코드를 컴파일하고 있을 때의 시간을 말하죠.
런타임에서 일어나는 결정은 사용자가 프로그램을 조작하고 있을 때 일어나는 변화라고 생각해도 됩니다.
오늘의 날씨나 기분에 따라서 여행지가 달라지는 우리의 변덕처럼 런타임에서 이루어지는 결정은 프로그램이 실행되기 전까지 컴퓨터 입장에서는 어떤 변덕이 일어날지 예상할 수 없죠.
컴파일 타임은 조금 다릅니다.
날씨가 비가 오든 눈이 오든, 기분이 어찌 되었든 간에 정해진 스케줄을 무조건 실행하는 것과 같습니다.
마치 출퇴근과 같습니다.
컴파일 타임은 코드가 실행되기 전에 이미 결정이 된 상태이기 때문에 컴퓨터 입장에서는 변덕을 예상하는 것 자체가 의미가 없습니다.
런타임에서 이루어지는 결정은 유연하게 다양한 상황에 맞춰 대응이 가능하다는 장점이 있습니다.
대표적인 예시를 Java 또는 C#으로 들어본다면, List와 Array의 차이입니다.
우리는 List를 선언할 때, List의 사이즈를 정해놓고 시작하는 경우는 잘 없습니다.
List<int> lst = new( );
lst.Add(1);
위와 같이 List 객체를 선언한 뒤에 값을 유동적으로 더하고 빼고 수정하죠.
하지만 Array는 절대 그럴 수 없습니다.
Array는 선언을 할 때 그 사이즈를 정해주어야 합니다.
이 사이즈는 컴파일 타임에 결정되고, 런타임에서 변동되지 않습니다.
만약 배열이 20개의 요소를 담아서 쓰고 싶다면, 아래와 같이 정의할 것입니다.
int[] arr = new int[20];
int 타입의 Array인 arr은 20개의 요소를 담을 수 있는 사이즈로 정의되었습니다.
이 사이즈는 변경이 불가능하기 때문에 21개의 요소를 담고 싶다면 새로 정의해야 합니다.
여기서 List 타입은 Array 타입과 달리 요소 그 자체를 저장하는 것이 아니라, 주소를 이용해 각 요소들을 연결하는, LinkedList 자료 구조를 사용하기 때문에 이러한 차이를 보입니다.
이것도 포인터의 개념과 동일합니다.
4. 포인터 변수 다루기
포인터 변수는 값이 아닌 메모리 주소를 저장한다는 특징이 있어 경우에 따라 매우 유용하게 다룰 수 있습니다.
int num = 10;
int *p = #
cout << p << endl; // 0x11dv...
cout << *p; << endl; // 10
*p = 5;
cout << num << endl; // 5
cout << *p << endl; // 5
위 코드는 '참조'의 개념을 포인터 변수를 사용해 최대한 간결하게 표현한 것입니다.
*p는 포인터 변수입니다.
또한 초기화를 할 때, num의 주소를 담았습니다.
즉, *p 포인터 변수를 num을 가리키고(num 변수의 메모리 상에서의 위치를 알고) 있습니다.
p를 출력한다는 것은 'p 변수에 담긴 값 그 자체'를 출력하라는 것입니다.
p 변수는 포인터 변수이므로, '담긴 값 그 자체'라면 당연히 'num 변수의 메모리 주소'가 될 것입니다.
그런데 *p에 5를 넣었습니다.
이는 *p가 가리키는 곳에 5를 대입하라는 것입니다.
즉, *p가 저장한 num 변수의 메모리 주소에 5를 대입하라는 것입니다.
이는 num 변수에 5를 대입하는 것과 다르지 않습니다.
이 때문에 마지막 출력문의 결과가 5로 동일하게 나오고 있습니다.
이 설명을 사진으로 표현한다면 아래와 같습니다.
(사진)
5. 포인터를 사용할 때 주의할 점
포인터 변수는 다른 변수와 다르게 메모리 주소를 담습니다.
이 특성 때문에 포인터를 사용할 때 주의해야 하는 사항들이 몇 가지 있습니다.
그중에서 가장 조심해야 하는 것이 포인터 변수를 초기화하지 않은 상태에서 호출해 사용하려는 실수입니다.
여러분이 포인터 변수를 정의하면, 컴퓨터는 메모리 주소를 저장할 수 있는 포인터 변수를 위한 메모리 공간을 할당하겠죠.
여기서 '메모리 주소를 저장할 수 있는'의 의미는 '메모리 주소에 저장되어 있는 값'하고는 전혀 연관이 없습니다.
long* fellow;
*fellow = 223323;
fellow는 포인터 변수이기 때문에 주소를 저장해야 합니다.
그런데 위 코드에서는 fellow 변수가 어떠한 곳을 가리키지 않은 상태에서 값을 할당했습니다.
'fellow 변수가 가리키는 주소'에 '223323이라는 데이터를 저장하라'는 명령을 내렸으나, fellow 변수가 가리키는 주소가 저장되지 않아 위 코드는 실행될 수 없습니다.
6. 포인터와 숫자
포인터는 정수형 타입이 아닙니다.
물론, 컴퓨터가 메모리 주소를 정수 형태로 다루기는 하지만 그것이 포인터 변수가 항상 정수형 타입이라는 의미는 아닙니다.
정수는 여러분들이 원하는 데로 더하고, 빼고, 나누고, 곱하는 등의 연산이 가능한 숫자들이죠.
하지만 포인터는 주소를 나타내기 때문에 그런 연산과는 관련이 없습니다.
예를 들어 두 개의 포인터 변수가 가지고 있는 주소 값들을 곱한다고 생각해 봅시다.
그건 각 주소에 들어있는 데이터를 가져와 곱하는 것과 다른 의미입니다.
그냥 엉뚱한 주소가 나올 뿐이죠.
실제로 코드를 짤 때도 이 개념을 알 수 있습니다.
int* pt;
pt = 0xB8000000; // ERROR
위 코드는 타입이 맞지 않아 에러를 발생시킵니다.
pt라는 이름의 포인터 변수를 선언하였고 거기에 주소를 직접 할당하려고 했지만, 타입 에러가 발생합니다.
이는 0xB8000000이 메모리 주소가 아니기 때문입니다.
C++는 이 값을 정수 값으로 판단했으며, 메모리 주소로 판단하지 않았습니다.
int* pt;
pt = (int*) 0xB8000000;
이제 타임 에러가 발생하지 않습니다.
(int*)를 통해 형변환 작업을 해주면 이제 C++는 0xB8000000을 메모리 주소로 인식합니다.
이처럼 포인터는 숫자, 그중에서도 정수형과 연관이 깊습니다.
7. new 키워드
포인터는 앞에서 살펴본 런타임과 컴파일 타임 중 런타임과 연관이 깊습니다.
변수는 명명된 메모리 공간을 의미하며, 컴파일 타임에 할당됩니다.
여태까지 변수의 메모리 주소를 포인터 변수에 담았으나, 사실 포인터 변수는 컴파일 타임보다 런타임에서 유동적으로 메모리 주소를 할당하고 싶을 때 더 유용합니다.
가장 대표적인 사례가 List입니다.
C++에서 포인터가 런타임에서 메모리 주소를 할당할 때는 new 키워드를 사용해야 합니다.
C#이나 Java와 같은 다른 OOP 언어를 접해보셨다면 익숙하실 겁니다.
int* pn = new int;
new int의 의미는 int 타입의 데이터를 저장하기 위한 새로운 메모리 공간을 할당하겠다는 의미입니다.
int higgens;
int* pt = &higgens;
pt 또한 int 타입의 데이터를 저장하기 위한 새로운 메모리 공간을 할당받고 있습니다.
여기서 pn과 pt의 차이는 int 변수의 존재 여부일 것입니다.
pn도 pt처럼 메모리 주소를 저장하기는 합니다.
하지만 pt처럼 따로 이름이 정해져 있지 않은데 그 주소를 뭐라고 불러야 할까요?
여기서 우리는 pn이 '데이터 객체(data object)'의 주소를 가리키고 있다고 말합니다.
주의할 점은 여기서 말한 '객체'는 '객체 지향 프로그래밍'을 칭할 때 말하는 그 '객체'가 아닙니다.
그냥, '무언가' 또는 '그것'에 더 가깝습니다.
말 그대로 데이터를 가지고 있는 무언가 또는 데이터를 가지고 있는 그것이라고 부를 수 있죠.
좀 더 추상적인 개념입니다.
이는 변수처럼 특별한 이름이 정해져 있지 않은 상태로 메모리에 할당된 이름 없는 블록을 가리키고 있기 때문입니다.
#include <iostream>
using namespace std;
int main() {
int nights = 1001;
int* pn = &nights;
int* pt = new int;
*pt = 1001;
cout << "pn : " << *pn << endl;
cout << "pt : " << *pt << endl;
return 0;
}
pt 변수를 보면 알 수 있듯이, int 타입의 데이터 객체를 저장하겠다는 정의를 내린 이후부터는 pn 변수와 동일하게 동작하고 있습니다.
메모리 주소를 다루기 때문에 기호가 하나 더 붙을 뿐, 어떻게 보면 int 타입의 변수와 크게 다르지 않게 느껴지기도 합니다.
포인터 변수를 호출해 사용할 때 * 기호를 붙이면 해당 포인터 변수가 가리키는 주소에 저장된 데이터 객체를 불러올 수 있습니다.
& 기호를 붙이면 앞에서 보았던 것처럼 포인터 변수가 가리키는 주소를 불러옵니다.
만약, *pt = 1005; 코드를 새로 입력해 준다면 pt 변수가 가리키고 있는 메모리 주소의 데이터 객체가 1005로 바뀐다는 것을 의미하겠죠.
수고하셨습니다!
'Dev. Study Note > C++' 카테고리의 다른 글
C++의 함수 프로토타입 (0) | 2024.06.26 |
---|---|
포인터와 배열, 포인터 산술 (0) | 2024.05.18 |
상속의 타입 (0) | 2024.02.13 |
2D Array Row, Column (0) | 2024.01.23 |
friend 키워드 (0) | 2024.01.10 |