http://www.ilit.co.kr cx8537@naver.com 최호성
Chapter 3 클래스
3장의 핵심 개념 클래스 생성자와 소멸자 메서드 정적 멤버 클래스, 생성자와 소멸자, 메서드, 정적 멤버 : 함수를 멤버로 갖는 구조체의 확장이라고 생각하면 이해하기 수월하다. 생성자와 소멸자 : 클래스 객체가 생성 및 소멸할 때 ‘자동으로’ 호출되는 함수이다. 메서드 : 함수 형태로 크래스의 실제 동작과 상태를 책임진다. 정적 멤버 : 문법적으로는 정적 ‘멤버’지만 실제로는 전역 변수나 함수같은 성격을 보인다.
객체지향 프로그래밍 개요 기존 절차지향 프로그래밍 코드 HelloOOP2.c – 제작자 코드 제작자가 자료구조(구조체)와 자료를 출력할 수 있는 함수를 제공하였다. #include <stdio.h> // 제작자의 코드 typedef struct USERDATA { int nAge; char szName[32]; } USERDATA; void PrintData(USERDATA *pUser) printf("%d, %s\n", pUser->nAge, pUser->szName); }
객체지향 프로그래밍 개요 기존 절차지향 프로그래밍 코드 HelloOOP2.c – 사용자 코드 1단계 코드의 경우 화면 출력과 관련해 자료구조가 변경되면 그에 맞춰 수정될 가능성이 매우 높다. 그러나 2단계 코드는 그리 큰 차이가 없다. 그러나 2단계 코드도 자료구조와 PrintData() 함수간의 관계를 설명하는 코드는 없다. // 사용자의 코드 int main(void) { USERDATA user = { 20, "철수" }; // printf("%d, %s\n", user.nAge, user.szName); // 1단계 PrintData(&user); // 2단계 return 0; }
객체지향 프로그래밍 개요 기존 절차지향 프로그래밍 코드 HelloOOP3.c – 제작자 코드 구조체 멤버로 함수 포인터를 넣어 자료구조와 함수를 묶으려 시도한다. #include <stdio.h> // 제작자의 코드 typedef struct USERDATA { int nAge; char szName[32]; void(*Print)(struct USERDATA *); } USERDATA; void PrintData(USERDATA *pUser) printf("%d, %s\n", pUser->nAge, pUser->szName); }
객체지향 프로그래밍 개요 기존 절차지향 프로그래밍 코드 HelloOOP3.c – 사용자 코드 멤버 접근 연산자를 통해 USERDATA 구조체에 연결된 함수를 호출한다. // 사용자의 코드 int main(void) { USERDATA user = { 20, "철수", PrintData }; // printf("%d, %s\n", user.nAge, user.szName); // 1단계 // PrintData(&user); // 2단계 user.Print(&user); // 3단계 return 0; }
기존 절차지향 프로그래밍 코드 HelloOOP3.c – 사용자 코드 객체지향 프로그래밍 개요 기존 절차지향 프로그래밍 코드 HelloOOP3.c – 사용자 코드 2단계 코드에서 가장 어색한 부분은 user 객체를 통해 Print를 수행하면서 user객체의 주소를 매개변수로 전달하는 것이다. // 사용자의 코드 int main(void) { USERDATA user = { 20, "철수", PrintData }; // printf("%d, %s\n", user.nAge, user.szName); // 1단계 // PrintData(&user); // 2단계 user.Print(&user); // 3단계 user.Print(); // 4단계 return 0; }
상속을 제외하면 접근제어 지시자, 멤버 변수 및 함수 등으로 구성된다. 클래스 기본 문법 상속을 제외하면 접근제어 지시자, 멤버 변수 및 함수 등으로 구성된다. class USERDATA { public: // 멤버 변수 선언 int nAge; char szName[32]; // 멤버 함수 선언 및 정의 void Print(void) // nAge와 szName은 Print() 함수의 지역 변수가 아니다! printf("%d, %s\n", nAge, szName); } }; 접근제어 지시자 멤버 변수(데이터) 멤버 함수(메서드)
멤버 선언 및 정의 클래스는 ‘생성자’를 이용해 멤버를 초기화 할 수 있다. 생성자는 제작자가 기술하는 함수이며 자동으로 호출된다. 따라서 객체를 사용하는 사람은 초기화 문제를 고민할 필요가 없다. class CTest { public: // CTest 클래스의 '생성자 함수' 선언 및 정의 CTest() // 인스턴스가 생성되면 멤버 데이터를 '자동으로' 초기화한다. m_nData = 10; } // 멤버 데이터 선언 int m_nData; ... }; // 사용자 코드 int _tmain(int argc, _TCHAR* argv[]) CTest t;
클래스는 ‘생성자’를 이용해 멤버를 초기화 할 수 있다. 멤버 선언 및 정의 클래스는 ‘생성자’를 이용해 멤버를 초기화 할 수 있다. 초기화 목록을 이용해 멤버를 초기화 할 수 있다. class CTest { public: // CTest 클래스의 '생성자 함수' 선언 및 정의 CTest() : m_nData(10) } // 멤버 데이터 선언 int m_nData1; ... }; // 사용자 코드 int _tmain(int argc, _TCHAR* argv[]) CTest t;
멤버함수의 정의는 클래스 선언 밖에서 할 수도 있다. 멤버 선언 및 정의 멤버함수의 정의는 클래스 선언 밖에서 할 수도 있다. // 제작자 코드 class CTest { public: // CTest 클래스의 '생성자 함수' 선언 및 정의 CTest() // 인스턴스가 생성되면 멤버 데이터를 '자동'으로 초기화한다. m_nData = 10; } // 멤버 데이터 선언 int m_nData; // 멤버 함수 선언. 정의는 분리했다! void PrintData(void); }; // 외부에 분리된 멤버 함수 정의 void CTest::PrintData(void) // 멤버 데이터에 접근하고 값을 출력한다. cout << m_nData << endl;
public, protected, private 접근제어 지시자 public, protected, private public : 클래스 외부 접근(보통 멤버 접근 연산자 활용)이 모두 허용된다. protected : 클래스 외부 접근이 차단된다. 하지만 파생 클래스에서의 접근은 허용된다. private : 외부는 물론 파생 클래스에서의 접근까지 모두 차단한다. 별도의 언급이 없다면 private이다.
private 멤버는 사용자 코드에서 임의로 접근할 수 없다! 접근제어 지시자 private 멤버는 사용자 코드에서 임의로 접근할 수 없다! // 제작자 코드 class CMyData { // 기본 접근 제어 지시자는 private이다. int m_nData; public: int GetData(void) { return m_nData; } void SetData(int nParam) { m_nData = nParam; } }; // 사용자 코드 int _tmain(int argc, _TCHAR* argv[]) CMyData data; data.m_nData = 10; // 허용되지 않는다! data.SetData(10); // 허용된다. cout << data.GetData() << endl; return 0; }
객체선언시 생성자가, 소멸할 때 소멸자가 자동으로 호출된다. 생성자와 소멸자 객체선언시 생성자가, 소멸할 때 소멸자가 자동으로 호출된다. class CTest { public: CTest() cout << "CTest::CTest()" << endl; } ~CTest() cout << "~CTest::CTest()" << endl; }; int _tmain(int argc, _TCHAR* argv[]) cout << "Begin" << endl; CTest a; cout << "End" << endl; return 0; 객체가 생성되는 지점 객체가 소멸하는 지점
생성자와 소멸자 만일 클래스 객체를 전역변수로 선언한다면 그 클래스의 생성자가 main() 함수보다 먼저 호출된다. 주의사항 만일 클래스 객체를 전역변수로 선언한다면 그 클래스의 생성자가 main() 함수보다 먼저 호출된다. 생성자는 다중 정의할 수 있다. 소멸자는 다중 정의할 수 없다. main() 함수가 끝난 후에 소멸자가 호출될 수 있다. (전역변수) 생성자와 소멸자는 생략할 수 있으나 이 경우 컴파일러가 기본 생성자/소멸자를 만들어 넣는다. 만일 생성자를 다중 정의했다면 기본 생성자를 아예 생략할 수도 있다. 배열로 객체를 동적 생성했다면 반드시 배열로 삭제해야 한다.
참조자 멤버는 반드시 초기화 목록을 이용해 초기화 한다. 참조 형식 멤버 초기화 참조자 멤버는 반드시 초기화 목록을 이용해 초기화 한다. // 제작자 코드 class CRefTest { public: // 참조형 멤버는 반드시 생성자 초기화 목록을 이용해 초기화한다. CRefTest(int &rParam) : m_nData(rParam) { }; int GetData(void) { return m_nData; } private: // 참조형 멤버는 객체가 생성될 때 반드시 초기화해야 한다. int &m_nData; }; // 사용자 코드 int _tmain(int argc, _TCHAR* argv[]) int a = 10; CRefTest t(a);
생성자 다중정의 및 위임 생성자 위임은 C++11 표준부터 지원한다. 생성자에서 다른 생성자가 호출되도록 ‘위임’했다. class CMyPoint { public: CMyPoint(int x) cout << "CMyPoint(int)" << endl; ... } CMyPoint(int x, int y) // x 값을 검사하는 코드는 이미 존재하므로 재사용한다. : CMyPoint(x) private: int m_x = 0; int m_y = 0; }; 생성자에서 다른 생성자가 호출되도록 ‘위임’했다.
메서드 메서드의 종류와 특징 종류 일반 상수화 정적 가상 관련 예약어 - const static virtual this 포인터 접근 가능 불가능 일반 멤버 읽기 가능(제한적) 일반 멤버 쓰기 정적 멤버 읽기 정적 멤버 쓰기 특징 가장 보편적인 메서드 멤버 쓰기 방지가 목적 C의 전역 함수와 유사 상속 관계에서 의미가 큼.
this 포인터 작성 중인 클래스의 실제 인스턴스에 대한 주소를 가리키는 포인터 휴대폰의 시리얼 번호로 생각하면 이해하기 쉽다. 메서드가 호출될 때 호출자 코드에 선언된 인스턴스의 주소가 함께 전달되며 이 값으로 this 포인터가 초기화 된다. // 사용자의 코드 int main(void) { USERDATA user = { 20, "철수", PrintData }; // printf("%d, %s\n", user.nAge, user.szName); // 1단계 // PrintData(&user); // 2단계 user.Print(&user); // 3단계 return 0; }
하지만 이렇게 생각하면 this 포인터를 정확히 이해하기 쉽다. 다음 코드는 불가능하다. 하지만 이렇게 생각하면 this 포인터를 정확히 이해하기 쉽다. class CMyData { public: CMyData(int nParam) : m_nData(nParam) { }; void PrintData(CMyData *pData) CMyData *this = pData; cout << this->m_nData << endl; } ... }; int _tmain(int argc, _TCHAR* argv[]) CMyData a(5); a.PrintData(&a); …
멤버 변수에 읽기 접근은 가능하지만 쓰기는 허용되지 않는다. 그러나 mutable로 선언한 멤버는 쓰기 허용된다. (예외) 상수형 메서드 멤버 변수에 읽기 접근은 가능하지만 쓰기는 허용되지 않는다. 그러나 mutable로 선언한 멤버는 쓰기 허용된다. (예외) class CTest { public: ... // 상수형 메서드로 선언 및 정의했다. int GetData() const // 멤버 변수의 값을 읽을 수는 있지만 쓸 수는 없다. m_nData = 20; SetData(20); return m_nData; } int SetData(int nParam) { m_nData = nParam; } private: int m_nData = 0; };
정적 멤버는 사실상 전역 변수나 함수로 생각하는 것이 좋다. 정적 멤버는 인스턴스 선언 없이 호출할 수 있다. : 예) CTest::PrintData(); 정적 메서드는 this 포인터가 없다. 정적 변수는 반드시 선언과 정의를 분리한다. 정적 변수는 ‘동시성’(예를 들어 멀티스레드 기반 프로그램)을 지원하지 못해 문제가 발생하므로 꼭 필요한 경우에만 제한적으로 사용한다.