본문 바로가기

Information Technology/C++

[C++] 클래스와 const

이 글은 개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;)


const int a = 10;
a = 5; // error!

우리는 변수의 값을 변경할 수 없게 만들어 꼭 상수처럼 사용하고 싶을 때 const를 선언합니다.

위의 코드 블록에서도 a는 int형 변수이지만, const 선언을 했기 때문에 값을 변경할 수 없는 것처럼요.

이는 클래스 내의 멤버들에서도 동일합니다.

#include <iostream>
using namespace std;

class Something
{
public:
	int m_value = 0;
    Something(int value) :m_value(value) { cout << "Constructor" << endl; }
	void setValue(int value) { m_value = value; }
	int getValue() { return m_value; }
};

int main()
{
	const Something something;
    
	something.setValue(3); //error!
	cout << something.getValue() << endl; //error!

	return 0;
}

 

코드 블록에서 Something 클래스를 선언했습니다.

그다음 main함수에서 Something 자료형의 instance인 something을 정의할 때 const 선언을 하게 되면,

something은 그 값을 바꿀 수 없기 때문에 something의 멤버 중 m_value의 값을 변경하는 setValue함수가 호출될 수 없죠.

그런데 의아한 점은 값을 바꾸지 않고 호출만 하는 getValue함수 역시 에러가 발생해서 호출할 수 없다는 점입니다.

그 이유는 컴파일러가 실행을 시킬지 말지 판단을 할 때, getValue함수가 내부에서 값을 변경하느냐 하지 않느냐를 보는 게 아니라 그 함수가 const냐 아니냐를 보고 판단을 하기 때문이라고 해요.

이 문제를 해결하는 방법은 매우 간단합니다.

int getValue() const { return m_value; }

위의 코드처럼 함수의 바디로 넘어가기 전에 const 선언을 해주면 됩니다.

여기에 붙는 const는 '이 함수는 내부에서 어떤 값도 변경하지 않는다'라는 의미를 컴파일러에게 전달해준다고 생각하면 될 것 같습니다.

이렇게 getValue 함수에 const선언을 해주면 main함수의 const 선언을 한 something 객체에서도 getValue함수를 호출할 수 있습니다!

 

그런데 또 드는 의문이, 그렇다면 setValue함수에도 const 선언을 하면 something에서 호출할 수 있는 게 아니냐? 하실 수 있습니다. 

void setValue(int value) const { m_value = value; }

그런 생각에 이렇게 코드를 짜 보면, 컴파일 에러가 발생합니다. const 선언이 붙은 함수의 내부에서는 값을 바꿀 수 없도록 컴파일러가 차단을 하는 것 같아요. const 선언의 의미 자체가 값을 상수화 시켜 변하지 못하게 만드는 것이니 어찌 보면 당연한 결과입니다.


void print(Something something)
{
	cout << something.m_value << endl;
   	cout << &something.m_value << endl;
}

 

이번에는 Something 클래스 외부에 print 함수를 정의했습니다. print 함수는 Something 클래스의 인스턴스를 받아와서 m_value를 출력하는 기능을 가집니다.

int main()
{
	const Something something;
	cout << &something << endl;

	print(something);

	return 0;
}

main함수를 다음과 같이 실행하면

다음과 같은 결과가 출력됩니다.

1. 처음 something을 정의할 때 생성자가 호출되면서 Construct라는 문구가 출력됩니다.

2. something의 주소가 출력되고

3. print 함수에 의해 something의 m_value인 0이 출력되고

4. 마지막으로 print 함수가 원래 something에서 복사해온 something의 주소가 출력됩니다.

 

변수를 함수의 매개변수로 삼을 때 포인터(*)나 레퍼런스(&)를 사용하지 않으면 복사를 해서 인스턴스를 가져오게 되는데 이는 사용자 정의 자료형을 가져올 때도 동일합니다. 클래스를 설정할 때 자동으로 복사 생성자가 생성되기 때문입니다.(int, float과 같은 자료형도 결국 클래스의 형태로 '미리' 정의된 자료형이기 때문에 이 복사 생성자에 의해 복사가 이뤄지는 것 같습니다.)

복사의 과정을 거치게 되면 프로그램의 효율이나 속도가 저하될 수밖에 없습니다. 이러한 문제점을 해결하기 위해서 클래스를 매개변수로 가져올 때에도 레퍼런스(&)를 사용합니다.

void print(const Something& something)
{
	cout << something.m_value << endl;
	cout << &something.m_value << endl;
}

위의 코드 블록처럼 매개 변수로 받는 값이 변경되는 걸 방지하기 위해 const를 선언하고 자료형 뒤에 레퍼런스를 붙여주면 됩니다.

print함수를 수정하고 다시 main함수를 실행시키면

원래 something 인스턴스의 주소와(두 번째 줄) print함수에서 레퍼런스로 받아온 something의 주소(네 번째 줄)가 동일함을 알 수 있습니다! 이는 다른 자료형을 받아올 때도 동일하게 적용됩니다.

복사 과정을 거치기보다는 인스턴스로 받아와서 처리하는 게 프로그램의 효율이나 속도면에서 바람직할 것 같습니다.


#include <iostream>
#include <string>
using namespace std;

class Something
{
public:
	string m_value = "default";

	const string& getValue() const 
	{
		cout << "const version" << endl;
		return m_value; 
	}
	string& getValue() 
	{
		cout << "non-const version" << endl;
		return m_value; 
	}
};

int main()
{
	Something something;
	something.getValue();

	const Something something2;
	something2.getValue();

	return 0;
}

이번에는 다른 예제를 다뤄보겠습니다.

여기서 Something 클래스는 함수에 const를 선언한 getValue와 그렇지 않은 getValue 함수를 모두 가지고 있습니다.

그리고 main함수에서 Something 자료형의 일반 인스턴스인 something과 const를 선언한 something2를 정의합니다.

각각의 인스턴스에 대해 getValue 함수를 선언하면 어떻게 될까요?

일반 인스턴스인 something에서는 const 선언을 하지 않은 getValue함수가 호출되고,

const선언을 한 인스턴스인 something2에서는 const 선언을 한 getValue가 호출됨을 알 수 있습니다.

즉, 어느 클래스의 인스턴스에서 멤버 함수를 호출할 때 컴파일러에서 그 인스턴스의 유형에 알맞은 함수를 호출한다고 보면 될 것 같습니다!

 

그리고 const 선언을 하지 않은 getValue 함수는 그냥 string 자료형을 리턴하기 때문에

Something something;
something.getValue()=10;

main함수에서 getValue를 통해 리턴한 값을 변경할 수 있습니다.

하지만 const 선언을 한 getValue함수는 string 자료형을 const로 리턴하기 때문에 getValue로 리턴한 값을 변경할 수 없습니다.


C++에서 함수의 오버 로딩은 보통 매개변수를 통해 같은 이름의 함수를 구분하고, 매개변수가 같은데 리턴 값만 다른 경우에는 오버 로딩이 불가능합니다. 하지만 매개변수는 같고 리턴 값은 다른데 const 선언을 통해 오버 로딩을 할 수 있다는 점은 신기하기도 하고 주의해야 할 것 같습니다.