프로그래밍 지식/C++

C++문법/ Static member 정적 멤버[this, 정적 멤버 변수, 정적 멤버 함수]

게임이 더 좋아 2021. 12. 8. 19:13
반응형
728x170

22.02.02 내용 업데이트

 

1. this

 

C++ 에서 객체의 고유한 상태를 저장하는 멤버 변수는 객체별로 따로 유지하고

객체의 동작을 정의하는 멤버 함수는 공유한다.

-> Static의 특징 객체의 개수와 관계없이 유일하게 존재함.

 

속성은 객체마다 다르지만 동작은 공통적이어서 각 객체가 따로 가질 필요 없다.

-> this는 다시 말해서 해당 멤버 함수를 불러낸 객체라고 할 수 있다.

호출의 주체, 함수를 실행시키는 객체 자체

 

더 설명해보자

 

// this
#include <stdio.h>

class Simple
{
private:
    int value;
    
public:
    Simple(int avalue) : value(avalue) {} // 생성자
    void OutValue(){
        printf("value= %d\n", value);
    }
};

 

main 함수는 아래 같이 한다.

int main()
{
    Simple A(1), B(2);
    A.OutValue();
    B.OutValue();
}

 

즉, 멤버변수는  객체별로 1,2 로 value를 각각 유지하고

멤버함수는 객체의 value를 이용해서 출력을 한다.

결과는 아래처럼 나온다.

 

1
2

 

위의 OutValue 함수 자체는 별도의 인수를 받지 않고 value 값만 출력할 뿐이다.

 

인수가 없는데 어떻게 다르게 결과가 나오는 것이지?????

 

사실 C++ 컴파일러는 호출 객체를 암시적인 인수로 전달한다.

즉, A.OutValue() 는 컴파일될 때 OutValue(&A)로 컴파일 된다.

때문에 호출한 객체를 전달하게 된다.

 

이 때, 암시적으로 전달하는 객체 포인터를 this라고 말하는데

this를 자기 자신으로 고정된 포인터 상수다.

this 인수는 함수를 호출한 객체의 포인터이며

멤버를 참조하는 모든 문장 앞에 this -> 가 암시적으로 적용된다.

 

A.OutValue() 호출문에서 this는 &A이며

따라서 this -> value는 A의 멤버 변수 value 이다.

 

마찬가지로 B.OutValue 호출문에서 참조하는 value는 B의 멤버이다.

호출 객체에 따라 함수가 참조하는 실제 value 값이 달라진다.

 

사실 OutValue의 내부적인 코드를 보자면

void OutValue(Simple *const this){
    printf("value=%d\n", this->value);
}

 

위와 같이 동작하는 호출 규약을 thiscall 이라고 한다.

모든 멤버 함수에 강제로 적용된다.

 

모든 것이 자동화되어 있어 this 존재를 크게 신경쓸 필요가 없지만

함수 내부에서 객체 자신을 칭할 때는 this가 필요하다.

 

아래 함수는 인수로 전달된 other와 자기 자신을 비교하여 큰 객체를 리턴한다.

Simple *FindBig(Simple *other){
    if(other->value > value){
        return order;
    } else {
        return this;
    }
}

 

other의 value와 자기 자신의 value를 비교해서 other가 더 크면 other를 리턴하고

그렇지 않으면 자기 자신을 리턴한다.

이 때 필요한 키워드는 this이다. 

멤버 함수에서 this는 나 자신을 의미하며 여러 가지 용도로 사용된다.

-> 해당 함수를 실행한 객체를 의미한다.

 

프로그램 종료나 치명적인 에러로 동적 할당된 객체가 자신을 삭제할 때

delete this; 

문장을 사용한다.

 

이 문장의 의미는 '나 좀 죽여줘'인데 '나'라는 1인칭을 칭하기 위해 this 키워드가 필요하다.

또 멤버 변수와 지역 변수의 이름이 충돌할 때도 멤버임을 분명히 하기 위해 this -> value 식으로 쓴다.

 


 

2.  정적 멤버 변수

정적 멤버 변수는 클래스 바깥에서 선언될 수도 있고 안에서 선언될 수도 있다.

-> 밖에서나 안에서나 선언될 수 있다는 것을 명심하자.

클래스에 소속되며 객체별로 할당되지 않고 모든 객체가 공유한다.

-> 모든 인스턴스가 정적 멤버 변수를 공유한다.

