프로그래밍 공부
작성일
2024. 2. 5. 17:35
작성자
WDmil
728x90

8.2 여러 종류의 데이터 멤버

C++는 여러 종류의 데이터 멤버를 지원한다. 단순한 데이터 멤버 외에도 특정 클래스의 모든 객체 간에 공유되는 static 데이터 멤버도 있고,const 멤버, 참조 멤버, const 참조 멤버 등 다양하게 존제한다.


8.2.1 static 데이터 멤버

어떤 경우에는 클래스의 객체별로 변수를 따로따로 가지는 것이 너무 중복되거나 의도에 맞지 않을 수 있다. 다시말해, 어떠 ㄴ데이터 멤버가 특정 클래스에 종속되기는 하지만 객체별로 따로 복제본을 가지는 것이 불합리할 수 있다.

 

예를 들어 각 스프레드 시트마다 순번을 매기려 한다고 하자, 순번을 중복되지 않게 0부터 순차적으로 부여하려면 최종 순번으로 몇 번이 부여되었는지 관리해야 한다. 이 값은 분명 SpreadSheet클래스에 종속적이지만 SpreadSHeet의 개별 객체가 모두 최종 순번을 가지고 있는 것은 불필요한 중복일 뿐만 아니라 SpreadSheet 객체가 새롭게 추가되거나 삭제되어 최종 순번이 바뀔 때 모든 객체의 멤버 변수를 동기화해야 하므로 매우 비효울적이다.

 

이러한 경우를 위해 C++에서는 static 데이터 멤버를 지원한다. static데이터 멤버는 C에서의 전역 변수와 유사하나 특정 클래스에 종속된다는 점이 다르다.

class SpreadSheet
{
	// 코드생략
    private:
    	static int sCounter;
};

static 데이터 멤버는 클래스 안에 정의하는 것으로 끝나지 않고 별도의 소스 파일 안에서 다시 한번 선언해야 하고 초기화도 이때 해야 한다. 단, static멤버 변수는 일반 변수와는 달리 자동으로 0이나 nullptr(포인터의 경우) 로 초기화되기 때문에 0값으로 초기화가 필요한 경우 생략해도 된다.

int SpreadSheet::sCounter;

위 코드는 전역 변수 선언처럼 함수든 매서드든 어떤 블록에도 속하지 않는다, 전역 변수와 다른 점은 자신이 속한 클래스를 알려주기 위해 스코프 지정 연산자를 붙인다는 것 이다.


8.2.1.1 클래스 메서드 내에서의 static 데이터 멤버 접근

메서드 안에서는 static 데이터 멤버를 일반 데이터 멤버와 같은 방식으로 이용할 수 있다. 예를 들어 생성자 안에서 스프레드시트 객체의 고유 순번 mid 를 최종 순번을 담고 있는 static멤버 sCount값으로 초기화하고 싶을 수 있다

class SpreadSheet
{
public:
	SpreadSheet(int inWidth, int inHeight);
	SpreadSheet(const SpreadSheet& src) = delete;
	~SpreadSheet();
	void setCellAt(int x, int y, const SpreadSheetCell& cell);
	SpreadSheetCell& getCellAt(int x, int y);
	int getId() const;

public:
	SpreadSheet& operator=(const SpreadSheet& rhs) = delete;

private:
	void copyFrom(const SpreadSheet& src);
	bool inRange(int val, int upper);
	int mWidth, mHeight;
	SpreadSheetCell** mCells;
	static int sCounter;
	int mId;
};

SpreadSheet 객체의 고유 순번 할당 작업이 포함된 생성자를 다음과 같이 구현할 수 있다.

SpreadSheet::SpreadSheet(int inWidth, int inHeight) :
    mWidth(inWidth), mHeight(inHeight)
{
    mId = sCounter++;
    mCells = new SpreadSheetCell * [mWidth];
    for (int i = 0; i < mWidth; i++)
        mCells[i] = new SpreadSheetCell[mHeight];
}

 

위 코드에서 알 수 있듯, sCounter는 일반 멤버처럼 접근할 수 있다. 객체가 생성될 때 복제 생성자가 이용될 수도 있다는 것을 잊지 말자, 다음과 같이 복제 생성자에서도 고유 순번 할당 작업을 한다.

SpreadSheet::SpreadSheet(const SpreadSheet& src)
{
    mId = sCounter++;
    copyFrom(src);
}

대입 연산자에서 고유 순번 (mId)를 복제해서는 안된다. 고유 순번은 객체 생성 시점에 한 번 할당된 후 변경되지 않아야 하기 때문이다. 따라서 mId는 8.2.2절에서 설명하는 const데이터 멤버로 만들어야 한다.


