Managed 언어

C나 CPP 같은 언어들은 Managed 언어에 속한다. 이를 변수들의 메모리를 직접 동적 할당(dynamic allocation)으로 힙(heap) 영역에 만들 수 있고 변수의 주소를 통해 변수의 값까지 바꿀 수 있는 특성이 있다. 또한, Unmanaged 언어에서는 가비지 콜렉터(Garbage Collector)라는 장치가 존재하여 아무도 참조하지 않는 변수들을 메모리에서 제거하는 역할을 하는데 Managed 언어에서는 동적 할당한 메모리에 대해서 다시 메모리를 직접 해제하는 작업이 필요하다.

포인터

포인터는 변수의 메모리 주소를 담을 수 있는 변수 타입이다. 포인터로는 Managed 언어에서만 가능한 다양한 작업들을 할 수 있는데 여기에는 역참조(de-reference), 포인터 연산 등이 있다.

역참조

역참조를 한줄로 설명한다면, 메모리 주소를 통해 역으로 포인터 변수가 가르키고 있는 값을 변경하는 것이다. 말로는 한번에 안 와닿을 수 있으니, 예시를 통해 역참조가 언제 필요하는지 알아보자.

#include <iostream>

using namespace std;

void changeNum(int input_num){
	++input_num;
}

int main(){

	int num_val = 0;
    changeNum(num_val);
	cout << "num_val의 값을 출력해주세요 " << num_val << endl; 
}

코드를 보면 changeNum 함수에서는 parameter로 int 변수를받고 있고 이를 ++해주는 상황이다. 그리고 main 함수에서는 num_val이라는 변수를 선언하고 changeNum 함수에 이 변수를 넣으므로서 num_val의 값이 바뀌기를 희망하고 있다. 하지만, 이는 의도대로 되지 않는다. 그 이유를 알려면 pass by value와 pass by reference의 차이점을 알아야 한다.

위의 코드는 pass by value에 해당되는 상황이다. changeNum 함수에서 int 값을 받으면 이는 내부에서 shallow copy를 통해 stack이라는 메모리에 값을 임시 저장한다. 함수가 끝나면 stack은 초기화 된다. 따라서 원본 변수인 num_val에 접근하지도, 값을 변경하지도 못하는 상황인 것이다. 그러면 어떻게 해야할까? 여기서 pointer의 역참조가 등장한다.

#include <iostream>

using namespace std;

void changeNum(int* num_ptr) {
	++(*num_ptr);
}

int main() {

	int num_val = 0;
	int* ptr = &num_val;
	changeNum(ptr);
	cout << "num_val의 값을 출력해주세요 " << num_val << endl;
}

위의 코드에서는 포인터의 선언 및 초기화, 함수 인자로서의 pointer 받기를 유심히 봐보자.

먼저, 포인터의 선언 및 초기화 부분이다.

int* ptr = &num_val;

모든 cpp 변수들이 그렇듯이 변수를 선언하려면 그 변수 타입을 알고 있어야 한다. 포인터 변수 타입은 그 포인터가 가르키고 있는 변수를 어떤 타입으로 바라보면 좋을지를 생각하고 설정하면 된다. 포인터 변수도 다른 포인터 변수 타입으로 형 변환(type casting)이 가능하다.

L-value에 포인터 변수를 설정하였으면 R-value에는 이 포인터가 가르킬 변수의 메모리 주소를입력하면 된다. 이렇게 포인터의 선언 및 초기화 부분을 완료했다.

다음은, 함수 인자로서의 pointer 받기 부분이다. 함수 선언 부분에서 parameter 인자로 int pointer 변수를 받고 있다. 이렇게 하는 방법을 pass by reference라고 한다. 이렇게 값을 넘겨주면 함수 내부에서 함수 밖에 선언되어 있는 값의 원본 값을 바꿀 수 있다.

배열과 포인터

#include <iostream>

