#!if 문서명2 != null
, [[객체 지향 프로그래밍]]#!if 문서명3 != null
, [[]]#!if 문서명4 != null
, [[]]#!if 문서명5 != null
, [[]]#!if 문서명6 != null
, [[]]| 프로그래밍 언어 문법 | |
| {{{#!folding [ 펼치기 · 접기 ] {{{#!wiki style="margin: 0 -10px -5px; word-break: keep-all" | 프로그래밍 언어 문법 C(포인터 · 구조체 · size_t) · C++(이름공간 · 클래스 · 특성 · 상수 표현식 · 람다 표현식 · 템플릿/제약조건/메타 프로그래밍) · C# · Forth · Java · Python(함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript(표준 내장 객체, this) · Haskell(모나드) · 숨 |
| 마크업 언어 문법 HTML · CSS | |
| 개념과 용어 함수(인라인 함수 · 고차 함수 · 콜백 함수 · 람다식) · 리터럴 · 문자열 · 식별자(예약어) · 상속 · 예외 · 조건문 · 반복문 · 비트 연산 · 참조에 의한 호출 · eval · 네임스페이스 · 호이스팅 | |
| 기타 #! · == · === · deprecated · GOTO · NaN · null · undefined · S-표현식 · 배커스-나우르 표기법 · 콰인(프로그래밍) | }}}}}} |
| 프로그래밍 언어 목록 · 분류 · 문법 · 예제 |
1. 개요2. 정보 은닉 (Information Hiding)3. 캡슐화 (Encapsulation)4. 인스턴스 생성5. 생성자 (Constructor)
5.1. 기본 생성자5.2. 멤버 초기자5.3. 표준 라이브러리: initializer_list5.4. explicit
6. 소멸자 (Destructor)7. 동적 메모리 할당8. 멤버 한정자9. friend10. 연산자 오버로딩11. 변환 함수12. 상속13. 특수 멤버 함수5.4.1. 설계 패턴: 함수 꼬리표 분배
5.5. 조건부 explicit13.1. 기본 생성자13.2. 소멸자13.3. 복사 생성자13.4. 복사 대입 연산자13.5. 이동 생성자13.6. 이동 대입 연산자13.7. 동등 비교 연산자13.8. 3방향 비교 연산자
14. 복사 생략15. delete16. 암시적 특수 멤버 함수17. default18. 자명함19. 클래스 유형20. 둘러보기1. 개요
|
C++의 클래스와 객체 지향 프로그래밍에 관해 포괄적으로 설명하는 문서. 객체 지향 언어들은 보통 클래스라고 부르는 독립되고 주어진 역할을 수행할 수 있는 객체를 가지고 있다. 마찬가지로 C++에서도 클래스를 사용할 수 있다. 프레임워크가 존재하는 언어에서 볼 수 있는 클래스와는 조금 다르지만, 지향하는 바는 같다.
#!syntax cpp
class Class { int variable; };
Class instance;
우리가 지금까지 원시자료형을 사용했던 것처럼, 클래스의 이름을 자료형으로 사용할 수 있다 [1]. 클래스로 정의한 변수를 인스턴스 (Instance) 또는 직역해서 개체 혹은 실체라고 칭한다. 객체(Object)라고 부르는 경우도 있지만 Object는 보통 클래스의 이름 자체에 붙는 경우가 많다. 클래스는 구조체처럼 내부에 변수를 저장할 수 있다. 본래 C++의 클래스는 C언어의 구조체(
struct)로부터 내려왔지만 지금은 독특한 요소를 많이 가지고 있다. 그러면 무엇이 어떻게 다를까?구조체는 그저 순수한 메모리 규격으로써 마치 JSON처럼 이름 붙인 자료형만을 나열한 꼴이었다. 그리고 구조체의 상호작용은 반드시 구조체 인스턴스와 별도의 함수가 필요했다. 특히 구조체 내부에 함수 인터페이스를 만들려면 별도로 구조체 바깥에 정의된 함수를 포인터의 형태로 가져와야 했다. 거기에 함수에서 구조체를 참조할 포인터 매개변수도 필요했다. 링킹 문제 때문에 함수의 정의를 별도의 소스 파일에 구현하는 것도 필수였다. 이렇게 가져온 함수가 어떻게 동작하는지 알려줄 방법도 부족했으며 코드를 읽기 불편했다. 마지막으로 구조체의 속성을 숨기거나, 최종 사용자에게 필요한 정보만 보여줄 수가 없었다.
C++의 클래스는 생성자로 인스턴스가 생성되는 방식을 결정할 수 있고, 소멸자를 통해 객체가 파괴되는 방식도 결정할 수 있다. 함수 인터페이스를 몸체(Class Body) 안으로 들여 간편한 제어가 가능해졌다. 클래스의 핵심 기능 중 하나는 내부 변수를 클래스 내부의 함수 안에서 언제든지 이용할 수 있다는 점이다. 그러나 이것만으로는 클래스가 구조체에서 발전하였다고 말하기는 힘들다. 다른 클래스로부터 속성을 받아오는 상속과 다형성은 코드의 재사용성을 향상시켰다. 연산자 오버로딩을 통해 어디에서나 일관된 코드 작성을 할 수 있다. 그리고 정적 데이터와 템플릿은 클래스가 어떤 특징을 가지고 있는지 메타 데이터로 표현할 수 있도록 해줬으며, 확장성 향상에 도움이 된다. C++의 클래스는 단순히 정보를 읽고 쓰는 인터페이스가 아니라 정보가 어떻게 흘러가는지 정의한 규칙 모음이라고 말할 수 있다.
2. 정보 은닉 (Information Hiding)
접근성 지시자 (Access Specifiers)클래스 몸체 안에 접근성 지시자와
:를 붙이면 일괄적으로 멤버의 접근 권한을 정할 수 있다. [2] 접근성 지시자는 private, public, protected 세 종류가 존재한다. 클래스의 핵심을 제외하고 사용에 불필요한 요소를 숨길 수 있다.#!syntax cpp
struct MyStruct
{
int myValue;
};
class MyClass
{
int myValue;
};
클래스는 구조체처럼 내부에 변수를 가질 수 있으며 사용법도 같다.#!syntax cpp
MyStruct instance_struct;
// 문제 없음.
instance_struct.myValue = 10;
MyClass instance_class;
// 컴파일 오류!
instance_class.myValue = 10;
하지만 이 코드를 쓰면 오류가 발생한다. 이유는 클래스의 멤버는 기본적으로 접근 권한이 비공개(Private)로 되어있기 때문이다.#!syntax cpp
class MyClass
{
public:
int myValue;
private:
// 클래스의 기본 접근 권한은 private로써 외부에 보이지 않는다.
};
MyClass instance_class;
// 문제 없음.
instance_class.myValue = 10;
상기 코드와 같이 public:을 붙이면 이후 선언된 멤버들은 외부에서 접근할 수 있다.public은 해당 클래스의 외부에서도 접근할 수 있음을 뜻한다. private은 해당 클래스 내부의 멤버에서만 접근할 수 있다. 클래스의 멤버들을 외부에서 바로 사용할 수 없는 이유가 바로 접근 권한 때문이다. 클래스 안의 멤버들은 기본적으로 숨겨져 있기 때문에 작성자가 직접 접근 권한을 설정해줘야 한다.#!syntax cpp
struct MyStruct
{
// 구조체의 기본 접근 권한은 public이다.
int myValue;
private:
int myHidden;
};
참고로 구조체 역시 접근성 지정자를 기입할 수 있다. 구조체의 기본 권한은 공개(Public)다.#!syntax cpp
class MyParent
{
protected:
int interitedProperty;
};
class MyClass : public MyParent
{};
protected(보호됨)는 자신 또는 해당 클래스를 상속받는 자식 클래스의 멤버에서만 접근할 수 있음을 뜻한다. 자세한 내용은 이후 상속 문단에서 설명한다.3. 캡슐화 (Encapsulation)
클래스 멤버 (Class Member)클래스는 멤버(Member)이라고 칭하는 여러가지 정보를 담을 수 있다. 그 동안 변수, 함수를 만들었던 것 처럼 똑같이 작성할 수 있다.
멤버의 종류에는 데이터 멤버, 멤버 함수, 멤버 자료형, 그리고 정적 데이터 멤버와 정적 멤버 함수 5가지가 있다. 데이터 멤버는 구조체처럼 내부에 정의된 변수를 말한다. 멤버 함수는 C++에서 도입된 기능 중에 하나로, 클래스 인스턴스에서 사용할 수 있는 함수를 말한다. 멤버 자료형은
using으로 선언한 자료형 별칭을 말한다. 정적 데이터 멤버와 정적 멤버 함수는 static 또는 extern으로 지정한 멤버들이다.#!syntax cpp
class MyClass
{
public:
int dataMember;
void MemberFunction();
};
MyClass instance;
instance.dataMember;
instance.MemberFunction();
데이터 멤버와 멤버 함수는 클래스의 인스턴스를 만들어야 사용할 수 있다. 인스턴스의 오른쪽에 .[구두점] 연산자를 붙여서 접근할 수 있다. 만약 인스턴스가 포인터인 경우 ->[화살표] 연산자로 접근할 수 있다. 또한 인스턴스의 데이터 멤버를 참조형으로 가져오면 마찬가지로 참조 변수로도 접근할 수 있다.한편 표준에서는 멤버라고 부르지만, 실상에서는 다른 언어에서 흔히 부르는대로 데이터 멤버는 필드(Field)로, 멤버 함수는 메서드(Method)라고 부르는 경우가 많다.
- <C++ 예제 보기>
#!syntax cpp // 표준 라이브러리의 동적 문자열 클래스 std::string 가져오기 import <string>; // 클래스 `Squirrel` class Squirrel { public: // 자료형 멤버 `name_t` (자료형 별칭) using name_t = std::string; // 정적 데이터 멤버 `canFly` static inline const bool canFly = false; // 멤버 함수 `SetName` void SetName(name_t name) { myName = name; } // 멤버 함수 `GetAcornsCount` size_t GetAcornsCount() { return myAcornsCount; } // 멤버 함수 `GetWeight` float GetWeight() { return myWeight; } // 정적 멤버 함수 `CanFly` static constexpr bool CanFly() noexcept { return canFly; } // 자료형 멤버 `Nest` (내포 클래스) class Nest {}; // 복합적 자료형의 데이터 멤버: std::string myName name_t myName; // 자료형 별칭의 데이터 멤버: size_t myAcornsCount; size_t myAcornsCount; // 원시 자료형의 데이터 멤버: float myWeight float myWeight; };
myName), 도토리의 개수(myAcornsCount), 무게(myWeight) 세 가지의 비정적 데이터 멤버와, 정적 데이터 멤버로 비행 가능 여부(canFly), 멤버 함수로는 이름을 정하는 SetName, 도토리의 개수를 반환하는 GetAcornsCount, GetWeight, 정적 멤버 함수 CanFly, 둥지 클래스 Nest, 그리고 이름 자료형을 지칭하는 자료형 별칭 name_t까지 10개의 멤버를 가졌다.3.1. 데이터 멤버
#!syntax cpp
class MyClass
{
int dataMember;
};
비정적 데이터 멤버(Non-Static Data Member)데이터 멤버는 클래스 안에 정의하고 사용할 수 있는 변수를 이르는 용어다. C++에서는 구조체의 내부 변수도 데이터 멤버라고 한다.
auto와 decltype(auto)를 사용할 수 없다.3.2. 멤버 함수
비정적 멤버 함수(Non-Static Member Function)멤버 함수(Member Function)는 클래스 내부에 정의하고 사용할 수 있는 함수를 이르는 용어다.
#!syntax cpp
class MyClass
{
public:
void MemberFunction();
void MemberFunction(const int); // 함수 오버로딩
};
MyClass instance;
instance.MemberFunction();
instance.MemberFunction(100);
클래스의 인스턴스에 . 연산자를 붙이면 인스턴스로부터 함수를 실행할 수 있다.#!syntax cpp
class MyClass
{
//private:
void MemberFunction();
void MemberFunction(const int); // 함수 오버로딩
public:
void MemberFunction(float&);
};
MyClass instance;
// 컴파일 오류!
instance.MemberFunction(5);
함수 오버로딩을 할 수 있다. 이때 비공개 멤버는 외부에서 사용할 수 없다.#!syntax cpp
class MyClass
{
public:
int MemberFunction()
{
MyHiddenFunction();
return myPrivate;
}
private:
void MyHiddenFunction();
int myPrivate;
};
멤버 함수에서는 비공개 멤버에 접근할 수 있다.3.3. this
this 포인터인스턴스 자기 자신의 주소를 의미하는 예약어다 [5]. 멤버 함수에서 다른 멤버에 접근할 때는 암시적으로
this->가 사용된다.- <C++ 예제 보기>
#!syntax cpp import <string>; class Squirrel { public: using name_t = std::string; void SetName(name_t Name) { // this->가 없어도 문제 없음 this->Name = Name; } name_t Name(name_t Name) { // 같은 이름인 멤버 함수와 데이터 멤버는 반드시 구분해줘야 함 (*) this->Name = Name; } void Name() { // (*) return this->Name; } size_t AcornsCount() { // (*) return this->AcornsCount; } float Weight() { // (*) return this->Weight; } name_t Name; private: size_t AcornsCount; float Weight; };
3.4. 정적 멤버
|
::를 붙여 접근할 수 있다.3.4.1. 정적 데이터 멤버
static 또는 extern 지시자가 붙은 데이터 멤버다. 클래스 내외부에서 언제든지 사용할 수 있다.#!syntax cpp
class Object
{
public:
static inline const bool isCreatable = true;
static constexpr int maxObjectId = 10'000;
private:
static int lastObjectId;
};
bool creatable = Object::isCreatable;
int max_id = Object::maxObjectId;
// 컴파일 오류!
int last_id = Object::lastObjectId;
정적 멤버 역시 접근 권한을 설정할 수 있다.#!syntax cpp
Object instance;
bool creatable = instance.isCreatable;
int max_id = instance.maxObjectId;
인스턴스를 통해서도 접근할 수 있다. 그러나 다른 비정적 멤버와 구별할 수 없으며 정적 멤버의 목적에서 벗어나므로 지양해야 한다.정적 데이터 멤버는 항상 초기화를 해주어야 한다 (정확히는 초기화할 수 있어야 한다). 만약 초기화를 하지 못하면 컴파일 오류가 발생한다. 예를 들어서 클래스가 기본 생성자가 없으면 문제가 발생한다.
C++에는 메타클래스가 없으며 클래스 자체는 메모리 규격만을 나타낸다. 그래서 인스턴스의 고정된 메모리 주소로부터 상대적 위치만 저장해서 멤버를 구별한다. C++에선 정적 멤버가 아니더라도 일단
Squirrel::myWeight와 같이 이름만이라도 접근할 수는 있다. 그러나 메모리 접근 위반 0x00000024 참조! 따위의 런타임 오류가 발생할 것이다. 여기서 0x00000024가 클래스 Squirrel에 대한 필드 myWeight의 상대적 주소다. 그러나 정적 멤버는 프로그램 안에서 고정된 메모리 주소를 갖고 있어서 실행시간 내내 일정한 메모리 위치에 존재한다. 덕분에 문맥 상관없이 항상 접근할 수 있는 객체가 된다.3.4.2. 정적 멤버 함수
#!syntax cpp
class Object
{
public:
static Object* CreateInstance()
{
// objectId는 단 한번 할당된다.
static int objectId = firstId;
return new Object{ .myID = objectId++, .myName = DefaultName };
}
int myID;
std::string myName;
private:
static inline const int firstId = 1000;
static const std::string DefaultName;
};
int main()
{
auto inst = Object::CreateInstance();
}
상기 코드는 공장 함수를 써서 객체를 생성하는 구현을 보여주고 있다.#!syntax cpp
template<typename crtp>
class ISingleton
{
public:
static void SetInstance(crtp* inst)
{
if (Instance == nullptr)
{
crtp = inst;
}
else
{
throw "Singleton error!";
}
}
[[nodiscard]]
static crtp* GetInstance() noexcept
{
return Instance;
}
protected:
static inline constinit crtp* Instance = nullptr;
}
std::string Object::DefaultName = "Object";
정적 멤버가 가장 잘 활용되는 곳은 싱글톤 패턴 클래스다. 싱글톤은 프로그램에서 클래스에 대하여 유일하게 존재하는 인스턴스를 구현하는 패턴인데, static이 정확하게 이 목적에 부합한다.3.4.3. 멤버 자료형
멤버 자료형은 클래스 안에 포함된 또다른 자료형을 이르는 용어다. 멤버 자료형은 반드시 클래스 이름에::를 붙여 사용해야 한다.#!syntax cpp
class Integer
{
public:
using Comparator = std::less<int>;
int myValue;
};
int main()
{
Integer a{ 5 }, b { 7 };
using Comparator = Integer::Comparator;
Comparator comparator;
bool result = comparator(a.myValue, b. myValue);
}
멤버 자료형에는 두 종류가 있는데 첫번째는 typedef/using으로 선언한 자료형 별칭(Type Aliases)이 있다.#!syntax cpp
class MyClass
{
public:
struct NestedClass
{
int value;
};
private:
union NestedUnion
{
std::intptr_t pointer_representation;
int* pointer;
};
public:
int MemberFunction()
{
NestedClass nested{ .value = 9 };
return nested.value;
}
};
내포 클래스(Nested Struct/Class)두번째는 내부의 구조체, 클래스를 부르는 내포 클래스와 내부의 공용체(Union)를 부르는 내포 공용체(Nested Union)가 있다.
4. 인스턴스 생성
4.1. 기본 초기화
기본 초기화 (Default Initialization)기본 초기화를 이용해 원시자료형을 쓰는 것과 같은 방식으로 인스턴스를 생성할 수 있다.
#!syntax cpp
class MyClass
{
int myValue;
};
MyClass instance;
데이터 멤버에는 기본값이 대입되지 않으며 메모리만 할당한다. 때문에 데이터 멤버에 쓸모없는 값이 들어있기 마련이라 다른 멤버 초기화 방법을 쓰거나, 괄호를 붙이면 된다.#!syntax cpp
// instance1.myValue, instance2.myValue, instance3.myValue의 값은 0
MyClass instance1 = MyClass();
MyClass instance2{};
MyClass instance3 = MyClass{};
괄호를 붙여서 초기화하면 인스턴스의 비정적 데이터 멤버들이 기본값으로 초기화된다 [6]. 데이터 멤버 중에 클래스 인스턴스가 있으면 그 인스턴스도 함께 기본값으로 초기화된다.4.1.1. 데이터 멤버의 기본값
#!syntax cpp
class MyClass
{
public:
int myValue = 5000;
};
// instance1.myValue의 값은 5000
MyClass instance1;
// instance2.myValue의 값은 5000
constexpr MyClass instance2;
비정적 데이터 멤버에 기본값이 주어지면 그 값으로 초기화된다.4.2. 집결 초기화
집결 초기화 (Aggregate Initialization)집결 초기화는 C언어의 구조체로부터 내려오는 인스턴스 초기화 방법이다. 인스턴스를 만들 때 인자를 중괄호 혹은 소괄호안에 전달한 순서대로 비정적 데이터 멤버에 초기값을 할당할 수 있다. 이를 지원하는 구조체/클래스/공용체를 각각 Aggregate Struct/Class/Union이라 부른다.
#!syntax cpp
class Complex
{
public:
float real;
float imaginary;
};
Complex complex1{ 10.0f, 0.0f };
Complex complex2{ -3.0f, 1.8f };
constexpr Complex complex3{ 9.8f, 4.0f };
상기 코드는 실수와 허수를 담는 클래스의 예시를 보여주고 있다.- <C++ 예제 보기>
#!syntax cpp class Person { public: using name_t = std::string; static Person CreatePerson(const name_t& name, unsigned int age, Squirrel&& pet, const name_t& address) { // 암시적 생성 return Person{ name, age, std::move(pet), address }; } name_t myName; unsigned int myAge; Squirrel myPet; protected: std::string myAddress; }; int main() { // (1) // Squirrel squirrel{ "Lamarr", 3, 1.6f }; 는 접근 권한 때문에 실행할 수 없다 // Squirrel의 유일한 공용 멤버는 `myName` 뿐이다 // ()는 사용할 수는 있지만 권장되지 않는다 Squirrel headcrab("Lamarr"); // (2) // C++11 이전부터 가능했던 집결 초기화 방식 // `dictator`의 멤버 `myPet`은 기본값으로 초기화되므로 굳이 초기화하지 않아도 문제 없다. // 인스턴스 생성에 등호를 쓰는 건 권장되지 않는다 Person dictator = { "Breen", 52 }; // (3) // C++11부터 가능한 집결 초기화 방식 // Person phdoctor{ "Kleiner", 67, std::move(squirrel), "Massachusetts" }; 는 접근 권한 때문에 실행할 수 없다 // Person의 마지막 멤버 myAddress는 private다 Person phdoctor{ "Kleiner", 67, std::move(headcrab) }; // (4) Squirrel robot("Dog"); Person freeman = CreatePerson("Gorden", 27, std::move(robot), "Seattle"); }
private, protected 멤버들에 접근할 수 없으므로 초기화도 할 수 없다. 이 경우 별도의 초기화 방법이 필요하다. 상단의 예제에서 클래스 Person은 private 멤버 myAddress의 초기화를 위해 정적 생성 함수를 제공하고 있다.집결 초기화는 간단하게 쓸 수 있지만 주의할 점이 있다. 일단 전술한 접근 권한 문제도 있고 외부에서 어떤 멤버가 초기화되는지 전혀 알 수 없다는 것이다. 집결 초기화는 해당 클래스를 작성한 사람이 아니면 활용이 거의 불가능하다. 그나마 잘 지원하려면
class, struct의 구분을 하는 걸 추천한다. C++에서 둘의 차이는 기본 권한이 private인지 public인지 여부 밖에 없지만 이게 객체 작성의 편의성과 생산성, 구현 시간, 그리고 미적으로도 영향을 준다. 많은 서적에서도 이 둘을 구분하는 걸 추천하는 편이다. 집결 초기화는 struct에만 쓰도록 하고, class에는 후술할 생성자 함수를 쓰는 게 좋다. 4.3. 지정 초기화
지정 초기화 (Designated Initialization)C++20지정 초기화는 전술한 집결 초기화의 특수한 형태다. 중괄호 안에서
.뒤에 데이터 멤버의 이름을 붙이면 해당 데이터 멤버를 초기화할 수 있다. C언어에서 최근에 지원하기 시작한 초기화 방법을 C++20부터 지원하고 있다. 불편함이 많았던 집결 초기화 대신 JSON처럼 이름을 명시할 수 있도록 문법이 개선된 것이 지정 초기화다.#!syntax cpp
class House
{
public:
using name_t = std::string;
name_t myName;
Person* myMembers;
size_t membersCount;
private:
unsigned long long membersAsset;
public:
int myAge;
unsigned long long myPrice;
};
int main()
{
// (1)
// 현재 스코프에서 공개된 멤버만 초기화할 수 있다
Squirrel squirrel
{
.myName = "Ramsy",
};
// (2)
// 마지막 항목의 `,`는 붙이지 않아도 상관없다
auto person = Person
{
.myName = "Sakura",
.myAge = 25
};
// (3)
House house
{
.myName = "Sweet home",
.myMembers = new Person[5]
{
Person{ .myName="Daddy" },
Person{ .myName="Mommy" },
Person{ .myName="Me" },
Person{ .myName="Parents' son" },
Person{ .myName="My litte brother" }
},
.membersCount = 5,
.myAge = 7,
.myPrice = 100'000'000
};
}
한편 C언어에서는 멤버 순서를 마음대로 둘 수 있었으나 C++에서는 무조건 순서대로 초기화해야 한다. 중간에 누락되는 멤버가 없어야 한다.5. 생성자 (Constructor)
|
생성자는
클래스 이름([매개 변수, ...]) 형식으로 정의되는 특수 함수다 [7]. 인스턴스가 생성되는 동시에 인스턴스의 초기화를 위하여 실행된다. 생성자는 원시 자료형과는 구분되는 특별한 기능으로써 데이터가 어떻게 생성 되는지에 관여한다.생성자가 있으면 집결 초기화와 지정 초기화는 어떤 형태로든 사용할 수 없다.
5.1. 기본 생성자
기본 생성자 (Default Constructor)매개변수가 없는 생성자를 기본 생성자라고 부른다. 앞서 소개했던 기본 초기화는 기본 생성자를 사용한 초기화다.
#!syntax cpp
class MyClass
{
public:
MyClass()
{
myValue = 3333;
}
int myValue;
};
// instance.myValue의 값은 3333
MyClass instance;
만약 기본 생성자를 가지고 있다면, 괄호없는 기본 초기화를 할 때 기본 생성자가 실행된다.#!syntax cpp
class MyClass
{
public:
MyClass(int default_value = 2222)
{
myValue = default_value;
}
int myValue;
};
// instance.myValue의 값은 2222
MyClass instance;
모든 매개변수가 선택적이라면 역시 기본 생성자로 취급된다. 다만 매개변수가 사용되면 그때는 기본 초기화가 아니다.5.2. 멤버 초기자
|
멤버 초기자 목록, 내지는 이니셜라이저라고 부르는 이 기능은 인스턴스의 생성 순간에 명시한 비정적 데이터 멤버를 초기화하는 역할을 수행한다. 생성자의 매개변수 뒤쪽 닫는 괄호 뒤에
:와 비정적 데이터 멤버의 이름, 값을 전달해서 명시적으로 멤버의 초기화를 수행할 수 있다. 중요한 점은 데이터 멤버에 대입 연산을 하는 게 아니라, 인스턴스와 동시에 멤버가 생성된다는 것이다. 같은 맥락으로 멤버 변수로써의 클래스 인스턴스의 초기화도 가능하다. 이를 잘 활용하면 객체 생성의 번거로움을 줄일 수 있다.#!syntax cpp
struct Squirrel
{
public:
Squirrel(std::string name, int age)
: myName(name), myAge(age)
{}
// 필드의 선언은 뒤쪽에 위치시킬 수 있다.
int myAge;
std::string myName;
};
초기화되는 데이터 멤버의 순서는 지킬 필요가 없다.#!syntax cpp
class Person
{
public:
using name_t = std::string;
// (1) 모든 데이터 멤버를 초기화하는 생성자.
Person(const name_t& name, unsigned int age, Squirrel&& pet, const std::string& address)
: myName(name), myAge(age)
, myPet(std::move(pet))
, myAddress(address)
{}
// (2) 반드시 모든 멤버를 초기화할 필요는 없다.
Person(const name_t& name, unsigned int age, Squirrel&& pet)
: myName(name), myAge(age)
, myPet(std::move(pet))
{}
// (3) 다른 생성자를 호출할 수 있다.
// 그러나 이때 다른 멤버를 같이 초기화 할 수는 없다.
Person(const name_t& name, unsigned int age)
: Person(name, age, Squirrel{})
{}
name_t myName;
unsigned int myAge;
Squirrel myPet;
protected:
std::string myAddress;
};
멤버 초기자에선 다른 생성자를 중복으로 호출할 수 있다. 데이터 멤버가 초기화되기 전에 현재 인스턴스가 먼저 초기화된다.상단의 예제에서는 생성자의 닫는 괄호 뒤에 오는
: myName(name), myAge(age), ...가 멤버 초기자 목록이다. name_t myName{ name };, unsigned int myAge{ age };, Squirrel myPet{ std::move(pet) };, std::string myAddress{ address };가 실행된다.5.3. 표준 라이브러리: initializer_list
초기자 목록 클래스 (Initializer List)C++11std::initializer_list<T>는 C++11부터 지원되는 가변 생성자를 지원하는 유틸리티 클래스다. 초기자 목록 클래스는 사용자가 만들 수 없고 오직 생성자에 중괄호와 다수의 인자를 전달했을 때에만 생성된다. [8] 가변 인자를 지원하며, 템플릿으로 제작되어 모든 자료형을 지원한다.- <C++ 예제 보기>
#!syntax cpp import <initializer_list>; class Adder { public: Adder(std::initializer_list<int> list) { for (auto it = list.begin(); it != list.end() ++it) { mySummary += *it; } } // 오버플로우 방지 long long mySummary; }; class Multiplier { public: Multiplier(std::initializer_list<int> list) { for (auto& value : list) { mySummary *= value; } } // 오버플로우 방지 long long mySummary; }; int main() { // (1) Adder adder{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 결과는 45 const int summary_added = adder.mySummary; // (2) Multiplier multi{ 2, 4, 8, 16, 32 }; // 결과는 4096 const int summary_multiplied = multi.mySummary; }
<initializer_list> 헤더를 가져와야 한다.5.4. explicit
명시적 (Explicit)explicit은 인스턴스를 생성할 때, 사용자에게 반드시 클래스의 이름을 쓰도록 강제한다.- <C++ 예제 보기>
#!syntax cpp class Integer { public: Integer(int value) noexcept : myValue(value) {} int myValue; }; class Adder { public: Adder(std::initializer_list<int> list) { for (auto& value : list) { mySummary += value; } } Adder(std::initializer_list<Integer> list) { for (auto& value : list) { mySummary += value.myValue; } } long long mySummary; }; int main() { // (1) // 어떤 생성자 오버로딩을 선택하는가? Adder adder0{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // (2) // 이 경우는? Adder adder1{ { 0 }, { 1 }, { 2 }, { 3 }, { 4 }, { 5 }, { 6 }, { 7 }, { 8 }, { 9 } }; // (3) // 이 경우에는 intializer_list<int>가 선택된다.`Integer` 클래스에 기본 생성자가 없으므로. Adder adder2{ {}, { 1 }, {}, { 3 }, {}, { 5 }, {}, { 7 }, {}, { 9 } }; // (4) // 이 경우에는 intializer_list<Integer>가 선택된다. Adder adder3{ { 0 }, { 1 }, { 2 }, { 3 }, { 4 }, { 5 }, Integer{ 6 }, { 7 }, { 8 }, { 9 } }; }
위쪽의 생성자 예제를 보면
Person 클래스의 생성자에 `Squirrel{}`으로 다람쥐 인스턴스를 만들어서 전달했으나 사실 `{}`만으로 인스턴스를 만들 수 있다.이는 C++에 있는 문법적 문제 중 하나이다. 사용자가 지금 만들어지는 인스턴스가 어떤 클래스인지 바로 알 수 없고, 어떤 인자에 전달하는지 알아보기 힘들며, 마지막으로 기본 초기화를 할 때 사용자가 의도하지 않은 동작이 일어날 수 있다는 것이다. 이것 때문에 다른 생성자와 혼동되어 컴파일 오류를 일으키기도 한다.
사용자는 오로지 어떤 방식으로 인스턴스를 생성하는 방법을 강제하고자 하는데, 그걸 위해 생성자가 필요한 법이다. 그러나 생성자를 정의하더라도 실제로 인스턴스를 생성하는 장소에 가면 여전히 뭘 어떻게 생성하는지 알 수가 없다는 것이다. 물론 IDE 등 개발 도구의 발전으로 매개변수 정도는 보여주겠지만 한눈에 보기는 불편하다.
- <C++ 예제 보기>
#!syntax cpp import <initializer_list>; class Integer { public: // explicit 사용 explicit Integer(int value) noexcept : myValue(value) {} int myValue; }; class Adder { public: // (1) // 문제 없음 Adder(std::initializer_list<int> list) { for (auto& value : list) { mySummary += value; } } // (2) // 문제 없음 Adder(std::initializer_list<DoubledInteger> list) { for (auto& value : list) { mySummary += value.myValue; } } long long mySummary; }; int main() { // (1) // intializer_list<int>가 선택된다 Adder adder0{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // (2) // intializer_list<int>가 선택된다 Adder adder1{ { 0 }, { 1 }, { 2 }, { 3 }, { 4 }, { 5 }, { 6 }, { 7 }, { 8 }, { 9 } }; // (3) // intializer_list<int>가 선택된다 Adder adder2{ {}, { 1 }, {}, { 3 }, {}, { 5 }, {}, { 7 }, {}, { 9 } }; // (4) // 이제 intializer_list<Integer>가 선택되게 하려면 클래스명을 다 명시해야 한다 Adder adder3{ Integer{ 0 }, Integer{ 1 }, Integer{ 2 }, Integer{ 3 }, Integer{ 4 }, Integer{ 5 }, Integer{ 6 }, Integer{ 7 }, Integer{ 8 }, Integer{ 9 } }; }
explicit을 쓰면 모호성을 제거하고 지금 어떤 생성자가 이용될지 구분할 수 있게 해준다.5.4.1. 설계 패턴: 함수 꼬리표 분배
꼬리표 분배 (Tag Dispatching)하지만 아직 모호성을 없애기는 부족하다. 원시 자료형에 적용되는 자동 형변환을 피할 수 없기 때문이다.
std::initializer_list<T>는 가변 인자를 지원하는 점은 좋으나 단일 자료형만 받을 수 있다는 약점이 있다. 만약 최적화를 위해 부동소수점, 문자열, 정수형을 받는 생성자들의 동작을 다르게 만든다고 해보자. 그런데 int, float은 32비트로서 서로 암시적으로 변환된다. 숫자에 직접 .0f 따위를 명시하면 문제가 안된다고 생각할 수 있으나, 임의의 숫자를 받는 경우와 템플릿에서 사용할 때 문제가 된다. 두 숫자 자료형의 공통 자료형으로 전부 형변환되므로 std::initializer_list<float>만 받아진다. 그리고 이 형변환 문제는 원시자료형만 해당되는 게 아니다.- <C++ 예제 보기>
#!syntax cpp #include <cstdint> #include <initializer_list> struct TagInt16 { explicit TagInt16() noexcept {} }; struct TagInt32 { explicit TagInt32() noexcept {} }; struct TagInt64 { explicit TagInt64() noexcept {} }; struct TagReplace{ explicit TagReplace() noexcept {} }; struct TagAdd { explicit TagAdd() noexcept {} }; class Storage { public: // 이름이 없는 매개변수는 C++17부터 쓸 수 있다. 이름없는 매개변수는 미사용 경고가 발생하지 않는다. // C++14까지는 `_` 따위의 준 익명 식별자가 필요하다 Storage(TagInt16, std::int16_t value) : myValue1(value), myValueIndex(0) {} Storage(TagInt32, std::int32_t value) : myValue2(value), myValueIndex(1) {} Storage(TagInt64, std::int64_t value) : myValue3(value), myValueIndex(2) {} void Calculate(TagReplace, TagInt16, std::int16_t value) { myValue1 = value; myValueIndex = 0; } void Calculate(TagReplace, TagInt32, std::int32_t value) { myValue2 = value; myValueIndex = 1; } void Calculate(TagReplace, TagInt64, std::int64_t value) { myValue3 = value; myValueIndex = 2; } void Calculate(TagAdd, std::int16_t value) { if (0 == myValueIndex) { myValue1 += value; } else if (1 == myValueIndex) { myValue2 += value; } else if (2 == myValueIndex) { myValue3 += value; } } void Calculate(TagAdd, std::int32_t value) { if (0 == myValueIndex) { myValue1 += static_cast<std::int16_t>(value); } else if (1 == myValueIndex) { myValue2 += value; } else if (2 == myValueIndex) { myValue3 += value; } } void Calculate(TagAdd, std::int64_t value) { if (0 == myValueIndex) { myValue1 += static_cast<std::int16_t>(value); } else if (1 == myValueIndex) { myValue2 += static_cast<std::int32_t>(value); } else if (2 == myValueIndex) { myValue3 += value; } } // 익명 공용체 union { std::int16_t myValue1; std::int32_t myValue2; std::int64_t myValue3; }; int myValueIndex; };
Tag*로 시작하는 구조체들이 꼬리표(Tag) 구조체다. 이 구조체들은 그 자체로는 아무것도 하는 게 없고 오직 실행되는 함수 오버로딩을 구분하는 역할만 한다. 이 방법을 쓰면 구현부가 쪼개지는 불편함은 있으나, 필요한 구현을 확실하게 드러낼 수 있다는 장점이 있다. C++17부터는 매개변수에 이름이 없어도 되므로 매우 편리하게 쓸 수 있다. 게다가 이름없는 매개변수는 성능에 영향도 없도록 최적화 되므로 더욱 좋다.5.5. 조건부 explicit
explicit(boolean-constant)C++20C++20부터
explicit(상수-진리값)의 형식으로 생성자를 명시하도록 하는 여부를 제어할 수 있다.6. 소멸자 (Destructor)
|
소멸자는
~클래스 이름()형식으로 정의되는 특수 함수다. 클래스 인스턴스가 파괴될 때 실행된다.#!syntax cpp
class MyString
{
public:
MyString(size_t length)
: myStr(new const char[length]), myLength(length)
{}
// 소멸자
~MyString()
{
delete[] myStr;
}
private:
const char* myStr;
size_t myLength;
};
위 예제에서는 ~MyString()이 소멸자다. 생성자에서 동적 할당된 myStr를 소멸자 ~MyString()에서 delete[]를 써서 메모리를 해제하고 있다.소멸자는 보통 인스턴스가 가진 자원의 해제를 위해 사용된다. 클래스 내부에서 동적 할당 등을 통해 관리하는 리소스가 있다면 소멸자 내부에 이 리소스들에 대한 해제 코드를 작성해야 한다. 만약 동적 메모리 할당 도중에 예외가 발생했다고 해보자. 이때 일반적으로는 해당 위치에서 함수를 종료하고 호출 스택을 되감는데, 메모리 해제해 주는 코드가 그 뒤에 있다면 정리하는 코드가 실행되지 않아 얄짤없이 메모리 누수가 일어나게 된다. 하지만 소멸자는 예외로 인해 종료되는 상황에서도 반드시 호출되기 때문에 꼭 소멸자 내부에 메모리 해제 코드를 작성해주도록 하자. 다만 생성자 내부에서 예외가 발생했을 경우 생성중인 객체 자체는 해제되지만 소멸자는 호출되지 않는 맹점이 있다.
C++17부터는 소멸자에
noexcept를 사용할 수 있다.하지만 원시자료형만 갖고 있거나, 멤버 인스턴스의 경우 만약 해당 인스턴스의 클래스가 소멸자를 구현하고 있다면 굳이 소멸자를 넣을 이유가 없다. 오히려 필요없는 소멸자를 넣는 건 성능에 영향을 미칠 뿐이다. 예를 들어서 표준 라이브러리의 동적 문자열 클래스
std::string은 내부에 동적 할당된 메모리를 들고 있지만 알아서 소멸자에서 해제하므로 사용자 단에선 신경쓰지 않아도 된다.여담으로
delete this;를 실행하면 소멸자가 즉시 호출된다. 그런데 코드는 계속 진행되는데 데이터 멤버의 메모리가 증발하므로, 이후 실행 경로에서 치명적인 메모리 오류를 낼 수 있기에 가급적 하면 안된다.7. 동적 메모리 할당
- <C++ 예제 보기>
#!syntax cpp class Person { public: using name_t = std::string; name_t myName; unsigned int myAge; }; int main() { // (1) // 기본값을 할당하지 않는 기본 초기화 Person person0; // (2) // 자료형 명시와 소괄호를 사용한 기본 초기화 Person person1 = Person(); // (3) // 자료형 명시와 중괄호를 사용한 대입 초기화 Person person2 = Person{}; // (4) // auto와 중괄호를 사용한 대입 초기화 auto person3 = Person{}; // (5) // 소괄호를 사용한 기본 초기화 Person person4(); // (6) // 중괄호를 사용한 기본 초기화 Person person5{}; // (7) // 기본값을 할당하지 않는 배열의 기본 초기화 Person people0[10]; // (8) // 중괄호를 사용한 배열의 기본 초기화 Person people1[10]{}; // (9) // 동적 메모리 할당에서의 기본값을 할당하지 않는 기본 초기화 Person* person6 = new Person; // (10) // 동적 메모리 할당에서의 소괄호를 사용한 기본 초기화 Person* person7 = new Person(); // (11) // 동적 메모리 할당에서의 중괄호를 사용한 기본 초기화 Person* person8 = new Person{}; // (12) // 동적 메모리 할당에서의 auto와 중괄호를 사용한 기본 초기화 // auto* 역시 사용할 수 있다 auto person9 = new Person{}; // (13) // 동적 할당된 배열의 기본값을 할당하지 않는 기본 초기화 Person* persons2 = new Person[10]; // (14) // 동적 할당된 배열의 중괄호를 사용한 기본 초기화 Person* people3 = new Person[10]{}; // (15) // 동적 할당된 배열의 auto와 중괄호를 사용한 기본 초기화 // auto* 역시 사용할 수 있다 auto people4 = new Person[10]{}; // (16) // new를 사용했다면 반드시 삭제해주자 delete person6; delete person7; delete person8; delete person9; delete[] people3; delete[] people4; }
new, delete는 존재는 한다 정도로 알아두는 게 훨씬 낫다.8. 멤버 한정자
8.1. 데이터 멤버의 한정자
일반적인 변수처럼 데이터 멤버에 한정자const, volatile, &, &&를 적용할 수 있다.#!syntax cpp
class MyClass
{
public:
MyClass() : myValue(6000) {}
const int myValue;
const int myConstant = 7000;
const int& constRef = 8000;
static inline int StaticValue = 9000;
};
한정자를 붙이면 사용자가 염두에 두어야 하는 것들이 있다. const 데이터 멤버, 참조자 데이터 멤버는 반드시 초기화해야만 한다. 그렇지 않으면 컴파일 오류가 발생한다. const는 일반적인 상수의 경우와 같이 비정적 멤버와 정적 멤버 두 경우 모두 초기화를 해줘야 한다. &의 경우 인스턴스가 생성될 때 참조할 원본 변수를 반드시 인자로 전달해야 한다. 이때 const&는 리터럴 같은 prvalue도 담을 수 있다.#!syntax cpp
class MyClass
{
public:
MyClass(int&& v0, int&& v1)
: mustBeTranslated1(std::move(v0)), mustBeTranslated2(v1)
{}
int&& mustBeTranslated1;
int mustBeTranslated2;
};
이동 초기화를 강제하겠다고 멤버에 &&를 쓸 필요는 없다. 어차피 생성자에서 && 매개변수를 받으면 무조건 rvalue만 받게 만들 수 있으므로 문제없다. 거기에 후술하겠지만 && 데이터 멤버는 클래스의 복사와 이동에 제약을 준다.참조형 데이터 멤버를 쓸 때 조심해야 할 것은 참조 대상 소실(Dangling)이다.
&와 &&는 하드 링크나 스마트 포인터가 아니기 때문에 포인터와 마찬가지로 참조했던 객체가 사라질 수 있다. 가령 문맥을 벗어나서 참조할 지역 변수가 사라졌거나, 포인터에 * 연산자를 써서 얻었던 참조형이 delete되어 사라진다거나 말이다.- <C++ 예제 보기>
#!syntax cpp class Squid { public: static const bool canFly; }; const bool Squid::canFly = false; // C 방식의 정의 class Squirrel { public: static inline const bool canFly = false; // C++11의 inline을 사용한 정의 static constexpr bool canFly = false; // C++11의 constexpr을 사용한 정의 }; class FlyingSquirrel { public: static inline const constinit bool canFly = true; // C++20의 constinit을 사용한 컴파일 시간 정의 };
8.1.1. mutable
수정 가능 (Mutable)구조체와 클래스의 비정적 데이터 멤버와 비정적 멤버 함수에 사용할 수 있다.
const 한정자와는 같이 적용할 수 없다. 이 한정자가 적용된 비정적 데이터 멤버는 상수 인스턴스, const 한정자의 멤버 함수에서도 수정할 수 있다.#!syntax cpp
class Test
{
public:
mutable int value = 0;
};
void Setter(Test& variable, const Test& immutable, int v1, int v2)
{
variable.value = v1;
immutable.value = v2;
}
int main()
{
Test test1;
const Test test2;
// (1) test1.value = 11
Setter(test1, test1, 10, 11);
// (2) test1.value = 20
// (2) test2.value = 21
Setter(test1, test2, 20, 21);
// (3) test1.value = 30
// (3) test2.value = 31
Setter(test2, test1, 30, 31);
// (4) test2.value = 41
Setter(test2, test2, 40, 41);
}
상기 코드에서 용례를 보여주고 있다. const 인스턴스인데 데이터 멤버 `value`를 수정할 수 있다.mutable은 보통 뮤텍스 같은 락을 구현하는 데 이용된다. 예제는 C++ 스레드 문서를 참고할 것.8.2. 멤버 함수의 한정자
멤버 함수 자료형 한정자멤버 함수의 매개변수 오른쪽 닫는 괄호 뒤에
const, volatile 등 자료형 한정자를 붙일 수 있다. 이 한정자는 바로 인스턴스 자기 자신한테 붙는 한정자로서, 데이터 멤버의 최종 한정자를 결정한다. 예를 들어 const가 붙은 멤버 함수에서는 this는 const Class*가 되며, 자신의 참조형은 const Class&가 된다. 이 기능을 쓰면 최적화가 용이해진다. 또한 수정 가능한 객체와 불가능한 객체를 따로 전달함으로써 멤버 함수의 역할 구분이 쉬워져 사용자의 실수를 줄이는 역할을 한다.대다수 상황에선
const를 붙이냐 마냐만 따지면 충분하다.멤버 함수 참조 한정자C++11
그러나 여기서 멈추면 C++이 아니다. 자료형을 정확하게 기입해야 성능에 문제가 생기지 않기 마련이다. C++11부터는
&, &&도 같이 추가할 수 있게 되었다. 역시 간편하게 하려면 const&와 && 두개로 대부분을 때울 수 있다. 표준 라이브러리 조차 이 기능을 적극적으로 응용하는 부분은 std::optional, std::expected 등 한정자를 준수하지 않으면 큰일나는 곳이 전부다.- <C++ 예제 보기>
#!syntax cpp enum class Gender { NonBinary = 0, Male, Female, Lesbian, Gay, Binary, TransgenderMF, TransgenderFM, }; class Person { public: using name_t = std::string; void BuyAPetIfHasNothing(Squirrel&& pet) { myPet = std::move(pet); } void SetGender(Gender gender) noexcept { myGender = gender; } // (1) const 한정자를 사용하면, 인스턴스가 어떤 상태이든 항상 사용할 수 있다. unsigned int GetAge() const noexcept { return myAge; } // (2) Squirrel& GetPet() noexcept { return myPet; } // (3) const Squirrel& GetPet() const noexcept { return myPet; } // (4) C++11부터 가능한 참조 한정자. name_t& GetName() & noexcept { return myName; } // (5) C++11부터 가능한 참조 한정자. const name_t& GetName() const& noexcept { return myName; } // (6) C++11부터 가능한 참조 한정자. name_t&& GetName() && noexcept { // rvalue로 형변환하지 않으면 오류가 발생한다. return std::move(myName); } // (7) C++11부터 가능한 참조 한정자 const name_t&& GetName() const&& noexcept { // rvalue로 형변환하지 않으면 오류가 발생한다. return std::move(myName); } // (8-a) 복사 // const&와 &&를 붙이는 것만으로 대부분의 경우의 수가 해결된다. // const& 한정자는 &, const&&를 대신해서 실행될 수 있다. const Gender& GetGender() const& noexcept { return myGender; } // (8-b) 이동 // const&와 &&를 붙이는 것만으로 대부분의 경우의 수가 해결된다. Gender&& GetGender() && noexcept { return std::move(myGender); } // (9) 상수 lvalue 한정자는 rvalue 상태에서는 사용할 수 없다. const std::string& GetAddress() const& noexcept { return myAddress; } name_t myName; unsigned int myAge; Gender myGender; Squirrel myPet; protected: std::string myAddress; }; int main() { // (1) person0은 비상수 lvalue: Person& // 현재 사용할 수 있는 멤버: BuyAPetIfHasNothing[non-const], SetGender[non-const], GetAge[const], GetPet[non-const, const], GetName[&, const&], GetGender[const&], GetAddress[const&] Person person0{ "Who", 20, Gender::TransgenderFM }; // (2) person0.GetAge(); // unsigned int person0.GetPet(); // Squirrel& person0.GetName(); // Person::name_t& (std::string&) person0.GetGender(); // const Gender& person0.GetAddress(); // const std::string& // (3) std::move(person0)은 비상수 rvalue: Person&& // 현재 사용할 수 있는 멤버: BuyAPetIfHasNothing[non-const], SetGender[non-const], GetAge[const], GetPet[const], GetName[&&], GetGender[&&] std::move(person0); // (4) std::move(person0).GetAge(); // unsigned int std::move(person0).GetPet(); // Squirrel&, std::move(person0.GetPet())을 권장한다 std::move(person0).GetName(); // Person::name_t&& (std::string&&) std::move(person0).GetGender(); // Gender&& // (5) // person1은 상수 lvalue: const Person& // 현재 사용할 수 있는 멤버: GetAge[const], GetPet[const], GetName[const&], GetGender[const&], GetAddress[const&] const Person person1{ "Three", 20, Gender::TransgenderMF }; // (6) person0.GetAge(); // unsigned int person0.GetPet(); // const Squirrel& person0.GetName(); // const Person::name_t& (const std::string&) person0.GetGender(); // const Gender& person0.GetAddress(); // const std::string& // (7) // std::move(person1)은 상수 rvalue: const Person&& // 현재 사용할 수 있는 멤버: GetAge[const], GetPet[const], GetName[const&&], GetGender[const&] std::move(person1); // (8) std::move(person1).GetAge(); // unsigned int std::move(person1).GetPet(); // const Squirrel& std::move(person1).GetName(); // const Person::name_t&& (const std::string&&) std::move(person1).GetGender(); // const Gender& }
lvalue이므로 &, const& 중 하나가 선택된다. && 형변환 또는 리터럴 또는 함수의 객체 반환 등 rvalue에서는 &&, const&& 중 하나가 선택된다. 이때 형변환을 해주지 않았다면 &&와 const&&는 각각 &와 const&로 연역된다.그런데 한정자마다 구현을 전부 따로 해줘야 하는 건 매우 불편하다.
volatile까지 붙일 경우 &, const&, volatile&, const volatile&, 그리고 rvalue 버전까지 8개를 죄다 오버로딩 해줘야 하기 때문에 아주 고역이다. 때문에 C++23에서는 Deducing this라는 신기능이 도입됐다.9. friend
친구 선언 (Friend Declaration)9.1. 친구 함수
친구 함수 (Friend Function)예약어
friend를 클래스 안의 함수에 붙여주면 해당 함수에서 클래스의 protected, private 멤버에 접근할 수 있다. 마치 친구처럼 비밀도 나누는 관계가 된 것이다.#!syntax cpp
namespace NamuWiki
{
struct House
{
using name_t = std::string;
name_t myName;
unsigned int myAge;
std::string myAddress;
}
// 이름공간 `NamuWiki`에 존재하는 클래스 `Person`
class Person
{
public:
using name_t = std::string;
Person(const name_t name, unsigned int age) noexcept
: myName(name), myAge(age), myHouse(nullptr)
{}
Person(const name_t name, unsigned int age, House& house) noexcept
: myName(name), myAge(age), myHouse(std::addressof(house))
{}
const name_t& GetName() const& noexcept { return myName; }
name_t&& GetName() && noexcept { return std::move(myName); }
unsigned int GetAge() const noexcept { return myAge; };
// (1) `ChangePersonName`은 이름공간 `NamuWiki`에 선언된다.
friend void ChangePersonName(Person& person, const name_t& name);
// (2) `MoveHouse`은 이름공간 `NamuWiki`에 정의된다.
friend /*inline*/ void MoveHouse(House& dest)
{
myHouse = std::addressof(dest);
}
// (3) `GetMarriageWith`은 이름공간 `NamuWiki`에 선언된다.
friend void GetMarriageWith(Person& lhs, Person& rhs);
private:
name_t myName;
unsigned int myAge;
House* myHouse;
};
}
// 정의를 `NamuWiki` 안에 해도 상관없다
void NamuWiki::ChangePersonName(NamuWiki::Person& person, const NamuWiki::Person::name_t& name)
{
person.myName = name;
}
이때 친구 함수는 클래스의 바깥에 정의된다. 이 예제에서는 Person 클래스가 있는 이름공간 NamuWiki에 정의된다. 친구 함수를 선언할 때는 즉시 함수를 정의하거나, 기존에 만들어진 함수를 지정할 수 있다. 예제에서처럼 선언만 있는 함수도 친구가 될 수 있다.9.2. 친구 클래스
친구 클래스 (Friend Class)어떤 클래스 안에서 예약어
friend와 함께 클래스 선언을 하면, 해당 클래스의 protected, private 멤버에 접근할 수 있다. 이때 클래스의 선언은 템플릿, 특성, struct/class 구분 등이 정확히 같아야 한다.#!syntax cpp
class Person
{
public:
// 친구 클래스 `Dog`
friend class Dog;
private:
House* myHouse;
};
class Dog
{
public:
void GotoHome()
{
if (myFriend != nullptr)
{
myHouse = myFriend->myHouse;
}
}
private:
Person* myFriend;
House* myHouse;
};
`Dog`는 `Person`의 친구 클래스라서 `Person`의 비공개된 데이터 멤버에 접근할 수 있다. 반대로 `Person`이 `Dog`의 비공개 데이터 멤버에 접근할 수는 없다.10. 연산자 오버로딩
연산자 오버로딩 (Operator Overloading)C++의 특별한 기능이라고 할만한 것은 템플릿 다음으로는 사용자가 연산자를 직접 정의할 수 있다는 점이다 [9]. 이렇게 연산자 동작을 함수의 형태로 재정의하는 것을 연산자 오버로딩이라고 한다. 예를 들면 단순히 클래스로 숫자를 감싸 내부에서 연산을 처리할 수도 있다. 또는
==, < 혹은 <=>C++20 연산자를 재정의하면 비교 연산도 수행할 수 있다. 잘 이용하면 프로그램에서 모든 클래스가 시시각각 바뀌는 메서드 이름이나 가상 함수에 의존하지 않고 연산자만 써서 프로그램 내내 일관적인 프로그램 논리를 구현할 수 있다. 표준 라이브러리의 많은 클래스가 ==, <=>C++20 또는 사칙 연산자들을 지원한다.아예 연산자 기호를 창조할 수 있는 함수형 언어들을 제외하면, 어떤 언어와도 비교 불가능할만큼 자유롭게 연산자의 동작을 바꿀 수 있다. 아예 원래 용도와 완전히 다른 기작을 만들 수도 있다. 라이브러리 중에는
-- 연산자와 > 연산자를 오버로딩해서 --> 모양의 변환 연산자를 만들기도 했다. 참고할 점은 연산자 오버로딩에는 원래 연산자가 쓰이는 위치대로 인자의 개수와 반환해야하는 자료형에 대해 제약이나 권고 사항이 있다. 예를 들어 = 및 단항 연산자의 정의는 클래스 내부에 있어야 한다, 등호 연산자의 반환형은 bool이여야 하고, const 한정자를 사용해야 한다든가 등. 경험이나 관습 상 연산자를 만들고 정의할 수는 있으나 표준에서 권고하는 사항을 바로 알 수는 없다[10].#!syntax cpp
struct Rectangle
{
float x, y, w, h;
// (1) 동등 비교 연산자
// 후술할 특수 함수 중에 하나.
[[nodiscard]] bool operator==(const Rectangle& rhs) const noexcept = default;
// (2) 다른 사각형 클래스와 크기를 비교한다.
[[nodiscard]]
bool operator<(const Rectangle& rhs) const noexcept
{
return w * h < rhs.w * rhs.h;
}
friend bool operator>=(const Rectangle& lhs const Rectangle& rhs) noexcept;
};
// (3) 다른 사각형 클래스와 크기를 비교한다.
// C++20부터는 == 와 < 연산자로부터 자동으로 연역되므로 작성하지 않아도 된다.
[[nodiscard]]
bool operator>=(const Rectangle& lhs const Rectangle& rhs) noexcept
{
//return !(lhs < rhs);
return w * h >= rhs.w * rhs.h;
}
상기 코드는 좌표와 크기를 가지는 사각형 클래스의 예시를 보여주고 있다. < 연산자, == 연산자, friend >= 연산자를 정의했다. friend, friend inline 혹은 static friend 지시자를 사용할 수 있다 [11]. static 연산자는 C++20의 모듈에서 export를 사용할 수 없으므로 이것도 주의해야 한다.여기서 동등 비교 연산자
==는 default로 대입되어 있다. 이는 몇몇 특별한 멤버 함수들을 위한 문법으로서 컴파일러가 기본적으로 만드는 구현을 내보일 수 있다. 후술하겠지만 동등 비교 연산자는 그 특별한 함수 중에 하나라서 가능하다면 모든 클래스가 가지고 있다. 그러므로 우리가 여기서 default로 선언하지 않아도 컴파일러가 알아서 만들어주니까 불필요한 작업에 가깝다. 그렇지만 어떤 기작을 갖고 있는지 소개하고자 한다.비교 연산자들에는 상수 참조자를 쓰는 게 좋다, 아니 무조건 써야한다. 참조자를 쓰지 않으면 비교만 해도 인스턴스가 복사되므로 성능에 큰 악영향을 미친다.
#!syntax cpp
struct Point
{
float x, y;
};
struct Rectangle
{
float x, y, w, h;
Rectangle& operator=(const Rectangle& rt) noexcept
{
w += rt.w;
h += rt.h;
return *this;
}
Rectangle& operator=(const Point& pt) noexcept
{
x = pt.x;
y = pt.y;
return *this;
}
Rectangle& operator+=(const Point& pt) noexcept
{
x += pt.x;
y += pt.y;
return *this;
}
};
int main()
{
Rectangle rect{ 0, 0, 10, 10 };
Point point{ 40, 30 };
rect += point;
// 상수 참조자가 Rectangle의 prvalue를 받을 수 있음.
// += 연산자에서 상수 참조자가 반환되므로 연쇄적으로 수행할 수 있음.
rect += Rectangle{ 35, 25, 30, 15 } += Rectangle{ 0, 75, 10, 90 };
}
상기 코드는 좌표와 크기를 가지는 사각형 클래스의 예시를 보여주고 있다. = 연산자, += 연산자를 정의했다. 이 연산자들은 인스턴스 자신의 참조자를 반환해야 한다.- <C++ 예제 보기>
#!syntax cpp // (1) `Vector3`를 전방위 선언. class Vector3; namespace std { // (2) C++17부터는 noexcept가 함수 서명에 포함되어서 선언-구현 구조에서 반드시 지켜줘야 한다. void swap(Vector3& lhs, Vector3& rhs) noexcept; } // (3) 전역 이름공간에 존재하는 클래스 `Vector3`. class Vector3 { public: const float& X() const& noexcept { return x; } const float& Y() const& noexcept { return y; } const float& Z() const& noexcept { return z; } float&& X() && noexcept { return std::move(x); } float&& Y() && noexcept { return std::move(y); } float&& Z() && noexcept { return std::move(z); } // (4) // friend 지정에 constexpr 여부는 상관없으나, noexcept는 반드시 동치시켜야 한다 friend void std::swap(Vector3& lhs, Vector3& rhs) noexcept; // (5) 덧셈 연산자 // 이때 `operator+`는 `Vector3` 내부에 정의된다 Vector3 operator+(const Vector3& rhs) const noexcept { return Vector3 { .x = x + rhs.x, .y = y + rhs.y, .z = z + rhs.z, }; } // (6) 뺄셈 연산자 // 이때 `operator-`는 `Vector3` 내부가 아닌 바깥 이름공간에 정의된다 // 매개변수가 꼭 const lvalue일 필요는 없으나 성능 때문에 하는 게 좋다 friend Vector3 operator-(const Vector3& lhs, const Vector3& rhs) noexcept { return Vector3{ lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z }; } // (7) 곱셈 연산자 // 이때 `operator*`는 `Vector3` 내부가 아닌 바깥 이름공간에 정의된다 // 매개변수가 꼭 const lvalue일 필요는 없으나 성능 때문에 하는 게 좋다 // inline이 아니라서 정의를 별도의 소스 파일에 해줘도 된다 friend Vector3 operator*(const Vector3& lhs, const Vector3& rhs) noexcept; // (8) 나눗셈 연산자 // 이때 `operator/`는 `Vector3` 내부에 정의된다 // 매개변수가 꼭 const lvalue일 필요는 없으나 성능 때문에 하는 게 좋다 // 그러나 inline이기 때문에 반드시 클래스 정의와 함께 같은 헤더 어딘가에 정의해줘야 한다 inline Vector3 operator/(const Vector3& rhs) const noexcept; // (9) 양의 부호 연산자 // 이때 `operator+`는 `Vector3` 내부가 아닌 바깥 이름공간에 정의된다 // 연산자는 friend가 아니면 static일 수 없다. // static friend 연산자는 헤더에서는 문제 없으나, 모듈에서는 내보낼 수 없기 때문에 주의해야 한다 static friend Vector3& operator+(Vector3& vector) noexcept { vector.x = std::abs(vector.x); vector.y = std::abs(vector.y); vector.z = std::abs(vector.z); return vector; } // (10) 음의 부호 연산자 // 이때 `operator-`는 `Vector3` 내부에 정의된다 // 연산자는 friend가 아니면 static일 수 없다. Vector2& operator-() noexcept { x = -std::abs(x); y = -std::abs(y); z = -std::abs(z); return *this; } // (11) 등호 연산자 // 이때 `operator==`는 `Vector3` 내부에 정의된다 bool operator==(const Vector3& rhs) const noexcept { return x == rhs.x and y == rhs.y and z == rhs.z; } float x, y, z; }; // (12) // `Vector3::operator/` 정의 Vector3 Vector3::operator/(const Vector3& rhs) const noexcept { return Vector3 { .x = x / rhs.x, .y = y / rhs.y, .z = z / rhs.z, }; } namespace std { constexpr void swap(const Vector3& lhs, const Vector3& rhs) noexcept { using ::std::swap; // 필요하다. 자세한 내용은 C++/문법 문서로.. swap(lhs.x, rhs.x); swap(lhs.Y, rhs.Y); swap(lhs.z, rhs.z); } }
11. 변환 함수
사용자 정의 변환 함수 (User-defined Conversion Function)변환 함수(Conversion Function)는 형변환 연산자라고도 부르며 클래스의 인스턴스를 다른 자료형으로 변환시키는 기능을 한다. 변환 함수는 암시적으로 수행하는 것과 명시적으로 수행하는 것 두 종류가 있다.
참고로 크기가 있는 배열(
T[Size])로는 변환할 수 없다.#!syntax cpp
struct Integer
{
operator int() const noexcept
{
return myValue;
}
int myValue;
};
상단의 예제는 암시적으로 수행되는 변환 함수를 보여주고 있다. 클래스 `Integer`는 자동으로 int로 변환해주는 멤버 함수를 갖고 있다.#!syntax cpp
Integer integer{ 6 };
// (1) C 스타일
int result_0 = (int)integer;
int result_1 = int(integer);
// (2) 정석
int result_2 = static_cast<int>(integer);
// (3) 문제 없음
int result_3 = const_cast<int>(integer);
// (4) C++23, auto는 못 씀.
int result_4 = auto(integer);
int result_5 = auto{ integer };
위와 같이 사용한다.11.1. 한정자
#!syntax cpp
struct IntPointer
{
operator int*() const& noexcept
{
return myPointer;
}
operator int* &&() && noexcept
{
return std::exchange(myPointer, nullptr);
}
int* myPointer;
};
auto iptr = IntPointer{ new int(5) };
int* raw1 = iptr;
// `iptr`의 `myPointer`은 nullptr가 됨.
int* raw2 = std::move(iptr);
변환 함수에도 한정자를 사용할 수 있다. 한정자는 하나도 빠짐없이 적어야 한다.11.2. explicit
명시적 자료형 변환 (Explicit Type Conversion)C++11암시적 형변환은 편리하지만 문제점을 가져다 준다는 것을 생성자 문단에서 알아보았다. 클래스도 마찬가지다. 원시자료형보다 복잡한 클래스가 아무 예고도 없이 변신하는 건 좋은 현상이 아니다.
#!syntax cpp
class Integer
{
public:
explicit operator int&() & noexcept
{
return myValue;
}
explicit operator const int&() const& noexcept
{
return myValue;
}
explicit operator int&&() && noexcept
{
return std::move(myValue);
}
explicit operator const int&&() const&& noexcept
{
return std::move(myValue);
}
int myValue;
};
생성자와 비슷하게 explicit을 기입하면, 변환 함수를 반드시 static_cast, dynamic_cast, const_cast, reinterpret_cast, 또는 C언어식 형변환을 써야 하도록 만들 수 있다. 이게 없는 변환 함수는 너무 위험하기 때문에 무조건 explicit을 붙이는 걸 추천한다. 불편함의 문제를 넘어 버그가 생길 확률이 너무 높고 예측할 수 없는 동작을 하기 때문이다. 암시적 변환이 많으면 코드를 읽기도 너무 힘들어진다.12. 상속
class DerivedClass : {{{#DodgerBlue,#CornFlowerBlue '접근성 지시자'}}} 기반-클래스-식별자 |
어떤 클래스는 다른 클래스와 종속관계를 형성하고 속성을 가져올 수 있다. 이것을 상속이라고 한다. 상속은 객체 지향언어에서 가장 핵심이 되는 요소로써 클래스를 재사용하고 작성해야 할 코드의 양을 줄일 수 있다. 같은 동작을 하나의 인터페이스로 통일할 수 있다. 여기서 상속의 대상이 되는 클래스를 기반 클래스(Base Class), 상속을 받는 클래스를 파생 클래스(Derived Class)라고 칭한다. 더 친근한 용어로는 부모 클래스(Parent Class)와 자식 클래스(Child Class)라고 부른다.
#!syntax cpp
struct BaseClass { int pvalue; };
struct DerivedClass : public BaseClass {};
DerivedClass instance;
instance.pvalue = 40;
C++에서는 class [파생 클래스 식별자] : [접근성 지시자] [기반 클래스 식별자]의 형식으로 상속을 수행할 수 있다. 파생 클래스는 기반 클래스의 멤버를 그대로 가져온다. 외부에서 멤버에 접근할 수 있는 권한을 접근성 지시자를 통해 결정할 수 있다. 상속 방식에 따라서 파생 클래스에서 접근하는 부모 클래스의 멤버에 대한 접근 권한이 달라진다. 멤버에 대한 접근 권한과 똑같이 private, protected, public이 있으나 둘을 혼동하면 안된다.| 파생 클래스 멤버 | private 상속 | protected 상속 | public 상속 |
| private 멤버 | 접근 불가 | 접근 불가 | 접근 불가 |
| protected 멤버 | 접근 불가 | 자신만 접근[protected] | 자신만 접근[protected] |
| public 멤버 | 접근 불가 | 자신만 접근[protected] | 공개적 접근[public] |
private관계: 오직 직접적인 파생 클래스만이 기반 클래스의 멤버에 접근할 수 있다.protected관계: 기반 클래스의 멤버를 상속받되 외부에서는 접근할 수 없게 만든다. 그리고 해당 파생 클래스를 상속받는 또 다른 클래스에서도 기반 클래스의 멤버에 접근할 수 있다.public관계: 파생 클래스에서 기반 클래스의public,protected멤버를 전부 사용할 수 있다.
부모 클래스의 멤버를 직접적으로 이용하려면 부모 클래스의 이름 뒤에
::를 붙여 사용할 수 있다. 기반 클래스 이름::멤버 식별자와 같이 사용할 수 있다. 한편 파생 클래스와 기반 클래스에서 각 멤버들의 식별자는 서로 같아도 상관 없지만 기본적으로는 파생 클래스의 멤버가 참조된다[16]. 때문에 이름이 겹치는 멤버가 있을 땐 구분해줘야 한다. 겹치는 식별자가 없으면 기반 클래스의 멤버를 참조하고 싶을 땐 그냥 하면 된다. 만약 기반 클래스를 명시하는 게 번거롭다면
using super = BaseClass;와 같이 자료형 별칭을 쓰면 간단하게 명시할 수 있다.#!syntax cpp
class MyBase
{
public:
MyBase();
MyBase(int);
};
class MyDerived : public MyBase
{
public:
// (1) 명시하지 않아도 MyBase::MyBase가 먼저 실행됨.
MyDerived()
{}
// (2) myMember(3) 초기화보다 MyBase::MyBase(0)이 먼저 실행됨.
MyDerived(int)
: MyBase(0), myMember(3)
{}
int myMember;
};
참고로 기반 클래스의 생성자는 항상 파생 클래스의 생성자보다 먼저 실행된다. 기반 클래스의 기본 생성자 (Default Initializer)는 명시하지 않아도 자동으로 실행되므로 유의해야 한다.생성자는 자동으로 상속되지 않는다. 이럴 때는
using 기반 클래스 이름::기반 클래스 이름;으로 모든 생성자를 불러올 수 있다.#!syntax cpp
class MyBase
{
public:
void print()
{
std::println("My number is {}", myNumber); // My number is 64
}
std::string myName;
protected:
int myNumber = 64;
};
class MyDerived : public MyBase
{
public:
void print()
{
// (1) MyDerived의 데이터 멤버 `myNumber`를 사용한다.
std::println("My number is {}", myNumber); // My number is 128
}
void print_of_parent()
{
// (2) 정적 데이터 멤버를 참조하는 것이 아니라, 기반 클래스의 비정적 데이터 멤버 `myNumber`를 가져온다.
std::println("Parent's number is {}", MyDerived::myNumber); // My number is 64
}
void legacy_of_void()
{
// (3) 정적 멤버 함수를 호출하는 것이 아니라, 기반 클래스의 `print`를 호출한다.
MyBase::print();
}
private:
// (4) `MyBase`의 데이터 멤버 `myNumber`를 이 클래스 한정으로 숨긴다.
int myNumber = 128;
// (5) `MyBase`의 데이터 멤버 `myName`를 이 클래스 한정으로 숨긴다.
std::string myName;
};
int main()
{
MyDerived child{};
// (6) `MyBase`의 public 멤버 `myName`를 참조한다.
child.myName;
}
상기 예제에서 MyDerived는 부모 클래스인 MyBase로부터 public 방식으로 상속받았다. 자식 클래스는 부모 클래스인 MyBase로부터 데이터 멤버 myNumber와 멤버 함수 print()를 물려받는다. MyBase 클래스에서 멤버 myNumber를 protected 권한으로 선언하고, public 상속을 했기 때문에 접근이 가능하다.여기서 두 클래스의 멤버 이름이 중복되는데 기반 클래스의 멤버가 사용된다.
여담으로 비정적 데이터 멤버가 없는 기반 클래스는 Empty base optimization 규칙이 적용되어 파생 클래스의 바이트 크기에 영향을 주지 않는다.
12.1. 선택적 멤버 상속
class 클래스-식별자 : {{{#DodgerBlue,#CornFlowerBlue '접근성-지시자'}}} 기반-클래스-식별자 |
using 기반 클래스 이름::멤버 식별자; 구문으로 특정 멤버만 불러올 수 있다. 이 구문을 쓰면 상속 권한과는 별개로 멤버마다 따로 접근 권한을 설정할 수 있다.12.2. 다형성
상속의 목적은 코드 재사용도 있지만 보다 유연한 코드를 작성하는 것에도 목적을 둔다. 파생 클래스는 기반 클래스에 종속적인 관계가 되며, 이때 파생 클래스가 바로 기반 클래스라고 취급될 수 있어야 한다 [17].12.2.1. 하위 유형 다형성
DerivedClass* child;BaseClass* parent = static_cast<BaseClass*>(child);BaseClass* child_again = dynamic_cast<DerivedClass*>(parent); |
C++에선
public으로 파생된 클래스를 기반 클래스로 형변환 할 수 있다. 파생 클래스의 참조형과 포인터를 각각 기반 클래스의 참조형 및 포인터로 변환할 수 있다. reinterpret_cast나 C스타일 형변환도 가능하지만, static_cast와 dynamic_cast를 쓰는 게 좋다. C++20부터는 dynamic_cast를 상수 시간에 수행할 수 있다.그럼 변환했을 때 멤버를 사용하면 어떤 멤버가 참조되느냐 하면 기반 클래스의 멤버가 사용된다. 기반 클래스로 형변환되면서 파생 클래스의 멤버 정보가 사라지는 것이다. 예를 들어서 멤버 함수가 오버로딩되지 못한다. 클래스의 구현이 기반 클래스에서 단 한치도 나아가지 못하고 유연성은 물건너 간 셈이다. 좀 더 유동적이고 사용자 친화적인 코드를 위해 클래스를 도입한 것인데 대신 단단한 돌덩어리를 던져 놓은 거나 다름없다.
virtual 함수로 파생 클래스에서 기반 클래스의 멤버 함수를 재정의하는 기능을 제공한다. C++에서는 오직 virtual 함수에서만 동적 바인딩을 지원한다. virtual 함수는 멤버 함수마다 별도의 구현을 담은 버퍼를 저장하고, 클래스마다 다른 버퍼를 참조하는 것이다. 그러나 멤버 구분을 위한 버퍼가 필요하므로 오버헤드가 있다. 성능을 다소 희생했기 때문에 이 기능을 회피할 수 있는 경로를 제공하고 있다.12.2.2. 가상 멤버 함수
class BaseClass{access-specifier:virtual 반환 자료형 멤버 함수 식별자(매개변수);};class DerivedClass : access-specifier BaseClass{access-specifier:{{{#DodgerBlue,#CornFlowerBlue ' virtual}}} {{{#LightSeaGreen,#DarkTurquoise 반환 자료형}}}{{{#DarkOrange 멤버 함수 식별자}}}(매개변수'') {{{#DodgerBlue,#CornFlowerBlue override'''}}};}; |
가상 함수는 언제나 파생된 멤버 함수를 사용하도록 해주는 기능이다.
#!syntax cpp
class MyParent
{
public:
virtual void Print()
{
std::println("{}", text);
}
protected:
std::string text = "Hello World";
};
기반 클래스에 virtual [반환형] [함수명] (매개 변수)라고 자식에게 상속할 멤버 함수를 선언한다. 이 virtual 함수를 바로 가상 함수라고 한다.#!syntax cpp
class MyChild : public MyParent
{
public:
/*virtual*/ void Print() override
{
std::println("{}", text);
}
private:
std::string text = "안녕하세요.";
};
파생 클래스에서는 [반환형] [파생 클래스의 멤버 함수명] ([해당 함수의 멤버 변수]) override[18]라고 선언한다. 기반 클래스인 MyParent의 멤버 함수 Print()에서는 멤버 변수인 text의 내용인 "Hello World"를 출력한다. 반면 파생 클래스인 My Child에서 데이터 멤버 str의 내용인 "안녕하세요."를 출력하도록 Print() 함수를 재정의하였다.12.2.3. 가상 소멸자
class Class{public:virtual ~Class();}; |
virtual로 지정하면 소멸자를 동적 바인딩하므로 이런 문제를 방지할 수 있다.#!syntax cpp
template<size_t Capacity>
class MyParent20
{
public:
static constexpr size_t myCapacity = Capacity;
constexpr MyParent20() noexcept
: myBuffer(new char[Capacity]), myCapacity(Capacity)
{}
virtual constexpr ~MyParent20() noexcept
{
delete[] myBuffer;
}
virtual constexpr size_t GetCapacity() const noexcept
{
return myCapacity;
}
protected:
char* myBuffer;
};
template<size_t Capacity, size_t InnerCapacity>
class MyClass20 : public MyParent20<InnerCapacity>
{
public:
static constexpr size_t myCapacity = Capacity;
// 부모의 생성자는 명시하지 않아도 알아서 호출됨.
constexpr MyClass20() noexcept
: childBuffer(new char[Capacity])
{}
// 파괴될 때 MyParent20::~MyParent20()이 실행된다.
constexpr ~MyClass20() noexcept(std::is_nothrow_destructible_v<std::string>) // noexcept(true)
{}
constexpr size_t GetCapacity() const noexcept override
{
return myCapacity;
}
private:
std::string childBuffer;
};
C++20부터는 constexpr 역시 사용할 수 있다. 그리고 virtual constexpr이 가능해졌다.12.2.4. final
12.2.4.1. final 클래스
class SealedClass final; |
sealed라는 이름으로 되어 있다12.2.4.2. final 멤버 함수
class DerivedClass : access-specifier BaseClass{public:virtual 반환 자료형 멤버 함수 식별자(매개변수) override final;}; |
12.2.5. 추상 클래스
class DerivedClass : access-specifier BaseClass{public:{{{#DodgerBlue,#CornFlowerBlue ' virtual'}}} 반환 자료형 멤버 함수 식별자(매개변수) = 0;}; |
12.2.6. 다중 상속
class DerivedClass : {{{#DodgerBlue,#CornFlowerBlue 'access-specifier1}}} Mixin1, {{{#DodgerBlue,#CornFlowerBlue access-specifier2'}}} Mixin2, ...{...}; |
#!syntax cpp
class BruceWayne : public RichMan, private Batman
{
...
};
클래스 BruceWayne은 RichMan라는 클래스로부터 public 상속을 받고, Batman라는 클래스에서는 private 상속을 받는다.12.2.7. 가상 상속
class DerivedClass : virtual {{{#DodgerBlue,#CornFlowerBlue 'access-specifier1}}}'' Mixin1, {{{#DodgerBlue,#CornFlowerBlue virtual}}} ''{{{#DodgerBlue,#CornFlowerBlue access-specifier2}}}'' Mixin2, {{{#DodgerBlue,#CornFlowerBlue virtual}}} ''{{{#DodgerBlue,#CornFlowerBlue access-specifier3'}}} Mixin3, ...; |
- <C++ 예제 보기>
#!syntax cpp class Base { public: Base() : value(0) {} Base(int n) : value(n) {} int value; }; class A : public virtual Base { public: using Base::Base; }; class B : public virtual Base { public: using Base::Base; }; class C : public Base { public: using Base::Base; }; class S : public A, public B, public C { public: S() : A(1), B(2), C(3) {} void SetAValue(int n) { A::value = n; } void SetBValue(int n) { B::value = n; } void SetCValue(int n) { C::value = n; } int GetAValue(int n) { return A::value; } int GetBValue(int n) { return B::value; } int GetCValue(int n) { return C::value; } }; class T : public A, public B { public: S() : Base(1), A(2), B(3) // `A`, `B` 둘 다 가상 상속이라 가능하다. 이때 A(2), B(3)은 Base(1)보다 먼저 실행된다 {} void SetAValue(int n) { A::value = n; } void SetBValue(int n) { B::value = n; } int GetAValue(int n) { return A::value; } int GetBValue(int n) { return B::value; } }; int main() { S instance_s{}; T instance_t{}; // (1) // 모든 가상 상속 구조의 생성자 호출은 비가상 생성자 뒤에 이루어진다 // result_0 == 3 int result_0 = instance_s.value; // (2) // 모든 가상 상속 구조의 생성자 호출은 비가상 생성자 뒤에 이루어진다 // result_1 == 1 int result_1 = instance_t.value; // (3) // result_2_a == 2 // result_2_b == 2 // result_2_c == 3 int result_2_a = instance_s.GetAValue(); int result_2_b = instance_s.GetBValue(); int result_2_c = instance_s.GetCValue(); // (4) // result_3_a == 1 // result_3_b == 1 int result_3_a = instance_t.GetAValue(); int result_3_b = instance_t.GetBValue(); // (5) // result_4_a == 10 // result_4_b == 10 instance_s.SetA(10); int result_4_a = instance_s.GetAValue(); int result_4_b = instance_s.GetBValue(); // (6) // result_5_a == 30 // result_5_b == 30 instance_s.SetB(30); int result_5_a = instance_s.GetAValue(); int result_5_b = instance_s.GetBValue(); // (7) // result_6_a == 30 // result_6_b == 30 // result_6_c == 50 instance_s.SetB(50); int result_6_a = instance_s.GetAValue(); int result_6_b = instance_s.GetBValue(); int result_6_c = instance_s.GetCValue(); }
13. 특수 멤버 함수
맨 처음 문단에서 C++의 클래스는 정보가 어떻게 흘러가는 지 정의한 규칙 모음이라고 말했었다. C++에서 객체(=데이터)의 수명을 추적하고 흐름을 파악할 수 있도록 하는 주요 수단은 복사와 이동이다. 사용자는 소개할 복사 생성자, 이동 생성자, 복사 대입 연산자, 이동 대입 연산자를 통해서 클래스 객체가 복사 가능한지 또는 어떻게 복사되는지, 이동 가능한지 또는 어떻게 이동시킬 수 있는지를 모두 정의할 수 있다.다만 입문 단계에서는 복사와 이동 논리를 신경 쓸 필요도 이유도 없다. 왜냐하면 C++이 알아서 처리해주는 게 매우 많아서 묵시적으로 복사, 이동 생성자와 대입 연산자를 만들어주기 때문이다. 거대한 규모의 클래스 라이브러리를 만들지 않는다면 정말 알아야 할까 싶은 내용이기도 하다. 그러나 모르면 걸림돌이 될만한 부분을 아직 짚지 않았다. 그리고 낮은 진입장벽으로 분명한 성능적 이득을 볼 수 있으므로 알아서 손해보는 내용은 아니다. 만약 깊은 이해가 필요하다면 값 범주론(Value Category)에 대해 읽어보는 것을 추천한다.
13.1. 기본 생성자
기본 생성자는 특수 멤버 함수에 속한다.- C++20 까지는
virtual함수나 코루틴 함수가 아니고, GOTO 및try-catch및 어셈블리 구문이 없으면constexpr일 수 있다. - C++23 부터는 완화되어
try-catch, 어셈블리 구문이 없으면constexpr일 수 있다. - C++26 부터는 완화되어 어셈블리 구문이 없으면
constexpr일 수 있다 [19].
13.2. 소멸자
소멸자 역시 특수 멤버 함수에 속한다.- C++17 까지는
constexpr일 수 없었다. 대신 자명한 소멸자는 암시적으로constexpr일 수는 있었다. - C++20 부터는
try-catch, 어셈블리 구문이 없으면constexpr일 수 있다. - C++26 부터는 완화되어 어셈블리 구문이 없으면
constexpr일 수 있다.
13.3. 복사 생성자
class Class {public:Class(Class&);Class(const Class&);Class(volatile Class&);Class(const volatile Class&);}; |
복사 생성자는 인자로 같은 클래스의
lvalue 인스턴스들을 받는 생성자다. 복사 생성자를 사용하면 기존에 만들었던 클래스의 데이터 멤버를 복사해서 새로운 인스턴스를 만들 수 있다. 사용법은 간단하게 생성자의 인자로 먼저 만들어진 인스턴스를 전달하는 것이다. 예를 들면 속성이 조금씩 다른 인스턴스들을 만들 때는 인자들을 전달하는 작업이 반복적인데 복사 생성자를 쓰면 번거로움을 줄일 수 있다.#!syntax cpp
class Paper
{
public:
Paper(int w = 1, h = 1) : Width(w), Height(h) {};
Paper(const Paper& rhs) : Width(rhs.w), Height(rhs.h) {};
private:
int Width, Height;
};
// 기본 생성
Paper paper1;
// 복사 생성
Paper paper2(paper1);
Paper paper3 = Paper(paper1);
// 복사 생성 (리스트 초기화)
Paper paper4{ paper1 };
Paper paper5 = Paper{ paper1 };
#!syntax cpp
Paper CopyPaper(const Paper& target)
{
auto result = Paper(target); // 복사 생성
return result;
}
Paper origin;
// (1) 함수 내부에서 매개변수 `target`이 `result`로 복사됨.
auto paper1 = CopyPaper(origin);
// (2) `target`으로 리터럴 Paper()가 이동됨, 함수 내부에서 매개변수 `target`이 `result`로 복사됨.
// const Paper& target에 임시 객체가 이동 생성됨.
auto paper2 = CopyPaper(Paper());
복사 생성자는 의외로 C++ 구석구석에 이용되는데 이게 성능 문제나 소멸자에서 말썽을 일으키는 경우가 있다. const&가 rvalue도 받으면서 임시 객체가 만들어지며 여기서 문제가 발생한다. 이것 때문에 C++11에서 rvalue만을 받는 이동 생성자가 추가되었다.- C++20 까지는
virtual함수나 코루틴 함수가 아니고, GOTO 및try-catch및 어셈블리 구문이 없으면constexpr일 수 있다. - C++23 부터는 완화되어
try-catch, 어셈블리 구문이 없으면constexpr일 수 있다. - C++26 부터는 완화되어 어셈블리 구문이 없으면
constexpr일 수 있다.
13.4. 복사 대입 연산자
class Class {public:Class& operator=(Class&);Class& operator=(const Class&);Class& operator=(volatile Class&);Class& operator=(const volatile Class&);}; |
복사 대입 연산자는 인자로 같은 클래스의
lvalue 인스턴스를 받는 대입 연산자다. 이 연산자는 인스턴스의 lvalue를 반환한다.13.5. 이동 생성자
class Class {public:Class(Class&&);Class(const Class&&);Class(volatile Class&&);Class(const volatile Class&&);}; |
이동 생성자는 인자로 같은 클래스의
xvalue, prvalue 인스턴스를 받는 생성자다. lvalue는 받을 수 없다.#!syntax cpp
class Paper
{
public:
Paper(int w = 1, h = 1) : Width(w), Height(h) {};
Paper(Paper&& rhs) : Width(std::move(rhs.w)), Height(std::move(rhs.h)) {};
private:
int Width, Height;
};
Paper paper1;
// 이동 생성 (리터럴 사용, prvalue)
Paper paper2(Paper());
Paper paper3{ Paper() };
// 이동 생성 (형변환 사용, xvalue)
Paper paper4(std::move(paper1));
Paper paper5{ static_cast<Paper&&>(paper1) };
참고로 이동 생성자를 정의하면 복사 생성자가 자동으로 삭제된다. 이를 원하지 않으면 다시 구현해줘야 한다.- C++20 까지는
virtual함수나 코루틴 함수가 아니고, GOTO 및try-catch및 어셈블리 구문이 없으면constexpr일 수 있다. - C++23 부터는 완화되어
try-catch, 어셈블리 구문이 없으면constexpr일 수 있다. - C++26 부터는 완화되어 어셈블리 구문이 없으면
constexpr일 수 있다.
13.6. 이동 대입 연산자
class Class {public:Class& operator=(Class&&);Class& operator=(const Class&&);Class& operator=(volatile Class&&);Class& operator=(const volatile Class&&);}; |
이동 대입 연산자는 인자로 같은 클래스의
rvalue 인스턴스를 받는 대입 연산자다. lvalue는 받을 수 없다.참고로 이동 대입 연산자를 정의하면 복사 생성자는 자동으로 삭제된다. 이를 원하지 않으면 다시 구현해줘야 한다.
13.7. 동등 비교 연산자
|
동등 비교 연산자 혹은 등호 연산자는
==로 수행하는 인스턴스 간의 동일성을 판단하는 연산자다. 이 연산자 역시 암시적으로 정의된다. 동치(Equivalence)가 아닌 동등(Equality) 연산자다. C++의 == 연산자는 다른 언어의 === 연산자와 같은 역할을 한다. 적당히 같다고 치는 동치의 경우 후술할 3방향 비교 연산자를 사용해야 한다.표준에선 이 연산자는
prvalue bool을 반환하도록 하고 있는데 쉽게 말해 반환형은 오직 bool 말고 다른건 쓰지 말라는 뜻이다.13.8. 3방향 비교 연산자
|
우주선 연산자라고도 부르는 3방향 비교 연산자는 C++20에서 추가된 비교 연산자다. C++20 이전까지의 관계 연산자(
<, <=, >, >=)들을 한번에 대체하기 위해 추가되었다. 이 연산자는 다른 언어에서 보이는 CompareTo() 류의 함수와 동등한 역할을 수행한다. <=> 연산자에서 반환하는 비교 결과 객체들은 -1, 0, +1 혹은 비교 결과에 따른 값을 내부에 들고 있다. 최종적으로는 int 내지는 정수형으로 반환되어 이 값이 0보다 큰지 작은지에 따라 <, <=, >, >=를 수행할지 말지를 결정한다. 이 연산자를 사용하려면 <compare>를 사용해야 한다. 이는 C++에서 언어 기능이 라이브러리 구현에 의존하는 요소 중 하나다 [20].비교 연산자를 구현하는 방식에는 여러 문제가 있었다. C++20 이전에는
==, < 뿐만 아니라 <, <=, >, >=도 전부 따로 구현해줘야 했다. ! 등을 붙여 역, 이, 대우로 구현할 때도 연산자 함수가 inline이 아니면 호출 오버헤드도 있다. C++20 부터는 ==, < 연산자만 추가하면 알아서 다른 비교 연산자를 정의해주지만 이것도 문제가 있다. 정확히 어떤 과정을 거쳐서 비교하는 지 알 수가 없었다.3방향 비교 연산자는 비교 방식을 구현자 입장에서 엄밀하게 정의할 수 있으며, 사용자 입장에서도 어떻게 비교가 이루어질지 알 수 있다. 또한 지원만 한다면 비교한 결과를 단순한
bool 뿐만 아니라 비교한 차이 값을 알아낼 수도 있다. 그리고 비교 과정을 단순화시켜 최적화에도 유리함이 있다.| <rowcolor=#d7d7d7,#a1a1a1>관계 | std::weak_ordering | std::strong_ordering | std::partial_ordering |
| 이전 순서 | <rowcolor=#090912,#bebebf>less | less | less |
| 다음 순서 | <rowcolor=#090912,#bebebf>greater | greater | greater |
| 동치 | <rowcolor=#090912,#bebebf>equivalent | equivalent | equivalent |
| 동등 | <rowcolor=#090912,#bebebf>- | equal | - |
| 비교 불가능 | <rowcolor=#090912,#bebebf>- | - | unordered |
| 변환 가능성 | <rowcolor=#090912,#bebebf>std::partial_ordering | std::weak_orderingstd::partial_ordering | - |
std::weak_ordering, std::strong_ordering, std::partial_ordering 3가지를 지원한다. 이 세 가지 클래스는 비교하는 방식 및 결과 값이 다르다. std::strong_ordering이 아니면 동등하다고 판정하지 않으며 덕분에 ==와 <=> 연산자는 서로 역할이 겹치지 않고 공존할 수 있다. 또한 비교용 함자 클래스 std::compare_three_way를 제공한다.std::partial_ordering은 비교가 실패할 경우도 상정하고 있다. C++20부터 표준 라이브러리의 부동 소수점 비교는 std::partial_ordering을 쓰는데, 정수형과는 다른 규칙이 많기 때문이다. 가령 부동 소수점의 -0.0f, +0.0f는 서로 다른 값이지만 서로 동치다. Infinity, NaN 등과 비교하는 경우 std::partial_ordering::unordered가 반환된다.동치와 동등의 차이는 정말로 완전히 동일한 객체인가, 아니면 일부 값 혹은 적당한 조건만 있으면 같다고 판정할 수 있는지의 차이다. 표준에서는 동등한 객체는
lhs == rhs가 동등함을 판정한다. 정말로 완전 동일한 객체라면 주소를 비교해서 같은지 비교할 수 있을 것이다. 하지만 주소를 비교하는 건 lvalue 에서만 쓸 수 있으므로 적절하지 않다. !(lhs < rhs) && !(rhs < lhs)로 동치임을 판정한다. 예시는 연관 컨테이너에서 값을 삽입하거나 찾아낼 때 lhs == rhs 대신 !(lhs < rhs) 처럼 비교하는 경우가 있다. 이때 비교해서 참이 나온 객체 둘이 정말 동일한 존재인지는 모르겠지만 어쨌든 연관 컨테이너 안에서는 같은 걸로 퉁치는 것이다.<=> 연산자는 암시적으로 생성되지는 않지만 후술할 default 예약어를 써서 생성할 수 있다. 이렇게 생성되는 <=> 연산자는 클래스의 비정적 데이터 멤버들을 전부 선언 순서대로 비교한다.14. 복사 생략
복사 생략#!syntax cpp
Paper MakePaper(int w, int h)
{
auto result = Paper(w, h); // 생성
return result; // RVO (Return Value Optimization), result가 이동된다.
}
함수 내부에서 할당한 인스턴스를 반환할 때는 특별한 최적화가 적용된다. 이때는 복사가 아닌 이동이 된다.15. delete
|
delete 예약어를 사용하면 해당 멤버 함수의 사용을 막을 수 있다. 함수가 구현되지 않았을 때 나는 오류를 사용자가 임의로 발생시킬 수 있는 기능이다. 정확한 기전은 먼저 해당 함수를 선언하고, 해당 함수가 사용되면 문법 오류 혹은 링크 오류를 발생시키는 것이다. 무엇이 문제인지 사용자가 즉시 알 수 있다. 이를테면 복사 생성만 지원하는 클래스를 만들때 원래대로라면 복사 생성자를 구현해주는 수고가 들었겠지만 그것 보다는 delete를 써서 원하는 멤버 함수만 선별할 수 있다.주로 클래스의 복사/이동 여부를 결정하는 데 쓰이지만 특수 멤버 함수 뿐만 아니라 모든 비정적/정적 함수에 사용할 수 있다.
16. 암시적 특수 멤버 함수
앞서 복사와 이동에 대해 알아보았다. 클래스의 데이터 조작 방법에 기본 생성자와 소멸자, 복사 생성자, 이동 생성자, 복사 대입 연산자, 이동 대입 연산자가 있음도 알아보았다. 특수 멤버 함수를 사용해서 데이터 흐름을 제어할 수 있다. 그리고 이를 제어하는 방법도 조금 알아보았다. 그러면 이 제어할 수 있다는 건 무슨 뜻일까? 해당하는 생성자와 대입 연산자가 존재한다는 뜻이다. 다시 존재한다는 건 무슨 뜻일까? 당연히 선언 또는 정의가 있다는 뜻이다.상기한 특수 멤버 함수들은 모두 클래스 안에 암시적으로 생성되는 선언이 존재한다. 그런데 특수 멤버 함수는 정의가 없어질 수도 있다! 이들이 필요로하는 조건이 충족되어야지 컴파일 과정에서 정의가 생성된다. 비정적 데이터 멤버들이 특수 멤버 함수의 정의를 생성하는데에 적합해야 하며 정의가 생성되지 않을 수도 있다. 가령 비정적 데이터 멤버가 복사 가능해야 인스턴스를 복사할 수 있으며, 이동 가능해야 이동시킬 수 있다. 이때 정의가 없이 선언만 있는 특수 멤버 함수는
delete와 같은 판정으로서 사용하면 컴파일 오류가 발생한다.만약 선언과 정의가 존재하며
delete되지 않은 특수 멤버 함수가 있으면 클래스가 적법한 생성자 또는 적법한 대입 연산자 등을 가지고 있다고 말한다. 적법(Eligible)하다는 것은 암시적으로 생성되지 않아도 상관없다. 직접 구현해도 적법한 함수로 인정한다. 클래스가 해당하는 멤버 함수를 갖고 있기만 하면 적법하다고 인정한다. 그렇지 않으면 적법한 생성자, 적법한 대입 연산자 등이 없다고 말한다.16.1. 암시적 기본 생성자
암시적으로 선언된 기본 생성자 (Implicit-declared Default Constructor)기본 생성자는 비정적 데이터 멤버들이 기본값으로 초기화할 수 있을 때 암시적으로 생성된다.
적법한 기본 생성자 (Eligible Default Constructor)
암시적 기본 생성자가 정의되지 않는 충분조건은 다음과 같다:
- 기본 생성자를
delete했을 경우. - 클래스 정의에서 상수가 아닌 비정적 데이터 멤버에 기본값을 할당한 경우.
- 클래스 정의에서 참조자가 아닌 비정적 데이터 멤버에 기본값을 할당한 경우.
- 비정적 데이터 멤버 중에 기본 생성자가
delete된 변수가 있는 경우. - 비정적 데이터 멤버 중에 기본 생성자에서 권한 때문에 접근하지 못하는 소멸자를 가진 변수가 있을 경우.
- 잠재적 기본 생성자의 후보 중에 적법한 대상이 없는 경우. C++20[21]
- 비정적 데이터 멤버 중에 잠재적 기본 생성자의 후보 중에 적법한 대상이 없어서 기본 생성자가 없는 변수가 있을 경우. C++20
- 비정적 데이터 멤버 중에 모든 비정적 데이터 멤버가 상수인
union이 있을 경우.
정리하자면 기본값이 필요한
const, &, &&를 제외한 비정적 데이터 멤버들에 기본값을 할당하면 안된다. 소멸자는 언제나 사용할 수 있어야 한다. 모든 비정적 데이터 멤버들이 적법한 기본 생성자를 갖고 있어야 한다. 사실 어려운 조건은 아니고 당연히 피해야 할 상황이다. 설령 암시적으로 생성된 기본 생성자가 아니더라도 이 조건들은 클래스를 사용하면 나타나면 안되는 상황들이다.16.2. 암시적 소멸자
암시적으로 선언된 소멸자 (Implicit-declared Destructor)소멸자는 비정적 데이터 멤버들을 파괴할 수 있으면 암시적으로 생성된다.
소멸자는 클래스의 핵심 기능으로써 항상 사용할 수 있음을 보장받아야 한다. 소멸자를 못 쓰는 경우 무언가 잘못된 상황인지 확인해야 한다. 암시적으로 생성되지 못한 경우 직접 구현해도 적법한 소멸자로 인정한다.
삭제된 소멸자 (Deleted Destructor)
암시적 기본 소멸자가 정의되지 않는 충분조건은 매우 적다:
#!syntax cpp
class Class
{
public:
~Class() = delete;
// C++20
~Class() requires (false);
union { const int data; };
private:
~Class();
};
// std::is_destructible<Class> == false
// std::is_nothrow_destructible<Class> == false
- 소멸자를
delete했을 경우. - 비정적 데이터 멤버 중에
delete된 소멸자 혹은 권한때문에 접근하지 못하는 소멸자를 가진 인스턴스가 있을 경우. - 잠재적 소멸자 후보 중에 적법한 오버로딩 대상이 없는 경우. C++20[22]
- 가상 기반 클래스에서 상속 받지 않은 비정적 데이터 멤버 중에 공용체(Union)가 있는 경우.
- 가상 소멸자인데
operator delete(Class*)가delete된 경우.
union 정도 빼고는 아주 예외적인 상황이다. union은 웬만하면 쓰지말고 표준 라이브러리의 std::variant를 쓰자.16.3. 암시적 복사 생성자
암시적으로 선언된 복사 생성자 (Implicit-declared Copy Constructor)비정적 데이터 멤버들이 복사가능할 때, 기본적으로 복사 생성자를 만들어준다. 이 보이지 않는 복사 생성자를 암시적 복사 생성자라고 칭한다. 암시적 복사 생성자는 문자 그대로
explicit(명시적) 생성자가 아니다. 암시적 복사 생성자는 Class(const Class&); 또는 Class(const volatile Class&);의 서명을 가진다.암시적 복사 생성자는 다음과 조건 중 하나라도 해당되면 정의되지 않는다:
- 복사 생성자가
delete된 경우. - 비정적 데이터 멤버 중에 우측값 참조자
&&가 있는 경우. - 직접 구현한 이동 생성자 혹은 이동 대입 연산자가 있는 경우. [23].
- 잠재적 복사 생성자 후보 중에 적법한 오버로딩 대상이 없는 경우. C++20
- 비정적 데이터 멤버 중에
delete된 소멸자 혹은 복사 생성자에서 권한 때문에 접근하지 못하는 소멸자를 가진 인스턴스가 있을 경우. - 공용체(
union) 데이터 멤버가 있는 경우.
사용자가 클래스에 다른 종류의 생성자를 만들었더라도, 그게 이동 생성자가 아니고 데이터 멤버가 복사 가능하다면 알아서 만들어진다.
16.4. 암시적 복사 대입 연산자
암시적으로 선언된 복사 대입 연산자 (Implicit-declared Copy Assignment Operator)만약 암시적 복사 생성자가 존재하면, 마찬가지로 암시적 복사 대입 연산자도 존재한다. 이 보이지 않는 대입 연산자를 암시적 복사 대입 연산자라고 칭한다. 암시적 복사 대입 연산자는
Class& operator=(const Class&); 형태로 volatile은 받지 않는다. 또한 인스턴스 자신의 lvalue를 반환한다.참고로 원래 복사가 불가능했던 클래스에 직접 복사 생성자를 작성해도 복사 대입 연산자는 자동으로 정의되지 않는다. 이것도 수동으로 작성해줘야 한다. 표준 라이브러리의
std::is_copy_constructible, std::is_copy_assignable의 구분은 이를 위한 것이다. 그러나 개념 std::copyable은 둘 모두를 검사하므로 이를 사용하기 위해서는 복사 생성자, 복사 대입 연산자를 둘 다 직접 구현해줘야 한다.16.5. 암시적 이동 생성자
암시적으로 선언된 이동 생성자 (Implicit-declared Move Constructor)C++에서는 비정적 데이터 멤버들이 이동가능할 때, 기본적으로 보이지 않는 이동 생성자를 만들어준다. 이 보이지 않는 이동 생성자를 암시적 이동 생성자라고 칭한다. 암시적 이동 생성자는 이름대로
explicit(명시적) 생성자가 아니다. 암시적 이동 생성자는 Class(Class&&); 서명을 가지고 const 및 volatile은 받지 않는다.암시적 이동 생성자는 다음과 조건 중 하나라도 해당되면 정의되지 않는다:
- 이동 생성자가
delete된 경우. - 암시적이 아니고 사용자가 직접 정의한 복사 생성자, 복사 대입 연산자, 이동 대입 연산자가 있는 경우.
- 비정적 데이터 멤버 중에
delete된 소멸자 혹은 이동 생성자에서 권한 때문에 접근하지 못하는 소멸자를 가진 인스턴스가 있을 경우. - 공용체(
union) 데이터 멤버가 있는 경우.
그외엔 사용자가 클래스에 어떠한 종류의 생성자를 만들었더라도 데이터 멤버가 이동 가능하다면 알아서 만들어진다.
16.6. 암시적 이동 대입 연산자
암시적으로 선언된 이동 대입 연산자 (Implicit-declared Move Assignment Operator)이동 생성자가 암시적으로 존재하면 마찬가지로 암시적인 이동 대입 연산자도 존재한다. 이 보이지 않는 대입 연산자를 암시적 이동 대입 연산자라고 칭한다. 암시적 이동 대입 연산자는
Class& operator=(Class&&); 형태로 const 및 volatile은 받지 않는다. 또한 인스턴스 자신의 lvalue를 반환한다.참고로 원래 이동이 불가능한 클래스에 새로 이동 생성자를 작성해도 이동 대입 연산자는 자동으로 정의되지 않는다. 표준 라이브러리의
std::is_move_constructible, std::is_move_assignable의 구분은 이를 위한 것이다. 그러나 개념 std::movable은 둘 모두를 검사하므로 이를 사용하기 위해서는 이동 생성자, 이동 대입 연산자를 둘 다 직접 구현해줘야 한다.17. default
|
= default; 구문으로 특수 함수들이 정의되도록 지시할 수 있다. 예를 들어 이동 생성자를 직접 정의하면 복사 생성자와 복사 대입 연산자가 자동으로 삭제되는데, Class(const Class&) = default;를 넣어주면 직접 구현할 필요없이 복사 생성자를 만들 수 있다.18. 자명함
자명함 (Trivial)C++11자명하다는 건 수학적으로 해를 언제나 찾을 수 있으며 찾는 과정도 명백하다는 뜻이다. 어떤 존재가 자명하다는 것은 그 객체가 스스로 그리고 자연스럽게 존재한다는 말이다. C++에서는 객체가 자명하다는 표현을 쓴다. 곧 해당 객체는 C++ 코드 어디에서나 바로 정의될 수 있고, 시작과 끝이 있는 명백한 동작을 수행한다는 뜻이다. 어디에서나 정의된다는 것은 링크 시점, 컴파일 시점, 실행 시점 모든 경우를 말한다. 시작과 끝이 있는 명백한 동작은 결정론적인 코드를 말한다. 즉 객체에 처음 조건이 주어지면 유한한 알고리즘 과정을 통해 상수 시간에 결과를 알 수 있다는 말이다. 다시 말해서 객체가 자명하다는 것은 객체의 선언, 정의, 실행이 상수 시간에 결정될 수 있음을 말하는 것이다. 자명성은 C++의 핵심 기능인 상수 표현식의 기초가 되었다.
#!syntax cpp
struct TrivialWithPrimitives
{
bool myBoolean;
char myChar;
short myShort;
unsigned short myUnsignedShort;
wchar_t myWideChar;
int myInteger;
unsigned int myUnsignedInteger;
long long myLongLong;
unsigned long long myUnsignedLongLong;
const char* myString;
void* myHandle;
};
모든 원시 자료형도 자명한 자료형으로 간주된다.C++에서 자명해질 수 있는 객체는 변수, 함수, 자료형(클래스)의 세가지 유형이 있다. C++11부터 변수와 함수는
constexpr, noexcept, 그리고 C++20에서 consteval을 도입해 자명함을 갖추었다. 하지만 클래스는 단순히 이 지시자들을 멤버에 적용하는 것만으로는 자명하다고 볼 수 없다. 컴파일 시점에 데이터 멤버의 값이 결정되더라도 멤버 함수의 동작이 결정론적일 것이라는 보장은 없으며, 반대의 경우도 마찬가지다. C++에서 클래스의 자명함을 평가할 때는 비정적 데이터 멤버에 초점을 맞춘다. 이는 정적 데이터 멤버가 외부에서 온 것이기 때문에, 클래스 자체에 대한 정보로 보기 어렵다는 점에서 비롯된다. 메타 데이터 역할을 하더라도, 정의를 위해선 외부 구현이 필요하다. 멤버 함수는 실행 시간에 수행되는 것이 주 목적이므로, 자명성 평가에서는 고려하지 않는다. 클래스의 어떤 동작이 O(1)에 완료되어 성능상의 이점을 가져오더라도, 이는 자명함에 영향을 주지 않는다. 자명함은 초기 조건에서 결과가 도출됨을 의미하며, 결정론적 함수의 존재나 클래스의 특정 성공은 중요하지 않다. 클래스의 용도가 멤버 함수에만 국한되지 않기 때문이다! 따라서 클래스의 자명함은 기본 생성자, 소멸자, 복사 생성자와 복사 대입 연산자, 이동 생성자와 이동 대입 연산자의 여섯 가지가 제대로 정의되었는지로 판단한다.18.1. 자명한 기본 생성자
Trivial Default Constructor자명한 기본 생성자를 갖기 위한 필요조건은 다음과 같다:
- 적법한 기본 생성자가 암시적으로 정의되거나,
default여야 한다. - 모든 비정적 데이터 멤버들이 자명한 기본 생성자를 갖고 있어야 한다.
- 비정적 데이터 멤버에 기본값을 할당해서는 안된다.C++11
- 소멸자를 포함해 가상 멤버 함수를 가질 수 없고 당연히 추상 클래스여서도 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 바로 상속받는(1계위) 기반 클래스들이 자명한 기본 생성자를 갖고 있어야 한다.
18.2. 자명한 소멸자
Trivial Destructor자명한 소멸자를 갖기 위한 필요조건은 다음과 같다:
- 적법한 소멸자가 암시적으로 정의되거나,
default여야 한다. - 모든 비정적 데이터 멤버들이 자명한 소멸자를 갖고 있어야 한다.
- 가상 소멸자이면 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 바로 상속받는(1계위) 기반 클래스들이 자명한 소멸자를 갖고 있어야 한다.
18.3. 자명한 복사 생성자
Trivial Copy Constructor자명한 복사 생성자를 갖기 위한 필요조건은 다음과 같다:
- 적법한 복사 생성자가 암시적으로 정의되거나,
default여야 한다. - 모든 비정적 데이터 멤버들이 자명한 복사 생성자를 갖고 있어야 한다.
- 소멸자를 포함해 가상 멤버 함수를 가질 수 없고 당연히 추상 클래스여서도 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 바로 상속받는(1계위) 기반 클래스들도 자명한 복사 생성자를 갖고 있어야 한다.
18.4. 자명한 복사 대입 연산자
Trivial Copy Assignment Operator자명한 복사 대입 연산자를 갖기 위한 필요조건은 다음과 같다:
- 적법한 복사 대입 연산자가 암시적으로 정의되거나,
default여야 한다. - 모든 비정적 데이터 멤버들이 자명한 복사 대입 연산자를 갖고 있어야 한다.
- 소멸자를 포함해 가상 멤버 함수를 가질 수 없고 당연히 추상 클래스여서도 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 바로 상속받는(1계위) 기반 클래스들이 자명한 복사 대입 연산자를 갖고 있어야 한다.
18.5. 자명한 이동 생성자
Trivial Move Constructor자명한 이동 생성자를 갖기 위한 필요조건은 다음과 같다:
- 적법한 이동 생성자가 암시적으로 정의되거나,
default여야 한다. - 모든 비정적 데이터 멤버들이 자명한 이동 생성자를 갖고 있어야 한다.
- 소멸자를 포함해 가상 멤버 함수를 가질 수 없고 당연히 추상 클래스여서도 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 바로 상속받는(1계위) 기반 클래스들이 자명한 이동 생성자를 갖고 있어야 한다.
18.6. 자명한 이동 대입 연산자
Trivial Move Assignment Operator자명한 이동 대입 연산자를 갖기 위한 필요조건은 다음과 같다:
- 적법한 이동 대입 연산자가 암시적으로 정의되거나,
default여야 한다. - 모든 비정적 데이터 멤버들이 자명한 이동 대입 연산자를 갖고 있어야 한다.
- 소멸자를 포함해 가상 멤버 함수를 가질 수 없고 당연히 추상 클래스여서도 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 바로 상속받는(1계위) 기반 클래스들이 자명한 이동 대입 연산자를 갖고 있어야 한다.
19. 클래스 유형
클래스의 속성19.1. 자명하게 복사 가능한 클래스
자명하게 복사 가능 (Trivially Copyable)C++11자명하게 복사 가능한 클래스는 내부 데이터 멤버가 잘 정의된 클래스를 말한다. 비정적 참조형 데이터 멤버가 없으며 모든 비정적 데이터 멤버는 자명한 값을 가지고 있다. 곧 모든 클래스의 정보가 외부로부터 의존하지 않으며 클래스 스스로 값을 가지고 있다. 그래서
std::memcpy, std::memmov, 혹은 기타 바이트 변환 함수를 사용해서 모든 클래스의 정보를 상수 시간에 직렬화할 수 있다.자명하게 복사 가능한 클래스가 되기 위한 필요조건은 다음과 같다:
delete되지 않은 자명한 소멸자를 갖고 있어야 한다.- 적법한 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자 중 하나 이상을 갖고 있어야 한다.
- 모든 적법한 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자는 암시적으로 정의되거나,
default여야 한다.
19.2. 자명한 클래스
자명한 클래스 (Trivial Class)C++11자명한 클래스는 C++ 코드 어디에서나, 자연스럽게 존재하는, 스스로 존재하는 클래스라는 뜻이다. 자명한 클래스는 다른 클래스에 의존하지 않고, 참조하지도 않는다. 또한 어떠한 문맥에도 의존하지 않는다. 클래스 자명성의 의의는 이 클래스의 존재, 생성, 파괴가 컴파일 시점에 결정될 수 있다는 것이다. 즉, 어떠한 부작용(Side Effect) 없이, 결정론적인(Deterministic) 코드를 생성할 수 있다. 사용자는 결정론적 코드로 데이터의 흐름을 결정하고 그 결과를
O(1)의 시간에 확인할 수 있다. constexpr 함수의 반환형, 인자로써, 그리고 함수 내부에서 생성하고 사용할 수 있다. 또한 템플릿의 매개변수로 직접 사용할 수 있다.자명한 클래스가 되기 위한 필요조건은 다음과 같다:
delete되지 않은 적법하고 자명한 기본 생성자를 갖고 있어야 한다.- 자명하게 복사 가능해야 한다.
19.3. 표준 규격 클래스
표준 규격 클래스 (Standary layout class)C++11표준 규격이란 C언어의 자료구조 규격을 말한다. 다형성, 상속 기능을 배제하고 데이터 멤버들이 규칙을 만족하면 C언어의 구조체와 완전히 호환된다. 자명한 클래스에서 모든 데이터 멤버의 순서와 권한이 고정된 상태와 같다. 마찬가지로
std::memcpy, std::memmov, 혹은 기타 바이트 변환 함수를 사용해서 모든 클래스의 정보를 상수 시간에 직렬화할 수 있다.표준 규격 클래스가 되기 위한 필요조건은 다음과 같다:
- 첫번째로 놓인 비정적 데이터 멤버의 자료형이 기반 클래스이면 안된다.
- 표준 규격이 아닌 자료형 및 표준 규격이 아닌 자료형의 배열을 비정적 데이터 멤버로 가지면 안된다.
- C++ 객체가 아닌 자료형의 비정적 데이터 멤버를 가지면 안된다. 즉 참조형 비정적 데이터 멤버는 가질 수 없다.
- 모든 비정적 데이터 멤버는 같은 접근 권한 하에 있어야 한다.
- 소멸자를 포함해 가상 멤버 함수를 가질 수 없고 당연히 추상 클래스여서도 안된다.
- 기반 클래스 중에 가상 클래스가 있으면 안된다.
- 기반 클래스 중에 비정적 데이터 멤버를 가진 클래스는 오직 한 개 이하여야 한다.
- 모든 기반 클래스가 상기한 표준 규격을 만족하는 클래스여야만 한다.
20. 둘러보기
||<:><-12><width=90%><tablewidth=100%><tablebordercolor=#20b580><rowbgcolor=#090f0a,#050b09><rowcolor=#d7d7d7,#a1a1a1>C++||||
}}}
}}}||
}}}||
| C언어와의 차이점 | 학습 자료 | 평가 | |||||||||||||
| <bgcolor=#20b580> | |||||||||||||||
| <rowcolor=#090912,#bebebf>C++ 문법 | |||||||||||||||
| <bgcolor=#ffffff> | |||||||||||||||
main | 헤더 | 모듈 | |||||||||||||
| 함수 | 구조체 | 이름공간 | |||||||||||||
| 한정자 | 참조자 | 포인터 | |||||||||||||
| 클래스 | 값 범주론 | 특성 | |||||||||||||
auto | using | decltype | |||||||||||||
| 상수 표현식 | 람다 표현식 | 객체 이름 검색 | |||||||||||||
| 템플릿 | 템플릿 제약조건 | 메타 프로그래밍 | |||||||||||||
| <bgcolor=#20b580> | |||||||||||||||
| <rowcolor=#090912,#bebebf>C++ 버전 | |||||||||||||||
| <bgcolor=#ffffff> | |||||||||||||||
| C++26 | C++23 | C++20 | |||||||||||||
| C++17 | C++14 | C++11 | |||||||||||||
| C++03 | C++98 | C with Classes | |||||||||||||
| <bgcolor=#20b580> | |||||||||||||||
| <rowcolor=#090912,#bebebf>C++ 표준 라이브러리 | |||||||||||||||
| <rowcolor=#090912,#bebebf>문서가 있는 모듈 목록 | |||||||||||||||
| <bgcolor=#ffffff> | |||||||||||||||
#개요 | C++11 #개요 | <unordered_map>C++11 #개요 | |||||||||||||
C++20 #개요 | #개요 | #개요 | |||||||||||||
C++11 #개요 | C++11 #개요 | C++17 #개요 | |||||||||||||
#개요 | <string_view>C++17 #개요 | C++20 #개요 | |||||||||||||
C++11 #개요 | C++11 #개요 | C++11 #개요 | |||||||||||||
C++20 #개요 | C++23 #개요 | ||||||||||||||
| <bgcolor=#20b580> | |||||||||||||||
| <rowcolor=#090912,#bebebf>예제 목록 | |||||||||||||||
| <bgcolor=#ffffff> | |||||||||||||||
| {{{#!wiki style=""text-align: center, margin: 0 -10px" {{{#!folding [ 펼치기 · 접기 ] | 임계 영역과 경쟁 상태std::mutex | 개선된 스레드 클래스std::jthread | 동시성 자료 구조 1 스레드 안전한 큐 구현 | 동시성 자료 구조 2 스레드 안전한 집합 구현 | |||||||||||
메모리 장벽std::atomic_thread_fence | 스레드 상태 동기화 1 스레드 대기와 기상 | 원자적 메모리 수정std::compare_exchange_strong | 스레드 상태 동기화 2 스핀 락 구현 | ||||||||||||
| 함수 템플릿 일반화 프로그래밍 | 전이 참조 완벽한 매개변수 전달 | 튜플 구현 가변 클래스 템플릿 | 직렬화 함수 구현 템플릿 매개변수 묶음 | ||||||||||||
| SFINAE 1 멤버 함수 검사 | SFINAE 2 자료형 태그 검사 | SFINAE 3 메타 데이터 | SFINAE 4 자료형 트레잇 | ||||||||||||
| 제약조건 1 개념 (Concept) | 제약조건 2 상속 여부 검사 | 제약조건 3 클래스 명세 파헤치기 | 제약조건 4 튜플로 함자 실행하기 | ||||||||||||
| 메타 프로그래밍 1 특수화 여부 검사 | 메타 프로그래밍 2 컴파일 시점 문자열 | 메타 프로그래밍 3 자료형 리스트 | 메타 프로그래밍 4 안전한 union | ||||||||||||
}}}||
| <bgcolor=#20b580> | |||||||||||||||
| <rowcolor=#090912,#bebebf>외부 링크 | |||||||||||||||
| <bgcolor=#ffffff> | |||||||||||||||
| {{{#!wiki style=""text-align: center, margin: 0 -10px" | |||||||||||||||
| <bgcolor=#20b580> | |||||||||||
| <rowcolor=#090912,#bebebf>C++ |
[1] 클래스는 사용자가 창조한 새로운 자료형임을 알 수 있다[2] 문법은 태곳적 GOTO문에서 쓰인 블록 선언문과 비슷하다[구두점] [화살표] [5] 값 범주론에서 보면 this는 변수가 아니라 이름이 없는 임시값인 prvalue다[6] 보통은 0이다[7] 함수 명칭을 클래스 이름과 같게 두고, 반환형을 명시하지 않는다[8] 자세한 내용은 레퍼런스를 참고[9] 이외에는 파이썬 등의 스크립트 언어, C# 등에서 연산자 오버로딩을 지원한다[10] Jetbrain의 IDE을 쓰면 메시지로 알려준다[11] 연산자는 friend가 아니면 static일 수 없다[protected] [protected] [protected] [public] [16] 즉 파생 클래스의 멤버는 기반 클래스의 것을 덮어 씌운다[17] 클래스 사이 관계의 종류에는 포함 관계와 종속 관계가 있는데 가령 포함 관계에는 클래스와 클래스의 멤버가 포함된다[18] virtual을 붙일지 말지는 자유다[19] 또한 예외 발생 여부를 컴파일 시점에 알 수 있다[20] 첫번째는 std::exception, 두번째는 코루틴[21] C++20부터 제약조건의 추가로 생성자 여러 개를 만들어두고 requires 구문으로 선택해서 실행할 수 있다[22] C++20부터 제약조건의 추가로 소멸자 여러 개를 만들어두고 requires 구문으로 선택해서 실행할 수 있다[23] 클래스가 이동 연산만 지원하는 걸 상정한 규칙이다
#!if version2 == null
{{{#!wiki style="border:1px solid gray;border-top:5px solid gray;padding:7px;margin-bottom:0px"
[[크리에이티브 커먼즈 라이선스|[[파일:CC-white.svg|width=22.5px]]]] 이 문서의 내용 중 전체 또는 일부는 {{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/C++/문법|C++/문법]]}}}{{{#!if external != "o"
[[C++/문법]]}}}}}} 문서의 {{{#!if uuid == null
'''uuid not found'''}}}{{{#!if uuid != null
[[https://namu.wiki/w/C++/문법?uuid=27a105d0-e4c1-4a4e-9a3c-5aeed834679d|r263]]}}} 판{{{#!if paragraph != null
, [[https://namu.wiki/w/C++/문법?uuid=27a105d0-e4c1-4a4e-9a3c-5aeed834679d#s-5|5번 문단]]}}}에서 가져왔습니다. [[https://namu.wiki/history/C++/문법?from=263|이전 역사 보러 가기]]}}}#!if version2 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="border:1px solid gray;border-top:5px solid gray;padding:7px;margin-bottom:0px"
[[크리에이티브 커먼즈 라이선스|[[파일:CC-white.svg|width=22.5px]]]] 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
{{{#!wiki style="text-align: center"
{{{#!folding [ 펼치기 · 접기 ]
{{{#!wiki style="text-align: left; padding: 0px 10px"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/C++/문법|C++/문법]]}}}{{{#!if external != "o"
[[C++/문법]]}}}}}} 문서의 {{{#!if uuid == null
'''uuid not found'''}}}{{{#!if uuid != null
[[https://namu.wiki/w/C++/문법?uuid=27a105d0-e4c1-4a4e-9a3c-5aeed834679d|r263]]}}} 판{{{#!if paragraph != null
, [[https://namu.wiki/w/C++/문법?uuid=27a105d0-e4c1-4a4e-9a3c-5aeed834679d#s-5|5번 문단]]}}} ([[https://namu.wiki/history/C++/문법?from=263|이전 역사]])
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid2 == null
'''uuid2 not found'''}}}{{{#!if uuid2 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph2 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]]){{{#!if version3 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid3 == null
'''uuid3 not found'''}}}{{{#!if uuid3 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph3 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version4 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid4 == null
'''uuid4 not found'''}}}{{{#!if uuid4 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph4 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version5 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid5 == null
'''uuid5 not found'''}}}{{{#!if uuid5 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph5 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version6 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid6 == null
'''uuid6 not found'''}}}{{{#!if uuid6 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph6 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version7 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid7 == null
'''uuid7 not found'''}}}{{{#!if uuid7 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph7 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version8 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid8 == null
'''uuid8 not found'''}}}{{{#!if uuid8 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph8 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version9 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid9 == null
'''uuid9 not found'''}}}{{{#!if uuid9 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph9 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version10 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid10 == null
'''uuid10 not found'''}}}{{{#!if uuid10 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph10 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version11 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid11 == null
'''uuid11 not found'''}}}{{{#!if uuid11 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph11 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version12 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid12 == null
'''uuid12 not found'''}}}{{{#!if uuid12 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph12 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version13 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid13 == null
'''uuid13 not found'''}}}{{{#!if uuid13 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph13 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version14 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid14 == null
'''uuid14 not found'''}}}{{{#!if uuid14 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph14 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version15 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid15 == null
'''uuid15 not found'''}}}{{{#!if uuid15 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph15 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version16 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid16 == null
'''uuid16 not found'''}}}{{{#!if uuid16 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph16 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version17 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid17 == null
'''uuid17 not found'''}}}{{{#!if uuid17 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph17 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version18 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid18 == null
'''uuid18 not found'''}}}{{{#!if uuid18 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph18 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version19 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid19 == null
'''uuid19 not found'''}}}{{{#!if uuid19 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph19 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version20 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid20 == null
'''uuid20 not found'''}}}{{{#!if uuid20 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph20 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version21 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid21 == null
'''uuid21 not found'''}}}{{{#!if uuid21 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph21 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version22 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid22 == null
'''uuid22 not found'''}}}{{{#!if uuid22 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph22 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version23 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid23 == null
'''uuid23 not found'''}}}{{{#!if uuid23 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph23 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version24 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid24 == null
'''uuid24 not found'''}}}{{{#!if uuid24 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph24 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version25 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid25 == null
'''uuid25 not found'''}}}{{{#!if uuid25 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph25 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version26 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid26 == null
'''uuid26 not found'''}}}{{{#!if uuid26 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph26 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version27 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid27 == null
'''uuid27 not found'''}}}{{{#!if uuid27 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph27 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version28 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid28 == null
'''uuid28 not found'''}}}{{{#!if uuid28 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph28 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version29 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid29 == null
'''uuid29 not found'''}}}{{{#!if uuid29 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph29 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version30 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid30 == null
'''uuid30 not found'''}}}{{{#!if uuid30 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph30 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version31 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid31 == null
'''uuid31 not found'''}}}{{{#!if uuid31 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph31 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version32 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid32 == null
'''uuid32 not found'''}}}{{{#!if uuid32 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph32 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version33 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid33 == null
'''uuid33 not found'''}}}{{{#!if uuid33 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph33 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version34 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid34 == null
'''uuid34 not found'''}}}{{{#!if uuid34 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph34 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version35 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid35 == null
'''uuid35 not found'''}}}{{{#!if uuid35 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph35 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version36 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid36 == null
'''uuid36 not found'''}}}{{{#!if uuid36 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph36 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version37 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid37 == null
'''uuid37 not found'''}}}{{{#!if uuid37 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph37 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version38 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid38 == null
'''uuid38 not found'''}}}{{{#!if uuid38 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph38 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version39 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid39 == null
'''uuid39 not found'''}}}{{{#!if uuid39 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph39 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version40 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid40 == null
'''uuid40 not found'''}}}{{{#!if uuid40 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph40 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version41 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid41 == null
'''uuid41 not found'''}}}{{{#!if uuid41 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph41 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version42 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid42 == null
'''uuid42 not found'''}}}{{{#!if uuid42 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph42 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version43 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid43 == null
'''uuid43 not found'''}}}{{{#!if uuid43 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph43 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version44 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid44 == null
'''uuid44 not found'''}}}{{{#!if uuid44 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph44 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version45 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid45 == null
'''uuid45 not found'''}}}{{{#!if uuid45 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph45 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version46 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid46 == null
'''uuid46 not found'''}}}{{{#!if uuid46 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph46 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version47 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid47 == null
'''uuid47 not found'''}}}{{{#!if uuid47 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph47 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version48 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid48 == null
'''uuid48 not found'''}}}{{{#!if uuid48 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph48 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version49 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid49 == null
'''uuid49 not found'''}}}{{{#!if uuid49 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph49 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}{{{#!if version50 != null
{{{#!wiki style="display: block;"
{{{#!wiki style="display: inline-block"
{{{#!if external == "o"
[[https://namu.wiki/w/|]]}}}{{{#!if external != "o"
[[]]}}}}}} 문서의 {{{#!if uuid50 == null
'''uuid50 not found'''}}}{{{#!if uuid50 != null
[[https://namu.wiki/w/?uuid=|r]]}}} 판{{{#!if paragraph50 != null
, [[https://namu.wiki/w/?uuid=#s-|번 문단]]}}} ([[https://namu.wiki/history/?from=|이전 역사]])}}}}}}}}}}}}}}}}}}}}}