[C++] 포인터 한방에 이해하기 (Call by Value vs Call by Reference)
IT/Programming

[C++] 포인터 한방에 이해하기 (Call by Value vs Call by Reference)

728x90
반응형

포인터 (Pointer)

포인터 변수

포인터란 "어떤 것을 가리키는 것"을 의미한다.

C나 C++ 등의 프로그래밍 언어에서 포인터는 "주소를 가리키는 것"을 뜻하며, 이러한 것을 저장하는 변수를 포인터 변수라고 한다.

프로그래밍에서 포인터가 악명이 높기로 유명하지만, 의외로 단순하니 겁먹을 필요가 없다.

 

아래 코드를 보자.

int a = 10;
int* p = &a;

a라는 int형 변수를 선언하고 10으로 초기화하였다.

p라는 int형 포인터 변수를 선언하고 a의 주소값으로 초기화하였다.

 

여기서 한가지 짚고 넘어가야 할 것이 있다.

  • & (주소 연산자)

    주소 연산자 &를 사용하면 변수에 할당된 메모리 주소를 확인할 수 있다.
    참고로 비트 연산자 AND(&)와 모양은 같지만, 주소 연산자는 단항 연산자이고 비트 AND연산자는 이항 연산자이다.

  • * (역참조 연산자)

    역참조 연산자 *를 사용하면 특정 메모리의 주소값에 접근할 수 있다.

 

즉 위의 코드는 아래 그림과 같은 구조를 가진다.

  1. int a = 10; // int형 변수 선언
    • int형이므로 a라는 이름의 4byte 메모리 공간을 할당 (메모리 주소 : 0x0014)
    • 해당 메모리에 숫자 10 입력
  2. int* p = &a; // int형 포인터 변수 선언
    • int형 이므로 p라는 이름의 4byte 메모리 공간 할당 (메모리 주소 : 0x0004)
    • 해당 메모리에 변수 a의 주소값(0x0014) 입력

 

한번 확인해보자.

int a = 10;
int* p = &a;

cout << "a : " << a << endl;
cout << "&a : " << &a << endl;
cout << "p : " << p << endl;
cout << "*p : " << *p << endl;
cout << "&p : " << &p << endl;

이러한 구조로 메모리에 할당이 된 것을 확인할 수 있다.

 

여기서 중요한 점은 역참조 연산자 *를 통해 포인터 변수가 가리키는 주소 a의 값 10을 알 수 있다는 것이다.

 

포인터 변수가 자료형을 가져야 하는 이유도 여기에 있다.

자료형이 없다면 포인터는 역참조 시 해당 메모리의 값을 해석할 수 없다.

 

위의 경우 int형이라고 명시가 되어 있었기 때문에 0073FA74 주소의 메모리 값을 int형으로 읽어올 수 있는 것이다.

 

이중 포인터, 3중 포인터

위의 개념을 잘 이해했다면 2중 포인터, 3중 포인터도 거뜬하다.

긴 말 필요 없이 아래 코드를 보자.

int a = 10;
int* p1 = &a;
int** p2 = &p1;
int*** p3 = &p2;

cout << "a : " << a << endl;
cout << "&a : " << &a << '\n' << endl;

cout << "p1 : " << p1 << endl;
cout << "*p1 : " << *p1 << endl;
cout << "&p1 : " << &p1 << '\n' <<  endl;

cout << "p2 : " << p2 << endl;
cout << "*p2 : " << *p2 << endl;
cout << "**p2 : " << **p2 << endl;
cout << "&p2 : " << &p2 << '\n' <<  endl;

cout << "p3 : " << p3 << endl;
cout << "*p3 : " << *p3 << endl;
cout << "**p3 : " << **p3 << endl;
cout << "***p3 : " << ***p3 << endl;
cout << "&p3 : " << &p3 << endl;

 

Call by Value vs Call by Reference

자 이제 우리는 포인터가 어떤 역할을 하는지 알고 있다.