이것이 가장 큰 특징이다.**

 

하나의 예를 들어보자

여기서는 count가 전역 변수로 선언되어 메인 함수 시작부터 끝까지 살아있다.

count 전역 변수는 Simple 타입의 객체가 몇 개나 생성되었는지 관리한다.

#include <stdio.h>

int count = 0;

class Simple
{
private:
    int value;

public:
    Simple(){count++;}
    ~Simeple(){count--};
    void OutCount(){
        printf("현재 객체 개수 = %d\n", count);
    }
};

 

main 함수는 아래와 같다.

int main(){
    Simple s, *ps;
    s.OutCount();
    ps = new Simple;
    ps->OutCount();
    delete ps;
    s.OutCount();
    printf("크기 = %d\n", sizeof(s));
}

 

결과는 이렇게 나온다.

1
2
1
4

 

count 전역 변수를 0으로 초기화하고

Simple의 생성자, 소멸자에서 count의 증감을 관리한다.

 

하지만 전역변수 자체는 많을수록 좋지 않다.

클래스와 관련된 정보 자체가 외부에 선언되어 있어 캡슐화를 위반하는 행위이며

전역 변수 의존적인 함수이기 때문에 재사용성이 떨어지고

전역 변수는 hide가 되지 않아서 노출되어 있어서 위험하다.

 

그렇다면 어떻게 클래스에서도 전역변수와 같은 역할을 하는 변수를 만들까??

 

즉, 구조적으로 전역 변수는 객체지향과 맞지 않는 것이다.

그래서 관련 정보를 클래스 안에 캡슐화해보기로 하였다.

class Simple
{
private:
    int value;
    int count = 0;
    
public:
    Simple(){count++;}
    ~Simple(){count--;}
    void OutCount(){
        printf("현재 객체 개수 = %d\n", count);
    }
};

 

그렇게 선언 후 main을 똑같이 실행하면??

결과는

1
1
1
8

 

count를 Simple 클래스 안으로 포함시키고 0으로 초기화했는데..

객체마다 count 멤버 변수가 생겨났기 때문에 count가 전체 객체의 개수를 셀 수는 없다.

 

즉, 이를 해결하려면 클래스의 멤버이면서도

클래스의 모든 객체가 공유할 수 있도록 만들어야 한다.

이것이 바로 정적 멤버 변수가 필요한 이유다.**

 

정적 멤버 변수를 만들어보자

class Simple
{
private:
    int value;
    static int count;
    
public:
    Simple(){count++;}
    ~Simple(){count--;}
    void OutCome(){
        printf("현재 객체 개수 = %d\n", count);
    }
};

int Simple::count = 0;

 

count 앞에 static 키워드를 붙여서 정적 멤버 변수임을 선언하고

Simple class 전체에서 정적으로 선언된 것이다.

 

**정적 멤버 변수는 클래스 외부에서 :: 연산자로 초기화할 수 있다.

-> 즉, 정적 멤버에 접근하기 위해선 범위 연산자인 ( :: )을 이용해서 접근하라는 말이다.

int Simple::count = 0;

 

위에서는 단일 소스파일이라서 클래스 선언문 바로 아래서 초기화하는 것이다.

만일 해당 클래스를 모듈로 작성한다면 헤더 파일에는 선언만 들어가있으므로

구현 파일에서 정적 멤버에 대한 정의를 하면 된다.

즉, 헤더 파일에서는 정적 멤버 변수를 초기화 하지 않는다.

 

다시 말해서 클래스 안에서 선언한 정적 멤버 변수는

해당 클래스의 모든 Instance가 공유한다.

 

 


 

3. 정적 멤버 함수

 

정적 멤버 함수는 객체가 아닌 클래스와 연관되어 모든 객체에 대해 공통적인 작업을 처리하는 것이다.

-> 정적 멤버 변수와 관련된 작업을 한다.

이 역시 선언할 때 함수 원형 앞에 static 키워드를 붙이며 외부에서 작성할 때는 static을 생략한다.

 

예를 들어보자

class Simple
{
private:
    int value;
    static int count;
    
public:
    Simple(){count++;}
    ~Simple(){count--;}
    static void InitCount(){
        count = 0;
    }
    static void OutCome(){
        printf("현재 객체 개수 = %d\n", count);
    }
};

 

메인 함수는?

int Simple::count;

