Chapter 11. Working with Classes

지환·2022년 7월 10일
0

C++이 워낙 특징이나 기억할게 많은 언어이다. 모든 특징을 매번 사용할 순 없으니 기억하기가 쉽지 않을 수 있다.
맘 편히 가지고 계속 연습하고 사용하면 된다.
(이 책 보고 accelerated 보면서 쭉 코드짜봐야지)

이 파트에서 자주 나오길래 언급하는데,
OOP의 장점이 implementation specific에 신경안쓰고 사용할 수 있단 것이다.
내부 구현만 알아서 잘 해놓으면, 그런거 신경안쓰고 interface 따라서 쓸 수 있는게 OOP의 장점이다.


Operator Overloading

사실은 이미 C와 C++의 많은 operator들은 overloading돼있다. (ex. *)
operator overloading에선 operands의 수와 type을 이용해 어떤 action을 취할지 결정한다.
(function overloading에서 signature을 이용하듯이..)
C++에선 아래 방법으로 user-defined type에 operator overloading을 하도록 해준다.

operator function
operatorop(argument-list)
여기서 op는 operator symbol인데, valid C++ operator여야한다.(새로운걸 만들 순 없다.)

member 함수로서 정의할 수도 있고, non-member 함수로서 정의할 수도 있다.
어쨌든 이렇게 operator function을 정의해두면,
district2 = sid + sara;(sid와 sara는 ABC class의 object라고 가정)
위 expression에서 compiler가 두 operands가 class object임을 인지하고
district2 = sid.operator+(sara); 혹은 district2 = operator+(sid, sara); 함수 호출로 대체한다.(이렇게 직접 호출해도 OK)
(member함수로 구현됐으면 전자, non member함수로 구현됐으면 후자를 호출함.)

Overloading Restrictions

  1. operands 중 최소 하나는 user-defined type이어야 한다.
    (즉, standard type의 operator는 overloading하지 못한다.)

  2. 본래 operator의 syntax rule을 깨지 못한다.
    예를들어, % operator를 operand 하나짜리로 못만들고, precedence도 변경하지 못한다.

  3. 새로운 operator symbol을 사용하지 못한다.

  4. 아래 overloading이 가능한 operator와 가능하지않은 operator가 있다.
    가능하지 않은 operator에는 추가로, typeid, const_cast, dynamic_cast, reinterpret_cast, static_cast가 있다.

  1. 아래를 제외하곤, non-member function으로도 operator overloading을 구현할 수 있다.
    =, (), [], -> : 얘네는 무조건 member function으로 overload해야함.
    non member 함수로는 operator overloading을 아래와 같이 구현한다.
    Time operator*(double m, const Time & t);
    member 함수에서도 순서가 있었듯이, 얘도 operand에 순서가 있다.
    a+b라면 왼쪽이 첫번째, 오른쪽이 두번째 인자로 들어간다.(일치해야함.)
    (member 함수는 인자 순서가 정해져있기때문에 그걸 임의로 변경하고싶으면 이 방법이 좋다.)
    (혹은 이미 class가 정의돼있어서 그 멤버함수로 들어갈 수 없는 경우에는 이 방법밖에 없을 것이고)
추가로, overloading도 센스있게 의미에맞게 해야된다.
뜬금없이 `*`을 swap 연산 operator로 overloading하면 헷갈리기만 함.
그럴땐 그냥 Swap() 함수 그대로 쓰는게 더 낫다.

예시 (멤버 함수로 구현)

//시간 덧셈을 구현해보자.
class Time {
private:
	int hours;
    int minutes;
public:
	Time();
    Time(int h, int m = 0);
    . . .
    Time operator+(const Time & t) const;
    //c structure랑 다르게 내부에서 본인 type 언급하는게 크게 제약은 없나보네
    //가 아니고 c 구조체도 tag써서 저렇게 앞에 이름 보여주면 쓸수있잖아..
};
Time Time::operator+(const Time & t) const {	//reference로 받음으로써 speed up
	Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

operator overloading을 위해prototype과 definition 모두 함수 이름 부분을 operator+ 해준다.

그러면
1) total = coding.operator+(fixing); : 함수 직접 호출
2) total = coding + fixing; : operator 사용
이렇게 두가지를 모두 사용할 수 있다. (왼쪽 operand가 함수를 호출하고, 오른쪽이 매개변수로 들어간다.)