// Function to print elements of an array
void printArray(int* arr, int size) {
    for(int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    // Declare and initialize an array
    int arr[5] = {1, 2, 3, 4, 5};
    
    // Declare a pointer to the array
    int* ptr = arr;  // ptr now points to the first element of arr
    
    // Print the array using the pointer
    printArray(ptr, 5);  // Passing the pointer and the size of the array
    
    return 0;
}

배열을 선언 및 초기화 하는 방법은 다음과 같다.

int arr[5] = {1,2,3,4,5};

여기서 배열의 이름은 곧 배열 인자의 첫 시작 주소가 된다. 즉 변수 1이 들어있는 메모리 주소는 arr를 통해 바로 접근이 가능하다.

다음은 포인터 변수를 만들어서 배열의 시작 주소를 받는 방법이다.

int* ptr = arr;

처음에는 배열이 이름이 곧 배열의 메모리 시작 주소하는 것이 안 와닿을 수 있다. 하지만 쓰다보면 익숙해 질 것이다! int 포인터로 만든 것은 배열의 인자가 int 타입이기 때문이다.

포인터 연산

놀라운 사실이지만 포인터 또한 연산이 가능하다. 하지만 일반적인 산술연산은 아니다.만약 일반적인 대수학에서 증감이 1을 기준으로 움직인다면 포인터 연산은 증감이 그 포인터의 변수 타입의 크기만큼 움직이게 된다. 예를 들어보면 int 로 선언된 포인터에 ++을 하면 4만큼 움직이고, double 로 선언된 포인터는 8만큼 움직이게 된다.

const와 pointer

pointer는 const와 같이 쓸 수 있다(사실 굉장히 많이...). 참으로 아이러니 한 것이 pointer를 통해 원본 값의 변경에 대한 문을 열어주었다면 const는 반대로 이를 막는 방법이다.

int x0 = 10;
int x1 = 10;
int x2 = 10;
int x3 = 10;

int* ptr = &x0;                  // 일반적인 포인터
int* const ptr1 = &x1;           // 포인터 값 변경 x
const int* ptr2 = &x2;           // 참조 값 변경 x
const int* const ptr3 = &x3;     // 포인터 & 참조 값 둘다 변경 x

위 코드에서는 일반적인 포인터 선언부터 const가 포인터 변수에 어떻게 들어갈 수 있는지 모든 경우를 다 나열한 케이스이다.

  • case 1.
    int* ptr = &x0;
    일반적인 포인터 선언
  • case 2.
    int* const ptr1 = &x1;
    ptr1이 const. ptr1의 값을 변경할 수 없다. x1의 값을 변경하는 것은 O
  • case 3.
    const int* ptr2 = &x2;
    ptr2를 통해 x2의 값을 바꿀 수 없다. 굉장히 많이 쓰이는 유형이며 특히 함수에서 pass-by-reference를 할때 함수 내에서 원본 값을 수정하는 것을 방지하고자 parameter type을 이렇게 받는다.
  • case 4.
    const int* const ptr3 = &x2;
    붙일 수 있는 모든 곳에 const를 붙였다...

포인터의 또 다른 강력한 장점은 함수에서 변수를 읽을때 많은 메모리를 아낄 수 있다는 점이다. 아직 복사 생성자와 shallow copy등에 대한 내용을 다루지는 않았지만, 함수에서 어떠한 외부 변수를 input으로 받는 경우에 복사 생성자로 shallow copy를 통해 이 외부 변수의 사본을 만들게 된다. 문제는, 외부 변수가 작은 값이면 상관없지만 큰 값인 경우에는 엄청나게 많은 메모리를 써야 된다. 만약 여기서 pass by reference로 이 외부 변수의 주소 값만 가져오게 된다면 shallow copy로 쓰게 되는 메모리는 오직 8 byte(64 운영체제라 가정, pointer 변수 type은 8byte을 쓴다.)만 사용하면 된다.

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글