본문 바로가기

프로그래밍 언어들/C++

CHAPTER06 - friend와 static, const

1. const

C언어와 달리 C++에서는 const와 관련한 내용이 많다.


(1) const 객체와 const 객체의 특성들

변수를 상수화 하듯이, 다음과 같이 객체도 상수화할 수 있다.

const Simple s(10);


객체에 const 선언이 붙게 되면, 이 객체를 대상으로는 'const 멤버함수'만 호출이 가능하다.

즉, "이 객체의 데이터 변경을 허용하지 않겠다."라는 의미이다.

물론 const로 선언되지 않은 함수 중에도 데이터를 변경하지 않는 함수가 있지만, 가능성이 있는 함수는 모두 배제한다.


※ 멤버변수의 값을 조작하지 않는 함수는 가급적 const로 선언해서, const 객체에서도 호출이 가능하도록 할 필요가 있다.


(2) const와 함수 오버로딩(Function Overloading)

함수의 오버로딩이 성립하려면 매개변수의 수나 자료형이 달라야 한다. 하지만 다음과 같이 'const'의 선언 유무도

함수 오버로딩의 조건에 해당이 된다.

    void simplefunc() {}
    void simplefunc() const {}


다음은 'const 함수'가 언제 호출되는지 확인할 수 있는 예제이다.

class Simple
{
private:
    int num;
public:
    Simple( const int n) : num(n) {}
    void show(void)
    {
        cout << "called show() : " << num << endl;
    }
    void show (void) const
    {
        cout << "called show() const : " << num << endl;
    }
};

void func(const Simple &ob)
{
    ob.show();
}
int main( void)
{
    Simple s(10);
    const Simple cs(20);

    s.show();
    cs.show();

    func(s);
    func(cs);

    return 0;
}
called show() : 10
called show() const : 20
called show() const : 10
called show() const : 20


위의 예제를 관찰하면, 언제 const 함수가 호출되는지 알 수 있을 것이다.


2. 클래스와 함수에 대한 friend 선언

'friend'란 말 그대로 '친구'라는 뜻이다. 이것을 클래스와 함수에 선언하면 위와 비슷한 의미로 선언된다.


(1) 클래스의 friend 선언

A와 B 두 개의 클래스가 있다고 가정하면 friend 선언의 의미는 다음과 같다.

- A 클래스가 B 클래스를 대상으로 friend 선언을 하면, B는 A의 'private' 멤버에 직접 접근이 가능하다.

- 단, A도 B의 'private' 멤버에 접근하려면, B가 A를 대상으로 friend 선언을 해줘야 한다.

즉, friend 선언은 private 멤버의 접근을 허용하는 선언이다.


class A
{
private:
    int num;
    friend class B; // declare 'friend'
public:
    explicit A(int n) : num(n) {}
};

class B
{
public:
    void show( const A &ob)
    {
        cout << ob.num << endl;
    }
};

int main( void)
{
    A a(10);
    B b;

    b.show(a);

    return 0;
}


A 클래스에서 B클래스를 대상으로 'friend' 선언을 했기 때문에, B 클래스에서 A클래스의 'private' 멤버에

직접 접근이 가능하다.


(2) friend 선언의 시기
C++ 문법 중에 논란이 되었던 것 중 하나가 바로 'friend'이다. 이는 객체지향의 대명사 중 하나인 '정보은닉'을

무너뜨리는 문법이기 때문이다.

즉, "friend 선언이 지나치면 위험할 수 있으므로, 필요한 상황에 극히 소극적으로 사용하자."

문법적으로만 이해하고, 가급적 사용하지 않는 연습을 하는게 바람직하다.


(3) 함수의 friend 선언

 전역함수를 대상으로도, 클래스 멤버함수를 대상으로도 friend 선언이 가능하다.

class A
{
private:
    int num;
public:
    explicit A(int n) : num(n) {}
    friend void show(const A&);
};