int main(){
    Simple::InitCount();
    Simple::OutCount();
    Simple s, *ps;
    Simple::OutCount();
    ps = new Simple;
    Simple::OutCount();
    delete ps;
    Simple::OutCount();
    printf("크기 = %d\n", sizeof(s));
}

 

결과는??

0
1
2
1
4

이런식으로 나온다.

 

Siple의 정적 멤버 변수를 선언만 하고초기화는 InitCount()가 해줬다.

출력도 OutCount()가 해준다.

count 같은 경우는 해당 클래스 전체와 연관되는 정보이고

그 변수를 다루는 함수도 클래스 전체에 대해서 선언하는 것이 옳기에 함수를 정적으로 선언했다.

 

위의 두 함수 모두 객체가 생성되지 않아도 호출할 수 있다.

객체를 생성하지 않았는데도 출력이 되는 것을 보면 알 수 있다.

 

다시 말하자면 클래스 안에서 정적으로 멤버함수로 선언되지만

객체가 없더라도 동작을 한다.

다시 말해서 정적 멤버 함수는 객체에 의해 호출되는 것이 아니고 따로 호출 객체가 없다.

그래서 정적 멤버 함수는 정적 멤버만 참조할 수 있고 일반적으로 선언된 멤버에는 접근이 불가능하다.

예를 들면 위에서 OutCount에서 value에 접근하려고 한다면 에러를 쓰로잉한다.

-> 정적 멤버 함수는 정적 멤버 변수에 대해서만 작업 하는 것이 좋다.

 


 

결국 정적 멤버, static으로 선언한 멤버들은 어떤식으로 활용되느냐??

 

솔직히 정적 멤버는 선언과 정의가 분리되어 있기도 하고

클래스 외부에서 정의하는 것을 보면 객체 지향 원칙에 위배된다.

하지만 클래스에 관련된 정보만을 처리하며 동작을 한다는 점에서는 클래스 안에 들어가있는 것 같다.

 

그래서 우리는 이러한 정적 멤버들을 필요한 곳에만 써야한다.

예를 들어

1) 전역 자원에 대해서 초기화를 할 때나

2)읽기 전용인 자원에 대해 초기화를 할 때

그리고

3)모든 객체가 공유하는 정보일 때다. -> 이 개념을 기초로 한다.

 

일반적으로 DB 연결이나 네트워크 접속같이 처음에 딱 한 번만 수행하는 초기화는

정적 멤버 함수에서 처리하고 그 결과를 정적 멤버 변수에 저장한다.

전역 초기화는 객체별로 할 필요 없이 클래스 수준에서 딱 한 번만 초기화를 해서

그 결과를 모든 객체가 공유하는 것이 더 효율적이다.

 

객체는 종종 자신이 동작하는 외부 환경에 대한 정보를 요구한다.

정확한 출력을 위해서 화면 크기를 알아야 한다거나 하드웨어 구성정보 같이 정보를 요구한다.

configuration을 본 적이 있다면 비슷하다고 생각하면 된다.

이런 정보는 실행 중에 절대 바뀔 일이 없다. -> 모든 객체가 동일한 상태

즉, 객체 별로 다를 수도 없다는 것이다.

그래서 클래스 수준에서 한 번만 꺼내오면 된다.

이때 정적 멤버 함수와 변수를 쓰는 것이다.

 

마지막으로 모든 객체가 공유하는 정보는 객체별로 만드는 것보다 하나로 정적으로 선언하는 것이 좋다.

싱글톤도 비슷한 느낌이다.

은행원들의 클래스가 있다고 해보자

이자율이 모두 다르게 적용될 수 있는가? 절대 아니다.

해당 은행의 이자율은 정해져있으니 은행원 클래스를 만들더라도 모두 이자율은 같을 것이다.

이와 같이 중요한 값이 있다면 개별 객체로 가지는 것보다는 정적으로 선언하는 것이 더 효율적이라는 말이다.

 


 

 

**만약 클래스 밖에서 정적멤버를 정의한다면 (::) 범위 연산자를 써서 정의할 수 있다.

static 키워드는 클래스 안에서 정적 멤버를 정의할 때만 쓴다. -> 밖에선 쓰지 않는다.

 

**static 멤버들은 객체에 구애받지 않으므로 생성할 때 초기화하지 않는 것이 원칙이다.

 

 

 

 

반응형
그리드형