레퍼런스의 바른 사용법

레퍼런스는 편리하지만 단점도 있기 때문에 올바르게 사용해야 합니다.

 

레퍼런스의 특성인 초기화가 끝나면 을 변경할 수 없다는 점은 동적할당(힙메모리)을 사용할 때 단점이 됩니다.

 

메모리는 해제했지만 레퍼런스의 주소는 여전히 남아있다는 것이 문제인데, 이 레퍼런스를 사용하면 해제된 메모리에 접근할 수 있습니다. 이런 경우를 살펴보겠습니다.

 

나쁜 사용법 (권장되지 않는)

아래의 함수는 참조형을 반환합니다. new 키워드로 힙메모리에 객체를 생성하고 그 포인터를 바깥으로 전달합니다.

 

 

바깥에서 받는 것은 레퍼런스입니다. 이 레퍼런스에 동적 메모리가 걸려있는데 동적 메모리는 프로그래머가 직접 해제해야 합니다.

 

 main 함수에서 레퍼런스에 delete 키워드를 사용할 수는 있습니다. 하지만 관례가 아니니 레퍼런스를 포인터에 할당해서 delete 하겠습니다. 여기서도 이미 꼬여버린 느낌입니다. 하나가 꼬이면 연달아 문제가 발생하는 것과 같습니다.

 

마지막으로 레퍼런스인 newPos 의 문제인데 해제한 메모리의 주소가 여전히 남아있습니다. 이런 상태에서는 NULL로 수정할 수도 없고 만약 delete 명령어를 사용하면 접근이 불가능한 메모리(해제한 힙 메모리)를 삭제하려는 행위이므로 프로세스는 크래시합니다.

 

허가가 없는 메모리를 접근하려는 행위는 치명적인 오류로 예외 메시지 하나 받지 못하고 그냥 멈춰있다가 운영체제가 수거해갑니다. 레퍼런스의 장점이 있지만 잘못 사용했을 시 치명적인 오류를 발생할 소지가 있습니다.

 

 

해결책

해결책은 레퍼런스의 원래 목적에 따라 사용하는 것 입니다. 레퍼런스는 메모리를 관리하는 구조에 적당하지 않습니다.

 

동적인 메모리는 main 함수에서 객체를 생성하여 함수로 넘겨줄 때 레퍼런스로 받는 것입니다. 그렇게 하면

 

▶ main 함수에서 new 와 delete 로 메모리를 관리한다.

    ∟ 메모리에 할당하는 함수측(calling function 호출하는 함수)이 해제까지 담당하는게 원칙이다.

        할당따로 해제따로 하지 않는다. (new 와 delete 는 같은 영역에 있어야 한다)

▶ 레퍼런스는 함수 종료시 지역변수와 함께 자연 소멸된다.

    ∟ 레퍼런스가 남은 것을 걱정할 필요가 없다.

 

아래 코드와 같이 void 에 레퍼런스로 받아서 사용할 수 있다. 레퍼런스는 말하자면 똑같은 대상을 부르는 또 하나의 별명입니다. 별명으로 불러서 값을 변경해도 원본에 똑같이 적용되는 겁니다.

 

 

main 함수에서 메모리를 해제하니 깔끔하다. 생성한 main 함수가 책임지고 delete 하는 모습입니다. 물론 경우에 따라 필요하겠지만 이것을 불필요하게 꼬아놓지 않도록 주의합니다.

 

레퍼런스를 사용하는 이유는?

레퍼런스의 위험성이 많은데도 불구하고 레퍼런스를 사용하는 이유는 무엇일까요?

 

첫번째로 성능입니다. pass by value 값에 의한 전달은 한계가 분명하죠. 클래스를 값으로 전달하는 경우 copy constructor 와 desctructor 를 총 4번이나 호출하는 구조입니다. 메모리와 속도에 있어서 비할바가 아닙니다. C++은 성능이 필요한 소프트웨어 제작에 최적화되어 있습니다.

 

포인터와 레퍼런스가 헷갈립니다.

레퍼런스는 초기화가 끝나면 NULL이나 재할당 할 수 없습니다. 하지만 사용이 쉽고 코드의 가독성이 좋죠. * 역참조가 없는 자체가 읽는 속도가 빠릅니다.

 

포인터는 레퍼런스와 같은 제약이 없지만 사용하는게 좀 더 어렵고 가독성이 떨어집니다. -> * ** 이런 문자들이 많이 나오니까 이 포인터가 어디를 가리키고 있는가를 한번 더 생각해야 합니다.

 

