티스토리 뷰

※ 주의 사항 ※

  • 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

const 상수와 static 변수

 

지금까지는 일반 변수만을 멤버 변수로서 가지는 클래스만 정의해왔는데, 사실 클래스는 일반 변수뿐만 아니라 const 상수와 static 변수도 멤버 변수로서 가질 수 있다. const 상수는 모를 사람이 없을 것이지만, static 변수는 처음 들어보는 사람도 있을 것이다. 일반 변수, const 상수, static 변수를 서로 비교해가면서 개념을 익혀보자.

 

일반 변수는 객체 차원에서 관리되는 데이터이고, const 상수와 static 변수는 클래스 차원에서 관리되는 데이터다. 클래스는 객체의 설계도이고 하나의 클래스로부터 무수히 많은 객체들이 생성될 수 있다는 것을 이해하고 있을 것이다. 이때 객체들은 저마다 서로 같은 값을 데이터로 가질 수도 있고, 하나의 뿌리(클래스)로부터 나왔기 때문에 공통된 값을 데이터로 가질 수도 있을 것이다. 객체마다 다른 값을 가질 수 있는 데이터를 객체 차원의 데이터라고 하고, 같은 클래스에서 생성된 객체의 경우 동일한 값을 가지는 데이터를 클래스 차원의 데이터라고 하겠다('객체 차원의 데이터'나 '클래스 차원의 데이터'라는 말은 그냥 내가 설명에 유리하게 하고자 붙인 이름이지 범용적으로 사용되는 용어는 아니다). 

 

그리고 const 상수와 static 변수는 클래스 차원의 데이터이지만, const 상수는 값의 변경이 불가능한 반면, static 변수는 값의 변경이 가능하다. 일반 변수와 const 상수, static 변수의 개념은 아래의 예제를 보면 이해가 쉬울 것이다.

#include <iostream>
#include <string>

class GalaxyS22 {
	const int price = 100;     //갤럭시 S22의 가격
	static int total;          //생산된 갤럭시 S22의 총 개수
	std::string phoneNum;      //각 갤럭시 S22의 고유 번호
public:

	GalaxyS22(const char* phoneNum) : phoneNum(phoneNum) { ++total;	}
	~GalaxyS22() { --total; }
	void showInfo() {
		std::cout << "phoneNum : " << phoneNum 
			<< ", price : " << price
			<< ", total : " << total << std::endl;
	}
};

int GalaxyS22::total = 0;    //static 변수의 초기화

int main() {
	GalaxyS22 gs1("010-1234-5678");
	gs1.showInfo();

	GalaxyS22 gs2("010-1598-7565");
	gs2.showInfo();

	GalaxyS22* gs3 = new GalaxyS22("010-5248-1547");
	gs3->showInfo();

	std::cout << "갤럭시 S22 하나를 폐기합니다." << std::endl;
	delete gs3;

	gs1.showInfo();
	gs2.showInfo();
	
	return 0;
}

/*
실행결과

phoneNum : 010-1234-5678, price : 100, total : 1
phoneNum : 010-1598-7565, price : 100, total : 2
phoneNum : 010-5248-1547, price : 100, total : 3
갤럭시 S22 하나를 폐기합니다.
phoneNum : 010-1234-5678, price : 100, total : 2
phoneNum : 010-1598-7565, price : 100, total : 2
*/

갤럭시 S22라는 객체를 생성할 클래스를 정의했다. 각 객체는 서로 다른 휴대폰 번호(일반 변수)를 가진다. 가격(const 상수)은 모두 동일하게 고정되어 있다. 생산된 갤럭시 S22의 개수(static 변수)는 때에 따라 값은 변하겠지만 그래도 모든 객체가 동일한 값을 공유한다.

 

const 상수는 클래스 내에 선언함과 동시에 초기화한다. 선언과 동시에 초기화해야 하는 것은 클래스 내에서 사용되지 않더라도 const 상수 그 자체의 고유 특징이다.

 

