관리 메뉴

Kim's Programming

C++ - 캡슐화(3/3) 본문

Programming/Cplusplus

C++ - 캡슐화(3/3)

Programmer. 2015. 9. 19. 23:04

상수멤버


상수멤버


상수 멤버는 한 번 값이 정해지면 변경될 수 없는 멤버입니다. 클래스 전체에서 참조하는 중요한 산수가 있다면 이를 상수 멤버로 정의하여 클래스에 포함시킬 수 있습니다. 예를 들어 수학 계산을 하는 클래스에서 원주율을 자주 사용한다면 다음과 같이 상수 멤버를 정의합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;
class Mathcalc
{
private:
    const double pie;
 
public:
    Mathcalc(double apie) :pie(apie){}
    ~Mathcalc(){}
    void DoCalculate(double r)
    {
        printf("반지름 %.2f인 원의 둘래 = %.2f\n", r, r * * pie);
    }
};
void main()
{
    Mathcalc M(3.1416);
    M.DoCalculate(4);
}
cs

원주율을 정의하는 값을 pie라는 상수 멤버로 포함시켰습니다. 3.1416이라는 값을 바로 쓰지 않고 상수 멤버를 사용할 때의 장점은 매크로 상수의 경우의 마찬가지로 값이 의미 파악이 쉽고 수정하기 쉽다는 점입니다. 상수는 대입을 받을 수 없기 때문에 반드시 생성자의 초기화 리스트에서 초기화해야 하는데 이는 앞에서 이미 알본 내용입니다. 상수 멤버가 모든 객체에 대해 항상 같은 값을 가진다면 객체를 생성할 때마다 매번 초기화 할 필요없이 정적 멤버로 선언한 후 딱 한 번만 초기화 할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;
class Mathcalc
{
private:
    static const double pie;
 
public:
    Mathcalc(){}
    ~Mathcalc(){}
    void DoCalculate(double r)
    {
        printf("반지름 %.2f인 원의 둘래 = %.2f\n", r, r * * pie);
    }
};
const double Mathcalc::pie = 3.1416;
void main()
{
    Mathcalc M;
    M.DoCalculate(6);
}
cs

pie 멤버 선언문 앞에 static을 붙이면 이 멤버는 클래스내의 모든 멤버가 공유하는 정적 멤버가 됩니다. 정적 멤버는 클래스 외부에서 다시 한 번 더 정의해야 하며 이때 초기값을 주는데 일반 정적 멤버와는 달리 상수 멤버는 선언할 때 초기값을 반드시 지정해야 합니다. pie는 정적이면서도 상수라는 성질이 있어 정의할 때 초기화하지 않으면 초기화할 기회가 없습니다. 초기식이 외부 정의로 이동되었으므로 생성자는 더 이상 이 멤버를 초기화하지 않아도 됩니다. 단 이렇게 정적 상수 멤버로 선언하면 클래스 전체를 통틀어 pie가 하나만 존재하므로 각각의 MathCalc객체는 모두 같은 상수를 공유하며 객체별로 다른 값을 가질 수 없습니다. 정적 상수가 아닐 때는 다음과 같이 객체별로 필요한 정밀도에 따라 다른 원주율 값을 가질 수도 있습니다. 

1
2
3
MathCalc M1(3.14);
MathCalc M1(3.1416);
MathCalc M1(3.14159265358979);
cs


상수 멤버 함수


상수 멤버 함수는 멤버 값을 변경할 수 없는 함수입니다. 멤버값을 단순히 읽기만 한다면 이 함수는 객체의 상태를 바꾸지 않는다는 의미로 상수 멤버 함수로 지정하는 것이 좋습니다. 클래스 선언문의 함수 원형 뒤쪽에 const 키워드를 붙이면 상수 멤버 함수가 됩니다. 함수의 앞쪽에서는 리턴값의 타입을 지정하기 떄문에 const를 함수 뒤에 붙이는 좀 별난 표기법을 사용합니다.

1
2
3
4
5
6
7
8
class Myclass
{
private:
     int value;
public:
    int Setvalue(int aValue);
    int GetValue() const;
};
cs