그렇다면 함수의 매개변수에 변수 값을 전달하는 것과 주소 값을 전달할 때 어떤 차이가 있는지 보자.

#include <iostream>
using namespace std;

void callByVal(int a){
    a = 20;
}

void callByRef(int* b){
    *b = 20;
}

int main()
{
    int a = 10;
    int b = 10;

    callByVal(a);
    callByRef(&b);

    cout << "a: " << a << endl;
    cout << "b: " << b << endl;

    return 0;
}

 

int형 ab변수를 선언하고 10으로 초기화하였다.

이후 callByVal 함수에서는 a의 값을 받아서 20으로 수정하였고, callByRef 함수에서는 b의 주소값을 받아 20으로 수정하였다.

그런 다음 다시 main에서 a와 b를 출력해보니 b20으로 바뀌었지만, a는 그대로 10으로 출력된다.

이제 이해가 되는가?

 

callByVal 함수에서는 a의 값 10을 받아서 메모리에 새로운 공간을 할당하고 받은 값 10을 입력하였다. 그런 다음 새로운 메모리 공간의 10을 20으로 변환하였다. 따라서 main에 있는 기존 a의 값은 전혀 변함이 없었던 것이다.

반면 callByRef 함수에서는 b의 주소값을 받아서 메모리에 새로운 공간을 할당하고 b의 주소값을 입력하였다. 그런 다음 새로운 메모리 공간에 있는 주소값이 가리키는 b변수를 찾아 해당 값을 20으로 변환하였다. 따라서 main에 있는 기존 b값이 20으로 변경된 것이다.

 

배열과 포인터

배열은 사실 포인터와 같은 역할을 한다.

포인터는 주소를 담는 변수라고 계속해서 이야기해왔다.

배열 변수 또한 포인터와 동일하게 배열의 첫 번째 index의 주소를 가지고 있다.

 

아래 코드를 보자.

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

cout << "arr[0] : " << arr[0] << endl;
cout << "arr : " << arr << endl;

 

arr 변수가 arr[0]의 주소값을 가지고 있음을 알 수 있다.

 

그렇다면 이를 응용하여 배열에 접근할 때 아래와 같이 코드를 작성할 수도 있다.

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

cout << "arr : " << arr << ",  ";
cout << "arr[0] : " << arr[0] << ",  ";
cout << "*arr : " << *arr << endl;

cout << "arr+1 : " << arr+1 << ",  ";
cout << "arr[1] : " << arr[1] << ",  ";
cout << "*(arr+1) : " << *(arr+1) << endl;

cout << "arr+2 : " << arr+2 << ",  ";
cout << "arr[2] : " << arr[2] << ",  ";
cout << "*(arr+2) : " << *(arr+2) << endl;

cout << "arr+3 : " << arr+3 << ",  ";
cout << "arr[3] : " << arr[3] << ",  ";
cout << "*(arr+3) : " << *(arr+3) << endl;

cout << "arr+4 : " << arr+4 << ",  ";
cout << "arr[4] : " << arr[4] << ",  ";
cout << "*(arr+4) : " << *(arr+4) << endl;

 

객체 멤버의 접근

C++에서 객체의 멤버에 접근하는 방법에 대해 알아보자.

객체에서 멤버에 접근할 때는 . 연산자를 사용하며 포인터 객체의 멤버에 접근할 때는 연산자를 사용한다.

#include <iostream>
using namespace std;

class Test{
public:
    int a;
    int b;
};

int main()
{
    Test T;
    Test* pT = &T;

    T.a = 1;
    pT->b = 2;

    cout << "a : " << pT->a << endl;
    cout << "b : " << T.b << endl;

    return 0;
}
  • Test Class 객체의 a 멤버변수에 접근하는 방법으로 T.a, (&T)→a, pT→a, (*pT).a 가 모두 가능하다.

 

 

728x90
반응형