NON-member function으로 구현됐다면, class 내에 prototype이 있단 것 말곤 동일하다.
operator 사용해도되고, 함수 직접 호출해도되고..

  • paramter 개수? (p.588)
    Nonmember 함수라면 당연히 해당 operator의 operands 개수만큼 받아야한다.
    하지만 member 함수라면 Nonmember version보다 하나 더 적게 받아야한다.

  • 번외
    t4 = t1 + t2 + t3; 이런건 어떻게 하지? valid한가??
    Yes, t4 = t1.operator+(t2.operator+(t3)); 처럼 operator function을 그냥 여러번 사용하도록 한다.
    (계산순서가 거꾸로 되긴하네.. 일단 계산순서 바뀐다고 알고만 있자.)

  • 추가 내용
    (검색해보다가 알게된 것들 추가)
    단항자 overloading시 ++가 prefix인지 postfix인지 구분하려고
    Stock operator++() 이런 header는 prefix ++,
    Stock operator++(int) 이런 header는 postfix ++로 한다.
    대입 연산자는 default assignment operator가 있기 때문에 간단한 assign(copy)는 가능하다.
    하지만 이 경우 복사 기능만하므로, 이전의 추가 작업이 필요하면 따로 정의해주는게 좋다.
    여기 마지막에서 두번째 code 보면, assign할때 다른 한쪽에 담겨있던 문자열이 붕뜨기때문에, 그걸 없애주기위해 따로 overloading한다.
    (메모리 관리하기가 쉽지않네, 이 technic 기억하고 있자.)
    (destructor 자동 호출되지않나라고 생각할 수 있는데, ch10에서 assginment때 destructor 호출된건 temporary 없어질때였음)

Introducing Friends

friend는 세 종류가 있다.
1. Friend functions
2. Friend classes
3. Friend member functions
Friend functions에 대해 알아보자.(나머지 둘은 Ch15에서)

Friend Functions은 그냥 일반 함수인데 class의 private에 접근할 수 있다.
이 함수 내부 영역에선 private member에도 접근 할 수 있는 것이다.
예를들자면 이렇게 그냥 특정 일반 함수를 friend로 만듦으로써 접근 권한이 생긴다.
(friend라는게 class의 친구란 의미인 것 같다. 친구니까 접근권한 허용)
그런데 이렇게 되면 data를 보호하고자하는 class의 의미가 퇴색된다.
그렇기때문에 필요한 경우에 class 통제하에 잘 사용해야한다.(필요한 경우 예시는 아래에)

Creating Friends

  1. friend keyword를 prototype 앞에 붙여서 class내에 작성한다.
    class내에 있지만 member function은 아니다. 그렇기때문에 object에 의해 .으로 호출될 수 없다.
    하지만 멤버함수와 같은 접근권한은 갖는다.
    (p.599/p.1351)class scope에 속하지 않으므로, member에 접근하려면 .이나 ::를 사용해줘야한다.
    (접근권한만을 갖는 것 뿐이다.)
    private/public 노상관

  2. definition을 작성한다.
    앞에 friend keyword는 작성하지 않고, member 함수도 아니므로 Time:: 같은 qualifier도 필요없다.
    (나머지 일반 멤버함수나 constructor/destructor 모두 qualifier 필요함)

문법도 기존과 다를게 없다. 바깥의 함수는 원래 그거대로 그냥 존재하는 것이고,
class내부에서 "이 prototype인 함수는 우리 friend야"라고 알려주기만하는 것이다.

ex)
class Time{ friend Time operator*(double m, const Time & t); }; : prototype
Time operator*(double m, const Time & t) { ~ } : definition

이렇게 friend는 "접근 권한"만을 갖는 것이 전부이다.
그러나 특수한 경우에 잘 통제해서 사용해야한다.
이 뒤에는 이걸 적용할 수 있는 예시를 보여주는 것이다.

operator overloading에서의 friend function