정수형의 Value 변수가 비공개 영역에 선언되어 있고 이 멤버값을 읽고 쓰는 Get/Set 엑세스 함수들은 공개 영역에 선언되어 있습니다. Value를 외부에서 변경하고 싶다면 SetValue 함수를 호출하고 Value를 읽고 싶을 때는 GetValue 함수를 호출합니다. 이때 GetValue는 객체의 어떠한 멤버값도 변경하지 않으므로 상수 멤버 함수이며 이 함수 원형 뒤에 const를 붙여 GetValue는 값을 읽기만 한다는 것을 컴파일러에게 확실하게 알려 줍니다. 상수로 선언된 객체에 대해서는 상수 멤버 함수만 호출할 수 있으며 비상수 멤버 함수는 호출할 수 없습니다. 왜냐하면 상수 객체는 읽기 전용이므로 어떤 멤버의 값도 변경되어서는 안되기 때문입니다. 다음 소스를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<iostream>
#include<Windows.h>
#include<conio.h>
using namespace std;
void gotoxy(int x, int y);
class Position
{
private:
    int x, y;
    char ch;
 
public:
    Position(int ax, int ay, char ach)
    {
        x = ax;
        y = ay;
        ch = ach;
    }
    void OutPosition() const
    {
        gotoxy(x, y);
        _putch(ch);
    }
    void MoveTo(int ax, int ay)
    {
        x = ax;
        y = ay;
    }
};
void main()
{
    Position Posit(12'A');
    Posit.MoveTo(2020);
    Posit.OutPosition();
 
    const Position There(34'B');
    //There.MoveTo(40,10) //에러
    There.OutPosition();
}
void gotoxy(int x, int y)
{
    COORD Pos = { x, y };
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), Pos);
}
cs

문자를 출력하는 OutPosition 함수는 값을 읽기만 하므로 const로 선언되어 있고 MoveTo 함수는 위치를 옮기기 위해 x,y 멤버의 값을 변경하므로 const가 아닙니다. 만약 MoveTo를 const로 지정하면 상수를 변경할 수 없다는 에러로 처리됩니다. 객체의 값을 조금이라도 변경하는 함수는 상수 멤버 함수로 지정하지 말아야 합니다. const로 선언된 OutPosition에 x++따위의 코드를 작성하면 상수 멤버 함수가 객체의 상태를 변경하려고 했으므로 역시 에러로 처리될 것입니다. 단, 상수 멤버 함수라도 정적 멤버 변수의 값은 변경할 수 있는데 정적 멤버는 객체의 소속이 아니며 객체의 상태를 나타내지도 않기 때문입니다.


main의 테스트 코드를 보겠습니다. Here는 비상수 객체로 선언되었으므로 OutPosition으로 문자를 출력함은 물론 moveTo로 위치를 옮길 수도 있습니다. 그러나 There은 상수 객체로 선언되었으므로 상수 멤버 함수인 OutPosition만 호출할 수 있으며 MoveTo호출문은 에러로 처리됩니다. 이 문장이 에러로 처리되는 이유는 다음 문장이 에러로 처리되는 이유가 동일합니다.

 

상수에 어떤 값을 대입하여 변경할 수 없는 것과 마찬가지로 상수 객체의 상태를 변경하는 함수를 호출하는 것도 불가능합니다. 비 상수 멤버가 받는 객체 포인터 this는 Position * const 형이며 this 자체는 상수이지만 this가 가르키는 대상은 상수이고 this가 가리키는 대상도 상수입니다. 결국 상수 멤버 함수의 제일 끝에 붙는 const는 이 함수로 전달되는 숨겨진 인수 this의 상수성을 지정합니다. 컴파일러는 멤버 함수의 코드를 보고 멤버값을 변경하는지 아닌지 정확하게 판단할 수 없습니다. 멤버의 값을 변경하는 방법에는 직접적인 대입만 있는 것이 아니라 포인터를 통한 간접 변경, 함수 호출을 통한 병경 등 여러 가지 변칙적인 방법들이 많기 때문에 함수의 내용만으로 상수성을 정확하게 지정해야합니다. 만약 OutPosition의 원형에 const를 빼 버리면 There.OutPosition() 호출 조차도 에러로 처리됩니다. 왜냐하면 컴파일러는 OutPosition 함수가 멤버값을 변경할 수도 있다고 생각하기 때문입니다.어떤 멤버가 함수 값을 읽기만 하고 바꾸지 않는다면 const를 붙이는 것이 원칙이면 이 원칙대로 클래스를 작성해야합니다.


mutable


mutable은 C++에서 새로 추가된 키워드인데 영어 뜻 그래도 번역해서 변덕스럽다는 뜻입니다. 상수의 반대 의미로 사용되며 "수정 가능"정도로 이해하면 됩니다. mutable로 지정된 멤버는 상수 함수나 상수 객체에 대해서도 값을 변경할 수 있습니다. 객체의 상태를 표현하는 중요한 멤버가 아닐떄 이 속성을 사용합니다. 잘 쓰이지 않지만 간단한 소스로 표현했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using namespace std;
 
