소년코딩

상수 클래스 객체와 멤버 함수 (Const class object and member function)

이전 포스트 상수 (const, constexpr, and symbolic constants) 에서 const 키워드를 통해 상수를 만들 수 있고, 모든 상수 변수는 생성할 때 초기화해야 한다는 것을 배웠다. 기본 상수 자료형이면 복사, 직접 또는 유니폼 초기화를 통해 초기화를 수행할 수 있다.

const int value1 = 5;      // 복사 초기화
const int value2(7);       // 직접 초기화
const int value3 { 9 };    // 유니폼 초기화(C++ 11)

1. 상수 클래스 (Const class)

마찬가지로 클래스 객체도 const 키워드를 사용해서 상수 객체로 만들 수 있다.

const Date date1;                   // 기본 생성자를 이용한 초기화
const Date Date2(2020, 8, 7);       // 인수가 있는 생성자를 이용한 초기화
const Date Date3 {2020, 8, 7};      // 인수가 있는 생성자를 이용한 초기화(C++ 11)

상수 객체가 생성자를 통해 초기화되면 객체의 멤버 변수의 값을 직접 변경하거나 멤버 변수의 값을 설정하는 멤버 함수를 호출하는 작업 등 멤버 변수를 수정하려는 어떠한 시도도 허용되지 않는다.

class Something
{
public:
    int m_Value;

    Somting() : m_Value(0) { }

    void SetValue(int value) { m_Value = value; }
    int GetValue() { return m_Value; }
};

int main()
{
    const Something something{}; // 기본 생성자 호출

    something.m_Value = 5;      // 컴파일 에러: const 위반
    something.SetValue(5);      // 컴파일 에러: const 위반

    return 0;
}

위 코드에서 상수 객체의 멤버 변수값을 바꾸는 행위는 모두 const 위반을 하므로 컴파일 에러가 발생한다.


2. 상수 멤버 함수 (Const member function)

아래 코드를 봐보자:

std::cout << something.GetValue();  // 컴파일 에러: const 위반

놀랍게도 GetValue() 멤버 함수는 멤버 변수의 값을 변경하는 어떠한 작업도 하지 않지만, 컴파일러 에러를 일으킨다. 상수 클래스 객체는 상수 멤버 함수만 명시적으로 호출할 수 있다.

상수 멤버 함수(const member function)는 호출한 객체의 멤버 변수를 변경할 수 없는 멤버 함수를 말한다. (멤버 함수의 선언 마지막에 const 키워드를 추가함으로써 사용한다.)

GetValue()를 상수 멤버 함수로 만들려면 const 키워드를 함수 선언의 매개 변수 목록 뒤에 추가하기만 하면 된다.

class Something
{
public:
    int m_Value;

    Somting() : m_Value(0) { }

    void SetValue(int value) { m_Value = value; }
    int GetValue() const { return m_Value; } // 함수 선언에서 매개 변수 목록 뒤에 const 키워드를 추가한다.
}

이제 GetValue()는 상수 멤버 함수인데 이것은 아래와 같은 특징을 가지고 있다.

  1. 상수 멤버 함수에서 멤버 변수 값을 바꾸는 행위를 할 수 없다.
  2. 상수 멤버 함수 안에서 비-상수 멤버 함수를 호출할 수 없다.
  3. 상수 클래스 객체에 대해서도 호출할 수 있다.

클래스 선언 외부에서 구현된 멤버 함수의 const 키워드는 클래스 선언과 정의 모두에서 포함되어야 한다:

class Something
{
public:
    int m_Value;

    Somting() : m_Value(0) { }

    void SetValue(int value) { m_Value = value; }
    int GetValue() const; // 함수 선언에서 매개 변수 목록 뒤에 const 키워드를 추가한다.
}

int Something::GetValue() const // 정의에도 추가해야 한다.
{
    return m_Value;
}

또한, 위에서 잠깐 설명한 특징에서 말한 것 처럼 상수 멤버 함수에서 멤버 변수의 값을 변경하거나 비-상수 멤버 함수를 호출하면 컴파일러는 오류를 발생시킨다:

int Something::GetValue() const // 정의에도 추가해야 한다.
{
    m_Value = 3; // 컴파일 에러: 상수 멤버 함수는 멤버 변수의 값을 바꿀 수 없다.
    SetValue(3); // 컴파일 에러: 상수 멤버 함수는 비-상수 멤버 함수를 호출할 수 없다.

    return m_Value;
}

