목차

     

     


     

     

    Chapter 2: 생성자, 소멸자 및 대입 연산자 


     

     

    항목 5:  C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 

     

    Q. C++이 만들어내는 기본적인 함수는 무엇이고, 어떠한 형태로 생성할까요? 프로그래머기 만들지 않으면 컴파일러가 기본으로 생성해주는 멤버 함수가 뭐가 있을까요?

    더보기

    A. 기본적인 함수는 복사 생성자, 복사 대입 연산자, 소멸자입니다. 생성자조차도 선언되어 있지 않으면 역시 컴파일러가 기본 생성자를 선언합니다. 이 4가지 함수는 inline 함수 형태로 생성됩니다.

    추가로 이동 생성자, 이동 대입 연산자도 생성됩니다

    public - inline 은 컴파일러가 선택한다.

    • move
    • &&
    • … (가변길이 템플릿)

    코드복사형태

    https://modoocode.com/290

     

    씹어먹는 C++ - <9 - 2. 가변 길이 템플릿 (Variadic template)>

    파라미터 팩(...)을 사용해서 임의의 개수의 인자를 받는 템플릿을 작성할 수 있습니다. C++ 17 에 새로 추가된 Fold 형식에 대해 배웠습니다.

    modoocode.com

     

     

    Q. 언리얼엔진 스크립트는 소멸자를 사용해야 할까요?

    더보기

    언리얼엔진 스크립트는 소멸자를 사용하지 않습니다. 언리얼엔진은 Garbarge Collection(GC)과 스마트 포인터로 관리를 하고 Native C++의 소멸자를 필요로 하지 않습니다.

     

    하지만 UObject 상속의 클래스가 아니라 Native C++에 가까운 class F클래스명 사용시 소멸자가 필요할 수 있습니다. 플러그인 사용시 생성자와 소멸자를 같이 만들어준 적이 있습니다.

    https://github.com/Desi9nerd/UE5_RPG/blob/main/Plugins/Weapon/Source/Weapon/WeaponCommand.h

     

     

    Q. C++14 이후부터 기본 생성자 상속받아서 함수를 금지시키는 것이 아닌 새로운 키워드가 나왔습니다. 이 키워드가 뭘까요?

    더보기

    = delete 


     

     

    항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자

     

    Q. 컴파일러에서 자동으로 제공하는 기능인 복사 생성자, 복사 대입 연산자 등 기능을 허용치 않기 위해서 어떤 방법을 사용할 수 있을까요?

    더보기

    대응되는 멤버 함수를 private로 선언한 후 구현을 하지 않으면 됩니다.

    class MyClass {
    public:
        MyClass() {} // 기본 생성자
        
    private:
        MyClass(const MyClass&);
        MyClass& operator=(const MyClass&);
    };

    C++14 이후에 추가된 = delete를 사용하면 아래와 같은 방법으로 구현할 수 있습니다.

    class MyClass {
    public:
        MyClass() {} // 기본 생성자
        
        MyClass(const MyClass&) = delete; // 복사 생성자 삭제
        MyClass& operator=(const MyClass&) = delete;  // 복사 할당 연산자 삭제
    };

     

     

    Q. 아래 코드의 출력값은?

    #include <iostream>
    
    template<class T>
    class NamedObject {
    public:
        NamedObject(std::string name, const T& value) : nameValue(name), objectValue(value) {}
    
        std::string getNameValue() const { return nameValue; }
        T getObjectValue() const { return objectValue; }
    
    private:
        std::string nameValue; 
        const T objectValue;
    };
    
    int main() {
        std::string newDog("Persephone");
        std::string oldDog("Satch");
    
        NamedObject<int> p(newDog, 2);
        NamedObject<int> s(oldDog, 36);
    
        p = s;
    
        std::cout << p.getNameValue(); 
    }
    더보기

    A. 위의 코드를 실행시키면 p = s에서 컴파일러가 거부합다.

    C++의 참조자는 원래 자신이 참조하고 있는 것과 다른 객체를 참조할 수 없습니다.

     

     

    Q. 다음 클래스를 복사하지 못하도록 코드를 수정해보세요.

    class MyClass{
    protected:
    	MyClass() {}
        ~MyClass() {}
    }
    더보기

     A.

    class MyClass{
    protected:
    	MyClass() {}
        ~MyClass() {}
        
    private:
        MyClass(const MyClass&);
    }

     

     

    Q. 이 ‘키워드’를 명시적으로 생성자에 적어주면, 컴파일러에 의해 암시적으로 복사 생성자가 호출되어 객체가 생성되는 것을 예방할 수 있습니다. 이 '키워드'는 뭘까요?

    class MyClass {
    public:
        (키워드) MyClass(int value) {
            // 초기화 로직
        }
    };
    더보기

    explicit 키워드

    단일 매개변수를 가진 생성자나 변환 연산자에 사용되어 암시적 변환을 막아서, 타입 변환 생성자에서 중요한 역할을 합니다.


    ex. value로 들어오는 값이 int가 아닌 경우, 객체가 생성되지 않는다?

     


     

     

    항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 

     

    Q. virtual 키워드의 역할과 virtual 키워드를 사용하게 되면 생기는 메모리 상 특징, 그리고 가상 함수의 중요성 모두 알고 계신가요?

    더보기

    A. virtual 키워드를 사용하면 vtable이 생성됩니다.

    가상함수로 만들시 가상 테이블에 만들어진 모든 엮어진 함수를 호출하게 되면서, 메모리 누수 문제나 상위 클래스 함수 호출 등을 한꺼번에 해결 할 수 있습니다.
    하지만, virtual 키워드를 모든 곳에 사용하면 안됩니다! 왜냐하면 virtual 키워드에 의해 가상 테이블이 생성되어 메모리가 증가하게 됩니다. 사소하게 메모리를 절약하고 싶다면 사용할 때와 사용하지 말아야할 때를 꼭 구분해서 사용하는 것이 좋습니다.

     

     

    Q. 새로 생성된 파생 클래스 객체에 대한 기본 클래스 포인터를 반환하는 OOO함수라고 부른다. OOO이 뭔지 적으시오.

    더보기

    A. 팩토리 함수

     

     

    Q. 기본 클래스 → 상속 클래스가 있는 경우 생성자 호출하는 순서와 소멸자가 호출되는 순서가 어떻게 될까요?

    더보기

    A. 생성자는 부모  → 자식 클래스 순서로 호출되고, 소멸자는 자식  → 부모 클래스 순서로 호출됩니다.

     

     

    Q. 다형성을 가진 기본 클래스에서 그냥 소멸자를 쓴다면 어떻게 될까요?

    더보기

    A. 기본클래스 부분만 소멸되는 부분소멸이 발생합니다.

    파생클래스를 포인터로 소멸시킬 때 기본클래스 포인터를 통해 삭제가 되는데, 기본클래스의 소멸자가 비가상 소멸자라면 기본클래스가 가진 부분들만 소멸됩니다.

     

    (STL컨테이너, string 등) 가상 소멸자가 없는 클래스 타입은 기본클래스로 삼는 경우는 절대 없어야 합니다. 그저 메모리 누수만…
    언리얼엔진 사용시 UObject를 상속받지 않는 Slate UI 같은 것을 사용할 시 가상 소멸자를 사용해야 하는 경우를 고려하고 코딩해야 합니다.


     

     

    항목 8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 

     

    Q. 소멸자에서 예외를 발생한 경우 대처할 수 있는 2가지 방법은?

    더보기
    •  호출 실패 로그를 작성하고 프로그램을 끝낸다.
    •  try catch 문으로 예외가 빠져나가지 못하게 예외를 삼겨버린다.

     

     

    Q. 가상 소멸자를 사용해야 하는 경우와 사용하지 않아야 하는 경우는?

    더보기

    해당 클래스에 가상 함수가 하나라도 들어 있는 경우 가상 소멸자를 사용합니다.

     

     

    Q. 가상 소멸자가 없는 클래스 C++ 라이브러리 클래스로는 뭐가 있을까요?

    더보기

    STL 컨테이너 타입. vector, list, set, unordered_map

     

     

    Q. 소멸자에서 Try Catch를 사용하면 안되는 이유를 설명해보세요. 

    더보기

    소멸자에서 try catch를 사용한다면 예외처리가 발생하여도 abort를 사용하지 않는다면 계속 소멸을 진행시키기 때문에 문제가 발생합니다. 이로 인해 데이터 누수가 발생하여 예기치 못한 문제가 발생할 수 있습니다.

     

     

    Q. 소멸자가 호출되는 경우 2가지에 대해 설명해보세요.

    더보기

    소멸자가 호출되는 2가지 경우로는 

    • 정상적으로 객체가 종료되었을 때
    • 예외처리 메커니즘에 의해 객체가 소멸될 때

    가 있습니다.

     


     

     

    항목 9:  객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자  

     

    Q. 생성자 혹은 소멸자 안에서 가상 함수를 쓰면 안되는 이유가 뭘까요? 어떻게 해결 할 수 있을까요?

    더보기

    A. 기본 클래스의 생성자가 호출될 동안에 가상 함수는 파생 클래스 쪽으로 내려가지 않습니다. 그래서 기본 클래스 타입인 것처럼 동작하는 문제가 발생합니다. 만약에 파생 클래스의 가상함수가 호출이 되더라도 파생 클래스의 멤버 변수는 초기화가 되지 않기 때문에 문제가 됩니다.

    이를 해결하는 방법으로는 비지역 정적 함수로 만들어 사용하는 것입니다. static을 사용해 비지역 정적 함수를 만들어서 파생 클래스에서 부모클래스 생성자 매개변수로 올려줍니다.

     

     

    Q. 파생 클래스 객체의 기본 클래스 부분이 생성되는 동안  (생성자가 실행되고 있는 동안) 그 객체의 타입은 무엇일까요? 

    더보기

    A. 기본 클래스입니다. 
    호출되는 가상 함수는 모두 기본 클래스의 것으로 결정되고, RTTI 를 사용한다고 해도 이 순간엔 모두 기본 클래스 타입의 객체로 취급합니다. 기본 클래스의 생성자 호출 중에는 파생 클래스의 데이터 멤버가 초기화된 상태가 아니기 때문입니다.

     

     

    Q. 다음 코드는 작성자가 원하지 않는 상황이 발생하였습니다. 무엇이 문제일까요?

    class Transcation{
    public:
    	Transcation() { Init(); }
    	virtual void logTransaction() const { number = 10;};
    	...
    private:
    	void Init(){ logTransaction(); }
    	int number = 8;
    }
    
    class SellTranscation : public Transcation {
    public:
    	virtual void logTransaction() const { number = 20 };
    }
    더보기

    부모 클래스인 Transcation클래스에서 생성자 내에서 init을 호출할 때 자식 클래스 버젼의 logTransaction()이 아닌 부모 클래스 버젼의 logTransaction()이 호출됩니다.

     

    생성자에서 가상 함수를 호출하면 자식클래스에서 재정의한게 아닌 부모클래스 버젼의 가상 함수가 호출됩니다. 왜냐하면, 부모클래스의 생성자 생성 지점에 자식 클래스의 생성자가 생성되지 않아 자식 클래스의 가상 함수를 호출할 수 없기 때문입니다.

     

     

    Q. 아래의 코드를 읽고 문제가 되는 부분을 찾아 설명하세요.

    class Transcation{
    public:
    	Transcation() { Init(); }
    	virtual void logTransation() const = 0;
    	...
    private:
    	void Init(){ logTranscation(); }
    }
    
    class SellTranscation : public Transcation {
    public:
    	virtual void logTransaction() const;
    }
    

    순수가상함수이니 abort

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Transcation {
    public:
    	Transcation() { Init(); }
    private:
    	void Init() { logTransaction(); }
    
    public:
    	virtual void logTransaction() const { cout << "Transcation::logTransaction" << endl; };
    };
    
    class SellTranscation : public Transcation {
    public:
    	virtual void logTransaction() const override
    	{
    		cout << "SellTranscation::logTransaction" << endl; 
    	}
    };
    
    int main() {
    	Transcation a;
    	a.logTransaction();
    
    	cout << endl;
    
      SellTranscation b;               // 이 부분
    	b.logTransaction();
        
    	return 0;
    }
    
    더보기
    Transcation::logTransaction
    Transcation::logTransaction
    
    Transcation::logTransaction
    SellTranscation::logTransaction

     

    순수가상함수이면 에러가 뜬다.

     

     


     

     

    항목 10:  대입 연산자는 *this의 참조자를 반환하게 하자

     

    Q. 대입 연산자에 레퍼런스가 필요한 이유에 대해 설명해보세요.

    더보기

    연산 후에 만약 값을 리턴한다면 변경된 값이 사라지기 때문에 원본값을 조작할 수 있게 레퍼런스를 사용해야 합니다.  (우측 연관, 다음 값에게 전달하기 위해서)

    x=y=z;
    

     

     

    Q. =연산자에서 반환(=리턴)해야하는 것은 무엇일까요?

    더보기

    A. *this

    대입 연산자는 *this 의 참조자를 반환합니다. 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 하는 것이 관례입니다.  이는 단순 대입이 아닌 += 과 같은 형태의 대입 연산자에서도 마찬가지입니다. 사용자 정의 클래스 또한 이러한 규칙을 지킬 수 있도록 구현하는 습관을 들이는게 좋습니다.

    Widget& oprater=(const Widget& rhs)
    {
    return *this;
    }
    

    표준 라이브러리들 또한 지키고 있는 관례이므로 반드시 지킬 수 있도록 하자!

     

     

    Q. 대입 연산자는 무조건 *this의 참조자를 반환해야 하나요? 아니면 다른 형식의 반환이어도 상관없나요?

    더보기

    대입 연산자에 *this 참조자를 사용하는 것은 하나의 관례일 뿐 해당 관례를 지키지 않는다고 해도 올바르게 코딩하면 컴파일이 됩니다.

     

     

    Q. 아래 코드를 읽고 빈칸을 완성하세요.

    struct MyClass
    {
        MyClass(const MyClass&); // Implement copy logic here
        void swap(MyClass&) throw(); // Implement a lightweight swap here (eg. swap pointers)
    
        MyClass& operator=(MyClass x)
        {
            x.swap( 빈칸 );
            return 빈칸;
        }
    };
    
    더보기

    A. *this

     

     

    Q. 아래 코드를 컴파일할 시 에러 메시지를 띄웁니다. 몇 번째 줄에서 문제가 발생했는지 찾고 그 이유에 대해 설명해보세요.

    #include <iostream>
    #include <string>
    using namespace std;
    
    class MyClass {
    public:
        // 기본 생성자
        explicit MyClass();
    
        // 복사 생성자
        MyClass(const MyClass& t);
    
        // 복사 대입 연산자
        void operator=(const MyClass& cla)
        {
            a = cla.a;
            b = cla.b;
        }
    
    public:
        int a;
        float b;
    
    };
    
    int main() {
        MyClass a,b,c;
        a = b;               
        a = b = c;
        
    	return 0;
    }
    
    더보기

    A.  a = (b = c). b = c 결과가 void값이 되니 그 이후에 a = (b = c)에서 맞는 연산자가 없다고 나온다.

     

    #include <iostream>
    #include <string>
    using namespace std;
    
    class MyClass {
    public:
        // 기본 생성자
        explicit MyClass();
    
        // 복사 생성자 삭제
        MyClass(const MyClass& t);
    
        // 복사 할당 연산자. 여기를 *this 반환 형식으로 변경해야 한다!
        MyClass& operator=(const MyClass& cla)
        {
            return *this;
        }
    
    public:
        int a;
        float b;
    
    };
    
    int main() {
        MyClass a, b, c;
        a = b;
        b = c;
        a = b = c;
    
        return 0;
    }

     

     

    항목 11:  operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자

     

    Q. 자기 대입 방식 문제점을 방지할 수 있는 2가지 방법은 무엇일까요?

    더보기
    • copy and swap
    • 자기 대입 함수의 구조 변경
      • 코드 순서만 변경되어도 문제점이 해결될 수 있습니다!

     

     

    Q. (= 연산자의) 자기 대입에 대한 문제를 발생하는 것이 뭘까요? 이것을 해결하는 방안은?

    더보기
    • 허상 포인터 문제가 발생할 수 있습니다. 여러 포인터가 참조하고 있는 상황에서 하나의 포인터가 Delete로 메모리를 삭제하면 나머지 포인터는 Dangling Pointer가 되기 때문입니다.
    • 여러 곳에서 하나의 객체를 참조하는 상태(=중복참조)일 때 자기대입이 발생할 수 있습니다.

     

     

    Q. 자기 대입을 하게 되면 생기는 문제가 뭘까요?

    더보기

    같은 객체를 여러 개가 참조(중복 참조 : aliasing) 하기 때문에 한쪽에서 객체를 소멸시키면 나머지 참조자 또는 포인터들은 ‘null 객체’ 를 가지게 된다.


     

     

    항목 12: 객체의 모든 부분을 빠짐없이 복사하자

     

    Q. 상속관계에서 복사 생성자 함수를 작성할 때 꼭 확인 해야 할 두 가지가 무엇일까요? 이렇게 해야 하는 이유는 무엇일까요?

    더보기

    고려할 점은 해당 멤버 변수가 모두 복사가 이루어져 있는지 입니다!

    상속 관계에서도 해당 부모와 하위 클래스 모두 복사가 이루어지도록 재정의해야합니다.

     

     

    Q. A1, A2 두가지 중 옳은 것은 어떤 문장일까요 ??

    Q. 복사 대입 연산자에서 복사 생성자를 호출한다면?

    A1. 이미 존재하는 객체를 생성하는 것이므로 불가.

    A2. 이미 존재하는 객체여도 다시 생성하는 것은 가능.

    더보기

    답:  A1

     

    Q. 복사 생성자에서 복사 대입 연산자를 호출한다면?

    A1. 아직 초기화도 안 된 객체에 대입이므로 불가.

    A2. 생성하면서 대입할 수 있다.

    더보기

    답:  A1

     

     

    Q. 복사 생성자에서 복사 대입 연산자를 호출하면 안 되는 이유는 뭘까요?

    더보기

    생성자의 역할은 새로 만들어진 객체를 초기화하는 것이지만, 대입 연산자의 역할은 ‘ 이미 ’ 초기화가 끝난 객체에게 값을 주는 것입니다. 대입연산자 초기화된 객체에만 적용됩니다. 그래서 대입 연산자의 역할은 ‘이미’ 초기화가 끝난 객체에게 값을 주는것입니다. 초기화된 객체에만 적용됩니다.

     

     

    Q. 하나의 클래스 안에 복사함수를 2개 구현하여야 한다. 적절한 방법은 무엇인가?

    더보기

    하나의 클래스 안에서 복사 함수 2개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 말아야 합니다.

    그 대신, 공통된 동작제 3의 함수에 분리해 놓고 양쪽에서 이것을 호출하게 만들어야 합니다.