본문 바로가기

C++ & etc

[Effective C++] Chapter 02. 생성자, 소멸자 및 대입 연산자

컴파일러가 자동으로 만드는 기본 함수들에 주의

컴파일러는 빈 클래스에서도 아래 함수들를 자동으로 생성한다.

  • (인자 없는)생성자
  • 복사 생성자
  • 대입 연산자
  • 소멸자

위 함수를 모두 public inline 으로 선언한다.

더보기

복사 생성자의 예시

   MyClass(const MyClass &obj) {
        value = obj.value; // Copy the value from the existing object
        cout << "Copy Constructor called with value: " << value << endl;
    }

→ 생성자를 선언하면 컴파일러가 함수를 만드는 것을 막을 수 있다.

(필요한 경우에만, 즉 호출되는 경우에만 만들어짐)

인자로 받은 객체의 멤버 변수를 하나하나 복사한다. 이때 멤버 변수들 각 타입의 대입 연산자도 사용된다.

 

문제 상황 : 멤버 변수가 참조자이거나, const 인 경우

template<typename T>
 class NamedObject {
 public:
	 // this ctor no longer takes a const name, because nameValue
	 // is now a reference-to-non-const string. The char* constructor
	 // is gone, because we must have a string to refer to.
 NamedObject(std::string& name, const T& value);
 ...
 private:
	 std::string& nameValue;
	 const T objectValue;
 };
 
 std::string newDog("Persephone");
 std::string oldDog("Satch");
 NamedObject<int> p(newDog, 2);
 NamedObject<int> s(oldDog, 36);
 p = s; -----> 문제 발생 지점

p에 s가 대입될 때, s 안에 있는 멤버변수 string”oldDog”, int 36을 p로 복사하려고 하겠지만,

nameValue가 참조자이기 때문에 복사할 수 없고. (참조자가 다른 것을 참조하도록 변경할 수 없음)

const 변수의 값을 변경할 수 없기 때문에 문제가 생긴다.

→ 따라서 컴파일러가 복사 생성자 함수를 만들기를 거부할 것이다. (컴파일 에러 발생)

: 이런 경우에는 직접 복사 생성자를 선언해 주고 적절한 동작을 하도록 해야 한다.

 

컴파일러가 만드는 함수들이 필요 없다면, 사용을 막아버리자

생성자를 private로 선언하면, 인스턴스를 생성할 수 없다.

더보기

참고 : protected 접근자

private는 자기 자신만 접근 가능하고, public은 다른 모든 클래스에서 접근 가능하다.

protected는 자기 자신과 자식 클래스 (=파생 클래스)만 접근할 수 있다.

friend class로 선언된 클래스들에 한해서 private, protected 멤버에 접근할 수 있다.

(두 클래스 간의 관계가 tight하게 연결되어 멤버들에 접근해야 할 필요가 있을 때 사용)

이렇게라도 선언해 두면, 컴파일러가 새 함수를 선언하지 않는다.

 class HomeForSale {
 public:
 ...
 private:
 ...
 HomeForSale(const HomeForSale&);
 HomeForSale& operator=(const HomeForSale&);
 };

함수 선언 시 파라미터의 이름은 필수가 아니다 - 어차피 호출될 일이 없는 함수이기 때문에, 이름도 생략한 것.

아래와 같은 클래스를 정의해 두고, 이것을 상속받게 하는 방법도 있다.

 

 class Uncopyable {
 protected:
	 Uncopyable() {}
	 ~Uncopyable() {}
 private:
	 Uncopyable(const Uncopyable&);
	 Uncopyable& operator=(const Uncopyable&);
 };
 
 class HomeForSale: private Uncopyable {
 ...
 };

 

다형성을 가진 기본(Base)클래스 라면, 소멸자를 virtual로 선언하자

  • 팩토리 함수(factory function)
    TimeKeeper 같은 클래스에서 다양한 파생 클래스가 생성되었을 때, 이 함수들에서 부모 클래스의 기능들을 사용하거나, 자식 클래스들의 동적 인스턴스들을 공통적으로 처리하는 함수가 필요하다고 가정했을 때, 이러한 포인터가 필요할 수 있다.

 class TimeKeeper {
 public:
 TimeKeeper();
 ~TimeKeeper();
 ...
 };
 class AtomicClock: public TimeKeeper { ... };
 class WaterClock: public TimeKeeper { ... };
 class WristWatch: public TimeKeeper { ... };
 
 TimeKeeper* getTimeKeeper();
 
 TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
 // from TimeKeeper hierarchy
 ...
 delete ptk;

 

더보기

팩토리 함수를 사용하면, 생성될 객체의 클래스를 특정짓지 않고도 객체를 생성할 수 있다. 생성자를 직접 호출하는 대신, 클래스의 생성 과정을 추상화하고 그 결과로 클래스의 포인터를 돌려 준다. (이 포인터는 부모 클래스 타입으로, 어떤 파생 클래스든 가리킬 수 있음) 어느 클래스의 객체가 생성될 지 런타임에서 결정되는 경우에 사용하기 좋다.

  1. 팩토리 함수가 객체의 생성 과정을 담당하고 encapsulate 한다. 클라이언트 코드는 이에 대해 모른다.
  2. 복잡한 오브젝트 계층 구조를 짜거나 다형성을 가진 클래스들을 사용할 때, 코드를 유연하게 짤 수 있다.
  3. 메모리 관리에 유리하다 - 결과값으로 생성된 객체의 포인터를 돌려주므로 ..

