Effective C++ : Chapter 4 설계 및 선언


 

 

항목 18:  인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

 

Q. 인터페이스에 대해 설명해보세요.

더보기
  • 소프트웨어식 인터페이스: 
    • 응용프로그램과 운영체제 간의 통신을 연결해주는 인터페이스
  • 언어적인 인터페이스:
    • 순수 가상함수로만 이루어진 클래스

 

Q. 인터페이스를 설계할 때, shared_ptr 을 사용하면 좋은 이유에 대해 설명해보세.

더보기

사용자 정의 삭제자를 통해 교차 DLL 문제 예방할 수 있습니다.
반환 타입을 포인터가 아닌 shared_ptr로 만들어서, 메모리 누수 예방할 수 있습니다.

 

 

Q. 교차 DLL 문제(cross-DLL problem)에 대해 설명해보세요. 왜 교차 DLL 문제가 발생할까요? 이에 대한 해결방안으로는 뭐가 있을까요?

더보기

[ 교차 DLL 문제 ]

교차 DLL 문제는 두 개 이상의 DLL이 서로 다른 메모리 할당 방식을 사용하여 객체를 생성하고 해제할 때 발생하는 문제입니다. 예를 들어, A DLL에서 생성된 객체를 B DLL에서 해제하려고 하면 메모리 누수 또는 액세스 위반이 발생할 수 있습니다.

 

[ 발생 원인 ]

객체 생성 시에 어떤 동적 링크 라이브러리(dynamically linked library: DLL)의 new를 썼는데 그 객체를 삭제할 때는 이전의 DLL과 다른 DLL에 있는 delete를 썼을 경우에 발생할 수 있습니다. new/delete 짝이 실행되는 DLL이 달라서 꼬이게 되면 런타임 에러가 일어납니다.

 

[ 해결 방안 ]

shared_ptr을 사용하면 이 문제를 피할수 있습니다. 이 클래스의 기본 삭제자는 shared_ptr이 생성된 DLL과 동일한 DLL에서 delete를 사용하도록 만들어져 있기 때문입니다. shared_ptr는 해당객체의 참조 카운트가 0이 될 때 어떤 DLL의 delete를 사용해야 하는지를 꼭 붙들고 잊지 않습니다.

shared_ptr은 교차 DLL 문제를 막아 주며, 뮤텍스 등을 자동으로잠금 해제하는데 쓸 수 있습니다.  


 

 

항목 19:  클래스 설계는 타입 설계와 똑같이 취급하자

 

Q. 다음 아래 상황에서 bool platoIsOK를 연산하는데 생성자와 소멸자의 호출 횟수를 계산하고 설명해보세요.

class Person {
public:
	Person();
	virutal ~Person();
	
private:
	std::string name;
	std::string address;
}

class Student : public Person{
public:
	Student();
	~Student();
	
private:
	std::string schoolName;
	std::string schoolAddress;
}

bool validateStudent(Student s); 

int main(){
	Student plato;
	
	bool platoIsOK = validateStudent(plato);  // 여기!!
}
더보기

생성자 6번, 소멸자 6번

생성자: Student의 복사 생성자 + Student 내의 name, address 복사 생성자 + Person의 복사 생성자 + Person 내의 schoolName, schoolAddress 복사 생성자

소멸자: 위와 동일

 


 

 

 

 

항목 20:  ‘값에 의한 전달’보다는 ‘상수객체 참조자에 의한 전달’방식을 택하는 편이 대개 낫다.

 

Q. 참조 연산자(&)의 구현은 어떤식으로 되어 있나요?

더보기

답:  참조 연산자(&)는 포인터(*)로 구현되어 있습니다.

 

 

Q. ‘값에 의한 전달’이 저비용이라고 가정해도 괜찮은 유일한 타입 3가지는 무엇이 있을까요?

더보기

참고 154쪽

  1. 기본 제공 타입
  2. STL 반복자
  3. 함수 객체 타입

'값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫습니다. 대체적으로 효율적일 뿐만 아니라 복사손실 문제까지 막아줍니다.

이번 항목에서 다룬 법칙은 '기본제공 타입', 'STL 반복자', 그리고 '함수 객체 타입'에는 맞지 않으며, 이들에 대해서는 '값에 의한 전달'이 더 적절하다고 합니다.

 

참고) 함수 객체란?  https://wikidocs.net/144316

 

 

