본문 바로가기
기타

[C++] Cat *cat = new Cat; 를 이해해보자

by Saerong2 2020. 10. 8.

C++을 통해 과제를 작성하는데 객체를 생성할 때 습관처럼 작성하는 한 줄이 있었습니다.

C++을 사용해서 PS 뿐만 아니라 객체지향 패러다임의 프로그래밍을 해보셨다면 누구나 작성했을 코드인 이 글의 제목에 해당하는 코드입니다. 간단한 코드이지만 저 한 줄을 제대로 이해하고 있는가에 대한 의문을 시작으로 관련된 내용을 간단히 정리를 해보려 합니다.

 

C와 C++

두 언어는 많은 부분이 비슷하지만 다른 언어입니다. C++는 기존의 C의 문법을 따르며 객체지향 패러다임을 지원하는 언어로 등장했고 C++의 컴파일러로 C언어로 작성한 코드를 컴파일 할 수 있어 초기 C언어에 익숙한 사용자들의 유입을 끌어낼 수 있었습니다. 그렇다면 C언어와 비교해서 가장 큰 차이점은 객체지향 프로그래밍 패러다임을 따르는 것인데 객체는 무엇일까요?

 

객체

객체는 자신만의 속성이나 행동을 가지며 물리적, 추상적으로 존재하는 것을 말합니다.

"강아지 꼬리를 흔들며 짖는다" 라는 문장을 살펴볼까요? 강아지는 꼬리를 가지고 있고 (속성) 이것을 흔들 수 있고(행동) 또 짖을 수 있습니다(행동). 따라서 강아지를 하나의 객체라고 볼 수 있는 것이죠.

 

객체지향 프로그래밍(OOP)

객체지향 프로그래밍 패러다임은 말 그대로 객체의 개념을 이용해서 프로그래밍을 하겠다는 것입니다. 객체를 정의하고 사용함으로써 절차지향 언어에 비해서 코드의 재사용이 쉽고 구조적이라 디버깅을 하기도 수월합니다. 대신 객체를 설계를 하는데 있어서 많은 고민이 필요합니다.

C언어와 같은 절차지향 언어도 함수를 통해 코드를 재사용 할 수 있지만 객체지향 언어를 통해 단위를 더 잘게 나누면 코드를 기능별로 그룹화하여 중복을 줄일 수 있고 따라서 유지보수에서도 더욱 편리하고 이는 생산성과도 직결됩니다.

 

 

 

여기까지가 C++언어에 대한 아주 간략한 배경이고 이제 본론으로 들어가보도록 하겠습니다.

A* a = new A; 에서는 생성자, new 키워드, 포인터 정도의 키워드를 뽑아낼 수 있습니다. 그래서 이것들에 대해서 정리를 해보고 추가적으로 소멸자에 대해서도 알아보도록 하겠습니다.


 

생성자

C++에서 클래스(Class)를 정의하였다는 것은 해당 클래스가 어떠한 속성을 갖고 어떠한 행동을 하는지에 대한 틀을 마련해 놓은 것입니다. 빵을 만들기 위해 빵틀을 사 놓고 빵을 안 만들 수는 없겠죠??? 여기서 빵틀에 해당하는 것이 클래스이고 빵에 해당하는 것이 클래스의 객체(Instance)입니다.

 

고양이 클래스로 예시를 들어보겠습니다.

고양이의 속성은 무궁무진 하지만 간단히 type, height, age 정도만 정의를 해볼게요.

클래스를 정의했으면 고양이 객체를 생성해야하는데 고양이의 종도 모르고 키도 모르고 나이도 모른다면 고양이를 만들 수가 없겠죠? 그래서 객체를 생성할 때는 고양이 클래스의 속성값들을 초기화 해주어야 합니다. 그래야 구체적인 하나의 객체로서 행동할 수 있겠죠.

이러한 초기화 작업을 위해서 C++에서는 객체가 생성될 때 초기화 하는 작업을 자동으로 할 수 있도록 생성자(Constructor)라는 함수를 자동으로 호출해줍니다. 클래스의 생성자(함수)이름은 클래스의 이름과 동일하며 일반적인 함수와는 다르게 반환(return) 하는 값이 없습니다.

