나는 포인터가 싫어요. (포인터변수와 참조자를 함수에 인자로 주고받는 것에 대해.)

2020. 9. 3. 18:29컴퓨터 수업/C++

이 포스트는 C를 기반으로 작성되었습니다.

 

@변수의 종류를 알아보자.

1.일반변수    ex) int x;     --> 일반변수를 저장합니다.

2.포인터변수 ex) int * px;  --> 메모리주소를 저장합니다.

3.참조자변수 ex) int & rx;  --> 도플갱어를 저장합니다.

 

@선언과 초기화, 그리고 정의를 풀어서 읽어보자.

1.일반변수

//선언과 초기화
int x; // 선언
x = 10;// 초기화(할당)

위와 같이 변수는 "선언"과 "초기화" 2단계로 나누어서 결정합니다.

이를 동시에 하는 행위를 "정의"라고 합니다.

//동시에
int x = 10; // 정의

2.포인터변수

먼저 포인터변수를 선언과 초기화 2단계로 나누어서 설정하자면 (먼저 x가 정의되었다고 가정합시다.)

//선언과 초기화
int * px ; //1
px = &x;   //2

1에서 포인터변수 px를 선언하였습니다.

(*px가 포인터변수의 이름이 아닙니다! * asterisk는 내가 앞으로 포인터변수를 선언할거야! 라는 의미입니다.)

px는 포인터변수이고 포인터변수는 위에서 설명한 것과 같이 메모리주소를 저장합니다. 따라서 이 포인터변수를 초기화 하려면 어떤 주소를 넣어주어야합니다.

 

따라서, 2에서 x에 주소연산자를 붙여 포인터변수 px에 대입하였습니다. 따라서 px는 현재 x의 위치를 가르킵니다.

 

포인터변수의 선언과 초기화를 동시에 하는 정의에 대해서 알아보겠습니다.

//동시에
int * px = &x; // 정의

많은 사람들이 *를 역참조연산자로 생각하고 헷갈리는데 선언부에 있는 *asterisk는 역참조기호가 아닙니다. 따라서 다음과 같이 읽습니다.

 

"데이터타입은 int이고, 포인터변수를 선언할건데, 그 이름은 px야, 그런데 그 px는, x의 주소를 가르킬거야." 라고 읽습니다.

 

3. 참조자변수

 

참조자변수는 조금 특이합니다. 위에 설명했듯이 참조자변수는 도플갱어를 저장합니다. 따라서 선언과 동시에 초기화 하는 정의가 아니라면 선언하지 못합니다. 따라할 본체가 없다면 참조자변수는 의미를 가지지 않으니까요 :)

그러므로, 참조자변수의 정의는 다음과 같습니다.

//참조자변수의 정의
int &rx = x; // 정의

 

@함수의 실인수와 가인수 (Call by reference)

 

1.

#include <iostream>
using namespace std;
int func(int a)
{
	return a;
}
int main()
{
	int x = 10;
	int* px = &x;
	int& rx = x;

	cout << func(x) << endl; // 여기!
}

출력결과를 예상해보자.  여기! 부터 읽자면 (cout같은건 설명치 않겠다.)

func(x)와 int func(int a)를 다음과 같이 풀어 읽을 수 있다.

x를 func함수에 집어넣는다, 그런데 이때 그 관계는 다음과 같다.

int a = x;

그리고 그 값이 int func(int a)의 맨 앞 int에 의해서, int값으로 return되어 나올것이니

출력결과는 10 일것이다.

 

2. 

#include <iostream>
using namespace std;
int func(int *a) // 여기1!
{
	return a;
}

int main()
{
	int x = 10;
	int* px = &x;
	int& rx = x;

	cout << func(x) << endl; 
}

여기1! 를 보면 알겠지만 위의 소스코드에서 가인수 부분이 int *a가 되었다.

이 소스코드의 출력결과는 어떨까?

 

답은 먼저, 컴파일오류가 난다는 것이다.

아까 위에 풀어서 쓴것처럼 해보자.

