Post List

2017/12/20

C++__const keyword(const 멤버함수와 const 객체)

이번에는 const 키워드에 대해 알아보겠습니다. 

변수를 상수화시킬 때 사용하는 키워드로 const를 사용합니다. 데이터를 상수화시켜서 사용자혹은 

의도치 않은 어느 곳에서 값이 변경되지 않게 보호하기 위해서 주로 사용하는데요.
const키워드는 클래스 바깥에서 전역 혹은 네임스페이스 유효범위의 상수를 선언하는데 쓸 수 있고 파일,  함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있으며, 클래스 내부에서는
정적 멤버 및 비정적 데이터 멤버 모두 상수로 선언할 수 있습니다.
그 외에도 포인터 자체나 포인터가 가리키는 데이터도 상수로 지정할 수 있습니다.

이 처럼 const의 사용함에 있어 제약에 맞지 않는 코드의 경우 컴파일러에서 에러로 내보내주며, 다른 프로그래머들과 불변하다는 의도를 나눌 수 있는 의미이기도 합니다.


const를 설명하기전 const와 비슷하게 많이 사용되고있는 define에 대해서 간단하게 이야기 해보겠습니다.
상수를 사용할때 #define 전처리기를 사용하는 경우도 있는데요, define을 사용하면 선행 처리자로 처리되기때문에
가급적 컴파일러에서 처리되는 방안으로 사용하길 추천을 하고 있습니다.

1
#define MATH_PI 3.14
cs
라는 구문이 있을때 우리는 MATH_PI라는 이름을 통해 3.14의 상수를 사용하게 됩니다.
코드에서는 MATH_PI를 사용하여도 MATH_PI는 define으로 정의된 상수이기 때문에 컴파일러가 실행되기 전 선행 처리자에 의해 코드는 상수로 바뀌게 됩니다. 컴파일러는 MATH_PI라는 이름의 존재에 대해서 알 수 없으며, 만일 상수로 대체된 코드에서 에러가 발생시 3.14로 에러메세지가 나오기때문에 해당 부분을 찾기도 어렵게 되겠지요.

그래서 컴파일러에서 식별이 가능한 구문으로 대체하여 사용하는것을 추천합니다.

1
const float math_pi = 3.14;
cs
그 방안으로는 const 키워드가 해당되겠습니다.

const를 사용하게 되면 컴파일러가 math_pi라는 이름을 식별할 수 있게되고, 에러 또한 금방 찾을 수 있습니다.

또 define으로 선언된 MATH_PI는  코드에 추가된 수 만큼 사본이 생기는데요, const로 선언된 math_pi의 경우 사용된 횟수에 무관하게 사본이 딱 한개만 생기기 때문에 코드의 크기가 define으로 선언된 파일보다 math_pi로 사용된 파일 크기가 더 작아질수 도 있습니다.

이번에는 const키워드를 클래스내에서 사용되는 경우에 대해 알아보겠습니다.

첫번째로, const 멤버변수 및 멤버함수입니다.
const 멤버 변수는 어떤 상수의 유효범위를 클래스로 한정하고자 할때, 혹은 멤버변수를 변경하지 않고자 할때 사용합니다. const는 선언과 동시에 초기화가 이루어져야 한다는것을 알고 계실겁니다.
그래서 const 멤버변수의 초기화는 생성자의 이니셜라이저를 통해서만 초기화가 가능한데요, (c++11에서는 약간 다릅니다.)


1
2
3
4
5
6
7
8
class Object
{
public:
    Object(int data) : m_data(data) {    }
private:
    const int m_data;
    
};
cs
이니셜라이저는 '생성자(인자) : 멤버변수1(초기화데이터1), 멤버변수2(초기화데이터2)..  { }'
이런식으로 변수명과 괄호안에 초기화할 값을 넣어주면 됩니다. const 변수뿐 아니라 레퍼런스변수 일반 자료형 변수, 포인터 변수 등 초기화가 가능합니다.

방금 이야기나온 멤버변수 초기화를 위한 이니셜라이저(Initialization)에 대해 잠깐 알아보겠습니다.
이니셜라이저는 생성자보다 먼저 호출이 되어 멤버변수를 초기화해주는데요. 객체가 메모리에 할당할 때 초기화가 이루어집니다. 그래서 const나 레퍼런스 멤버변수는 메모리에 할당됨과 동시에 초기화가 이루어져야하는 특징때문에 생성자의 이니셜라이저에서만 초기화가 가능하답니다.

이니셜라이져를 반드시 사용해야 하는 경우는 크게 두가지로 볼 수 있는데요, 첫번째로 const 또는 레퍼런스 멤버변수가 있는 경우.
두번째로 부모클래스의 인자가 있는 생성자를 호출할 때입니다.


1
2
3
private:
    const int m_data = 3;
cs
단, C++11에서는 const 멤버변수 선언과 동시에 초기화가 가능하다고 하니 이점도 참고하시기 바랍니다.

그래서 한가지 테스트를 해보았습니다. 