언제 필요할까?
binary operator를 overloading할때 피연산자의 type이 다르면 문제가 생긴다.
(A와 B가 모두 Time class object라고 하고 다음을 보자.)
A = B * 2.5;를 하게되면 알다시피 left operand가 invoking object이고, right operand가 매개인자로 들어간다
그럼 A = 2.5 * B;를 하게됐을때 문제가 된다. 2.5는 object가 아니므로 멤버함수를 호출할 수 없다.

해결방법
1. 무조건 object를 먼저 적도록 (기억)한다. 하지만 이처럼 client에 짐(?)을 좀 넘기는건 OOP 방식이 아니다.
2. non-member function을 사용한다.
Time operator*(double m, const Time & t); 이렇게 non member 함수를 선언해두면, 해당 경우도 처리할 수 있다.
하지만 이 함수는 private 공간에 접근할 수 없다는게 문제이다.

이때 우리는 friend를 사용하는 것이다.
(딱히 별다른건 없고 접근권한만 해결해준다. 접근이 필요한 class에 friend로 들어가면 된다.)

  • ex) Time operator*(double m, const Time & t) { return t * m; }
    이렇게 순서만 바꿔서 기존의 operator overloading 멤버함수를 이용하기도한다.
    그래서 이 경우 private에 접근하지 않으므로 friend일 필요가 없지만, 그래도 friend로 만드는게 좋다.
    그래야 공식적으로 class interface에 속함으로써 그 class에 묶일 수 있고, 나중에 private에 접근할 일이 생겨도 이 함수만 수정하면 되기 때문이다.

<< operator overloading 해보자

cout은 이미 ostream class에서 operator<<()를 모든 경우에 대해 overload했기 때문에 거의 모든 type에 적용이 된다.
우리가 만든 type에도 적용해보자.
1. ostream class를 수정해서 overload하는건 위험하다.
2. 그러니 우리가 만든 class내에서 cout을 처리하도록 하자.

일반적인 member 함수의 operator overloading은 좌측 operand가 함수를 호출한다.
그러니 cout << 뭔가의object; 꼴을 유지하려면 그럼 위 방법은 안되고, 아래와 같이 구현한다.

prototype: friend ostream & operator<<(ostream & osm, const Time & t);
definition: ostream & operator<<(ostream & osm, const Time & t) { os << t.hours << ":" << t.minitues; }

  1. operands의 순서를 유지하기위해, non-member 함수로 구현한다.
  2. 이 함수는 Time class의 private에 접근하기때문에 friend 함수로도 해줘야한다.
  3. cout의 복사본이 아닌 그 자체가 쓰여야하므로(왜인진 잘 모르겠는데 복사해서쓰니까 안되네),
    reference로 받았다.(const reference도 안된다. 내부에서 수정하나? 잘모르겠네)
  4. paremeter보면 알겠지만, cout이 아니어도 ostream의 object면 된다.
    cerr 같은 애도 올 수 있다.(표준 error stream의 출력 경로)
순서를 보면, 일단 non member 함수로 operator overloading하고, friend일 필요가 있어서 그렇게 함.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
상기: a라는 파일에서 b를 include하고, c에서 a를 include하면 a에는 b도 include된 셈이다.

Member Versus Nonmember Functions

보다시피 대게 Nonmember overloading functions은 friend로 구현이 된다.
그럼 뭐가 더 낫나???
member function으로만 구현될 수 있는 operator(=,(),[],->)를 제외하곤 딱히 차이는 없다.
(nonmember 함수가 이점이 되는 경우가 가끔 있다고 함. ex. class의 type conversion 정의할때)


※ Friend Function 주의
답변 참고. class내에서 friend function을 선언하는 것은 해당 name을 scope에 추가하는 것이 아니라, 그냥 그런 name의 function이 friend라는 것을 알려주는 것이다.
그렇기 때문에 friend 함수를 사용하려면, class내의 frined가 붙은 prototype이 아닌, 우리가 알던 prototype 형태를 class 바깥에 제공해줘야 한다.
Q. 기존엔 잘 작동했는데?
A. 그건 사실 Argument Dependent Lookup(ADL)이란 놈 덕분이었다. 답변 참고
ADL의 detail 부분을 보면 알겠지만, ADL이란 특정 함수를 호출했을때 그 함수의 argument type을 보고서 그거에 맞는 namespace/class를 알아서 찾는 것이다.
:: 없이도 잘 작동한다. 예시
즉, 우리가 위에서 했던 operator overloading하는 friend 함수에서는 argument로 들어온 object를 보고서 그 class를 찾아 그 안의 friend prototype을 보고 잘 작동한 것이다.(이것도 결국 declaration이다. scope에 추가가 안될 뿐이지)
주의해야할 것이, argument를 보고서 (뭐 어떤 class의 object라면) class를 찾아가는 것이기 때문에 연관되는 argument가 없다면 위에서 말한대로 prototype을 제대로 제공해줘야한다.
ADL:wiki


