프로그래밍 지식/C++

클래스 생성, 복사, 대입, 소멸 C/C++

게임이 더 좋아 2022. 2. 2. 16:45
반응형
728x170

 

C++의 핵심인 클래스에서

가장 중요한 생성, 복사, 대입, 소멸에 대해서 알아보자.

 


 

클래스를 정의할 때는 명시적이든 암시적으로 하든

클래스 타입 객체를 복사하거나 대입 또는 소멸할 때 만들어줘야할 것이 많다.

 

즉, 클래스가 정상적으로 작동하기 위해서는 5가지 멤버함수가 필요하다.

1. 복사 생성자 (디폴트 있음)

 

2. 복사 대입 연산자 (디폴트 있음)

 

(일반적으로 1번과 2번은 거의 비슷함)

-> 둘다 정의 안하면 컴파일러가 만들어줌

 

3. 이동 생성자(다루지 않을 예정)

 

4. 이동 대입 연산자(다루지 않을 예정)

 

5. 소멸자 (디폴트 있음)

 

생성자, 소멸자까지만 듣고 포기한 사람이 많겠지만 

쉽게 알아보도록 하자.

 

**디폴트가 있는 생성(복사, 대입), 소멸은 무조건 중요하다는 말이 되겠다.

 


 

1. 복사 생성자

 

첫 매개 변수가 해당 클래스 타입에 대한 참조자로 받아서 작동하는 방식이다.

추가 매개변수로는 초기화 값을 받는 것이 보통이다.

 

?? 무슨 말이지??? 참조자를 받는다고?

 

클래스를 정의해보자.

class Foo{

public:
    Foo(); //클래스 이름으로 정의되는 생성자이며 기본 생성자이다.
    Foo(const Foo&) //이것이 바로 복사 생성자로 Foo 타입의 참조자를 받는다.
    ...
    
}

 

** 우선 여기서는 변수를 선언하지 않아서 초기화값을 받지 않았지만

저기 복사 생성자의 뒤의 매개변수로는 각종 초기화 값을 받을 수 있다.

 

왜 const로 받아야하느냐? 라는 질문을 할 수 있다.

아니 어차피 해당 Foo 타입의 값 자체를 받으면 되는 것아냐..?

 

알다시피 참조자나 포인터가 아닌 값을 받을 때는 값복사가 일어난다.

값을 복사하기 위해 Foo의 복사생성자를 다시 부르게 되고

그 복사 생성자는 해당 값을 받아와 다시 복사하기 위해

복사 생성자를 부르는 꼬꼬무(꼬리에 꼬리를 무는 관계)가 되어버린다.

즉, 오류가 난다는 말이다.

 

복잡해서 정의하기 싫다면

물론 복사 생성자를 정의하지 않아도 컴파일러에서 기본 생성자를 만들어서 해준다.

 

복사 생성자를 정의한다면

우리는 이제 어떠한 객체를 만들 때 직접 초기화하는 것과 복사해서 초기화하는 것을 구분할 수가 있다.

 

왜냐?? 복사 초기화는 참조자를 받는다고 했으니까!!

 

string dots(10, '.'); // 당연히 직접 초기화
string s(dots); // 직접 초기화
string s2 = dots; //복사 초기화
string null_book = "99999"; //복사 초기화
string nines = string(100, '9'); //복사 초기화

 

?? 그냥 =을 사용하면 복사 초기화인건가???

맞다.

하지만 = 을 사용할 때만 일어나는 것은 아니다.

그리고 당연하게 객체를 복사할 때 모두 사용된다.

 

예를 들어

1. 객체를 비참조자 타입으로 매개변수에 인자로 전달할 때 -> 이것이 가장 대표적인 예이다.

2. 비참조자 타입을 반환하는 함수에서 객체를 반환할 때

3. 배열 내 요소나 집합클래스의 멤버를 중괄호로 초기화할 때( 초기화 리스트)

 

 

더 자세히 설명하자면

함수를 호출할 때 비참조자 타입인 매개변수를 받게 되면 복사 초기화를 한다.

모든 함수는 값으로( 비참조자 타입)의 변수를 초기화할 때는 해당 값을 복사해서 사용한다.

즉, 해당 객체를 복사할 때, 복사 생성자를 사용하게 된다는 말이다.

 

 

 


 

2. 복사 대입 연산자

 

?? 그냥 = 쓰는 것이 대입 아냐??

맞다.

하지만 복사 대입 연산자를 정의해줘야 한다.

만약 정의하지 않는다면 컴파일러가 또 디폴트 복사 대입 연산자를 만들어준다.

우리의 의도대로 작동하기를 원한다면 정의해주자.

 

우리가 복사 대입 연산자를 정의해봐야겠지?

그것을 위해서는 연산자를 오버로딩해야하는데 여기서는 =(대입)연산자를 오버로딩을 해야 한다.

 