디자인 패턴 중 Factory Method Pattern과 관련 있다.

아래는 예시 코드이다.

#include <iostream>
#include <cstring>  // For strcmp

// Base class
class Animal {
public:
    virtual void Speak() = 0;  // Pure virtual function
    virtual ~Animal() {}       // Virtual destructor for proper cleanup
};

// Derived classes
class Dog : public Animal {
public:
    void Speak() override {
        std::cout << "Woof!\n";
    }
};

class Cat : public Animal {
public:
    void Speak() override {
        std::cout << "Meow!\n";
    }
};

// Factory function to create Animal instances
Animal* CreateAnimal(const char* type) {
    if (std::strcmp(type, "dog") == 0) {
        return new Dog();
    } else if (std::strcmp(type, "cat") == 0) {
        return new Cat();
    } else {
        return nullptr;  // Return nullptr if type is unrecognized
    }
}

// Example usage
int main() {
    // Create a Dog
    Animal* myAnimal = CreateAnimal("dog");
    if (myAnimal) {
        myAnimal->Speak();
        delete myAnimal;  // Manual deletion required to prevent memory leak
    }

    // Create a Cat
    myAnimal = CreateAnimal("cat");
    if (myAnimal) {
        myAnimal->Speak();
        delete myAnimal;  // Manual deletion required
    }

    return 0;
}

이렇게 만들어진 인스턴스를 제거할 때, delete 를 호출하면 부모 클래스인 TimeKeeper 포인터를 통해 해제되기 때문에, 그에 해당되는 부모 클래스 정보들만 delete 될 뿐, 자식 클래스에서 추가된 부분들 (예 - AtomicClock 클래스에서 정의된 멤버 변수 등)에 대해서는 메모리 누수가 발생한다.

AtomicClock 의 소멸자가 호출되지 않게 되기 때문이다..

This is a recipe for disaster, because C++ specifies that when a derived class object is deleted through a pointer to a base class with a non-virtual destructor, results are undefined.

 

이 문제를 방지하려면, 부모 클래스의 소멸자에 아래와 같이 virtual로 선언해 준다.

class TimeKeeper {
 public:
 TimeKeeper();
 virtual ~TimeKeeper();
 ...
 };
 TimeKeeper *ptk = getTimeKeeper();
 ...
 delete ptk;

 

이것은 virtual function을 1개라도 가지고 있는 클래스에는 모두 해당한다 (이 클래스는 부모 클래스로서 사용되도록 의도되었다 라는 의미이므로). 그렇지 않은 경우에는 좋은 선택이 아니다.

이 내용은 부모 클래스의 인터페이스로 자식 클래스를 조작하는, 즉 다형성을 가지도록 설계된 부모 클래스의 경우에만 해당된다. (부모 클래스가 될 수는 있지만 이렇게 설계되지는 않은 클래스들도 있다)

그 이유는 아래와 같다 -

virtual 함수를 사용하는 클래스의 객체들에는 virtual table 을 가리키는 virtual table pointer라는 것이 필요하다. 이 테이블에는 런타임에서 어느 virtual 함수가 불려아 하는지에 대한 정보도 함께 들어있어야 하는데, 이것이 객체의 크기를 50%~100%까지 늘리게 된다.

 

포인터의 크기

64-bit 운영체제에서 포인터의 크기(=주소의 크기) 는 항상 64 비트이다. 이것은 포인터가 가리키는 자료형과는 무관하게 모두 동일하다. 포인터에 부여된 자료형은 해당 주소부터 몇 바이트를 사용할 것인지를 알려주는 역할을 한다. 포인터가 가리키는 주소로부터 실제 데이터를 얻을 때, 자료형에 관한 정보가 있어야 이것을 사용할 수 있다.

void 형 포인터를 사용할 수도 있다 - 이 포인터는 어떤 자료형도 가리킬 수 있다. 이 포인터는 cast 하기 전에는 포인터끼리의 산술이나 dereferencing은 할 수 없다.

 

순수 가상 함수(Pure virtual function)

가상 함수 중 아래와 같이 함수 본문이 정의되지 않은 가상 함수를 뜻한다.

`virtual void Draw() const = 0;`

순수 가상 함수를 하나라도 포함한 클래스는 추상 클래스이고, 자신의 인스턴스를 만들 수 없다. 이 클래스의 자식 클래스에서 반드시 이 함수의 본문을 정의해 주어야 한다.

아래와 같은 형태로 자식 클래스에서 override 한다.

void Draw() const override { std::cout << "Drawing a Circle" << std::endl; }

 

이 함수는 자식 뿐 아니라 손자 클래스에서도 계속 override 할 수 있는 가상 함수이다.

더 이상 가상 함수가 아닌 일반 함수로 선언하려면 override 뒤에 final을 붙인다.

가상 함수가 아닌 함수를 자식 클래스에서 같은 이름으로 선언하는 경우, 이것은 override 가 아닌 hide 이다 - 이렇게 되면 자식 클래스의 인스턴스에서 호출하거나, 자식 클래스 형의 포인터를 통해 호출했을 때, 시그니처가 일치하더라도 호출할 수 없다.

(명시적으로 부모 클래스::함수 형태로 호출해야 한다)