Q. 다음 아래 상황에서 bool platoIsOK를 연산하는데 생성자와 소멸자의 호출 횟수를 계산하고 설명해보세요.

class Person {
public:
	Person();
	virutal ~Person();
	
private:
	std::string name;
	std::string address;
}

class Student : public Person{
public:
	Student();
	~Student();
	
private:
	std::string schoolName;
	std::string schoolAddress;
}

bool validateStudent(Student s); 

int main(){
	Student plato;
	
	bool platoIsOK = validateStudent(plato);  // 여기!!
}
더보기

 

생성자 6번, 소멸자 6번

  • 생성자:  Student의 복사 생성자 + Student 내의 name, address 복사 생성자 + Person의 복사 생성자 + Person 내의 schoolName, schoolAddress 복사 생성자
  • 소멸자:  위와 동일

 

 

항목 21:  함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자.

 

Q. 스택에 객체를 만들어 지역변수를 정의, 힙 기반으로 정의 했을 때 왜 오류가 뜨는지 설명해보세요.

더보기

답:

[ 스택 ]

  • 스택에 객체를 생성하여 지역 변수를 정의하면, 해당 함수의 실행이 끝날 때 스택 메모리가 자동으로 해제됩니다. 따라서 함수가 종료되면 지역 변수로 생성된 객체도 함께 소멸됩니다.

[ 힙 ]

  • 힙 기반으로 객체를 생성하면, 명시적으로 new를 사용하여 메모리를 할당하고 delete를 사용하여 해제해야 합니다. 만약 함수 내에서 힙 기반으로 객체를 생성하고 delete를 호출하지 않으면, 메모리 누수가 발생할 수 있습니다.
  • 또한, 함수가 종료되었을 때 누군가가 언제 delete를 호출해야 하는지 명확하지 않다면, 메모리 누수나 잘못된 메모리 접근(댕글링 포인터)과 같은 문제가 발생할 수 있습니다.

 

 

Q. 유리수를 나타내는 클래스 Rational 의 곱셈 연산자 함수로 적절한 것은 두 함수 중 몇 번 일까요?

1번.

inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

2번

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	Rational result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	return result;
}
더보기

답:  1번 ( 159쪽 참고 )

객체를 반환하는 함수에서는 새로운 객체를 반환하게 만들어줘야 합니다. 이 때 객체 생성에 소모되는 비용은 올바른 동작에 지불되는 작은 비용입니다.

 

[ 2번이 오답인 이유 ] ( 156쪽 참고 )
이 함수에서 반환하는 result 객체는 지역 객체이므로 해당 함수가 끝날 때 소멸됩니다. 즉, new로 할당한 메모리에 대한 해제가 이루어지지 못하므로 메모리 누수가 발생합니다.

 

 

Q.  inline 함수로 선언된 함수는 항상 inline 함수로 취급되나요?

더보기

답:  아닙니다. inline 함수로 선언하였어도 inline 함수로 취급되지 않을 수도 있습니다. 컴파일러가 inline 여부를 판단합니다. 상황에 따라 일반함수로 처리할 수도 있습니다. 

  • 인라인 코드 대체는 컴파일러의 재량에  따라 수행됩니다. 해당 주소가 사용되거나 컴파일러가 너무 크다고 판단하는 경우, 컴파일러는 함수를 인라인으로 처리하지 않습니다. 
  • 인라인 처리가 안 된 경우
    • 디스어셈블리 코드로 확인해보면 인라인 함수가 들어갈 자리에  jmp가 있습니다 → 복사가 안 되고 jmp로 특정 위치로 이동한 것이므로 , inline 처리가 안 된 것으로 볼 수 있습니다. 

 

※ 참고: [ inline 함수가 컴파일 되지 않는 경우 ] 

  • inline 안에서 재귀랑 반복이 들어가면 inline 컴파일이 처리를 안 해줍니다 → inline이 무조건 되는 게 아닙니다.

 

 

Q. 아래의 코드를 읽고 분석하세요.

#include <string>
#include <iostream>
using namespace std;

class Base{
public:
    int a;
    
public:
    inline void Set(int num) { a = num; }
    inline int Get() { return a; }
};

int main()
{
    Base b;
    int c = 1;
    
    while(true)
    {        
        b.Set(c++);
        cout << b.Get() << endl;
    }

    return 0;
}
#include <string>
#include <iostream>
using namespace std;

