1. 특수 멤버 함수
컴파일러가 자동으로 생성하는 클래스나 구조체의 멤버 함수
생성자/할당 연산자/소멸자로 구성된다
특수 멤버 함수는 클래스나 구조체에서, 컴파일러가 자동으로 생성하는 멤버 함수(메서드)를 뜻한다.
특수 멤버 함수는 생성자, 할당 연산자, 소멸자로 구성되며 그 종류는 다음과 같다.
class X{
public:
/* --- 생성자 --- */
X(); //기본 생성자
X(const X&); //복사 생성자
X(X&&); //이동 생성자
X(int a, int b); //이런 형태의 "일반적인 생성자"는 특수 멤버 함수가 아니다.
/* --- 할당 연산자 --- */
X& operator=(const X&); //복사 할당자
X& operator=(X&&); //이동 할당자
/* --- 소멸자 --- */
~X(); //소멸자
};
- 생성자: 객체 생성 시 호출된다
- 할당자: 이미 생성된 객체에 새로운 값을 할당한다
- 소멸자: 객체 소멸 시 호출된다
이러한 특수 멤버 함수는 개발자가 직접 구현할 수 있다. 만약 구현하지 않은 경우, 컴파일러는 함수가 필요할 경우 암묵적으로 함수를 선언한다(이를 기본 특수 멤버 함수라고 한다)
이 중, 복사와 이동에 대해 설명한다.
2. 복사 생성자/할당자
기본 복사 생성자/할당자는 각 멤버 변수(필드)의 값을 복사한다
직접 선언하는 것으로 얕은 복사를 방지하고, 원하는 동작을 구현할 수 있다.
복사는 우측 객체의 변수 값을 복사하여, 좌측 객체를 초기화하거나 재설정한다.
1-1. 기본 복사 생성자/할당자
기본 복사는 멤버 변수의 값을 그대로 복사한다.
이는 얕은 복사등의 문제가 발생할 수 있다.
다음은 "기본" 복사 생성자와 할당자의 동작모습이다.
class CopyTest{
private:
int intData;
float* ptr;
public:
//생성자
CopyTest() = default;
CopyTest(int intInput, int sz): intData{intInput}, ptr{new float[sz]} {};
/* --- !!!!기본 복사 동작의 명시적 선언!!!! --- */
/* --- !!!!선언하지 않더라도 필요할 시 암묵적으로 선언된다!!!! --- */
CopyTest(const CopyTest&) = default; //기본 복사 생성자의 명시적 선언
CopyTest& operator=(const CopyTest&) = default;//기본 복사 할당자의 명시적 선언
//정보 출력
void Info(){
cout << "intData: " << intData <<" / ptr: " << ptr << endl;
}
};
int main(){
CopyTest a{10, 5}; //생성자
CopyTest b{a}; //기본 복사 생성자. 객체 a의 멤버들의 값을 복사해 b를 초기화
CopyTest c{};
c = a; //기본 복사 할당자. 객체 a의 멤버들의 값을 복사해 c에 덮어씌운다
a.Info();
b.Info();
c.Info();
}

기본 복사의 경우, 우측 객체(a)의 각 멤버 변수(intData와 ptr)의 값을 좌측 객체(b와 c)에 그대로 복사한다.
하지만 포인터의 값도 그대로 복사해, 얕은 복사로 인한 문제가 발생할 수 있다.
2-1. 복사 생성자/할당자 직접 정의
포인터를 통해 객체를 처리하는 경우 복사를 직접 구현하여 얕은 복사를 방지한다
이외에도 원하는 멤버 변수만 복사하는 등, 직접 원하는 동작을 구현할 수 있다.
포인터를 통해 객체를 처리하는, 자원 핸들(resource handle)이나 컨테이너 클래스 등에서는 직접 복사를 구현해야 할 필요가 있다.
class IntContainer
{
private:
int* data;
int sz;
public:
//기본 생성자
IntContainer() = default;
//일반적인 생성자
IntContainer(int s): data{new int[s]}, sz{s} {}
/* --- !!!!직접 정의한 복사 생성자!!!! --- */
IntContainer(const IntContainer& obj): data{new int[obj.size()]}, sz{obj.size()}{
for(int i = 0; i < obj.size(); i++){
data[i] = obj[i];
}
}
/* --- !!!!직접 정의한 복사 할당자!!!! --- */
IntContainer& operator=(const IntContainer& obj){
int* ptr = new int[obj.size()];
for(int i = 0; i < obj.size(); i++){
ptr[i] = obj[i];
}
delete[] data;
data = ptr;
sz = obj.size();
return *this;
}
//소멸자
~IntContainer() { delete[] data;}
//기타 멤버 함수
int size() const{return sz;}
int& operator[](int idx) {return data[idx];}
const int& operator[](int idx) const {return data[idx];}
void info() { cout << "data ptr: " << data <<" / size: " << sz << endl; }
};
int main(){
IntContainer a{10};
IntContainer b{a};
IntContainer c{};
c = a;
a.info();
b.info();
c.info();
}

