컴파일러가 자동으로 만드는 기본 함수들에 주의
컴파일러는 빈 클래스에서도 아래 함수들를 자동으로 생성한다.
- (인자 없는)생성자
- 복사 생성자
- 대입 연산자
- 소멸자
위 함수를 모두 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;
팩토리 함수를 사용하면, 생성될 객체의 클래스를 특정짓지 않고도 객체를 생성할 수 있다. 생성자를 직접 호출하는 대신, 클래스의 생성 과정을 추상화하고 그 결과로 클래스의 포인터를 돌려 준다. (이 포인터는 부모 클래스 타입으로, 어떤 파생 클래스든 가리킬 수 있음) 어느 클래스의 객체가 생성될 지 런타임에서 결정되는 경우에 사용하기 좋다.
- 팩토리 함수가 객체의 생성 과정을 담당하고 encapsulate 한다. 클라이언트 코드는 이에 대해 모른다.
- 복잡한 오브젝트 계층 구조를 짜거나 다형성을 가진 클래스들을 사용할 때, 코드를 유연하게 짤 수 있다.
- 메모리 관리에 유리하다 - 결과값으로 생성된 객체의 포인터를 돌려주므로 ..
디자인 패턴 중 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 이다 - 이렇게 되면 자식 클래스의 인스턴스에서 호출하거나, 자식 클래스 형의 포인터를 통해 호출했을 때, 시그니처가 일치하더라도 호출할 수 없다.
(명시적으로 부모 클래스::함수 형태로 호출해야 한다)
'C++ & etc' 카테고리의 다른 글
[Effective C++] Chapter 01. C++ 문법을 따르자 (0) | 2024.11.17 |
---|---|
Maxscript / DirectX Shader for 3dsMax (2) | 2023.10.08 |
VS Code에서 C++ 컴파일 환경 만들기 (1) | 2023.10.08 |
3dsmax 에서 HLSL 커스텀 셰이더 사용하기 (with Unreal) (0) | 2023.02.09 |