class Base{
public:
    int a;
    
public:
    inline void Set(int num) 
    {
        for (int i = 0; i < num; i++)
        {
            cout << i << endl;
        }
    }
    inline int Get() { return a; }
};

int main()
{
    Base b;
   
    b.Set(10);

    return 0;
}

 

 

Q. 아래의 코드에서 두 유리수 쌍의 곱 연산 후 == 연산을 하면 항상 같다고 나오는 이유를 설명해보세요.

bool operator==(const Rational& lhs, const Rational& rhs);

Rational a, b, c, d;
...
if ((a * b) == (c * d)) {
}
else {
}
더보기

답:  

(a*b) == (c*d)를 해석할 때, 값으로 해석하지 않고, Rational == Rational으로 해석하기 때문에 같은걸로 처리됩니다.

 

 

Q. 함수에서 객체를 반환하는 경우, 참조자 반환보다 값에 의한 반환이 더 좋은 이유에 대해 설명해보세요.

더보기

함수 수준에서 참조자 반환으로 새로운 객체를 만드는 방법은 ‘스택에 만드는 것’과 ‘힙에 만드는 것’ 두 가지 방법이 있습니다. 

스택’에 객체를 만드는 경우, 지역 변수를 정의하게 되는데 지역객체는 함수가 끝날 때 덩달아 소멸되기 때문에 문제가 됩니다. 참조자 반환 시 참조가가 가리키고 있는 대상은 전 객체가 되어 미정의 동작을 야기할 수 있습니다.

’에 객체를 생성하는 경우, 함수가 반환할 객체를 힙에 생성해 뒀다가 참조자로 반환하게 됩니다. 생성자가 호출되기 때문에 해당 객체를 사용 후 소멸자를 호출해야 합니다. 하지만 함수 내의 숨겨진 포인터에 대해서 사용자는 접근할 방법이 없습니다. 그렇게 때문에 소멸자가 호출되지 않아 메모리 누수가 발생합니다.

다음과 같은 이유로 함수에서 객체를 반환하는 경우, ‘새로운 객체를 반환하는 것’이 가장 좋습니다. 생성자와 소멸자가 호출되어 비용이 발생하지만 2개 이상의 객체가 생성되는 경우, 해당 방법이 가장 안전합니다. 그리고 현대 컴파일러는 RVO & NRVO를 사용하기 때문에 수행 성능이 높습니다.


 

 

 

항목 22:  데이터 멤버가 선언될 곳은 private 영역임을 명심하자.

 

Q. 상속관계가 아닌 클래스 2개가 서로 접근할 수 있게 하는 방법으로는 무엇이 있을까요?

더보기

답:  friend 선언, Getter/Setter 함수, 전달자 패턴 (Mediator Pattern), 관찰자 패턴 (Observer Pattern), 인터페이스 클래스, static  

 

1. friend 선언

  • 한 클래스에서 다른 클래스의 멤버 함수나 변수를 friend로 선언하여 접근 권한을 부여할 수 있습니다.
class B;

class A {
private:
    int value;
public:
    A(int v) : value(v) {}
    friend class B;  // B가 A의 private 멤버에 접근할 수 있도록 허용
};

class B {
public:
    void showAValue(A& a) {
        cout << "A의 value: " << a.value << endl; // A의 private 멤버에 접근 가능
    }
};

 

 

 2. Getter/Setter 함수 사용

  • 클래스의 private 멤버에 접근할 수 있도록 public getter 및 setter 함수를 제공하는 방법입니다.
class A {
private:
    int value;
public:
    A(int v) : value(v) {}
    int getValue() const { return value; }
    void setValue(int v) { value = v; }
};

class B {
public:
    void showAValue(const A& a) {
        cout << "A의 value: " << a.getValue() << endl;
    }
};

 

 

3. 전달자 패턴 (Mediator Pattern):

  • 두 클래스가 서로 직접 접근하지 않고, 중재자를 통해 상호작용하게 할 수 있습니다.
class A;
class B;

class Mediator {
public:
    static void showAValue(const A& a, const B& b);
};

class A {
private:
    int value;
public:
    A(int v) : value(v) {}
    friend class Mediator;
};

class B {
public:
    void showAValue(const A& a) {
        Mediator::showAValue(a, *this);
    }
};