More Overloading: A Vector Class

구현해보며 class design 측면에서 배울게 있다고 함

vector template class에서의 "vector"가 아니라 수학물리에서 말하는 vector를 구현
vector=magnitude+directionvector = magnitude + direction
한 class를 이용해 한 data를 동시에 두가지 표현법으로 나타낸다.
여기선 vector를 (1)좌표와 (2)극좌표를 이용해 나타낸다.

class declaration file

//vect.h
#ifndef VECTOR_H_
#define VECTOR_H_
#include <iostream>
namespace VECTOR {
	class Vector {
    public:
    	enum Mode {RECT, POL};
    private:
    	double x;        //좌표 표현 멤버
        double y;        //좌표 표현 멤버
        double mag;      //극좌표 표현 멤버
        double ang;      //극좌표 표현 멤버
        Mode mode;       //모드 지정
        void set_mag();
        void set_ang();
        void set_x();
        void set_y();
    public:
    	. . .  //기타 멤버 함수들
    };
}
#endif

<자세한 code는 p.591부터>
표현법이 2가지이므로, mode를 나누기 위해 enum을 사용한다. 이를 state member라고 한다.
enum Mode {RECT, POL}; : 특정 object의 mode를 왔다갔다하며 출력 형식 등을 변경한다.
특정 mode로 object를 만들면, 자동으로 다른 표현법도 초기화 시키도록한다.(변경할때도 마찬가지)

언급할만한 것들

  • object의 값을 변경할때 member 함수를 이용해도 되지만,
    shove = Vector(100,300); 이렇게 constructor를 이용할 수도 있다.
    단점은, temporary object를 만들기때문에 시/공간 측면에서 좀 손해가 있을 수 있다는 것.

  • (상기할겸) integer는 자동으로 enumeration으로 변환되지 않는다.

  • friend 함수는 접근 권한만 부여할뿐 scope 내에 들어오진 않는다. 따라서 필요하다면 ::. operator를 사용해서 접근해야한다.

  • 특정 method가 새로운 object의 값을 계산해야된다면(예를들어 +를 overload하려면 새로운 object에 값들 넣어서 return 값으로 copy되게 해야함.),
    constructor 함수 사용하는걸 고려해봐라. 훨씬 더 간단하고 믿을만 할 수 있음.(p.600)
    Vector Vector::operator+(const Vector & b) const { return Vector(x+b.x, y+b.y); }
    원래라면 위 코드도 함수내부에서 object만든 다음에 이것저것 assign하고 그 object를 반환하는 식이었음.
    하지만 위처럼하면 constructor가 알아서 초기화해서 만들고, 그 만들어진 object의 copy가 반환됨->간단
    (constructor가 호출되면 결국 object가 temporary든 어떻게든 만들어지는데, 그걸 이렇게도 활용할 수 있구나. 좀 특수하긴해도 그냥 함수네. 호출되는 경우가 다양할 뿐이지..(명확하게 호출하지 않더라도 형변환 등에서 자동호출된단 소리))

  • unary operator를 overloading할때는 매개변수 X

  • using declaration은 "namespace 내의" entity를 해당 declarative region으로 가져올때 사용하는 것이다.
    using VECTOR::Vector; : VectorVECTOR namespace내에 선언된 class이다.

  • object끼리의 assignment는 기본적으로 각 member 변수끼리 assign된다.


Class의 Automatic Conversions 과 Type Casts

built-in type에서 두 type이 compatible하지 않다면 자동으로 변환되지 않는다.
(ex. int는 pointer로 변환 X)