class Myclass
{
private:
    mutable int v;
public:
    Myclass(){}
    void function() const { v = 0; }
};
 
void main()
{
    Myclass M;
    M.function();
    const Myclass M1;
    M1.function();
}
cs

func 함수는 상수 멤버 함수로 선언되었지만 멤버 변수 v의 값을 변경할 수 있습니다. v가 상수 멤버 함수에서도 값을 변경할 수 있는 mutable로 선언되었기 때문입니다. 만약 mutable을 빼버리면 상수 함수에서는 멤버값을 변경할 수 없다는 에러로 처리됩니다.  M1은 상수 객체로 선언되었지만 마찬가지로 v를 변경할 수 있습니다. mutable은 상수 멤버 함수나 상수 객체의 상수성을 완전히 무시해 버립니다. 변수는 본질적으로 값을 마음대로 바꿀 수 있지만 const에 의해 값 변경이 금지됩니다. mutable은 이런 const의 값 변경 금지기능을 금지하여 값 변경을 다시 허용하는 복잡한 지정을 합니다. 도대체 이런 지정이 왜 필요할까요? 객체에 상수성을 주는 이유는 객체의 상태가 우발적으로 변경되는 것을 금지하여 안정성을 높이자는 취지입니다. 그런데 떄로는 객체의 멤버이면서도 객체의 상태에 포함되지 않는 멤버가 존재하기도 하는데 예를 들어 값 교환을 위한 임시 변수가 이에 해당합니다. 또는 i,j같은 통상적인 루프 제어 변수도 객체의 상태라고 볼 수 없으며 디버깅을 위해 임시적으로 추가된 멤버로 mutable이어야 합니다. 예를들어 객체의 상태를 출력해 보기 위한 문자열 버퍼를 멤버로 잠시 선언했다면 이 버퍼는 객체의 주요 멤버 변수에 포함되지 않습니다. 다음 소스를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<conio.h>
#include<Windows.h>
using namespace std;
void gotoxy(int x, int y);
class Position
{
private:
    int x, y;
    char ch;
    mutable char info[256];
public:
    Position(int ax, int ay, char ach)
    {
        x = ax; y = ay; ch = ach;
    }
    void OutPosition() const { gotoxy(x, y); _putch(ch); }
    void MoveTo(int ax, int ay){ x = ax; y = ay; }
    void MakeInfo() const{ sprintf(info, "x = %d , y = %d, ch = %c", x, y, ch); }
    void OutInfo() const { puts(info); }
};
void main()
{
    const Position Posit(1122'Z');
    Posit.MakeInfo();
    Posit.OutInfo();
}
void gotoxy(int x, int y)
{
    COORD Pos = { x, y };
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), Pos);
}
cs

객체의 현재 상태를 문자열로 출력하기 위해 info라는 문자열 버퍼를 멤버 변수로 선언했으며 이 버퍼에 상태를 조립하는 MakeInfo와 OutInfo 함수를 선언했습니다. Position 클래스는 워낙 간단해서 상태를 조사하는 것이 아주 쉽지만 복잡한 클래스는 상태가 수시로 변하며 특정 시점의 상태를 즉시 조사하기 힘든 경우도 있어 미리 조사해 두어야합니다. 이때 info는 객체 자체의 상태가 아니라 속도 향상을 위한 임시적인 캐시 정보일 뿐이며 원한다면 언제든지 다시 조사할 수 있습니다. 객체의 속성이 아닌 멤버에 대해 예외적으로 아무 값을 변경할 수 있도록 하는 장치가 바로 mutable입니다. 위 소스에서 info가 mutable이 아니라면 MakeInfo는 상수 멤버 함수가 될 수 없으며 상수 객체에 대해서는 정보를 조사하거나 출력하는 것이 불가능해질 것입니다.


'Programming > Cplusplus' 카테고리의 다른 글

C++ - 연산자 오버로딩(2/3)  (0) 2015.09.20
C++ - 연산자 오버로딩(1/3)  (1) 2015.09.20
C++ - 캡슐화(2/3)  (0) 2015.09.19
C++ - 캡슐화(1/3)  (0) 2015.09.19
C++ - 클래스 생성자/파괴자(3/3)  (0) 2015.09.03