따라서 생성자는 위와 같이 작성할 수 있으며 이렇게 메인 함수에서 Cat 객체를 선언만 했을뿐인데 Cat 클래스의 생성자가 호출된 것을 확인 할 수 있습니다. 따라서 type, height, age 속성의 초기화는 Cat( ) 생성자 안에서 작성해주면 자동으로 객체가 생성될 때 클래스의 속성값은 생성자에 작성한대로 초기화 됩니다.

 

생성자 오버로딩

지금까지 예제에서 Cat 객체를 선언할 때 는 괄호가 없었는데 이는 사실 기본 생성자를 호출한 것과 같습니다. 

다른 함수와 마찬가지로 생성자 역시 오버로딩이 가능하기 때문에 생성자의 인자는 다양할 수 있으며 인자가 아무것도 없는 생성자를 기본 생성자라고 합니다. 다시말해, 이름은 Cat으로 같고, 인자가 포함된 생성자를 정의할 수 있습니다.

예를 들어서 위의 예제에서는 무조건 초기에 고양이 객체를 생성할 때, 이름이 레옹이고 키가 70, 나이가 3인 고양이로 초기화 됩니다. 이를 생성자 오버로딩을 이용해서 생성자의 인자로 초기화할 객체의 속성을 넘겨줄 수 있습니다.

생성자를 이용해서 객체를 초기화 하는 것은 객체를 생성할 당시에 초기화를 하는 것이 가장 안정적이며 그 객체를 사용하는 사용자는 내부적으로 객체가 어떻게 구현되어 있는지 알 필요가 없다는 장점이 있습니다.

 

생성자도 Virtual로 만들 수 있나요?

상속에 대한 개념도 필요한 조금 더 심화적인 내용이지만 생성자를 가상함수로 만들 수 있을까요?

결론부터 이야기하면 생성자는 가상함수로 만들 수 없습니다.

C++ 언어의 창시자인 Bjarne Stroustrup의 Technique FAQ에 포함된 내용입니다.

 https://www.stroustrup.com/bs_faq2.html#virtual-ctor

 

Stroustrup: C++ Style and Technique FAQ

Morgan Stanley | Columbia University | Churchill College, Cambridge home | C++ | FAQ | technical FAQ | publications | WG21 papers | TC++PL | Tour++ | Programming | D&E | bio | interviews | videos | applications | guidelines | compilers Modified November 22

www.stroustrup.com

C++에서 Virtual은 다형성을 제공하는 것으로, 부모 클래스에서 자식 클래스에 인터페이스를 제공하여 자식 클래스에서 이를 오버라이딩 하여 새롭게 정의하여 사용할 수 있습니다. 클래스의 생성자는 객체를 생성할 때 호출되는 함수로, 하나의 객체를 생성하기 위해서는 완전한 정보가 필요하기 떄문에 논리적으로도 가상 생성자는 의미가 없습니다. 만약 가상 생성자라는 것이 호출된다면 부모의 생성자는 호출되지 않으며 자식 클래스의 인스턴스가 온전히 생성되지 않겠죠.

또한, C++에서는 Virtual 키워드가 포함된 클래스의 객체가 생성되면 VTABLE을 만들고 VPTR이라는 포인터로 그 테이블의 주소를 가리켜 가상함수가 실행될 때 어떤 함수를 실행할지 참조합니다. 가상 생성자라면 아직 객체가 생성되지 않아서 VTABLE이 만들어지지 않았는데 어떤 함수를 실행시켜야 하는지 VTABLE을 참조해야하니까 넌센스하게 되 버리죠.

new 키워드

C언어에서는 malloc으로 동적 메모리(heap)를 할당했습니다. C++에서는 new라는 키워드가 추가 됐는데 이 둘의 차이는 무엇일까요?