staitc 변수의 초기화는 조금 특이한데 클래스의 밖에서 진행한다. static 변수의 데이터 타입을 먼저 적어주는데 이때 static 키워드는 제외한다. 그리고 static 변수의 이름을 기입한다. 그런데 그냥 변수명 total만 적으면 새로운 전역 변수 total을 선언하는 것으로 컴파일러가 오해할 수 있다. 따라서 클래스 GalaxyS22의 멤버로서 선언되어 있는 변수임을 명시하기 위해 ::(범위 확인 연산자) 연산자를 사용해서 클래스 이름을 앞에 기입해준다. 그리고 이를 0으로 초기화시킨다. 초기화하는 값은 꼭 0이 아니라도 사용 목적에 맞게 해 주면 된다. 

int GalaxyS22::total = 0;

그런데 static 변수의 초기화 과정이 클래스 밖에서 이뤄지기 때문에 꼭 전역 변수를 새로 선언하고 초기화하는 것만 같아 많이 어색할 것이다. 실제 static 변수를 사용할 때는 초기화 과정에서 클래스 이름과 ::연산자를 빼먹어서 전역 변수를 선언해버리는 일이 없도록 주의하자.

 

static 키워드의 특징

static 키워드는 멤버 변수뿐만 아니라 멤버 함수에도 사용할 수 있다. static 키워드는 또 하나의 특징이 있는데, static 키워드가 붙은 멤버는 객체를 생성하지 않고도 접근하는 것이 가능하다. 객체를 생성하지 않고 static 멤버에 접근할 때는 클래스 이름과 ::연산자가 사용된다. 아래의 예제를 보자.

#include <iostream>
#include <string>

class GalaxyS22 {
	const int price = 100;     
	static int total;          
	std::string phoneNum;     
public:

	GalaxyS22(const char* phoneNum) : phoneNum(phoneNum) { ++total; }
	~GalaxyS22() { --total; }
	void showInfo() {
		std::cout << "phoneNum : " << phoneNum
			<< ", price : " << price
			<< ", total : " << total << std::endl;
	}

	//새로 추가된 static 멤버 함수
	static void showTotal() {
		std::cout << "total : " << total << std::endl;
	}
};

int GalaxyS22::total = 0;    

int main() {
	GalaxyS22 gs1("010-1234-5678");
	gs1.showInfo();

	GalaxyS22 gs2("010-1598-7565");
	GalaxyS22* gs3 = new GalaxyS22("010-5248-1547");

	//클래스 이름과 ::연산자를 통해 static 멤버 함수 호출
	GalaxyS22::showTotal();

	std::cout << "갤럭시 S22 하나를 폐기합니다." << std::endl;
	delete gs3;

	GalaxyS22::showTotal();

	return 0;
}

/*
실행결과

phoneNum : 010-1234-5678, price : 100, total : 1
total : 3
갤럭시 S22 하나를 폐기합니다.
total : 2
*/

외부에서 클래스 이름과 ::연산자를 이용해 static 멤버에 접근하려면 해당 멤버가 public으로 선언되어 있어야 한다.

 

static 멤버 함수를 다룰 때는 주의할 점이 하나 있는데 해당 함수 내에서 static이 아닌 멤버에 접근하는 일이 없도록 해야 한다. 왜냐하면 static이 아닌 멤버들은 객체 생성 이후 객체를 통해서만 접근이 가능한데, static 멤버는 객체 생성 없이도 접근할 수 있으므로, static 멤버 함수에서 static이 아닌 멤버에 접근하는 행위는 객체를 생성하지 않고도 static이 아닌 멤버에 접근하는 것을 허용하는 것과 마찬가지가 된다. 이는 에러를 발생할 우려가 있기에 컴파일러에서도 이를 막는다.

 

객체 지향 핵심 요소 3. 상속