다음과 같이 직접 복사 생성자/할당자를 정의하는 것으로, 얕은 복사를 방지할 수 있으며 원하는 방식으로 구현이 가능하다.
3. 이동 생성자/할당자
이동은 RValue를 참조한다.
기본 이동은 실질적으로 기본 복사와 같다.
복사 과정의 비용을 없애고, 기존 객체의 자원의 소유권을 이전하는 것을 목적으로 구현한다.
class Container
{
private:
int* arrData;
int sz;
}
다음과 같은 컨테이너의 "arrData"를 다른 객체에도 저장하고자 한다. 각 방법과 그 문제점은 다음과 같을 것이다
- "arrData"의 값 자체를 모두 복사(깊은 복사) - arrData의 크기가 클 수록 부담이 커진다
- "arrData"의 포인터를 복사(얕은 복사) - 두 객체가 동일한 주소를 참조하는 문제가 발생한다
이러한 복사는 생각보다 자주 일어날 수 있으며(관련 글), 이때 객체가 크면 상당히 곤란한 상황이 될 수 있다.
이를 해결하기 위해 포인터를 복사하되, 기존 객체의 소유권은 해제하는 것이 바로 이동이다.
X(X&&); //이동 생성자
X& operator=(X&&); //이동 할당자
//이때 &&는 Rvalue 참조 연산자이다
이동 생성자/할당자는 다음과 같은 형태이며, Rvalue 참조를 인자로 받는다.
3-1. Rvalue
식별 가능함은, 값을 다시 가리킬 수 있는 이름이나 주소를 지닌 것이다.
Lvalue는 식별 가능한 값(할당문 왼쪽에 올 수 있는 값)이다
Rvalue는 식별 할 수 없으며, 메모리에 잠시 존재하는 임시 값이다(할당문 왼쪽에 올 수 없는 값)
이동 연산은 Rvalue 참조를 인자로 받는다. 이때 Rvalue는 식별 불가능한 값을 의미한다.
int a = 10;
//a는 Lvalue
//10은 Rvalue
여기서 a와 같이 이름이나 주소를 가져 추적 가능하며, 읽고 쓸수 있는 값을 Lvalue라고 한다.(변수, 참조 등)
그리고 10과 같이 메모리에는 존재하지만 추적할 수 없는, 임시 값을 Rvalue하고 한다.
값을 다시 가리킬 수 있는 이름이나 주소를 가질 시 이를 식별 가능하다고 한다.
즉 Lvalue는 식별 가능한 값, Rvalue는 식별 불가능한 값이다.
정말 간단하게는 Lvalue는 할당문 왼쪽에 올 수 "있는" 값, Rvalue는 왼쪽에 올 수 "없는" 값 정도로 이해하면 된다.
(a = 10, a = b는 가능해도 10 = a는 어떻게 봐도 이상하다)
이와 관련한 자세한 내용은 다른 글에서 설명한다.
3-2. std::move
move는 Lvalue를 Rvalue의 참조로 Casting한다
이동 연산은 Rvalue의 참조(T&&)를 받는다
객체를 Rvalue 참조로 Casting하기 위해 <utility>의 std::move를 사용한다
Class MyClass
{
/* ... */
MyClass(X&&) = defalut; //기본 이동 생성자
MyClass& operator=(X&&) = defalut; //기본 이동 할당자
/* ... */
};
int main{
MyClass a{};
MyClass b{move(a)}; //이동 생성자
MyClass c{};
c = move(b); //이동 할당자
/* ... */
}
함수 move()는 이름과 달리, 그 자체로는 무엇도 이동시키지 않는다. 이 함수는 받은 인자를 Rvalue 참조로 캐스팅하는 역할을 하며 이를 이동 연산에 사용하게 된다.
3-3. 이동 생성자/할당자
이동 연산은 객체의 리소스를 새로운 객체로 이전하고, 원래 객체는 자원을 더 이상 보유하지 않도록 초기화한다.
앞서 말했듯 이동은 복사에 사용되는 자원을 줄이고, 기존 소유권을 해제한다. 다음은 '2-1. 복사 생성자/할당자 직접 정의'의 IntContainer에 이동 연산을 구현한다.
class IntContainer
{
public:
/* ... */
//이동 생성자
IntContainer(IntContainer&& obj): data{obj.data}, sz{obj.size()}{
obj.data = nullptr;//기존 객체의 소유권 초기화
obj.sz = 0;
}
//이동 할당자
IntContainer& operator= (IntContainer&& obj){
data = obj.data;
sz = obj.sz;
obj.data = nullptr; //기존 객체의 소유권 초기화
obj.sz = 0;
return *this;
}
/*...*/
};
다음과 같이 기존 객체의 포인터를 null로 설정해주는 것으로, 새로운 객체만이 주소를 통해 접근하는 것이 가능해진다.
4. default/delete
"= delete"를 통해 함수를 생성을 막는다.
"= default"를 통해 기본 함수를 명시적으로 생성할 수 있다.
복사 혹은 이동을 막아야 하는 경우, "= delete"를 통해 함수를 금지할 수 있다.
다음은 복사를 금지하고 이동을 허용한 예시이다.
class X{
public:
/* --- 생성자 --- */
X() = default; //기본 생성자
X(const X&) = delete; //복사 불가
X(X&&) = default; //기본 이동 생성자
X(int a, int b); //이런 형태의 "일반적인 생성자"는 특수 멤버 함수가 아니다.
/* --- 할당 연산자 --- */
X& operator=(const X&) = delete; //복사 불가
X& operator=(X&&) = default; //기본 이동 할당자
/* --- 소멸자 --- */
~X(); //소멸자
};
"= delete"를 통해 복사 생성자/할당자를 삭제하는 것으로 복사를 방지한다.
이외의 기본 함수의 경우는, 컴파일러가 필요하면 암묵적으로 생성하지만 "= default"를 통해 명시적으로 생성할 수도 있다.
5. Rule of Zero/Five
자원 관리에 대한 규칙
Rule of Zero: 특수 멤버 함수를 하나도 정의하지 않음
Rule of Five(Six): 특수 멤버 함수를 모두 정의
클래스 작성 시 자원 관리에 대한 규칙이 존재한다.
간단히 요약하면 "필수 연산을 전부 정의하지 않거나, 전부 정의하는 것이다"
5-1.Rule of Zero
특수 멤버 함수를 전부 정의하지 않고, 자원 관리는 멤버에게 맡긴다
struct Student {
std::string name; // 복사/이동은 std::string이 처리
std::unique_ptr<int> ptr; // 소유권은 unique_ptr이 처리
};
자원은 RAII 멤버가 각자 관리하며, 특수 맴버 함수를 하나도 정의하지 않는 것을 의미한다.
(RAII - Resource acquisition is initializtion: 생성자가 모든 자원을 확보&소멸자가 모든 자원을 해제해 자원 관리를 암묵적으로 보장)
5-2. Rule of Five(Six)
모든 특수 멤버 함수, 소멸자/복사 생성자/복사 할당자/이동 생성자/이동 할당자 (+ 기본 생성자)를 전부 정의해 자원을 직접 관리
직접 자원을 관리하는 경우, 모든 특수 멤버 함수를 전부 정의하는 것이다.(위의 IntContainer가 그 예이다)
소멸자/복사 생성자/복사 할당자/이동 생성자/이동 할당자를 정의하는 것은 Rule of Five, 여기에 기본 생성자를 포함해 Rule of Six로 부르기도 한다.
잘못된 정보 등 지적 환영
참고 자료
'C++' 카테고리의 다른 글
| C++ - 객체의 이동/복사 발생 경우와 최적화 (3) | 2025.06.13 |
|---|---|
| C++ - 클래스 소개(구체/추상/파생 클래스) (0) | 2025.04.24 |
| C++ - 함수 객체와 람다 (0) | 2025.04.21 |
| C++ - 내장 타입과 사용자 정의 타입 (0) | 2025.04.21 |
| C++ - 초기화 (0) | 2025.04.21 |
댓글