일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- queue
- html
- Deque
- WinAPI
- 자료구조
- set
- priority_queue
- 수광 소자
- list
- stl
- 라인트레이서
- 통계학
- 아두이노
- vector
- LineTracer
- Algorithm
- C언어
- 아두이노 소스
- Visual Micro
- 아두이노 컴파일러
- c++
- Array
- arduino compiler
- Arduino
- directx
- 운영체제
- 컴퓨터 그래픽스
- Stack
- 시스템프로그래밍
- map
- Today
- Total
Kim's Programming
C++ - 캡슐화(2/3) 본문
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 | #include<iostream> using namespace std; int num = 0; class Count { private: int Value; public: Count(){ num++; } ~Count(){ num--; } void Output() { cout << "현재 객체 개수 = " << num << endl; } }; void main() { Count C,*pC; C.Output(); pC = new Count; pC->Output(); delete pC; C.Output(); cout << "크기 = " << sizeof(C) << endl; } | cs |
프로그램이 실행된 직후에 전역변수 num은 0으로 초기화될 것입니다. main 함수가 시작되기 전에 지역 객체 C가 생성되며 이때 C의 생성자에서 num을 1증가시키므로 num은 1이 됩니다. new 연산자로 Count 클래스의 객체를 동적으로 생성하면 이때도 생성자가 호출되어 num은 2가 되며 delete연산자로 객체를 파괴하면 파괴자가 호출되어 num은 1이 됩니다. 메인함수가 종료되면 물론 객체 전부가 파괴되므로 num은 초기상태 0으로 돌아갑니다.
정적이든 동적이든 객체가 생성, 파괴될 때는 생성자와 파괴자가 호출되며 이 함수들이 num을 관리하고 있으므로 num은 항상 생성된 객체의 개수를 정확하게 유지합니다. 디버거로 한 줄씩 실행해 가면서 num의 값을 살펴보면 생성된 객체의 개수를 정확하게 세고 있는것을 확인 할 수 있습니다. 하지만 원하는 목적은 달성했어도 객체 지향적이지는 못합니다. 전역 변수는 3가지 면에서 문제가 있습니다.
- 클래스와 관련된 중요한 정보를 왜 클래스 바깥의 전역변수로 선언하는가가 일단 불만입니다. 자신의 정보를 완전히 캡슐화하지 못했으므로 이 클래스는 독립적인 부품으로 동작할 수 없습니다.
- 전역 변수가 있어야만 동작할 수 있으므로 재사용하고자 할 경우 항상 전역변수와 함께 배포해야 합니다. 클래스만 배포해서는 제대로 동작하지 않습니다.
- 전역변수는 은폐할 방법이 없기 때문에 외부에서 누구나 마음대로 수정할 수 있습니다. 어떤 코드에서 고의든 실수든 num=1234라고 대입해 버리면 생성된 객체 수가 1234개라고 오해하게 될 것입니다.
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 | #include<iostream> using namespace std; class Count { private: int Value; int num; public: Count(){ num++; } ~Count(){ num--; } void Output() { cout << "현재 객체 개수 = " << num << endl; } }; void main() { Count C,*pC; C.Output(); pC = new Count; pC->Output(); delete pC; C.Output(); cout << "크기 = " << sizeof(C) << endl; } | cs |
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 | #include<iostream> using namespace std; class Count { private: int Value; static int num; public: Count(){ num++; } ~Count(){ num--; } void Output() { cout << "현재 객체 개수 = " << num << endl; } }; int Count::num=0; void main() { Count C,*pC; C.Output(); pC = new Count; pC->Output(); delete pC; C.Output(); cout << "크기 = " << sizeof(C) << endl; } | cs |
정적 멤버 함수의 개념도 정적 멤버 변수의 경우와 비슷합니다. 객체와 직접적으로 연관된다기보다는 클래스와 연관되며생성된 객체가 하나도 없더라도 클래스의 이름만으로 호출할 수 있습니다. 일반 멤버 함수는 객체를 먼저 생성한 후 object.function()형식으로 호출한 객체에 대해 어떤 작업을 합니다. 이와는 다르게 정적 멤버 함수는 Class::function() 형식으로 호출하며 클래스 전체에 대한 전반적인 작업을 합니다. 주로 정적 멤버 변수를 조작하거나 이 클래스에 속한 모든 객체를 위한 어떤 처리를 합니다. 정적 멤버 함수를 선언하는 방법은 정적 멤버 변수와 동일합니다. 클래스 선언부의 함수원형 앞에 static이라는 키워드만 붙여주면 됩니다. 정적 멤버 함수의 본체는 클래스 선언부에 인라인 형식으로 작성할 수도 있고 아니면 따로 정의할 수도 있는데 외부에서 작성할 때는 static 키워드는 생략합니다. 다음 소스는 위에서 객체 갯수를 세는 소스를 수정한 것입니다.
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 | #include<iostream> using namespace std; class Count { private: int Value; static int num; public: Count(){ num++; } ~Count(){ num--; } static void InitNum() { num = 0; } static void Output() { cout << "현재 객체 개수 = " << num << endl; } }; int Count::num; void main() { Count::InitNum(); Count::Output(); Count C,*pC; C.Output(); pC = new Count; pC->Output(); delete pC; C.Output(); cout << "크기 = " << sizeof(C) << endl; } | cs |
정적 멤버 변수 num을 정의할 때 0으로 초기화하지 않았으며 이 작업은 새로 추가된 정적 멤버 함수 InitNum이 담당합니다. InitNum은 정적 멤버 함수이므로 Count 클래스의 객체가 전혀 없는 상태에서도 호출될 수 있습니다. main에서 Count::InitNum()을 먼저 호출하여 num을 0으로 초기화하였습니다. 변수를 초기화하는 별도의 함수를 만들었으므로 원한다면 실행중에 언제든지 이 함수를 호출하여 Num을 0으로 리셋할 수 있습니다.
객체의 개수를 출력하는 Output 함수도 개별 객체에 대한 함수가 아니기 떄문에 정적 멤버 함수로 수정할 수 있습니다. Output 함수가 객체로 부터 호출되지 않으므로 이제 객체가 전혀 생성되지 않은 상태, 즉 Num이 0인 상태에 대한 출력도 가능합니다. 정적 멤버 함수가 아니면 이런 호출은 불가합니다. main에서 지역 객체 C를 생성하기 전에 Count::Output()을 호출했는데 이 호출문은 0을 출력하며 아직 생성된 객체가 없다는 것을 보여줍니다.
C객체를 생성한 후 C.Output()을 호출하면 1이 출력되고 pC 객체를 동적생성한후 pC->Output()을 호출하면 2가 출력됩니다. 이 두 호출의 예처럼 정적 멤버 함수를 객체의 이름으로 호출할 수도 있지만 이때 객체의 이름은 아무런 의미가 없으며 컴파일러는 객체가 소속된 클래스의 정보만 사용합니다. 편의상 C.Output(), pC->Output();이라는 표현을 허용할 뿐이지 이 호출은 실제로 Count::Output()으로 컴파일 된다는 이야기 입니다.
정적 멤버 함수는 특정 객체에 의해 호출되는 것이 아니므로 숨겨진 인수 this가 전달되지 않습니다. 클래스에 대한 작업을 하기 떄문에 어떤 객체가 자신을 호출했는지 구분할 필요가 없으며 따라서 호출 객체에 대한 정보도 필요없습니다. 그래서 정적 멤버 함수는 정적 멤버만 엑세스할 수 있으며 일반 멤버(비정적 멤버)는 참조 할 수 없습니다. 왜냐하면 일반 멤버 앞에는 암시적으로 this->가 붙는데 정적 멤버 함수는 this를 전달받지 않기 떄문입니다. 정적 멤버 함수인 InitNum에서 비정적 멤버인 Value를 참조하는 것은 불가능합니다.
1 2 3 4 5 | static void InitNum() { num = 0; Value = 5; } | cs |
이 코드를 컴파일하면 정적 멤버 함수에서 Value를 불법을 ㅗ참조했다는 에러 메세지가 출력됩니다. InitNum의 본체에서 Value를 칭하면 누구의 Value를 칭하면 누구의 Value인지 판달할 수 없습니다. 또한 정적 멤버 함수는 생성된 객체가 없어도 호출할 수 있는데 이떄 Value는 아예 존재하지도 않습니다. 비정적 멤버 함수도 호출할 수 없으며 오로지 정적 멤버만 참조할 수 있습니다.
정적 멤버의 활용
정적 멤버는 필요한 모든 것을 객체 내에 둔다는 캡슐화 원칙에 위배된 것처럼 보이기도 하고 정적 멤버 변수의 경우 선언과 정의가 두 번 나타나기 떄문에 문법적으로도 조금 어색해보입니다. 그러나 물리적으로는 객체 바깥에 선언되어 있지만 논리적으로는 클래스에 속해있고 액세스 지정에 의해 정보 은폐도 가능하므로 캡슐화 위반은 아닙니다. 정적 멤버의 개념이 꼭 필요한 이유는 여러가지 경우에 이것이 굉장히 유용하기 때문입니다. 정적 멤버를 훌륭하게 활용하는 몇 가지 예를 보겠습니다.
- 단 한 번만 해야 하는 전역 자원의 초기화
데이어베이스 연결이나 네트워크 연결, 윈도우 클래스 등록 등과 같이 단 한 번만 하면 되는 초기화는 정적 멤버 함수에서 하고 그 결과를 정적 멤버 변수에 저장합니다. 이런 전역 초기화는 일반적으로 두 번 할 필요도 없고 두 번 초기화는 허용되지 않습니다. 그래서 객체별로 초기화해서는 안 되며 클래스 수준에서 딱 한 번만 초기화하고 그 결과는 모든 객체가 공유합니다.
데이터베이스에서 질의를 하는 클래스를 예를 들어보겠습니다. 질의를 하기 위해서는 먼저 정보가 저장되어있는 DB서버에 연결하는 인증 절차를 거쳐야합니다. 연결이나 인증이나 두번 한다는 것은 의미가 없으므로 한 번만 연결하고 이후부터는 모든 질의 객체가 이 연결을 공유하면 될 것입니다. 다음은 질의 클래스의 가상코드입니다. 실제 DB 접속을 하려면 복잡하기 떄문에 가상 코드를 예로 들었습니다.12345678910111213141516171819202122232425262728293031323334353637383940#include<iostream>using namespace std;class DBQuery{private:static HANDLE hCon;int nResult;public:DBQuery(){};static void DBConnect(char *Server. char *ID, char *pass);static void DBDisconnect();bool RunQuery(char *SQL);};HANDLE DBQUERY::hCon;void DBQUERY::DBConnect(char *Server, char *ID, char *pass){//여기서 DB서버에 접속한다//hCon = 접속핸들}void DBQuery::DBDisconnect(){//접속을 해제한다//hCon=NULL;}bool DBQuery::RunQuery(char *SQL){//Query(hCon,SQL)return true;}void main(){DBQuery::DBConnect("Secret", "Hong", "gildong");DBQuery Q1, Q2, Q3;//필요한 DB 징의를 한다//Q1.RunQuery("select * from ")DBQuery::DBDisconnect();}cs DB서버와의 연결은 DBConnect 정적 멤버 함수가 처리합니다. 이 함수는 서버 이름, ID, 비밀번호 를 인수로 전달받아 BD서버와 연결 및 인증을 하고 연결 결과는 정적 멤버 변수 hCon에 저장한다. 정적 멤버 함수는 정적 멤버 변수를 엑세스할 수 있으므로 DBConnect 에서는 hCon을 당연히 엑세스 할 수 있습니다. 연결을 해제하는 작업도 역시 적정 멤버 함수인 DBDisConnect에서 처리합니다.
main 함수에서는 DBQuery 객체를 생성하기 전에 DBConnect 함수를 호출해서 DB 서버에 연결하며 이로서 DBQuery 객체가 질의를 할 수 있는 환경을 만들어 놓습니다. 이후 생성되는 DBQuery 객체 Q1, Q2, Q3의 RunQuery함수는 정적 멤버 hCon에 저장된 연결 핸들로 원하는 질의를 처리할 것입니다. RunQuery함수는 정적 멤버는 아니지만 공유된 연결 핸들 hCon은 얼마든지 엑세스할 수 있습니다. 질의를 마치고 프로그램을 종료하기 전에 DBDisConnect 정적 멤버 함수를 호출하여 DB서버와의 연결을 끊고 필요한 뒷처리를 합니다. DB서버에 연결하는 과정은 굉장히 느리고 리소스를 많이 차지하기 때문에 객체 별로 따로 연결하지 않고 딱 한 번만 연결해야 합니다. 이럴때 사용하는 것이 바로 정적 멤버입니다. 물론 각 객체별로 따로 연결 핸들을 가지고 생성자에서 접속, 파괴자에서 해제하는 것도 가능합니다. 읽기 전용 자원의 초기화
객체는 스스로 동작할 수 있지만 때로는 외부의 환경이나 자원에 대한 정보를 필요로 합니다. 예를 들어 정확한 출력을 위해 화면 크기를 알아야 할 경우도 있고 장식을 위해 외부에 정의된 예쁜 비트맵 리소스를 읽어야 하는 경우도 있습니다. 이런 정보들은 일반적으로 한 번 읽어서 여러번 사용할 수 있는 읽기 전용이기 떄문에 객체별로 이 값을 일일이 조사하고 따로 유지할 필요가 없습니다. 다음 소스는 화면 크기에 대한 정보를 정적 멤버로 가집니다.12345678910111213141516171819202122232425262728#include<iostream>#include<windows.h>using namespace std;class Shape{private:int shapeType;RECT ShapeArea;COLORREF Color;public:static int srcx, srcy;static void GetScreenSize();};int Shape::srcx;int Shape::srcy;void Shape::GetScreenSize(){srcx = GetSystemMetrics(SM_CXSCREEN);srcy = GetSystemMetrics(SM_CYSCREEN);}void main(){Shape::GetScreenSize();Shape C, E, R;cout << "화면 = " << Shape::srcx << "," << Shape::srcy;}cs Shape 클래스는 화면에 도형을 그리는 클래스인데 이 클래스의 객체들은 공통적으로 현재 화면 크기에 대한 정보를 필요로 한다고 하겠습니다. 각 객체별로 scrx, scry를 가지고 생성자에서 일일이 조사할 수도 있지만 이렇게 하면 기억 공간이 낭비되며 실행 시간도 느려집니다. 각 객체들은 동일한 화면에서 실행되며 각자가 조사하는 화면 크기가 다르지 않으므로 여러 번 조사할 필요가 전혀 없습니다.
정적 멤버 변수 srx,scry를 만들고 이 변수의 값을 초기화하는 정적 멤버 함수 GetScreensize()함수를 정의한 후 main에서 객체를 생성하기 전에 딱 한 번만 이 함수를 호출하면 됩니다. 정적 멤버 함수이므로 생성된 객체가 없어도 호출할 수 있습니다. 이후 생성되는 모든 Shape 객체는 별도의 조사과정을 거치지 않고 공유된 scrx, scry 멤버 변수를 읽는 것으로 언제든지 화면 크기를 참조할 수 있습니다.
여기서는 간략함을 위해 조사하기 쉬운 화면 해상도를 사용했는데 떄로는 공유 정보가 비트맵 이나 멀티미디어 파일, 대화상자 같은 덩치가 큰 자원일 수도 있습니다. 각 정보가 읽기 전용이 아니라 객체별로 다른 값을 가져야 하는 경우라면 얘기가 달라지겠지만 일반적으로 장식이나 정보 취득에 사용되는 자원들은 읽기 전용이며 실행 중에 값이 변하지 않습니다. 이런 자원들은 반드시 정적 멤버로 관리해야 하며 그렇지 않을 경우 속도나 크기면에서 아주 불리해집니다.모든 객체가 공유해야 하는 정보 관리
중요한 계산을 하는 객체의 경우 계산에 필요한 기준값이 있을 수 있습니다. 예를 들어 환율이나 이자율 따위는 금융, 재무 처리에 상당히 중요한 기준값으로 작용하며 기준값에 따라 계산 결과가 달라집니다. 이런 값들은 프로그램이 동작중일 떄도 수시로 변할 수 있지만 일단 정해지면 모든 객체에 일관되게 적용됩니다. 그래서 개별 객체들이 각자 멤버로 가질 필요가 없으며 정적 멤버로 선언해 두고 공유하면 항상 최신의 기준값을 제공받게 됩니다. 다음은 환율을 계산하는 소스입니다.123456789101112131415161718192021222324#include<iostream>using namespace std;class Exchange{private:static double Rate;public:static double GetRate(){ return Rate; }static void SetRate(double aRate){ Rate = aRate; }double DollarToWon(double d){ return d*Rate; }double WonToDollar(double w){ return w / Rate; }};double Exchange::Rate;void main(){Exchange::SetRate(1200);Exchange A, B;cout << "1달러는" << A.DollarToWon(1.0) << "원이다" << endl;Exchange::SetRate(1150);cout << "1달러는" << B.DollarToWon(1.0) << "원이다" << endl;}cs 정적 멤버 변수 Rate는 Exchange 클래스에 속해 있고 이 클래스의 클래스의 모든 객체가 같이 참조합니다. 누구든지 환율이 필요하면 이 값을 읽을 수 있고 또한 변경할 수 있어 관리가 편리합니다. 만약 객체별로 환율을 따로 가지면 객체를 초기화할 때마다 현재의 기준 환율을 전달해야 하며 환율이 변했을 때 생성되어 있는 모든 객체에게 이 사실을 알려야 합니다. 현재 생성된 모든 객체의 목록을 유지하는 것은 생각보다 훨씬 어려운 일입니다. 하나의 값은 하나의 기억 장소에 두는 것이 가장 바람직 합니다.
정적 멤버를 쓰는 대신 기준값을 필요로 하는 모든 멤버 함수들이 기준값을 인수로 전달받는 방법도 생각해 볼 수 있습니다. 하지만 이렇게 도면 클래스 외부에서 별도의 전역변수로 기준값을 저장 및 관리해야 하며 이는 캡슐화에 위배됩니다. 함수가 호출될 때 최신값을 인수로 제공받으므로 결과는 가장 정확하겠지만 호출할 때 최신값을 인수로 제공받으므로 결과는 가장 정확하겠지만 호출할 때마다 인수를 일일이 전달하는 것은 아주 비효율적입니다. 예제의 실행 결과는 다음과 같습니다.main에서 최초 정적 멤버 함수 SetRate를 호출하여 환율을 1200으로 설정했습니다. 이 값은 정적 멤버 변수 Rate에 저장되며 이후 생성되는 모든 Exchange 객체는 이값을 공유합니다. 중간에 환률이 변경되었다면 Exchange::SetRate()함수로 새 기준값을 Rate에 설정하여 모든 객체들이 다음 계산에 이 값을 사용하도록 합니다.
'Programming > Cplusplus' 카테고리의 다른 글
C++ - 연산자 오버로딩(1/3) (1) | 2015.09.20 |
---|---|
C++ - 캡슐화(3/3) (0) | 2015.09.19 |
C++ - 캡슐화(1/3) (0) | 2015.09.19 |
C++ - 클래스 생성자/파괴자(3/3) (0) | 2015.09.03 |
C++ - 클래스 생성자/파괴자(2/3) (2) | 2015.08.31 |