void Mediator::showAValue(const A& a, const B& b) {
    cout << "A의 value: " << a.value << endl;
}

 

4. [ 관찰자 패턴 (Observer Pattern) ]

  • 이 패턴은 객체의 상태 변화를 관찰하는 관찰자(Observer)들에게 알림을 전송하는 방식으로 동작합니다. 이를 통해 느슨한 결합을 유지하면서도 상태 변화에 따른 적절한 반응이 가능하게 합니다.
    • Subject: 관찰 대상이 되는 객체로, 하나 이상의 Observer 객체를 등록, 삭제, 알림하는 기능을 갖습니다.
    • Observer: Subject의 상태 변화를 관찰하는 인터페이스를 제공합니다. Subject의 상태 변화에 따라 업데이트됩니다.
    • ConcreteSubject: Subject 인터페이스를 구현하며, Observer 객체들을 관리합니다.
    • ConcreteObserver: Observer 인터페이스를 구현하며, ConcreteSubject 객체의 상태 변화에 따라 업데이트됩니다.
#include <iostream>
#include <list>

class Observer {
public:
    virtual void update(int value) = 0;
};

class Subject {
    list<Observer*> observers;
    int value;
    
public:
    void attach(Observer* observer) {
        observers.push_back(observer);
    }
    void detach(Observer* observer) {
        observers.remove(observer);
    }
    void notify() {
        for(Observer* observer : observers) {
            observer->update(value);
        }
    }
    void setValue(int value) {
        this->value = value;
        notify();
    }
};

class ConcreteObserver : public Observer {
    std::string name;
public:
    ConcreteObserver(std::string name) : name(name) {}
    void update(int value) override {
        cout << name << " received value: " << value << endl;
    }
};

int main() {
    Subject subject;
    ConcreteObserver observer1("Observer 1");
    ConcreteObserver observer2("Observer 2");

    subject.attach(&observer1);
    subject.attach(&observer2);

    subject.setValue(10); // Observer 1 received value: 10
                          // Observer 2 received value: 10

    return 0;
}

 

 

5. [ 인터페이스 클래스 사용 ]

  • 두 클래스가 공통된 인터페이스를 구현하여 상호작용할 수 있게 합니다.
class IAccessible {
public:
    virtual int getValue() const = 0;
};

class A : public IAccessible {
private:
    int value;
public:
    A(int v) : value(v) {}
    int getValue() const override { return value; }
};

class B {
public:
    void showValue(const IAccessible& accessible) {
        cout << "Value: " << accessible.getValue() << endl;
    }
};

 

6. [ static 사용 ]

  • static 멤버 함수나 변수를 사용하면 특정 클래스의 인스턴스에 의존하지 않고 클래스 자체에 속하는 멤버에 접근할 수 있습니다. 
  • static을 사용하면 두 클래스 간의 의존성을 줄이면서도 필요한 정보를 공유하거나 작업을 수행할 수 있습니다.
  • 하지만, static 멤버는 클래스 전체에서 공유되므로 멀티스레드 환경에서는 동기화 문제를 주의해야 합니다.

Static 멤버변수 사용:  한 클래스에 static 멤버 변수를 선언하고 다른 클래스에서 이를 접근할 수 있게 합니다.

class A {
public:
    static int value; // static 멤버 변수 선언
};

int A::value = 0;    // static 멤버 변수 정의

class B {
public:
    void setValue(int v) {
        A::value = v;    // A의 static 멤버 변수에 접근
    }
    int getValue() const {
        return A::value; // A의 static 멤버 변수에 접근
    }
};

// 사용 예시
int main() {
    B b;
    b.setValue(42);
    cout << "A의 static value: " << b.getValue() << endl; // 출력: A의 static value: 42
    return 0;
}

 

Static 멤버 함수 사용:  한 클래스에 static 멤버 함수를 선언하고 다른 클래스에서 이를 호출할 수 있게 합니다.

class A {
public:
    static void printMessage() {
        std::cout << "A 클래스의 static 함수입니다." << std::endl;
    }
};

class B {
public:
    void callPrintMessage() {
        A::printMessage(); // A의 static 멤버 함수 호출
    }
};

// 사용 예시
int main() {
    B b;
    b.callPrintMessage(); // 출력: A 클래스의 static 함수입니다.
    return 0;
}

 

 

 