void show(const A& ob)
{
    cout << ob.num << endl;
}

int main( void)
{
    A a(10);

    return 0;
}


멤버함수가 아닌 'show()'를 friend 선언 했기 때문에, 이 함수에서 멤버변수 'num'의 직접접근이 가능하다.


3. C++에서의 static

C언어에서의 static은 C++에서도 그대로 통용된다. 그러나 C++에서는 멤버변수와 멤버함수에 static을 추가할 수 있다.


(1) C언어에서 이야기한 static

- 전역변수에 선언된 static => 선언된 파일 내에서만 참조를 허용하겠다는 의미

- 지역변수에 선언된 static => 한번만 초기화되고, 지역변수와 달리 함수를 빠져나가도 소멸되지 않는다.

void counter(void)
{
    static int cnt;

    cnt ++;

    cout << cnt << endl;
}

int main( void)
{
    for(int i = 0 ; i < 10 ; i ++)
    {
        counter();
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
10


(2) 전역변수가 필요한 상황

C++에서의 static이 지니는 의미를 이야기하기에 앞서, 간단한 예제 하나를 소개하겠다.

이 예제는 객체가 생성될 때마다 "n번째 객체가 생성되었습니다."라는 메시지를 출력한다.

int Acnt    = 0;
int Bcnt    = 0;

class A
{
public:
    explicit A(void)
    {
        cout << ++Acnt << "번째 A 객체" << endl;
    }
};

class B
{
public:
    explicit B(void)
    {
        cout << ++Bcnt << "번째 B 객체" << endl;
    }
};

int main( void)
{
    A a;
    B b;

    A a2;
    B b2;

    return 0;
}
1번째 A 객체
1번째 B 객체
2번째 A 객체
2번째 B 객체


즉, 전역변수 'Acnt'와 'Bcnt'는 객체들이 공유하는 변수이다. 그런데 이 둘은 어디서든 접근이 가능하므로

문제를 일으킬 소지가 매우 높다.

그러나 위의 전역변수들을 각 클래스의 'static 멤버'로 선언하면 이러한 문제의 소지를 없앨 수 있다.

(3) static 멤버변수(클래스 변수)
static 멤버변수는 '클래스 변수'라고도 한다. 일반적인 멤버변수와 달리 클래스 당 하나씩만 생성되기 때문이다.

class A
{
private:
    static int Acnt;
public:
    explicit A(void)
    {
        cout << ++Acnt << "번째 A 객체" << endl;
    }
};

class B
{
private:
    static int Bcnt;
public:
    explicit B(void)
    {
        cout << ++Bcnt << "번째 B 객체" << endl;
    }
};
// initialize the static variables.
int A::Acnt = 0;
int B::Bcnt = 0;

int main( void)
{
    A a;
    B b;

    A a2;
    B b2;

    return 0;
}


위의 코드에 선언된 static 변수는 객체를 생성하건 하지 않건, 메모리 공간에 유일하게 하나만 할당이 된다.

해당 static 변수를 멤버로 갖는 생성되는 모든 클래스는 해당 멤버에 접근이 가능하다.

그렇다고, 위의 변수가 객체 내에 존재하는 것은 아니다. 단지 객체에게 접근할 수 있는 권한을 줬을 뿐이다.


static 변수는 생성자에서 초기화하면 안 된다.

- 객체가 생성될 때마다 초기화가 이루어진다.
따라서 위의 예제처럼 클래스 외부에 별도로 정의한다.


(4) static 멤버변수의 또 다른 접근방법

사실 static 멤버변수는 어디서든 접근이 가능한 변수이다. static 멤버가 'private'으로 선언되면,

해당 클래스의 객체들만 접근이 가능하지만, 'public'으로 선언이 되면, 클래스의 이름 또는 객체의 이름을 통해서

어디서든 접근이 가능하다.

class A
{
public:
    static int Acnt;
    explicit A(void)
    {
        Acnt ++;
    }
};

int A::Acnt = 0;

int main( void)
{
    cout << A::Acnt << "번째 A 객체" << endl;
    A a1;
    A a2;

    cout << A::Acnt << "번째 A 객체" << endl;
    cout << a1.Acnt << "번째 A 객체" << endl;
    cout << a2.Acnt << "번째 A 객체" << endl;

    return 0;
}
0번째 A 객체
2번째 A 객체
2번째 A 객체
2번째 A 객체


'a1.Acnt'와 'a2.Acnt'는 각 객체의 멤버변수에 접근하는 것과 같은 오해를 불러일으킨다.

그러므로 public static에 접근할 때는 첫 문장에서 보이듯이 클래스의 이름을 이용해서 접근하는 것이 좋다.


(5) static 멤버함수

static 멤버함수 역시 그 특성이 static 멤버변수와 동일하다. 따라서 위에서 설명한 다음 특성이 그대로 적용된다.

- 선언된 클래스의 모든 객체가 공유한다.

- public으로 선언이 되면, 클래스의 이름을 이용해서 호출이 가능하다.

- 객체의 멤버로 존재하는 것이 아니다.


여기서 중요한 것은, "객체의 멤버로 존재하는 것이 아니다."라는 점이다. 따라서 아래 코드는 컴파일 에러가 발생한다.

class A
{
private:
    int num;
public:
    static int Acnt;
    explicit A(void)
    {
        Acnt ++;
    }
    static void show(void)
    {
        cout << num << endl; // occur the compile errer
    }
};


즉, 논리적으로 생각해보면 다음과 같다.

- 객체의 멤버가 아닌데, 어떻게 멤버변수에 접근을 하겠는가?

- 객체생성 이전에도 호출이 가능하다. 그런데 어떻게 멤버변수에 접근이 가능하겠는가?

- 멤버변수에 접근을 한다고 가정하자. 그렇다면 어떤 객체의 멤버변수에 접근을 하겠는가?


이렇듯, 논리적으로 판단해도 static 멤버함수 내에서는 static으로 선언된 멤버의 호출만 가능하다.

이러한 static을 특성을 이용하면 대부분의 경우에 전역변수와 전역함수를 대체할 수 있다.


(6) const static 멤버

클래스 내에 선언된 const 멤버변수(상수)의 초기화는 이니셜라이저를 통해야만 가능하다.

그러나 'const static'으로 선언되는 멤버변수(상수)는 다음과 같이 선언과 동시에 초기화가 가능하다.

class A
{
public:
    const static int hi     = 10;
    const static int hello  = 20;
};

int main( void)
{
    return 0;
}


const static 멤버변수는, 클래스가 정의될 때 지정된 값이 유지되는 상수이기 때문에,

위 예제에서 보이는 바와 같이 초기화가 가능하다.


(7) 키워드 mutable
앞에서 'const'와 'explicit'에 대해서 공부하였다. 이 둘은 나름의 의미가 있으며, 매우 유용한 키워드이다.

그런데 이번에 설명하는 'mutable'이라는 키워드는 사용의 빈도수가 낮은, 아니 가급적 사용의 빈도수를 낮춰야 한다.

그 이유는 잠시 후에 설명하기로 하고, mutable의 의미부터 설명하겠다.

- const 함수 내에서 값의 변경을 예외적으로 허용한다.

class A
{
private:
    int num1;
    mutable int num2;
    void copytonum2(void) const
    {
        num2 = num1;
    }
};


위의 예제를 보면, mutable에 대해서 이해가 될 것이다.

'const'로 선언된 함수 내에서는 값의 변경이 불가능한데, 이는 mutable로 선언되었기에 가능한 일이다.
따라서, mutable의 과도한 사용은 C++에 있어서 그 중요성을 인정받은 const의 선언을 의미가 없게 만들어버린다.


출처 : 윤성우 열혈 C++ 프로그래밍