chapter 05. 클래스 완성
01. 복사 생성자 복사 생성(Copy Construction) 생성될 때 자신과 같은 타입의 객체를 변수로 받아, 이 객체와 같은 값을 갖는 새로운 객체를 생성하는 것 명시적인 생성 과정뿐만 아니라 인자의 전달, 함수의 수행 결과 반환 등에서도 발생함. C++언어에서는 사용자 정의 타입을 포함한 모든 타입의 인스턴스에 대해 복사 생성을 허용함. 복사 생성을 이용하면 생성과 동시에 값의 복사가 이루어지므로 번역기에 따라 좀 더 나은 실행성능을 기대할 수 있으며, 직관적인 프로그래밍이 가능함. 복사 생성을 이용하지 않는 코드 int data1(10); int data2; data2 = data1; 복사생성을 이용한 코드 int data2(data1);
기본 복사 생성자 기본 복사 생성자의 개념 사용자가 별도로 지정하지 않으면 자동으로 정의됨. 자동으로 생성된 복사생성자를 이용하면 클래스 내의 모든 멤버에 일일이 복사(Meberwise copy)가 이루어짐 각 멤버가 복사되는 순서는 클래스가 생성될 때 각 멤버가 초기화되는 순서와 동일함. 기본 복사 생성자의 예 struct X { int m1; long m2; }; X f ( X o1 ) o1.m1 = 10; o1.m2 = 5; X o2( o1 ); // 복사 생성 cout << o2.m1 << ", " << o2.m2 << endl; // print : 10, 5 f( o2 ); // 복사 생성 return 01; // 복사 생성 }
기본 복사 생성자의 문제점 메모리 관련 문제의 예 기본 복사 생성은 각 멤버를 그대로 복사해 올 뿐 이므로, 이에 합당하지 않는 복사 과정이 필요할 경우 문제가 발생함. 메모리 관련 문제의 예 b.p가 소멸된 후 a.p가 가리키던 공간도 같이 사라짐. 메모리 관련 문제의 예 class X { public: X(): p(new int){} ~X(){ delete p; } private: int *p; }; void f () X a; X b(a); } int i = *a.p; // 에러
사용자 정의 복사 생성자 1 X 클래스의 복사생성자의 예 복사 생성자의 특성 복사 생성자가 아닌 예 X::X(const X &); X::X(X &, int=10, char=‘a’); X::X(volatile X &); 복사 생성자가 아닌 예 X::X(const int, X &); X::X(X *); X::X(X const *); 복사 생성자의 특성 생성자의 일종으로 인자에 강한 제약을 두고 있음. 복사 생성자의 인자는 자신의 타입에 대한 레퍼런스가 되어야 하며 추가로 const, volatile, const volatile등의 한정된 조건이 올 수 있음. 복사생성자는 클래스의 외부에서 호출이 가능해야 하므로 public 영역에 정의되어야 함. X라는 클래스의 복사 생성자의 첫번째의 인자로는 X &, const X &, volatile X &, const volatile X & 등이 올 수 있으며 const X &의 형식이 가장 일반적인 형식이다
사용자 정의 복사 생성자 2 ex 클래스의 사용자 정의 복사 생성자의 예 class ex { public: ex(const ex &obj) value = obj.value } private: int value; }; 중요한 세 개의 법칙(The Law Of Big Three) 셋 중 어느 하나에 대한 사용자 정의가 필요하다면 나머지 둘 역시 직접 정의해야 한다. 사용자가 복사 생성자를 정의할 경우, 소멸자나 대입 연산자를 추가로 정의해야 올바른 사용을 유도할 수 있는지를 고려해야 함. 일부의 경우를 제외하고 복사 생성자, 소멸자, 대입 연산자 중에 어느 한 가지를 특별히 정의한 경우 나머지 두 개도 역시 정의해야 하는 경우가 많음.
복사 생성자를 포함한 Array 클래스 생성자 소멸자 정의 복사 생성자 정의 Array 클래스의 기본 구조 ex_array::ex_array( size_t sz ) : size( sz ) , p( new int[ size ] ) { } ex_array::~ex_array() { delete [] p; } 복사 생성자 정의 ex_array::ex_array( const ex_array &a ) : size( a.sz ), p( new int[ size ] ) { for (i = 0; i < a.size; i++) { p[i] = a.p[i]; } } Array 클래스의 기본 구조 class ex_array { public: ex_array( size_t ); ex_array( const ex_array & ); ~ex_array(); void put( size_t, int ); int get( size_t ); private: size_t size; int *p; };
02. 프렌드 프렌드의 개념 클래스에서 접근 지정을 통해 외부에 공개하지 않은 멤버라 할지라도 접근할 수 있도록 하는 것. 클래스나 함수를 프렌드로 선언하면 프렌드로 선언된 함수나 클래스는 프렌드를 선언한 클래스의 내부와 마찬가지로 모든 멤버에 대해 자유로이 접근이 가능해짐. 프렌드의 장점 클래스를 사용하는 측면에서 추가적인 함수의 호출이 필요없으므로 나은 성능을 기대할 수 있다. 프렌드의 단점 객체지향의 입장에서 클래스의 캡슐화에 악영향 클래스간의 의존도를 높혀 클래스의 변화에 매우 민감하게 작동됨.
프렌드의 선언 1 프렌드 클래스의 선언 예 프렌드의 선언방법 접근지정자에 관계없이 클래스의 어디에서나 선언이 가능. class tv { // 프렌드 선언 friend class remote_controller; public: size_t channel_control( size_t num ) { return channel = num; } size_t volume_control( size_t v ) { return volume = v; } size_t curr_channel() { return channel; } size_t curr_volume() { return volume; } private: size_t channel; size_t volume; }; 프렌드의 선언방법 접근지정자에 관계없이 클래스의 어디에서나 선언이 가능. 프렌드로 선언하고자 하는 멤버의 앞에 friend 키워드를 붙여서 선언. 프렌드로 선언하기 위한 클래스나 함수는 그 존재를 선언된 지역에서 알 수 있어야 함. 명시적으로 클래스나 함수가 선언되기 전에 프렌드로 선언하는 경우에는 extern과 동일한 효과
프렌드의 선언 2 프렌드 멤버 함수 프렌드 함수의 선언 예 클래스 전체가 아닌 클래스의 특정 멤버 함수만을 프렌드로 선언 프렌드로 선언하기 위한 멤버 함수 앞에 friend 키워드를 붙임 프렌드 함수 외부 함수라 할지라도 클래스의 모든 멤버에 접근이 가능함 일반 함수를 선언하는 경우에도 동일한 방법으로 friend를 함수의 선언 앞에 붙임 프렌드 함수의 선언 예 class X { friend void f ( X ) ; int data; }; void f ( X x ) x.data ++ ; }
프렌드의 선언 3 프렌드 멤버 함수의 선언 예 class tv; public: class remote_controller { public: remote_controller( tv & ) ; void channel_control( size_t ) ; void volume_control( size_t ) ; private: tv &my_tv; }; class tv // 프렌드 선언 friend void remote_controller::channel_control( size_t ); friend void remote_controller::volume_control( size_t ); public: size_t channel_control( size_t num ) { return channel = num; } size_t volume_control( size_t v ) { return volume = v; } size_t curr_channel() { return channel; } size_t curr_volume() { return volume; } private: size_t channel; size_t volume; }; remote_controller::remote_controller( tv &target ) : my_tv( target ) {} void remote_controller::channel_control( size_t num ) { my_tv.channel = num; } void remote_controller::volume_control( size_t vol ) { my_tv.volume = vol; }
프렌드 관계의 속성 1 비 반사성 비 상속성 프렌드의 관계는 방향성을 가지고 언제나 한쪽으로만 흐름. 클래스 A가 클래스 B를 프렌드로 선언했다고 해서, 클래스 B의 프렌드가 클래스 A는 아님. class A; class B { void set ( A &, int ) ; int value; }; class A friend class B; void set ( B &, int data ) { b.value = data; } // 에러 } void B::set ( A &a, int data ) { a.value = data; } // OK 비 상속성 프렌드 관계는 상속되지 않음 클래스 B가 클래스 A를 상속받아 정의되어도, 클래스 B의 프렌드로 선언된 클래스나 함수 C에서 클래스 A의 가려진 멤버를 접근할 수 없음. class A { int value; }; class B : public A { friend void set (B &b); int data; }; void set (B &b) b.data = 10; // OK b.value = 10; // 에러 }
프렌드 관계의 속성 2 비 전이성 프렌드 관계는 전이되지 않음. 클래스 B가 클래스 A의 프렌드로 선언되어 있고, 클래스 C가 클래스 B의 프렌드로 선언되어 있다고 하더라도 클래스 C가 클래스 A의 프렌드는 아님. class A { friend class B int value; }; class B friend class C; class C int set ( A &a, B &b, int data) b.value = data; // OK a.value = data; // 에러 }
상호 프렌드와 공유 프렌드 상호 프렌드 관계 공유 프렌드 관계 프렌드의 방향성을 극복하고 두 클래스간에 서로의 내용을 참조하기 위해 양방향에서 서로를 프렌드로 선언하는 것. 상호 프렌드 관계로 설정되어 있는 경우 두 개의 클래스는 상대 클래스의 private 멤버를 클래스 내부의 멤버와 마찬가지로 접근이 가능해 짐. class A { friend class B; // … }; class B friend class A; 공유 프렌드 관계 두 클래스 전체가 서로를 참조할 필요는 없으나 특정 함수나 클래스를 통해 두 클래스 모두에 접근하기 위한 관계 별도의 두 클래스에 있는 개별 데이터에 접근하기 위한 목적으로 사용됨. class X { friend void f( X &, const Y& ); // … }; class Y void f( X& x, const Y& y ){ … }
03. 내장 클래스 내장 클래스(Nested Class) 다른 클래스에 포함되는 클래스로 내장 클래스는 자신을 감싸고 있는 클래스를 통하여 접근할 수 있음. 내장 클래스는 상위 클래스를 통해서만 접근이 가능하므로 범위 연산자를 통해서 접근해야 함. 내장 클래스는 일반적인 접근 권한이 그대로 적용됨. 내장 클래스의 사용 범위 특성 선언된 위치 클래스 내 파생 클래스 외부 public ○ protected ⅹ private
내장 클래스를 이용한 큐의 구현 1 문제정의 내장 클래스를 이용하여 큐를 구현하라. 큐는 선입선출(FIFO, First-In First-Out)의 특성을 가지는 자료구조로 각 데이터는 노드에 저장되고, 큐는 이 노드를 통해 데이터 접근한다. 큐는 front와 rear라는 두 개의 포인터를 이용해서 데이터를 저장, 삭제할 수 있으며 다음과 같은 기본 연산들이 가능하도록 구현하라. 큐의 기본 연산 새로운 큐 생성 큐 삭제 새로운 데이터를 맨 뒤에 삽입 맨 앞의 데이터 제거
내장 클래스를 이용한 큐의 구현 2 queue 클래스와 node 클래스의 선언 인터페이스 함수의 선언 class queue { private : struct node int item; node *next; node( int i ) : item( i ), next( 0 ) { } }; private: node *front; // 큐의 front 포인터 node *rear; // 큐의 rear 포인터 size_t items; size_t qsize; … 인터페이스 함수의 선언 class queue { // … public: bool empty () { return items == 0; } bool full() { return items == qsize; } size_t size() { return items; } // 큐에 새로운 데이터를 넣음. bool push( int item ); // 큐에서 데이터를 꺼냄. bool pop( int &item ); };
내장 클래스를 이용한 큐의 구현 3 push 함수의 구현 pop 함수의 구현 bool queue::push( int item ) { // 꽉찾으면 실패 if ( full() ) return false; // 새로운 노드 생성 node *add = new node( item ); // 비어 있는가? if ( front == 0 ) // 처음 front = add; else // 새로운 노드 추가 rear->next = add; // 끝 rear = add; // 노드의 수 증가 items++; return true; } pop 함수의 구현 bool queue::pop( int &item ) { // 비어 있는가? if ( front == 0 ) return false; // 데이터 획득 item = front->item; // 노드의 수 감소 items--; // 맨 앞 노드에 대한 포인터 임시 저장 node *temp = front; // 다음 노드로 front 이동 front = front->next; // 저장된 맨 앞 노드 제거 delete temp; // 노드가 더 이상 남아 있지 않다면 rear에 널 대입 if ( items == 0 ) rear = 0; return true; }
04. 클래스와 static, const, mutable 개별적인 객체들과 관계없이 전역 변수처럼 클래스 당 하나의 인스턴스만을 생성하고, 모든 객체가 이를 공유하도록 만듬. 전역변수와 달리 클래스에 속하게 되고, 접근 지정의 영향 역시 동일하게 적용 받음. 캡슐화를 깨지 않으면서 동일한 정보를 모든 클래스의 인스턴스에서 공유할 수 있도록 함. Static 멤버의 선언 예 struct X { X(); static int n1; // static 데이터 멤버의 선언 static void f ( X ) ; // static 멤버 함수의 선언 void g () ; int n2; }; static 데이터 멤버의 특성 static 데이터 멤버는 인스턴스와 독립적으로 움직이므로, 초기화 목록을 통해 생성지정을 할 수 없다. static으로 정의된 함수 내에서는 static으로 지정되지 않은 다른 데이터 멤버나 멤버 함수를 사용할 수 없다. 인스턴스를 전혀 가지고 있지 않은 상태에서도 클래스를 통해 static 멤버에 대한 접근이 가능하다.
const 한정자 const 한정자의 특성 const 멤버의 선언 예 cosnt한정자는 데이터 멤버와 멤버 함수 모두에 적용할 수 있으며, 멤버 함수에 적용될 때는 함수 선언 맨 끝에 const 키워드를 붙임. Const 데이터 멤버는 그 자체가 인스턴스 값의 일부이므로 변경할 수 없음. Const 데이터 멤버를 가진 경우에는 반드시 생성자를 명시적으로 정의해야 함. Const로 한정된 멤버 함수의 경우에는 this포인터를 통해 자신을 변경시키는 어떤 일도 할 수 없다. 인스턴스가 const로 한정된 것이라면, 이 인스턴스를 통해서는 일반 멤버 함수는 호출할 수 없게 된다. const 멤버의 선언 예 struct X { // … int j; // 일반 데이터 멤버 int g(); // 일반 멤버 함수 const int i; // const 데이터 멤버 void f ( int ) const; // const 멤버 함수 void h ( int ) const; // const 멤버 함수 };
mutable 지시자 mutable 지시자의 특성 mutable의 사용예 저장 부류 지시자(Storage Class Spefier)의 일종 지정된 데이터 멤버가 자신이 속한 인스턴스의 const 속성이 해당 데이터 멤버에 적용되지 않게 하는 역할을 함. mutable로 지정된 멤버는 const 인스턴스라 할지라도 변경이 가능하게 const 효과를 무효화(Nullify)하는 역할을 함. mutable 속성은 클래스의 데이터 멤버에 대해서만 사용이 가능하고 const나 static과 함께 사용하거나 레퍼런스 멤버에 대해서 사용할 수 없다. mutable의 사용예 struct X { mutable size_t l; size_t m; }; void f() X x; const X &r; r.l = 10; r.m = 10; // 에러 }
05. explicit 지시자 explicit의 특성 explicit의 사용 예 암시적인 변환(implicit conversion)에서 발생할 수 있는 오류를 방지하기 위해 암시적인 변환이 일어나지 않도록 한정함. explicit의 사용 예 struct X { explicit X( int ); explicit X( const char * ); }; void f( X x ); void g( int &xx ) X x(1); // OK X y = 1; // 에러 X z = "test"; // 에러 x = 3; // 에러 y = "test"; // 에러 f( 4 ); // 에러 f( "test" ); // 에러 }