마지막으로 C++ 클래스의 생성자는 멤버 변수를 초기화할 수 있어야 하므로 const 키워드를 사용을 허용하지 않는다.


3. 상수 참조(레퍼런스) (Const reference)

함수의 인자로 객체를 전달할 때 상수 참조(레퍼런스)를 이용하는 것이 일반적인 방법이다.

이전 포스트 참조로 전달(Pass by reference)에서 참조로 전달의 장점을 다루었다. 요약하자면, 값으로 전달의 경우 복사본이 만들어지게 되는데, 복사본이 필요하지 않은 경우가 대부분이므로 참조로 전달이 불필요한 복사를 하지 않아 효과적이라는 것이다. 또한, 프로그래머 무심코 함수 안에서 전달된 인수를 변경하는 것을 막기 위해 일반적으로 상수로 참조를 만든다는 것이다.

아래 코드에서 문제점을 찾아보자:

#include <iostream>
using namespace std;

class Date
{
private:
    int m_Year;
    int m_Month;
    int m_Day;

public:
    Date(int year, int month, int date)
    {
        SetDate(year, month, date);
    }

    void SetDate(int year, int month, int date)
    {
        m_Year  = year;
        m_Month = month;
        m_Day   = day;
    }

    int GetYear() { return m_Year; }
    int GetMonth() { return m_Month; }
    int GetDay() { returm m_Day; }
};

// date 객체를 복사하지 않기 위해 const 참조를 통해 date 객체를 전달하고 있다.
void PrintDate(const Date& date)
{
    cout << date.GetYear() << '/' << date.GetMonth() << '/' << date.GetDay() << endl;
}

int main()
{
    Date date{2020, 8, 7};

    PrintDate(date);

    return 0;
}

정답은 PrintDate() 함수 안에서 date 객체는 상수 객체로 취급된다는 것이다. 그리고 상수 date 객체와 함께 GetYear(), GetMonth()GetDay() 멤버 함수를 호출하는데, 이 멤버 함수는 모두 상수 멤버 함수가 아니다. 상수 객체에 대해 비-상수 멤버 함수를 호출할 수 없으므로 컴파일 오류가 발생할 것이다.

해결법은 간단하다. 상수 멤버 함수로 만든다:

class Date
{
    // 생략..


    // getter를 모두 상수 멤버 함수로 만든다.
    int GetYear() const { return m_Year; }
    int GetMonth() const { return m_Month; }
    int GetDay() const { returm m_Day; }
};

이제 PrintData() 함수 안에서 상수 date 객체의 GetYear(), GetMonth()GetDay()를 성공적으로 호출할 수 있다.


4. 상수 멤버 함수와 비-상수 멤버 함수의 오버로딩 (Overloading const and non-const function)

마지막으로, 같은 멤버 함수에 대해 상수 버전과 비-상수 버전으로 오버로딩이 가능하다.

#include <string>
using namespace std;

class Something
{
private:
    string m_Value;

public:
    Something(const string& value = ""): m_Value{value} {}

    string& GetValue() const { return m_Value; }  // 상수 버전
    string& GetValue() { return m_Value; }        // 비-상수 버전
};

멤버 함수의 상수 버전은 상수 객체에서 호출되며, 비-상수 버전은 비-상수 객체에서 호출된다.

int main()
{
    Something something{};
    something.GetValue() = "Hi"; // 비-상수 버전 호출

    const Something something2{};
    something2.GetValue();       // 상수 버전 호출

    return 0;
}

이와 같은 오버로딩은 반환 값에 대해서 상수/비-상수 차이가 필요할 때 사용한다. 위의 코드에서 GetValue()의 비-상수 버전은 비-상수 객체에서 m_Value 문자열의 값을 읽고 쓸 수 있게 하고, 상수 버전은 상수 객체에서 데이터를 수정할 수 없도록 강제하므로 유연하게 작동한다.

상수 참조로 전달하는 경우가 매우 많고 일반적이므로 const 키워드에 친숙해져야 한다. 즉, 객체의 상태를 수정하지 않는 멤버 함수를 만드는 것이다.


cpp 번역: 이 포스트의 원문은 https://www.learncpp.com/cpp-tutorial/810-const-class-objects-and-member-functions/ 입니다.

댓글 로드 중…

블로그 정보

소년코딩 - 소년코딩

소년코딩, 자바스크립트, C++, 물리, 게임 코딩 이야기

최근에 게시된 이야기