8.2.1.2 클래스 메서드 밖에서의 static 데이터 멤버 접근

sCounter를 선언한 예제에서는 접근자로 private를 사용했다. 이 때문에 클래스 메서드 바깥에서는 sCounter 변수를 이용할 수 없다.

 

만약 public으로 선언한다면 클래스 바깥에서도 접근할 수 있다. 클래스 밖에서 static 데이터 멤버에 접근하기 위해서는 다음과 같이 스코프 지정 연산자를 붙여주어야 한다.

int c = SpreadSheet::sCounter;

하지만 클래스 데이터 멤버를 public 으로 선언하여 외부에 노출시키는 것은 바람직하지 않다.

public get/set 메서드를 통해 접근 권한을 얻도록 하는 것이 좋다. static 멤버에 대한 get/set메서드를 구현하려면 static 메서드를 사용해야 한다.


8.2.2 const 데이터 멤버

클래스의 데이터 멤버를 const로 선언하면 생성 시점에 초깃값을 부여한 다음 더는 값을 변경할 수 없게 된다. 그런데 객체 수준에서 상수값을 보유하는 것은 대부분 메모리 낭비다. 이 떄는 static const 멤버를 이용해서 객체 간에 상수값을 공유하도록 할 수 있다.

 

예를 들어 스프레드시트가 가질 수 있는 가로와 세로 최대 크기를 관리해야 한다면 다음과 같이 static const 멤버로 선언하여 사용자가 최댓값 이상으로 스프레드시트를 키우려고 할 떄 최댓값까지만 설정되도록 강제할 수 있다.

	static const int kMaxHeight = 100;
	static const int kMaxWidth = 100;

이렇게 선언된 상수값은 다음처럼 생성자에도 이용할 수 있다.

SpreadSheet::SpreadSheet(int inWidth, int inHeight) :
    mWidth(inWidth < kMaxWidth ? inWidth : kMaxWidth),
    mHeight(inHeight < kMaxHeight ? inHeight : kMaxHeight)
{
    mId = sCounter++;
    mCells = new SpreadSheetCell * [mWidth];
    for (int i = 0; i < mWidth; i++)
        mCells[i] = new SpreadSheetCell[mHeight];
}

 

가로와 세로 크기가 허용 범위를 넘어설 때 최댓값에 강제로 맞추는 대신 익셉션을 발생시킬 수도 있다.

그런데 생성자 안에서 익셉션을 발생시키면 소멸자가 불리지 않는 문제가 있기 때문에, 조심해서 사용해 야 한다.

 

static이 아닌 데이터멤버도 cosnt로 선언할 수 있다. 예를 들어 데이터 멤버 mId는 cosnt로 선언할 수 있다.

 그런데, cosnt 변수는 대입이 허용되지 않기 때문에 초깃값을 지정하려면 생성자와 초기화 리스트를 사용해야 한다.

 

kMaxHeight와 kMaxWidth는 public 이기 때문에 프로그램의 어디서든 마치 전역 변수처럼 접근할 수 있다.

단, SpreadSheet클래스에 속한 변수이기 때문에 스코프 지정 연산자를 붙여야 한다.


8.2.3 참조형 데이터 멤버

스프레드 시트 프로그램으로 온전하게 기능하려면, SpreadSheet와 SpreadSheetCell 클래스를 SpreadSheetApplication 클래스에 통합해야 한다.

 

SpreadSheetApplication 클래스를 구현하려면 구조적인 문제 한 가지를 해결해야 한다.

SpreadSheetApplication은 복수의 SpreadSheet를 관리하기 때문에, 당연히 SpreadSheet 클래스를 알아야 한다, 그런데 SpreadSheet클래스도 SpreadSheetApplication 클래스와 커뮤니케이션 해야 하기 때문에, SpreadSheetApplication을 알아야 한다.

 

즉, 두 클래스 모두 정의되어 있어서 서로 참조할 수 있어야 한다.

 

이러한 상황은 닭과 달걀 문제를 발생시킨다. SpreadSheetApplication을 정의하기 전에 이미 SpreadSheet가 정의되어 있어야 하고 SpreadSheet도 마찬가지로 SpreadSheetApplication가 정의되어 있어야 SpreadSheet 정의 시점에 참조할 수 있다.

 

#include로는 이런 문제를 해결할 방법이 없다. 이에 대한 해결책으로 포워드 선언이 이용된다.