1. Built-in type -> Class

사실 이미 parameter가 하나인 constructor는 typeconversion 역할을 한다.
(Stonewt(int stn, double lbs = 0);: 이렇게 default argument 쓰는 경우도 마찬가지.)

ch10에서 assignment시 constructor가 호출된다고 했다.
그런게 type consversion인 셈인 것이다.
(내부적으론 constructor로 임시객체만들고 해당 class의 assignment operator 적용됨)

Constructor를 이용한 conversion 방법(복습 및 추가)

  1. implicit conversion (class object가 있을 자리에 built-in type이 있을 경우)
    이런 parameter 하나인 constructor는 myy = 16.3; 식으로 사용할 수 있다.
    (추가)
    저 경우 말고도 assign이나 initialize나 return에서나 전부 implicit conversion이 일어난다.

  2. explicit conversion
    원래 하던대로 Stonewt myy(16.3);이나 Stonewt myy = Stonewt(16.3); 식으로 사용할 수 있다.
    (추가)
    myy = (Stonewt) 16.3; 식의 typecasting도 가능

- 결국 위 경우 둘 다 Constructor 함수를 사용한다.

- 예전에 typecasting을 함수호출처럼 int()로도 쓴다고 했었는데, 그게 이거때문이었구나
  constructor 호출이 typecasting처럼 보이네

- long type의 constructor 밖에 없는데, int를 assign한다면? (implicit이든 explicit이든)
: prototype을 보고서 compiler가 한번 convert하고,
  constructor가 object type으로 convert한다.
  위처럼 그냥 두번 convert된다, 원래 알던거에서 딱히 추가되는 내용은 없다.
  ch8보면 알겠지만 굳이 완벽히 딱 일치하지않아도 모호함만 없으면 인정됨.

근데 implicit conversion은 원하지 않을때 일어날 수도 있어서 문제가 된다.
그래서 explicit keyworddeclaration에 사용implicit conversion을 막을 수 있다.
explicit Stonewt(double lbs); : explicit conversion만 가능.

2. Class -> Built-in type

Conversion Functions이라는 것을 사용

  • operator typeName();
    1) class method여야 한다.
    2) return type은 없다.
    3) argument도 없다.

ex)
operator int() const; : prototype (class declaration 내부에 있어야 함.)
아래는 definition인데, return type은 없지만 return statement는 있다.

Stonewt::operator int() const {
	return int(pounds+0.5);  //return문에 type conversion 결과 적어준다.
}

Conversion Function을 이용한 conversion 방법

  1. implicit conversion (built-in type이 있을 자리에 class object가 있을 경우)
    위랑 마찬가지로 명시적인 변환이 아닌 경우(리턴, assign 등..)
    ambiguous만 아니라면 그에 맞게 변환된다. 위 built-in type이 class object로 변환될때도 ambiguous면 당연히 안된다.(그 경우는 overloading이 ambiguous인거임)
    뭐 예를들자면 conversion function이 하나 뿐이라cout << popins; 같은데서도 conversion function을 통해 형 변환이 일어난다.

  2. explicit conversion
    마찬가지로 int (poppins);(int) poppins;처럼 typecast 이용 가능
    (책엔 안나오는데 직접 해보니 object.operator double(); 식의 직접 호출도 가능하다.)

여기서도 마찬가지로 자동 형변환은 문제가 될 수 있다.
1) 그래서 C++11부턴 conversion functions에서도 explicit을 사용할 수 있도록 한다.
2) 혹은 nonconversion function으로 바꿔서 사용해도 된다. 일반 함수인 Stone_to_Int() 같은 멤버함수를 선언해서 사용할 수 있다.

정리

class object가 built-in type으로 바뀌든, 그 반대로 바뀌든,
implicit 버전은 본래 type이 와야할 위치에 다른 놈이 와있으면 실행되는 것이고,
explicit 버전은 명확하게 constructor나 typecasting, 혹은 conversion function을 사용하도록 명시해주는 것이다.

conversion은 주의해서 다뤄야 한다.
★★ implicit의 가능성을 배제하고 explicit을 사용하는 것이 BEST


Conversions과 Friends