int * a = x;

a는 포인터변수이므로 메모리주소를 담는다, 그런데 x는 일반변수 이므로 일반변수의 값을 저장하고 있다.

이 둘은 양립할 수 없으므로 컴파일에러가 난다.

 

3. 주소를 받고, 주소를 내보내고 싶다.

#include <iostream>
using namespace std;
int* func(int* a)
{
	return a;
}

int main()
{
	int x = 10;
	int* px = &x;
	int& rx = x;

	cout << func(&x) << endl;
	cout << &x << endl;
}

int*a = &x 이후, 그에대한 메모리주소를 return한다 (return a) 그런데 그 리턴값이 포인터변수 형식이므로

int* func(int *a) 에서 맨 앞 asterisk이다.

 

따라서 다음 그림과 같이, 연결된 것끼리 호환성을 지켜야된다는것을 알 수 있다.

@포인터와 참조자 예제

#include <iostream>
using namespace std;
void    f1(int* q)
{
    q = new int;
    *q = 2;
}

void    f2(int*& q)
{
    q = new int;
    *q = 2;
}

int     main()
{
    int	a = 1;
    int* p;

    p = &a;
    cout << *p << endl;
    f1(p);
    cout << *p << endl;
    f2(p);
    cout << *p << endl;
    return 0;
}

 

위 문제는 교수님이 내준 문제이다. 무언가에 중요하다고는 했는데.... 기억이 도무지 나질 않는다...

위 소스코드의 출력 결과를 예상하는것이 목표였는데 먼저 답은 1 1 2 이다.

 

천천히 한줄 한줄 읽어 보겠다.

int a=1;
int* p;
p = &a;
cout << *p << endl;

a라는 일반 변수를 선언했으며 1로 초기화 하였다.

p라는 포인터 변수를 선언했다.

p라는 포인터 변수를 a의 주소로 초기화 했다.

이때 p의 역참조(*p)는 ? 당연하게도 a의 일반변수값을 나타낼 것이다. -> 1

 f1(p);
 cout << *p << endl;
 void    f1(int* q)
{
    q = new int;
    *q = 2;
}

f1이라는 함수에 p를 가인수로 전달한다. 이때 실인수 p는 포인터변수 이다.

void 형식으로 함수가 선언되어 있는것을 보니, 아무것도 return하지 않는다, int *q라고 되어있으니 포인터변수로 가인수를 설정하였고, 따라서 위의 f1(p)와 같이 int* q = p이다. 따라서 포인터변수 q는 p로 초기화 되었다. (call by reference)

q = new int 라고 하였으니, q에 새로운 주소를 할당한다. 따라서 기존의 q와 p의 연결은 끊어진다.

그리고 역참조를 통해 해당 메모리공간에 2를 할당한다.

q와 p의 연결이 끊어진 이후에 새로운 숫자2를 할당했으니, p의 값은 그대로 1이다 -> 1

 

    f2(p);
    cout << *p << endl;
    void    f2(int*& q)
{
    q = new int;
    *q = 2;
}

위와 마찬가지의 흐름을 가지다가,

int*& q = p 이다. 이걸 풀어서 읽어보자면.

 

int ---> int 형식의

*   ---> 포인터변수의

&  ---> 도플갱어

q   ---> q를 선언하고

= p---> p로 초기화 하겠다.

 

"int형식의 포인터변수 도플갱어 q를 선언하고 p로 초기화 하겠다."

 

q는 포인터변수 이므로 메모리 주소를 담고 있을것이며, p 또한 마찬가지 일 것이다. 그런데 이 둘은 도플갱어 참조자 연산자에 의해 묶여있으므로, q를 바꾸면 p도 바뀔것이며, p를 바꿔도 q가 바뀔것이다.

 

따라서, q = new int를 통해 새로운 heap memory를 할당할때, p또한 마찬가지이다. (애초에 q는 실체가 없는 변수이므로 그저 p에 new int를 한것과 사실은 같다.)

 

그러므로 당연히 답은 ->2이다.