위와 같은 코드로 cat1과 cat2는 해당 객체의 크기만큼 메모리를 동적으로 할당받게 됩니다. 하지만 이 둘에는 큰 차이가 있는데요, 객체는 선언되어 생성될 때 생성자가 호출되며 객체의 초기화를 한다고 했습니다. malloc을 사용한 cat1은 메모리의 할당은 되지만 Cat 클래스의 생성자가 호출되지 않습니다. 반면에 new 키워드를 사용한 Cat2는 메모리의 할당뿐만 아니라 생성자가 호출되며 생성된 객체의 초기화가 이루어집니다. 따라서 메모리는 할당하지만 인스턴스화는 되지않은 malloc과는 다르게 new 키워드는 객체를 동적으로 인스턴스화 한다고 표현할 수 있습니다.

 

포인터

C/C++ 에서는 포인터를 통해 메모리 주소를 가리킬 수 있습니다.

여기서 포인터 b가 선언, 초기화 된 줄을 풀어서 설명하면 어떻게 말 할 수 있을까요?

"b는 a의 값이 들어 있는 메모리의 주소를 가지고 있기 때문에, b라는 변수를 통해서 a의 메모리 주소를 참조할 수 있습니다. 그리고 그곳에는 int형 데이터가 있습니다." 라고 할 수 있습니다.  포인터는 b의 타입은 결국 b가 가리키는 a의 시작주소에서 몇 바이트를 읽어야 하는지를 알려주는 셈이죠.

 

Cat *cat = new Cat()  이제는 이해 할 수 있다!

 

지금까지 설명한 것을 바탕으로 위의 한 줄을 다시 바라봅시다. 이제는 처음보다 잘 이해할 수 있을까요?

"new 키워드를 통해 Cat클래스의 객체 크기만큼 동적으로 메모리를 할당하고 기본 생성자가 호출되어 객체를 인스턴스화합니다. 그리고 cat이라는 포인터 변수가 할당받은 주소를 가리키는데 그 곳에는 Cat형 데이터가 있습니다." 라고 할 수 있겠습니다.

 

 

 

* (번외) 가상 생성자가 안되면 가상 소멸자는..?

위에서 가상 생성자는 만들 수 없다는 것을 다루었는데 소멸자도 가상 소멸자는 만들 수 없을까요?

소멸자의 경우는 가상 생성자를 만들 수 있습니다. 필요한 경우가 있기 때문인데요, 아래 예제에서 확인해보겠습니다.

위의 결과는 어떻게 나올까요? Cat이 Animal을 상속받기 때문에 생성자는 부모클래스가 먼저 호출되고 소멸자는 자식클래스가 먼저 호출되며 Animal( ) / Cat ( ) / ~Cat( ) / ~Animal ( ) 이지 않을까? 생각할 수 있습니다.

하지만 결과는 Animal ( ) / Cat( ) / ~Animal ( ) 으로 나옵니다. 자식 클래스의 소멸자가 호출 되었다면 부모 클래스의 소멸자까지 호출 될텐데 이는 자식 클래스인 Cat의 소멸자가 호출되지 않고 부모 클래스인 Animal의 소멸자만 호출된 결과이죠.

 

 위의 예시처럼 다형성을 이용해서 부모 클래스(Animal)의 포인터로 자식 클래스의 객체를 가리킬 때, 가상 함수로 정의되지 않은 자식 클래스의 오버라이딩 된 함수를 호출하면, 부모 클래스의 멤버 함수가 호출됩니다. 소멸자도 함수이므로 오버라이딩 된 함수라고 볼  수 있기 때문에 Animal 포인터인 cat으로 객체를 삭제하면 부모 클래스의 소멸자가 호출되어 자식 클래스의 소멸자는 결코 호출되지 않는 것입니다.

 

위와 같이 소멸자를 가상함수로 선언하여야 자식 클래스의 소멸자를 호출 하여 자식 클래스와 부모 클래스의 소멸자를 모두 호출 할 수 있습니다. 즉, 상속 관계의 두 클래스에서 리소스를 소멸자에서 해제하는 경우 반드시 가상 소멸자를 사용해야 합니다.

 

댓글