Stonewt class에서 double 덧셈 기능을 구현해보자.

  1. 기존엔 "member함수로 operator overloading" 하고 + "friend로 하나 더 정의" 해서 교환법칙 성립돼게 했었다.

  2. 그냥 operator+(const Stonewt &, const Stonewt &); 이것만 friend로 만들어두고,
    Stonewt(double) constructor만 있어도 해결된다.
    double이 constructor에 의해 자동 형변환 되기때문이다.

각각 장단점이 있는데, 전자가 확실히 코드는 길지만 overhead도 적고 후자에 비해 빠르다.
(impicit conversion 피하랬으니 후자는 지양하는게 좋지않을까)
그러니 Stonewt object에 double을 +로 더할 일이 많다면 전자가 낫고, 가끔 사용한다면 후자가 더 낫다.

하나 짚을거)
object가 와야할자리에 기본type이 오면 변환된다고 했는데,
object가 와야할 자리여도 constructor에서 인자로 들어갈 자리여야 변환이 되는거지
constructor를 호출하는 자리면 안된다.
ex. `total = d + s;`에서 d가 double이라하자.
total = d.Stonewt(s);` 식으로 변경됐을때 d는 Stonewt object로 변환되지 않는다.

Summary

operator overloading은 사실상 "member 함수로", "nonmember 함수이지만 friend로" 하는 방식으로 나뉜다.
(nonmember여도 friend일 필요가 없을 수 있지만 그렇게 하는게 좋다.)

<< operator를 overload해서 cout에 적용되도록하는건 흔한 일이다.(잘 배워두자)

type 변환을 implicit하게 하는 것도 가능하긴 하지만 되도록 명시적으로 하는 것이 좋다.


Chapter Review & Programming exercises

private에 접근할게 아니라면 friend function이 아니어도 되긴한다.
public에만 접근한다면 friend 아니어도 됨.
friend function은 member 함수가 아니기때문에 member에 접근하려면 membership operator 사용해야한다.

programming exercises 실수 정리

destructor는 선언했으면 정의도 해야한다.
필요할때 호출되는데 정의가 없으면 안된다. 아니면 애초에 선언을 안해서 compiler가 알아서 만들도록 하던가..
선언만해두고 정의안하니까 link 에러 뜸.

istream cin overloading하면서 두번째 인자의 object를 const로 받아버림.
input을 해서 해당 object 값을 수정해야되는데 const로 받으면 어쩌잔거냐?

rvalue 조심
<< operator를 overload해서 특정 class의 object를 print하도록 만들었다.
이때 그 object는 complex & 식으로 reference로 받았다.
그리고 또 + operator를 overload해서 complex object끼리 덧셈 기능을 구현했다.
그리고 함수 내에서 해당 object의 임시 변수를 만들어서 complex type이 반환되도록 했다.(복사되도록)

이렇게 해두고 cout << a+c; 같은걸 하니까 자꾸 type이 맞지 않는다고 안됐다.
여기서 문제는 reference에 rvalue를 넘겨주려고 한 것이었다.
rvalue는 3+56;처럼 지속성이 없는 값이다. 함수 return 값도 그때만 사용될 수 있을 뿐 바로 사라지는 rvalue이다.
그리고 당연히 이런 rvalue에는 (l-value) reference를 지정해줄 수 없다.
(물론 rvalue를 rvalue reference type으로 받는다면 잘 작동함. ch18참고)

그래서 위 문제를 해결하기위해 const complex &를 parameter로 수정해서 rvalue이거나 type이 맞지 않는다면 temporary를 만들어서 잘 작동하도록 했다.
(reference 매개변수 받을때 수정 안하면 그냥 바로 const 박자.)


영단어

sanity 온전한 정신
restraint 규제, 통제
inimical 해로운, 적대적인
tottering 비틀거리는
vase 꽃병
hasten 서둘러하다, 재촉하다
doom 죽음, 파멸, 비운
immunologist 면역학자
displacement (제자리에서 쫓겨난) 이동
gymnast 체조선수
virtue 선, 장점
sneak 살금살금 가다, 몰래하다
encompass 포함하다, 둘러싸다
lamppost 가로등 기둥
farther 더 멀리
obviate 제거하다

0개의 댓글