Q. friend를 사용하본 경험이 있나요?

더보기

잠시 사용해본적은 있지만 리펙토링 과정에서 삭제했습니다. friend를 사용하면 데이터 보호가 어렵고 캡슐화에 방해가 되어 되도록 사용을 지양하려고 합니다.

 

 

Q. 데이터 멤버를 함수 인터페이스 뒤에 감추게 되면 (캡슐화하게 되면) 편해지는 것들 중 거짓인 명제는 몇 번일까요?

1.  데이터 멤버를 읽거나 쓸 때 다른 객체에 알림 메시지를 보내기
2.  클래스의 불변속성, 사전조건(precondition), 사후조건(postcondition) 을 검증하기
3.  데이터 멤버에 직접 접근하여 직접 수정하기
4.  스레딩 환경에서 동기화를 걸기

더보기

답:  3번.  3번은 public 데이터 멤버에 대한 설명입니다.

(책에서 해당 항목에서는) 데이터 멤버에 접근할 수 있는 통로는 멤버 함수뿐이면 좋다고 강조하고 있습니다.

 

 

Q. 데이터 멤버를 public 으로 두면 안되는 이유 2가지를 설명해보세요.

더보기

1. 문법적 일관성

  • 데이터 멤버가 public이 아니라면 객체에 접근할 수 있는 유일한 수단은 멤버 함수입니다. 멤버에 접근하고 싶을 때 그냥 함수를 쓰면 되기 때문에 사용자 입장에서 생각할 일이 줄어듭니다.

2. 접근성 제한

  • 함수를 사용할 시 데이터 멤버의 접근성에 대해 훨씬 정교한 제어 가능 접근 불가, 읽기 전용, 읽기 쓰기, 쓰기 전용 접근을 직접 구현할 수 있습니다.
  • 어떤 식으로든 외부에 노출 시키면 안되는 데이터 멤버들이 꽤 많기 때문에 세밀한 접근 제어는 중요합니다.

 

 

Q. protected 멤버 변수와 public 멤버 변수가 가지는 공통된 단점을 각각 말하시오.

더보기

public 멤버 변수를 제거하거나 수정하면, 많은 양의 코드를 수정해야 합니다.
protected 멤버 변수를 제거하거나 수정하면 파생 클래스 전부를 수정해야 합니다.

 

 

Q. 이 데이터 멤버들을 각각 사용 용도에 맞게 사용하고 싶다. 올바른 멤버 함수를 작성하시오.

noAccess - 접근 권한 없는 데이터 멤버,  readOnly - 읽기 전용,  readWrite - 읽기 / 쓰기,  writeOnly - 쓰기 전용

private:
	int noAccess;
    int readOnly;
    int readWrite;
    int writeOnly;
}
더보기

답:

noAccess -> 아무것도 선언하지 않으면 자동으로 접근 권한 X

// readOnly
int GetreadOnly() { return readOnly; }

// readWrite
int GetreadWrite() { return readWrite; }
void SetreadWrite(int NewreadWrite) { readWrite = NewreadWrite; }

// writeOnly
void SetwriteOnly(int NewWriteOnly) { WriteOnly = NewWriteOnly; }

 

 

 

항목 23:  멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자.

 

Q. namespace를 사용하면 함수 집합의 확장이 가능합니다. 그 이유가 무엇인지 설명해보세요.

더보기

답:  네임스페이스(namespace)를 사용하면 동일한 이름을 가진 함수나 변수를 구분할 수 있기 때문에 함수 집합의 확장이 가능합니다. 네임스페이스를 사용하면 코드를 모듈화하고, 기능별로 그룹화하여 구성할 수 있어 코드의 가독성과 유지보수성을 높일 수 있습니다. 또한, 서로 다른 라이브러리나 모듈에서 동일한 이름을 사용하는 경우 충돌을 방지할 수 있습니다.

namespace Math {
    int add(int a, int b) {
        return a + b;
    }
}

namespace StringOperations {
    std::string add(const std::string& a, const std::string& b) {
        return a + b;
    }
}

int main() {
    int result = Math::add(3, 4); // Math 네임스페이스의 add 함수 호출
    std::string combined = StringOperations::add("Hello, ", "World!"); // StringOperations 네임스페이스의 add 함수 호출

    std::cout << "Math::add 결과: " << result << std::endl; 				// 출력: Math::add 결과: 7
    std::cout << "StringOperations::add 결과: " << combined << std::endl; // 출력: StringOperations::add 결과: Hello, World!
    
    return 0;
}