예를 들어 보자

 

= operator에 대해서 오버로딩을 하고 싶다?

거기다 해당 연산자를 그냥 복사하는 역할을 하고 싶다??

 

class Foo{
public:
    Foo& operator=(const Foo&); //대입 연산자 오버로딩
    
    ...
    
}

 

어?? operator 를 쓰고 뒤에 연산자를 쓰는 구나.. 라고 깨달았으면 우선 굿

그 다음에는 저 연산자의 리턴타입을 주목해보자.

 

Foo& 이다. 엥.. 참조자를 반환한다고..?

 

이것은 대입 연산자에 대한 일반적인 리턴타입인데

**일반적으로 대입 연산자는 왼쪽 피연산자에 대한 참조자를 반환해야 한다.

 

그리고 여전히 참조자를 받아서 실행하는 것은 복사생성자와 같다.

 

그렇다면 얘는 언제쓸까??

 

복사 생성자와 비슷하게 작동하는 경우가 많다.

굳이 다른 경우를 찾자면 오른쪽 객체의 멤버들을 복사 대입 연산자를 사용해서

왼쪽 객체의 멤버에 대입할 수도 있다.

 

예를 들면

...

Foo::operator=(const Foo &obj)
{
    name = obj.name;
    money = obj.money;
    ...
    
}

 

하지만.. 저럴거면 복사생성자 쓰는 것이 낫다.

아무튼 다르게 쓸 수도 있다는 것을 명심하자.

 


 

3. 소멸자

 

소멸자는 생성자의 역으로 생각하면 된다. 

생성자가 객체에서 static이 아닌 멤버들을 초기화하는 일을 한다면

소멸자는 객체에서 사용한 자원을 해제하고 static이 아닌 멤버들을 없앤다.

 

생성자가 클래스이름을 따왔듯이

소멸자는 클래스이름을 따서 ~(물결)을 붙여서 표현한다.

소멸자는 반환 값이 당연히 없고 매개변수 또한 없다.

매개변수를 필요로 하지 않으므로 당연히 오버로딩은 할 수 없다.

즉, 소멸자는 생성자와 다르게 무조건 클래스에 하나만 존재한다.

 

 

생성자에는 초기화 부분과 함수 본체 있는 것처럼 소멸자에는 함수 본체와 소멸 부분이 있다.

 

생성자에서는 함수 본체를 실행하기 전에 멤버를 초기화하는데

초기화 순서는 멤버가 해당 클래스에 나타나는 순이다.

생성자 : 멤버 초기화(멤버 선언 순) -> 함수 실행

 

소멸자에서는 함수 본체를 먼저 실행하고 멤버를 소멸하는데 초기화 순서의 역순으로 소멸하게 된다.

소멸자 : 함수 실행(소멸자 함수 본체) -> 멤버 소멸(클래스에 있는 멤버 static 제외)

 

소멸자 함수 본체에서는 객체를 마지막으로 사용한 이후 클래스를 만든 사람의 의도대로 짜는 것이 보통이다.

보통 소멸자에서는 바라는 것이 별로 없고 그냥 객체에서 할당한 자원을 해제하는 것이 보통이다.

 

소멸자에서는 생성자처럼 멤버들의 초기값을 받는것과 같은 부분은 없다.

어차피 다 없애야하니까!

 

무엇인가를 없앨 때, 다시 말해서

 

멤버를 소멸시킬 때 일어나는 일은 멤버 타입에 따라 조금 다르다.

- 클래스 타입 멤버는 멤버 자신의 소멸자를 실행시켜서 소멸하고

- 나머지는 소멸자가 없는 내장타입들은 그냥 아무것도 하지 않는다.

 

소멸자가 쓰이는 경우는 바로 생각할 수 있다.

 

1. 변수(소멸자가 없는 내장타입)은 유효 범위를 벗어나면 소멸한다.

2. 멤버가 속한 객체가 소멸할 때 해당 객체의 멤버가 소멸한다.

3. 컨테이너 라이브러리 또는 배열에 관계없이 컨테이너 요소는 해당 컨테이너가 소멸할 때 같이 소멸한다.

4. 동적으로 할당한 객체는 그 객체에 대한 포인터에 delete 연산을 적용시켜야 소멸한다. ****(중요)

5. 임시 객체는 그 임시 객체를 생성한 표현식 전체를 마치면 소멸한다.

 

소멸자는 보통 자동으로 자원 할당, 해제가 일어나므로 걱정하지 않아도 된다.

다만 동적할당을 했을 경우는 직접 해제하는 것이 원칙이다.

 

소멸자가 무조건 있어야하듯이

사용자가 직접 정의하지 않는다면 또 컴파일러가 만들어준다.

다만 멤버 소멸 외에는 다른 일을 하지 못한다.

반응형
그리드형