교차 잠조되는 클래스의 헤더 파일 중 어느 한쪽에 상대편 클래스의 헤더 파일을 인클루드하는 대신 포워드 선언을 해두면 컴파일러가 나중에 해당 정의를 찾아서 타입 매칭을 한다.

class SpreadSheet
{
public:
	SpreadSheet(int inWidth, int inHeight,
		SpreadSheetApplication& theApp);
	SpreadSheet(const SpreadSheet& src) = delete;
	~SpreadSheet();
	void setCellAt(int x, int y, const SpreadSheetCell& cell);
	SpreadSheetCell& getCellAt(int x, int y);
	int getId() const;

	static const int kMaxHeight = 100;
	static const int kMaxWidth = 100;

public:
	SpreadSheet& operator=(const SpreadSheet& rhs) = delete;

private:
	void copyFrom(const SpreadSheet& src);
	bool inRange(int val, int upper);
	int mWidth, mHeight;
	SpreadSheetCell** mCells;
	SpreadSheetApplication& mTheApp;
	static int sCounter;
	int mId;
};

이 클래스 정의에서는 SpreadSheetApplication에 대한 참조형 변수를 멤버로 선언하고 있다. 이 경우 Spreadsheet객체는 항상 SpreadSheetApplication 객체를 참조할 것이기 떄문에 포인터 보다는 참조형을 사용하는 것이 바람직하다. 포인터와 달리 참조형은 적합한 객체로 초기화되어야 존재할 수 있기 때문에 훨씬 안전하다.

 

SpreadSheet을 SpreadSheetApplication에 참조형 변수로 두는 것은 단지 참조 타입을 데이터 멤버로 사용하는 예를 들기 위한 것이다. 실제 상황이라면 MVC 패러다임을 적용하여 의존성을 없애는 것이 바람직하다.

 

SpreadSheetApplication객체는 생성자에서 파라미터로 받고 있다. 참조형 변수는 생성과 동시에 다른 객체를 참조하도록 초기화되어야 하므로 객체가 생성되고 나서 나중에 세팅할 수 없다. 이 떄문에 다음 코드처럼 생성자 초기화 리스트에서 mTheApp 변수를 초기화한다.

SpreadSheet::SpreadSheet(int inWidth, int inHeight, 
    SpreadSheetApplication& theApp) :
    mWidth(inWidth < kMaxWidth ? inWidth : kMaxWidth),
    mHeight(inHeight < kMaxHeight ? inHeight : kMaxHeight),
    mTheApp(theApp)
{
    mId = sCounter++;
    mCells = new SpreadSheetCell * [mWidth];
    for (int i = 0; i < mWidth; i++)
        mCells[i] = new SpreadSheetCell[mHeight];
}

또 다른 객체 생성 경로인 복제 생성자도 잊지 말고 초기화해야 한다. 원본이 가진 SpreadSheetApplication객체로 mTheApp 참조형 멤버를 초기화한다.

SpreadSheet::SpreadSheet(const SpreadSheet& src) :
    mTheApp(src.mTheApp)
{
    mId = sCounter++;
    copyFrom(src);
}

참조형 변수는 한 번 초기화되고 나면 다른 값으로 바꿀 수 없다. 이 때문에 대입 연산자에서 참조형 변수를 대입하려 하면 컴파일 에러가 발생한다.


8.2.4 const 참조형 데이터 멤버

참조형 데이터 멤버는 보통의 참조형 변수와 마찬가지로 const 객체를 참조할 수 있다. 예를 들어 SpreadSheet객체가 애플리케이션 객체를 cosnt 타입으로 참조해야 한다면 다음처럼 mTheApp변수를 const로 선언하기만 하면 된다.

class SpreadSheet
{
public:
	SpreadSheet(int inWidth, int inHeight,
		const SpreadSheetApplication& theApp);

public:
	SpreadSheet& operator=(const SpreadSheet& rhs) = delete;

private:
	const SpreadSheetApplication& mTheApp;

};

참조형 변수가 cosnt타입이냐 아니냐에 따라 큰 차이가 있다. cosnt 참조형변수로 참조된 SpreadSheetApplication데이터 멤버는 SpreadSheetApplication객체에 대해 cosnt 메서드만 호출할 수 있다. 만약, cosnt참조된 객체에서 const가 아닌 메섣를 호출하려 하면 컴파일 에러가 발생한다.

 

static참조형 멤버나 static const 참조형 멤버도 선언할 수 있다. 하지만, 그런 타입은 특별히 의미 있는 사용처를 찾기 어렵다.

728x90