상속이라고 하면 부모의 재산을 자식에게 물려주는 것을 생각하기 쉽다. 클래스의 상속도 이와 다를 바 없다. 클래스에서의 상속이란, 부모 클래스의 자원(멤버 변수나 멤버 함수와 같은 모든 것들)을 자식 클래스가 사용할 수 있게 물려주는 것을 말한다. 현실의 재산 상속과 차이가 있다면 부모가 자식에게 재산을 물려주면 부모는 가지고 있던 재산이 사라지지만, 클래스에서는 자식 클래스에게 부모 클래스의 자원을 물려주더라도 부모 클래스의 자원은 그대로 유지된다.

 

상속 관계에 있는 두 클래스를 각각 부르는 이름은 여러 가지가 있다. 방금 사용한 부모 클래스와 자식 클래스 외에도 더 있다.

자원을 주는 클래스 자원을 받는 클래스
부모 클래스 자식 클래스
기초 클래스 파생 클래스
상위 클래스 하위 클래스

 

이제 클래스 상속 예제를 볼 텐데 그전에 부모 클래스를 정의하겠다.

class BaseClass {
public:
	int num;
};

다음은 자식 클래스의 정의다.

class DerivedClass {

};

부모 클래스는 멤버 변수 num 하나를 가지고 있고, 자식 클래스는 아무런 멤버가 없다. 코드 상으로 상속을 표현할 때는 다음과 같이 작성한다.

class DerivedClass : public BaseClass {    //public의 의미는 일단 무시하자

};

간단하지 않은가? 콜론(:)을 먼저 붙이고 접근 제어 지시자를 명시한 후 부모 클래스의 이름을 적어주면 끝이다. 이렇게 되면 DerivedClass는 BaseClass의 멤버인 num을 물려받아 자기 것인 양 사용할 수 있게 된다. 아래의 예제를 보자.

#include <iostream>

class BaseClass {
public:
	int num;
};

class DerivedClass : public BaseClass {    //public의 의미는 일단 무시하자

};

int main() {
	BaseClass bc;
	DerivedClass dc;

	bc.num = 10;
	//DerivedClass는 원래 비어 있었으나 BaseClass로부터 물려 받은 num 사용 가능
	dc.num = 200;

	std::cout << "base class의 num : " << bc.num << std::endl << "derived class의 num : " << dc.num << std::endl;

	return 0;
}

/*
실행결과

base class의 num : 10
derived class의 num : 200
*/

자식 클래스가 부모 클래스의 자원을 물려받았다고 해서 해당 자원을 공유하는 것은 아니다. DerivedClass의 num은 비록 BaseClass에게서 물려받은 것이지만 두 클래스의 num은 완전 별개의 것이다. 그렇기에 DerivedClass의 num 값을 변경했다고 해서 BaseClass의 num까지 같이 변경되는 일은 발생하지 않는다.

 

protected 접근 제어 지시자

아까부터 클래스의 상속에 사용된 public이 무슨 의미인지 궁금할 것이다. 그런데 조금만 기다려 달라. 해당 개념을 설명하기 이전에 먼저 해야 할 것이 있다. 이제 protected에 대해 설명할 기회가 왔다.

 

어느 클래스의 멤버가 protected 선언되었다면 이 멤버에 직접 접근하는 것을 허용하는 범위를 해당 클래스와, 그 클래스를 상속받는 자식 클래스로 제한한다. 그 외의 모든 곳에서는 이 멤버에 직접 접근할 수 없다. private은 이 범위를 해당 클래스 하나만으로 제한하기 때문에 자식 클래스에서조차 직접 접근이 불가능하게 만든다. 즉, 직접 접근의 허용 범위로 따지면 private, protected, public 순서로 커진다. 아래의 예제를 보자.

#include <iostream>

class BaseClass {
private:
	int prvNum;
protected:
	int prtNum;
public:
	int pblNum;

	void baseFunc() {
		//클래스 내에서는 private 멤버까지 직접 접근 가능
		prvNum = 101;
		prtNum = 102;
		pblNum = 103;
	}
};