Math와 StringOperations 네임스페이스는 각각 add라는 함수를 가지고 있지만, 서로 다른 네임스페이스에 속해 있기 때문에 충돌 없이 사용할 수 있습니다. 이처럼 네임스페이스를 사용하면 함수나 변수의 이름 충돌을 피할 수 있어 함수 집합을 확장하는 데 유리합니다.

이와 같이 네임스페이스를 사용하면 코드의 구조화와 확장이 용이해져 보다 큰 프로젝트나 라이브러리를 관리할 때 유용합니다.

 

 

Q. 거짓인 명제는 고르시오.

1.  “함수는 어떤 클래스의 비멤버가 되어야 한다”라는 주장은 “그 함수는 다른 클래스의 멤버가 될 수 없다”라는 의미가 아니다

2.  표준 C++ 라이브러리는 컴파일 의존성을 고려하여, 수십 개의 헤더(<vector>, <algorithm> 등)에 흩어져 선언되어있다.

3.  private 멤버는 비프렌드 함수에서도 접근할 수 있다.

더보기

답:  3번

 

 

Q. 비멤버 비프렌드 함수가 private 멤버함수보다 캡슐화에서 더 나은 이유가 뭔지 설명해보세요.

더보기

답:  

1. 캡슐화 정도가 높아집니다.

  • 비멤버 비프렌드 함수는 private 데이터 멤버에 접근하는 함수의 수를 늘리지 않기 때문입니다.

2.  패키징의 유연성이 커집니다.

  • 반드시 사용해야 하는 핵심 기능들을 멤버함수로 만들어 하나의 헤더에 선언해 두고, 나머지 부가기능들은 같은 namespace 내의 다른 헤더에 기능별로 분류하여 비멤버 비프렌드 함수로 선언해 두면 사용자는 필수 헤더와 사용하고자 하는 기능이 포함된 헤더만 include하여 라이브러리를 사용할 수 있습니다.

3.  기능적인 확장성이 높습니다.

  • 사용자가 기능 확장을 원하는 클래스와 동일한 namespace 안에 편의를 위한 비멤버 비프렌드 함수를 추가로 정의해 넣을 수 있습니다. 단, std namespace에는 사용자가 내용을 추가해선 안 됩니다.

 

 

Q. 3가지 기능을 순차적으로 작동하도록 Class WebBroser의 3개의 함수를 한 함수에 모아서 사용하려고 합니다. 

class WebBrowser {
public:
	void clearCache();
    void clearHistory();
    void removeCookies();
};

아래의 둘 중 어떤 것이 더 효율적인가요? 이유와 주의점을 설명해보세요.

1. 멤버 함수로 호출

class WebBrowser {
public:
	void clearEverything(); // clearCache, clearHistory, removeCookies를 호출합니다.
}

2. 비멤버 함수로 호출

void clearBrowser(WebBrowser& wb)
{
	wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}
더보기

답:  2. 비멤버 함수로 호출

캡슐화, 패키징 유연성, 확장성에서 비멤버 함수로 호출하는 것이 더 효율적입니다.
주의점은 비멤버 함수일 경우 비프렌드 함수여야 한다는 것입니다.

 

 

Q. 멤버함수(= 클래스의 멤버로 선언되는 연산자 및 함수)를 잘 안 쓰는 이유가 뭘까요? 설명해보세요.

더보기

답:  캡슐화. 유연성. 멤버함수를 사용하면 캡슐화하는 과정에서 들여다볼 데이터가 늘어나기 때문에 좋지 않습니다. 그렇기에 가능하다면 비멤버 함수를 사용하는게 좋습니다.


 

 

 

항목 24:  타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자.

 

Q. 모든 매개변수에 대해 타입 변환을 해줄 필요가 있다면, 그 함수는 비멤버함수여야 합니다. 그 이유가 무엇이라고 생각하나요? 24장을 다같이 정리해보아요.

더보기

기본 타입(=해당 클래스가 아닌 객체의 경우, 해당 함수에 타입 변환 불가능)의 경우, (애초에 기본 타입은 클래스가 아니니) 정의한 클래스에 대한 operator* 멤버함수로 재정의 불가능합니다. 

 