1
2
3
4
5
6
7
8
9
10
11
class Object
{
public:
    Object(int data) : m_data(data) {    }
private:
    const int m_data = 3;
};
int main()
{
    const Object* const pObject = new Object(5);
cs
멤버변수 선언 시 m_data를 3으로 넣어주고 이니셜라이저로 다른 값으로 초기화 해주었을 때
객체가 생성된 후 실제로 어느 값이 저장이 되는지 궁금해졌습니다..

디버깅 결과.. 


m_data는 이니셜라이저로 초기화된값이 최종적으로 들어가는걸 확인할 수 있었습니다.


이번에는 멤버함수에 const가 붙은 경우인데요, const 멤버함수로 인해 해당 멤버 함수가 상수 객체에 대해 호출될 함수라는 사실을 알려주는 것입니다.
const 멤버함수를 사용하면 클래스의 인터페이스를 이해하기 좋게 하기 위함인데요, 그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자쪽에서 알 수 있게 해줍니다.

그리고 const키워드를 통해 상수 객체를 사용할 수 있게 하자는 것인데, 코드의 효율을 위해 아주 중요한 부분이기도 합니다.


두번째로 const 객체입니다.
멤버변수나 멤버함수 뿐만 아니라 객체에도 const 키워드를 사용할 수 있습니다. 객체 생성시에 const 키워드를 사용하면, 그 객체는 상수로 취급되어 초기화된 데이터 외의 다른 데이터로 변경할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Object
{
public:
    Object(int data) : m_data(data)    {
    }
public:
    int GetData() const { return m_data;  }
    void SetData(int data) { m_data = data;  }
private:
    int m_data;
};
cs
간단한 클래스로 확인해보겠습니다.

1
2
3
4
5
6
int main()
{
    const Object* pObject = new Object(5);
    pObject->GetData();
cs
const로 객체를 생성하고, GetData()를 호출하는데 까지는 문제가 없습니다.

1
    pObject->SetData(1);
cs
SetData(1)를 호출하여 m_data를 변경해보겠습니다. 위 코드를 추가하는 순간
컴파일 에러가 발생합니다. 왜냐하면 위에서 말했듯 객체에 const가 붙으면 그 객체의 어떠한 멤버변수도
수정을 할 수 없다는 의미입니다.

당연하게도 const 객체가 const가 붙은 함수를 호출하는데에는 전혀 문제가 없습니다.
하지만 멤버함수라도 함수내에서 멤버변수의 데이터가 변경이 일어날 소지가 있다면 컴파일 에러가 발생합니다.

1
2
public:
    void InitData() { m_data = 0; }
cs
m_data를 초기화하는 멤버함수를 추가하고, pObject->InitData()를 호출해보겠습니다.
그러하듯 컴파일 에러가 발생합니다. 
아무리 멤버함수내에서 값이 변경된다고 하여도, const 객체는 멤버변수의 값이 변경되는 일이 있어선 안됩니다.


포인터 타입의 변수에 const를 사용하게 되면 const의 위치에 따라 변경할수 없는 값이 달라집니다.

1
const Object* pObject = new Object(5);
cs
const 객체를 생성할 때 포인터 타입 앞에 const를 선언하게 됩니다.
이렇게 되면 pObject의 멤버변수의 데이터는 변경이 될 수 없게됩니다. 하지만 pObject는 Object타입으로
할당된 변수라면 어떤 변수도 담을 수 있습니다.

하지만, 아래와 같이 포인터 타잎 앞에 const를 붙이는 경우도 있습니다.
1
Object* const  pObject = new Object(5);
cs
이 경우는 포인터 타입과 변수 명 사이에 붙여주어 pObject가 가진 메모리값을 변경 할 수 
없다는 의미입니다.

1
2
3
    Object* const pObject = new Object(5);
    pObject = new Object(0);
cs
이처럼 초기화시 생성된 pObject변수에 다시 Object 객체를 생성하게되면 컴파일 에러가 납니다.
이미 pObject는 선언과 동시에 초기화된 메모리 주소값을 유지해야 하는데 새로운 객체 생성으로 인한
메모리 주소값이 변경이 이루어지다 보니 컴파일 에러가 발생하게됩니다.
그래서 이 같은 경우에는 선언과 동시에 메모리 할당이 이루어져야 합니다. 

1
const Object* const pObject = new Object(5);
cs
위 구문처럼 포인터 타입 앞과 뒤에 const가 두개 붙는 경우도 있습니다. 이럴때는 위에서 설명했던 두가지 경우가 모두 적용되는 특징을 가지는 변수가 되겠지요.

마지막으로, 포인터 타입이 아닌 데이터 타입에 const를 사용할 경우에는 선언과 동시에 초기화가 
이루어진다는 점이 있습니다.

1
const int data = 3;
cs
일반 자료형일 경우 선언과 동시에 초기화가 이루어져야 합니다.

1
2
3
4
5
// 디폴트 생성자 및 기본 생성자가 존재한 
const Object object;
// 인자를 받는 생성자가 있는 경우
const Object object(3);
cs
구조체나 클래스 타입의 경우 생성자에 따라 선언하는 방식이 달라지겠지요.