class DerivedClass : public BaseClass {    //public의 의미는 일단 무시하자
public:
	void derivedFunc() {
		//prvNum = 201;    //private은 자식 클래스로부터의 직접 접근도 차단
		prtNum = 202;      //protected는 자식 클래스로부터의 직접 접근 허용
		pblNum = 203;
	}
};

int main() {
	BaseClass bc;

	//bc.prvNum = 301;
	//bc.prvNum = 302;
	bc.pblNum = 303;    //public만 외부에서의 직접 접근까지 허용

	return 0;
}

 

클래스의 상속과 접근 제어 지시자

이제 클래스의 상속에서 아래와 같이 사용된 public의 의미에 대해 알아보자.

class DerivedClass : public BaseClass {

};

짐작했겠지만 해당 자리에는 public뿐만 아니라 protected, private도 올 수 있다. 위와 같이 public 지시자를 사용하여 부모 클래스를 상속받는 것을 public 상속이라고 하겠다. public 상속의 의미는 '부모 클래스의 멤버 중 직접 접근의 허용 범위가 public보다 더 넓은 것이 있다면 이를 public으로 변경하여 상속받겠다'이다. 결과적으로 public 보다 직접 접근 허용 범위가 더 넓은 지시자는 없으므로 부모 클래스의 멤버들에 사용된 접근 제어 지시자를 그대로 유지하면서 받게 된다. 아래의 상속 관계에서,

class BaseClass {
private:
	int prvNum;
protected:
	int prtNum;
public:
	int pblNum;
};

class DerivedClass : public BaseClass{ 

};

DerivedClass는 다음과 동일한 클래스가 된다.

class DerivedClass {
//부모 클래스의 모든 멤버를 변경없이 그대로 받음
private:
	int prvNum;
protected:
	int prtNum;
public:
	int pblNum;
};

 

protected 상속과 private 상속에 대해서도 유추해볼 수 있겠는가? protected 상속은 '부모 클래스의 멤버 중 직접 접근의 허용 범위가 protected보다 더 넓은 것이 있다면 이를 protected로 변경하여 상속받겠다'는 의미를 가지므로 다음과 같은 상속 관계에서,

class BaseClass {
private:
	int prvNum;
protected:
	int prtNum;
public:
	int pblNum;
};

class DerivedClass : protected BaseClass{

};

DerivedClass는 다음과 동일한 클래스가 된다.

class DerivedClass {
private:
	int prvNum;
protected:
	int prtNum;
	int pblNum;    //부모 클래스의 멤버 중 public은 protected로 변경 후 받음
};

 

이제 패턴이 눈에 들어올 것이다. 그럼 private 선언에 대한 설명은 필요 없을 듯하다. 예시만 보이겠다. 아래와 같은 상속 관계에서,

class BaseClass {
private:
	int prvNum;
protected:
	int prtNum;
public:
	int pblNum;
};

class DerivedClass : private BaseClass{

};

DerivedClass는 다음과 동일한 클래스가 된다.

class DerivedClass {
	//부모 클래스의 모든 멤버를 private으로 변경 후 받음
private:
	int prvNum;
	int prtNum;
	int pblNum;
};

 

private 상속, protected 상속, public 상속 등 다양하지만 실제 public 상속 외에는 자주 사용되지 않는다. 오죽하면 한 대학 교수는 클래스의 상속에 대해서는 public 상속만 가르친다고 할 정도..

 

클래스를 상속하는 이유

클래스의 상속이라는 개념은 왜 생겨났으며, 주로 어느 경우에 사용하게 될까? 클래스의 상속은 다음과 같은 관계를 구현할 때 유용하다.

  • 학생은 사람이다.
  • 사자는 동물이다.
  • 호랑이는 고양잇과다.

이런 관계를 'is-a관계'라고 한다. 예를 하나 들어 보겠다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
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
글 보관함