각각 장단점이 있으니까 최대한 활용합니다.

요약

위의 코드들은 하나의 예에 지나지 않습니다. 어떻게 코드를 작성하는게 옳은가에 대하여는 스타일의 차이가 있을 수 있습니다. 또 맥락이란 것도 있지요. 누군가에게는 잘한 코드가 누군가 봤을 때는 Garbage 가 될 수도 있습니다. 그 코드가 쓰여진 환경이나 상황이 다를 수 있지요.

 

절대선에 가까운 것들이 있겠지만 내 일이 아니라면 그런 것들에 일일히 흥분하는 것은 불필요할지 모릅니다.

 

그러니까 절대적인 법칙을 생각하는 것 보다 이 코드를 누구를 위해 작성하는가를 생각해야 합니다. 회사를 위해서, 자기 자신을 위해서, 혹은 오픈 소스를 위해서...? 컴파일러를 위한 코드를 쓰는 것과 사람이 읽기 쉬운 코드를 쓰는 것은 분명 다릅니다. 컴퓨터가 숫자를 읽는 방식과 인간이 코드를 읽고 이해하는 방식은 차이가 크니까요.

 

일단 인간을 위한 코드를 쓰는게 맞습니다. 왜냐하면 오늘 쓴 코드를 수정하기 위해 3개월뒤에 처음으로 볼 사람은 자신일 확률이 높기 때문입니다. 그런면에서 인간 -> 자기자신 을 위한 코드를 생각해보는 일은 좋네요. 3개월 후에 빡치는가 아니면 커피 한잔의 여유를 느끼는가 아마 그런 차이겠죠.

*예제코드

#include <iostream>

using namespace std;

#define Line cout <<"\n--------------------------------------------------------\n"

class MyPosition {
public:
	MyPosition();
	MyPosition(MyPosition&);
	MyPosition(int x, int y);
	~MyPosition();
	void setXPos(int x) { this->xPos = x; }
	int getXPos() const {
		// this->xPos = 999;
		return this->xPos;}	
	void setYPos(int y) { this->yPos = y; }
	int getYPos() const { return this->yPos; }

private:
	int xPos;
	int yPos;
};

MyPosition::MyPosition(int x, int y): xPos(x), yPos(y) 
{
	cout << " [constructor called..      ] ";
	cout << " address: " << this << endl;
}

MyPosition::MyPosition()
{
	xPos = 50;
	yPos = 70;
	cout << " [basic constructor called..] ";
	cout << " address: " << this << endl;

}


MyPosition::MyPosition(MyPosition&)
{
	cout << " [copy constructor called...] ";
	cout << " address: " << this << endl;
}

MyPosition::~MyPosition()
{
	cout << " [desctrutor called...      ] ";
	cout << " address: " << this << endl;
}

MyPosition& NewFunciton();
void safeFunction(MyPosition& safePos);

int main()
{
	Line;
	cout << "    -> making new position..." << endl;
	
	Line;
	MyPosition& newPos = NewFunciton();
	int posX = newPos.getXPos();
	int posY = newPos.getYPos();
	Line;
	cout << " newPos, posX : " << posX << endl;
	cout << " newPos, posY : " << posY << endl;


	MyPosition* myPos = &newPos;
	delete myPos;

	Line;
	// 레퍼런스는 수정 불가능
	// &newPos = NULL;

	// 메모리를 해제해도 레퍼런스에 여전히 주소가 남아있다.
	cout << " <<< danger >>> : " << &newPos << endl;

	// 해제된 메모리의 주소를 delete 하면 프로세스가 크래시함.
	// delete& newPos;
	
	Line;
	MyPosition* safePos = new MyPosition();
	safeFunction(*safePos);
	delete safePos;

	return 0;
}

MyPosition& NewFunciton()
{
	MyPosition* HeapPos = new MyPosition(70, 99);
	cout << " HeapPos : " << HeapPos << endl;
	return *HeapPos;
}

void safeFunction(MyPosition& safePos)
{
	Line;
	cout << " --->> Safe function : " << &safePos << endl;
	cout << " --->> Reference getXPos : " << safePos.getXPos() << endl;
	cout << " --->> Reference getYPos : " << safePos.getYPos() << endl;
}

 

공유하기

facebook twitter kakaoTalk kakaostory naver band