AcceleratedC++/Chapter11 | AcceleratedC++/Chapter13 |
1. Chapter 12 Making class objects act like values ¶
일반적인 C++의 기본형 데이터처럼 클래스도 여러가지의 연산자를 재정의 함으로써 마치 값처럼 동작하도록 할 수 있다.
이 장에서는 여러가지 연산자, 형변환을 클래스의 제작자가 제어하는 방법에 대해서 배운다.
12장에서는 string 클래스의 클론 버전인 Str 클래스를 제작한다. Str 클래스는 string 클래스의 형변환과 연산자의 구현에 초점을 맞추어서 제작한다.
이 장에서는 여러가지 연산자, 형변환을 클래스의 제작자가 제어하는 방법에 대해서 배운다.
12장에서는 string 클래스의 클론 버전인 Str 클래스를 제작한다. Str 클래스는 string 클래스의 형변환과 연산자의 구현에 초점을 맞추어서 제작한다.
1.1. 12.1 A simple string class ¶
~cpp class Str { public: typedef Vec<char>::size_type size_type; Str() {} Str(size_type n, char c):data(n, c) { } Str(const char* cp) { std::copy(cp, cp+std::strlen(cp), std::back_inserter(data)); } template<class In> Str(In b, In e) { std::copy(b, e, std::back_inserter(data)); } private: Vec<char> data; }
만약 생성자가 하나라도 존재하면 컴파일러는 암시적 기본 생성자를 만들지 않는다. 따라서 아무런 인자를 받지 않는 기본 생성자를 만들 필요가 있다.
이 클래스는 복사 생성자, 대입 연산자, 소멸자가 하는 모든일을 Vec클래스에 일임한다.
이 클래스는 복사 생성자, 대입 연산자, 소멸자가 하는 모든일을 Vec클래스에 일임한다.
1.2. 12.2 Automatic conversions ¶
~cpp Str s("hello"); // s를 생성한다. Str t = "hello"; // t를 초기화 한다. s = "hello"; // 새로운 값을 s에 대입한다.
클래스는 기본적으로 복사 생성자, 대입 연산자의 기본형을 제공한다. 위의 클래스는 이런 연산에 대한 기본적인 요건을 만족하기 때문에 const char* 가 const Str& 로 변환되어서 정상적으로 동작한다.
상기의 클래스에는 Str(const char*) 타입의 생성자가 존재하기 때문에 이 생성자가 Str 임시 객체를 생성해서 마치 사용자 정의 변환(user-define conversion)처럼 동작한다.
상기의 클래스에는 Str(const char*) 타입의 생성자가 존재하기 때문에 이 생성자가 Str 임시 객체를 생성해서 마치 사용자 정의 변환(user-define conversion)처럼 동작한다.
~cpp s="hello" s=Str("hello"); // 의 임시객체를 만들어서 만들어진 임시객체가 디폴트 복사 생성자를 통해서 할당되게 된다.
1.3. 12.3 Str operations ¶
string 에서 가능했던 연산들
~cpp cin>>s; cout<<s; s[i]; s1 + s1
operator[] implementation
~cpp class Str{ public: // 이전의 생성자들 char& operator[](size_type i) { return data[i]; } const char& operator[](size_type i) const { return data[i]; } private: Vec<int> data; };이 구현의 세부적인 작동방식은 모두 Vec 클래스로 위임하였다. 대신에 const 클래스와 const 가 아닌 클래스에 대한 버전을 제공하였고, 표준 string 함수와의 일관성 유지를 위해서 string 대신에 char& 형을 리턴하도록 하였음.
1.3.1. 12.3.1 입출력 연산자들 ¶
입력 연산자는 일견 객체의 상태를 바꾸기 때문에 멤버함수로 선언이 되어야 한다고 생각하기 쉽다. 그러나 이항 연산자의 경우 파라메터의 맵핑이 좌항의 경우 첫번째 우항의 경우 두번째인자로 받는데, 이렇게 될 경우 멤버함수로 >>연산자를 오버로딩하면 우리가 워하는 결과를 얻을 수 없다.
~cpp cin>>s; cin.operator>>(s); // istream 을 우리가 만든 객체가 아니기 때문에 재정의할 수 없다. s.operator>>(cin); // 표준 istream 의 형태가 아니다. s>>cin상기와 같은 이유로 operator>>는 비멤버함수로서 선언이 되어야 한다.
~cpp std::istream& operator>>(std::istream&, Str&); std::ostream& operator<<(std::ostream&, const Str&); ostream& operator<<(ostream& os, const Str& s) { for(Str::size_type i=0; i!= s.size(); ++i) os<<s[i]; return os; }위의 식 처럼 사용하기 위해서는 Str::size() 가 정의 되어야한다.
~cpp class Str{ public: size_type size() const { return data.size(); } // 이전과 동일 }
1.3.2. 12.3.2 프렌드(Friend) ¶
~cpp istream& operator>>(istream& is, Str& s) { s.data.clear(); // compile error. private 멤버로 접근 char c; while(is.get(c) && isspace(c)) // 입력이 있고 값이 공백이라면 무시 ; if(is) { // EOF를 만나면 입력 스트림을 false 값을 리턴한다. do s.data.push_back(c); // compile error. private 멤버로 접근 while(is.get(c) && !isspace(C)); if(is) // 입력중 공백 문자를 만났을 경우 방금전에 입력으로 들어왔던 한문자를 무시한다. is.unget(); } return is; }상기의 함수는 Str 자료형에 입력을 하기 때문에 Str 형에 대한 쓰기 권한이 필요하다. 그러나 9.3.1절처럼 단순히 입력 함수를 만들게 되면 일반 사용자가 객체의 내부 구조를 건드릴 수 있는 인터페이스를 제공하는 꼴이 되기 때문에 옳지 못하다.
따라서 우리는 operator>>를 public 멤버로 만들고 data에 대한 쓰기 권한을 가지게 해서는 안된다.
이런 경우의 함수를 friend 로 정의 하여 해결하는 것이 가능하다.
함수를 friend 로 정의하면 인자로 받은 형에대해서는 형의 접근자가 무시된다.
~cpp class Str { friend std::istream& operator>>(std::istream&, Str&); };friend 함수는 접근제어 레이블에 영향을 받지 않기 때문에 어디에 선언을 해도 무관하나, 가능하면 클래스 선언의 최초 부분에 놓는 것이 좋다.
1.3.3. 12.3.3 다른 이항 연산자들 ¶
operator+ 구상하기
~cpp s1 + s2 + s3;
- operator+는 각 인수로 받는 객체의 값을 변화시키지 ㅤㅇㅏㅎ는다. 따라서 비멤버함수로 구현하는 것이 적당하다.
- operator+는 연산의 결과는 Str 형을 리턴해야한다.
~cpp Str operator+(const Str&, const Str&);
그리고 operator+=() 역시도 구현해야할 필요가 있다.
이런 식을 만족하는 Str선언은 다음과 같다.
~cpp class Str { // input operator implemented in 12.3.2/216 friend std::istream& operator>>(std::istream&, Str&); public: Str& operator+=(const Str& s) { std::copy(s.data.begin(), s.data.end(), std::back_inserter(data)); return *this; } // as before typedef Vec<char>::size_type size_type; Str() { } Str(size_type n, char c): data(n, c) { } Str(const char* cp) { std::copy(cp, cp + std::strlen(cp), std::back_inserter(data)); } template <class In> Str(In i, In j) { std::copy(i, j, std::back_inserter(data)); } char& operator[](size_type i) { return data[i]; } const char& operator[](size_type i) const { return data[i]; } size_type size() const { return data.size(); } private: Vec<char> data; }; // output operator implemented in 12.3.2/216 std::ostream& operator<<(std::ostream&, const Str&); Str operator+(const Str&, const Str&);
상기에서 구현된 operator+= 는 Vec를 copy함수를 이용해서 구현하였다.
operator+는 operator+=를 이용해서 다음과 같이 간단하게 구현이 가능하다.
~cpp Str operator+(const Str& s, const Str& t) { Str r =s; r += t; return r; }
지역변수로 생성된 r를 복사생성자를 통해 생성된 임시 객체로 리턴시킨다.
1.3.4. 12.3.4 혼합-타입 표현식(Mixed-type expression) ¶
~cpp Str greeting = "Hello, " + name + "!";상기의 문장은 다음과 동일한 순서로 동작하게 된다.
~cpp Str temp1("Hello, "); Str temp2 = temp1 + name; Str temp3("!"); Str greeting = temp2 + temp3;이런식으로 동작하게 하면 임시 변수의 생성으로 인한 오버헤드가 상당함으로 알 수 있다. 이런 문제를 해결하기 위해서 string 클래스는 자동변환에 의존하지 않고, 피연산자들의 모든 조합에 대해 결합 연산자를 제공한다.
1.3.5. 12.3.5 이항 연산자 설계하기 ¶
이항연산자를 설계할 경우에는 변환(conversion)이 많은 도움이 된다.
이항연산자는 비 멤버함수로 설계하는 것이 좋다. 이유는 멤버함수의 경우 첫번째 인자가 객체의 특정형으로 고정되기 때문에 자동 형변환을 이용할 수 없기 때문이다. 즉 대칭성(symmetry)를 유지하는 것이 가능하다.
멤버함수로 이용되는 경우 우항 피연산자가 자동 형변환되어 좌항과 일치하지 않는 것과 같은 문제가 발생할 수 잇다.
반면 대입연산자는 연산의 결과가 특정 객체에 영향을 주어야 하기 때문에 반드시 클래스의 멤버함수로 작성하는 것이 옳다.
이항연산자는 비 멤버함수로 설계하는 것이 좋다. 이유는 멤버함수의 경우 첫번째 인자가 객체의 특정형으로 고정되기 때문에 자동 형변환을 이용할 수 없기 때문이다. 즉 대칭성(symmetry)를 유지하는 것이 가능하다.
멤버함수로 이용되는 경우 우항 피연산자가 자동 형변환되어 좌항과 일치하지 않는 것과 같은 문제가 발생할 수 잇다.
반면 대입연산자는 연산의 결과가 특정 객체에 영향을 주어야 하기 때문에 반드시 클래스의 멤버함수로 작성하는 것이 옳다.
1.4. 12.4 Some conversions are hazardous ¶
11.2.2 에서의 explicit 키워드의 사용은 자동 형변환에 이용이 되는 단일 인자를 받는 생성자의 행위를 제한함으로써 프로그래머가 원치 않는 변환을 막는 기능을 한다.
만약 explicit 으로 Vec 클래스의 Vec<T>::Vec(size_type n) 을 정의 했다고 가정하고 다음의 코드를 보자
실행 : 42개의 빈 행에 테두리가 그려진 결과
만약 explicit 으로 Vec 클래스의 Vec<T>::Vec(size_type n) 을 정의 했다고 가정하고 다음의 코드를 보자
~cpp Vec<string> p = frame(42);원래의 의도 : 42라는 글자에 테두리가 처진 결과
실행 : 42개의 빈 행에 테두리가 그려진 결과
일반적으로 객체의 구조를 결정하는 생성자는 explicit 으로 내용을 구성하는 경우에는 암묵적인 실행이 가능하도록 하는 것이 일반적이다.
Vec의 경우처럼 size_type 을 인자로 받는 경우에는 요소의 개수라는 구조를 결정하기 때문에 explicit 이 적당하다.
Vec의 경우처럼 size_type 을 인자로 받는 경우에는 요소의 개수라는 구조를 결정하기 때문에 explicit 이 적당하다.
1.5. 12.5 Conversion operators ¶
클래스의 제작자들은 명시적으로 변환 연산자(conversion operator)를 정의하여 그 객체의 타입이 목적인 객체의 타입으로 변환하는 것을 정하는 것이 가능하다.
변환 연산자의 모양은 operator 목적이 되는 타입의 이름으로 정의된다.
변환 연산자의 모양은 operator 목적이 되는 타입의 이름으로 정의된다.
~cpp class Student_info { public: operator double(); // ... } ... vector<Student_info> vs; ... double d = 0; d += vs[i]; // 이와 같이 형변환을 할 수 있다.
이런 자동 형변환이 이용된 표준 라이브러리로는 cin 들 수 있다.
그런데 istream 클래스는 istream::operator void*()를 정의하여 만약 입력에 문제가 있으면 void* 형으로 0을 그렇지 않은 경우에는 void* 를 리턴하게 함으로써 마치 bool 형처럼 사용하는 것이 가능하다.
void*로 리턴값을 정함으로써 bool 로 정했을 때 나타나는 자동형 변환(정수형으로의) 문제점을 해결할 수 있다.
~cpp if (cin>>x) { /* ... */ } cin>>x if (cin) { /* ... */ }이경우 istream 은 void *를 리턴하게 된다.
그런데 istream 클래스는 istream::operator void*()를 정의하여 만약 입력에 문제가 있으면 void* 형으로 0을 그렇지 않은 경우에는 void* 를 리턴하게 함으로써 마치 bool 형처럼 사용하는 것이 가능하다.
void*로 리턴값을 정함으로써 bool 로 정했을 때 나타나는 자동형 변환(정수형으로의) 문제점을 해결할 수 있다.
void* 타입의 포인터는 어떤 객체라도 가리킬 수 있는 포인터 이므로 유니버셜 포인터(universal point)라고 부른다. 그러나 그러나 포인털르 역참조하는 것은 불가능하다. 그러나 bool 타입으로의 변환은 가능하다.
1.6. 12.6 Conversions and memory management ¶
char*, const char* 형으로의 변환
그렇다고해서 data가 가리키는 포인터를 바로 넘기면 프로그램에서 그 포인터를 통해서 데이터의 수정을 할 수 잇기 때문에 캡슐화의 장점이 사라진다.
이 것을 해결할 방법은 data의 복사본을 만들어서 그 것을 리턴하고 사용이 끝난 뒤에는 복사본을 제거하는 방식으로 해결이 가능하다.
그러나 사용자는 암묵적 변환사이에서 소멸 시켜야할 포인터를 찾을 수 없다.
~cpp class Str{ public: operator char* (); operator const char* () const; // 이전과 동일 private: Vec<char> data; }; Str s; ifstream in(s);하단의 코드가 올바르게 동작하지 못한다. 변환되는 형이 요구되는 형과 전혀 맞지 않기 때문이다.
그렇다고해서 data가 가리키는 포인터를 바로 넘기면 프로그램에서 그 포인터를 통해서 데이터의 수정을 할 수 잇기 때문에 캡슐화의 장점이 사라진다.
이 것을 해결할 방법은 data의 복사본을 만들어서 그 것을 리턴하고 사용이 끝난 뒤에는 복사본을 제거하는 방식으로 해결이 가능하다.
그러나 사용자는 암묵적 변환사이에서 소멸 시켜야할 포인터를 찾을 수 없다.
표준 string 의 경우에는 3가지 종료의 char* 형으로의 변환을 제공하는데
c_str() | string 내부의 char*를 리턴한다. 따라서 사용자가 delete를 할 수는 없지만 포인터를 얻어서 수정할 수 있다. |
data() | c_str()과 비슷하지만 '\0'로 종료되는 문자열이 아닌 배열을 리턴한다. |
copy(char* ) | 인자로 받은 char*의 공간에 내부의 문제들을 복사해 넣는다. char*공간은 프로그램가 할당하고 해제하는 공간이다. |