하지만, 비멤버함수를 사용해서 operator*를 재정의할 경우 암시적 변환이 가능합니다. (연산을 수행할 때 가까운 operator*를 호출)

 

 

Q  명시적 변환보다 암시적 변환을 쓰는게 더 좋은 경우로 뭐가 있을까요?

더보기

답:  숫자 타입을 만드는 경우

 

 

Q. 아래의 코드에서 에러가 나는 부분을 설명해보세요. 

class Rational {
public:
	const Rational operator*(const Rational& rhs) const;
}

int main()
{
	Rational oneEighth(1, 8);
	Rational oneHalf(1, 2);
    
    Rational result = oneHalf * oneEighth; // 좋습니다.
    
    result = result * oneEighth; 		   // 좋습니다.

    result = oneHalf * 2;   		// 좋습니다.
    result = 2 * oneHalf;   		// 에러!

    result = oneHalf.operator*(2);  // 좋습니다.
    result = 2.operator*(oneHalf);	// 에러!
}
더보기

답:  곱하기는 왼쪽이 기준이 됩니다. 2는 상수입니다. 상수는 재정의할 수 없기 때문에 밖으로 나와있어야 합니다. 
그래서 객체가 왼쪽에 있는 oneHalf * 2는 통과하고 상수가 왼쪽에 있는 2 * oneHalf는 통과하지 않습니다.


 

 

항목 25:  예외를 던지지 않는 swap에 대한 지원도 생각해 보자.

 

Q. std::swap에서 ‘std’ 한정자를 붙이면 안되는 이유에 대해 설명해보세요.

 

 

 

[   l-value 왼값   ]

  • 단일식을 넘어서 계속 지속되는 개체

[   r-value 오른값   ]

  • lvalue가 아닌 나머지 (임시 값, 열거형, 람다, i++ 등)
  • 즉, 단일식을 벗어나면 사용 불가능한 아이들
  • 참고로 임시 객체는 스택에 잠시 몸을 담는다

[   오른값 참조   ]

  • 오른값만 참조할 수 있는 참조 타입
  • 오른값 참조라고 해서 꼭 오른값인 것은 아니다!

아마 직접적으로 오른값 참조를 쓸 일은 없겠다만 && 이 무엇인지 알아야 한다

벡터의 이사 방식이 이동이다. 경우에 따라 이동과 복사의 차이가 엄청날 수 있기 때문이다.

 

 

class Knight()
{
public:
	// 복사 대입 연산자
    void operator=(const Knight& knight)
    {
    	_hp = knight._hp;
        
        if (knight._pet)
        	_pet = new Pet(*knight._pet); // 깊은 복사
    }
    
    // 이동 생성자
    Knight(Knight&& knight) noexcept
    {
    	_hp = knight._hp;
        _pet = knight._pet;
        knight._pet = nullptr;
    }
    
    // 이동 대입 연산자 ... 그냥 "소유권을 이전했다" 라고 생각해도 좋다. 복사한게 아니라.
	void operator=(Knight&& knight) noexcept
    {
    	_hp = knight._hp;
        _pet = knight._pet;
        knight._pet = nullptr; // 어차피 사라질 애니까 nullptr 로 밀어주기
    }
 
public:
	int _hp = 0;
    Pet* _pet = nullptr;
};

void TestKnight_LValueRef(Knight& knight)
{
	// 원본 넘겨줄테니... 건드려도 됨
}

void TestKnight_ConstLValueRef(const Knight& knight)
{
	// 원본 넘겨줄테니... 건드릴 수 없다.
}

void TestKnight_RValueRef(Knight&& knight) // 오른값 참조라는 뜻
{
	// 원본 넘겨줄테니... 더 이상 활용하지 않을테니 맘대로 해도 됨.
    // 일회성이니 원본 더 안쓸거니 알아서 해
}

int main()
{	
	Knight k1;
    k1._pet = new Pet();
    
    Knight k2;
    // k2 = static_cast<Knight&&>(k1); // 이동~~
    k2 = std::move(k1); // 위 문장과 완전 똑같은 의미다
    
	TestKnight_ConstLValueRef(Knight()); // 임시객체 넘기기
	
    // TestKnight_RValueRef(Knight());
    TestKnight_RValueRef(static_cast<Knight&&>(k1)); // 오른값 참조
}